Compare commits

...

289 Commits

Author SHA1 Message Date
Aiden Cline
ac3d0cb5a3 review cleanup 2026-01-18 00:24:11 -06:00
Aiden Cline
06d69ab609 cleanup 2026-01-18 00:17:13 -06:00
Aiden Cline
c2cc486c7d exclude write tool too 2026-01-17 23:46:23 -06:00
Aiden Cline
8a6b8e5339 tweak wording to say Patched for ui rendered tool parts 2026-01-17 23:25:07 -06:00
Aiden Cline
cfd6a7ae96 add apply patch to desktop app 2026-01-17 22:47:35 -06:00
Aiden Cline
4173ee0e0b add lsp diagnostics to apply patch 2026-01-17 22:47:26 -06:00
Aiden Cline
22b5d7e570 rm assertion for deletes 2026-01-17 22:19:02 -06:00
Aiden Cline
f1ec28176f wip - ui 2026-01-17 22:03:47 -06:00
Aiden Cline
ab78a46396 wip 2026-01-17 21:15:27 -06:00
Aiden Cline
2ed18ea1fe wip 2026-01-17 20:48:09 -06:00
Aiden Cline
40eddce435 wip 2026-01-17 15:13:57 -06:00
Aiden Cline
78f8cc9418 wip 2026-01-17 14:25:29 -06:00
Aiden Cline
58f7da6e9f docs: document the plural forms 2026-01-17 13:09:30 -06:00
Rahul Mishra
5a199b04cb fix: don't try to open command palette if a dialog is already open (#9116) 2026-01-17 13:08:11 -06:00
Bernat Pericàs
eb968a6651 docs(config): explain that autoupdate doesn't work when installed with a package manager (#9092) 2026-01-17 13:07:03 -06:00
Colby Gilbert
a813fcb41c docs: add firmware provider to providers docs (#8993) 2026-01-17 13:04:43 -06:00
GitHub Action
a58d1be822 ignore: update download stats 2026-01-17 2026-01-17 12:04:18 +00:00
Slone
07dc8d8ce4 fix: escape CSS selector keys to handle special characters (#9030) 2026-01-17 05:48:38 -06:00
GitHub Action
d377246491 chore: generate 2026-01-17 11:47:55 +00:00
Javier Aceña
7030f49a74 fix: mdns discover hostname (#9039) 2026-01-17 05:47:19 -06:00
Eric Guo
c4e4f2a058 fix(desktop): Added a Windows-only guard that makes window.getComputedStyle fall back to document.documentElement (#9054) 2026-01-17 05:45:31 -06:00
Adam
2729705594 fix(app): archive session sometimes flaky 2026-01-17 05:23:17 -06:00
Aiden Cline
ea13b6e8aa test: add azure test case 2026-01-17 00:35:49 -06:00
GitHub Action
85ab9798c6 chore: generate 2026-01-17 04:18:35 +00:00
Aiden Cline
33290c54cd Revert "feat(mcp): add OAuth redirect URI configuration for MCP servers (#7379)"
This reverts commit 40b275d7e6.
2026-01-16 22:17:33 -06:00
GitHub Action
5d613a038d chore: generate 2026-01-17 04:16:35 +00:00
ben
db78a59f03 docs: Add OpenWork to ecosystem (#8741) 2026-01-16 22:15:59 -06:00
Aiden Cline
7c3eeeb0fa fix: gpt id stuff fr fr this time :/ (#9006) 2026-01-16 22:09:36 -06:00
Github Action
e8357a87b0 Update node_modules hashes 2026-01-17 02:51:01 +00:00
Jérôme Benoit
06c543e938 fix(nix): resolve hash race condition in parallel matrix jobs (#8995) 2026-01-16 20:26:08 -06:00
David Hill
759ce8fb8e fix: prevent text clipping on search button descenders 2026-01-17 01:06:53 +00:00
David Hill
38847e13bb fix: truncate long search queries in empty state 2026-01-17 00:55:13 +00:00
David Hill
e0c6459faa fix: remove smooth scroll behavior from list component 2026-01-17 00:55:13 +00:00
David Hill
80b278ddab fix: remove the secondary text from commands 2026-01-17 00:55:13 +00:00
David Hill
ef7ef6538e fix: limit search modal max-height to 480px 2026-01-17 00:55:13 +00:00
David Hill
d23c21023a fix: refine search modal styling and list component 2026-01-17 00:55:13 +00:00
David Hill
dfa2a9f225 fix: reduce command item left padding in search modal 2026-01-17 00:55:13 +00:00
David Hill
6f78a71fa7 feat: add hideIcon and class options to List search, customize search modal input 2026-01-17 00:55:13 +00:00
David Hill
f8f1f46a4f fix: adjust command item left padding in search modal 2026-01-17 00:55:13 +00:00
David Hill
ab705dacfa fix: add left padding to command items in search modal 2026-01-17 00:55:13 +00:00
David Hill
d1b93616f7 fix: increase keybind border-radius in search modal 2026-01-17 00:55:13 +00:00
David Hill
69215d456c fix: display arrow keys as symbols in keybind formatting 2026-01-17 00:55:13 +00:00
David Hill
54e52896a4 refactor: use Keybind component in search modal list 2026-01-17 00:55:13 +00:00
David Hill
b18fb16e9c refactor: use Keybind component in titlebar search button 2026-01-17 00:55:13 +00:00
David Hill
1250486ddf feat: add Keybind component for displaying keyboard shortcuts 2026-01-17 00:55:13 +00:00
David Hill
d645e8bbe1 fix: (desktop) command palette width 2026-01-17 00:55:13 +00:00
David Hill
cad415872e fix: recent sessions gutter 2026-01-17 00:55:13 +00:00
Frank
e8746ddb1d zen: fix opus unicode characters
closes #8967
2026-01-16 18:52:08 -05:00
GitHub Action
80020ade2e chore: generate 2026-01-16 23:23:57 +00:00
Amir Hasanbasic
08ef97b162 fix(opencode): add oauth polling safety margin in copilot device authentication (#8986) 2026-01-16 17:23:18 -06:00
Github Action
1aedb265dd Update node_modules hash (aarch64-darwin) 2026-01-16 23:16:53 +00:00
Github Action
5c13b209aa Update node_modules hash (x86_64-darwin) 2026-01-16 23:11:48 +00:00
Github Action
43a9c50389 Update node_modules hash (x86_64-linux) 2026-01-16 23:02:26 +00:00
Github Action
55224d64a2 Update flake.lock 2026-01-16 23:01:30 +00:00
Daniel Polito
c325aa1142 fix(desktop): Stream bash output + strip-asni (#8961) 2026-01-16 17:00:56 -06:00
Caleb Norton
6e020ef9ef chore: cleanup nix (#8964) 2026-01-16 16:59:34 -06:00
Caleb Norton
aca1eb6b5b fix(nix): add desktop application entry (#8972) 2026-01-16 16:59:07 -06:00
b3nw
3d095e7fe7 fix: centralize OSC 52 clipboard support for SSH sessions (#8974) 2026-01-16 16:57:17 -06:00
GitHub Action
632f20558a chore: generate 2026-01-16 22:49:19 +00:00
Frank
f96c4badd8 wip: black 2026-01-16 17:48:26 -05:00
Frank
cbe1c81470 wip: black 2026-01-16 17:46:36 -05:00
Akshar Patel
c25155586c fix: open help dialog with tui/open-help route (#8596) 2026-01-16 16:42:27 -06:00
Seth Carlton
08b94a6890 fix: keep primary model after subagent runs (#8951) 2026-01-16 16:19:17 -06:00
GitHub Action
8cddc9ea55 chore: generate 2026-01-16 22:14:23 +00:00
Aiden Cline
578239e0d0 chore: cleanup transform code a tad 2026-01-16 16:13:38 -06:00
Ariane Emory
626fa1462b fix: make home/end keys work in menu list modal windows (resolves #7190) (#8347) 2026-01-16 21:57:59 +00:00
opencode
968239bb76 release: v1.1.25 2026-01-16 21:57:58 +00:00
Aiden Cline
8c24879246 test: fix 2026-01-16 15:52:51 -06:00
Aiden Cline
9127055ae7 tweak: wording 2026-01-16 15:50:24 -06:00
Aiden Cline
f5a6a4af7f Revert "fix: ensure that tool attachments arent sent as user messages (#8944)"
This reverts commit 8fd1b92e6e.
2026-01-16 15:50:24 -06:00
Adam
6e00348bd7 fix(app): remember last opened project 2026-01-16 15:49:35 -06:00
Adam
95f7403daf fix(app): truncate workspace title 2026-01-16 15:49:35 -06:00
Aiden Cline
14d1e20287 Revert "fix(app): support anthropic models on azure cognitive services" (#8966) 2026-01-16 15:25:23 -06:00
Unies Ananda Raja
b8e2895dfc fix(app): support anthropic models on azure cognitive services (#8335) 2026-01-16 15:24:06 -06:00
GitHub Action
6e028ec2dc chore: generate 2026-01-16 21:21:54 +00:00
Aiden Cline
8e0ddd1ac9 chore: cleanup server routes (#8965)
Co-authored-by: Leka74 <leke.dobruna@gmail.com>
Co-authored-by: Leka74 <791494+Leka74@users.noreply.github.com>
2026-01-16 15:21:13 -06:00
Adam
da78b758d4 fix(app): handle new session correctly 2026-01-16 14:49:04 -06:00
Adam
360765c591 fix(app): center dialog on page instead of session 2026-01-16 14:33:32 -06:00
GitHub Action
db0078bf17 chore: generate 2026-01-16 20:32:44 +00:00
kenryu42
98578d3a7b fix(bun): reinstall plugins when cache module missing (#8815) 2026-01-16 14:32:05 -06:00
opencode
bc3616d9c6 release: v1.1.24 2026-01-16 20:15:19 +00:00
Adam
71306cbd1f Revert "feat(desktop): Terminal Splits (#8767)"
This reverts commit 88fd6a294b.
2026-01-16 14:03:13 -06:00
Adam
0866034946 feat(app): edit project and session titles 2026-01-16 13:55:59 -06:00
Adam
2ccaa10e79 fix(app): open workspace if navigating to session in workspace 2026-01-16 13:24:47 -06:00
Adam
e92d5b592c fix(app): can't expand workspaces 2026-01-16 13:24:47 -06:00
Adam
00ec29dae6 fix(app): scroll jumping when expanding workspaces 2026-01-16 13:24:47 -06:00
Github Action
438916de5f Update node_modules hash (x86_64-darwin) 2026-01-16 18:58:41 +00:00
Github Action
8d4a67324e Update node_modules hash (aarch64-linux) 2026-01-16 18:53:19 +00:00
Sebastian Herrlinger
0d683eaa8e upgrade opentui to v0.1.74, fix tmux kitty keyboard regression 2026-01-16 19:52:05 +01:00
Aiden Cline
8fd1b92e6e fix: ensure that tool attachments arent sent as user messages (#8944) 2026-01-16 12:47:43 -06:00
Github Action
22e3240296 Update node_modules hash (x86_64-darwin) 2026-01-16 17:43:43 +00:00
Aiden Cline
e1d0b2ba6e fix: use dynamic import for session event in config.ts to avoid circular dep 2026-01-16 11:39:22 -06:00
Bernat Pericàs
ccc27e23df fix(docs): Broken URL (#8918) 2026-01-16 11:37:05 -06:00
Github Action
ad4bdd9f0f Update node_modules hash (x86_64-linux) 2026-01-16 17:36:52 +00:00
Aiden Cline
5479928a9d Reapply "chore(sdk): update @hey-api/openapi-ts to 0.90.4" (#8927)
This reverts commit 91b8ba2186.
2026-01-16 11:34:59 -06:00
Aiden Cline
40836e9683 fix: fix the itemId stripping logic, this time it should fix that id issue w/ gpt models fr 2026-01-16 11:34:45 -06:00
Github Action
9a48f8e9e3 Update node_modules hash (aarch64-darwin) 2026-01-16 17:18:39 +00:00
Aiden Cline
91b8ba2186 Revert "chore(sdk): update @hey-api/openapi-ts to 0.90.4" (#8927) 2026-01-16 11:11:33 -06:00
Cole Leavitt
d075c097ac chore(sdk): update @hey-api/openapi-ts to 0.90.4 (#8921) 2026-01-16 11:11:07 -06:00
Daniel Polito
88fd6a294b feat(desktop): Terminal Splits (#8767) 2026-01-16 10:51:02 -06:00
Frank
ea8ef37d50 wip: zen 2026-01-16 10:40:25 -05:00
Aiden Cline
d510bd52a4 Revert "test: fix test now that image fix went in"
This reverts commit fffa718f5e.
2026-01-16 09:34:00 -06:00
Aiden Cline
e0a854f035 Revert "fix: rm user message when dealing w/ image attachments, use proper tool attachment instead"
This reverts commit de2de099b4.
2026-01-16 09:33:54 -06:00
Aiden Cline
bd914a8c06 Revert "stop select dialog event propagation"
This reverts commit 46be47d0be.
2026-01-16 09:30:06 -06:00
David Hill
a49102db01 fix: truncate the workspace name on hover 2026-01-16 13:46:15 +00:00
David Hill
21012fab4b fix: load more label alignment 2026-01-16 13:38:52 +00:00
David Hill
9a71a73f50 fix: updating panel min size and button max-width 2026-01-16 13:37:14 +00:00
David Hill
2190e8c656 Revert "fix: expand workspaces by default when enabled"
This reverts commit 1fd496a5e2.
2026-01-16 13:18:56 +00:00
David Hill
1fd496a5e2 fix: expand workspaces by default when enabled 2026-01-16 13:17:22 +00:00
David Hill
74d584af34 fix: session icon and label alignment 2026-01-16 13:14:05 +00:00
David Hill
46f415ecb0 fix: desktop hamburger shift 2026-01-16 13:05:50 +00:00
David Hill
d0399045da fix: make hamburger centred with project avatars 2026-01-16 13:00:44 +00:00
David Hill
4be0ba19ca fix: web mobile menu
there is a small shift/misalignment in the side panel on web, not investigating too much as this whole area is in development and may change to make the projects avatar list always in view.
2026-01-16 12:54:54 +00:00
Aaron Iker
d5a5e6e062 feat(console): /black shader improvements, performance, details (#8871) 2026-01-16 13:35:58 +01:00
David Hill
e8dad85233 fix: responsive menu desktop 2026-01-16 12:29:28 +00:00
GitHub Action
f197b8a0cd ignore: update download stats 2026-01-16 2026-01-16 12:05:18 +00:00
GitHub Action
efaf854e09 chore: generate 2026-01-16 11:59:12 +00:00
David Hill
704276753b bug: moved createMemo down 2026-01-16 11:58:31 +00:00
David Hill
2c5437791b fix: updated project/sessions list width
this was originally 280px and included the 64px project avatar rail on the left, so the sessions list portion was actually 280 - 64 = 216px wide (as seen in line 1362: Math.max(layout.sidebar.width() - 64, 0)px).

i kept the logic the same in case it broke anything, i just updated the new width to account for new projects avatar panel
2026-01-16 11:55:15 +00:00
David Hill
94ab87ffad fix: view all sessions state styles 2026-01-16 11:55:15 +00:00
David Hill
416f419a81 fix: add default icon to sessions 2026-01-16 11:55:15 +00:00
David Hill
3ba03a97dc fix: search bar size and padding, and shortcut style 2026-01-16 11:55:15 +00:00
David Hill
b1a22e08f5 fix: avatar radius and current project 2026-01-16 11:55:15 +00:00
Adam
c551a4b6e3 fix(app): persist workspace order and collapsed state 2026-01-16 05:20:43 -06:00
Aiden Cline
524ea95a00 update gpt models prompt 2026-01-16 01:34:01 -06:00
Github Action
0e9664d300 Update node_modules hash (x86_64-darwin) 2026-01-16 06:17:52 +00:00
Aiden Cline
fffa718f5e test: fix test now that image fix went in 2026-01-16 00:11:30 -06:00
Github Action
cce4f64e0b Update node_modules hash (x86_64-linux) 2026-01-16 06:11:25 +00:00
Github Action
ce6e9a822d Update node_modules hash (aarch64-linux) 2026-01-16 06:09:23 +00:00
Frank
f66e6d7033 wip: zen 2026-01-16 01:07:03 -05:00
Aiden Cline
de2de099b4 fix: rm user message when dealing w/ image attachments, use proper tool attachment instead 2026-01-16 00:05:10 -06:00
GitHub Action
0233dd1b39 chore: generate 2026-01-16 05:37:24 +00:00
Christopher Tso
40b275d7e6 feat(mcp): add OAuth redirect URI configuration for MCP servers (#7379) 2026-01-15 23:36:48 -06:00
Hyeonjong
e4a34beb8b chore: update GitHub stars and commits statistics (#8793) 2026-01-15 23:17:32 -06:00
Akshar Patel
ac54535486 feat: add version to session header and /status dialog (#8802) 2026-01-15 22:58:41 -06:00
Aiden Cline
1a43e5fe87 fix: adjust websearch tool to emphasize that it ISNT 2024, give more info as to current date 2026-01-15 20:50:22 -06:00
Sebastian Herrlinger
46be47d0be stop select dialog event propagation 2026-01-16 03:17:32 +01:00
Kit Langton
4af9defb89 fix(tui): correct theme count tip (#8779) 2026-01-15 19:39:35 -06:00
Kit Langton
12b621068a fix(tui): dim question option prefixes (#8776) 2026-01-15 19:38:55 -06:00
Kit Langton
07e7ebdb8e fix(tui): add tab navigation in questions (#8777) 2026-01-15 19:38:11 -06:00
Kit Langton
5092b5f07b docs: clarify question tool guidance (#8778) 2026-01-15 19:37:56 -06:00
Aiden Cline
d8ef9f808d test: fix transform test 2026-01-15 19:27:04 -06:00
Aiden Cline
d7192d6af9 tweak: set opencode as user agent for most interefence requests 2026-01-15 19:25:58 -06:00
GitHub Action
68e6c540bb chore: generate 2026-01-16 01:22:16 +00:00
Dan Lapid
b572c68100 fix(mcp): show auth URL when browser cannot open in remote sessions (#7884) 2026-01-15 20:21:39 -05:00
Aiden Cline
25cb03dbe5 chore: cleanup 2026-01-15 19:19:28 -06:00
Aiden Cline
d47510785a strip itemIds in more cases 2026-01-15 18:00:58 -06:00
Adam
657f3d5089 feat(app): unified search for commands and files 2026-01-15 17:59:26 -06:00
Adam
49939c4d8d feat(app): skeleton loader for sessions 2026-01-15 17:59:25 -06:00
Adam
529eb6e147 fix(app): persist workspace order and collapsed state 2026-01-15 17:59:25 -06:00
Ricardo Valero de la Rosa
a7cae8f674 fix: nix desktop workflow (#8747)
Co-authored-by: Github Action <action@github.com>
2026-01-15 17:34:36 -06:00
Frank
12ae80856e wip: zen 2026-01-15 18:21:19 -05:00
Frank
7e619a9302 zen: black admin 2026-01-15 18:21:19 -05:00
GitHub Action
2abafbcd2f chore: generate 2026-01-15 23:19:16 +00:00
Daniel Polito
8b08d340ac fix: stop changing main model/agent from subtasks invocation (#7681)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-15 17:18:39 -06:00
Sercan Sagman
81983d4a2e fix(agent): default agent selection in acp and headless mode (#8678)
Signed-off-by: assagman <ahmetsercansagman@gmail.com>
2026-01-15 17:09:19 -06:00
Aaron Iker
7443b99295 feat(console): Fix /black page View Transition Safari issue (#8755) 2026-01-15 23:49:40 +01:00
David Hill
306fc05c00 fix: project avatar border radius 2026-01-15 22:11:07 +00:00
Aiden Cline
9d8d0e97ec Revert "fix:subagent reasoningEffort not being applied (#8646)"
This reverts commit f4086ac459.
2026-01-15 16:06:35 -06:00
Jeong Juahn
f4086ac459 fix:subagent reasoningEffort not being applied (#8646)
Co-authored-by: Bot <bot@example.com>
2026-01-15 16:04:01 -06:00
GitHub Action
b9b5d42bd8 chore: generate 2026-01-15 22:03:17 +00:00
outside.observer
83ed1adcbd feat: add Carbonfox theme (#8723) 2026-01-15 16:02:29 -06:00
seilk
9b57db30d1 feat: add litellmProxy provider option for explicit LiteLLM compatibility (#8658)
Co-authored-by: Mark Henderson <Mark.Henderson99@hotmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-15 22:01:15 +00:00
opencode
df8e6e6014 release: v1.1.23 2026-01-15 22:01:14 +00:00
Adam
472a6cc83e fix(app): sidebar toggle on desktop 2026-01-15 15:45:12 -06:00
Adam
47d43aaf2d feat(app): persist workspace branch 2026-01-15 15:45:12 -06:00
Adam
da3dea0429 fix(app): persist workspace order and collapsed state 2026-01-15 15:45:11 -06:00
Ricardo Valero de la Rosa
9862303eed fix: update hix hashes for all systems (#8732)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-15 15:45:03 -06:00
Qunhong Zeng
b14622352e fix(session): ensure agent exists before processing title in session summary (#8662) 2026-01-15 15:24:13 -06:00
Aaron Iker
ea643f1e3f feat(console): Style improvements for /black, View Transition fixes (#8739)
Co-authored-by: Github Action <action@github.com>
2026-01-15 22:21:35 +01:00
Nhan Nguyen
f5fd54598f docs: add /thinking command documentation (#8722) 2026-01-15 15:14:23 -06:00
David Hill
0f7b17b1b4 fix: thinking animation opacity and design 2026-01-15 20:42:14 +00:00
David Hill
4d3e983edb fix: session icon and name alignment 2026-01-15 20:42:14 +00:00
Github Action
d3fc29bdec Update aarch64-darwin hash 2026-01-15 20:39:01 +00:00
Aaron Iker
fe58c649cb feat(console): Update /black plan selection, light rays effect. mobile styles (#8731)
Co-authored-by: Github Action <action@github.com>
2026-01-15 21:31:50 +01:00
Adam
af2a09940c fix(core): more defensive project list 2026-01-15 13:58:39 -06:00
Adam
7e016fdda6 chore: cleanup 2026-01-15 13:34:53 -06:00
Adam
beb97d21ff fix(app): show session busy even for active session 2026-01-15 13:33:49 -06:00
Adam
b0345284f9 fix(core): filter dead worktrees 2026-01-15 13:33:49 -06:00
Adam
d71153eae6 fix(core): loading models.dev in dev 2026-01-15 13:33:48 -06:00
dbpolito
e60ded01df chore(desktop): Stop Killing opencode-cli on dev 2026-01-15 13:17:57 -06:00
dbpolito
4b2a14c154 chore(desktop): Question Tools Updates 2026-01-15 13:17:31 -06:00
David Hill
b4717d8092 bun/package.json updates
this may not be required
2026-01-15 19:15:21 +00:00
David Hill
dc8f8cc567 fix: current session background color 2026-01-15 19:15:21 +00:00
David Hill
99110d12c4 fix: remove the active state from load more button after press 2026-01-15 19:15:21 +00:00
David Hill
74b1349cf6 fix: new session tooltip position and add shortcut 2026-01-15 19:15:21 +00:00
David Hill
3b3505cfe8 fix: remove more options tooltip 2026-01-15 19:15:21 +00:00
David Hill
55bd6e487e fix: workspace name color 2026-01-15 19:15:21 +00:00
David Hill
1ee916a3c3 fix: hide view all sessions on active project 2026-01-15 19:15:21 +00:00
David Hill
a5d47f076e fix: avatar button states 2026-01-15 19:15:21 +00:00
David Hill
acd1eb574d fix: load more button font size 2026-01-15 19:15:21 +00:00
David Hill
a71dcc189e fix: recent sessions title color 2026-01-15 19:15:21 +00:00
David Hill
3789a31423 fix: project dropdown labels and order 2026-01-15 19:15:21 +00:00
David Hill
bb6e350d68 fix: move left panel toggle over
- not sure how this impacts on the titlebar when the traffic lights are there
2026-01-15 19:15:21 +00:00
David Hill
f9a441d4f4 fix: avatar background 2026-01-15 19:15:21 +00:00
David Hill
1c05ebaea2 fix: show project options on hover of row 2026-01-15 19:15:21 +00:00
David Hill
520c47e81d fix: increase delay on session list tooltips 2026-01-15 19:15:21 +00:00
David Hill
e5b08da0f1 fix: tooltip gutter spacing on session items and archive buttons 2026-01-15 19:15:21 +00:00
David Hill
fe2cc0cff1 fix: archive icon replaces diff count on hover 2026-01-15 19:15:21 +00:00
David Hill
fbc8f6eba9 fix: recent sessions hover gutter 2026-01-15 19:15:21 +00:00
David Hill
8cba7d7f53 fix: tooltips cleanup 2026-01-15 19:15:21 +00:00
David Hill
6450ba1b79 fix: search bar in header 2026-01-15 19:15:21 +00:00
Aiden Cline
dc1c25cff5 fix: ensure frontmatter can process same content as other agents (#8719) 2026-01-15 13:06:14 -06:00
Github Action
3f3550a16e Update aarch64-darwin hash 2026-01-15 18:29:11 +00:00
Github Action
161e3db795 Update Nix flake.lock and x86_64-linux hash 2026-01-15 18:17:44 +00:00
Aiden Cline
5a8a0f6a56 fix: downgrade bun to fix avx issue 2026-01-15 12:16:17 -06:00
Maciek Szczesniak
37f30993fa fix: show toast error message on ConfigMarkdown parse error (#8049)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-15 11:53:06 -06:00
opencode-agent[bot]
ebc194ca9a Prettify retry duration display in TUI (#8608)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-01-15 11:50:18 -06:00
andrew-kramer-inno
4edb4fa4fa fix: handle broken symlinks gracefully in grep tool (#8612)
Co-authored-by: Alex Johnson <nvidiattxpsli@gmail.com>
2026-01-15 11:40:37 -06:00
Aryan "LAG" Gupta
63176bb049 docs: fix typos in documentation (#8703) 2026-01-15 11:06:16 -06:00
GitHub Action
216a2d87cf chore: generate 2026-01-15 16:32:09 +00:00
Dax
dd1f981d23 fix: honor per-server MCP timeouts (#8706) 2026-01-15 11:31:31 -05:00
Sebastian Herrlinger
bfc9b24b48 use native text truncation for sidebar diff paths 2026-01-15 17:23:48 +01:00
Github Action
2691e1e666 Update aarch64-darwin hash 2026-01-15 15:47:53 +00:00
Github Action
3f16e0d89f Update Nix flake.lock and x86_64-linux hash 2026-01-15 15:41:26 +00:00
Sebastian Herrlinger
994c55f709 upgrade opentui to v0.1.73, fixing CJK word wrapping and thai text rendering (non-tmux) 2026-01-15 16:39:48 +01:00
Adam
2f32f2ceb5 chore: cleanup 2026-01-15 07:29:13 -06:00
Adam
076dfb3752 chore: cleanup 2026-01-15 07:29:13 -06:00
Github Action
60aa0cb96e Update Nix flake.lock and x86_64-linux hash 2026-01-15 07:29:13 -06:00
Adam
e5973e2860 chore: cleanup 2026-01-15 07:29:13 -06:00
Adam
dbd1987f0a chore: cleanup 2026-01-15 07:29:13 -06:00
Adam
f270ea65c5 fix(app): new layout issues 2026-01-15 07:29:13 -06:00
Adam
1698448016 fix(app): new layout sessions stale 2026-01-15 07:29:13 -06:00
Adam
564d3edfac fix(app): new layout issues 2026-01-15 07:29:13 -06:00
Adam
679270d9e0 feat(app): new layout 2026-01-15 07:29:13 -06:00
adamelmore
9f66a45970 feat(app): new layout 2026-01-15 07:29:13 -06:00
Turcu Laurentiu
779610d668 fix(desktop): open external links in system browser instead of webview (#7360) 2026-01-15 02:12:27 -06:00
Ryan Vogel
1fb611ef0a fix: enable sticky header on changelog and download pages (#8556) 2026-01-15 02:09:23 -06:00
GitHub Action
972f5ecc7d chore: generate 2026-01-15 07:35:52 +00:00
Brandon Smith
8d720f9463 fix(opencode): add input limit for compaction (#8465) 2026-01-15 01:35:16 -06:00
Aiden Cline
92931437c4 fix: codex id issue (#8605) 2026-01-15 01:31:50 -06:00
Ariane Emory
08ca1237cc fix(tui): Center the initially selected session in the session_list (resolves #8558) (#8560) 2026-01-15 01:04:20 -06:00
GitHub Action
6473e15793 chore: generate 2026-01-15 06:45:39 +00:00
Aiden Cline
16cac69a72 Revert "feat: allow provider-level store option (#8000)" (#8613) 2026-01-15 00:45:03 -06:00
GitHub Action
b2da41cfad chore: generate 2026-01-15 06:36:30 +00:00
Call White
fcf2da9571 feat: allow provider-level store option (#8000) 2026-01-15 00:35:53 -06:00
GitHub Action
253b7ea784 chore: generate 2026-01-15 06:04:47 +00:00
Kit Langton
3a9fd1bb36 fix: restore brand integrity of TUI wordmark (#8584) 2026-01-15 00:04:11 -06:00
GitHub Action
f84ac697dc chore: generate 2026-01-15 05:40:29 +00:00
Cas
76a79284d2 feat(tui): make dialog keybinds configurable (#6143) (#6144) 2026-01-14 23:39:52 -06:00
opencode
99a1e73fa1 release: v1.1.21 2026-01-15 02:34:07 +00:00
GitHub Action
ba4c86448b chore: generate 2026-01-15 02:21:43 +00:00
Aiden Cline
b36837ae93 tweak: add error message so people know to reauthenticate with copilot 2026-01-14 20:21:03 -06:00
Frank
e03932e586 zen: black usage 2026-01-14 21:20:26 -05:00
Idris Gadi
6b019a125a docs: fix permission system documentation in agents section (#7652) 2026-01-14 20:17:04 -06:00
Aiden Cline
6a2fed7042 chore: bump cache version 2026-01-14 17:44:16 -06:00
Aiden Cline
74baae597a chore: bump plugin version 2026-01-14 17:43:12 -06:00
Aiden Cline
d78d31430d feat: official copilot plugin (#8393) 2026-01-14 17:42:51 -06:00
Aiden Cline
096e14d787 tweak: adjust lsp wording a bit more to encourage fixing 2026-01-14 15:44:44 -06:00
Frank
bbb3120b59 zen: gpt-5.2-codex 2026-01-14 16:03:04 -05:00
Frank
9e4438f5bf wip: black 2026-01-14 16:03:04 -05:00
opencode-agent[bot]
87438fb38e ci: dedup stuff in changelog (#8522)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-01-14 14:34:57 -06:00
Dax Raad
6b6d6e9e07 update security 2026-01-14 14:52:39 -05:00
Dax Raad
207a59aad4 docs: add comprehensive security threat model and architecture documentation 2026-01-14 14:49:27 -05:00
Aiden Cline
b3ae1931fc fix: plan path permissions 2026-01-14 13:28:56 -06:00
Samiul Islam
4d08123ca0 feat(install): respect ZDOTDIR for zsh config detection (#8511)
Signed-off-by: sami <samiulsami7786@gmail.com>
2026-01-14 12:30:54 -06:00
Aiden Cline
7d3c7a9f65 add check incase provider doesnt exist in models list 2026-01-14 12:16:12 -06:00
Aiden Cline
50dfa9caf3 chore: upgrade bun from 1.3.5 -> 1.3.6, also update types/bun from 1.3.4 -> 1.3.6 and fix type errs (#8499)
Co-authored-by: Github Action <action@github.com>
2026-01-14 11:53:12 -06:00
Aiden Cline
1f86aa8bb9 fix: adjust gitlab logic in provider.ts 2026-01-14 11:42:45 -06:00
GitHub Action
d83756eaaf chore: generate 2026-01-14 17:41:16 +00:00
Aiden Cline
c0b43d3cb4 ignore: add slash command to checks ai sdk deps 2026-01-14 11:40:36 -06:00
Ryan Vogel
3206ed47e0 feat(console): add OG image and SEO meta tags for /black page (#8506) 2026-01-14 11:20:50 -06:00
Ryan Vogel
346c5e0da6 fix(console): make logo on /black link back to homepage (#8498) 2026-01-14 11:49:44 -05:00
Dax Raad
5b431c36f8 ignore: remove nowrap constraint to allow text wrapping in console UI 2026-01-14 11:39:52 -05:00
Dax Raad
44d24d42b8 ignore: fix auth redirect to preserve selected plan during subscription flow 2026-01-14 11:25:50 -05:00
Jacopo Binosi
3a9e6b558c feat(opencode): add AWS Web Identity Token File support for Bedrock (#8461) 2026-01-14 10:20:47 -06:00
Dax Raad
9d92ae7530 copy changes 2026-01-14 11:17:11 -05:00
Ryan Vogel
e6e7eaf6e0 docs: add Web usage page (#8482) 2026-01-14 10:03:48 -06:00
Aaron Iker
8ce5c2b900 feat(console/app): Style changes, view transitions, small improvements (#8481) 2026-01-14 10:02:18 -06:00
Ryan Vogel
78be8fecdc feat(console): add /changelog page (#8476) 2026-01-14 10:01:30 -06:00
Github Action
b5e9f96660 Update aarch64-darwin hash 2026-01-14 15:39:39 +00:00
Mani Sundararajan
ad17e8d1f0 feat: add choco and scoop to opencode upgrade methods (#8439) 2026-01-14 09:39:01 -06:00
Ryan Vogel
b75d4d1c5e docs: update screenshot images (#8479) 2026-01-14 09:36:10 -06:00
Github Action
cc67bc005d Update Nix flake.lock and x86_64-linux hash 2026-01-14 15:35:07 +00:00
Vladimir Glafirov
0ce849c3d5 chore: update gitlab-ai-provider to 3.1.1 and remove unused parameter (#8424) 2026-01-14 09:34:02 -06:00
Cas
6e13e2f74e fix(session): remove typo'd duplicate path import (#8408) (#8412)
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-01-14 09:04:03 -06:00
GitHub Action
9fd61aef6e chore: generate 2026-01-14 15:01:42 +00:00
Ryan Vogel
bb3926bf45 fix(homepage): Update FAQ to include desktop and web links (#8453) 2026-01-14 09:01:02 -06:00
Kit Langton
b2b123a392 feat(tui): improve question prompt UX (#8339) 2026-01-14 10:00:29 -05:00
opencode
09ff3b9bb9 release: v1.1.20 2026-01-14 13:41:08 +00:00
GitHub Action
2256362ba2 chore: generate 2026-01-14 13:36:44 +00:00
Shane Bishop
077ca4454f fix(desktop): "load more" button behavior in desktop sidebar (#8430) 2026-01-14 07:36:08 -06:00
Andrew Jazbec
05cbb11709 fix(ui): layout-bottom icons (#8330) 2026-01-14 07:25:09 -06:00
Dax Raad
fcc561ebb7 fix plan mode when not in git worktree 2026-01-14 08:21:26 -05:00
Filip
ee6ca104e5 fix(app): file listing (#8309) 2026-01-14 07:09:36 -06:00
GitHub Action
4347a77d89 ignore: update download stats 2026-01-14 2026-01-14 12:05:15 +00:00
GitHub Action
76b10d85ee chore: generate 2026-01-14 07:37:11 +00:00
Goni Zahavy
45a770cdb1 fix(opencode): fix docker image after sst rename in tips (#8376) 2026-01-14 01:36:36 -06:00
Akshar Patel
a57c8669b6 feat: show connected providers in /connect dialog (#8351) 2026-01-14 01:35:59 -06:00
255 changed files with 16966 additions and 7759 deletions

View File

@@ -9,6 +9,7 @@ on:
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
pull_request:
paths:
- "flake.nix"
@@ -16,6 +17,7 @@ on:
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
workflow_dispatch:
jobs:
@@ -25,6 +27,8 @@ jobs:
matrix:
os:
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-ubuntu-2404-arm
- macos-15-intel
- macos-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 60
@@ -33,7 +37,7 @@ jobs:
uses: actions/checkout@v6
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v21
uses: nixbuild/nix-quick-install-action@v34
- name: Build desktop via flake
run: |

View File

@@ -10,22 +10,24 @@ on:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- ".github/workflows/update-nix-hashes.yml"
pull_request:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- ".github/workflows/update-nix-hashes.yml"
jobs:
update-linux:
update-flake:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
SYSTEM: x86_64-linux
TITLE: flake.lock
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
@@ -33,39 +35,32 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v20
uses: nixbuild/nix-quick-install-action@v34
- name: Configure git
run: |
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Update flake.lock
- name: Update ${{ env.TITLE }}
run: |
set -euo pipefail
echo "📦 Updating flake.lock..."
echo "Updating $TITLE..."
nix flake update
echo "✅ flake.lock updated successfully"
echo "$TITLE updated successfully"
- name: Update node_modules hash for x86_64-linux
run: |
set -euo pipefail
echo "🔄 Updating node_modules hash for x86_64-linux..."
nix/scripts/update-hashes.sh
echo "✅ node_modules hash for x86_64-linux updated successfully"
- name: Commit Linux hash changes
- name: Commit ${{ env.TITLE }} changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
set -euo pipefail
echo "🔍 Checking for changes in tracked Nix files..."
echo "Checking for changes in tracked files..."
summarize() {
local status="$1"
{
echo "### Nix Hash Update (x86_64-linux)"
echo "### Nix $TITLE"
echo ""
echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}"
@@ -75,42 +70,53 @@ jobs:
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
FILES=(flake.lock flake.nix)
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
echo "No changes detected. Hashes are already up to date."
echo "No changes detected."
summarize "no changes"
exit 0
fi
echo "📝 Changes detected:"
echo "Changes detected:"
echo "$STATUS"
echo "🔗 Staging files..."
echo "Staging files..."
git add "${FILES[@]}"
echo "💾 Committing changes..."
git commit -m "Update Nix flake.lock and x86_64-linux hash"
echo "Changes committed"
echo "Committing changes..."
git commit -m "Update $TITLE"
echo "Changes committed"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
echo "🌳 Pulling latest from branch: $BRANCH"
git pull --rebase origin "$BRANCH"
echo "🚀 Pushing changes to branch: $BRANCH"
echo "Pulling latest from branch: $BRANCH"
git pull --rebase --autostash origin "$BRANCH"
echo "Pushing changes to branch: $BRANCH"
git push origin HEAD:"$BRANCH"
echo "Changes pushed successfully"
echo "Changes pushed successfully"
summarize "committed $(git rev-parse --short HEAD)"
update-macos:
needs: update-linux
compute-node-modules-hash:
needs: update-flake
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- system: x86_64-linux
host: blacksmith-4vcpu-ubuntu-2404
- system: aarch64-linux
host: blacksmith-4vcpu-ubuntu-2404-arm
- system: x86_64-darwin
host: macos-15-intel
- system: aarch64-darwin
host: macos-latest
runs-on: ${{ matrix.host }}
env:
SYSTEM: aarch64-darwin
SYSTEM: ${{ matrix.system }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
@@ -118,7 +124,105 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v20
uses: nixbuild/nix-quick-install-action@v34
- name: Compute node_modules hash
run: |
set -euo pipefail
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
HASH_FILE="nix/hashes.json"
OUTPUT_FILE="hash-${SYSTEM}.txt"
export NIX_KEEP_OUTPUTS=1
export NIX_KEEP_DERIVATIONS=1
BUILD_LOG=$(mktemp)
TMP_JSON=$(mktemp)
trap 'rm -f "$BUILD_LOG" "$TMP_JSON"' EXIT
if [ ! -f "$HASH_FILE" ]; then
mkdir -p "$(dirname "$HASH_FILE")"
echo '{"nodeModules":{}}' > "$HASH_FILE"
fi
# Set dummy hash to force nix to rebuild and reveal correct hash
jq --arg system "$SYSTEM" --arg value "$DUMMY" \
'.nodeModules = (.nodeModules // {}) | .nodeModules[$system] = $value' "$HASH_FILE" > "$TMP_JSON"
mv "$TMP_JSON" "$HASH_FILE"
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
echo "Building node_modules for ${SYSTEM} to discover correct hash..."
echo "Attempting to realize derivation: ${DRV_PATH}"
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
CORRECT_HASH=""
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
echo "Realized node_modules output: $BUILD_PATH"
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
fi
# Try to extract hash from build log
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
fi
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
fi
# Try to hash from kept failed build directory
if [ -z "$CORRECT_HASH" ]; then
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1 || true)
if [ -z "$KEPT_DIR" ]; then
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1 || true)
fi
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
HASH_PATH="$KEPT_DIR"
[ -d "$KEPT_DIR/build" ] && HASH_PATH="$KEPT_DIR/build"
if [ -d "$HASH_PATH/node_modules" ]; then
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
fi
fi
fi
if [ -z "$CORRECT_HASH" ]; then
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
cat "$BUILD_LOG"
exit 1
fi
echo "$CORRECT_HASH" > "$OUTPUT_FILE"
echo "Hash for ${SYSTEM}: $CORRECT_HASH"
- name: Upload hash artifact
uses: actions/upload-artifact@v6
with:
name: hash-${{ matrix.system }}
path: hash-${{ matrix.system }}.txt
retention-days: 1
commit-node-modules-hashes:
needs: compute-node-modules-hash
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
TITLE: node_modules hashes
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Configure git
run: |
@@ -130,27 +234,71 @@ jobs:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git pull origin "$BRANCH"
git pull --rebase --autostash origin "$BRANCH"
- name: Update node_modules hash for aarch64-darwin
- name: Download all hash artifacts
uses: actions/download-artifact@v7
with:
pattern: hash-*
merge-multiple: true
- name: Merge hashes into hashes.json
run: |
set -euo pipefail
echo "🔄 Updating node_modules hash for aarch64-darwin..."
nix/scripts/update-hashes.sh
echo "✅ node_modules hash for aarch64-darwin updated successfully"
- name: Commit macOS hash changes
HASH_FILE="nix/hashes.json"
if [ ! -f "$HASH_FILE" ]; then
mkdir -p "$(dirname "$HASH_FILE")"
echo '{"nodeModules":{}}' > "$HASH_FILE"
fi
echo "Merging hashes into ${HASH_FILE}..."
shopt -s nullglob
files=(hash-*.txt)
if [ ${#files[@]} -eq 0 ]; then
echo "No hash files found, nothing to update"
exit 0
fi
EXPECTED_SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin"
for sys in $EXPECTED_SYSTEMS; do
if [ ! -f "hash-${sys}.txt" ]; then
echo "WARNING: Missing hash file for $sys"
fi
done
for f in "${files[@]}"; do
system="${f#hash-}"
system="${system%.txt}"
hash=$(cat "$f")
if [ -z "$hash" ]; then
echo "WARNING: Empty hash for $system, skipping"
continue
fi
echo " $system: $hash"
jq --arg sys "$system" --arg h "$hash" \
'.nodeModules = (.nodeModules // {}) | .nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp"
mv "${HASH_FILE}.tmp" "$HASH_FILE"
done
echo "All hashes merged:"
cat "$HASH_FILE"
- name: Commit ${{ env.TITLE }} changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
set -euo pipefail
echo "🔍 Checking for changes in tracked Nix files..."
HASH_FILE="nix/hashes.json"
echo "Checking for changes..."
summarize() {
local status="$1"
{
echo "### Nix Hash Update (aarch64-darwin)"
echo "### Nix $TITLE"
echo ""
echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}"
@@ -161,27 +309,22 @@ jobs:
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(nix/hashes.json)
FILES=("$HASH_FILE")
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
echo "No changes detected. Hash is already up to date."
echo "No changes detected."
summarize "no changes"
exit 0
fi
echo "📝 Changes detected:"
echo "Changes detected:"
echo "$STATUS"
echo "🔗 Staging files..."
git add "${FILES[@]}"
echo "💾 Committing changes..."
git commit -m "Update aarch64-darwin hash"
echo "✅ Changes committed"
git commit -m "Update $TITLE"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
echo "🌳 Pulling latest from branch: $BRANCH"
git pull --rebase origin "$BRANCH"
echo "🚀 Pushing changes to branch: $BRANCH"
git pull --rebase --autostash origin "$BRANCH"
git push origin HEAD:"$BRANCH"
echo "Changes pushed successfully"
echo "Changes pushed successfully"
summarize "committed $(git rev-parse --short HEAD)"

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ opencode.json
a.out
target
.scripts
.direnv/
# Local dev files
opencode-dev

View File

@@ -0,0 +1,24 @@
---
description: "Bump AI sdk dependencies minor / patch versions only"
---
Please read @package.json and @packages/opencode/package.json.
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
I want a report of every dependency and the version that can be upgraded to.
What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
Consider using subagents for each dep to save your context window.
Here is a short list of some deps (please be comprehensive tho):
- "ai"
- "@ai-sdk/openai"
- "@ai-sdk/anthropic"
- "@openrouter/ai-sdk-provider"
- etc, etc
DO NOT upgrade the dependencies yet, just make a list of all dependencies and their versions that can be upgraded to minor or patch versions only.
Write up your findings to ai-sdk-updates.md

View File

@@ -1,3 +1,32 @@
# Security
## Threat Model
### Overview
OpenCode is an AI-powered coding assistant that runs locally on your machine. It provides an agent system with access to powerful tools including shell execution, file operations, and web access.
### No Sandbox
OpenCode does **not** sandbox the agent. The permission system exists as a UX feature to help users stay aware of what actions the agent is taking - it prompts for confirmation before executing commands, writing files, etc. However, it is not designed to provide security isolation.
If you need true isolation, run OpenCode inside a Docker container or VM.
### Server Mode
Server mode is opt-in only. When enabled, set `OPENCODE_SERVER_PASSWORD` to require HTTP Basic Auth. Without this, the server runs unauthenticated (with a warning). It is the end user's responsibility to secure the server - any functionality it provides is not a vulnerability.
### Out of Scope
| Category | Rationale |
| ------------------------------- | ----------------------------------------------------------------------- |
| **Server access when opted-in** | If you enable server mode, API access is expected behavior |
| **Sandbox escapes** | The permission system is not a sandbox (see above) |
| **LLM provider data handling** | Data sent to your configured LLM provider is governed by their policies |
| **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
---
# Reporting Security Issues
We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.

403
STATS.md
View File

@@ -1,202 +1,205 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | -------------------- | ------------------- | -------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | -------------------- | -------------------- | -------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |

102
bun.lock
View File

@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -81,6 +81,8 @@
"@opencode-ai/console-mail": "workspace:*",
"@opencode-ai/console-resource": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@smithy/eventstream-codec": "4.2.7",
"@smithy/util-utf8": "4.2.0",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
@@ -95,13 +97,14 @@
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"@webgpu/types": "0.1.54",
"typescript": "catalog:",
"wrangler": "4.50.0",
},
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -128,7 +131,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -152,7 +155,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -176,7 +179,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -205,7 +208,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -234,7 +237,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -250,7 +253,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.19",
"version": "1.1.25",
"bin": {
"opencode": "./bin/opencode",
},
@@ -278,7 +281,7 @@
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.1.0",
"@gitlab/gitlab-ai-provider": "3.1.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
@@ -290,8 +293,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.72",
"@opentui/solid": "0.1.72",
"@opentui/core": "0.1.74",
"@opentui/solid": "0.1.74",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -354,7 +357,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -374,9 +377,9 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.19",
"version": "1.1.25",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@hey-api/openapi-ts": "0.90.4",
"@tsconfig/node22": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -385,7 +388,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -398,7 +401,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -410,7 +413,7 @@
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"dompurify": "catalog:",
"dompurify": "3.3.1",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
@@ -421,6 +424,7 @@
"shiki": "catalog:",
"solid-js": "catalog:",
"solid-list": "catalog:",
"strip-ansi": "7.1.2",
"virtua": "catalog:",
},
"devDependencies": {
@@ -438,7 +442,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"zod": "catalog:",
},
@@ -449,7 +453,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -505,7 +509,7 @@
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.4",
"@types/bun": "1.3.5",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -913,17 +917,17 @@
"@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-S0MVXsogrwbOboA/8L0CY5sBXg2HrrO8gdeUeHd9yLZDPsggFD0FzcSuzO5vBO6geUOpruRa8Hqrbb6WWu7Frw=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.3.3", "", { "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg=="],
"@hey-api/codegen-core": ["@hey-api/codegen-core@0.5.2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
"@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
@@ -1215,21 +1219,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.72", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.72", "@opentui/core-darwin-x64": "0.1.72", "@opentui/core-linux-arm64": "0.1.72", "@opentui/core-linux-x64": "0.1.72", "@opentui/core-win32-arm64": "0.1.72", "@opentui/core-win32-x64": "0.1.72", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-l4WQzubBJ80Q0n77Lxuodjwwm8qj/sOa7IXxEAzzDDXY/7bsIhdSpVhRTt+KevBRlok5J+w/KMKYr8UzkA4/hA=="],
"@opentui/core": ["@opentui/core@0.1.74", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.72", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RoU48kOrhLZYDBiXaDu1LXS2bwRdlJlFle8eUQiqJjLRbMIY34J/srBuL0JnAS3qKW4J34NepUQa0l0/S43Q3w=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.72", "", { "os": "darwin", "cpu": "x64" }, "sha512-hHUQw8i2LWPToRW1rjAiRqmNf34iJPS9ve9CJDygvFs5JOqUxN5yrfLfKfE+1bQjfFDHnpqW1HUk96iLhkPj8Q=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.72", "", { "os": "linux", "cpu": "arm64" }, "sha512-63yml0OQ8tVa0JuDF9lBAWiChX6Q+iDO7lKv7c2n0352n/WyPr3iAgq4uSoH49HXuKeAXY/VwHGjvPzjXD/SDA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.72", "", { "os": "linux", "cpu": "x64" }, "sha512-51veiQXNLvzDsFzsEvt71uK7WhiRe2DnvlJSGBSe6aRRHHxjCFYHzYi7t6bitJqtDTUj+EaMPbH81oZ6xy7tyg=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.72", "", { "os": "win32", "cpu": "arm64" }, "sha512-1Ep6OcaYTy1RlLOln+LNN7DL1iNyLwLjG2M8aO0pVJKFvxeD5P7rdRzY065E4uhkHeJIHuduUqxvUjD0dyuwbw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.72", "", { "os": "win32", "cpu": "x64" }, "sha512-5QUv91UkOINlkEaPky3kaxmJvshcJMBAX7LZtIroduaKBGpWRA1aogNhPZzp+30WkvgOU7aOtUktAZuFXb9WdQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
"@opentui/solid": ["@opentui/solid@0.1.72", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.72", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-hytoLPboL/MTY/BQUnf/HlBuNXTVONney0X+PIQI82wT7kMx7+HHI2wnowpM3dyvA7l6NfORSud2cs9kIUBFBw=="],
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1521,7 +1525,7 @@
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.5", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw=="],
@@ -1773,7 +1777,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@@ -1903,7 +1907,7 @@
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
"@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
"@webgpu/types": ["@webgpu/types@0.1.54", "", {}, "sha512-81oaalC8LFrXjhsczomEQ0u3jG+TqE6V9QHLA8GNZq/Rnot0KDugu3LhSYSlie8tSdooAN1Hov05asrUUp9qgg=="],
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
@@ -2075,7 +2079,7 @@
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
@@ -2091,7 +2095,7 @@
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"c12": ["c12@3.3.2", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A=="],
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -3491,7 +3495,7 @@
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
@@ -3965,6 +3969,8 @@
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.57", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DREpYqW2pylgaj69gZ+K8u92bo9DaMgFdictYnY+IwYeY3bawQ4zI7l/o1VkDsBDljAx8iYz5lPURwVZNu+Xpg=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
@@ -4235,6 +4241,10 @@
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="],
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.5", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA=="],
"@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"@solidjs/start/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
@@ -4271,6 +4281,8 @@
"astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="],
"astro/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"astro/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
"astro/unstorage": ["unstorage@1.17.3", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-i+JYyy0DoKmQ3FximTHbGadmIYb8JEpq7lxUjnjeB702bCPum0vzo6oy5Mfu0lpqISw7hCyMW2yj4nWC8bqJ3Q=="],
@@ -4291,6 +4303,10 @@
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
"bun-webgpu/@webgpu/types": ["@webgpu/types@0.1.66", "", {}, "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA=="],
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -4307,6 +4323,8 @@
"editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="],
"editorconfig/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"engine.io-client/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"es-get-iterator/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
@@ -4333,6 +4351,8 @@
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"gel/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
"globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -4347,6 +4367,8 @@
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
@@ -4431,6 +4453,8 @@
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"shiki/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
@@ -4909,6 +4933,8 @@
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1768302833,
"narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=",
"lastModified": 1768456270,
"narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "61db79b0c6b838d9894923920b612048e1201926",
"rev": "f4606b01b39e09065df37905a2133905246db9ed",
"type": "github"
},
"original": {

View File

@@ -7,6 +7,7 @@
outputs =
{
self,
nixpkgs,
...
}:
@@ -107,33 +108,10 @@
};
in
{
default = opencodePkg;
default = self.packages.${system}.opencode;
opencode = opencodePkg;
desktop = desktopPkg;
}
);
apps = forEachSystem (
system:
let
pkgs = pkgsFor system;
in
{
opencode-dev = {
type = "app";
meta = {
description = "Nix devshell shell for OpenCode";
runtimeInputs = [ pkgs.bun ];
};
program = "${
pkgs.writeShellApplication {
name = "opencode-dev";
text = ''
exec bun run dev "$@"
'';
}
}/bin/opencode-dev";
};
}
);
};
}

View File

@@ -119,6 +119,7 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS5"),
new sst.Secret("ZEN_MODELS6"),
new sst.Secret("ZEN_MODELS7"),
new sst.Secret("ZEN_MODELS8"),
]
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")

View File

@@ -369,7 +369,7 @@ case $current_shell in
config_files="$HOME/.config/fish/config.fish"
;;
zsh)
config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
;;
bash)
config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"

View File

@@ -15,6 +15,8 @@
cargo,
rustc,
makeBinaryWrapper,
copyDesktopItems,
makeDesktopItem,
nodejs,
jq,
}:
@@ -57,12 +59,28 @@ rustPlatform.buildRustPackage rec {
pkg-config
bun
makeBinaryWrapper
copyDesktopItems
cargo
rustc
nodejs
jq
];
# based on packages/desktop/src-tauri/release/appstream.metainfo.xml
desktopItems = lib.optionals stdenv.isLinux [
(makeDesktopItem {
name = "ai.opencode.opencode";
desktopName = "OpenCode";
comment = "Open source AI coding agent";
exec = "opencode-desktop";
icon = "opencode";
terminal = false;
type = "Application";
categories = [ "Development" "IDE" ];
startupWMClass = "opencode";
})
];
buildInputs = [
openssl
]
@@ -121,6 +139,10 @@ rustPlatform.buildRustPackage rec {
# It looks for them in the location specified in tauri.conf.json.
postInstall = lib.optionalString stdenv.isLinux ''
# Install icon
mkdir -p $out/share/icons/hicolor/128x128/apps
cp ../../../packages/desktop/src-tauri/icons/prod/128x128.png $out/share/icons/hicolor/128x128/apps/opencode.png
# Wrap the binary to ensure it finds the libraries
wrapProgram $out/bin/opencode-desktop \
--prefix LD_LIBRARY_PATH : ${

View File

@@ -1,6 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-wENwhwRVfgoVyA9YNGcG+fAfu46JxK4xvNgiPbRt//s=",
"aarch64-darwin": "sha256-vm1DYl1erlbaqz5NHHlnZEMuFmidr/UkS84nIqLJ96Q="
"x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=",
"aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=",
"aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=",
"x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE="
}
}

View File

@@ -1,119 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
SYSTEM=${SYSTEM:-x86_64-linux}
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
if [ ! -f "$HASH_FILE" ]; then
cat >"$HASH_FILE" <<EOF
{
"nodeModules": {}
}
EOF
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
fi
fi
export DUMMY
export NIX_KEEP_OUTPUTS=1
export NIX_KEEP_DERIVATIONS=1
cleanup() {
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
}
trap cleanup EXIT
write_node_modules_hash() {
local value="$1"
local system="${2:-$SYSTEM}"
local temp
temp=$(mktemp)
if jq -e '.nodeModules | type == "object"' "$HASH_FILE" >/dev/null 2>&1; then
jq --arg system "$system" --arg value "$value" '.nodeModules[$system] = $value' "$HASH_FILE" >"$temp"
else
jq --arg system "$system" --arg value "$value" '.nodeModules = {($system): $value}' "$HASH_FILE" >"$temp"
fi
mv "$temp" "$HASH_FILE"
}
TARGET="packages.${SYSTEM}.default"
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
CORRECT_HASH=""
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
write_node_modules_hash "$DUMMY"
BUILD_LOG=$(mktemp)
JSON_OUTPUT=$(mktemp)
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
echo "Attempting to realize derivation: ${DRV_PATH}"
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
echo "Realized node_modules output: $BUILD_PATH"
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
fi
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
fi
if [ -z "$CORRECT_HASH" ]; then
echo "Searching for kept failed build directory..."
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
if [ -z "$KEPT_DIR" ]; then
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
fi
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
echo "Found kept build directory: $KEPT_DIR"
if [ -d "$KEPT_DIR/build" ]; then
HASH_PATH="$KEPT_DIR/build"
else
HASH_PATH="$KEPT_DIR"
fi
echo "Attempting to hash: $HASH_PATH"
ls -la "$HASH_PATH" || true
if [ -d "$HASH_PATH/node_modules" ]; then
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
echo "Computed hash from kept build: $CORRECT_HASH"
fi
fi
fi
fi
if [ -z "$CORRECT_HASH" ]; then
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
echo "Build log:"
cat "$BUILD_LOG"
exit 1
fi
write_node_modules_hash "$CORRECT_HASH"
jq -e --arg system "$SYSTEM" --arg hash "$CORRECT_HASH" '.nodeModules[$system] == $hash' "$HASH_FILE" >/dev/null
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
rm -f "$BUILD_LOG"
unset BUILD_LOG

View File

@@ -21,7 +21,7 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.4",
"@types/bun": "1.3.5",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",

View File

@@ -13,12 +13,11 @@
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
<!-- Theme preload script - applies cached theme to avoid FOUC -->
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh"></div>
<div id="root" class="flex flex-col h-dvh p-px"></div>
<script src="/src/entry.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.19",
"version": "1.1.25",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,14 +1,30 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
import { createMemo, createSignal, onCleanup, Show } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file"
type EntryType = "command" | "file"
type Entry = {
id: string
type: EntryType
title: string
description?: string
keybind?: string
category: "Commands" | "Files"
option?: CommandOption
path?: string
}
export function DialogSelectFile() {
const command = useCommand()
const layout = useLayout()
const file = useFile()
const dialog = useDialog()
@@ -16,35 +32,148 @@ export function DialogSelectFile() {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const view = createMemo(() => layout.view(sessionKey()))
const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false)
const common = ["session.new", "session.previous", "session.next", "terminal.toggle", "review.toggle"]
const limit = 5
const allowed = createMemo(() =>
command.options.filter(
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
),
)
const commandItem = (option: CommandOption): Entry => ({
id: "command:" + option.id,
type: "command",
title: option.title,
description: option.description,
keybind: option.keybind,
category: "Commands",
option,
})
const fileItem = (path: string): Entry => ({
id: "file:" + path,
type: "file",
title: path,
category: "Files",
path,
})
const list = createMemo(() => allowed().map(commandItem))
const picks = createMemo(() => {
const all = allowed()
const order = new Map(common.map((id, index) => [id, index]))
const picked = all.filter((option) => order.has(option.id))
const base = picked.length ? picked : all.slice(0, limit)
const sorted = picked.length ? [...base].sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)) : base
return sorted.map(commandItem)
})
const recent = createMemo(() => {
const all = tabs().all()
const active = tabs().active()
const order = active ? [active, ...all.filter((item) => item !== active)] : all
const seen = new Set<string>()
const items: Entry[] = []
for (const item of order) {
const path = file.pathFromTab(item)
if (!path) continue
if (seen.has(path)) continue
seen.add(path)
items.push(fileItem(path))
}
return items.slice(0, limit)
})
const items = async (filter: string) => {
const query = filter.trim()
setGrouped(query.length > 0)
if (!query) return [...picks(), ...recent()]
const files = await file.searchFiles(query)
const entries = files.map(fileItem)
return [...list(), ...entries]
}
const handleMove = (item: Entry | undefined) => {
state.cleanup?.()
if (!item) return
if (item.type !== "command") return
state.cleanup = item.option?.onHighlight?.()
}
const open = (path: string) => {
const value = file.tab(path)
tabs().open(value)
file.load(path)
view().reviewPanel.open()
}
const handleSelect = (item: Entry | undefined) => {
if (!item) return
state.committed = true
state.cleanup = undefined
dialog.close()
if (item.type === "command") {
item.option?.onSelect?.("palette")
return
}
if (!item.path) return
open(item.path)
}
onCleanup(() => {
if (state.committed) return
state.cleanup?.()
})
return (
<Dialog title="Select file">
<Dialog class="pt-3 pb-0 !max-h-[480px]">
<List
search={{ placeholder: "Search files", autofocus: true }}
emptyMessage="No files found"
items={file.searchFiles}
key={(x) => x}
onSelect={(path) => {
if (path) {
const value = file.tab(path)
tabs().open(value)
file.load(path)
view().reviewPanel.open()
}
dialog.close()
}}
search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }}
emptyMessage="No results found"
items={items}
key={(item) => item.id}
filterKeys={["title", "description", "category"]}
groupBy={(item) => item.category}
onMove={handleMove}
onSelect={handleSelect}
>
{(i) => (
<div class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
{(item) => (
<Show
when={item.type === "command"}
fallback={
<div class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: item.path ?? "", type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(item.path ?? "")}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(item.path ?? "")}</span>
</div>
</div>
</div>
}
>
<div class="w-full flex items-center justify-between gap-4 pl-1">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
<Show when={item.description}>
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
</Show>
</div>
<Show when={item.keybind}>
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
</Show>
</div>
</div>
</Show>
)}
</List>
</Dialog>

View File

@@ -1,267 +1,203 @@
import { createMemo, createResource, Show } from "solid-js"
import { A, useNavigate, useParams } from "@solidjs/router"
import { Portal } from "solid-js/web"
import { useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
import { useServer } from "@/context/server"
import { useDialog } from "@opencode-ai/ui/context/dialog"
// import { useServer } from "@/context/server"
// import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { base64Decode } from "@opencode-ai/util/encode"
import { iife } from "@opencode-ai/util/iife"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Select } from "@opencode-ai/ui/select"
import { Popover } from "@opencode-ai/ui/popover"
import { TextField } from "@opencode-ai/ui/text-field"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import type { Session } from "@opencode-ai/sdk/v2/client"
import { same } from "@/utils/same"
import { Keybind } from "@opencode-ai/ui/keybind"
export function SessionHeader() {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const navigate = useNavigate()
const command = useCommand()
const server = useServer()
const dialog = useDialog()
// const server = useServer()
// const dialog = useDialog()
const sync = useSync()
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
const parentSession = createMemo(() => {
const current = currentSession()
if (!current?.parentID) return undefined
return sync.data.session.find((s) => s.id === current.parentID)
const project = createMemo(() => {
const directory = projectDirectory()
if (!directory) return
return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
})
const name = createMemo(() => {
const current = project()
if (current) return current.name || getFilename(current.worktree)
return getFilename(projectDirectory())
})
const hotkey = createMemo(() => command.keybind("file.open"))
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const worktrees = createMemo(() => layout.projects.list().map((p) => p.worktree), [], { equals: same })
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey()))
function navigateToProject(directory: string) {
navigate(`/${base64Encode(directory)}`)
}
function navigateToSession(session: Session | undefined) {
if (!session) return
// Only navigate if we're actually changing to a different session
if (session.id === params.id) return
navigate(`/${params.dir}/session/${session.id}`)
}
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={layout.mobileSidebar.toggle}
>
<Icon name="menu" size="small" />
</button>
<div class="px-4 flex items-center justify-between gap-4 w-full">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={worktrees()}
current={sync.project?.worktree ?? projectDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Show
when={parentSession()}
fallback={
<>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</>
}
<>
<Show when={centerMount()}>
{(mount) => (
<Portal mount={mount()}>
<button
type="button"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
>
<div class="flex items-center gap-2 min-w-0">
<Select
options={sessions()}
current={parentSession()}
placeholder="Back to parent session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={(session) => {
// Only navigate if selecting a different session than current parent
const currentParent = parentSession()
if (session && currentParent && session.id !== currentParent.id) {
navigateToSession(session)
}
}}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
<div class="text-text-weaker">/</div>
<div class="flex items-center gap-1.5 min-w-0">
<Tooltip value="Back to parent session">
<button
type="button"
class="flex items-center justify-center gap-1 p-1 rounded hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors flex-shrink-0"
onClick={() => navigateToSession(parentSession())}
>
<Icon name="arrow-left" size="small" class="text-icon-base" />
</button>
</Tooltip>
</div>
<div class="flex items-center gap-2">
<Icon name="magnifying-glass" size="normal" class="icon-base" />
<span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-3.5 flex items-center overflow-visible">
Search {name()}
</span>
</div>
</Show>
</div>
<Show when={currentSession() && !parentSession()}>
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
</TooltipKeybind>
</Show>
</div>
<div class="flex items-center gap-3">
<div class="hidden md:flex items-center gap-1">
<Button
size="small"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<Icon name="server" size="small" class="text-icon-weak" />
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
</Button>
<SessionLspIndicator />
<SessionMcpIndicator />
</div>
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle review"
keybind={command.keybind("review.toggle")}
>
<Button
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
<Show when={hotkey()}>{(keybind) => <Keybind>{keybind()}</Keybind>}</Show>
</button>
</Portal>
)}
</Show>
<Show when={rightMount()}>
{(mount) => (
<Portal mount={mount()}>
<div class="flex items-center gap-3">
{/* <div class="hidden md:flex items-center gap-1"> */}
{/* <Button */}
{/* size="small" */}
{/* variant="ghost" */}
{/* onClick={() => { */}
{/* dialog.show(() => <DialogSelectServer />) */}
{/* }} */}
{/* > */}
{/* <div */}
{/* classList={{ */}
{/* "size-1.5 rounded-full": true, */}
{/* "bg-icon-success-base": server.healthy() === true, */}
{/* "bg-icon-critical-base": server.healthy() === false, */}
{/* "bg-border-weak-base": server.healthy() === undefined, */}
{/* }} */}
{/* /> */}
{/* <Icon name="server" size="small" class="text-icon-weak" /> */}
{/* <span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span> */}
{/* </Button> */}
{/* <SessionLspIndicator /> */}
{/* <SessionMcpIndicator /> */}
{/* </div> */}
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle review"
keybind={command.keybind("review.toggle")}
>
<Button
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</Show>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle terminal"
keybind={command.keybind("terminal.toggle")}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={view().reviewPanel.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</Show>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle terminal"
keybind={command.keybind("terminal.toggle")}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: projectDirectory() })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
}
return shareURL
},
{ initialValue: "" },
)
return (
<Show when={url.latest}>
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</div>
</header>
<Button
variant="ghost"
class="group/terminal-toggle size-6 p-0"
onClick={() => view().terminal.toggle()}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={view().terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: projectDirectory() })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
}
return shareURL
},
{ initialValue: "" },
)
return (
<Show when={url.latest}>
{(shareUrl) => <TextField value={shareUrl()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</Portal>
)}
</Show>
</>
)
}

View File

@@ -0,0 +1,118 @@
import { createEffect, createMemo, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme"
import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command"
export function Titlebar() {
const layout = useLayout()
const platform = usePlatform()
const command = useCommand()
const theme = useTheme()
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const reserve = createMemo(
() => platform.platform === "desktop" && (platform.os === "windows" || platform.os === "linux"),
)
const web = createMemo(() => platform.platform === "web")
const getWin = () => {
if (platform.platform !== "desktop") return
const tauri = (
window as unknown as {
__TAURI__?: { window?: { getCurrentWindow?: () => { startDragging?: () => Promise<void> } } }
}
).__TAURI__
if (!tauri?.window?.getCurrentWindow) return
return tauri.window.getCurrentWindow()
}
createEffect(() => {
if (platform.platform !== "desktop") return
const scheme = theme.colorScheme()
const value = scheme === "system" ? null : scheme
const tauri = (window as unknown as { __TAURI__?: { webviewWindow?: { getCurrentWebviewWindow?: () => unknown } } })
.__TAURI__
const get = tauri?.webviewWindow?.getCurrentWebviewWindow
if (!get) return
const win = get() as { setTheme?: (theme?: "light" | "dark" | null) => Promise<void> }
if (!win.setTheme) return
void win.setTheme(value).catch(() => undefined)
})
const interactive = (target: EventTarget | null) => {
if (!(target instanceof Element)) return false
const selector =
"button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']"
return !!target.closest(selector)
}
const drag = (e: MouseEvent) => {
if (platform.platform !== "desktop") return
if (e.buttons !== 1) return
if (interactive(e.target)) return
const win = getWin()
if (!win?.startDragging) return
e.preventDefault()
void win.startDragging().catch(() => undefined)
}
return (
<header class="h-10 shrink-0 bg-background-base flex items-center relative">
<div
classList={{
"flex items-center w-full min-w-0 pr-2": true,
"pl-2": !mac(),
}}
onMouseDown={drag}
>
<Show when={mac()}>
<div class="w-[72px] h-full shrink-0" data-tauri-drag-region />
<div class="xl:hidden w-10 shrink-0 flex items-center justify-center">
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
</div>
</Show>
<Show when={!mac()}>
<div class="xl:hidden w-[48px] shrink-0 flex items-center justify-center">
<IconButton icon="menu" variant="ghost" class="size-8 rounded-md" onClick={layout.mobileSidebar.toggle} />
</div>
</Show>
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
placement="bottom"
title="Toggle sidebar"
keybind={command.keybind("sidebar.toggle")}
>
<IconButton
icon={layout.sidebar.opened() ? "layout-left" : "layout-right"}
variant="ghost"
class="size-8 rounded-md"
onClick={layout.sidebar.toggle}
/>
</TooltipKeybind>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
<div class="flex-1 h-full" data-tauri-drag-region />
<div id="opencode-titlebar-right" class="flex items-center gap-3 shrink-0" />
<Show when={reserve()}>
<div class="w-[120px] h-full shrink-0" data-tauri-drag-region />
</Show>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div id="opencode-titlebar-center" class="pointer-events-auto" />
</div>
</header>
)
}

View File

@@ -1,8 +1,6 @@
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
@@ -107,74 +105,27 @@ export function formatKeybind(config: string): string {
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
if (kb.key) {
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
const arrows: Record<string, string> = {
arrowup: "↑",
arrowdown: "↓",
arrowleft: "←",
arrowright: "→",
}
const displayKey =
arrows[kb.key.toLowerCase()] ??
(kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1))
parts.push(displayKey)
}
return IS_MAC ? parts.join("") : parts.join("+")
}
function DialogCommand(props: { options: CommandOption[] }) {
const dialog = useDialog()
let cleanup: (() => void) | void
let committed = false
const handleMove = (option: CommandOption | undefined) => {
cleanup?.()
cleanup = option?.onHighlight?.()
}
const handleSelect = (option: CommandOption | undefined) => {
if (option) {
committed = true
cleanup = undefined
dialog.close()
option.onSelect?.("palette")
}
}
onCleanup(() => {
if (!committed) {
cleanup?.()
}
})
return (
<Dialog title="Commands">
<List
search={{ placeholder: "Search commands", autofocus: true }}
emptyMessage="No commands found"
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
key={(x) => x?.id}
filterKeys={["title", "description", "category"]}
groupBy={(x) => x.category ?? ""}
onMove={handleMove}
onSelect={handleSelect}
>
{(option) => (
<div class="w-full flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
<Show when={option.description}>
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
</Show>
</div>
<Show when={option.keybind}>
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
</Show>
</div>
)}
</List>
</Dialog>
)
}
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
name: "Command",
init: () => {
const dialog = useDialog()
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
const dialog = useDialog()
const options = createMemo(() => {
const seen = new Set<string>()
@@ -202,14 +153,21 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const suspended = () => suspendCount() > 0
const showPalette = () => {
if (!dialog.active) {
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
for (const option of options()) {
if (option.id === id || option.id === "suggested." + id) {
option.onSelect?.(source)
return
}
}
}
const showPalette = () => {
run("file.open", "palette")
}
const handleKeyDown = (event: KeyboardEvent) => {
if (suspended()) return
if (suspended() || dialog.active) return
const paletteKeybinds = parseKeybind("mod+shift+p")
if (matchKeybind(paletteKeybinds, event)) {
@@ -248,12 +206,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
})
},
trigger(id: string, source?: "palette" | "keybind" | "slash") {
for (const option of options()) {
if (option.id === id || option.id === "suggested." + id) {
option.onSelect?.(source)
return
}
}
run(id, source)
},
keybind(id: string) {
const option = options().find((x) => x.id === id || x.id === "suggested." + id)

View File

@@ -19,15 +19,29 @@ import {
type QuestionRequest,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js"
import {
batch,
createContext,
createEffect,
getOwner,
runWithOwner,
useContext,
onCleanup,
onMount,
type Accessor,
type ParentProps,
Switch,
Match,
} from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
import { usePlatform } from "./platform"
import { Persist, persisted } from "@/utils/persist"
type State = {
status: "loading" | "partial" | "complete"
@@ -38,6 +52,7 @@ type State = {
config: Config
path: Path
session: Session[]
sessionTotal: number
session_status: {
[sessionID: string]: SessionStatus
}
@@ -67,9 +82,18 @@ type State = {
}
}
type VcsCache = {
store: Store<{ value: VcsInfo | undefined }>
setStore: SetStoreFunction<{ value: VcsInfo | undefined }>
ready: Accessor<boolean>
}
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const platform = usePlatform()
const owner = getOwner()
if (!owner) throw new Error("GlobalSync must be created within owner")
const vcsCache = new Map<string, VcsCache>()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
error?: InitError
@@ -85,53 +109,81 @@ function createGlobalSync() {
provider_auth: {},
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
children[directory] = createStore<State>({
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
status: "loading" as const,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
permission: {},
question: {},
mcp: {},
lsp: [],
vcs: undefined,
limit: 5,
message: {},
part: {},
})
bootstrapInstance(directory)
const cache = runWithOwner(owner, () =>
persisted(
Persist.workspace(directory, "vcs", ["vcs.v1"]),
createStore({ value: undefined as VcsInfo | undefined }),
),
)
if (!cache) throw new Error("Failed to create persisted cache")
vcsCache.set(directory, { store: cache[0], setStore: cache[1], ready: cache[3] })
const init = () => {
children[directory] = createStore<State>({
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
status: "loading" as const,
agent: [],
command: [],
session: [],
sessionTotal: 0,
session_status: {},
session_diff: {},
todo: {},
permission: {},
question: {},
mcp: {},
lsp: [],
vcs: cache[0].value,
limit: 5,
message: {},
part: {},
})
bootstrapInstance(directory)
}
runWithOwner(owner, init)
}
return children[directory]
const childStore = children[directory]
if (!childStore) throw new Error("Failed to create store")
return childStore
}
async function loadSessions(directory: string) {
const [store, setStore] = child(directory)
globalSDK.client.session
.list({ directory })
const limit = store.limit
return globalSDK.client.session
.list({ directory, roots: true })
.then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
if (sandboxWorkspace) {
setStore("session", reconcile(nonArchived, { key: "id" }))
return
}
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
if (i < limit) return true
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
return updated > fourHoursAgo
})
// Store total session count (used for "load more" pagination)
setStore("sessionTotal", nonArchived.length)
setStore("session", reconcile(sessions, { key: "id" }))
})
.catch((err) => {
@@ -144,6 +196,8 @@ function createGlobalSync() {
async function bootstrapInstance(directory: string) {
if (!directory) return
const [store, setStore] = child(directory)
const cache = vcsCache.get(directory)
if (!cache) return
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
fetch: platform.fetch,
@@ -151,6 +205,13 @@ function createGlobalSync() {
throwOnError: true,
})
createEffect(() => {
if (!cache.ready()) return
const cached = cache.store.value
if (!cached?.branch) return
setStore("vcs", (value) => value ?? cached)
})
const blockingRequests = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () =>
@@ -180,7 +241,11 @@ function createGlobalSync() {
loadSessions(directory),
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
sdk.vcs.get().then((x) => {
const next = x.data ?? store.vcs
setStore("vcs", next)
if (next?.branch) cache.setStore("value", next)
}),
sdk.permission.list().then((x) => {
const grouped: Record<string, PermissionRequest[]> = {}
for (const perm of x.data ?? []) {
@@ -286,6 +351,23 @@ function createGlobalSync() {
bootstrapInstance(directory)
break
}
case "session.created": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
if (!event.properties.info.parentID) {
setStore("sessionTotal", store.sessionTotal + 1)
}
break
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
@@ -297,6 +379,8 @@ function createGlobalSync() {
}),
)
}
if (event.properties.info.parentID) break
setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
if (result.found) {
@@ -393,7 +477,10 @@ function createGlobalSync() {
break
}
case "vcs.branch.updated": {
setStore("vcs", { branch: event.properties.branch })
const next = { branch: event.properties.branch }
setStore("vcs", next)
const cache = vcsCache.get(directory)
if (cache) cache.setStore("value", next)
break
}
case "permission.asked": {

View File

@@ -47,12 +47,34 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const server = useServer()
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
const migrate = (value: unknown) => {
if (!isRecord(value)) return value
const sidebar = value.sidebar
if (!isRecord(sidebar)) return value
if (typeof sidebar.workspaces !== "boolean") return value
return {
...value,
sidebar: {
...sidebar,
workspaces: {},
workspacesDefault: sidebar.workspaces,
},
}
}
const target = Persist.global("layout", ["layout.v6"])
const [store, setStore, _, ready] = persisted(
Persist.global("layout", ["layout.v6"]),
{ ...target, migrate },
createStore({
sidebar: {
opened: false,
width: 280,
width: 344,
workspaces: {} as Record<string, boolean>,
workspacesDefault: false,
},
terminal: {
height: 280,
@@ -304,6 +326,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
resize(width: number) {
setStore("sidebar", "width", width)
},
workspaces(directory: string) {
return createMemo(() => store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false)
},
setWorkspaces(directory: string, value: boolean) {
setStore("sidebar", "workspaces", directory, value)
},
toggleWorkspaces(directory: string) {
const current = store.sidebar.workspaces[directory] ?? store.sidebar.workspacesDefault ?? false
setStore("sidebar", "workspaces", directory, !current)
},
},
terminal: {
height: createMemo(() => store.terminal.height),

View File

@@ -5,6 +5,9 @@ export type Platform = {
/** Platform discriminator */
platform: "web" | "desktop"
/** Desktop OS (Tauri only) */
os?: "macos" | "windows" | "linux"
/** App version */
version?: string

View File

@@ -36,6 +36,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
createStore({
list: [] as string[],
projects: {} as Record<string, StoredProject[]>,
lastProject: {} as Record<string, string>,
}),
)
@@ -197,6 +198,16 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
result.splice(toIndex, 0, item)
setStore("projects", key, result)
},
last() {
const key = origin()
if (!key) return
return store.lastProject[key]
},
touch(directory: string) {
const key = origin()
if (!key) return
setStore("lastProject", key, directory)
},
},
}
},

View File

@@ -14,7 +14,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
const chunk = 200
const chunk = 400
const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>()

View File

@@ -5,3 +5,7 @@
cursor: default;
}
}
*[data-tauri-drag-region] {
app-region: drag;
}

View File

@@ -1,4 +1,3 @@
import { useGlobalSync } from "@/context/global-sync"
import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
@@ -12,6 +11,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useServer } from "@/context/server"
import { useGlobalSync } from "@/context/global-sync"
export default function Home() {
const sync = useGlobalSync()
@@ -24,6 +24,7 @@ export default function Home() {
function openProject(directory: string) {
layout.projects.open(directory)
server.projects.touch(directory)
navigate(`/${base64Encode(directory)}`)
}

File diff suppressed because it is too large Load Diff

View File

@@ -419,7 +419,6 @@ export default function Page() {
{
id: "session.new",
title: "New session",
description: "Create a new session",
category: "Session",
keybind: "mod+shift+s",
slash: "new",
@@ -428,7 +427,7 @@ export default function Page() {
{
id: "file.open",
title: "Open file",
description: "Search and open a file",
description: "Search files and commands",
category: "File",
keybind: "mod+p",
slash: "open",
@@ -437,7 +436,7 @@ export default function Page() {
{
id: "terminal.toggle",
title: "Toggle terminal",
description: "Show or hide the terminal",
description: "",
category: "View",
keybind: "ctrl+`",
slash: "terminal",
@@ -446,7 +445,7 @@ export default function Page() {
{
id: "review.toggle",
title: "Toggle review",
description: "Show or hide the review panel",
description: "",
category: "View",
keybind: "mod+shift+r",
onSelect: () => view().reviewPanel.toggle(),
@@ -885,6 +884,19 @@ export default function Page() {
window.history.replaceState(null, "", `#${anchor(id)}`)
}
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
const root = scroller
if (!root) {
el.scrollIntoView({ behavior, block: "start" })
return
}
const a = el.getBoundingClientRect()
const b = root.getBoundingClientRect()
const top = a.top - b.top + root.scrollTop
root.scrollTo({ top, behavior })
}
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
setActiveMessage(message)
@@ -896,7 +908,7 @@ export default function Page() {
requestAnimationFrame(() => {
const el = document.getElementById(anchor(message.id))
if (el) el.scrollIntoView({ behavior, block: "start" })
if (el) scrollToElement(el, behavior)
})
updateHash(message.id)
@@ -904,7 +916,7 @@ export default function Page() {
}
const el = document.getElementById(anchor(message.id))
if (el) el.scrollIntoView({ behavior, block: "start" })
if (el) scrollToElement(el, behavior)
updateHash(message.id)
}
@@ -956,7 +968,7 @@ export default function Page() {
const hashTarget = document.getElementById(hash)
if (hashTarget) {
hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
scrollToElement(hashTarget, "auto")
return
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.1.19",
"version": "1.1.25",
"type": "module",
"license": "MIT",
"scripts": {
@@ -20,6 +20,8 @@
"@opencode-ai/console-mail": "workspace:*",
"@opencode-ai/console-resource": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@smithy/eventstream-codec": "4.2.7",
"@smithy/util-utf8": "4.2.0",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
@@ -34,6 +36,7 @@
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"@webgpu/types": "0.1.54",
"typescript": "catalog:",
"wrangler": "4.50.0"
},

View File

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

View File

@@ -24,6 +24,9 @@ export function Footer() {
<div data-slot="cell">
<a href="/docs">Docs</a>
</div>
<div data-slot="cell">
<a href="/changelog">Changelog</a>
</div>
<div data-slot="cell">
<a href="/discord">Discord</a>
</div>

View File

@@ -0,0 +1,15 @@
.spotlight-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50dvh;
pointer-events: none;
overflow: hidden;
}
.spotlight-container canvas {
display: block;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,820 @@
import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
import "./spotlight.css"
export interface ParticlesConfig {
enabled: boolean
amount: number
size: [number, number]
speed: number
opacity: number
drift: number
}
export interface SpotlightConfig {
placement: [number, number]
color: string
speed: number
spread: number
length: number
width: number
pulsating: false | [number, number]
distance: number
saturation: number
noiseAmount: number
distortion: number
opacity: number
particles: ParticlesConfig
}
export const defaultConfig: SpotlightConfig = {
placement: [0.5, -0.15],
color: "#ffffff",
speed: 0.8,
spread: 0.5,
length: 4.0,
width: 0.15,
pulsating: [0.95, 1.1],
distance: 3.5,
saturation: 0.35,
noiseAmount: 0.15,
distortion: 0.05,
opacity: 0.325,
particles: {
enabled: true,
amount: 70,
size: [1.25, 1.5],
speed: 0.75,
opacity: 0.9,
drift: 1.5,
},
}
export interface SpotlightAnimationState {
time: number
intensity: number
pulseValue: number
}
interface SpotlightProps {
config: Accessor<SpotlightConfig>
class?: string
onAnimationFrame?: (state: SpotlightAnimationState) => void
}
const hexToRgb = (hex: string): [number, number, number] => {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
}
const getAnchorAndDir = (
placement: [number, number],
w: number,
h: number,
): { anchor: [number, number]; dir: [number, number] } => {
const [px, py] = placement
const outside = 0.2
let anchorX = px * w
let anchorY = py * h
let dirX = 0
let dirY = 0
const centerX = 0.5
const centerY = 0.5
if (py <= 0.25) {
anchorY = -outside * h + py * h
dirY = 1
dirX = (centerX - px) * 0.5
} else if (py >= 0.75) {
anchorY = (1 + outside) * h - (1 - py) * h
dirY = -1
dirX = (centerX - px) * 0.5
} else if (px <= 0.25) {
anchorX = -outside * w + px * w
dirX = 1
dirY = (centerY - py) * 0.5
} else if (px >= 0.75) {
anchorX = (1 + outside) * w - (1 - px) * w
dirX = -1
dirY = (centerY - py) * 0.5
} else {
dirY = 1
}
const len = Math.sqrt(dirX * dirX + dirY * dirY)
if (len > 0) {
dirX /= len
dirY /= len
}
return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
}
interface UniformData {
iTime: number
iResolution: [number, number]
lightPos: [number, number]
lightDir: [number, number]
color: [number, number, number]
speed: number
lightSpread: number
lightLength: number
sourceWidth: number
pulsating: number
pulsatingMin: number
pulsatingMax: number
fadeDistance: number
saturation: number
noiseAmount: number
distortion: number
particlesEnabled: number
particleAmount: number
particleSizeMin: number
particleSizeMax: number
particleSpeed: number
particleOpacity: number
particleDrift: number
}
const WGSL_SHADER = `
struct Uniforms {
iTime: f32,
_pad0: f32,
iResolution: vec2<f32>,
lightPos: vec2<f32>,
lightDir: vec2<f32>,
color: vec3<f32>,
speed: f32,
lightSpread: f32,
lightLength: f32,
sourceWidth: f32,
pulsating: f32,
pulsatingMin: f32,
pulsatingMax: f32,
fadeDistance: f32,
saturation: f32,
noiseAmount: f32,
distortion: f32,
particlesEnabled: f32,
particleAmount: f32,
particleSizeMin: f32,
particleSizeMax: f32,
particleSpeed: f32,
particleOpacity: f32,
particleDrift: f32,
_pad1: f32,
_pad2: f32,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) vUv: vec2<f32>,
};
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
var positions = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
var output: VertexOutput;
let pos = positions[vertexIndex];
output.position = vec4<f32>(pos, 0.0, 1.0);
output.vUv = pos * 0.5 + 0.5;
return output;
}
fn hash(p: vec2<f32>) -> f32 {
let p3 = fract(p.xyx * 0.1031);
return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
}
fn hash2(p: vec2<f32>) -> vec2<f32> {
let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
return fract(vec2<f32>(n * 262144.0, n * 32768.0));
}
fn fastNoise(st: vec2<f32>) -> f32 {
return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
}
fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
let sourceToCoord = coord - lightSource;
let distSq = dot(sourceToCoord, sourceToCoord);
let distance = sqrt(distSq);
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
if (distance > maxDistance) {
return 0.0;
}
let invDist = 1.0 / max(distance, 0.001);
let dirNorm = sourceToCoord * invDist;
let cosAngle = dot(dirNorm, lightRefDirection);
if (cosAngle < 0.0) {
return 0.0;
}
let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
let time = uniforms.iTime;
let speed = uniforms.speed;
let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
var pulse: f32 = 1.0;
if (uniforms.pulsating > 0.5) {
let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
}
let timeSpeed = time * speed;
let wave = 0.5
+ 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
+ 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
+ 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
let minStrength = 0.14 + asymNoise * 0.06;
let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
return max(lightStrength, ambientLight);
}
fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
let delta = coord - particlePos;
let distSq = dot(delta, delta);
let sizeSq = size * size;
if (distSq > sizeSq * 9.0) {
return 0.0;
}
let d = sqrt(distSq);
let core = smoothstep(size, size * 0.35, d);
let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
return core + glow;
}
fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
return 0.0;
}
var particleSum: f32 = 0.0;
let particleCount = i32(uniforms.particleAmount);
let time = uniforms.iTime * uniforms.particleSpeed;
let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
let maxDist = max(baseSize * uniforms.lightLength, 1.0);
let spreadScale = uniforms.lightSpread * baseSize * 0.65;
let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
for (var i: i32 = 0; i < particleCount; i = i + 1) {
let fi = f32(i);
let seed = vec2<f32>(fi * 127.1, fi * 311.7);
let rnd = hash2(seed);
let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
let lifeProgress = fract((time + lifeOffset) / lifeDuration);
let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
let lifeFade = fadeIn * fadeOut;
if (lifeFade < 0.01) {
continue;
}
let alongLight = rnd.x * maxDist * 0.8;
let perpOffset = (rnd.y - 0.5) * spreadScale;
let floatPhase = rnd.y * 6.28318 + fi * 0.37;
let floatSpeed = 0.35 + rnd.x * 0.9;
let drift = vec2<f32>(
sin(time * floatSpeed + floatPhase),
cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
) * uniforms.particleDrift * baseSize * 0.08;
let wobble = vec2<f32>(
sin(time * 1.4 + floatPhase * 2.1),
cos(time * 1.1 + floatPhase * 1.6)
) * uniforms.particleDrift * baseSize * 0.03;
let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
let toParticle = basePos - lightSource;
let projLen = dot(toParticle, lightDir);
if (projLen < 0.0 || projLen > maxDist) {
continue;
}
let sideDist = abs(dot(toParticle, perpDir));
if (sideDist > coneHalfWidth) {
continue;
}
let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
if (distFade < 0.01) {
continue;
}
let p = particle(coord, basePos, size);
if (p > 0.0) {
particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
if (particleSum >= 1.0) {
break;
}
}
}
return min(particleSum, 1.0);
}
@fragment
fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
let coord = vec2<f32>(fragCoord.x, fragCoord.y);
let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
if (lightValue < 0.001) {
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
if (particles < 0.001) {
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
}
let particleBrightness = particles * 1.8;
return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
}
var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
if (uniforms.noiseAmount > 0.01) {
let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
}
let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
fragColor = vec4<f32>(
fragColor.x * (0.15 + brightness * 0.85),
fragColor.y * (0.35 + brightness * 0.65),
fragColor.z * (0.55 + brightness * 0.45),
fragColor.a
);
if (abs(uniforms.saturation - 1.0) > 0.01) {
let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
}
fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
if (particles > 0.001) {
let particleBrightness = particles * 1.8;
fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
}
return fragColor;
}
`
const UNIFORM_BUFFER_SIZE = 144
function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
buffer[0] = data.iTime
buffer[2] = data.iResolution[0]
buffer[3] = data.iResolution[1]
buffer[4] = data.lightPos[0]
buffer[5] = data.lightPos[1]
buffer[6] = data.lightDir[0]
buffer[7] = data.lightDir[1]
buffer[8] = data.color[0]
buffer[9] = data.color[1]
buffer[10] = data.color[2]
buffer[11] = data.speed
buffer[12] = data.lightSpread
buffer[13] = data.lightLength
buffer[14] = data.sourceWidth
buffer[15] = data.pulsating
buffer[16] = data.pulsatingMin
buffer[17] = data.pulsatingMax
buffer[18] = data.fadeDistance
buffer[19] = data.saturation
buffer[20] = data.noiseAmount
buffer[21] = data.distortion
buffer[22] = data.particlesEnabled
buffer[23] = data.particleAmount
buffer[24] = data.particleSizeMin
buffer[25] = data.particleSizeMax
buffer[26] = data.particleSpeed
buffer[27] = data.particleOpacity
buffer[28] = data.particleDrift
}
export default function Spotlight(props: SpotlightProps) {
let containerRef: HTMLDivElement | undefined
let canvasRef: HTMLCanvasElement | null = null
let deviceRef: GPUDevice | null = null
let contextRef: GPUCanvasContext | null = null
let pipelineRef: GPURenderPipeline | null = null
let uniformBufferRef: GPUBuffer | null = null
let bindGroupRef: GPUBindGroup | null = null
let animationIdRef: number | null = null
let cleanupFunctionRef: (() => void) | null = null
let uniformDataRef: UniformData | null = null
let uniformArrayRef: Float32Array | null = null
let configRef: SpotlightConfig = props.config()
let frameCount = 0
const [isVisible, setIsVisible] = createSignal(false)
createEffect(() => {
configRef = props.config()
})
onMount(() => {
if (!containerRef) return
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
setIsVisible(entry.isIntersecting)
},
{ threshold: 0.1 },
)
observer.observe(containerRef)
onCleanup(() => {
observer.disconnect()
})
})
createEffect(() => {
const visible = isVisible()
const config = props.config()
if (!visible || !containerRef) {
return
}
if (cleanupFunctionRef) {
cleanupFunctionRef()
cleanupFunctionRef = null
}
const initializeWebGPU = async () => {
if (!containerRef) {
return
}
await new Promise((resolve) => setTimeout(resolve, 10))
if (!containerRef) {
return
}
if (!navigator.gpu) {
console.warn("WebGPU is not supported in this browser")
return
}
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "high-performance",
})
if (!adapter) {
console.warn("Failed to get WebGPU adapter")
return
}
const device = await adapter.requestDevice()
deviceRef = device
const canvas = document.createElement("canvas")
canvas.style.width = "100%"
canvas.style.height = "100%"
canvasRef = canvas
while (containerRef.firstChild) {
containerRef.removeChild(containerRef.firstChild)
}
containerRef.appendChild(canvas)
const context = canvas.getContext("webgpu")
if (!context) {
console.warn("Failed to get WebGPU context")
return
}
contextRef = context
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
context.configure({
device,
format: presentationFormat,
alphaMode: "premultiplied",
})
const shaderModule = device.createShaderModule({
code: WGSL_SHADER,
})
const uniformBuffer = device.createBuffer({
size: UNIFORM_BUFFER_SIZE,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})
uniformBufferRef = uniformBuffer
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: "uniform" },
},
],
})
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: { buffer: uniformBuffer },
},
],
})
bindGroupRef = bindGroup
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
})
const pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vertexMain",
},
fragment: {
module: shaderModule,
entryPoint: "fragmentMain",
targets: [
{
format: presentationFormat,
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "one",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
},
},
],
},
primitive: {
topology: "triangle-list",
},
})
pipelineRef = pipeline
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
const dpr = Math.min(window.devicePixelRatio, 2)
const w = wCSS * dpr
const h = hCSS * dpr
const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
uniformDataRef = {
iTime: 0,
iResolution: [w, h],
lightPos: anchor,
lightDir: dir,
color: hexToRgb(config.color),
speed: config.speed,
lightSpread: config.spread,
lightLength: config.length,
sourceWidth: config.width,
pulsating: config.pulsating !== false ? 1.0 : 0.0,
pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
fadeDistance: config.distance,
saturation: config.saturation,
noiseAmount: config.noiseAmount,
distortion: config.distortion,
particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
particleAmount: config.particles.amount,
particleSizeMin: config.particles.size[0],
particleSizeMax: config.particles.size[1],
particleSpeed: config.particles.speed,
particleOpacity: config.particles.opacity,
particleDrift: config.particles.drift,
}
const updatePlacement = () => {
if (!containerRef || !canvasRef || !uniformDataRef) {
return
}
const dpr = Math.min(window.devicePixelRatio, 2)
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
const w = Math.floor(wCSS * dpr)
const h = Math.floor(hCSS * dpr)
canvasRef.width = w
canvasRef.height = h
uniformDataRef.iResolution = [w, h]
const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
uniformDataRef.lightPos = anchor
uniformDataRef.lightDir = dir
}
const loop = (t: number) => {
if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
return
}
const timeSeconds = t * 0.001
uniformDataRef.iTime = timeSeconds
frameCount++
if (props.onAnimationFrame && frameCount % 2 === 0) {
const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
const pulseValue =
configRef.pulsating !== false
? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
: 1.0
const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
props.onAnimationFrame({
time: timeSeconds,
intensity,
pulseValue: Math.max(pulseValue, 0.9),
})
}
try {
if (!uniformArrayRef) {
uniformArrayRef = new Float32Array(36)
}
updateUniformBuffer(uniformArrayRef, uniformDataRef)
deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
const commandEncoder = deviceRef.createCommandEncoder()
const textureView = contextRef.getCurrentTexture().createView()
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
clearValue: { r: 0, g: 0, b: 0, a: 0 },
loadOp: "clear",
storeOp: "store",
},
],
})
renderPass.setPipeline(pipelineRef)
renderPass.setBindGroup(0, bindGroupRef)
renderPass.draw(3)
renderPass.end()
deviceRef.queue.submit([commandEncoder.finish()])
animationIdRef = requestAnimationFrame(loop)
} catch (error) {
console.warn("WebGPU rendering error:", error)
return
}
}
window.addEventListener("resize", updatePlacement)
updatePlacement()
animationIdRef = requestAnimationFrame(loop)
cleanupFunctionRef = () => {
if (animationIdRef) {
cancelAnimationFrame(animationIdRef)
animationIdRef = null
}
window.removeEventListener("resize", updatePlacement)
if (uniformBufferRef) {
uniformBufferRef.destroy()
uniformBufferRef = null
}
if (deviceRef) {
deviceRef.destroy()
deviceRef = null
}
if (canvasRef && canvasRef.parentNode) {
canvasRef.parentNode.removeChild(canvasRef)
}
canvasRef = null
contextRef = null
pipelineRef = null
bindGroupRef = null
uniformDataRef = null
}
}
initializeWebGPU()
onCleanup(() => {
if (cleanupFunctionRef) {
cleanupFunctionRef()
cleanupFunctionRef = null
}
})
})
createEffect(() => {
if (!uniformDataRef || !containerRef) {
return
}
const config = props.config()
uniformDataRef.color = hexToRgb(config.color)
uniformDataRef.speed = config.speed
uniformDataRef.lightSpread = config.spread
uniformDataRef.lightLength = config.length
uniformDataRef.sourceWidth = config.width
uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
uniformDataRef.fadeDistance = config.distance
uniformDataRef.saturation = config.saturation
uniformDataRef.noiseAmount = config.noiseAmount
uniformDataRef.distortion = config.distortion
uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
uniformDataRef.particleAmount = config.particles.amount
uniformDataRef.particleSizeMin = config.particles.size[0]
uniformDataRef.particleSizeMax = config.particles.size[1]
uniformDataRef.particleSpeed = config.particles.speed
uniformDataRef.particleOpacity = config.particles.opacity
uniformDataRef.particleDrift = config.particles.drift
const dpr = Math.min(window.devicePixelRatio, 2)
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
uniformDataRef.lightPos = anchor
uniformDataRef.lightDir = dir
})
return (
<div
ref={containerRef}
class={`spotlight-container ${props.class ?? ""}`.trim()}
style={{ opacity: props.config().opacity }}
/>
)
}

View File

@@ -9,8 +9,8 @@ export const config = {
github: {
repoUrl: "https://github.com/anomalyco/opencode",
starsFormatted: {
compact: "60K",
full: "60,000",
compact: "70K",
full: "70,000",
},
},
@@ -23,7 +23,7 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "500",
commits: "6,500",
commits: "7,000",
monthlyUsers: "650,000",
},
} as const

View File

@@ -14,13 +14,14 @@ export const github = query(async () => {
fetch(`${apiBaseUrl}/releases`, { headers }).then((res) => res.json()),
fetch(`${apiBaseUrl}/contributors?per_page=1`, { headers }),
])
if (!Array.isArray(releases) || releases.length === 0) {
return undefined
}
const [release] = releases
const contributorCount = Number.parseInt(
contributors.headers
.get("Link")!
.match(/&page=(\d+)>; rel="last"/)!
.at(1)!,
)
const linkHeader = contributors.headers.get("Link")
const contributorCount = linkHeader
? Number.parseInt(linkHeader.match(/&page=(\d+)>; rel="last"/)?.at(1) ?? "0")
: 0
return {
stars: meta.stargazers_count,
release: {

View File

@@ -1,3 +1,114 @@
::view-transition-group(*) {
animation-duration: 250ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 250ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-image-pair(root) {
isolation: isolate;
}
::view-transition-old(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes reveal-terms {
from {
mask-position: 0% 200%;
}
to {
mask-position: 0% 50%;
}
}
@keyframes hide-terms {
from {
mask-position: 0% 50%;
}
to {
mask-position: 0% 200%;
}
}
::view-transition-old(terms-20),
::view-transition-old(terms-100),
::view-transition-old(terms-200) {
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
mask-repeat: no-repeat;
mask-size: 100% 200%;
animation: hide-terms 200ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
}
::view-transition-new(terms-20),
::view-transition-new(terms-100),
::view-transition-new(terms-200) {
mask-image: linear-gradient(to bottom, transparent, black 25% 75%, transparent);
mask-repeat: no-repeat;
mask-position: 0% 200%;
mask-size: 100% 200%;
animation: reveal-terms 300ms cubic-bezier(0.25, 0, 0.5, 1) 50ms forwards;
}
::view-transition-old(actions-20),
::view-transition-old(actions-100),
::view-transition-old(actions-200) {
animation: fade-out 80ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
::view-transition-new(actions-20),
::view-transition-new(actions-100),
::view-transition-new(actions-200) {
animation: fade-in-up 300ms cubic-bezier(0.16, 1, 0.3, 1) 300ms forwards;
opacity: 0;
}
::view-transition-group(card-20),
::view-transition-group(card-100),
::view-transition-group(card-200) {
animation-duration: 250ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-page="black"] {
background: #000;
min-height: 100vh;
@@ -8,13 +119,18 @@
font-family: var(--font-mono);
color: #fff;
[data-component="header-gradient"] {
[data-component="header-logo"] {
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.25)) drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
position: relative;
z-index: 1;
}
.header-light-rays {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 288px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
inset: 0 0 auto 0;
height: 30dvh;
pointer-events: none;
z-index: 0;
}
[data-component="header"] {
@@ -24,9 +140,6 @@
justify-content: center;
padding-top: 40px;
flex-shrink: 0;
/* [data-component="header-logo"] { */
/* } */
}
[data-component="content"] {
@@ -51,27 +164,35 @@
h1 {
color: rgba(255, 255, 255, 0.92);
font-size: 18px;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 160%;
line-height: 1.45;
margin: 0;
@media (min-width: 768px) {
font-size: 24px;
font-size: 20px;
}
@media (max-width: 480px) {
font-size: 14px;
}
}
p {
color: rgba(255, 255, 255, 0.59);
font-size: 15px;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 160%;
line-height: 1.45;
margin: 0;
@media (min-width: 768px) {
font-size: 18px;
font-size: 20px;
}
@media (max-width: 480px) {
font-size: 14px;
}
}
}
@@ -79,6 +200,7 @@
[data-slot="hero-black"] {
margin-top: 40px;
padding: 0 20px;
position: relative;
@media (min-width: 768px) {
margin-top: 60px;
@@ -86,9 +208,29 @@
svg {
width: 100%;
max-width: 540px;
max-width: 590px;
height: auto;
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
overflow: visible;
filter: drop-shadow(0 0 20px rgba(255, 255, 255, calc(0.1 + var(--hero-black-glow-intensity, 0) * 0.15)))
drop-shadow(0 -5px 30px rgba(255, 255, 255, calc(var(--hero-black-glow-intensity, 0) * 0.2)));
mask-image: linear-gradient(to bottom, black, transparent);
stroke-width: 1.5;
[data-slot="black-base"] {
fill: url(#hero-black-fill-gradient);
stroke: url(#hero-black-stroke-gradient);
}
[data-slot="black-glow"] {
fill: url(#hero-black-top-glow);
pointer-events: none;
}
[data-slot="black-shimmer"] {
fill: url(#hero-black-shimmer-gradient);
pointer-events: none;
mix-blend-mode: overlay;
}
}
}
@@ -111,7 +253,7 @@
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 160%; /* 28.8px */
line-height: 160%;
span {
display: inline-block;
@@ -172,7 +314,7 @@
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.17);
color: rgba(255, 255, 255, 0.59);
font-family: "JetBrains Mono Nerd Font";
font-family: var(--font-mono);
font-size: 14px;
font-style: normal;
font-weight: 400;
@@ -185,7 +327,7 @@
flex-direction: column;
gap: 16px;
width: 100%;
max-width: 540px;
max-width: 660px;
padding: 0 20px;
@media (min-width: 768px) {
@@ -197,16 +339,21 @@
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.17);
background: black;
background-clip: padding-box;
border-radius: 4px;
text-decoration: none;
transition: border-color 0.15s ease;
background: transparent;
cursor: pointer;
text-align: left;
&:hover {
@media (max-width: 480px) {
padding: 16px;
}
&:hover:not(:active) {
border-color: rgba(255, 255, 255, 0.35);
}
@@ -247,19 +394,27 @@
display: flex;
flex-direction: column;
gap: 32px;
width: fit-content;
max-width: calc(100% - 40px);
width: 100%;
max-width: 660px;
margin: 0 auto;
position: relative;
background-color: rgba(0, 0, 0, 0.75);
z-index: 1;
@media (max-width: 480px) {
margin: 0 20px;
width: calc(100% - 40px);
}
}
[data-slot="selected-card"] {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
gap: 12px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.17);
border-radius: 4px;
width: fit-content;
width: 100%;
[data-slot="icon"] {
color: rgba(255, 255, 255, 0.59);
@@ -299,16 +454,15 @@
margin: 0;
display: flex;
flex-direction: column;
gap: 12px;
gap: 8px;
text-align: left;
li {
color: rgba(255, 255, 255, 0.59);
font-size: 13px;
font-size: 14px;
line-height: 1.5;
padding-left: 16px;
position: relative;
white-space: nowrap;
&::before {
content: "▪";
@@ -316,6 +470,10 @@
left: 0;
color: rgba(255, 255, 255, 0.39);
}
@media (max-width: 768px) {
font-size: 12px;
}
}
}
@@ -341,22 +499,26 @@
}
[data-slot="cancel"] {
background: transparent;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.17);
color: rgba(255, 255, 255, 0.92);
transition-property: background-color, border-color;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
&:hover {
border-color: rgba(255, 255, 255, 0.35);
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.25);
}
}
[data-slot="continue"] {
background: rgba(255, 255, 255, 0.17);
border: 1px solid rgba(255, 255, 255, 0.17);
color: rgba(255, 255, 255, 0.59);
background: rgb(255, 255, 255);
color: rgb(0, 0, 0);
transition: background-color 150ms cubic-bezier(0.25, 0, 0.5, 1);
&:hover {
background: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.9);
}
}
}
@@ -386,7 +548,7 @@
align-items: center;
margin-top: -18px;
width: 100%;
max-width: 540px;
max-width: 660px;
padding: 0 20px;
@media (min-width: 768px) {
@@ -419,6 +581,8 @@
[data-slot="icon"] {
color: rgba(255, 255, 255, 0.59);
isolation: isolate;
transform: translateZ(0);
}
[data-slot="price"] {
@@ -460,39 +624,6 @@
font-weight: 400;
}
[data-slot="tax-id-section"] {
display: flex;
flex-direction: column;
gap: 8px;
[data-slot="label"] {
color: rgba(255, 255, 255, 0.59);
font-size: 14px;
}
[data-slot="input"] {
width: 100%;
height: 44px;
padding: 0 12px;
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.17);
border-radius: 4px;
color: #ffffff;
font-family: var(--font-mono);
font-size: 14px;
outline: none;
transition: border-color 0.15s ease;
&::placeholder {
color: rgba(255, 255, 255, 0.39);
}
&:focus {
border-color: rgba(255, 255, 255, 0.35);
}
}
}
[data-slot="checkout-form"] {
display: flex;
flex-direction: column;
@@ -533,52 +664,6 @@
text-align: center;
}
[data-slot="success"] {
display: flex;
flex-direction: column;
gap: 24px;
[data-slot="title"] {
color: rgba(255, 255, 255, 0.92);
font-size: 18px;
font-weight: 400;
margin: 0;
}
[data-slot="details"] {
display: flex;
flex-direction: column;
gap: 16px;
> div {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
}
dt {
color: rgba(255, 255, 255, 0.59);
font-size: 14px;
font-weight: 400;
}
dd {
color: rgba(255, 255, 255, 0.92);
font-size: 14px;
font-weight: 400;
margin: 0;
text-align: right;
}
}
[data-slot="charge-notice"] {
color: #d4a500;
font-size: 14px;
text-align: left;
}
}
[data-slot="loading"] {
display: flex;
justify-content: center;
@@ -595,6 +680,7 @@
text-align: center;
font-size: 13px;
font-style: italic;
view-transition-name: fine-print;
a {
color: rgba(255, 255, 255, 0.39);

File diff suppressed because one or more lines are too long

View File

@@ -2,8 +2,8 @@ import { Match, Switch } from "solid-js"
export const plans = [
{ id: "20", multiplier: null },
{ id: "100", multiplier: "6x more usage than Black 20" },
{ id: "200", multiplier: "21x more usage than Black 20" },
{ id: "100", multiplier: "5x more usage than Black 20" },
{ id: "200", multiplier: "20x more usage than Black 20" },
] as const
export type PlanID = (typeof plans)[number]["id"]
@@ -14,28 +14,47 @@ export function PlanIcon(props: { plan: string }) {
<Switch>
<Match when={props.plan === "20"}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
<title>Black 20 plan</title>
<rect x="0.5" y="0.5" width="23" height="23" stroke="currentColor" />
</svg>
</Match>
<Match when={props.plan === "100"}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
<rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
<rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
<rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
<title>Black 100 plan</title>
<rect x="0.5" y="0.5" width="9" height="9" stroke="currentColor" />
<rect x="0.5" y="14.5" width="9" height="9" stroke="currentColor" />
<rect x="14.5" y="0.5" width="9" height="9" stroke="currentColor" />
<rect x="14.5" y="14.5" width="9" height="9" stroke="currentColor" />
</svg>
</Match>
<Match when={props.plan === "200"}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="2" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="10" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="18" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="2" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="10" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<rect x="18" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
<title>Black 200 plan</title>
<rect x="0.5" y="0.5" width="3" height="3" stroke="currentColor" />
<rect x="0.5" y="5.5" width="3" height="3" stroke="currentColor" />
<rect x="0.5" y="10.5" width="3" height="3" stroke="currentColor" />
<rect x="0.5" y="15.5" width="3" height="3" stroke="currentColor" />
<rect x="0.5" y="20.5" width="3" height="3" stroke="currentColor" />
<rect x="5.5" y="0.5" width="3" height="3" stroke="currentColor" />
<rect x="5.5" y="5.5" width="3" height="3" stroke="currentColor" />
<rect x="5.5" y="10.5" width="3" height="3" stroke="currentColor" />
<rect x="5.5" y="15.5" width="3" height="3" stroke="currentColor" />
<rect x="5.5" y="20.5" width="3" height="3" stroke="currentColor" />
<rect x="10.5" y="0.5" width="3" height="3" stroke="currentColor" />
<rect x="10.5" y="5.5" width="3" height="3" stroke="currentColor" />
<rect x="10.5" y="10.5" width="3" height="3" stroke="currentColor" />
<rect x="10.5" y="15.5" width="3" height="3" stroke="currentColor" />
<rect x="10.5" y="20.5" width="3" height="3" stroke="currentColor" />
<rect x="15.5" y="0.5" width="3" height="3" stroke="currentColor" />
<rect x="15.5" y="5.5" width="3" height="3" stroke="currentColor" />
<rect x="15.5" y="10.5" width="3" height="3" stroke="currentColor" />
<rect x="15.5" y="15.5" width="3" height="3" stroke="currentColor" />
<rect x="15.5" y="20.5" width="3" height="3" stroke="currentColor" />
<rect x="20.5" y="0.5" width="3" height="3" stroke="currentColor" />
<rect x="20.5" y="5.5" width="3" height="3" stroke="currentColor" />
<rect x="20.5" y="10.5" width="3" height="3" stroke="currentColor" />
<rect x="20.5" y="15.5" width="3" height="3" stroke="currentColor" />
<rect x="20.5" y="20.5" width="3" height="3" stroke="currentColor" />
</svg>
</Match>
</Switch>

View File

@@ -1,13 +1,39 @@
import { A, useSearchParams } from "@solidjs/router"
import { Title } from "@solidjs/meta"
import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js"
import { PlanIcon, plans } from "./common"
export default function Black() {
const [params] = useSearchParams()
const [selected, setSelected] = createSignal<string | null>((params.plan as string) || null)
const [mounted, setMounted] = createSignal(false)
const selectedPlan = createMemo(() => plans.find((p) => p.id === selected()))
onMount(() => {
requestAnimationFrame(() => setMounted(true))
})
const transition = (action: () => void) => {
if (mounted() && "startViewTransition" in document) {
;(document as any).startViewTransition(action)
return
}
action()
}
const select = (planId: string) => {
if (selected() === planId) {
return
}
transition(() => setSelected(planId))
}
const cancel = () => {
transition(() => setSelected(null))
}
return (
<>
<Title>opencode</Title>
@@ -17,7 +43,12 @@ export default function Black() {
<div data-slot="pricing">
<For each={plans}>
{(plan) => (
<button type="button" onClick={() => setSelected(plan.id)} data-slot="pricing-card">
<button
type="button"
onClick={() => select(plan.id)}
data-slot="pricing-card"
style={{ "view-transition-name": `card-${plan.id}` }}
>
<div data-slot="icon">
<PlanIcon plan={plan.id} />
</div>
@@ -31,14 +62,11 @@ export default function Black() {
)}
</For>
</div>
<p data-slot="fine-print">
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
</p>
</Match>
<Match when={selectedPlan()}>
{(plan) => (
<div data-slot="selected-plan">
<div data-slot="selected-card">
<div data-slot="selected-card" style={{ "view-transition-name": `card-${plan().id}` }}>
<div data-slot="icon">
<PlanIcon plan={plan().id} />
</div>
@@ -49,7 +77,7 @@ export default function Black() {
<span data-slot="multiplier">{plan().multiplier}</span>
</Show>
</p>
<ul data-slot="terms">
<ul data-slot="terms" style={{ "view-transition-name": `terms-${plan().id}` }}>
<li>Your subscription will not start immediately</li>
<li>You will be added to the waitlist and activated soon</li>
<li>Your card will be only charged when your subscription is activated</li>
@@ -58,8 +86,8 @@ export default function Black() {
<li>Limits may be adjusted and plans may be discontinued in the future</li>
<li>Cancel your subscription at anytime</li>
</ul>
<div data-slot="actions">
<button type="button" onClick={() => setSelected(null)} data-slot="cancel">
<div data-slot="actions" style={{ "view-transition-name": `actions-${plan().id}` }}>
<button type="button" onClick={() => cancel()} data-slot="cancel">
Cancel
</button>
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
@@ -67,13 +95,13 @@ export default function Black() {
</a>
</div>
</div>
<p data-slot="fine-print">
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
</p>
</div>
)}
</Match>
</Switch>
<p data-slot="fine-print" style={{ "view-transition-name": "fine-print" }}>
Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A>
</p>
</section>
</>
)

View File

@@ -18,10 +18,10 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
const getWorkspaces = query(async () => {
const getWorkspaces = query(async (plan: string) => {
"use server"
const actor = await getActor()
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe")
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
return withActor(async () => {
return Database.use((tx) =>
tx
@@ -258,15 +258,16 @@ function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data
}
export default function BlackSubscribe() {
const workspaces = createAsync(() => getWorkspaces())
const params = useParams()
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
const plan = planData.id
const workspaces = createAsync(() => getWorkspaces(plan))
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
const [failure, setFailure] = createSignal<string | undefined>(undefined)
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
const params = useParams()
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
const plan = planData.id
// Resolve stripe promise once
createEffect(() => {

View File

@@ -0,0 +1,477 @@
::selection {
background: var(--color-background-interactive);
color: var(--color-text-strong);
@media (prefers-color-scheme: dark) {
background: var(--color-background-interactive);
color: var(--color-text-inverted);
}
}
[data-page="changelog"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
--color-background-weak-hover: hsl(0, 8%, 94%);
--color-background-strong: hsl(0, 5%, 12%);
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
--color-text-weaker: hsl(30, 2%, 81%);
--color-text-strong: hsl(0, 5%, 12%);
--color-text-inverted: hsl(0, 20%, 99%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-icon: hsl(0, 1%, 55%);
background: var(--color-background);
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
--color-background-weak: hsl(0, 6%, 10%);
--color-background-weak-hover: hsl(0, 6%, 15%);
--color-background-strong: hsl(0, 15%, 94%);
--color-background-strong-hover: hsl(0, 15%, 97%);
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
--color-text-strong: hsl(0, 15%, 94%);
--color-text-inverted: hsl(0, 9%, 7%);
--color-border: hsl(0, 3%, 28%);
--color-border-weak: hsl(0, 4%, 23%);
--color-icon: hsl(10, 3%, 43%);
}
/* Header styles - copied from download */
[data-component="top"] {
padding: 24px 5rem;
height: 80px;
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-background);
border-bottom: 1px solid var(--color-border-weak);
z-index: 10;
@media (max-width: 60rem) {
padding: 24px 1.5rem;
}
img {
height: 34px;
width: auto;
}
[data-component="nav-desktop"] {
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
text-decoration: none;
span {
color: var(--color-text-weak);
}
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@media (max-width: 40rem) {
display: none;
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
outline: none;
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
background: var(--color-background-weak);
}
[data-component="nav-mobile"] {
display: none;
@media (max-width: 40rem) {
display: block;
[data-component="nav-mobile-icon"] {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
[data-component="nav-mobile-menu-list"] {
position: fixed;
background: var(--color-background);
top: 80px;
left: 0;
right: 0;
height: 100vh;
ul {
list-style: none;
padding: 20px 0;
li {
a {
text-decoration: none;
padding: 20px;
display: block;
span {
color: var(--color-text-weak);
}
}
a:hover {
background: var(--color-background-weak);
}
}
}
}
}
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
}
[data-component="footer"] {
border-top: 1px solid var(--color-border-weak);
display: flex;
flex-direction: row;
margin-top: 4rem;
@media (max-width: 65rem) {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
width: 100%;
display: block;
span {
color: var(--color-text-weak);
@media (max-width: 40rem) {
display: none;
}
}
}
a:hover {
background: var(--color-background-weak);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
@media (max-width: 25rem) {
flex-wrap: wrap;
[data-slot="cell"] {
flex: 1 0 100%;
border-left: none;
border-top: 1px solid var(--color-border-weak);
}
[data-slot="cell"]:nth-child(1) {
border-top: none;
}
}
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
border: 1px solid var(--color-border-weak);
border-top: none;
@media (max-width: 65rem) {
border: none;
}
}
[data-component="content"] {
padding: 6rem 5rem;
@media (max-width: 60rem) {
padding: 4rem 1.5rem;
}
}
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
padding: 2rem 5rem;
display: flex;
gap: 32px;
justify-content: center;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
/* Changelog Hero */
[data-component="changelog-hero"] {
margin-bottom: 4rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border-weak);
@media (max-width: 50rem) {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 8px;
}
p {
color: var(--color-text);
}
}
/* Releases */
[data-component="releases"] {
display: flex;
flex-direction: column;
gap: 0;
}
[data-component="release"] {
display: grid;
grid-template-columns: 180px 1fr;
gap: 3rem;
padding: 2rem 0;
border-bottom: 1px solid var(--color-border-weak);
@media (max-width: 50rem) {
grid-template-columns: 1fr;
gap: 1rem;
}
&:first-child {
padding-top: 0;
}
&:last-child {
border-bottom: none;
}
header {
display: flex;
flex-direction: column;
gap: 4px;
@media (max-width: 50rem) {
flex-direction: row;
align-items: center;
gap: 12px;
}
[data-slot="version"] {
a {
font-weight: 600;
color: var(--color-text-strong);
text-decoration: none;
&:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
time {
color: var(--color-text-weak);
font-size: 14px;
}
}
[data-slot="content"] {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
[data-component="section"] {
h3 {
font-size: 14px;
font-weight: 600;
color: var(--color-text-strong);
margin-bottom: 8px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
li {
color: var(--color-text);
line-height: 1.5;
padding-left: 16px;
position: relative;
&::before {
content: "-";
position: absolute;
left: 0;
color: var(--color-text-weak);
}
[data-slot="author"] {
color: var(--color-text-weak);
font-size: 13px;
margin-left: 4px;
text-decoration: none;
&:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
}
}
[data-component="contributors"] {
font-size: 13px;
color: var(--color-text-weak);
padding-top: 0.5rem;
span {
color: var(--color-text-weak);
}
a {
color: var(--color-text);
text-decoration: none;
&:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
}
a {
color: var(--color-text-strong);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
&:hover {
text-decoration-thickness: 2px;
}
}
}

View File

@@ -0,0 +1,147 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
import { createAsync, query } from "@solidjs/router"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { config } from "~/config"
import { For, Show } from "solid-js"
type Release = {
tag_name: string
name: string
body: string
published_at: string
html_url: string
}
const getReleases = query(async () => {
"use server"
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "OpenCode-Console",
},
cf: {
cacheTtl: 60 * 5,
cacheEverything: true,
},
} as any)
if (!response.ok) return []
return response.json() as Promise<Release[]>
}, "releases.get")
function formatDate(dateString: string) {
const date = new Date(dateString)
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})
}
function parseMarkdown(body: string) {
const lines = body.split("\n")
const sections: { title: string; items: string[] }[] = []
let current: { title: string; items: string[] } | null = null
let skip = false
for (const line of lines) {
if (line.startsWith("## ")) {
if (current) sections.push(current)
const title = line.slice(3).trim()
current = { title, items: [] }
skip = false
} else if (line.startsWith("**Thank you")) {
skip = true
} else if (line.startsWith("- ") && !skip) {
current?.items.push(line.slice(2).trim())
}
}
if (current) sections.push(current)
return { sections }
}
function ReleaseItem(props: { item: string }) {
const parts = () => {
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
if (match) {
return {
text: match[1],
username: match[3],
}
}
return { text: props.item, username: undefined }
}
return (
<li>
<span>{parts().text}</span>
<Show when={parts().username}>
<a data-slot="author" href={`https://github.com/${parts().username}`} target="_blank" rel="noopener noreferrer">
(@{parts().username})
</a>
</Show>
</li>
)
}
export default function Changelog() {
const releases = createAsync(() => getReleases())
return (
<main data-page="changelog">
<Title>OpenCode | Changelog</Title>
<Link rel="canonical" href={`${config.baseUrl}/changelog`} />
<Meta name="description" content="OpenCode release notes and changelog" />
<div data-component="container">
<Header hideGetStarted />
<div data-component="content">
<section data-component="changelog-hero">
<h1>Changelog</h1>
<p>New updates and improvements to OpenCode</p>
</section>
<section data-component="releases">
<For each={releases()}>
{(release) => {
const parsed = () => parseMarkdown(release.body || "")
return (
<article data-component="release">
<header>
<div data-slot="version">
<a href={release.html_url} target="_blank" rel="noopener noreferrer">
{release.tag_name}
</a>
</div>
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
</header>
<div data-slot="content">
<For each={parsed().sections}>
{(section) => (
<div data-component="section">
<h3>{section.title}</h3>
<ul>
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
</ul>
</div>
)}
</For>
</div>
</article>
)
}}
</For>
</section>
<Footer />
</div>
</div>
<Legal />
</main>
)
}

View File

@@ -34,7 +34,6 @@
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
overflow-x: hidden;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);

View File

@@ -441,7 +441,8 @@ export default function Download() {
</li>
<li>
<Faq question="Can I only use OpenCode in the terminal?">
Not anymore! OpenCode is now available as an app for your desktop.
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
<a href="/docs/cli/#web">web</a>!
</Faq>
</li>
<li>

View File

@@ -692,7 +692,8 @@ export default function Home() {
</li>
<li>
<Faq question="Can I only use OpenCode in the terminal?">
Not anymore! OpenCode is now available as an app for your desktop.
Not anymore! OpenCode is now available as an app for your <a href="/download">desktop</a> and{" "}
<a href="/docs/web">web</a>!
</Faq>
</li>
<li>

View File

@@ -183,7 +183,12 @@ export async function POST(input: APIEvent) {
.set({
customerID,
subscriptionID,
subscriptionCouponID: couponID,
subscription: {
status: "subscribed",
coupon: couponID,
seats: 1,
plan: "200",
},
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
@@ -408,7 +413,7 @@ export async function POST(input: APIEvent) {
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({ subscriptionID: null, subscriptionCouponID: null })
.set({ subscriptionID: null, subscription: null })
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))

View File

@@ -5,4 +5,58 @@
align-items: center;
gap: var(--space-4);
}
[data-slot="usage"] {
display: flex;
gap: var(--space-6);
margin-top: var(--space-4);
@media (max-width: 40rem) {
flex-direction: column;
gap: var(--space-4);
}
}
[data-slot="usage-item"] {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
[data-slot="usage-header"] {
display: flex;
justify-content: space-between;
align-items: baseline;
}
[data-slot="usage-label"] {
font-size: var(--font-size-md);
font-weight: 500;
color: var(--color-text);
}
[data-slot="usage-value"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
[data-slot="progress"] {
height: 8px;
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
[data-slot="progress-bar"] {
height: 100%;
background-color: var(--color-accent);
border-radius: var(--border-radius-sm);
transition: width 0.3s ease;
}
[data-slot="reset-time"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}

View File

@@ -1,10 +1,58 @@
import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { Show } from "solid-js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Black } from "@opencode-ai/console-core/black.js"
import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./black-section.module.css"
const querySubscription = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const row = await Database.use((tx) =>
tx
.select({
rollingUsage: SubscriptionTable.rollingUsage,
fixedUsage: SubscriptionTable.fixedUsage,
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
})
.from(SubscriptionTable)
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
.then((r) => r[0]),
)
if (!row) return null
return {
rollingUsage: Black.analyzeRollingUsage({
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? new Date(),
}),
weeklyUsage: Black.analyzeWeeklyUsage({
usage: row.fixedUsage ?? 0,
timeUpdated: row.timeFixedUpdated ?? new Date(),
}),
}
}, workspaceID)
}, "subscription.get")
function formatResetTime(seconds: number) {
const days = Math.floor(seconds / 86400)
if (days >= 1) {
const hours = Math.floor((seconds % 86400) / 3600)
return `${days} ${days === 1 ? "day" : "days"} ${hours} ${hours === 1 ? "hour" : "hours"}`
}
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours >= 1) return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`
if (minutes === 0) return "a few seconds"
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
}
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
"use server"
return json(
@@ -26,6 +74,7 @@ export function BlackSection() {
const params = useParams()
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
const subscription = createAsync(() => querySubscription(params.id!))
const [store, setStore] = createStore({
sessionRedirecting: false,
})
@@ -53,6 +102,32 @@ export function BlackSection() {
</button>
</div>
</div>
<Show when={subscription()}>
{(sub) => (
<div data-slot="usage">
<div data-slot="usage-item">
<div data-slot="usage-header">
<span data-slot="usage-label">5-hour Usage</span>
<span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
</div>
<div data-slot="progress">
<div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
</div>
<span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
</div>
<div data-slot="usage-item">
<div data-slot="usage-header">
<span data-slot="usage-label">Weekly Usage</span>
<span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
</div>
<div data-slot="progress">
<div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
</div>
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
</div>
</div>
)}
</Show>
</section>
)
}

View File

@@ -3,7 +3,6 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
import { action, json, query } from "@solidjs/router"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { User } from "@opencode-ai/console-core/user.js"
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
@@ -96,11 +95,22 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
return withActor(async () => {
const billing = await Billing.get()
return {
...billing,
customerID: billing.customerID,
paymentMethodID: billing.paymentMethodID,
paymentMethodType: billing.paymentMethodType,
paymentMethodLast4: billing.paymentMethodLast4,
balance: billing.balance,
reload: billing.reload,
reloadAmount: billing.reloadAmount ?? Billing.RELOAD_AMOUNT,
reloadAmountMin: Billing.RELOAD_AMOUNT_MIN,
reloadTrigger: billing.reloadTrigger ?? Billing.RELOAD_TRIGGER,
reloadTriggerMin: Billing.RELOAD_TRIGGER_MIN,
monthlyLimit: billing.monthlyLimit,
monthlyUsage: billing.monthlyUsage,
timeMonthlyUsageUpdated: billing.timeMonthlyUsageUpdated,
reloadError: billing.reloadError,
timeReloadError: billing.timeReloadError,
subscriptionID: billing.subscriptionID,
}
}, workspaceID)
}, "billing.get")

View File

@@ -9,7 +9,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { BlackData } from "@opencode-ai/console-core/black.js"
import { Black, BlackData } from "@opencode-ai/console-core/black.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
@@ -81,12 +81,13 @@ export async function handler(
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
const stickyProvider = await stickyTracker?.get()
const authInfo = await authenticate(modelInfo)
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(
model,
zenData,
authInfo,
modelInfo,
@@ -101,7 +102,7 @@ export async function handler(
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
@@ -135,7 +136,7 @@ export async function handler(
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
res.status !== 404 &&
// ie. cannot change codex model providers mid-session
!modelInfo.stickyProvider &&
modelInfo.stickyProvider !== "strict" &&
modelInfo.fallbackProvider &&
providerInfo.id !== modelInfo.fallbackProvider
) {
@@ -194,17 +195,19 @@ export async function handler(
// Handle streaming response
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
const usageParser = providerInfo.createUsageParser()
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
const decoder = new TextDecoder()
const encoder = new TextEncoder()
let buffer = ""
let responseLength = 0
function pump(): Promise<void> {
return (
reader?.read().then(async ({ done, value }) => {
reader?.read().then(async ({ done, value: rawValue }) => {
if (done) {
logger.metric({
response_length: responseLength,
@@ -230,6 +233,10 @@ export async function handler(
"timestamp.first_byte": now,
})
}
const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
if (!value) return
responseLength += value.length
buffer += decoder.decode(value, { stream: true })
dataDumper?.provideStream(buffer)
@@ -331,6 +338,7 @@ export async function handler(
}
function selectProvider(
reqModel: string,
zenData: ZenData,
authInfo: AuthInfo,
modelInfo: ModelInfo,
@@ -339,7 +347,7 @@ export async function handler(
retry: RetryOptions,
stickyProvider: string | undefined,
) {
const provider = (() => {
const modelProvider = (() => {
if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
}
@@ -372,18 +380,19 @@ export async function handler(
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`)
if (!modelProvider) throw new ModelError("No provider available")
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
return {
...provider,
...zenData.providers[provider.id],
...modelProvider,
...zenData.providers[modelProvider.id],
...(() => {
const format = zenData.providers[provider.id].format
if (format === "anthropic") return anthropicHelper
if (format === "google") return googleHelper
if (format === "openai") return openaiHelper
return oaCompatHelper
const format = zenData.providers[modelProvider.id].format
const providerModel = modelProvider.model
if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
if (format === "google") return googleHelper({ reqModel, providerModel })
if (format === "openai") return openaiHelper({ reqModel, providerModel })
return oaCompatHelper({ reqModel, providerModel })
})(),
}
}
@@ -495,27 +504,28 @@ export async function handler(
// Check weekly limit
if (sub.fixedUsage && sub.timeFixedUpdated) {
const week = getWeekBounds(now)
if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
const result = Black.analyzeWeeklyUsage({
usage: sub.fixedUsage,
timeUpdated: sub.timeFixedUpdated,
})
if (result.status === "rate-limited")
throw new SubscriptionError(
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
retryAfter,
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
result.resetInSec,
)
}
}
// Check rolling limit
if (sub.rollingUsage && sub.timeRollingUpdated) {
const rollingWindowMs = black.rollingWindow * 3600 * 1000
const windowStart = new Date(now.getTime() - rollingWindowMs)
if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
const result = Black.analyzeRollingUsage({
usage: sub.rollingUsage,
timeUpdated: sub.timeRollingUpdated,
})
if (result.status === "rate-limited")
throw new SubscriptionError(
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
retryAfter,
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
result.resetInSec,
)
}
}
return

View File

@@ -1,4 +1,6 @@
import { EventStreamCodec } from "@smithy/eventstream-codec"
import { ProviderHelper, CommonRequest, CommonResponse, CommonChunk } from "./provider"
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
type Usage = {
cache_creation?: {
@@ -14,65 +16,169 @@ type Usage = {
}
}
export const anthropicHelper = {
format: "anthropic",
modifyUrl: (providerApi: string) => providerApi + "/messages",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("x-api-key", apiKey)
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
if (body.model.startsWith("claude-sonnet-")) {
headers.set("anthropic-beta", "context-1m-2025-08-07")
}
},
modifyBody: (body: Record<string, any>) => {
return {
export const anthropicHelper: ProviderHelper = ({ reqModel, providerModel }) => {
const isBedrockModelArn = providerModel.startsWith("arn:aws:bedrock:")
const isBedrockModelID = providerModel.startsWith("global.anthropic.")
const isBedrock = isBedrockModelArn || isBedrockModelID
const isSonnet = reqModel.includes("sonnet")
return {
format: "anthropic",
modifyUrl: (providerApi: string, isStream?: boolean) =>
isBedrock
? `${providerApi}/model/${isBedrockModelArn ? encodeURIComponent(providerModel) : providerModel}/${isStream ? "invoke-with-response-stream" : "invoke"}`
: providerApi + "/messages",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
if (isBedrock) {
headers.set("Authorization", `Bearer ${apiKey}`)
} else {
headers.set("x-api-key", apiKey)
headers.set("anthropic-version", headers.get("anthropic-version") ?? "2023-06-01")
if (body.model.startsWith("claude-sonnet-")) {
headers.set("anthropic-beta", "context-1m-2025-08-07")
}
}
},
modifyBody: (body: Record<string, any>) => ({
...body,
service_tier: "standard_only",
}
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
...(isBedrock
? {
anthropic_version: "bedrock-2023-05-31",
anthropic_beta: isSonnet ? "context-1m-2025-08-07" : undefined,
model: undefined,
stream: undefined,
}
: {
service_tier: "standard_only",
}),
}),
createBinaryStreamDecoder: () => {
if (!isBedrock) return undefined
return {
parse: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
const decoder = new TextDecoder()
const encoder = new TextEncoder()
const codec = new EventStreamCodec(toUtf8, fromUtf8)
let buffer = new Uint8Array(0)
return (value: Uint8Array) => {
const newBuffer = new Uint8Array(buffer.length + value.length)
newBuffer.set(buffer)
newBuffer.set(value, buffer.length)
buffer = newBuffer
let json
try {
json = JSON.parse(data.slice(6))
} catch (e) {
return
const messages = []
while (buffer.length >= 4) {
// first 4 bytes are the total length (big-endian)
const totalLength = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint32(0, false)
// wait for more chunks
if (buffer.length < totalLength) break
try {
const subView = buffer.subarray(0, totalLength)
const decoded = codec.decode(subView)
buffer = buffer.slice(totalLength)
/* Example of Bedrock data
```
{
bytes: 'eyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ19iZHJrXzAxMjVGdHRGb2lkNGlwWmZ4SzZMbktxeCIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOltdLCJzdG9wX3JlYXNvbiI6bnVsbCwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjEsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjoxMTk2MywiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MSwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjF9fX0=',
p: '...'
}
```
const usageUpdate = json.usage ?? json.message?.usage
if (!usageUpdate) return
usage = {
...usage,
...usageUpdate,
cache_creation: {
...usage?.cache_creation,
...usageUpdate.cache_creation,
},
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
Decoded bytes
```
{
type: 'message_start',
message: {
model: 'claude-opus-4-5-20251101',
id: 'msg_bdrk_0125FttFoid4ipZfxK6LnKqx',
type: 'message',
role: 'assistant',
content: [],
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: 4,
cache_creation_input_tokens: 1,
cache_read_input_tokens: 11963,
cache_creation: [Object],
output_tokens: 1
}
}
}
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => ({
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
reasoningTokens: undefined,
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
}),
} satisfies ProviderHelper
```
*/
/* Example of Anthropic data
```
event: message_delta
data: {"type":"message_start","message":{"model":"claude-opus-4-5-20251101","id":"msg_01ETvwVWSKULxzPdkQ1xAnk2","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":11543,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":11543,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}}}
```
*/
if (decoded.headers[":message-type"]?.value === "event") {
const data = decoder.decode(decoded.body, { stream: true })
const parsedDataResult = JSON.parse(data)
delete parsedDataResult.p
const binary = atob(parsedDataResult.bytes)
const uint8 = Uint8Array.from(binary, (c) => c.charCodeAt(0))
const bytes = decoder.decode(uint8)
const eventName = JSON.parse(bytes).type
messages.push([`event: ${eventName}`, "\n", `data: ${bytes}`, "\n\n"].join(""))
}
} catch (e) {
console.log("@@@EE@@@")
console.log(e)
break
}
}
return encoder.encode(messages.join(""))
}
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
const data = chunk.split("\n")[1]
if (!data.startsWith("data: ")) return
let json
try {
json = JSON.parse(data.slice(6))
} catch (e) {
return
}
const usageUpdate = json.usage ?? json.message?.usage
if (!usageUpdate) return
usage = {
...usage,
...usageUpdate,
cache_creation: {
...usage?.cache_creation,
...usageUpdate.cache_creation,
},
server_tool_use: {
...usage?.server_tool_use,
...usageUpdate.server_tool_use,
},
}
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => ({
inputTokens: usage.input_tokens ?? 0,
outputTokens: usage.output_tokens ?? 0,
reasoningTokens: undefined,
cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
}),
}
}
export function fromAnthropicRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body

View File

@@ -26,16 +26,17 @@ type Usage = {
thoughtsTokenCount?: number
}
export const googleHelper = {
export const googleHelper: ProviderHelper = ({ providerModel }) => ({
format: "google",
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
modifyUrl: (providerApi: string, isStream?: boolean) =>
`${providerApi}/models/${providerModel}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("x-goog-api-key", apiKey)
},
modifyBody: (body: Record<string, any>) => {
return body
},
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\r\n\r\n",
createUsageParser: () => {
let usage: Usage
@@ -71,4 +72,4 @@ export const googleHelper = {
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
})

View File

@@ -21,7 +21,7 @@ type Usage = {
}
}
export const oaCompatHelper = {
export const oaCompatHelper: ProviderHelper = () => ({
format: "oa-compat",
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
...(body.stream ? { stream_options: { include_usage: true } } : {}),
}
},
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
@@ -68,7 +69,7 @@ export const oaCompatHelper = {
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
})
export function fromOaCompatibleRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body

View File

@@ -12,7 +12,7 @@ type Usage = {
total_tokens?: number
}
export const openaiHelper = {
export const openaiHelper: ProviderHelper = () => ({
format: "openai",
modifyUrl: (providerApi: string) => providerApi + "/responses",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@@ -21,6 +21,7 @@ export const openaiHelper = {
modifyBody: (body: Record<string, any>) => {
return body
},
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
@@ -58,7 +59,7 @@ export const openaiHelper = {
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
})
export function fromOpenaiRequest(body: any): CommonRequest {
if (!body || typeof body !== "object") return body

View File

@@ -33,11 +33,12 @@ export type UsageInfo = {
cacheWrite1hTokens?: number
}
export type ProviderHelper = {
export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
format: ZenData.Format
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
modifyUrl: (providerApi: string, isStream?: boolean) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any>
createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
streamSeparator: string
createUsageParser: () => {
parse: (chunk: string) => void

View File

@@ -1,6 +1,6 @@
import { Resource } from "@opencode-ai/console-resource"
export function createStickyTracker(stickyProvider: boolean, session: string) {
export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
if (!stickyProvider) return
if (!session) return
const key = `sticky:${session}`

View File

@@ -12,7 +12,7 @@
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vite/client"],
"types": ["vite/client", "@webgpu/types"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]

View File

@@ -0,0 +1 @@
ALTER TABLE `billing` ADD `subscription` json;

View File

@@ -0,0 +1 @@
ALTER TABLE `billing` DROP COLUMN `subscription_coupon_id`;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -372,6 +372,20 @@
"when": 1768343920467,
"tag": "0052_aromatic_agent_zero",
"breakpoints": true
},
{
"idx": 53,
"version": "5",
"when": 1768599366758,
"tag": "0053_gigantic_hardball",
"breakpoints": true
},
{
"idx": 54,
"version": "5",
"when": 1768603665356,
"tag": "0054_numerous_annihilus",
"breakpoints": true
}
]
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.1.19",
"version": "1.1.25",
"private": true,
"type": "module",
"license": "MIT",
@@ -32,6 +32,7 @@
"promote-models-to-dev": "script/promote-models.ts dev",
"promote-models-to-prod": "script/promote-models.ts production",
"pull-models-from-dev": "script/pull-models.ts dev",
"pull-models-from-prod": "script/pull-models.ts production",
"update-black": "script/update-black.ts",
"promote-black-to-dev": "script/promote-black.ts dev",
"promote-black-to-prod": "script/promote-black.ts production",

View File

@@ -0,0 +1,41 @@
import { subscribe } from "diagnostics_channel"
import { Billing } from "../src/billing.js"
import { and, Database, eq } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const workspaceID = process.argv[2]
if (!workspaceID) {
console.error("Usage: bun script/foo.ts <workspaceID>")
process.exit(1)
}
console.log(`Removing from Black waitlist`)
const billing = await Database.use((tx) =>
tx
.select({
subscriptionPlan: BillingTable.subscriptionPlan,
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
if (!billing?.timeSubscriptionBooked) {
console.error(`Error: Workspace is not on the waitlist`)
process.exit(1)
}
await Database.use((tx) =>
tx
.update(BillingTable)
.set({
subscriptionPlan: null,
timeSubscriptionBooked: null,
})
.where(eq(BillingTable.workspaceID, workspaceID)),
)
console.log(`Done`)

View File

@@ -0,0 +1,112 @@
import { Billing } from "../src/billing.js"
import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
import { UserTable } from "../src/schema/user.sql.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
import { Identifier } from "../src/identifier.js"
import { centsToMicroCents } from "../src/util/price.js"
import { AuthTable } from "../src/schema/auth.sql.js"
const plan = "200"
const workspaceID = process.argv[2]
const seats = parseInt(process.argv[3])
console.log(`Gifting ${seats} seats of Black to workspace ${workspaceID}`)
if (!workspaceID || !seats) throw new Error("Usage: bun foo.ts <workspaceID> <seats>")
// Get workspace user
const users = await Database.use((tx) =>
tx
.select({
id: UserTable.id,
role: UserTable.role,
email: AuthTable.subject,
})
.from(UserTable)
.leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
)
if (users.length === 0) throw new Error(`Error: No users found in workspace ${workspaceID}`)
if (users.length !== seats)
throw new Error(`Error: Workspace ${workspaceID} has ${users.length} users, expected ${seats}`)
const adminUser = users.find((user) => user.role === "admin")
if (!adminUser) throw new Error(`Error: No admin user found in workspace ${workspaceID}`)
if (!adminUser.email) throw new Error(`Error: Admin user ${adminUser.id} has no email`)
// Get Billing
const billing = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
if (!billing) throw new Error(`Error: Workspace ${workspaceID} has no billing record`)
if (billing.subscriptionID) throw new Error(`Error: Workspace ${workspaceID} already has a subscription`)
// Look up the Stripe customer by email
const customerID =
billing.customerID ??
(await (() =>
Billing.stripe()
.customers.create({
email: adminUser.email,
metadata: {
workspaceID,
},
})
.then((customer) => customer.id))())
console.log(`Customer ID: ${customerID}`)
const couponID = "JAIr0Pe1"
const subscription = await Billing.stripe().subscriptions.create({
customer: customerID!,
items: [
{
price: `price_1SmfyI2StuRr0lbXovxJNeZn`,
discounts: [{ coupon: couponID }],
quantity: 2,
},
],
})
console.log(`Subscription ID: ${subscription.id}`)
await Database.transaction(async (tx) => {
// Set customer id, subscription id, and payment method on workspace billing
await tx
.update(BillingTable)
.set({
customerID,
subscriptionID: subscription.id,
subscription: { status: "subscribed", coupon: couponID, seats, plan },
})
.where(eq(BillingTable.workspaceID, workspaceID))
// Create a row in subscription table
for (const user of users) {
await tx.insert(SubscriptionTable).values({
workspaceID,
id: Identifier.create("subscription"),
userID: user.id,
})
}
//
// // Create a row in payments table
// await tx.insert(PaymentTable).values({
// workspaceID,
// id: Identifier.create("payment"),
// amount: centsToMicroCents(amountInCents),
// customerID,
// invoiceID,
// paymentID,
// enrichment: {
// type: "subscription",
// couponID,
// },
// })
})
console.log(`done`)

View File

@@ -12,7 +12,7 @@ const email = process.argv[3]
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
if (!workspaceID || !email) {
console.error("Usage: bun onboard-zen-black.ts <workspaceID> <email>")
console.error("Usage: bun foo.ts <workspaceID> <email>")
process.exit(1)
}
@@ -50,7 +50,7 @@ const existingSubscription = await Database.use((tx) =>
tx
.select({ workspaceID: BillingTable.workspaceID })
.from(BillingTable)
.where(eq(BillingTable.subscriptionID, subscriptionID))
.where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
.then((rows) => rows[0]),
)
if (existingSubscription) {
@@ -128,10 +128,15 @@ await Database.transaction(async (tx) => {
.set({
customerID,
subscriptionID,
subscriptionCouponID: couponID,
paymentMethodID,
paymentMethodLast4,
paymentMethodType,
subscription: {
status: "subscribed",
coupon: couponID,
seats: 1,
plan: "200",
},
})
.where(eq(BillingTable.workspaceID, workspaceID))

View File

@@ -0,0 +1,163 @@
import { Billing } from "../src/billing.js"
import { and, Database, desc, eq, isNotNull, lt, sql } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const fromWrkID = process.argv[2]
const toWrkID = process.argv[3]
if (!fromWrkID || !toWrkID) {
console.error("Usage: bun foo.ts <fromWrkID> <toWrkID>")
process.exit(1)
}
console.log(`Transferring subscription from ${fromWrkID} to ${toWrkID}`)
// Look up the FROM workspace billing
const fromBilling = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
subscription: BillingTable.subscription,
paymentMethodID: BillingTable.paymentMethodID,
paymentMethodType: BillingTable.paymentMethodType,
paymentMethodLast4: BillingTable.paymentMethodLast4,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, fromWrkID))
.then((rows) => rows[0]),
)
if (!fromBilling) throw new Error(`Error: FROM workspace has no billing record`)
if (!fromBilling.customerID) throw new Error(`Error: FROM workspace has no Stripe customer ID`)
if (!fromBilling.subscriptionID) throw new Error(`Error: FROM workspace has no subscription`)
const fromSubscription = await Database.use((tx) =>
tx
.select({ userID: SubscriptionTable.userID })
.from(SubscriptionTable)
.where(eq(SubscriptionTable.workspaceID, fromWrkID))
.then((rows) => rows[0]),
)
if (!fromSubscription) throw new Error(`Error: FROM workspace has no subscription`)
// Look up the previous customer ID in FROM workspace
const subscriptionPayment = await Database.use((tx) =>
tx
.select({
customerID: PaymentTable.customerID,
timeCreated: PaymentTable.timeCreated,
})
.from(PaymentTable)
.where(and(eq(PaymentTable.workspaceID, fromWrkID), sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`))
.then((rows) => {
if (rows.length > 1) {
console.error(`Error: Multiple subscription payments found for workspace ${fromWrkID}`)
process.exit(1)
}
return rows[0]
}),
)
const fromPrevPayment = await Database.use((tx) =>
tx
.select({ customerID: PaymentTable.customerID })
.from(PaymentTable)
.where(
and(
eq(PaymentTable.workspaceID, fromWrkID),
isNotNull(PaymentTable.customerID),
lt(PaymentTable.timeCreated, subscriptionPayment.timeCreated),
),
)
.orderBy(desc(PaymentTable.timeCreated))
.limit(1)
.then((rows) => rows[0]),
)
if (!fromPrevPayment?.customerID) throw new Error(`Error: FROM workspace has no previous Stripe customer to revert to`)
if (fromPrevPayment.customerID === fromBilling.customerID)
throw new Error(`Error: FROM workspace has the same Stripe customer ID as the current one`)
const fromPrevPaymentMethods = await Billing.stripe().customers.listPaymentMethods(fromPrevPayment.customerID, {})
if (fromPrevPaymentMethods.data.length === 0)
throw new Error(`Error: FROM workspace has no previous Stripe payment methods`)
// Look up the TO workspace billing
const toBilling = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, toWrkID))
.then((rows) => rows[0]),
)
if (!toBilling) throw new Error(`Error: TO workspace has no billing record`)
if (toBilling.subscriptionID) throw new Error(`Error: TO workspace already has a subscription`)
console.log(`FROM:`)
console.log(` Old Customer ID: ${fromBilling.customerID}`)
console.log(` New Customer ID: ${fromPrevPayment.customerID}`)
console.log(`TO:`)
console.log(` Old Customer ID: ${toBilling.customerID}`)
console.log(` New Customer ID: ${fromBilling.customerID}`)
// Clear workspaceID from Stripe customer metadata
await Billing.stripe().customers.update(fromPrevPayment.customerID, {
metadata: {
workspaceID: fromWrkID,
},
})
await Billing.stripe().customers.update(fromBilling.customerID, {
metadata: {
workspaceID: toWrkID,
},
})
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
customerID: fromPrevPayment.customerID,
subscriptionID: null,
subscription: null,
paymentMethodID: fromPrevPaymentMethods.data[0].id,
paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null,
paymentMethodType: fromPrevPaymentMethods.data[0].type,
})
.where(eq(BillingTable.workspaceID, fromWrkID))
await tx
.update(BillingTable)
.set({
customerID: fromBilling.customerID,
subscriptionID: fromBilling.subscriptionID,
subscription: fromBilling.subscription,
paymentMethodID: fromBilling.paymentMethodID,
paymentMethodLast4: fromBilling.paymentMethodLast4,
paymentMethodType: fromBilling.paymentMethodType,
})
.where(eq(BillingTable.workspaceID, toWrkID))
await tx
.update(SubscriptionTable)
.set({
workspaceID: toWrkID,
userID: fromSubscription.userID,
})
.where(eq(SubscriptionTable.workspaceID, fromWrkID))
await tx
.update(PaymentTable)
.set({
workspaceID: toWrkID,
})
.where(
and(
eq(PaymentTable.workspaceID, fromWrkID),
sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`,
eq(PaymentTable.amount, 20000000000),
),
)
})
console.log(`done`)

View File

@@ -1,4 +1,6 @@
import { Billing } from "../src/billing.js"
import { Database, eq } from "../src/drizzle/index.js"
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
// get input from command line
const workspaceID = process.argv[2]
@@ -9,6 +11,19 @@ if (!workspaceID || !dollarAmount) {
process.exit(1)
}
// check workspace exists
const workspace = await Database.use((tx) =>
tx
.select()
.from(WorkspaceTable)
.where(eq(WorkspaceTable.id, workspaceID))
.then((rows) => rows[0]),
)
if (!workspace) {
console.error("Error: Workspace not found")
process.exit(1)
}
const amountInDollars = parseFloat(dollarAmount)
if (isNaN(amountInDollars) || amountInDollars <= 0) {
console.error("Error: dollarAmount must be a positive number")

View File

@@ -55,8 +55,9 @@ if (identifier.startsWith("wrk_")) {
),
)
// Get all payments for these workspaces
await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID)))
for (const user of users) {
await printWorkspace(user.workspaceID)
}
}
async function printWorkspace(workspaceID: string) {
@@ -113,8 +114,13 @@ async function printWorkspace(workspaceID: string) {
.select({
balance: BillingTable.balance,
customerID: BillingTable.customerID,
reload: BillingTable.reload,
subscriptionID: BillingTable.subscriptionID,
subscriptionCouponID: BillingTable.subscriptionCouponID,
subscription: {
plan: BillingTable.subscriptionPlan,
booked: BillingTable.timeSubscriptionBooked,
enrichment: BillingTable.subscription,
},
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspace.id))
@@ -123,6 +129,16 @@ async function printWorkspace(workspaceID: string) {
rows.map((row) => ({
...row,
balance: `$${(row.balance / 100000000).toFixed(2)}`,
subscription: row.subscriptionID
? [
`Black ${row.subscription.enrichment!.plan}`,
row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "",
row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "",
`(ref: ${row.subscriptionID})`,
].join(" ")
: row.subscription.booked
? `Waitlist ${row.subscription.plan} plan`
: undefined,
}))[0],
),
)
@@ -133,6 +149,7 @@ async function printWorkspace(workspaceID: string) {
amount: PaymentTable.amount,
paymentID: PaymentTable.paymentID,
invoiceID: PaymentTable.invoiceID,
customerID: PaymentTable.customerID,
timeCreated: PaymentTable.timeCreated,
timeRefunded: PaymentTable.timeRefunded,
})

View File

@@ -8,33 +8,25 @@ const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 8
// read the secret
const ret = await $`bun sst secret list`.cwd(root).text()
const lines = ret.split("\n")
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
if (!value5) throw new Error("ZEN_MODELS5 not found")
if (!value6) throw new Error("ZEN_MODELS6 not found")
if (!value7) throw new Error("ZEN_MODELS7 not found")
const values = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
return value
})
// validate value
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
ZenData.validate(JSON.parse(values.join("")))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} --stage ${stage} -- ${values[i]}`
}

View File

@@ -8,32 +8,25 @@ const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
const PARTS = 8
// read the secret
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
const lines = ret.split("\n")
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
if (!value5) throw new Error("ZEN_MODELS5 not found")
if (!value6) throw new Error("ZEN_MODELS6 not found")
if (!value7) throw new Error("ZEN_MODELS7 not found")
const values = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
return value
})
// validate value
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
ZenData.validate(JSON.parse(values.join("")))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1}`
await $`bun sst secret set ZEN_MODELS2 ${value2}`
await $`bun sst secret set ZEN_MODELS3 ${value3}`
await $`bun sst secret set ZEN_MODELS4 ${value4}`
await $`bun sst secret set ZEN_MODELS5 ${value5}`
await $`bun sst secret set ZEN_MODELS6 ${value6}`
await $`bun sst secret set ZEN_MODELS7 ${value7}`
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${values[i]}`
}

View File

@@ -1,78 +0,0 @@
import { Billing } from "../src/billing.js"
import { and, Database, eq } from "../src/drizzle/index.js"
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
const workspaceID = process.argv[2]
if (!workspaceID) {
console.error("Usage: bun remove-black.ts <workspaceID>")
process.exit(1)
}
console.log(`Removing subscription from workspace ${workspaceID}`)
// Look up the workspace billing
const billing = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
if (!billing) {
console.error(`Error: No billing record found for workspace ${workspaceID}`)
process.exit(1)
}
if (!billing.subscriptionID) {
console.error(`Error: Workspace ${workspaceID} does not have a subscription`)
process.exit(1)
}
console.log(` Customer ID: ${billing.customerID}`)
console.log(` Subscription ID: ${billing.subscriptionID}`)
// Clear workspaceID from Stripe customer metadata
if (billing.customerID) {
//await Billing.stripe().customers.update(billing.customerID, {
// metadata: {
// workspaceID: "",
// },
//})
//console.log(`Cleared workspaceID from Stripe customer metadata`)
}
await Database.transaction(async (tx) => {
// Clear subscription-related fields from billing table
await tx
.update(BillingTable)
.set({
// customerID: null,
subscriptionID: null,
subscriptionCouponID: null,
// paymentMethodID: null,
// paymentMethodLast4: null,
// paymentMethodType: null,
})
.where(eq(BillingTable.workspaceID, workspaceID))
// Delete from subscription table
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
// Delete from payments table
await tx
.delete(PaymentTable)
.where(
and(
eq(PaymentTable.workspaceID, workspaceID),
eq(PaymentTable.enrichment, { type: "subscription" }),
eq(PaymentTable.amount, 20000000000),
),
)
})
console.log(`Successfully removed subscription from workspace ${workspaceID}`)

View File

@@ -7,34 +7,24 @@ import { ZenData } from "../src/model"
const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
const PARTS = 8
// read the line starting with "ZEN_MODELS"
const lines = models.split("\n")
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
const oldValues = Array.from({ length: PARTS }, (_, i) => {
const value = lines
.find((line) => line.startsWith(`ZEN_MODELS${i + 1}`))
?.split("=")
.slice(1)
.join("=")
if (!value) throw new Error(`ZEN_MODELS${i + 1} not found`)
return value
})
// store the prettified json to a temp file
const filename = `models-${Date.now()}.json`
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
await tempFile.write(
JSON.stringify(
JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
null,
2,
),
)
await tempFile.write(JSON.stringify(JSON.parse(oldValues.join("")), null, 2))
console.log("tempFile", tempFile.name)
// open temp file in vim and read the file on close
@@ -43,19 +33,11 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
ZenData.validate(JSON.parse(newValue))
// update the secret
const chunk = Math.ceil(newValue.length / 7)
const newValue1 = newValue.slice(0, chunk)
const newValue2 = newValue.slice(chunk, chunk * 2)
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
const newValue5 = newValue.slice(chunk * 4, chunk * 5)
const newValue6 = newValue.slice(chunk * 5, chunk * 6)
const newValue7 = newValue.slice(chunk * 6)
const chunk = Math.ceil(newValue.length / PARTS)
const newValues = Array.from({ length: PARTS }, (_, i) =>
newValue.slice(chunk * i, i === PARTS - 1 ? undefined : chunk * (i + 1)),
)
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
await $`bun sst secret set ZEN_MODELS7 ${newValue7}`
for (let i = 0; i < PARTS; i++) {
await $`bun sst secret set ZEN_MODELS${i + 1} -- ${newValues[i]}`
}

View File

@@ -25,22 +25,7 @@ export namespace Billing {
export const get = async () => {
return Database.use(async (tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
paymentMethodID: BillingTable.paymentMethodID,
paymentMethodType: BillingTable.paymentMethodType,
paymentMethodLast4: BillingTable.paymentMethodLast4,
balance: BillingTable.balance,
reload: BillingTable.reload,
reloadAmount: BillingTable.reloadAmount,
reloadTrigger: BillingTable.reloadTrigger,
monthlyLimit: BillingTable.monthlyLimit,
monthlyUsage: BillingTable.monthlyUsage,
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
reloadError: BillingTable.reloadError,
timeReloadError: BillingTable.timeReloadError,
})
.select()
.from(BillingTable)
.where(eq(BillingTable.workspaceID, Actor.workspace()))
.then((r) => r[0]),

View File

@@ -1,6 +1,8 @@
import { z } from "zod"
import { fn } from "./util/fn"
import { Resource } from "@opencode-ai/console-resource"
import { centsToMicroCents } from "./util/price"
import { getWeekBounds } from "./util/date"
export namespace BlackData {
const Schema = z.object({
@@ -18,3 +20,73 @@ export namespace BlackData {
return Schema.parse(json)
})
}
export namespace Black {
export const analyzeRollingUsage = fn(
z.object({
usage: z.number().int(),
timeUpdated: z.date(),
}),
({ usage, timeUpdated }) => {
const now = new Date()
const black = BlackData.get()
const rollingWindowMs = black.rollingWindow * 3600 * 1000
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
const windowStart = new Date(now.getTime() - rollingWindowMs)
if (timeUpdated < windowStart) {
return {
status: "ok" as const,
resetInSec: black.rollingWindow * 3600,
usagePercent: 0,
}
}
const windowEnd = new Date(timeUpdated.getTime() + rollingWindowMs)
if (usage < rollingLimitInMicroCents) {
return {
status: "ok" as const,
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
usagePercent: Math.ceil(Math.min(100, (usage / rollingLimitInMicroCents) * 100)),
}
}
return {
status: "rate-limited" as const,
resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
usagePercent: 100,
}
},
)
export const analyzeWeeklyUsage = fn(
z.object({
usage: z.number().int(),
timeUpdated: z.date(),
}),
({ usage, timeUpdated }) => {
const black = BlackData.get()
const now = new Date()
const week = getWeekBounds(now)
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
if (timeUpdated < week.start) {
return {
status: "ok" as const,
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
usagePercent: 0,
}
}
if (usage < fixedLimitInMicroCents) {
return {
status: "ok" as const,
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
usagePercent: Math.ceil(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
}
}
return {
status: "rate-limited" as const,
resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
usagePercent: 100,
}
},
)
}

View File

@@ -35,7 +35,7 @@ export namespace ZenData {
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.boolean().optional(),
stickyProvider: z.enum(["strict", "prefer"]).optional(),
trial: TrialSchema.optional(),
rateLimit: z.number().optional(),
fallbackProvider: z.string().optional(),
@@ -74,7 +74,8 @@ export namespace ZenData {
Resource.ZEN_MODELS4.value +
Resource.ZEN_MODELS5.value +
Resource.ZEN_MODELS6.value +
Resource.ZEN_MODELS7.value,
Resource.ZEN_MODELS7.value +
Resource.ZEN_MODELS8.value,
)
return ModelsSchema.parse(json)
})

View File

@@ -21,8 +21,13 @@ export const BillingTable = mysqlTable(
reloadError: varchar("reload_error", { length: 255 }),
timeReloadError: utc("time_reload_error"),
timeReloadLockedTill: utc("time_reload_locked_till"),
subscription: json("subscription").$type<{
status: "subscribed"
coupon?: string
seats: number
plan: "20" | "100" | "200"
}>(),
subscriptionID: varchar("subscription_id", { length: 28 }),
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
timeSubscriptionBooked: utc("time_subscription_booked"),
},

View File

@@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.1.19",
"version": "1.1.25",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.1.19",
"version": "1.1.25",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -17,7 +17,7 @@
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-screen"></div>
<div id="root" class="flex flex-col h-dvh"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.1.19",
"version": "1.1.25",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -41,6 +41,7 @@ semver = "1.0.27"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
uuid = { version = "1.19.0", features = ["v4"] }
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
webkit2gtk = "=2.0.1"

View File

@@ -7,6 +7,7 @@
"core:default",
"opener:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:webview:allow-set-webview-zoom",
"core:window:allow-is-focused",
"core:window:allow-show",

View File

@@ -14,7 +14,7 @@ use std::{
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewWindowBuilder};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_store::StoreExt;
@@ -223,7 +223,7 @@ async fn check_server_health(url: &str, password: Option<&str>) -> bool {
pub fn run() {
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", not(debug_assertions)))]
let _ = std::process::Command::new("killall")
.arg("opencode-cli")
.output();
@@ -237,7 +237,14 @@ pub fn run() {
}
}))
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(
tauri_plugin_window_state::StateFlags::all()
- tauri_plugin_window_state::StateFlags::DECORATIONS,
)
.build(),
)
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
@@ -268,29 +275,30 @@ pub fn run() {
.map(|m| m.size().to_logical(m.scale_factor()))
.unwrap_or(LogicalSize::new(1920, 1080));
#[allow(unused_mut)]
let mut window_builder =
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
.title("OpenCode")
.inner_size(size.width as f64, size.height as f64)
.decorations(true)
.zoom_hotkeys_enabled(true)
.disable_drag_drop_handler()
.initialization_script(format!(
r#"
let config = app
.config()
.app
.windows
.iter()
.find(|w| w.label == "main")
.expect("main window config missing");
let window_builder = WebviewWindowBuilder::from_config(&app, config)
.expect("Failed to create window builder from config")
.inner_size(size.width as f64, size.height as f64)
.initialization_script(format!(
r#"
window.__OPENCODE__ ??= {{}};
window.__OPENCODE__.updaterEnabled = {updater_enabled};
"#
));
));
#[cfg(target_os = "macos")]
{
window_builder = window_builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
}
let window_builder = window_builder
.title_bar_style(tauri::TitleBarStyle::Overlay)
.hidden_title(true);
window_builder.build().expect("Failed to create window");
let _window = window_builder.build().expect("Failed to create window");
let (tx, rx) = oneshot::channel();
app.manage(ServerState::new(None, rx));

View File

@@ -11,6 +11,20 @@
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"label": "main",
"create": false,
"title": "OpenCode",
"url": "/",
"decorations": true,
"dragDropEnabled": false,
"zoomHotkeysEnabled": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
}
],
"withGlobalTauri": true,
"security": {
"csp": null

View File

@@ -2,6 +2,27 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCode",
"identifier": "ai.opencode.desktop",
"app": {
"windows": [
{
"label": "main",
"create": false,
"title": "OpenCode",
"url": "/",
"decorations": true,
"dragDropEnabled": false,
"zoomHotkeysEnabled": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"trafficLightPosition": { "x": 12.0, "y": 18.0 }
}
],
"withGlobalTauri": true,
"security": {
"csp": null
},
"macOSPrivateApi": true
},
"bundle": {
"createUpdaterArtifacts": true,
"icon": [

View File

@@ -13,7 +13,7 @@ import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store"
import { Logo } from "@opencode-ai/ui/logo"
import { createSignal, Show, Accessor, JSX, createResource } from "solid-js"
import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
@@ -26,10 +26,27 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
)
}
const isWindows = ostype() === "windows"
if (isWindows) {
const originalGetComputedStyle = window.getComputedStyle
window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
if (!(elt instanceof Element)) {
// WebView2 can call into Floating UI with non-elements; fall back to a safe element.
return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined)
}
return originalGetComputedStyle(elt, pseudoElt ?? undefined)
}) as typeof window.getComputedStyle
}
let update: Update | null = null
const createPlatform = (password: Accessor<string | null>): Platform => ({
platform: "desktop",
os: (() => {
const type = ostype()
if (type === "macos" || type === "windows" || type === "linux") return type
return undefined
})(),
version: pkg.version,
async openDirectoryPickerDialog(opts) {
@@ -296,12 +313,24 @@ render(() => {
const [serverPassword, setServerPassword] = createSignal<string | null>(null)
const platform = createPlatform(() => serverPassword())
function handleClick(e: MouseEvent) {
const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null
if (link?.href) {
e.preventDefault()
platform.openLink(link.href)
}
}
onMount(() => {
document.addEventListener("click", handleClick)
onCleanup(() => {
document.removeEventListener("click", handleClick)
})
})
return (
<PlatformProvider value={platform}>
<AppBaseProviders>
{ostype() === "macos" && (
<div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
)}
<ServerGate>
{(data) => {
setServerPassword(data().password)

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.1.19",
"version": "1.1.25",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.1.19"
version = "1.1.25"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.19/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.19/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.19/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.19/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.19/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.25/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.1.19",
"version": "1.1.25",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -134,6 +134,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS8": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_SESSION_SECRET": {
"type": "sst.sst.Secret"
"value": string

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.1.19",
"version": "1.1.25",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -70,7 +70,7 @@
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.51",
"@clack/prompts": "1.0.0-alpha.1",
"@gitlab/gitlab-ai-provider": "3.1.0",
"@gitlab/gitlab-ai-provider": "3.1.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.2",
@@ -82,8 +82,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.72",
"@opentui/solid": "0.1.72",
"@opentui/core": "0.1.74",
"@opentui/solid": "0.1.74",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",

View File

@@ -731,6 +731,9 @@ export namespace ACP {
const defaultAgentName = await AgentModule.defaultAgent()
const currentModeId = availableModes.find((m) => m.name === defaultAgentName)?.id ?? availableModes[0].id
// Persist the default mode so prompt() uses it immediately
this.sessionManager.setMode(sessionId, currentModeId)
const mcpServers: Record<string, Config.Mcp> = {}
for (const server of params.mcpServers) {
if ("type" in server) {

View File

@@ -13,6 +13,8 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { PermissionNext } from "@/permission/next"
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"
export namespace Agent {
export const Info = z
@@ -88,9 +90,13 @@ export namespace Agent {
PermissionNext.fromConfig({
question: "allow",
plan_exit: "allow",
external_directory: {
[path.join(Global.Path.data, "plans", "*")]: "allow",
},
edit: {
"*": "deny",
".opencode/plans/*.md": "allow",
[path.join(".opencode", "plans", "*.md")]: "allow",
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
},
}),
user,
@@ -249,7 +255,20 @@ export namespace Agent {
}
export async function defaultAgent() {
return state().then((x) => Object.keys(x)[0])
const cfg = await Config.get()
const agents = await state()
if (cfg.default_agent) {
const agent = agents[cfg.default_agent]
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`)
if (agent.mode === "subagent") throw new Error(`default agent "${cfg.default_agent}" is a subagent`)
if (agent.hidden === true) throw new Error(`default agent "${cfg.default_agent}" is hidden`)
return agent.name
}
const primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
if (!primaryVisible) throw new Error("no primary visible agent found")
return primaryVisible.name
}
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {

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