Compare commits

..

535 Commits

Author SHA1 Message Date
opencode
a84826061d release: v0.15.11 2025-10-21 15:04:22 +00:00
Dax Raad
7a20f77ebf core: improve error handling with console output for debugging 2025-10-21 10:55:10 -04:00
Giuseppe Rota
731122bf99 fix(acp): use newer acp package (#3317) 2025-10-21 09:35:09 -05:00
GitHub Action
f9036734eb ignore: update download stats 2025-10-21 2025-10-21 12:06:20 +00:00
kcrommett
a99bd3aa2c tweak: adjust file api to encode images (#3292) 2025-10-21 00:52:39 -05:00
zWing
96efede846 fix: openai options when using @ai-sdk/azure (#3315) 2025-10-20 23:30:13 -05:00
Cheol Kang
2f66055d25 feat: add -f/--file flag to opencode run command (#3295)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-20 23:25:54 -05:00
opencode
6995dab1dc release: v0.15.10 2025-10-20 22:21:03 +00:00
Dax Raad
a0a09f421c core: add session diff API to show file changes between snapshots 2025-10-20 17:59:26 -04:00
Joe Schmitt
f3f21194ae feat: Add ACP (Agent Client Protocol) support (#2947)
Co-authored-by: opencode-bot <devnull@opencode.local>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
2025-10-20 16:55:22 -05:00
opencode
835fa9fb81 release: v0.15.9 2025-10-20 21:16:57 +00:00
Aiden Cline
96ae6d51aa ignore: link labels 2025-10-20 16:02:23 -05:00
Frank
075ef0fa34 wip: share 2025-10-20 16:17:51 -04:00
Aiden Cline
89b72e4442 fix: bash tool permission merges (#3302) 2025-10-20 15:15:14 -05:00
Frank
7a7b3c6315 wip: zen 2025-10-20 13:22:43 -04:00
Rui Ferrão
3d48c14d29 docs: clarify the effects of the subtask command option with primary … (#3299) 2025-10-20 12:16:09 -05:00
GitHub Action
bfa79ed44b ignore: update download stats 2025-10-20 2025-10-20 12:04:59 +00:00
Hieu Nguyen
4d8268c818 feat: support astro lsp (#3242) 2025-10-19 22:49:06 -05:00
Squibid
95d413bec6 theme: add mellow theme (#3198) 2025-10-19 22:34:04 -05:00
Joscha Götzer
1cb5a70382 feat(theme): Add Night Owl theme (#3269) 2025-10-19 22:30:04 -05:00
Walter
6adc16ca8a fix(tui): Allow modals to handle ESC key before force closing (#3279) 2025-10-19 22:16:54 -05:00
Zeldris
10ebe9ae09 docs: add scoop & winget to readme (#3215) 2025-10-19 21:50:51 -05:00
Aiden Cline
43a07c6aca hide invalid option 2025-10-19 20:51:33 -05:00
Aiden Cline
5d3a88f34f fix: snapshot undo bug (#3290) 2025-10-19 18:42:34 -05:00
Ivan Uhalin
e47edfffe4 docs: fix Grep by Vercel link in documentation (#3280) 2025-10-19 11:51:57 -05:00
GitHub Action
141097fc73 ignore: update download stats 2025-10-19 2025-10-19 12:04:24 +00:00
Aiden Cline
c8898463a7 ignore: adjust template 2025-10-18 23:45:01 -05:00
Aiden Cline
1c7bd6365e replace all $ARGUMENTS (#3276) 2025-10-18 23:37:17 -05:00
Aiden Cline
290d15a80f ignore: add version to bug template 2025-10-18 14:56:06 -05:00
Aiden Cline
233a018fe5 docs: contributing.md (#3248)
Co-authored-by: Jay <air@live.ca>
2025-10-18 14:10:14 -05:00
opencode
d69beec087 release: v0.15.8 2025-10-18 18:36:52 +00:00
Aiden Cline
1f869bccc1 make compact interruptable (#3251) 2025-10-18 11:49:29 -05:00
GitHub Action
8da8c9e78c ignore: update download stats 2025-10-18 2025-10-18 12:04:14 +00:00
Adam
335d833655 wip: desktop work 2025-10-17 15:22:11 -05:00
Haris Gušić
1dba01e057 Improve typo directory error (#3247) 2025-10-17 14:34:37 -05:00
Jay V
a3de43f3de ignore: clarify that workspaces are free for teams during beta 2025-10-17 15:19:19 -04:00
Jay V
22ad4f5365 ignore: help users discover that teams feature is free during beta 2025-10-17 14:33:21 -04:00
Jay V
5bfbec60b5 docs: clarify team collaboration features and pricing 2025-10-17 13:58:10 -04:00
Adam
cc18b58ff9 wip: desktop work 2025-10-17 12:07:58 -05:00
Adam
887a819f24 wip: desktop work 2025-10-17 12:06:36 -05:00
mgrachten
fe8b3a2515 set 755 permissions (#3237) 2025-10-17 10:42:42 -05:00
GitHub Action
86079353ef ignore: update download stats 2025-10-17 2025-10-17 12:04:43 +00:00
Frank
b7c8690414 wip: zen 2025-10-17 02:23:45 -04:00
Frank
c25b9bf65a wip: zen 2025-10-17 01:15:25 -04:00
Matt Gillard
ddb2e6957c added AU inference for bedrock haiku 4.5 (#3206)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
2025-10-16 23:48:41 -05:00
Frank
a590b32a10 wip: zen 2025-10-17 00:26:00 -04:00
Frank
5f7bba11fd wip: zen 2025-10-16 23:43:49 -04:00
Frank
4663ea5faa wip: zen 2025-10-16 23:19:25 -04:00
Frank
dd581e8577 wip: zen 2025-10-17 03:17:07 +00:00
opencode
bad01d76de release: v0.15.7 2025-10-17 03:17:07 +00:00
Aiden Cline
d69366b00c Revert "try to avoid persisting empty thinking/text blocks"
This reverts commit d8a15e7bc9.
2025-10-16 22:04:55 -05:00
Frank
1947580b08 wip: zen 2025-10-16 22:28:34 -04:00
Frank
ca9b13e8a2 wip: zen 2025-10-16 22:28:09 -04:00
Hieu Nguyen
92d9a0ec61 feat: deno lsp (#3210)
Co-authored-by: hiunguynx <hieu.nm1@teko.vn>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-10-16 18:08:36 -05:00
opencode
2be9ed2590 release: v0.15.6 2025-10-16 23:02:59 +00:00
Aiden Cline
25861f6d0d sync 2025-10-16 17:49:02 -05:00
Aiden Cline
b24f4e3d2c fix: timeout option (#3229) 2025-10-16 17:47:41 -05:00
Frank
729ad1cb75 wip: zen 2025-10-16 17:50:32 -04:00
Haris Gušić
fb4105a46c docs: fix Ctrl+K styling in documentation (#3021) 2025-10-16 15:30:28 -05:00
Frank
7abc3e9794 ci: fix 2025-10-16 16:04:25 -04:00
Frank
88fef05923 wip: zen 2025-10-16 15:58:51 -04:00
GitHub Action
8552f3555e chore: format code 2025-10-16 19:55:30 +00:00
Adam
47d9e01765 wip: css/ui and desktop work 2025-10-16 14:53:44 -05:00
Aiden Cline
fc18fc8a08 fix: bash hangs & orphans (#3225) 2025-10-16 14:39:36 -05:00
Dax Raad
7474788778 ci: fix 2025-10-16 15:36:12 -04:00
GitHub Action
26d0d20e4d chore: format code 2025-10-16 19:08:21 +00:00
Adam
20229f147b wip: css/ui and desktop work 2025-10-16 14:07:37 -05:00
Aiden Cline
149cb6a9ec fix: connection closed 2025-10-16 14:00:07 -05:00
Frank
7ec5e49e19 zen: support stripe link 2025-10-16 14:59:46 -04:00
Aiden Cline
1c1380d3c8 adjust action 2025-10-16 13:15:27 -05:00
Adam
10680f0cf0 wip: css/ui work 2025-10-16 18:06:59 +00:00
opencode
2517b22552 release: v0.15.5 2025-10-16 18:06:59 +00:00
Aiden Cline
64617c113a ignore: tweak permissions 2025-10-16 12:48:39 -05:00
Aiden Cline
860c6338fc fix: github action (#3223) 2025-10-16 12:33:33 -05:00
Aiden Cline
4a7551e87b ci: fix changelog generation 2025-10-16 11:29:58 -05:00
Moisès Macià
285cc4b9fd docs: fix misspelled word (#3211) 2025-10-16 10:04:11 -05:00
Dax Raad
d8a15e7bc9 try to avoid persisting empty thinking/text blocks 2025-10-16 10:54:10 -04:00
opencode
542b9fa342 release: v0.15.4 2025-10-16 14:53:32 +00:00
GitHub Action
9159afb54b ignore: update download stats 2025-10-16 2025-10-16 12:04:48 +00:00
seridescent
536934548a fix: use ai-sdk openai chat language model instead of completion language model (#3204) 2025-10-16 00:59:49 -05:00
Aiden Cline
1c59530115 Revert "fix: Text content blocks must contain non-whitespace text" (#3200) 2025-10-15 20:02:17 -05:00
Dax Raad
ab8471a7ff core: filter out alpha status models from provider list 2025-10-15 20:12:37 -04:00
Dax Raad
4c674b075b ci: stuff 2025-10-15 19:59:46 -04:00
Dax Raad
ba8a4c5e9f snapshot publish everything 2025-10-15 19:53:14 -04:00
Dax Raad
790fe72f39 sync 2025-10-15 19:48:57 -04:00
Aiden Cline
2d2d4641cb ignore: update readme 2025-10-15 16:06:11 -05:00
Aiden Cline
d3caa55c10 Revert "fix: spawns hanging (#3192)"
This reverts commit 278ffb9a4e.
2025-10-15 15:20:14 -05:00
Fabian Kukuck
ca534a36e5 feat: make compact feature use streaming API (#3079)
Co-authored-by: fku <fabian.kukuck@ipt.ch>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-15 13:44:16 -05:00
Aiden Cline
278ffb9a4e fix: spawns hanging (#3192) 2025-10-15 13:01:54 -05:00
Aiden Cline
b2ff4be4c6 fix: Text content blocks must contain non-whitespace text (#3194) 2025-10-15 13:00:26 -05:00
Frank
2267ce2511 zen: support haiku 4.5 2025-10-15 13:53:00 -04:00
Matt Gillard
e29d1d339c updated bedrock provider for the new Australian sonnet 4.5 cross region inference (#3050)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
2025-10-15 11:09:22 -05:00
Haris Gušić
92bc78a2d3 Improve http error codes (#3186) 2025-10-15 10:53:09 -05:00
GitHub Action
1ba5535460 ignore: update download stats 2025-10-15 2025-10-15 12:04:51 +00:00
Frank
7fa9a73bf0 wip: zen 2025-10-15 02:55:01 -04:00
Aiden Cline
b3fcc9a81d tweak: consolidate session lock logic (#3185) 2025-10-15 01:12:51 -05:00
Frank
e8751d976e wip: zen 2025-10-15 01:07:57 -04:00
Frank
43c9702aa7 wip: zen 2025-10-15 00:02:38 -04:00
Frank
ae609be710 wip: zen 2025-10-14 23:45:06 -04:00
Frank
86ee36f562 wip: zen 2025-10-14 23:38:21 -04:00
Frank
0657f09139 wip: zen 2025-10-14 23:04:41 -04:00
Frank
182949dee4 wip: zen 2025-10-14 23:03:13 -04:00
Dax Raad
83655a3b09 ci: run typecheck before tests to catch type errors early 2025-10-14 18:36:03 -04:00
Dax Raad
62e5f4b154 try tsgo 2025-10-14 18:30:32 -04:00
Jay V
ea926f0e1a ignore: prompt 2025-10-14 17:45:10 -04:00
Jay V
6191232d5f web: colocate copy button styles with components that use them 2025-10-14 17:41:17 -04:00
Frank
95f4ce86d6 ci: fix 2025-10-14 17:22:29 -04:00
Frank
5999aefde3 wip: zen 2025-10-14 17:18:39 -04:00
Frank
babe3a0f40 wip: zen 2025-10-14 17:13:21 -04:00
Frank
29b95dee53 wip: zen 2025-10-14 17:06:49 -04:00
Frank
ef9a1e911e wip: zen 2025-10-14 17:06:49 -04:00
Jay V
7eddaa806d docs: improve MCP server configuration guidance with examples and caveats 2025-10-14 16:43:59 -04:00
Dax Raad
d07e79e6ad ci: channels 2025-10-14 15:09:18 -04:00
Dax Raad
f17a7cde8d sync 2025-10-14 14:57:34 -04:00
GitHub Action
6d446c2a03 chore: format code 2025-10-14 18:56:54 +00:00
Dax Raad
61f6091de1 ci: test 2025-10-14 14:56:21 -04:00
Dax Raad
289783f627 ci: version stuff 2025-10-14 14:52:05 -04:00
Dax Raad
4c464cf4c0 ci: fix 2025-10-14 18:44:22 +00:00
opencode
83be5b0171 release: v0.15.3 2025-10-14 18:44:21 +00:00
Dax Raad
0c022ef39d ci: stuff 2025-10-14 14:35:04 -04:00
Aiden Cline
717b544633 fix: false positive package manager detection in upgrade (#3181) 2025-10-14 13:18:40 -05:00
Frank
c1a420717a ci: fix 2025-10-14 13:58:54 -04:00
Frank
42c2ffd842 wip: zen 2025-10-14 13:52:30 -04:00
GitHub Action
5192c51843 chore: format code 2025-10-14 17:08:18 +00:00
Adam
96d7ccea48 wip: css/ui work 2025-10-14 12:07:45 -05:00
Adam
49e859cfd6 wip: css/ui work 2025-10-14 12:06:18 -05:00
Adam
6c57a69af4 wip: desktop work 2025-10-14 12:06:17 -05:00
Netanel Draiman
4d019430e2 feat(cli): add session option to attach command (#3167) 2025-10-14 11:04:32 -05:00
Adam
37e6c8342f wip: css and ui packages 2025-10-14 07:16:24 -05:00
Adam
c04e892991 wip: desktop work 2025-10-14 07:15:08 -05:00
Adam
bb82d43094 wip: desktop work 2025-10-14 07:15:08 -05:00
GitHub Action
2893b6e3a5 ignore: update download stats 2025-10-14 2025-10-14 12:04:50 +00:00
Dax
54c3361be7 feat: use realtime events for live tool call updates in Slack (#3163) 2025-10-14 03:13:48 -04:00
Dax Raad
c50cf21f18 fix: update tsconfig for Slack package 2025-10-14 02:55:21 -04:00
Dax Raad
cb73e2d9e1 fix: export trimDiff function from edit tool 2025-10-14 02:55:02 -04:00
Dax Raad
48057c2c21 fix: resolve TypeScript errors in SDK and Slack package 2025-10-14 02:54:37 -04:00
Dax Raad
1923ddab6e feat: add Slack integration package with Bolt framework 2025-10-14 02:53:55 -04:00
Dax Raad
b8249cde4b core: improve dependency management and error handling for more reliable builds 2025-10-14 01:33:25 -04:00
Dax Raad
19b3f3d7ce core: standardize dependency versions for better reliability
Ensures consistent versions across packages by using workspace catalog for
tailwindcss and tsconfig dependencies, reducing potential conflicts and
installation issues.
2025-10-14 01:27:17 -04:00
Dax Raad
e5e05d390d core: reduce dependency conflicts by standardizing package versions through catalog
This eliminates duplicate package versions that were causing build issues and
inconsistent behavior across the monorepo. Dependencies now resolve to single
versions through the workspace catalog, making installs faster and more reliable.
2025-10-14 01:23:54 -04:00
opencode
38ad6707cf release: v0.15.2 2025-10-14 04:56:00 +00:00
Alberto Fanton
7ef246f98f fix: disable GPG signing in snapshot tests (#3102) 2025-10-13 23:40:41 -05:00
Aiden Cline
b91582d68a fix: config dir overrides (#3160) 2025-10-13 23:25:53 -05:00
Aiden Cline
682d30bd12 fix: custom model (#3156) 2025-10-13 19:58:19 -05:00
pancake
4d68ee5d2c fix: clang formatter name (#3042)
Co-authored-by: pancake <pancake@nopcode.org>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-10-13 19:55:18 -05:00
Haris Gušić
dbe9fd00b7 fix: make shell more robust (#3051) 2025-10-13 17:37:35 -05:00
maple
cd13a8524e docs: typo in custom-tools.mdx (#3152) 2025-10-13 17:19:20 -05:00
Aiden Cline
59765e0157 fix: typecheck (#3149) 2025-10-13 14:51:12 -05:00
AB
d0519be0d0 fix: add useCompletionUrls option to fix certain azure setups (#2528)
Co-authored-by: andreas.blass <andreas.blass@outlook.com>
Co-authored-by: GitHub Action <action@github.com>
2025-10-13 14:16:21 -05:00
Tommy D. Rossi
066e4f064d tweak: include stack trace in server error responses (#3134) 2025-10-13 14:10:35 -05:00
opencode
f81c469f17 release: v0.15.1 2025-10-13 18:14:52 +00:00
Dax Raad
a398013ecb fix: disable workspace symbol lookup to prevent LSP performance issues 2025-10-13 14:05:54 -04:00
Aiden Cline
53d9717d90 fix: pass options to compact (#3136) 2025-10-13 10:42:39 -05:00
Aiden Cline
5885b691b9 docs: update recommended models list (#3121) 2025-10-12 21:35:31 -05:00
Aiden Cline
fd70b9b057 fix: adjust list tool prompt to handle cwd better (#3115) 2025-10-12 16:48:03 -05:00
Ravshan Samandarov
de13ccb757 docs: Update README.md (#3100) 2025-10-12 16:23:25 -04:00
Jay
7e1abb7bbf docs: Fix formatting of num_ctx in providers.mdx 2025-10-12 16:01:32 -04:00
Aiden Cline
83afcb9c42 docs: ollama num_ctx (#3111) 2025-10-12 10:40:51 -05:00
GitHub Action
afb406c5ff ignore: update download stats 2025-10-12 2025-10-12 12:04:09 +00:00
OpeOginni
36cf9b9922 fix: add timeout to fetch models.dev refresh request (#3059) 2025-10-12 00:20:22 -05:00
opencode
0d21164255 release: v0.15.0 2025-10-12 05:12:15 +00:00
Dax Raad
3ad6f84adb ci: centralize bun setup to reduce duplication and improve caching 2025-10-12 00:46:37 -04:00
Dax Raad
24a5b16af8 ci: tweak 2025-10-12 00:40:59 -04:00
Tommy D. Rossi
b4171aa8e8 fix: rg hanging forever when run in bash, waiting for stdin (#3103)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-11 23:24:48 -05:00
Dax Raad
d7a79733ea ci: re-enable aur 2025-10-12 00:22:16 -04:00
Frank
34e5b9bdb0 wip: zen 2025-10-11 16:17:39 -04:00
Frank
d32ec9bd52 ci: fix 2025-10-11 15:08:45 -04:00
Frank
89fcfcc50b wip: zen 2025-10-11 15:07:06 -04:00
Frank
9a6fd6a5ee wip: zen 2025-10-11 14:48:34 -04:00
Frank
f144a0384d ci: fix 2025-10-11 14:41:52 -04:00
Frank
a67920a25e wip: zen 2025-10-11 14:38:53 -04:00
Frank
67f894e5d0 Bump sst to latest 2025-10-11 14:38:45 -04:00
Frank
fc1eda5c77 wip: zen 2025-10-11 10:51:17 -04:00
Frank
371fddc820 wip: zen 2025-10-11 09:29:26 -04:00
Frank
8e89c38480 wip: zen 2025-10-11 08:27:32 -04:00
Frank
b732b4caeb wip: zen 2025-10-11 08:27:32 -04:00
Frank
1940d1cf87 wip: zen 2025-10-11 08:27:32 -04:00
GitHub Action
1f0ed24402 ignore: update download stats 2025-10-11 2025-10-11 12:04:02 +00:00
Frank
133da0f448 wip: zen refactor selector 2025-10-11 07:54:57 -04:00
Frank
f93e1e5c92 wip: zen fix drop down style 2025-10-11 07:15:19 -04:00
Frank
ae4af54c7d wip: zen fix template 2025-10-11 06:33:47 -04:00
Dax Raad
9d30bc692c ci: fixes 2025-10-11 00:24:35 -04:00
Dax Raad
44b63dc259 ci: stuff 2025-10-11 00:06:46 -04:00
Dax Raad
de2b4f6538 ci: fix 2025-10-10 23:43:32 -04:00
Frank
b6b82aa847 wip: zen 2025-10-10 21:26:16 -04:00
Frank
2d35b78333 Merge branch 'console-workspaces' into dev 2025-10-10 21:24:05 -04:00
Frank
c7dfbbeed0 wip: zen 2025-10-10 21:21:55 -04:00
Frank
b946fd21b1 wip: zen 2025-10-10 20:32:28 -04:00
Frank
daa0ca40f2 wip: zen 2025-10-10 20:30:13 -04:00
Frank
5b27130d60 wip: zen 2025-10-10 20:16:44 -04:00
Frank
ee1eb35269 wip: zen 2025-10-10 20:02:17 -04:00
Frank
4dda7cc6a4 wip: zen 2025-10-10 19:56:40 -04:00
Frank
cc590364e9 wip: zen 2025-10-10 19:49:59 -04:00
Frank
f14cd4a3db wip: zen 2025-10-10 19:39:01 -04:00
Dax Raad
07645e0705 ci: fixes 2025-10-10 18:17:10 -04:00
Dax Raad
f053862018 ci: fix 2025-10-10 18:11:19 -04:00
Dax Raad
69127aeaa0 ci: stuff 2025-10-10 18:04:08 -04:00
Dax Raad
847455383d ci: stuff 2025-10-10 17:56:33 -04:00
Dax Raad
9da95cb805 upgrade to bun 1.3.0 2025-10-10 17:53:32 -04:00
Frank
48008f91ac wip: zen 2025-10-10 16:42:27 -04:00
Frank
d8b3aa9382 wip: zen 2025-10-10 16:34:07 -04:00
Frank
ea9b5b8d76 wip: zen 2025-10-10 16:04:06 -04:00
Frank
4227b89ebc wip: zen 2025-10-10 14:54:49 -04:00
Frank
ee846235f2 wip: zen 2025-10-10 14:19:06 -04:00
Frank
9463ce8006 wip: zen 2025-10-10 14:11:48 -04:00
Frank
756fb61691 wip: zen 2025-10-10 13:52:54 -04:00
Frank
94d0a3d888 wip: zen style members 2025-10-10 13:48:56 -04:00
Frank
d83af721a6 wip: zen style api keys 2025-10-10 13:45:06 -04:00
Frank
0bc00bef32 wip: zen 2025-10-10 13:26:39 -04:00
Frank
98c13a965b wip: zen 2025-10-10 13:21:51 -04:00
Frank
310065bd0a wip: zen 2025-10-10 12:46:42 -04:00
Rustafarian Dev
34ec6cc978 fix: perl6 file extension (#3066) 2025-10-10 11:28:49 -05:00
Frank
5a90e5f9e2 wip: zen 2025-10-10 12:22:36 -04:00
Frank
5ee3063aab wip: sync 2025-10-10 12:16:57 -04:00
Frank
920373d252 wip: zen settings 2025-10-10 12:04:02 -04:00
Frank
c9155c117a wip: zen 2025-10-10 09:03:49 -04:00
GitHub Action
28d617d867 ignore: update download stats 2025-10-10 2025-10-10 12:04:18 +00:00
Frank
593d0737b5 wip: zen style byok 2025-10-10 03:15:55 -04:00
Frank
64409182ec wip: zen style byok 2025-10-10 02:53:05 -04:00
Frank
8d4607ebd5 wip: zen style byok 2025-10-10 02:37:50 -04:00
Frank
250393978b wip: style byok 2025-10-10 02:34:06 -04:00
Frank
fec70ae9c9 wip: zen 2025-10-10 01:36:15 -04:00
Frank
ad7b4b1fcd wip: zen style nav bar 2025-10-10 00:56:16 -04:00
Frank
03d5089436 wip: zen style model 2025-10-10 00:02:04 -04:00
Dax Raad
9b52d33889 core: improve directory validation error messages to help users fix invalid directory names 2025-10-09 22:40:23 -04:00
Frank
bc0e00cbb7 wip: zen style header 2025-10-09 22:38:42 -04:00
Dax Raad
096710a8cc ensure @opencode-ai/plugin is available in .opencode folder 2025-10-09 21:18:49 -04:00
opencode
50bb201187 release: v0.14.7 2025-10-10 01:02:37 +00:00
Dax Raad
f211fc45a3 drop excess dependency in opencode sdk 2025-10-09 20:55:25 -04:00
Dax Raad
d91781c639 core: use platform-specific watcher backends for better file system monitoring 2025-10-09 18:29:18 -04:00
Dax Raad
f3b71007d2 core: replace chokidar with @parcel/watcher for better performance and cross-platform support 2025-10-09 18:21:38 -04:00
Frank
60dd987efd wip: zen 2025-10-09 17:18:55 -04:00
Dax Raad
0a96d254e8 ignore: add common build and framework directories to ignore list 2025-10-09 16:35:56 -04:00
Frank
51e9979457 wip: zen nav bar 2025-10-09 16:01:52 -04:00
Dax Raad
dfc7ac4cf0 ignore: improve file ignore performance and cross-platform support
- Replace glob patterns with Set lookup for common folders to speed up matching
- Use path.sep for cross-platform compatibility on Windows/Unix systems
- Add comprehensive test coverage for nested and non-nested folder matching
- Simplify implementation by removing unnecessary caching complexity
2025-10-09 15:54:01 -04:00
Adam
c2950d26f0 feat: experimental skip bootstrap 2025-10-09 14:51:11 -05:00
Aiden Cline
47dfebf277 docs: fix bugged example (#3068) 2025-10-09 12:21:44 -05:00
Jay V
f3b5021936 docs: adding tools doc 2025-10-09 13:19:51 -04:00
Jay V
7be9a84b72 docs: document ripgrep .ignore file override in tools 2025-10-09 13:19:51 -04:00
Jay V
78321a95e8 docs: adding spellcheck command 2025-10-09 13:19:51 -04:00
Aiden Cline
225adc46ba feat: allow read tool to handle images (#3052) 2025-10-09 09:05:11 -05:00
GitHub Action
eb4b5721cd ignore: update download stats 2025-10-09 2025-10-09 12:04:27 +00:00
Dax Raad
979c9ea569 lsp: fix root detection to use instance directory instead of worktree 2025-10-09 04:30:30 -04:00
Dax Raad
c0bd29155d lsp: simplify language server root detection to use lock files
Improves project boundary detection by focusing on package manager lock files instead of config files, providing more reliable workspace identification across different project types.
2025-10-09 04:22:38 -04:00
Haris Gušić
c5b5795636 fix: process.stdout.write instead of console.log for export cmd (#3049) 2025-10-09 00:46:19 -05:00
Frank
3ed4f1078f wip: zen 2025-10-08 22:33:20 -04:00
Frank
5b1fd7e539 wip: zen 2025-10-08 18:59:41 -04:00
Frank
d18b6673e6 wip: zen 2025-10-08 17:03:42 -04:00
Frank
c93c0d402d wip: zen 2025-10-08 15:20:50 -04:00
Frank
b168bfe40d wip: zen 2025-10-08 13:31:15 -04:00
Jay V
1d621260ff docs: fix permission docs 2025-10-08 12:13:42 -04:00
GitHub Action
a63fa64dec ignore: update download stats 2025-10-08 2025-10-08 12:04:31 +00:00
Adam
3c282c3c37 fix(tui): suggestions gap on home page 2025-10-08 06:56:18 -05:00
Dax Raad
2046f2e8e7 add free workspace 2025-10-08 02:27:01 -04:00
Frank
af684c80d4 wip: zen 2025-10-08 01:14:39 -04:00
Frank
99b72eb1ea wip: zen 2025-10-08 00:03:36 -04:00
opencode
22a6849ff8 release: v0.14.6 2025-10-07 19:59:08 +00:00
Dax Raad
dca3a5d80d fix issue with blank new version popup 2025-10-07 15:51:59 -04:00
Frank
508067ba5d wip: zen 2025-10-07 13:37:38 -04:00
Aiden Cline
b6c9df970a docs: troubleshooting ProviderModelNotFoundError (#3016) 2025-10-07 11:50:37 -05:00
Sai
1f725cc3ed docs: add agent specific permission example (#3009) 2025-10-07 10:08:52 -05:00
Frank
6c99b833e4 wip: zen 2025-10-07 09:17:08 -04:00
GitHub Action
cd3780b7f5 ignore: update download stats 2025-10-07 2025-10-07 12:04:53 +00:00
Dax Raad
a440e09cfe core: improve MCP reliability and add status monitoring
- Added 5-second timeout to MCP client verification to prevent hanging connections
- New GET /mcp endpoint to monitor server connection status
- Automatically removes unresponsive MCP clients during initialization
2025-10-07 04:04:19 -04:00
opencode
27c211ef86 release: v0.14.5 2025-10-07 06:21:31 +00:00
Aiden Cline
cd528ae78f fix: mcp error (#3006) 2025-10-07 00:45:46 -05:00
Aiden Cline
06c42093c8 tweak: grep tool to handle single file better (#3004) 2025-10-06 23:24:00 -05:00
Frank
0534bc0c09 wip: zen 2025-10-06 23:57:55 -04:00
Frank
4f33594b99 wip: zen 2025-10-06 23:57:54 -04:00
opencode
e3f9e7785e release: v0.14.4 2025-10-07 03:32:10 +00:00
Dax Raad
a20fc2dfdf ignore: 2025-10-06 23:25:01 -04:00
Dax Raad
2bf0e42367 core: restore bash command security validation to prevent accidental directory traversal
The permission validation that prevents commands from accessing paths outside the project directory was accidentally disabled, which could allow commands like 'cd ../' to escape the workspace. This restores the security check that keeps your commands safely contained within your project boundaries.
2025-10-06 23:24:18 -04:00
Dax Raad
10998d62b9 core: improve session API reliability with proper input validation 2025-10-06 19:37:44 -04:00
Dax Raad
aee240150b Update todo tool to use centralized Todo module 2025-10-06 18:54:05 -04:00
Dax Raad
cdd6e98af9 Add missing files and fix type aliases for opentui features 2025-10-06 18:53:35 -04:00
Dax Raad
6417edf998 Add todo list and session forking API endpoints 2025-10-06 18:51:57 -04:00
Dax Raad
9a0735de76 Add session forking functionality and simplify remove logic 2025-10-06 18:50:56 -04:00
Frank
a470859f6f wip: zen 2025-10-06 17:23:10 -04:00
Frank
f47c7c5a07 wip: zen 2025-10-06 17:17:02 -04:00
Frank
c2f57ea74d wip: zen 2025-10-06 17:13:19 -04:00
Frank
9e8fd16e6e wip: zen 2025-10-06 17:13:19 -04:00
Jay V
1b17d8070b docs: update footer 2025-10-06 17:05:45 -04:00
Jay V
1db028dc05 docs: fix styles and zen doc, closes #2912 2025-10-06 17:00:10 -04:00
Jay V
b351b75156 docs: share page css 2025-10-06 16:13:21 -04:00
GitHub Action
2faa28e162 ignore: update download stats 2025-10-06 2025-10-06 12:04:17 +00:00
Aiden Cline
bdf77701cf fix: add timeout message if command times out (#2986) 2025-10-05 23:55:01 -05:00
Mani Sundararajan
889c276558 fix: file references & grep tool for windows (#2980) 2025-10-05 14:32:07 -05:00
GitHub Action
9c6192b00d ignore: update download stats 2025-10-05 2025-10-05 12:03:55 +00:00
opencode
d2a4a0375f release: v0.14.3 2025-10-05 11:22:57 +00:00
Dax Raad
aced8c95f2 ci: publish 2025-10-05 07:14:52 -04:00
Dax Raad
1bb664869c ci: disable aur 2025-10-05 07:12:33 -04:00
Dax Raad
116a006ce6 sdk: simplify getting started with single createOpencode function
Makes it easier for developers to get started by providing a single function that creates both server and client, removing the need to manually coordinate separate server and client creation
2025-10-05 07:01:32 -04:00
Dax Raad
f3c2d1b6c2 sdk: simplify getting started with single createOpencode function
Makes it easier for developers to get started by providing a single function that creates both server and client, removing the need to manually coordinate separate server and client creation
2025-10-05 07:00:29 -04:00
Aiden Cline
71a7e8ef36 fix: max output tokens when using large thinking budget (#2976) 2025-10-04 23:38:41 -05:00
Dax Raad
5f7ae6477b sync 2025-10-04 21:33:47 -04:00
Aiden Cline
f41a54b4b0 fix: allow LSP filename matching when extension is missing (#2975) 2025-10-04 20:30:53 -05:00
iwauo
080fce9601 docs: java-lsp support (#2958) 2025-10-04 11:28:09 -05:00
GitHub Action
b2222cc278 ignore: update download stats 2025-10-04 2025-10-04 12:03:53 +00:00
Frank
82509e8604 wip: zen 2025-10-04 01:12:32 -04:00
Yuku Kotani
e7b6ffb314 feat: Vertex AI support; add google-vertex and google-vertex-anthropic providers (#2347) 2025-10-04 01:10:38 -04:00
Aiden Cline
395c41b748 add command to debug config (#2962) 2025-10-03 23:07:58 -05:00
Frank
a11a608760 wip: zen 2025-10-03 23:48:34 -04:00
Dax Raad
477586835a ci: try regional hostname again 2025-10-03 19:05:32 -04:00
Rovshan Muradov
085f4adbc3 docs: Update models.mdx (#2916) 2025-10-03 17:06:20 -04:00
Frank
9671872059 wip: zen 2025-10-03 16:32:53 -04:00
Jay V
6378e6c06f docs: rename opencode to OpenCode 2025-10-03 13:46:56 -04:00
Frank
4159db4549 wip: zen 2025-10-03 12:54:52 -04:00
Adam
79764c8c4c fix: github stats 2025-10-03 09:34:17 -05:00
Adam
006cb5b36d fix: user-agent 2025-10-03 09:30:51 -05:00
Adam
8ce7d58e6d chore: user-agent header 2025-10-03 09:27:12 -05:00
Adam
b622e924b6 chore: logging errors 2025-10-03 09:19:54 -05:00
Adam
8e80b8f2fa chore: logging errors 2025-10-03 09:10:33 -05:00
Adam
3fa280d218 chore: app -> desktop 2025-10-03 09:04:28 -05:00
Frank
1d58b55482 wip: zen 2025-10-03 08:25:51 -04:00
GitHub Action
aae387f7dc ignore: update download stats 2025-10-03 2025-10-03 12:04:07 +00:00
Frank
60e21642a5 wip: zen 2025-10-03 07:36:16 -04:00
Frank
600b512c9c wip: zen 2025-10-03 07:36:16 -04:00
Frank
3be1f9b67e wip: zen 2025-10-03 07:36:16 -04:00
Dax Raad
ad0f137e35 ci: stuff 2025-10-03 10:53:10 +00:00
opencode
253105bcf5 release: v0.14.1 2025-10-03 10:53:10 +00:00
Dax Raad
bd0ba5ab88 turn on codex medium reasoning again 2025-10-03 06:46:07 -04:00
David Hill
ea993976b0 Firefox email input fix 2025-10-02 23:50:51 +01:00
Jay
4c11ccd334 docs: update theme (#2929)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2025-10-02 23:20:30 +01:00
David Hill
d766ca23e8 Update index.css 2025-10-02 23:19:53 +01:00
Jay
fe4589d335 ignore: Workspace updates (#2930)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2025-10-02 23:18:57 +01:00
Frank
6036a1d611 wip: zen 2025-10-02 18:16:29 -04:00
Frank
a8341e2b8b wip: zen 2025-10-02 17:55:54 -04:00
Frank
73115efab1 wip: zen 2025-10-02 16:09:42 -04:00
Frank
a45fa7a93c wip: zen 2025-10-02 13:58:40 -04:00
Jay
ae15c91455 docs: README 2025-10-02 12:33:32 -04:00
Jay V
52f16c496b docs: update README 2025-10-02 12:31:58 -04:00
David Hill
24d9f45506 Copy tweaks 2025-10-02 17:02:33 +01:00
David Hill
2404d70a33 Border top only on safari fix 2025-10-02 16:55:48 +01:00
David Hill
26f1cc87ca Update favicon.svg 2025-10-02 16:55:29 +01:00
Aiden Cline
860e47edea fix: run cmd json format when running command (#2926) 2025-10-02 10:37:42 -05:00
David Hill
e2378f2237 Style fixes 2025-10-02 16:35:23 +01:00
David Hill
9e197a5b67 Update dock.png 2025-10-02 16:24:14 +01:00
David Hill
5f4041c58f Update dock.png 2025-10-02 16:14:52 +01:00
David Hill
d56e81f02b Email input color fix 2025-10-02 16:11:55 +01:00
David Hill
6022d12ea2 Update dock.png 2025-10-02 15:50:56 +01:00
David Hill
f7ef1c286f Testimonial tweaks 2025-10-02 15:37:36 +01:00
David Hill
b35c6b9fff Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-02 15:37:13 +01:00
David Hill
30ec02e82d faq icon fix 2025-10-02 15:37:10 +01:00
Frank
bc9522d5d8 ignore: fix 2025-10-02 10:10:07 -04:00
David Hill
b6e80e72f6 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-02 15:03:59 +01:00
David Hill
2ded2aa2d9 Body background fix 2025-10-02 15:03:56 +01:00
David Hill
30dc0cbe58 Mobile nav icon fix 2025-10-02 15:02:57 +01:00
David Hill
2bd0c9c6d2 Mobile nav icon fix 2025-10-02 15:02:44 +01:00
David Hill
f9229889a1 Mobile nav fix 2025-10-02 15:01:22 +01:00
Adam
eb4f55bdf6 fix: broken links 2025-10-02 09:00:33 -05:00
David Hill
9ee4e2e3d4 Update posters for videos 2025-10-02 14:55:38 +01:00
David Hill
decb6ff2d3 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-02 14:54:16 +01:00
David Hill
b9de71dbfa Testimonial tweak 2025-10-02 14:53:54 +01:00
Adam
124e355a3c chore: add email signup back 2025-10-02 08:46:25 -05:00
David Hill
095fe68786 Update faq.tsx 2025-10-02 14:42:49 +01:00
David Hill
afc67caa48 Faq icon color fix 2025-10-02 14:41:57 +01:00
Adam
189b7f1172 fix: testimonial link 2025-10-02 08:34:22 -05:00
Adam
cc955098cd wip: desktop work 2025-10-02 08:34:01 -05:00
GitHub Action
8699e896e6 ignore: update download stats 2025-10-02 2025-10-02 12:04:06 +00:00
opencode
ca4cb85dcd release: v0.14.0 2025-10-02 05:33:58 +00:00
Dax Raad
88474e0653 ci: fix 2025-10-02 01:25:27 -04:00
Dax Raad
5667a7ed16 ci: publsih 2025-10-02 01:24:33 -04:00
Dax Raad
2ae3231ff9 ci: test 2025-10-02 01:19:53 -04:00
Dax Raad
d92fc25e26 ci: install opencode 2025-10-02 01:19:53 -04:00
Dax Raad
c8c0373f1d ci: fix 2025-10-02 05:18:01 +00:00
opencode
bccee29d2f release: v0.13.9 2025-10-02 05:18:01 +00:00
Dax Raad
ad307f7f89 ci: sync 2025-10-02 01:09:58 -04:00
Dax Raad
eac11c0753 ci: stuff 2025-10-02 01:08:29 -04:00
Aiden Cline
0e804c302c docs: fix install fmt (#2914) 2025-10-01 23:32:39 -05:00
Aiden Cline
fb88cb0aa3 docs: fix more links (#2913) 2025-10-01 23:22:17 -05:00
Sandip Wane
8fc6a25142 chore: rm empty try-catch block (#2769)
Co-authored-by: Sandip Wane <sandip.wane@cloudhedge.io>
2025-10-01 23:21:57 -05:00
Dax Raad
5079ba7ce5 core: fix file search limit handling and ensure File module initialization 2025-10-02 00:18:18 -04:00
opencode
19cb211b62 release: v0.13.8 2025-10-02 04:16:48 +00:00
Dax Raad
b2440e92e7 core: improve file search reliability and performance 2025-10-02 00:09:02 -04:00
Aiden Cline
125624489b docs: fix (#2910) 2025-10-01 22:20:35 -05:00
Jay V
991f85c907 docs: hide email signup 2025-10-01 20:16:02 -04:00
Jay V
c00fbbdcae docs: testimonials 2025-10-01 19:57:17 -04:00
Dax Raad
d4e9c60af7 wip: lander 2025-10-01 19:54:58 -04:00
Jay V
0691815c0a docs: fix link 2025-10-01 19:52:31 -04:00
Dax Raad
985fd4d9a8 wip: lander 2025-10-01 19:49:00 -04:00
Dax Raad
87fa8dc70c ignore: fix 2025-10-01 19:40:56 -04:00
Dax
a782e3dac2 Zen lander (#2907)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
2025-10-01 19:38:15 -04:00
Frank
70da3a9399 wip: zen 2025-10-01 19:34:37 -04:00
Frank
1024537b47 doc: update zen pricing 2025-10-01 17:48:12 -04:00
GitHub Action
2a4f21a694 ignore: update download stats 2025-10-01 2025-10-01 12:04:26 +00:00
Dax Raad
5f61945090 core: remove redundant patch integration test
The integration test was duplicating coverage already provided by the comprehensive
patch namespace tests. Users benefit from faster test runs without losing any
coverage of patch functionality. The remaining tests provide complete validation
of patch parsing, application, and tool integration.
2025-10-01 06:49:19 -04:00
Dax Raad
41ce56494b core: make patch tool more reliable and consistent with other editing tools
The patch tool now works seamlessly alongside other file editing tools with improved
error handling and a more intuitive permission system. Users will experience:

- More reliable patch application with better error messages
- Consistent permission prompts that match other editing tools
- Smoother integration when applying complex multi-file changes
- Better feedback on what changes are being made before applying patches

This refactoring leverages the robust patch parsing engine while making the tool
feel native to the opencode workflow, reducing friction when making bulk changes
to your codebase.
2025-10-01 06:45:43 -04:00
opencode
172aeaaf14 release: v0.13.7 2025-10-01 09:13:16 +00:00
Dax Raad
bd69c5aca8 codex should not have reasoning effort 2025-10-01 05:06:37 -04:00
Dax Raad
6a7eeb39c3 core: prevent file deletion when reverting changes to existing files 2025-10-01 05:06:37 -04:00
opencode
35a608cd53 release: v0.13.6 2025-10-01 07:44:18 +00:00
Dax Raad
6e19200fca overhaul file search and support @ mentioning directories 2025-10-01 03:37:01 -04:00
Aiden Cline
fe45a76c55 fix: adjust model dialog to handle same model id but different names (#2881) 2025-09-30 11:43:57 -05:00
GitHub Action
bdac22cb07 ignore: update download stats 2025-09-30 2025-09-30 12:04:27 +00:00
Dax Raad
5a507023a6 update anthropic system prompts 2025-09-30 04:41:36 -04:00
Aiden Cline
c398485213 fix: tui stuck saying generating... even when it is done (#2872) 2025-09-29 23:55:47 -05:00
Aiden Cline
bc9ff7e99f fix: worktree cmd (#2870) 2025-09-29 22:21:54 -05:00
Frank
7447460b5a wip: zen 2025-09-29 14:17:53 -04:00
opencode
5345c828ca release: v0.13.5 2025-09-29 14:12:58 +00:00
Aiden Cline
edeaab321a fix: bash regex (#2858) 2025-09-29 08:51:46 -05:00
GitHub Action
478ead6a05 ignore: update download stats 2025-09-29 2025-09-29 12:04:29 +00:00
Giuseppe Rota
468201190e docs: document model id behavior (#2856) 2025-09-29 06:52:26 -05:00
opencode
cc0d460904 release: v0.13.4 2025-09-29 05:53:07 +00:00
Dax Raad
f8ab0de0ad ci: fix homebrew 2025-09-29 01:40:27 -04:00
Dax Raad
322363f11b release: v0.13.2 2025-09-29 01:38:53 -04:00
Dax Raad
acd33c2fc5 ci: publish 2025-09-29 01:24:35 -04:00
Dax Raad
b6fba03a7d ci: fix 2025-09-29 01:22:39 -04:00
Dax Raad
fbced21b8e ci: fix 2025-09-29 01:06:12 -04:00
Aiden Cline
e7cb5d8345 ignore: go mod tidy (#2851) 2025-09-28 23:05:56 -05:00
Frank
918739057d wip: zen 2025-09-28 19:55:40 -04:00
Frank
e3a7096e44 wip: zen 2025-09-28 19:55:40 -04:00
Dax Raad
c148f10bbd core: improve webfetch tool content negotiation and format handling 2025-09-28 17:57:50 -04:00
Frank
06495ea964 wip: zen 2025-09-28 12:49:28 -04:00
GitHub Action
b64cecb079 ignore: update download stats 2025-09-28 2025-09-28 12:03:57 +00:00
Frank
e10bb58cb3 wip: zen 2025-09-27 21:53:50 -04:00
Aiden Cline
89167ae387 respect model id in opencode.json (#2833) 2025-09-27 17:09:21 -05:00
Aurélien Tollard
4b429029df Fix: Set OPENCODE_CALLER env in a more portable way for vscode extension (#2230) 2025-09-27 10:36:24 -04:00
Frank
c7e5d29109 wip: zen 2025-09-27 10:19:58 -04:00
Frank
a564267b29 wip: zen 2025-09-27 10:05:19 -04:00
GitHub Action
594bdb43c2 ignore: update download stats 2025-09-27 2025-09-27 12:04:01 +00:00
Dax Raad
ea66c02633 ci: tweaks 2025-09-27 04:12:55 -04:00
Dax Raad
925ce6503e sync 2025-09-27 04:10:56 -04:00
Arjun Singh
8a28d34fe9 Include step-start and step-finish for cost tracking (#2810) 2025-09-27 03:45:36 -04:00
Dax Raad
8bea479df9 ci: wtf 2025-09-27 03:20:08 -04:00
Dax Raad
468d919a92 ci: fix 2025-09-27 03:19:34 -04:00
Dax Raad
7e5527379d core: configure turbo to avoid building opencode for web tests 2025-09-27 03:19:29 -04:00
Dax Raad
fcbc78180b ci: fix 2025-09-27 03:18:12 -04:00
Dax Raad
bab1ca54e4 ci: test 2025-09-27 03:17:22 -04:00
Dax Raad
d644e0b8a7 core: fix config test by removing model field expectation 2025-09-27 03:10:01 -04:00
Dax Raad
e54ec45002 ci: fix git identity in test workflow 2025-09-27 03:07:37 -04:00
Dax Raad
4b94d98f89 ci: improve test coverage 2025-09-27 03:04:42 -04:00
Dax Raad
d0043a4a78 sync 2025-09-27 02:53:20 -04:00
Dax Raad
26ebf85b0e ci: format 2025-09-27 02:22:03 -04:00
Dax Raad
53481f9790 wip: bun test improvements 2025-09-27 02:17:08 -04:00
Dax Raad
eadc2a8535 ci: give up 2025-09-27 01:51:54 -04:00
Dax Raad
00a5ec5bd2 ci: add regional hostname 2025-09-27 01:28:11 -04:00
Dax Raad
0b6b9062d9 fix zen cookie 2025-09-27 01:12:25 -04:00
opencode
c450549d0f release: v0.12.1 2025-09-26 21:18:45 +00:00
Frank
1ba0155943 zen: accept tax id 2025-09-26 17:10:45 -04:00
Timo Clasen
3d332a06b5 fix(tool): follow symlinks when looking for tools (#2809) 2025-09-26 16:01:08 -05:00
opencode
f709e0b48b release: v0.12.0 2025-09-26 20:55:00 +00:00
Frank
57e1bffbd5 zen: model management helper 2025-09-26 15:18:24 -04:00
sonsulee
f321661b4c docs: add TUI configuration options and examples (#2212)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Jay <air@live.ca>
2025-09-26 13:09:46 -04:00
Yihui Khuu
7ecdc1b5d8 fix: config loading not considering symlinks (#2800) 2025-09-26 09:46:49 -05:00
GitHub Action
39917a35ce ignore: update download stats 2025-09-26 2025-09-26 12:04:26 +00:00
Dax Raad
bfe3f03e03 ci: fix deploy 2025-09-26 11:57:49 +00:00
opencode
d01af65dbc release: v0.11.8 2025-09-26 11:57:49 +00:00
Dax Raad
80305813f5 disable aggressive config validation 2025-09-26 07:51:53 -04:00
opencode
061877e275 release: v0.11.7 2025-09-26 11:19:15 +00:00
Adam
05e6c3d8a0 fix(tui): cursor position 2025-09-26 06:13:23 -05:00
Dax Raad
093fbca711 core: add themes to allowed config directories 2025-09-26 06:40:41 -04:00
opencode
81deea855f release: v0.11.6 2025-09-26 09:52:23 +00:00
Dax Raad
5c67bebf86 tui: fix cursor position offset in home screen 2025-09-26 05:46:16 -04:00
opencode
9cc1f2884f release: v0.11.5 2025-09-26 09:40:16 +00:00
Dax Raad
f2b547cc45 fix erroring on custom tool folder 2025-09-26 05:33:38 -04:00
Dax Raad
70310a37b3 validate config directory 2025-09-26 03:23:25 -04:00
Dax Raad
eb7f4e20df core: add config update endpoint and functionality 2025-09-26 02:37:19 -04:00
Dax Raad
ea21bfd3c6 ci: ignore 2025-09-26 02:01:19 -04:00
Dax Raad
22d5be9bf8 ci: setup husky pre-push hook to run typecheck 2025-09-26 02:01:19 -04:00
opencode
1c878c662b release: v0.11.4 2025-09-26 05:56:03 +00:00
Dax Raad
55d154d4ac tui: fix opencode logo spacing in home view 2025-09-26 01:40:32 -04:00
Dax Raad
f5c7a94abe turn reasoning summaries back on by default for zen 2025-09-26 01:37:44 -04:00
Dax Raad
7ec3900208 core: enable reasoning.encrypted_content and reasoningSummary for opencode provider\ntui: adjust editorY position calculation 2025-09-26 01:27:53 -04:00
Aiden Cline
5d95846df1 fix: openai reasoning issue (#2780) 2025-09-26 01:23:30 -04:00
Aiden Cline
d47feb9969 tweak: include usage by default for openai compatible providers (#2788) 2025-09-25 21:06:58 -05:00
David Hill
8f135d13e3 Update opencode logo 2025-09-25 23:22:50 +01:00
Frank
f9ab4102f6 zen: track tps 2025-09-25 17:56:41 -04:00
Frank
f9117bcc7f zen: check balance on enable billing 2025-09-25 17:47:50 -04:00
Frank
6e712f9faf zen: fix parsing cache write tokens 2025-09-25 17:02:23 -04:00
Adam
b207ed2b7b wip: better desktop file status state and timeline 2025-09-25 14:41:31 -05:00
Adam
945de4eddc wip: watch select .git files in watcher 2025-09-25 14:41:31 -05:00
GitHub Action
cd655177d9 ignore: update download stats 2025-09-25 2025-09-25 12:04:27 +00:00
Frank
8f90497fc4 zen: billing 2025-09-24 21:26:04 -04:00
GitHub Action
9659efca46 chore: format code 2025-09-24 23:10:17 +00:00
Frank
d0377a95cf zen: billing 2025-09-24 19:09:28 -04:00
Adam
3b20bf6d4f fix(app): changes view 2025-09-24 15:49:40 -05:00
Adam
c3e52580b0 feat(app): changes view 2025-09-24 15:46:33 -05:00
Adam
2badfcdcf4 fix: select dialog hover 2025-09-24 15:08:58 -05:00
Adam
f589fc2327 feat: fuzzy file open 2025-09-24 12:40:54 -05:00
Filip
d3b6545e7c feat(app): added command palette (#2630)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-09-24 11:05:15 -05:00
GitHub Action
3f911b22b0 ignore: update download stats 2025-09-24 2025-09-24 12:04:37 +00:00
opencode
5199141369 release: v0.11.3 2025-09-24 03:43:46 +00:00
Dax Raad
d86d3e7ea1 update copilot auth 2025-09-23 23:36:27 -04:00
GitHub Action
fe8d29cb2b chore: format code 2025-09-23 21:59:09 +00:00
Frank
edd6198999 zen: refund 2025-09-23 17:58:27 -04:00
GitHub Action
c3b2c27997 chore: format code 2025-09-23 21:37:20 +00:00
Jay V
679aeb29f0 docs: add codex to zen 2025-09-23 17:36:40 -04:00
Jay V
190413580f docs: edit 2025-09-23 17:34:52 -04:00
Jay V
8c9fbc7717 docs: edits 2025-09-23 17:34:52 -04:00
Jay V
9d3fdda674 docs: edit 2025-09-23 17:34:52 -04:00
Siddhant Choudhary
449994f120 feat: output-format flag to stream json output (#2471)
Co-authored-by: Siddhant Choudhary <sid@treaps.com>
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-23 16:19:32 -05:00
opencode
d772fff776 release: v0.11.2 2025-09-23 21:03:59 +00:00
Dax Raad
71b43fd02e fix codex errors 2025-09-23 16:56:40 -04:00
Dax Raad
f40b91ab7a fix: remove file existence check from LSP debug and format storage code 2025-09-23 16:49:57 -04:00
Alain Schlesser
6404bd006d ignore: more reliable install script, handle non prettified json responses (#2745)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-23 14:58:04 -05:00
Frank
75157e515c wip: remove 2025-09-23 14:21:39 -04:00
Frank
5a96ee8e1b zen: update logged endpoints 2025-09-23 14:21:39 -04:00
Adam
ee6ceb4c64 fix: open text files 2025-09-23 12:43:24 -05:00
David Hill
9d53628e19 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-09-23 18:36:27 +01:00
David Hill
869b476145 ignore: Copy update 2025-09-23 18:35:42 +01:00
David Hill
223d487787 ignore: Footer update 2025-09-23 18:30:06 +01:00
Adam
5ead6d7dd5 fix: exclude generated css file 2025-09-23 12:08:49 -05:00
GitHub Action
a98454217f chore: format code 2025-09-23 17:05:32 +00:00
Adam
cbb75d8577 fix: theme css format 2025-09-23 12:04:50 -05:00
GitHub Action
4ab992a9a9 chore: format code 2025-09-23 16:41:57 +00:00
Adam
80b0a93d64 wip: desktop file updates 2025-09-23 11:41:15 -05:00
Grégoire Paris
e749d48534 docs: fix grammar mistake in lsp docs(#2744) 2025-09-23 08:12:46 -05:00
GitHub Action
d7e873f807 ignore: update download stats 2025-09-23 2025-09-23 12:04:24 +00:00
Aiden Cline
c23510346b ignore: lsp debug file check (#2743) 2025-09-22 22:16:03 -05:00
Aiden Cline
f9c5df05a1 docs: github copilot model enable note (#2741) 2025-09-22 21:29:16 -05:00
Aiden Cline
02b4d1e2fc fix: lsp extension undefined handle (#2739) 2025-09-22 21:14:55 -05:00
Joseph Hanson
0b36eb8760 docs: fix permissions link (#2738) 2025-09-22 20:28:54 -05:00
Adam Hosker
36bec9948c docs: Add editor setup tip (#2638) 2025-09-22 16:01:27 -04:00
GitHub Action
2db73c39df chore: format code 2025-09-22 19:59:29 +00:00
Jay V
6107666d04 docs: edit tools doc 2025-09-22 15:58:48 -04:00
Aiden Cline
cc2bd7141f fix: enforce extensions requirement for custom lsp servers (#2734) 2025-09-22 11:45:47 -05:00
GitHub Action
ee442975df ignore: update download stats 2025-09-22 2025-09-22 12:04:25 +00:00
Dax Raad
9b1a508657 ci: bump 2025-09-22 01:47:28 -04:00
Dax Raad
288c977596 ci: snapshot builds 2025-09-22 01:45:23 -04:00
iwauo
6b799b304c feat: add Java LSP server support (#2547)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-21 23:55:15 -05:00
Aiden Cline
92c126d875 fix: lsp spawn logic (#2723) 2025-09-21 11:25:47 -05:00
GitHub Action
7123fbeb47 ignore: update download stats 2025-09-21 2025-09-21 12:04:06 +00:00
opencode
84bb692193 release: v0.11.1 2025-09-21 08:51:11 +00:00
Dax Raad
079095d7a9 core: filter models without keys in opencode provider 2025-09-21 04:43:32 -04:00
opencode
28e1d67ea4 release: v0.11.0 2025-09-21 08:04:21 +00:00
GitHub Action
c1940d1d2c chore: format code 2025-09-21 04:23:40 +00:00
Frank
869f629c14 wip: zen 2025-09-21 00:23:05 -04:00
Frank
a55943e469 wip: zen 2025-09-21 00:06:50 -04:00
Aiden Cline
84d95a0d2a ignore: lsp log (#2715) 2025-09-20 23:02:06 -05:00
opencode
7dfed8ca35 release: v0.10.4 2025-09-20 23:00:32 +00:00
Dax Raad
38ea0fc051 turn back on compaction summaries 2025-09-20 18:52:30 -04:00
Osinachi Okpara
9223b6ed8f Enhance theme documentation with links (#2707)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-20 11:25:12 -05:00
GitHub Action
f8528c52d9 ignore: update download stats 2025-09-20 2025-09-20 12:04:03 +00:00
Aiden Cline
d63ce40af2 fix: no payment method (#2706) 2025-09-20 04:15:44 -04:00
Jay V
5acdd70587 docs: zen 2025-09-19 20:13:57 -04:00
1619 changed files with 45528 additions and 12473 deletions

59
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Bug report
description: Report an issue that should be fixed
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Description
description: Describe the bug you encountered
placeholder: What happened?
validations:
required: true
- type: input
id: opencode-version
attributes:
label: OpenCode version
description: What version of OpenCode are you using?
validations:
required: false
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: How can we reproduce this issue?
placeholder: |
1.
2.
3.
validations:
required: false
- type: textarea
id: screenshot-or-link
attributes:
label: Screenshot and/or share link
description: Run `/share` to get a share link, or attach a screenshot
placeholder: Paste link or drag and drop screenshot here
validations:
required: false
- type: input
id: os
attributes:
label: Operating System
description: what OS are you using?
placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11
validations:
required: false
- type: input
id: terminal
attributes:
label: Terminal
description: what terminal are you using?
placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 💬 Discord Community
url: https://discord.gg/opencode
about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question.

View File

@@ -0,0 +1,20 @@
name: 🚀 Feature Request
description: Suggest an idea, feature, or enhancement
labels: [discussion]
title: "[FEATURE]:"
body:
- type: checkboxes
id: verified
attributes:
label: Feature hasn't been suggested before.
options:
- label: I have verified this feature I'm about to request hasn't been suggested before.
required: true
- type: textarea
attributes:
label: Describe the enhancement you want to request
description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :)
validations:
required: true

11
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
name: Question
description: Ask a question
labels: ["question"]
body:
- type: textarea
id: question
attributes:
label: Question
description: What's your question?
validations:
required: true

20
.github/actions/setup-bun/action.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: "Setup Bun"
description: "Setup Bun with caching and install dependencies"
runs:
using: "composite"
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v4
with:
path: ~/.bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: bun install
shell: bash

View File

@@ -15,11 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.21
- run: bun install
- uses: ./.github/actions/setup-bun
- run: bun sst deploy --stage=${{ github.ref_name }}
env:

View File

@@ -20,13 +20,10 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.21
uses: ./.github/actions/setup-bun
- name: run
run: |
bun install
./script/format.ts
env:
CI: true

View File

@@ -8,7 +8,9 @@ jobs:
opencode:
if: |
contains(github.event.comment.body, ' /oc') ||
contains(github.event.comment.body, ' /opencode')
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -24,4 +26,4 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/sonic
model: opencode/kimi-k2

View File

@@ -19,16 +19,13 @@ jobs:
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.21
- uses: ./.github/actions/setup-bun
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce
- name: Publish
run: |
bun install
./script/publish
working-directory: ./sdks/vscode
env:

View File

@@ -35,18 +35,7 @@ jobs:
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.21
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-1-2-21-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-1-2-21-
- uses: ./.github/actions/setup-bun
- name: Install makepkg
run: |
@@ -60,14 +49,17 @@ jobs:
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- name: Install dependencies
run: bun install
- name: Install OpenCode
run: curl -fsSL https://opencode.ai/install | bash
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_CHANNEL: latest
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}

34
.github/workflows/snapshot.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: snapshot
on:
push:
branches:
- dev
- opentui
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: ./.github/actions/setup-bun
- name: Publish
run: |
./script/publish.ts
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -16,9 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
uses: ./.github/actions/setup-bun
- name: Run stats script
run: bun script/stats.ts

30
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: test
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: run
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun turbo typecheck
bun turbo test
env:
CI: true

View File

@@ -13,12 +13,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.21
- name: Install dependencies
run: bun install
uses: ./.github/actions/setup-bun
- name: Run typecheck
run: bun typecheck

2
.husky/pre-push Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
bun run typecheck

View File

@@ -13,7 +13,7 @@ avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is spearated by a divider of 3 dashes
Each section is separated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized

View File

@@ -1,3 +1,7 @@
---
description: Git commit and push
---
commit and push
make sure it includes a prefix like
@@ -7,3 +11,10 @@ core:
ci:
ignore:
wip:
For anything in the packages/web use the docs: prefix.
For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.

View File

@@ -0,0 +1,5 @@
---
description: Spellcheck all markdown file changes
---
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.

View File

@@ -1,11 +0,0 @@
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "call this tool when you want to give up",
args: {
message: tool.schema.string().describe("give up message"),
},
async execute(args) {
return "Hey fuck you!"
},
})

View File

@@ -14,3 +14,34 @@
## Debugging
- To test opencode in the `packages/opencode` directory you can run `bun dev`
## Tool Calling
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environnement:
json
{
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
}

71
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,71 @@
# Contributing to OpenCode
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
- Bug fixes
- Additional LSPs / Formatters
- Improvements to LLM performance
- Support for new providers
- Fixes for environment-specific quirks
- Missing standard behavior
- Documentation improvements
However, any UI or core product feature must go through a design review with the core team before implementation.
> [!IMPORTANT]
> We do not accept PRs for core features.
Take a look at recent git history to understand what usually lands.
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with either of the following labels:
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
## Developing OpenCode
- Requirements: Bun 1.3+, Go 1.24.x.
- Install dependencies and start the dev server from the repo root:
```bash
bun install
bun dev
```
- Core pieces:
- `packages/opencode`: OpenCode core business logic & server.
- `packages/tui`: The TUI code, written in Go (will be removed soon in favor of [opentui](https://github.com/sst/opentui))
- `packages/plugin`: Source for `@opencode-ai/plugin`
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, the OpenCode team must regenerate the Stainless SDK before any client updates merge.
## Pull Request Expectations
- Try to keep pull requests small and focused.
- Link relevant issue(s) in the description
- Explain the issue and why your change fixes it
- Avoid having verbose LLM generated PR descriptions
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
### Style Preferences
These are not strictly enforced, they are just general guidelines:
- **Functions:** Keep logic within a single function unless breaking it out adds clear reuse or composition benefits.
- **Destructuring:** Do not do unnecessary destructuring of variables.
- **Control flow:** Avoid `else` statements.
- **Error handling:** Prefer `.catch(...)` instead of `try`/`catch` when possible.
- **Types:** Reach for precise types and avoid `any`.
- **Variables:** Stick to immutable patterns and avoid `let`.
- **Naming:** Choose concise single-word identifiers when they remain descriptive.
- **Runtime APIs:** Use Bun helpers such as `Bun.file()` when they fit the use case.
## Feature Requests
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.

View File

@@ -1,20 +1,20 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">AI coding agent, built for the terminal.</p>
<p align="center">The AI coding agent built for the terminal.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
[![opencode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
@@ -26,6 +26,8 @@ curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
winget install opencode # Windows
brew install sst/tap/opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
```
@@ -50,45 +52,11 @@ XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
### Documentation
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
### Contributing
opencode is an opinionated tool so any fundamental feature needs to go through a
design process with the core team.
> [!IMPORTANT]
> We do not accept PRs for core features.
However we still merge a ton of PRs - you can contribute:
- Bug fixes
- Improvements to LLM performance
- Support for new providers
- Fixes for env specific quirks
- Missing standard behavior
- Documentation
Take a look at the git history to see what kind of PRs we end up merging.
> [!NOTE]
> If you do not follow the above guidelines we might close your PR.
To run opencode locally you need.
- Bun
- Golang 1.24.x
And run.
```bash
$ bun install
$ bun dev
```
#### Development Notes
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
### FAQ
@@ -97,9 +65,10 @@ $ bun dev
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- Out of the box LSP support
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
#### What's the other repo?

199
STATS.md
View File

@@ -1,86 +1,117 @@
# 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) |
| 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) |

2211
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,11 @@ branding:
inputs:
model:
description: "The model to use with opencode. Takes the format of `provider/model`."
description: "Model to use"
required: true
share:
description: "Whether to share the opencode session. Defaults to true for public repositories."
required: false
token:
description: "Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. Defaults to the installation access token from the opencode GitHub App."
description: "Share the opencode session (defaults to true for public repos)"
required: false
runs:
@@ -24,20 +20,10 @@ runs:
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Install bun
shell: bash
run: npm install -g bun
- name: Install dependencies
shell: bash
run: |
cd ${GITHUB_ACTION_PATH}
bun install
- name: Run opencode
shell: bash
run: bun ${GITHUB_ACTION_PATH}/index.ts
id: run_opencode
run: opencode github run
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
TOKEN: ${{ inputs.token }}

View File

@@ -14,6 +14,6 @@
"@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@opencode-ai/sdk": "0.5.4"
"@opencode-ai/sdk": "workspace:*"
}
}

2
github/sst-env.d.ts vendored
View File

@@ -6,4 +6,4 @@
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
export {}

View File

@@ -2,6 +2,8 @@ import { domain } from "./stage"
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY")
const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET")
const bucket = new sst.cloudflare.Bucket("Bucket")
export const api = new sst.cloudflare.Worker("Api", {
@@ -11,7 +13,7 @@ export const api = new sst.cloudflare.Worker("Api", {
WEB_DOMAIN: domain,
},
url: true,
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY],
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET],
transform: {
worker: (args) => {
args.logpush = true

View File

@@ -1,5 +1,5 @@
import { WebhookEndpoint } from "pulumi-stripe"
import { domain } from "./stage"
import { EMAILOCTOPUS_API_KEY } from "./app"
////////////////
// DATABASE
@@ -68,13 +68,14 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
// GATEWAY
////////////////
export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", {
url: $interpolate`https://${domain}/stripe/webhook`,
enabledEvents: [
"checkout.session.async_payment_failed",
"checkout.session.async_payment_succeeded",
"checkout.session.completed",
"checkout.session.expired",
"charge.refunded",
"customer.created",
"customer.deleted",
"customer.updated",
@@ -93,9 +94,6 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
"customer.subscription.resumed",
"customer.subscription.trial_will_end",
"customer.subscription.updated",
"customer.tax_id.created",
"customer.tax_id.deleted",
"customer.tax_id.updated",
],
})
@@ -112,6 +110,9 @@ const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
// CONSOLE
////////////////
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
let logProcessor
if ($app.stage === "production" || $app.stage === "frank") {
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
@@ -124,7 +125,16 @@ if ($app.stage === "production" || $app.stage === "frank") {
new sst.cloudflare.x.SolidStart("Console", {
domain,
path: "packages/console/app",
link: [database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, ZEN_MODELS],
link: [
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ZEN_MODELS,
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),
//VITE_API_URL: gateway.url.apply((url) => url!),

View File

@@ -2,7 +2,7 @@ import { domain } from "./stage"
new sst.cloudflare.StaticSite("Desktop", {
domain: "desktop." + domain,
path: "packages/app",
path: "packages/desktop",
build: {
command: "bun turbo build",
output: "./dist",

View File

@@ -3,3 +3,11 @@ export const domain = (() => {
if ($app.stage === "dev") return "dev.opencode.ai"
return `${$app.stage}.dev.opencode.ai`
})()
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
new cloudflare.RegionalHostname("RegionalHostname", {
hostname: domain,
regionKey: "us",
zoneId: zoneID,
})

View File

@@ -46,7 +46,7 @@ mkdir -p "$INSTALL_DIR"
if [ -z "$requested_version" ]; then
url="https://github.com/sst/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
@@ -96,6 +96,7 @@ download_and_install() {
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
mv opencode "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
cd .. && rm -rf opencodetmp
}

View File

@@ -3,42 +3,58 @@
"name": "opencode",
"private": true,
"type": "module",
"packageManager": "bun@1.2.21",
"packageManager": "bun@1.3.0",
"scripts": {
"dev": "bun run --conditions=development packages/opencode/src/index.ts",
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun turbo typecheck",
"generate": "(cd packages/sdk && ./js/script/generate.ts) && (cd packages/sdk/stainless && ./generate.ts)",
"postinstall": "./script/hooks"
"prepare": "husky"
},
"workspaces": {
"packages": [
"packages/*",
"packages/console/*",
"packages/sdk/js"
"packages/sdk/js",
"packages/slack"
],
"catalog": {
"@types/bun": "1.2.21",
"@types/bun": "1.3.0",
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.8",
"hono": "4.7.10",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-js": "1.9.9"
"solid-js": "1.9.9",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.42.3",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8"
}
},
"dependencies": {
"pulumi-stripe": "0.0.24"
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.13",
"sst": "3.17.19",
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*"
},
"repository": {
"type": "git",
"url": "https://github.com/sst/opencode"
@@ -58,5 +74,9 @@
],
"patchedDependencies": {
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
},
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:"
}
}

View File

@@ -1,24 +0,0 @@
<!doctype html>
<html lang="en" class="h-full bg-background">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.svg" />
<link rel="stylesheet" href="/src/assets/theme.css" />
<title>opencode</title>
</head>
<body class="h-full overscroll-none select-none">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "opencode"
const savedDarkMode = localStorage.getItem("darkMode") !== "false"
document.documentElement.setAttribute("data-theme", savedTheme)
document.documentElement.setAttribute("data-dark", savedDarkMode.toString())
})()
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,163 +0,0 @@
import type { Plugin } from "vite"
import { readdir, readFile, writeFile } from "fs/promises"
import { join, resolve } from "path"
interface ThemeDefinition {
$schema?: string
defs?: Record<string, string>
theme: Record<string, any>
}
interface ResolvedThemeColor {
dark: string
light: string
}
class ColorResolver {
private colors: Map<string, any> = new Map()
private visited: Set<string> = new Set()
constructor(defs: Record<string, string> = {}, theme: Record<string, any> = {}) {
Object.entries(defs).forEach(([key, value]) => {
this.colors.set(key, value)
})
Object.entries(theme).forEach(([key, value]) => {
this.colors.set(key, value)
})
}
resolveColor(key: string, value: any): ResolvedThemeColor {
if (this.visited.has(key)) {
throw new Error(`Circular reference detected for color ${key}`)
}
this.visited.add(key)
try {
if (typeof value === "string") {
if (value === "none") return { dark: value, light: value }
if (value.startsWith("#")) {
return { dark: value.toUpperCase(), light: value.toUpperCase() }
}
const resolved = this.resolveReference(value)
return { dark: resolved, light: resolved }
}
if (typeof value === "object" && value !== null) {
const dark = this.resolveColorValue(value.dark || value.light || "#000000")
const light = this.resolveColorValue(value.light || value.dark || "#FFFFFF")
return { dark, light }
}
return { dark: "#000000", light: "#FFFFFF" }
} finally {
this.visited.delete(key)
}
}
private resolveColorValue(value: any): string {
if (typeof value === "string") {
if (value === "none") return value
if (value.startsWith("#")) {
return value.toUpperCase()
}
return this.resolveReference(value)
}
return value
}
private resolveReference(ref: string): string {
const colorValue = this.colors.get(ref)
if (colorValue === undefined) {
throw new Error(`Color reference '${ref}' not found`)
}
if (typeof colorValue === "string") {
if (colorValue === "none") return colorValue
if (colorValue.startsWith("#")) {
return colorValue.toUpperCase()
}
return this.resolveReference(colorValue)
}
return colorValue
}
}
function kebabCase(str: string): string {
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
}
function parseTheme(themeData: ThemeDefinition): Record<string, ResolvedThemeColor> {
const resolver = new ColorResolver(themeData.defs, themeData.theme)
const colors: Record<string, ResolvedThemeColor> = {}
Object.entries(themeData.theme).forEach(([key, value]) => {
colors[key] = resolver.resolveColor(key, value)
})
return colors
}
async function loadThemes(): Promise<Record<string, Record<string, ResolvedThemeColor>>> {
const themesDir = resolve(__dirname, "../../tui/internal/theme/themes")
const files = await readdir(themesDir)
const themes: Record<string, Record<string, ResolvedThemeColor>> = {}
for (const file of files) {
if (!file.endsWith(".json")) continue
const themeName = file.replace(".json", "")
const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8"))
themes[themeName] = parseTheme(themeData)
}
return themes
}
function generateCSS(themes: Record<string, Record<string, ResolvedThemeColor>>): string {
let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n`
const defaultTheme = themes["opencode"] || Object.values(themes)[0]
if (defaultTheme) {
Object.entries(defaultTheme).forEach(([key, color]) => {
const cssVar = `--theme-${kebabCase(key)}`
css += ` ${cssVar}: ${color.light};\n`
})
}
css += `}\n\n`
Object.entries(themes).forEach(([themeName, colors]) => {
css += `[data-theme="${themeName}"][data-dark="false"] {\n`
Object.entries(colors).forEach(([key, color]) => {
const cssVar = `--theme-${kebabCase(key)}`
css += ` ${cssVar}: ${color.light};\n`
})
css += `}\n\n`
css += `[data-theme="${themeName}"][data-dark="true"] {\n`
Object.entries(colors).forEach(([key, color]) => {
const cssVar = `--theme-${kebabCase(key)}`
css += ` ${cssVar}: ${color.dark};\n`
})
css += `}\n\n`
})
return css
}
export function generateThemeCSS(): Plugin {
return {
name: "generate-theme-css",
async buildStart() {
try {
console.log("Generating theme CSS...")
const themes = await loadThemes()
const css = generateCSS(themes)
const outputPath = resolve(__dirname, "../src/assets/theme.css")
await writeFile(outputPath, css)
console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`)
console.log(` Output: ${outputPath}`)
} catch (error) {
throw new Error(`Theme CSS generation failed: ${error}`)
}
},
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,601 +0,0 @@
import { transformerNotationDiff } from "@shikijs/transformers"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import { codeToHtml } from "shiki"
import { createResource } from "solid-js"
const markedWithShiki = marked.use(
markedShiki({
highlight(code, lang) {
return codeToHtml(code, {
// structure: "inline",
lang: lang || "text",
tabindex: false,
theme: {
colors: {
"actionBar.toggledBackground": "var(--theme-background-element)",
"activityBarBadge.background": "var(--theme-accent)",
"checkbox.border": "var(--theme-border)",
"editor.background": "transparent",
"editor.foreground": "var(--theme-text)",
"editor.inactiveSelectionBackground": "var(--theme-background-element)",
"editor.selectionHighlightBackground": "var(--theme-border-active)",
"editorIndentGuide.activeBackground1": "var(--theme-border-subtle)",
"editorIndentGuide.background1": "var(--theme-border-subtle)",
"input.placeholderForeground": "var(--theme-text-muted)",
"list.activeSelectionIconForeground": "var(--theme-text)",
"list.dropBackground": "var(--theme-background-element)",
"menu.background": "var(--theme-background-panel)",
"menu.border": "var(--theme-border)",
"menu.foreground": "var(--theme-text)",
"menu.selectionBackground": "var(--theme-primary)",
"menu.separatorBackground": "var(--theme-border)",
"ports.iconRunningProcessForeground": "var(--theme-success)",
"sideBarSectionHeader.background": "transparent",
"sideBarSectionHeader.border": "var(--theme-border-subtle)",
"sideBarTitle.foreground": "var(--theme-text-muted)",
"statusBarItem.remoteBackground": "var(--theme-success)",
"statusBarItem.remoteForeground": "var(--theme-text)",
"tab.lastPinnedBorder": "var(--theme-border-subtle)",
"tab.selectedBackground": "var(--theme-background-element)",
"tab.selectedForeground": "var(--theme-text-muted)",
"terminal.inactiveSelectionBackground": "var(--theme-background-element)",
"widget.border": "var(--theme-border)",
},
displayName: "opencode",
name: "opencode",
semanticHighlighting: true,
semanticTokenColors: {
customLiteral: "var(--theme-syntax-function)",
newOperator: "var(--theme-syntax-operator)",
numberLiteral: "var(--theme-syntax-number)",
stringLiteral: "var(--theme-syntax-string)",
},
tokenColors: [
{
scope: [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python",
],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "emphasis",
settings: {
fontStyle: "italic",
},
},
{
scope: "strong",
settings: {
fontStyle: "bold",
},
},
{
scope: "header",
settings: {
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "comment",
settings: {
foreground: "var(--theme-syntax-comment)",
},
},
{
scope: "constant.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent",
],
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "constant.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.tag",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["entity.name.tag.css", "entity.name.tag.less"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.other.attribute-name",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "invalid",
settings: {
foreground: "var(--theme-error)",
},
},
{
scope: "markup.underline",
settings: {
fontStyle: "underline",
},
},
{
scope: "markup.bold",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-strong)",
},
},
{
scope: "markup.heading",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "markup.italic",
settings: {
fontStyle: "italic",
},
},
{
scope: "markup.strikethrough",
settings: {
fontStyle: "strikethrough",
},
},
{
scope: "markup.inserted",
settings: {
foreground: "var(--theme-diff-added)",
},
},
{
scope: "markup.deleted",
settings: {
foreground: "var(--theme-diff-removed)",
},
},
{
scope: "markup.changed",
settings: {
foreground: "var(--theme-diff-context)",
},
},
{
scope: "punctuation.definition.quote.begin.markdown",
settings: {
foreground: "var(--theme-markdown-block-quote)",
},
},
{
scope: "punctuation.definition.list.begin.markdown",
settings: {
foreground: "var(--theme-markdown-list-enumeration)",
},
},
{
scope: "markup.inline.raw",
settings: {
foreground: "var(--theme-markdown-code)",
},
},
{
scope: "punctuation.definition.tag",
settings: {
foreground: "var(--theme-syntax-punctuation)",
},
},
{
scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "meta.preprocessor.string",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "meta.preprocessor.numeric",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "meta.structure.dictionary.key.python",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "meta.diff.header",
settings: {
foreground: "var(--theme-diff-hunk-header)",
},
},
{
scope: "storage",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "storage.type",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["storage.modifier", "keyword.operator.noexcept"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["string", "meta.embedded.assembly"],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.tag",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.value",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["meta.template.expression"],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "keyword",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.control",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.operator",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.other.unit",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "support.function.git-rebase",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "constant.sha.git-rebase",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java",
],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "variable.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal",
],
settings: {
foreground: "var(--theme-syntax-function)",
},
},
{
scope: [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"keyword.control",
"source.cpp keyword.operator.new",
"keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["variable.other.constant", "variable.other.enummember"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["meta.object-literal.key"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "keyword.operator.quantifier.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["constant.character", "constant.other.option"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "constant.character.escape",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.label",
settings: {
foreground: "var(--theme-text-muted)",
},
},
],
type: "dark",
},
transformers: [transformerNotationDiff()],
})
},
}),
)
function strip(text: string): string {
const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/
const match = text.match(wrappedRe)
return match ? match[2] : text
}
export default function Markdown(props: { text: string; class?: string }) {
const [html] = createResource(
() => strip(props.text),
async (markdown) => {
return markedWithShiki.parse(markdown)
},
)
return (
<div
class={`min-w-0 max-w-full text-xs overflow-auto no-scrollbar prose ${props.class ?? ""}`}
innerHTML={html()}
/>
)
}

View File

@@ -1,188 +0,0 @@
import { Select as KobalteSelect } from "@kobalte/core/select"
import { createEffect, createMemo, Show } from "solid-js"
import type { ComponentProps } from "solid-js"
import { Icon } from "@/ui/icon"
import fuzzysort from "fuzzysort"
import { pipe, groupBy, entries, map } from "remeda"
import { createStore } from "solid-js/store"
export interface SelectProps<T> {
variant?: "default" | "outline"
size?: "sm" | "md" | "lg"
placeholder?: string
filter?:
| false
| {
placeholder?: string
keys: string[]
}
options: T[]
current?: T
value?: (x: T) => string
label?: (x: T) => string
groupBy?: (x: T) => string
onFilter?: (query: string) => void
onSelect?: (value: T | undefined) => void
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
}
export function Select<T>(props: SelectProps<T>) {
let inputRef: HTMLInputElement | undefined = undefined
let listboxRef: HTMLUListElement | undefined = undefined
const [store, setStore] = createStore({
filter: "",
})
const grouped = createMemo(() => {
const needle = store.filter.toLowerCase()
const result = pipe(
props.options,
(x) =>
!needle || !props.filter
? x
: fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj),
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
// mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
entries(),
map(([k, v]) => ({ category: k, options: v })),
)
return result
})
// const flat = createMemo(() => {
// return pipe(
// grouped(),
// flatMap(({ options }) => options),
// )
// })
createEffect(() => {
store.filter
listboxRef?.scrollTo(0, 0)
// setStore("selected", 0)
// scroll.scrollTo(0)
})
return (
<KobalteSelect<T, { category: string; options: T[] }>
value={props.current}
options={grouped()}
optionValue={(x) => (props.value ? props.value(x) : (x as string))}
optionTextValue={(x) => (props.label ? props.label(x) : (x as string))}
optionGroupChildren="options"
placeholder={props.placeholder}
sectionComponent={(props) => (
<KobalteSelect.Section class="text-xs uppercase text-text-muted/60 font-light mt-3 first:mt-0 ml-2">
{props.section.rawValue.category}
</KobalteSelect.Section>
)}
itemComponent={(itemProps) => (
<KobalteSelect.Item
classList={{
"relative flex cursor-pointer select-none items-center": true,
"rounded-sm px-2 py-0.5 text-xs outline-none text-text": true,
"transition-colors data-[disabled]:pointer-events-none": true,
"data-[highlighted]:bg-background-element data-[disabled]:opacity-50": true,
[props.class ?? ""]: !!props.class,
}}
{...itemProps}
>
<KobalteSelect.ItemLabel>
{props.label ? props.label(itemProps.item.rawValue) : (itemProps.item.rawValue as string)}
</KobalteSelect.ItemLabel>
<KobalteSelect.ItemIndicator
classList={{
"ml-auto": true,
}}
>
<Icon name="checkmark" size={16} />
</KobalteSelect.ItemIndicator>
</KobalteSelect.Item>
)}
onChange={(v) => {
if (props.onSelect) props.onSelect(v ?? undefined)
if (v !== null) {
// close the select
}
}}
onOpenChange={(v) => v || setStore("filter", "")}
>
<KobalteSelect.Trigger
classList={{
...(props.classList ?? {}),
"flex w-full items-center justify-between rounded-md transition-colors": true,
"focus-visible:outline-none focus-visible:ring focus-visible:ring-border-active/30": true,
"disabled:cursor-not-allowed disabled:opacity-50": true,
"data-[placeholder-shown]:text-text-muted cursor-pointer": true,
"hover:bg-background-element focus-visible:ring-border-active": true,
"bg-background-element text-text": props.variant === "default" || !props.variant,
"border-2 border-border bg-transparent text-text": props.variant === "outline",
"h-6 pl-2 text-xs": props.size === "sm",
"h-8 pl-3 text-sm": props.size === "md" || !props.size,
"h-10 pl-4 text-base": props.size === "lg",
[props.class ?? ""]: !!props.class,
}}
>
<KobalteSelect.Value<T>>
{(state) => {
const selected = state.selectedOption() ?? props.current
if (!selected) return props.placeholder || ""
if (props.label) return props.label(selected)
return selected as string
}}
</KobalteSelect.Value>
<KobalteSelect.Icon
classList={{
"size-fit shrink-0 text-text-muted transition-transform duration-100 data-[expanded]:rotate-180": true,
}}
>
<Icon name="chevron-down" size={24} />
</KobalteSelect.Icon>
</KobalteSelect.Trigger>
<KobalteSelect.Portal>
<KobalteSelect.Content
onKeyDown={(e) => {
if (!props.filter) return
if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
return
}
inputRef?.focus()
}}
classList={{
"min-w-32 overflow-hidden rounded-md border border-border-subtle/40": true,
"bg-background-panel p-1 shadow-md z-50": true,
"data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95": true,
"data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
}}
>
<Show when={props.filter}>
<input
ref={(el) => (inputRef = el)}
id="select-filter"
type="text"
placeholder={props.filter ? props.filter.placeholder : "Filter items"}
value={store.filter}
onInput={(e) => setStore("filter", e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Escape") {
e.preventDefault()
e.stopPropagation()
listboxRef?.focus()
}
}}
classList={{
"w-full": true,
"px-2 pb-2 text-text font-light placeholder-text-muted/70 text-xs focus:outline-none": true,
}}
/>
</Show>
<KobalteSelect.Listbox
ref={(el) => (listboxRef = el)}
classList={{
"overflow-y-auto max-h-48 no-scrollbar": true,
}}
/>
</KobalteSelect.Content>
</KobalteSelect.Portal>
</KobalteSelect>
)
}

View File

@@ -1,28 +0,0 @@
import { useSync, useLocal } from "@/context"
import { Button, Tooltip } from "@/ui"
import { VList } from "virtua/solid"
export default function SessionList() {
const sync = useSync()
const local = useLocal()
return (
<VList data={sync.data.session} class="p-2 no-scrollbar">
{(session) => (
<Tooltip placement="right" value={session.title} class="w-full min-w-0">
<Button
size="sm"
variant="ghost"
classList={{
"w-full min-w-0 py-1 text-left truncate justify-start text-text-muted text-xs": true,
"text-text!": local.session.active()?.id === session.id,
}}
onClick={() => local.session.setActive(session.id)}
>
<span class="truncate">{session.title}</span>
</Button>
</Tooltip>
)}
</VList>
)
}

View File

@@ -1,369 +0,0 @@
import { useLocal, useSync } from "@/context"
import { Collapsible, Icon, type IconProps } from "@/ui"
import type { Part, ToolPart } from "@opencode-ai/sdk"
import { DateTime } from "luxon"
import {
createSignal,
onMount,
For,
Match,
splitProps,
Switch,
type ComponentProps,
type ParentProps,
createEffect,
createMemo,
} from "solid-js"
import { getFilename } from "@/utils"
import Markdown from "./markdown"
import { Code } from "./code"
import { createElementSize } from "@solid-primitives/resize-observer"
import { createScrollPosition } from "@solid-primitives/scroll"
function TimelineIcon(props: { name: IconProps["name"]; class?: string }) {
return (
<div
classList={{
"relative flex flex-none self-start items-center justify-center bg-background h-6 w-6": true,
[props.class ?? ""]: !!props.class,
}}
>
<Icon name={props.name} class="text-text/40" size={18} />
</div>
)
}
function CollapsibleTimelineIcon(props: { name: IconProps["name"]; class?: string }) {
return (
<>
<TimelineIcon
name={props.name}
class={`group-hover/li:hidden group-has-[[data-expanded]]/li:hidden ${props.class ?? ""}`}
/>
<TimelineIcon
name="chevron-right"
class={`hidden group-hover/li:flex group-has-[[data-expanded]]/li:hidden ${props.class ?? ""}`}
/>
<TimelineIcon name="chevron-down" class={`hidden group-has-[[data-expanded]]/li:flex ${props.class ?? ""}`} />
</>
)
}
function ToolIcon(props: { part: ToolPart }) {
return (
<Switch fallback={<TimelineIcon name="hammer" />}>
<Match when={props.part.tool === "read"}>
<TimelineIcon name="file" />
</Match>
<Match when={props.part.tool === "edit"}>
<CollapsibleTimelineIcon name="pencil" />
</Match>
<Match when={props.part.tool === "write"}>
<CollapsibleTimelineIcon name="file-plus" />
</Match>
</Switch>
)
}
function Part(props: ParentProps & ComponentProps<"div">) {
const [local, others] = splitProps(props, ["class", "classList", "children"])
return (
<div
classList={{
...(local.classList ?? {}),
"h-6 flex items-center": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
>
<p class="text-xs leading-4 text-left text-text-muted/60 font-medium">{local.children}</p>
</div>
)
}
function CollapsiblePart(props: { title: ParentProps["children"] } & ParentProps & ComponentProps<typeof Collapsible>) {
return (
<Collapsible {...props}>
<Collapsible.Trigger class="peer/collapsible">
<Part>{props.title}</Part>
</Collapsible.Trigger>
<Collapsible.Content>
<p class="flex-auto py-1 text-xs min-w-0 text-pretty">
<span class="text-text-muted/60 break-words">{props.children}</span>
</p>
</Collapsible.Content>
</Collapsible>
)
}
function ReadToolPart(props: { part: ToolPart }) {
const local = useLocal()
return (
<Switch>
<Match when={props.part.state.status === "completed" && props.part.state}>
{(state) => {
const path = state().input["filePath"] as string
return (
<Part class="cursor-pointer" onClick={() => local.file.open(path)}>
<span class="text-text-muted">Read</span> {getFilename(path)}
</Part>
)
}}
</Match>
</Switch>
)
}
function EditToolPart(props: { part: ToolPart }) {
return (
<Switch>
<Match when={props.part.state.status === "completed" && props.part.state}>
{(state) => (
<CollapsiblePart
defaultOpen
title={
<>
<span class="text-text-muted">Edit</span> {getFilename(state().input["filePath"] as string)}
</>
}
>
<Code
path={state().input["filePath"] as string}
code={state().metadata["diff"] as string}
class="[&_code]:pb-0!"
/>
</CollapsiblePart>
)}
</Match>
</Switch>
)
}
function WriteToolPart(props: { part: ToolPart }) {
return (
<Switch>
<Match when={props.part.state.status === "completed" && props.part.state}>
{(state) => (
<CollapsiblePart
title={
<>
<span class="text-text-muted">Write</span> {getFilename(state().input["filePath"] as string)}
</>
}
>
<div class="p-2 bg-background-panel rounded-md border border-border-subtle"></div>
</CollapsiblePart>
)}
</Match>
</Switch>
)
}
function ToolPart(props: { part: ToolPart }) {
return (
<Switch
fallback={
<div class="flex-auto min-w-0 text-xs">
{props.part.type}:{props.part.tool}
</div>
}
>
<Match when={props.part.tool === "read"}>
<div class="min-w-0 flex-auto">
<ReadToolPart part={props.part} />
</div>
</Match>
<Match when={props.part.tool === "edit"}>
<div class="min-w-0 flex-auto">
<EditToolPart part={props.part} />
</div>
</Match>
<Match when={props.part.tool === "write"}>
<div class="min-w-0 flex-auto">
<WriteToolPart part={props.part} />
</div>
</Match>
</Switch>
)
}
export default function SessionTimeline(props: { session: string; class?: string }) {
const sync = useSync()
const [scrollElement, setScrollElement] = createSignal<HTMLElement | undefined>(undefined)
const [root, setRoot] = createSignal<HTMLDivElement | undefined>(undefined)
const [tail, setTail] = createSignal(true)
const size = createElementSize(root)
const scroll = createScrollPosition(scrollElement)
onMount(() => sync.session.sync(props.session))
const messages = createMemo(() => sync.data.message[props.session] ?? [])
const working = createMemo(() => {
const last = messages()[messages().length - 1]
if (!last) return false
if (last.role === "user") return true
return !last.time.completed
})
const getScrollParent = (el: HTMLElement | null): HTMLElement | undefined => {
let p = el?.parentElement
while (p && p !== document.body) {
const s = getComputedStyle(p)
if (s.overflowY === "auto" || s.overflowY === "scroll") return p
p = p.parentElement
}
return undefined
}
createEffect(() => {
if (!root()) return
setScrollElement(getScrollParent(root()!))
})
const scrollToBottom = () => {
const element = scrollElement()
if (!element) return
element.scrollTop = element.scrollHeight
}
createEffect(() => {
size.height
if (tail()) scrollToBottom()
})
createEffect(() => {
if (working()) {
setTail(true)
scrollToBottom()
}
})
let lastScrollY = 0
createEffect(() => {
if (scroll.y < lastScrollY) {
setTail(false)
}
lastScrollY = scroll.y
})
const valid = (part: Part) => {
if (!part) return false
switch (part.type) {
case "step-start":
case "step-finish":
case "file":
case "patch":
return false
case "text":
return !part.synthetic
case "reasoning":
return part.text.trim()
default:
return true
}
}
const duration = (part: Part) => {
switch (part.type) {
default:
if (
"time" in part &&
part.time &&
"start" in part.time &&
part.time.start &&
"end" in part.time &&
part.time.end
) {
const start = DateTime.fromMillis(part.time.start)
const end = DateTime.fromMillis(part.time.end)
return end.diff(start).toFormat("s")
}
return ""
}
}
return (
<div
ref={setRoot}
classList={{
"p-4 select-text flex flex-col gap-y-8": true,
[props.class ?? ""]: !!props.class,
}}
>
<For each={messages()}>
{(message) => (
<ul role="list" class="space-y-2">
<For each={sync.data.part[message.id]?.filter(valid)}>
{(part) => (
<li classList={{ "relative group/li flex gap-x-4 min-w-0 w-full": true }}>
<div
classList={{
"absolute top-0 left-0 flex w-6 justify-center": true,
"last:h-10 not-last:-bottom-10": true,
}}
>
<div class="w-px bg-border-subtle" />
</div>
<Switch
fallback={
<div class="m-0.5 relative flex size-5 flex-none items-center justify-center bg-background">
<div class="size-1 rounded-full bg-text/10 ring ring-text/20" />
</div>
}
>
<Match when={part.type === "text"}>
<Switch>
<Match when={message.role === "user"}>
<TimelineIcon name="avatar-square" />
</Match>
<Match when={message.role === "assistant"}>
<TimelineIcon name="sparkles" />
</Match>
</Switch>
</Match>
<Match when={part.type === "reasoning"}>
<CollapsibleTimelineIcon name="brain" />
</Match>
<Match when={part.type === "tool" && part}>{(part) => <ToolIcon part={part()} />}</Match>
</Switch>
<Switch fallback={<div class="flex-auto min-w-0 text-xs mt-1 text-left">{part.type}</div>}>
<Match when={part.type === "text" && part}>
{(part) => (
<Switch>
<Match when={message.role === "user"}>
<div class="w-full flex flex-col items-end justify-stretch gap-y-1.5 min-w-0">
<p class="w-full rounded-md p-3 ring-1 ring-text/15 ring-inset text-xs bg-background-panel">
<span class="font-medium text-text whitespace-pre-wrap break-words">{part().text}</span>
</p>
<p class="text-xs text-text-muted">12:07pm · adam</p>
</div>
</Match>
<Match when={message.role === "assistant"}>
<Markdown text={part().text} class="text-text" />
</Match>
</Switch>
)}
</Match>
<Match when={part.type === "reasoning" && part}>
{(part) => (
<CollapsiblePart
title={
<>
<span class="text-text-muted">Thought</span> for {duration(part())}s
</>
}
>
<Markdown text={part().text} />
</CollapsiblePart>
)}
</Match>
<Match when={part.type === "tool" && part}>{(part) => <ToolPart part={part()} />}</Match>
</Switch>
</li>
)}
</For>
</ul>
)}
</For>
</div>
)
}

View File

@@ -1,48 +0,0 @@
import { For } from "solid-js"
import { Icon, Link, Logo, Tooltip } from "@/ui"
import { useLocation } from "@solidjs/router"
const navigation = [
{ name: "Sessions", href: "/sessions", icon: "dashboard" as const },
{ name: "Commands", href: "/commands", icon: "slash" as const },
{ name: "Agents", href: "/agents", icon: "bolt" as const },
{ name: "Providers", href: "/providers", icon: "cloud" as const },
{ name: "Tools (MCP)", href: "/tools", icon: "hammer" as const },
{ name: "LSP", href: "/lsp", icon: "code" as const },
{ name: "Settings", href: "/settings", icon: "settings" as const },
]
export default function SidebarNav() {
const location = useLocation()
return (
<div class="hidden md:fixed md:inset-y-0 md:left-0 md:z-50 md:block md:w-16 md:overflow-y-auto md:bg-background-panel md:pb-4">
<div class="flex h-16 shrink-0 items-center justify-center">
<Logo variant="mark" size={28} />
</div>
<nav class="mt-5">
<ul role="list" class="flex flex-col items-center space-y-1">
<For each={navigation}>
{(item) => (
<li>
<Tooltip placement="right" value={item.name}>
<Link
href={item.href}
classList={{
"bg-background-element text-text": location.pathname.startsWith(item.href),
"text-text-muted hover:bg-background-element hover:text-text": location.pathname !== item.href,
"flex gap-x-3 rounded-md p-3 text-sm font-semibold": true,
"focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-border-active": true,
}}
>
<Icon name={item.icon} size={20} />
<span class="sr-only">{item.name}</span>
</Link>
</Tooltip>
</li>
)}
</For>
</ul>
</nav>
</div>
)
}

View File

@@ -1,4 +0,0 @@
export { LocalProvider, useLocal } from "./local"
export { SDKProvider, useSDK } from "./sdk"
export { SyncProvider, useSync } from "./sync"
export { ThemeProvider, useTheme } from "./theme"

View File

@@ -1,418 +0,0 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
import { useSync } from "./sync"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode } from "@opencode-ai/sdk"
import { useSDK } from "./sdk"
export type LocalFile = FileNode &
Partial<{
loaded: boolean
pinned: boolean
expanded: boolean
content: FileContent
selection: { startLine: number; startChar: number; endLine: number; endChar: number }
scrollTop: number
view: "raw" | "diff-unified" | "diff-split"
folded: string[]
selectedChange: number
}>
export type TextSelection = LocalFile["selection"]
export type View = LocalFile["view"]
function init() {
const sdk = useSDK()
const sync = useSync()
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
const agent = (() => {
const [store, setStore] = createStore<{
current: string
}>({
current: list()[0].name,
})
return {
list,
current() {
return list().find((x) => x.name === store.current)!
},
set(name: string | undefined) {
setStore("current", name ?? list()[0].name)
},
move(direction: 1 | -1) {
let next = list().findIndex((x) => x.name === store.current) + direction
if (next < 0) next = list().length - 1
if (next >= list().length) next = 0
const value = list()[next]
setStore("current", value.name)
if (value.model)
model.set({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
},
}
})()
const model = (() => {
const [store, setStore] = createStore<{
model: Record<
string,
{
providerID: string
modelID: string
}
>
recent: {
providerID: string
modelID: string
}[]
}>({
model: {},
recent: [],
})
const value = localStorage.getItem("model")
setStore("recent", JSON.parse(value ?? "[]"))
createEffect(() => {
localStorage.setItem("model", JSON.stringify(store.recent))
})
const fallback = createMemo(() => {
if (store.recent.length) return store.recent[0]
const provider = sync.data.provider[0]
const model = Object.values(provider.models)[0]
return {
providerID: provider.id,
modelID: model.id,
}
})
const current = createMemo(() => {
const a = agent.current()
return store.model[agent.current().name] ?? (a.model ? a.model : fallback())
})
const list = createMemo(() =>
sync.data.provider.flatMap((x) => Object.values(x.models).map((m) => ({ providerID: x.id, modelID: m.id }))),
)
return {
list,
current,
recent() {
return store.recent
},
parsed: createMemo(() => {
const value = current()
const provider = sync.data.provider.find((x) => x.id === value.providerID)!
const model = provider.models[value.modelID]
return {
provider: provider.name ?? value.providerID,
model: model.name ?? value.modelID,
}
}),
set(model: { providerID: string; modelID: string } | undefined, options?: { recent?: boolean }) {
batch(() => {
setStore("model", agent.current().name, model ?? fallback())
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
setStore("recent", uniq)
}
})
},
}
})()
const file = (() => {
const [store, setStore] = createStore<{
node: Record<string, LocalFile>
opened: string[]
active?: string
}>({
node: Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
opened: [],
})
const active = createMemo(() => {
if (!store.active) return undefined
return store.node[store.active]
})
const opened = createMemo(() => store.opened.map((x) => store.node[x]))
const changes = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
const status = (path: string) => sync.data.changes.find((f) => f.path === path)
const changed = (path: string) => {
const set = changes()
if (set.has(path)) return true
for (const p of set) {
if (p.startsWith(path ? path + "/" : "")) return true
}
return false
}
const resetNode = (path: string) => {
setStore("node", path, {
loaded: undefined,
pinned: undefined,
content: undefined,
selection: undefined,
scrollTop: undefined,
folded: undefined,
view: undefined,
selectedChange: undefined,
})
}
const load = async (path: string) =>
sdk.file.read({ query: { path } }).then((x) => {
setStore(
"node",
path,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
const open = async (path: string) => {
const relative = path.replace(sync.data.path.directory + "/", "")
if (!store.node[relative]) {
const parent = relative.split("/").slice(0, -1).join("/")
if (parent) {
await list(parent)
}
}
setStore("opened", (x) => {
if (x.includes(relative)) return x
return [
...opened()
.filter((x) => x.pinned)
.map((x) => x.path),
relative,
]
})
setStore("active", relative)
if (store.node[relative].loaded) return
return load(relative)
}
const list = async (path: string) => {
return sdk.file.list({ query: { path: path + "/" } }).then((x) => {
setStore(
"node",
produce((draft) => {
x.data!.forEach((node) => {
if (node.path in draft) return
draft[node.path] = node
})
}),
)
})
}
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "message.part.updated":
const part = event.properties.part
if (part.type === "tool" && part.state.status === "completed") {
switch (part.tool) {
case "read":
console.log("read", part.state.input)
break
case "edit":
const absolute = part.state.input["filePath"] as string
const path = absolute.replace(sync.data.path.directory + "/", "")
load(path)
break
default:
break
}
}
break
}
}
})
return {
active,
opened,
node: (path: string) => store.node[path],
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
open,
load,
close(path: string) {
setStore("opened", (opened) => opened.filter((x) => x !== path))
if (store.active === path) {
const index = store.opened.findIndex((f) => f === path)
const previous = store.opened[Math.max(0, index - 1)]
setStore("active", previous)
}
resetNode(path)
},
expand(path: string) {
setStore("node", path, "expanded", true)
if (store.node[path].loaded) return
setStore("node", path, "loaded", true)
list(path)
},
collapse(path: string) {
setStore("node", path, "expanded", false)
},
select(path: string, selection: TextSelection | undefined) {
setStore("node", path, "selection", selection)
},
scroll(path: string, scrollTop: number) {
setStore("node", path, "scrollTop", scrollTop)
},
move(path: string, to: number) {
const index = store.opened.findIndex((f) => f === path)
if (index === -1) return
setStore(
"opened",
produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0])
}),
)
setStore("node", path, "pinned", true)
},
view(path: string): View {
const n = store.node[path]
return n && n.view ? n.view : "raw"
},
setView(path: string, view: View) {
setStore("node", path, "view", view)
},
unfold(path: string, key: string) {
setStore("node", path, "folded", (xs) => {
const a = xs ?? []
if (a.includes(key)) return a
return [...a, key]
})
},
fold(path: string, key: string) {
setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key))
},
folded(path: string) {
const n = store.node[path]
return n && n.folded ? n.folded : []
},
changeIndex(path: string) {
return store.node[path]?.selectedChange
},
setChangeIndex(path: string, index: number | undefined) {
setStore("node", path, "selectedChange", index)
},
changed,
status,
children(path: string) {
return Object.values(store.node).filter(
(x) =>
x.path.startsWith(path) &&
x.path !== path &&
!x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
)
},
}
})()
const layout = (() => {
const [store, setStore] = createStore<{
rightPane: boolean
leftWidth: number
rightWidth: number
}>({
rightPane: false,
leftWidth: 200, // Default 50 * 4px (w-50 = 12.5rem = 200px)
rightWidth: 320, // Default 80 * 4px (w-80 = 20rem = 320px)
})
const value = localStorage.getItem("layout")
if (value) {
const v = JSON.parse(value)
if (typeof v?.rightPane === "boolean") setStore("rightPane", v.rightPane)
if (typeof v?.leftWidth === "number") setStore("leftWidth", Math.max(150, Math.min(400, v.leftWidth)))
if (typeof v?.rightWidth === "number") setStore("rightWidth", Math.max(200, Math.min(500, v.rightWidth)))
}
createEffect(() => {
localStorage.setItem("layout", JSON.stringify(store))
})
return {
rightPane() {
return store.rightPane
},
leftWidth() {
return store.leftWidth
},
rightWidth() {
return store.rightWidth
},
toggleRightPane() {
setStore("rightPane", (x) => !x)
},
openRightPane() {
setStore("rightPane", true)
},
closeRightPane() {
setStore("rightPane", false)
},
setLeftWidth(width: number) {
setStore("leftWidth", Math.max(150, Math.min(400, width)))
},
setRightWidth(width: number) {
setStore("rightWidth", Math.max(200, Math.min(500, width)))
},
}
})()
const session = (() => {
const [store, setStore] = createStore<{
active?: string
}>({})
const active = createMemo(() => {
if (!store.active) return undefined
return sync.session.get(store.active)
})
return {
active,
setActive(sessionId: string | undefined) {
setStore("active", sessionId)
},
clearActive() {
setStore("active", undefined)
},
}
})()
const result = {
model,
agent,
file,
layout,
session,
}
return result
}
type LocalContext = ReturnType<typeof init>
const ctx = createContext<LocalContext>()
export function LocalProvider(props: ParentProps) {
const value = init()
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useLocal() {
const value = useContext(ctx)
if (!value) {
throw new Error("useLocal must be used within a LocalProvider")
}
return value
}

View File

@@ -1,165 +0,0 @@
import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "./sdk"
import { createContext, Show, useContext, type ParentProps } from "solid-js"
import { Binary } from "@/utils/binary"
function init() {
const [store, setStore] = createStore<{
ready: boolean
provider: Provider[]
agent: Agent[]
config: Config
path: Path
session: Session[]
message: {
[sessionID: string]: Message[]
}
part: {
[messageID: string]: Part[]
}
node: FileNode[]
changes: File[]
}>({
config: {},
path: { state: "", config: "", worktree: "", directory: "" },
ready: false,
agent: [],
provider: [],
session: [],
message: {},
part: {},
node: [],
changes: [],
})
const sdk = useSDK()
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "session.updated": {
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)
}),
)
break
}
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
break
}
setStore(
"message",
event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
setStore("part", event.properties.part.messageID, [event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
break
}
setStore(
"part",
event.properties.part.messageID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.part)
}),
)
break
}
}
}
})
Promise.all([
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.path.get().then((x) => setStore("path", x.data!)),
sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
sdk.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).slice().sort((a, b) => a.id.localeCompare(b.id)),
),
),
sdk.config.get().then((x) => setStore("config", x.data!)),
sdk.file.status().then((x) => setStore("changes", x.data!)),
sdk.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
]).then(() => setStore("ready", true))
return {
data: store,
set: setStore,
session: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)
if (match.found) return store.session[match.index]
return undefined
},
async sync(sessionID: string) {
const [session, messages] = await Promise.all([
sdk.session.get({ path: { id: sessionID } }),
sdk.session.messages({ path: { id: sessionID } }),
])
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
draft.session[match.index] = session.data!
draft.message[sessionID] = messages
.data!.map((x) => x.info)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
}
}),
)
},
},
}
}
type SyncContext = ReturnType<typeof init>
const ctx = createContext<SyncContext>()
export function SyncProvider(props: ParentProps) {
const value = init()
return (
<Show when={value.data.ready}>
<ctx.Provider value={value}>{props.children}</ctx.Provider>
</Show>
)
}
export function useSync() {
const value = useContext(ctx)
if (!value) {
throw new Error("useSync must be used within a SyncProvider")
}
return value
}

View File

@@ -1,92 +0,0 @@
import {
createContext,
useContext,
createSignal,
createEffect,
onMount,
type ParentComponent,
onCleanup,
} from "solid-js"
export interface ThemeContextValue {
theme: string | undefined
isDark: boolean
setTheme: (themeName: string) => void
setDarkMode: (isDark: boolean) => void
}
const ThemeContext = createContext<ThemeContextValue>()
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider")
}
return context
}
interface ThemeProviderProps {
defaultTheme?: string
defaultDarkMode?: boolean
}
const themes = ["opencode", "tokyonight", "ayu", "nord", "catppuccin"]
export const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => {
const [theme, setThemeSignal] = createSignal<string | undefined>()
const [isDark, setIsDark] = createSignal(props.defaultDarkMode ?? false)
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "t" && event.ctrlKey) {
event.preventDefault()
const current = theme()
if (!current) return
const index = themes.indexOf(current)
const next = themes[(index + 1) % themes.length]
setTheme(next)
}
}
onMount(() => {
window.addEventListener("keydown", handleKeyDown)
})
onCleanup(() => {
window.removeEventListener("keydown", handleKeyDown)
})
onMount(() => {
const savedTheme = localStorage.getItem("theme") ?? "opencode"
const savedDarkMode = localStorage.getItem("darkMode") ?? "true"
setIsDark(savedDarkMode === "true")
setTheme(savedTheme)
})
createEffect(() => {
const currentTheme = theme()
const darkMode = isDark()
if (currentTheme) {
document.documentElement.setAttribute("data-theme", currentTheme)
document.documentElement.setAttribute("data-dark", darkMode.toString())
}
})
const setTheme = async (theme: string) => {
setThemeSignal(theme)
localStorage.setItem("theme", theme)
}
const setDarkMode = (dark: boolean) => {
setIsDark(dark)
localStorage.setItem("darkMode", dark.toString())
}
const contextValue: ThemeContextValue = {
theme: theme(),
isDark: isDark(),
setTheme,
setDarkMode,
}
return <ThemeContext.Provider value={contextValue}>{props.children}</ThemeContext.Provider>
}

View File

@@ -1,155 +0,0 @@
@import "tailwindcss";
:root {
interpolate-size: allow-keywords;
}
@layer components {
[data-popper-positioner] {
pointer-events: none;
}
body {
line-height: 1;
}
::selection {
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
/* background-color: var(--color-primary); */
/* color: var(--color-background); */
}
.prose h1 {
color: var(--color-text);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
margin-bottom: calc(var(--spacing) * 3);
}
.prose h2 {
color: var(--color-text);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
margin-bottom: calc(var(--spacing) * 3);
}
.prose h3 {
color: var(--color-text);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
margin-bottom: calc(var(--spacing) * 2);
}
.prose h4 {
color: var(--color-text);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
margin-bottom: calc(var(--spacing) * 2);
}
.prose h5 {
color: var(--color-text);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
margin-bottom: calc(var(--spacing) * 2);
}
.prose h6 {
color: var(--color-text);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
margin-bottom: calc(var(--spacing) * 2);
}
.prose p {
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
margin-bottom: calc(var(--spacing) * 2);
}
.prose strong {
color: var(--color-text);
}
.prose ul,
ol {
list-style-type: disc;
list-style-position: inside;
margin-bottom: calc(var(--spacing) * 2);
}
.prose pre {
background-color: var(--color-background-panel);
padding: calc(var(--spacing) * 2);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-subtle);
overflow-x: auto;
white-space: pre;
margin-bottom: calc(var(--spacing) * 2);
@apply no-scrollbar;
}
.prose code {
font-family: var(--font-mono);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
}
.prose blockquote {
margin-bottom: calc(var(--spacing) * 2);
}
}
@utility no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
& {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
@theme {
--color-*: initial;
--color-primary: var(--theme-primary);
--color-secondary: var(--theme-secondary);
--color-accent: var(--theme-accent);
--color-error: var(--theme-error);
--color-warning: var(--theme-warning);
--color-success: var(--theme-success);
--color-info: var(--theme-info);
--color-text: var(--theme-text);
--color-text-muted: var(--theme-text-muted);
--color-background: var(--theme-background);
--color-background-panel: var(--theme-background-panel);
--color-background-element: var(--theme-background-element);
--color-border: var(--theme-border);
--color-border-active: var(--theme-border-active);
--color-border-subtle: var(--theme-border-subtle);
--color-diff-added: var(--theme-diff-added);
--color-diff-removed: var(--theme-diff-removed);
--color-diff-context: var(--theme-diff-context);
--color-diff-hunk-header: var(--theme-diff-hunk-header);
--color-diff-highlight-added: var(--theme-diff-highlight-added);
--color-diff-highlight-removed: var(--theme-diff-highlight-removed);
--color-diff-added-bg: var(--theme-diff-added-bg);
--color-diff-removed-bg: var(--theme-diff-removed-bg);
--color-diff-context-bg: var(--theme-diff-context-bg);
--color-diff-line-number: var(--theme-diff-line-number);
--color-diff-added-line-number-bg: var(--theme-diff-added-line-number-bg);
--color-diff-removed-line-number-bg: var(--theme-diff-removed-line-number-bg);
--color-markdown-text: var(--theme-markdown-text);
--color-markdown-heading: var(--theme-markdown-heading);
--color-markdown-link: var(--theme-markdown-link);
--color-markdown-link-text: var(--theme-markdown-link-text);
--color-markdown-code: var(--theme-markdown-code);
--color-markdown-block-quote: var(--theme-markdown-block-quote);
--color-markdown-emph: var(--theme-markdown-emph);
--color-markdown-strong: var(--theme-markdown-strong);
--color-markdown-horizontal-rule: var(--theme-markdown-horizontal-rule);
--color-markdown-list-item: var(--theme-markdown-list-item);
--color-markdown-list-enumeration: var(--theme-markdown-list-enumeration);
--color-markdown-image: var(--theme-markdown-image);
--color-markdown-image-text: var(--theme-markdown-image-text);
--color-markdown-code-block: var(--theme-markdown-code-block);
--color-syntax-comment: var(--theme-syntax-comment);
--color-syntax-keyword: var(--theme-syntax-keyword);
--color-syntax-function: var(--theme-syntax-function);
--color-syntax-variable: var(--theme-syntax-variable);
--color-syntax-string: var(--theme-syntax-string);
--color-syntax-number: var(--theme-syntax-number);
--color-syntax-type: var(--theme-syntax-type);
--color-syntax-operator: var(--theme-syntax-operator);
--color-syntax-punctuation: var(--theme-syntax-punctuation);
}

View File

@@ -1,34 +0,0 @@
/* @refresh reload */
import { render } from "solid-js/web"
import { Router, Route } from "@solidjs/router"
import "@/index.css"
import Layout from "@/pages/layout"
import Home from "@/pages"
import { SDKProvider, SyncProvider, LocalProvider, ThemeProvider } from "@/context"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
)
}
render(
() => (
<div class="h-full bg-background text-text-muted">
<ThemeProvider defaultTheme="opencode" defaultDarkMode={true}>
<SDKProvider>
<SyncProvider>
<LocalProvider>
<Router root={Layout}>
<Route path="/" component={Home} />
</Router>
</LocalProvider>
</SyncProvider>
</SDKProvider>
</ThemeProvider>
</div>
),
root!,
)

View File

@@ -1,617 +0,0 @@
import { FileIcon, Icon, IconButton, Logo, Tooltip } from "@/ui"
import { Tabs } from "@/ui/tabs"
import { Select } from "@/components/select"
import FileTree from "@/components/file-tree"
import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
import { useLocal, useSDK } from "@/context"
import { Code } from "@/components/code"
import {
DragDropProvider,
DragDropSensors,
DragOverlay,
SortableProvider,
createSortable,
closestCenter,
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { LocalFile } from "@/context/local"
import SessionList from "@/components/session-list"
import SessionTimeline from "@/components/session-timeline"
import { createStore } from "solid-js/store"
export default function Page() {
const sdk = useSDK()
const local = useLocal()
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
activeItem: undefined as string | undefined,
prompt: "",
dragging: undefined as "left" | "right" | undefined,
})
let inputRef: HTMLInputElement | undefined = undefined
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
onMount(() => {
document.addEventListener("keydown", handleKeyDown)
})
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
})
const handleKeyDown = (e: KeyboardEvent) => {
const inputFocused = document.activeElement === inputRef
if (inputFocused) {
if (e.key === "Escape") {
inputRef?.blur()
}
return
}
if (document.activeElement?.id === "select-filter") {
return
}
if (local.file.active()) {
if (e.getModifierState(MOD)) {
if (e.key.toLowerCase() === "a") {
return
}
if (e.key.toLowerCase() === "c") {
return
}
}
}
if (e.key.length === 1 && e.key !== "Unidentified") {
inputRef?.focus()
}
}
const navigateChange = (dir: 1 | -1) => {
const active = local.file.active()
if (!active) return
const current = local.file.changeIndex(active.path)
const next = current == undefined ? (dir === 1 ? 0 : -1) : current + dir
local.file.setChangeIndex(active.path, next)
}
const resetClickTimer = () => {
if (!store.clickTimer) return
clearTimeout(store.clickTimer)
setStore("clickTimer", undefined)
}
const startClickTimer = () => {
const newClickTimer = setTimeout(() => {
setStore("clickTimer", undefined)
}, 300)
setStore("clickTimer", newClickTimer as unknown as number)
}
const handleFileClick = async (file: LocalFile) => {
if (store.clickTimer) {
resetClickTimer()
local.file.update(file.path, { ...file, pinned: true })
} else {
local.file.open(file.path)
startClickTimer()
}
}
const handleTabChange = (path: string) => {
local.file.open(path)
}
const handleTabClose = (file: LocalFile) => {
local.file.close(file.path)
}
const onDragStart = (event: any) => {
setStore("activeItem", event.draggable.id as string)
}
const onDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const currentFiles = local.file.opened().map((f) => f.path)
const fromIndex = currentFiles.indexOf(draggable.id.toString())
const toIndex = currentFiles.indexOf(droppable.id.toString())
if (fromIndex !== toIndex) {
local.file.move(draggable.id.toString(), toIndex)
}
}
}
const onDragEnd = () => {
setStore("activeItem", undefined)
}
const handleLeftDragStart = (e: MouseEvent) => {
e.preventDefault()
setStore("dragging", "left")
const startX = e.clientX
const startWidth = local.layout.leftWidth()
const handleMouseMove = (e: MouseEvent) => {
const deltaX = e.clientX - startX
const newWidth = startWidth + deltaX
local.layout.setLeftWidth(newWidth)
}
const handleMouseUp = () => {
setStore("dragging", undefined)
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
}
const handleRightDragStart = (e: MouseEvent) => {
e.preventDefault()
setStore("dragging", "right")
const startX = e.clientX
const startWidth = local.layout.rightWidth()
const handleMouseMove = (e: MouseEvent) => {
const deltaX = startX - e.clientX
const newWidth = startWidth + deltaX
local.layout.setRightWidth(newWidth)
}
const handleMouseUp = () => {
setStore("dragging", undefined)
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
}
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
const prompt = store.prompt
setStore("prompt", "")
inputRef?.blur()
const session =
(local.layout.rightPane() ? local.session.active() : undefined) ??
(await sdk.session.create().then((x) => x.data!))
local.session.setActive(session!.id)
local.layout.openRightPane()
const response = await sdk.session.prompt({
path: { id: session!.id },
body: {
agent: local.agent.current()!.name,
model: local.model.current(),
parts: [
{
type: "text",
text: prompt,
},
...local.file
.opened()
.filter((f) => f.selection || local.file.active()?.path === f.path)
.flatMap((f) => [
{
type: "file" as const,
mime: "text/plain",
url: `file://${f.absolute}${f.selection ? `?start=${f.selection.startLine}&end=${f.selection.endLine}` : ""}`,
filename: f.name,
source: {
type: "file" as const,
text: {
value: "@" + f.name,
start: 0, // f.start,
end: 0, // f.end,
},
path: f.absolute,
},
},
]),
],
},
})
console.log("response", response)
}
return (
<div class="relative">
<div
class="fixed top-0 left-0 h-full border-r border-border-subtle/30 flex flex-col overflow-hidden"
style={`width: ${local.layout.leftWidth()}px`}
>
<Tabs class="relative flex flex-col h-full" defaultValue="files">
<div class="sticky top-0 shrink-0 flex">
<Tabs.List class="grow w-full after:hidden">
<Tabs.Trigger value="files" class="flex-1 justify-center text-xs">
Files
</Tabs.Trigger>
<Tabs.Trigger value="changes" class="flex-1 justify-center text-xs">
Changes
</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.Content value="files" class="grow min-h-0 py-2 bg-background">
<FileTree path="" onFileClick={handleFileClick} />
</Tabs.Content>
<Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
<div class="px-2 text-xs text-text-muted">No changes yet</div>
</Tabs.Content>
</Tabs>
</div>
<div
class="fixed top-0 h-full w-1.5 bg-transparent cursor-col-resize z-50 group"
style={`left: ${local.layout.leftWidth()}px`}
onMouseDown={(e) => handleLeftDragStart(e)}
>
<div
classList={{
"w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors": true,
"bg-border-active!": store.dragging === "left",
}}
/>
</div>
<Show when={local.layout.rightPane()}>
<div
class="fixed top-0 right-0 h-full border-l border-border-subtle/30 flex flex-col overflow-hidden"
style={`width: ${local.layout.rightWidth()}px`}
>
<div class="relative flex-1 min-h-0 overflow-y-auto no-scrollbar">
<Show when={local.session.active()} fallback={<SessionList />}>
{(activeSession) => (
<div class="relative">
<div class="sticky top-0 bg-background z-50 px-2 h-8 border-b border-border-subtle/30">
<div class="h-full flex items-center gap-2">
<IconButton
size="xs"
variant="ghost"
onClick={() => local.session.clearActive()}
class="text-text-muted hover:text-text"
>
<Icon name="arrow-left" size={14} />
</IconButton>
<h2 class="text-sm font-medium text-text truncate">
{activeSession().title || "Untitled Session"}
</h2>
</div>
</div>
<SessionTimeline session={activeSession().id} />
</div>
)}
</Show>
</div>
</div>
<div
class="fixed top-0 h-full w-1.5 bg-transparent cursor-col-resize z-50 group flex justify-end"
style={`right: ${local.layout.rightWidth()}px`}
onMouseDown={(e) => handleRightDragStart(e)}
>
<div
classList={{
"w-0.5 h-full bg-transparent group-hover:bg-border-active transition-colors": true,
"bg-border-active!": store.dragging === "right",
}}
/>
</div>
</Show>
<div
class="relative"
style={`margin-left: ${local.layout.leftWidth()}px; margin-right: ${local.layout.rightPane() ? local.layout.rightWidth() : 0}px`}
>
<Logo
size={64}
variant="ornate"
class="absolute top-2/5 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
/>
<DragDropProvider
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs
class="relative grow w-full flex flex-col h-screen"
value={local.file.active()?.path}
onChange={handleTabChange}
>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List class="grow">
<SortableProvider ids={local.file.opened().map((f) => f.path)}>
<For each={local.file.opened()}>
{(file) => <SortableTab file={file} onTabClick={handleFileClick} onTabClose={handleTabClose} />}
</For>
</SortableProvider>
</Tabs.List>
<div class="shrink-0 h-full flex items-center gap-1 px-2 border-b border-border-subtle/40">
<Show when={local.file.active() && local.file.active()!.content?.diff}>
{(() => {
const f = local.file.active()!
const view = local.file.view(f.path)
return (
<div class="flex items-center gap-1">
<Show when={view !== "raw"}>
<div class="mr-1 flex items-center gap-1">
<Tooltip value="Previous change" placement="bottom">
<IconButton size="xs" variant="ghost" onClick={() => navigateChange(-1)}>
<Icon name="arrow-up" size={14} />
</IconButton>
</Tooltip>
<Tooltip value="Next change" placement="bottom">
<IconButton size="xs" variant="ghost" onClick={() => navigateChange(1)}>
<Icon name="arrow-down" size={14} />
</IconButton>
</Tooltip>
</div>
</Show>
<Tooltip value="Raw" placement="bottom">
<IconButton
size="xs"
variant="ghost"
classList={{
"text-text": view === "raw",
"text-text-muted/70": view !== "raw",
"bg-background-element": view === "raw",
}}
onClick={() => local.file.setView(f.path, "raw")}
>
<Icon name="file-text" size={14} />
</IconButton>
</Tooltip>
<Tooltip value="Unified diff" placement="bottom">
<IconButton
size="xs"
variant="ghost"
classList={{
"text-text": view === "diff-unified",
"text-text-muted/70": view !== "diff-unified",
"bg-background-element": view === "diff-unified",
}}
onClick={() => local.file.setView(f.path, "diff-unified")}
>
<Icon name="checklist" size={14} />
</IconButton>
</Tooltip>
<Tooltip value="Split diff" placement="bottom">
<IconButton
size="xs"
variant="ghost"
classList={{
"text-text": view === "diff-split",
"text-text-muted/70": view !== "diff-split",
"bg-background-element": view === "diff-split",
}}
onClick={() => local.file.setView(f.path, "diff-split")}
>
<Icon name="columns" size={14} />
</IconButton>
</Tooltip>
</div>
)
})()}
</Show>
<Tooltip value={local.layout.rightPane() ? "Close pane" : "Open pane"} placement="bottom">
<IconButton size="xs" variant="ghost" onClick={() => local.layout.toggleRightPane()}>
<Icon name={local.layout.rightPane() ? "close-pane" : "open-pane"} size={14} />
</IconButton>
</Tooltip>
</div>
</div>
<For each={local.file.opened()}>
{(file) => (
<Tabs.Content value={file.path} class="grow h-full pt-1 select-text">
{(() => {
const view = local.file.view(file.path)
const showRaw = view === "raw" || !file.content?.diff
const code = showRaw ? (file.content?.content ?? "") : (file.content?.diff ?? "")
return <Code path={file.path} code={code} />
})()}
</Tabs.Content>
)}
</For>
</Tabs>
<DragOverlay>
{store.activeItem &&
(() => {
const draggedFile = local.file.node(store.activeItem!)
return (
<div
class="relative px-3 h-8 flex items-center
text-sm font-medium text-text whitespace-nowrap
shrink-0 bg-background-panel
border-x border-border-subtle/40 border-b border-b-transparent"
>
<TabVisual file={draggedFile} />
</div>
)
})()}
</DragOverlay>
</DragDropProvider>
<form
onSubmit={handleSubmit}
class="peer/editor absolute inset-x-4 z-50 flex items-center justify-center"
classList={{
"bottom-8": !!local.file.active(),
"bottom-2/5": local.file.active() === undefined,
}}
>
<div
class="w-full max-w-xl min-w-0 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
flex flex-col gap-1
bg-gradient-to-b from-background-panel/90 to-background/90
ring-1 ring-border-active/50 border border-transparent
shadow-[0_0_33px_rgba(0,0,0,0.8)]
focus-within:ring-2 focus-within:ring-primary/40 focus-within:border-primary"
>
<div class="flex flex-wrap gap-1">
<Show when={local.file.active()}>
<FileTag
default
file={local.file.active()!}
onClose={() => local.file.close(local.file.active()?.path ?? "")}
/>
</Show>
<For each={local.file.opened().filter((x) => x.selection)}>
{(file) => <FileTag file={file} onClose={() => local.file.select(file.path, undefined)} />}
</For>
</div>
<input
ref={(el) => (inputRef = el)}
type="text"
value={store.prompt}
onInput={(e) => setStore("prompt", e.currentTarget.value)}
placeholder="It all starts with a prompt..."
class="w-full p-1 pb-4 text-text font-light placeholder-text-muted/70 text-sm focus:outline-none"
/>
<div class="flex justify-between items-center text-xs text-text-muted">
<div class="flex gap-2 items-center">
<Select
options={local.agent.list().map((a) => a.name)}
current={local.agent.current().name}
onSelect={local.agent.set}
size="sm"
class="uppercase"
/>
<Select
options={local.model.list()}
current={local.model.current()}
onSelect={local.model.set}
label={(x) => x.modelID}
value={(x) => `${x.providerID}.${x.modelID}`}
filter={{
keys: ["providerID", "modelID"],
placeholder: "Filter models",
}}
groupBy={(x) => x.providerID}
size="sm"
class="uppercase"
/>
<span class="text-text-muted/70">{local.model.parsed().provider}</span>
</div>
<div class="flex gap-1 items-center">
<IconButton class="text-text-muted" size="xs" variant="ghost">
<Icon name="photo" size={16} />
</IconButton>
<IconButton class="text-background-panel! bg-primary rounded-full!" size="xs" variant="ghost">
<Icon name="arrow-up" size={14} />
</IconButton>
</div>
</div>
</div>
</form>
</div>
</div>
)
}
const TabVisual = (props: { file: LocalFile }) => {
const local = useLocal()
return (
<div class="flex items-center gap-x-1.5">
<FileIcon node={props.file} class="" />
<span
classList={{ "text-xs": true, "text-primary": local.file.changed(props.file.path), italic: !props.file.pinned }}
>
{props.file.name}
</span>
<span class="text-xs opacity-70">
<Switch>
<Match when={local.file.status(props.file.path)?.status === "modified"}>
<span class="text-primary">M</span>
</Match>
<Match when={local.file.status(props.file.path)?.status === "added"}>
<span class="text-success">A</span>
</Match>
<Match when={local.file.status(props.file.path)?.status === "deleted"}>
<span class="text-error">D</span>
</Match>
</Switch>
</span>
</div>
)
}
const SortableTab = (props: {
file: LocalFile
onTabClick: (file: LocalFile) => void
onTabClose: (file: LocalFile) => void
}) => {
const sortable = createSortable(props.file.path)
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-0": sortable.isActiveDraggable }}>
<Tooltip value={props.file.path} placement="bottom">
<div class="relative">
<Tabs.Trigger value={props.file.path} class="peer/tab pr-7" onClick={() => props.onTabClick(props.file)}>
<TabVisual file={props.file} />
</Tabs.Trigger>
<IconButton
class="absolute right-1 top-1.5 opacity-0 text-text-muted/60
peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text
peer-data-[selected]/tab:hover:bg-border-subtle
hover:opacity-100 peer-hover/tab:opacity-100"
size="xs"
variant="ghost"
onClick={() => props.onTabClose(props.file)}
>
<Icon name="close" size={16} />
</IconButton>
</div>
</Tooltip>
</div>
)
}
const FileTag = (props: { file: LocalFile; default?: boolean; onClose: () => void }) => (
<div
class="flex items-center bg-background group/tag
border border-border-subtle/60 border-dashed
rounded-md text-xs text-text-muted"
>
<IconButton class="text-text-muted" size="xs" variant="ghost" onClick={props.onClose}>
<Switch fallback={<FileIcon node={props.file} class="group-hover/tag:hidden size-3!" />}>
<Match when={props.default}>
<Icon name="file" class="group-hover/tag:hidden" size={12} />
</Match>
</Switch>
<Icon name="close" class="hidden group-hover/tag:block" size={12} />
</IconButton>
<div class="pr-1 flex gap-1 items-center">
<span>{props.file.name}</span>
<Show when={!props.default && props.file.selection}>
<span class="">
({props.file.selection!.startLine}-{props.file.selection!.endLine})
</span>
</Show>
</div>
</div>
)
const ConstrainDragYAxis = () => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-y-axis",
order: 100,
callback: (transform) => ({ ...transform, y: 0 }),
}
onDragStart((event: any) => {
addTransformer("draggables", event.draggable.id, transformer)
})
onDragEnd((event: any) => {
removeTransformer("draggables", event.draggable.id, transformer.id)
})
return <></>
}

View File

@@ -1,5 +0,0 @@
import { type ParentProps } from "solid-js"
export default function Layout(props: ParentProps) {
return <main class="">{props.children}</main>
}

View File

@@ -1,49 +0,0 @@
import { Button as KobalteButton } from "@kobalte/core/button"
import { splitProps } from "solid-js"
import type { ComponentProps } from "solid-js"
export interface ButtonProps extends ComponentProps<typeof KobalteButton> {
variant?: "primary" | "secondary" | "outline" | "ghost"
size?: "sm" | "md" | "lg"
}
export const buttonStyles = {
base: "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
variants: {
primary: "bg-primary text-background hover:bg-secondary focus-visible:ring-primary data-[disabled]:opacity-50",
secondary:
"bg-background-panel text-text hover:bg-background-element focus-visible:ring-secondary data-[disabled]:opacity-50",
outline:
"border border-border bg-transparent text-text hover:bg-background-panel focus-visible:ring-border-active data-[disabled]:border-border-subtle data-[disabled]:text-text-muted",
ghost: "text-text hover:bg-background-panel focus-visible:ring-border-active data-[disabled]:text-text-muted",
},
sizes: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
},
}
export function getButtonClasses(
variant: keyof typeof buttonStyles.variants = "primary",
size: keyof typeof buttonStyles.sizes = "md",
className?: string,
) {
return `${buttonStyles.base} ${buttonStyles.variants[variant]} ${buttonStyles.sizes[size]}${className ? ` ${className}` : ""}`
}
export function Button(props: ButtonProps) {
const [local, others] = splitProps(props, ["variant", "size", "class", "classList"])
return (
<KobalteButton
classList={{
...(local.classList ?? {}),
[buttonStyles.base]: true,
[buttonStyles.variants[local.variant || "primary"]]: true,
[buttonStyles.sizes[local.size || "md"]]: true,
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}

View File

@@ -1,15 +0,0 @@
import { A } from "@solidjs/router"
import { splitProps } from "solid-js"
import type { ComponentProps } from "solid-js"
import { getButtonClasses } from "./button"
export interface LinkProps extends ComponentProps<typeof A> {
variant?: "primary" | "secondary" | "outline" | "ghost"
size?: "sm" | "md" | "lg"
}
export function Link(props: LinkProps) {
const [local, others] = splitProps(props, ["variant", "size", "class"])
const classes = local.variant ? getButtonClasses(local.variant, local.size, local.class) : local.class
return <A class={classes} {...others} />
}

View File

@@ -1,125 +0,0 @@
import type { ComponentProps } from "solid-js"
export interface LogoProps extends ComponentProps<"svg"> {
variant?: "mark" | "full" | "ornate"
size?: number
}
export function Logo(props: LogoProps) {
const { variant = "mark", size = 64, ...others } = props
if (variant === "mark") {
return (
<svg
width={size}
height={size * (42 / 64)}
viewBox="0 0 64 42"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={`text-text ${props.class ?? ""}`}
{...others}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z"
fill="currentColor"
/>
<path d="M40 0H64V8.5H48V33H64V41.5H40V0Z" fill="currentColor" />
</svg>
)
}
if (variant === "full") {
return (
<svg
width={size * (289 / 42)}
height={size}
viewBox="0 0 289 42"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...others}
>
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="currentColor" />
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" />
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
fill="currentColor"
/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" />
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" />
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
fill="currentColor"
/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" />
</svg>
)
}
return (
<svg
width={size * (289 / 42)}
height={size}
viewBox="0 0 289 50"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...others}
>
<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="currentColor" fill-opacity="0.2" />
<path
d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z"
fill="currentColor"
fill-opacity="0.95"
/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" fill-opacity="0.95" />
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" fill-opacity="0.95" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
fill="currentColor"
fill-opacity="0.95"
/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" fill-opacity="0.5" />
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" fill-opacity="0.5" />
<path
d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z"
fill="currentColor"
fill-opacity="0.5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
fill="currentColor"
fill-opacity="0.5"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
fill="currentColor"
fill-opacity="0.5"
/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" fill-opacity="0.95" />
</svg>
)
}

View File

@@ -1,71 +0,0 @@
import { Tabs as KobalteTabs } from "@kobalte/core/tabs"
import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface TabsProps extends ComponentProps<typeof KobalteTabs> {}
export interface TabsListProps extends ComponentProps<typeof KobalteTabs.List> {}
export interface TabsTriggerProps extends ComponentProps<typeof KobalteTabs.Trigger> {}
export interface TabsContentProps extends ComponentProps<typeof KobalteTabs.Content> {}
function TabsRoot(props: TabsProps) {
return <KobalteTabs {...props} />
}
function TabsList(props: TabsListProps) {
const [local, others] = splitProps(props, ["class"])
return (
<KobalteTabs.List
classList={{
"relative flex items-center bg-background overflow-x-auto no-scrollbar": true,
"divide-x divide-border-subtle/40": true,
"after:content-[''] after:block after:grow after:h-8": true,
"after:border-l empty:after:border-l-0! after:border-b after:border-border-subtle/40": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}
function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
const [local, others] = splitProps(props, ["class", "children"])
return (
<KobalteTabs.Trigger
classList={{
"relative px-3 h-8 flex items-center": true,
"text-sm font-medium text-text-muted/60 cursor-pointer": true,
"whitespace-nowrap shrink-0 border-b border-border-subtle/40": true,
"disabled:pointer-events-none disabled:opacity-50": true,
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring": true,
"data-[selected]:text-text data-[selected]:bg-background-panel": true,
"data-[selected]:!border-b-transparent": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
>
{local.children}
</KobalteTabs.Trigger>
)
}
function TabsContent(props: ParentProps<TabsContentProps>) {
const [local, others] = splitProps(props, ["class", "children"])
return (
<KobalteTabs.Content
classList={{
"bg-background-panel overflow-y-auto h-full no-scrollbar": true,
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
>
{local.children}
</KobalteTabs.Content>
)
}
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
})

View File

@@ -1,9 +0,0 @@
export function getFilename(path: string) {
const parts = path.split("/")
return parts[parts.length - 1]
}
export function getFileExtension(path: string) {
const parts = path.split(".")
return parts[parts.length - 1]
}

View File

@@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"lib": ["DOM", "DOM.Iterable"],
"customConditions": ["development"],
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,18 +1,20 @@
{
"name": "@opencode/console-app",
"name": "@opencode-ai/console-app",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.10.3"
"version": "0.15.11"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
"@opencode-ai/console-core": "workspace:*",
"@opencode-ai/console-resource": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
@@ -20,6 +22,10 @@
"vinxi": "^0.5.7",
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
},
"engines": {
"node": ">=22"
}

View File

@@ -0,0 +1 @@
../../mail/emails/templates/static

View File

@@ -0,0 +1,23 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="400" fill="#FDFCFC"/>
<path d="M96 122.001V70.001H148V122.001H96Z" fill="#17181C"/>
<path d="M148.004 122.001V70.001H200.004V122.001H148.004Z" fill="#17181C"/>
<path d="M200.008 122.001V70.001H252.008V122.001H200.008Z" fill="#17181C"/>
<path d="M251.996 122.001V70.001H303.996V122.001H251.996Z" fill="#17181C"/>
<path d="M251.996 173.988V121.988H303.996V173.988H251.996Z" fill="#17181C"/>
<path d="M96 225.998V173.998H148V225.998H96Z" fill="#CFCECD"/>
<rect width="52" height="52" transform="translate(148.004 173.998)" fill="#17181C"/>
<path d="M148.004 225.998V173.998H200.004V225.998H148.004Z" fill="#17181C" fill-opacity="0.1"/>
<path d="M200.008 225.998V173.998H252.008V225.998H200.008Z" fill="#17181C"/>
<path d="M252.016 225.998V173.998H304.016V225.998H252.016Z" fill="#CFCECD"/>
<rect width="52" height="52" transform="translate(96 226.002)" fill="#17181C"/>
<path d="M96 278.002V226.002H148V278.002H96Z" fill="#17181C" fill-opacity="0.1"/>
<rect width="52" height="52" transform="translate(148.004 226.002)" fill="white"/>
<path d="M148.004 278.002V226.002H200.004V278.002H148.004Z" fill="#CFCECD"/>
<path d="M200.008 278.002V226.002H252.008V278.002H200.008Z" fill="#CFCECD"/>
<path d="M252.016 278.002V226.002H304.016V278.002H252.016Z" fill="#CFCECD"/>
<path d="M96 330.012V278.012H148V330.012H96Z" fill="#17181C"/>
<path d="M148.004 330.012V278.012H200.004V330.012H148.004Z" fill="#17181C"/>
<path d="M200.008 329.99V277.99H252.008V329.99H200.008Z" fill="#17181C"/>
<path d="M251.996 330.012V278.012H303.996V330.012H251.996Z" fill="#17181C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,5 +1,4 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M115 180H300V420H115V180ZM253.75 229.044H161.25V370.405H253.75V229.044Z" fill="white"/>
<path d="M346.25 180H485V229.044H392.5V370.405H485V419.449H346.25V180Z" fill="white"/>
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="400" fill="#0E0E0E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M312 340H88V60H312V340ZM256 116H144V284H256V116Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 377 B

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,2 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"/></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 14.3581L10.0541 17.7027L18 7" stroke="#8E8B8B" stroke-width="1.5" stroke-linecap="square"/>
</svg>

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -1,2 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><rect width="336" height="336" x="128" y="128" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="57" ry="57"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"/></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z" stroke="#8E8B8B" stroke-width="1.5" stroke-linecap="square"/>
</svg>

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 KiB

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -1,19 +1,18 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 16.5H24.5V33H8.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M48.5 16.5H64.5V33H48.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M120.5 16.5H136.5V33H120.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M160.5 16.5H176.5V33H160.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M192.5 16.5H208.5V33H192.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M232.5 16.5H248.5V33H232.5V16.5Z" fill="white" fill-opacity="0.2"/>
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white" fill-opacity="0.95"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white" fill-opacity="0.95"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white" fill-opacity="0.5"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white" fill-opacity="0.5"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white" fill-opacity="0.95"/>
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#4B4646"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#B7B1B1"/>
<path d="M48 30H36V18H48V30Z" fill="#4B4646"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#B7B1B1"/>
<path d="M84 24V30H66V24H84Z" fill="#4B4646"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#B7B1B1"/>
<path d="M108 36H96V18H108V36Z" fill="#4B4646"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#B7B1B1"/>
<path d="M144 30H126V18H144V30Z" fill="#4B4646"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#F1ECEC"/>
<path d="M168 30H156V18H168V30Z" fill="#4B4646"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#F1ECEC"/>
<path d="M198 30H186V18H198V30Z" fill="#4B4646"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#F1ECEC"/>
<path d="M234 24V30H216V24H234Z" fill="#4B4646"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#F1ECEC"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,18 +1,18 @@
<svg width="288" height="50" viewBox="0 0 288 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16.5H24V33H8V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M48 16.5H64V33H48V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M120 16.5H136V33H120V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M160 16.5H176V33H160V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M192 16.5H208V33H192V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M232 16.5H248V33H232V16.5Z" fill="black" fill-opacity="0.15"/>
<path d="M264 0H288V8.5H272V16.5H288V25H272V33H288V41.5H264V0Z" fill="black" fill-opacity="0.95"/>
<path d="M248 0H224V41.5H248V33H232V8.5H248V0Z" fill="black" fill-opacity="0.95"/>
<path d="M256 8.5H248V33H256V8.5Z" fill="black" fill-opacity="0.95"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184 0H216V41.5H184V0ZM208 8.5H192V33H208V8.5Z" fill="black" fill-opacity="0.95"/>
<path d="M144 8.5H136V41.5H144V8.5Z" fill="black" fill-opacity="0.55"/>
<path d="M136 0H112V41.5H120V8.5H136V0Z" fill="black" fill-opacity="0.55"/>
<path d="M80 0H104V8.5H88V16.5H104V25H88V33H104V41.5H80V0Z" fill="black" fill-opacity="0.55"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40 0H72V41.5H48V49.5H40V0ZM64 8.5H48V33H64V8.5Z" fill="black" fill-opacity="0.55"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H32V41.5955H0V0ZM24 8.5H8V33H24V8.5Z" fill="black" fill-opacity="0.55"/>
<path d="M152 0H176V8.5H160V33H176V41.5H152V0Z" fill="black" fill-opacity="0.95"/>
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#CFCECD"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#656363"/>
<path d="M48 30H36V18H48V30Z" fill="#CFCECD"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#656363"/>
<path d="M84 24V30H66V24H84Z" fill="#CFCECD"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#656363"/>
<path d="M108 36H96V18H108V36Z" fill="#CFCECD"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#656363"/>
<path d="M144 30H126V18H144V30Z" fill="#CFCECD"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#211E1E"/>
<path d="M168 30H156V18H168V30Z" fill="#CFCECD"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#211E1E"/>
<path d="M198 30H186V18H198V30Z" fill="#CFCECD"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#211E1E"/>
<path d="M234 24V30H216V24H234Z" fill="#CFCECD"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#211E1E"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +1,18 @@
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="black"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="black"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="black"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="black"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="black"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="black"/>
<svg width="234" height="42" viewBox="0 0 234 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 30H6V18H18V30Z" fill="#CFCECD"/>
<path d="M18 12H6V30H18V12ZM24 36H0V6H24V36Z" fill="#656363"/>
<path d="M48 30H36V18H48V30Z" fill="#CFCECD"/>
<path d="M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z" fill="#656363"/>
<path d="M84 24V30H66V24H84Z" fill="#CFCECD"/>
<path d="M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z" fill="#656363"/>
<path d="M108 36H96V18H108V36Z" fill="#CFCECD"/>
<path d="M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z" fill="#656363"/>
<path d="M144 30H126V18H144V30Z" fill="#CFCECD"/>
<path d="M144 12H126V30H144V36H120V6H144V12Z" fill="#211E1E"/>
<path d="M168 30H156V18H168V30Z" fill="#CFCECD"/>
<path d="M168 12H156V30H168V12ZM174 36H150V6H174V36Z" fill="#211E1E"/>
<path d="M198 30H186V18H198V30Z" fill="#CFCECD"/>
<path d="M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z" fill="#211E1E"/>
<path d="M234 24V30H216V24H234Z" fill="#CFCECD"/>
<path d="M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z" fill="#211E1E"/>
</svg>

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,8 @@
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H6V18H18V12H24V24ZM6 18H0V12H6V18Z" fill="#4B4646"/>
<path d="M6 24H24V30H0V18H6V24ZM18 18H6V12H18V18ZM24 12H18V6H0V0H24V12Z" fill="#F1ECEC"/>
<path d="M54 18V24H36V18H54Z" fill="#4B4646"/>
<path d="M54 18H36V24H54V30H30V0H54V18ZM36 12H48V6H36V12Z" fill="#F1ECEC"/>
<path d="M78 30H66V12H78V30Z" fill="#4B4646"/>
<path d="M78 6H66V30H60V0H78V6ZM84 30H78V6H84V30Z" fill="#F1ECEC"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -0,0 +1,8 @@
<svg width="84" height="30" viewBox="0 0 84 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H6V18H18V12H24V24ZM6 18H0V12H6V18Z" fill="#CFCECD"/>
<path d="M6 24H24V30H0V18H6V24ZM18 18H6V12H18V18ZM24 12H18V6H0V0H24V12Z" fill="#211E1E"/>
<path d="M54 18V24H36V18H54Z" fill="#CFCECD"/>
<path d="M54 18H36V24H54V30H30V0H54V18ZM36 12H48V6H36V12Z" fill="#211E1E"/>
<path d="M78 30H66V12H78V30Z" fill="#CFCECD"/>
<path d="M78 6H66V30H60V0H78V6ZM84 30H78V6H84V30Z" fill="#211E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -0,0 +1,80 @@
[data-component="dropdown"] {
position: relative;
[data-slot="trigger"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: none;
border-radius: var(--border-radius-sm);
background-color: transparent;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
}
span {
flex: 1;
text-align: left;
font-weight: 500;
}
}
[data-slot="chevron"] {
flex-shrink: 0;
color: var(--color-text-secondary);
}
[data-slot="dropdown"] {
position: absolute;
top: 100%;
z-index: 1000;
margin-top: var(--space-1);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 160px;
&[data-align="left"] {
left: 0;
}
&[data-align="right"] {
right: 0;
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
[data-slot="item"] {
display: block;
width: 100%;
padding: var(--space-2-5) var(--space-3);
border: none;
background: none;
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
text-align: left;
cursor: pointer;
transition: background-color 0.15s ease;
&:hover {
background-color: var(--color-bg-surface);
}
&[data-selected="true"] {
background-color: var(--color-accent-alpha);
}
}
}

View File

@@ -0,0 +1,79 @@
import { JSX, Show, createEffect, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { IconChevron } from "./icon"
import "./dropdown.css"
interface DropdownProps {
trigger: JSX.Element | string
children: JSX.Element
open?: boolean
onOpenChange?: (open: boolean) => void
align?: "left" | "right"
class?: string
}
export function Dropdown(props: DropdownProps) {
const [store, setStore] = createStore({
isOpen: props.open ?? false,
})
let dropdownRef: HTMLDivElement | undefined
createEffect(() => {
if (props.open !== undefined) {
setStore("isOpen", props.open)
}
})
createEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
setStore("isOpen", false)
props.onOpenChange?.(false)
}
}
document.addEventListener("click", handleClickOutside)
onCleanup(() => document.removeEventListener("click", handleClickOutside))
})
const toggle = () => {
const newValue = !store.isOpen
setStore("isOpen", newValue)
props.onOpenChange?.(newValue)
}
return (
<div data-component="dropdown" class={props.class} ref={dropdownRef}>
<button data-slot="trigger" type="button" onClick={toggle}>
{typeof props.trigger === "string" ? <span>{props.trigger}</span> : props.trigger}
<IconChevron data-slot="chevron" />
</button>
<Show when={store.isOpen}>
<div data-slot="dropdown" data-align={props.align ?? "left"}>
{props.children}
</div>
</Show>
</div>
)
}
interface DropdownItemProps {
children: JSX.Element
selected?: boolean
onClick?: () => void
type?: "button" | "submit" | "reset"
}
export function DropdownItem(props: DropdownItemProps) {
return (
<button
data-slot="item"
data-selected={props.selected ?? false}
type={props.type ?? "button"}
onClick={props.onClick}
>
{props.children}
</button>
)
}

View File

@@ -0,0 +1,51 @@
import { action, useSubmission } from "@solidjs/router"
import dock from "../asset/lander/dock.png"
import { Resource } from "@opencode-ai/console-resource"
import { Show } from "solid-js"
const emailSignup = action(async (formData: FormData) => {
"use server"
const emailAddress = formData.get("email")!
const listId = "8b9bb82c-9d5f-11f0-975f-0df6fd1e4945"
const response = await fetch(`https://api.emailoctopus.com/lists/${listId}/contacts`, {
method: "PUT",
headers: {
Authorization: `Bearer ${Resource.EMAILOCTOPUS_API_KEY.value}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email_address: emailAddress,
}),
})
console.log(response)
return true
})
export function EmailSignup() {
const submission = useSubmission(emailSignup)
return (
<section data-component="email">
<div data-slot="dock">
<img src={dock} alt="" />
</div>
<div data-slot="section-title">
<h3>OpenCode will be available on desktop soon</h3>
<p>Join the waitlist for early access.</p>
</div>
<form data-slot="form" action={emailSignup} method="post">
<input type="email" name="email" placeholder="Email address" required />
<button type="submit" disabled={submission.pending}>
Subscribe
</button>
</form>
<Show when={submission.result}>
<div style="color: #03B000; margin-top: 24px;">
Almost done, check your inbox and confirm your email address
</div>
</Show>
<Show when={submission.error}>
<div style="color: #FF408F; margin-top: 24px;">{submission.error}</div>
</Show>
</section>
)
}

View File

@@ -0,0 +1,33 @@
import { Collapsible } from "@kobalte/core/collapsible"
import { ParentProps } from "solid-js"
export function Faq(props: ParentProps & { question: string }) {
return (
<Collapsible data-slot="faq-item">
<Collapsible.Trigger data-slot="faq-question">
<svg
data-slot="faq-icon-plus"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z" fill="currentColor" />
</svg>
<svg
data-slot="faq-icon-minus"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 11.5H19V12.5H5Z" fill="currentColor" />
</svg>
<div data-slot="faq-question-text">{props.question}</div>
</Collapsible.Trigger>
<Collapsible.Content data-slot="faq-answer">{props.children}</Collapsible.Content>
</Collapsible>
)
}

View File

@@ -0,0 +1,34 @@
import { createAsync } from "@solidjs/router"
import { createMemo } from "solid-js"
import { github } from "~/lib/github"
export function Footer() {
const githubData = createAsync(() => github())
const starCount = createMemo(() =>
githubData()?.stars
? new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(githubData()!.stars!)
: "25K",
)
return (
<footer data-component="footer">
<div data-slot="cell">
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</div>
<div data-slot="cell">
<a href="/docs">Docs</a>
</div>
<div data-slot="cell">
<a href="/discord">Discord</a>
</div>
<div data-slot="cell">
<a href="https://x.com/opencode">X</a>
</div>
</footer>
)
}

View File

@@ -0,0 +1,127 @@
import logoLight from "../asset/logo-ornate-light.svg"
import logoDark from "../asset/logo-ornate-dark.svg"
import { A, createAsync } from "@solidjs/router"
import { createMemo, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { github } from "~/lib/github"
export function Header(props: { zen?: boolean }) {
const githubData = createAsync(() => github())
const starCount = createMemo(() =>
githubData()?.stars
? new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(githubData()?.stars!)
: "25K",
)
const [store, setStore] = createStore({
mobileMenuOpen: false,
})
return (
<section data-component="top">
<A href="/">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
</A>
<nav data-component="nav-desktop">
<ul>
<li>
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
</Match>
</Switch>
</li>
</ul>
</nav>
<nav data-component="nav-mobile">
<button
type="button"
data-component="nav-mobile-toggle"
aria-expanded="false"
aria-controls="nav-mobile-menu"
class="nav-toggle"
onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)}
>
<span class="sr-only">Open menu</span>
<Switch>
<Match when={store.mobileMenuOpen}>
<svg
class="icon icon-close"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.7071 11.9993L18.0104 17.3026L17.3033 18.0097L12 12.7064L6.6967 18.0097L5.98959 17.3026L11.2929 11.9993L5.98959 6.69595L6.6967 5.98885L12 11.2921L17.3033 5.98885L18.0104 6.69595L12.7071 11.9993Z"
fill="currentColor"
/>
</svg>
</Match>
<Match when={!store.mobileMenuOpen}>
<svg
class="icon icon-hamburger"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M19 17H5V16H19V17Z" fill="currentColor" />
<path d="M19 8H5V7H19V8Z" fill="currentColor" />
</svg>
</Match>
</Switch>
</button>
<Show when={store.mobileMenuOpen}>
<div id="nav-mobile-menu" data-component="nav-mobile">
<nav data-component="nav-mobile-menu-list">
<ul>
<li>
<A href="/">Home</A>
</li>
<li>
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
</Match>
</Switch>
</li>
</ul>
</nav>
</div>
</Show>
</nav>
</section>
)
}

View File

@@ -2,70 +2,89 @@ import { JSX } from "solid-js"
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="currentColor" />
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="currentColor" />
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z"
<svg width="64" height="32" viewBox="0 0 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 9.14333V4.5719H4.57143V9.14333H0Z" fill="currentColor" />
<path d="M4.57178 9.14333V4.5719H9.14321V9.14333H4.57178Z" fill="currentColor" />
<path d="M9.1438 9.14333V4.5719H13.7152V9.14333H9.1438Z" fill="currentColor" />
<path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
<path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
<path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
<rect width="4.57143" height="4.57143" transform="translate(4.57178 13.7141)" fill="currentColor" />
<path d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z" fill="currentColor" fill-opacity="0.2" />
<path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
<path d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z" fill="currentColor" fill-opacity="0.2" />
<rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
<path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
<rect
width="4.57143"
height="4.57143"
transform="translate(4.57178 18.2859)"
fill="currentColor"
fill-opacity="0.2"
/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="currentColor" />
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="currentColor" />
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z"
fill="currentColor"
/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="currentColor" />
<path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
<path d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" fill="currentColor" fill-opacity="0.2" />
<path d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z" fill="currentColor" fill-opacity="0.2" />
<path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
<path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
<path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
<path d="M13.7124 27.4292V22.8578H18.2838V27.4292H13.7124Z" fill="currentColor" />
<path d="M22.8572 9.14333V4.5719H27.4286V9.14333H22.8572Z" fill="currentColor" />
<path d="M27.426 9.14333V4.5719H31.9975V9.14333H27.426Z" fill="currentColor" />
<path d="M32.001 9.14333V4.5719H36.5724V9.14333H32.001Z" fill="currentColor" />
<path d="M36.5698 9.14333V4.5719H41.1413V9.14333H36.5698Z" fill="currentColor" />
<path d="M22.8572 13.7152V9.1438H27.4286V13.7152H22.8572Z" fill="currentColor" />
<path d="M36.5698 13.7152V9.1438H41.1413V13.7152H36.5698Z" fill="currentColor" />
<path d="M22.8572 18.2855V13.7141H27.4286V18.2855H22.8572Z" fill="currentColor" />
<path d="M27.4292 18.2855V13.7141H32.0006V18.2855H27.4292Z" fill="currentColor" />
<path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
<path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
<path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
<path d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" fill="currentColor" fill-opacity="0.2" />
<path d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z" fill="currentColor" fill-opacity="0.2" />
<path d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z" fill="currentColor" fill-opacity="0.2" />
<path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
<path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
<path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
<path d="M36.5698 27.4292V22.8578H41.1413V27.4292H36.5698Z" fill="currentColor" />
<path d="M45.7144 9.14333V4.5719H50.2858V9.14333H45.7144Z" fill="currentColor" />
<path d="M50.2861 9.14333V4.5719H54.8576V9.14333H50.2861Z" fill="currentColor" />
<path d="M54.855 9.14333V4.5719H59.4264V9.14333H54.855Z" fill="currentColor" />
<path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
<path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
<path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
<path d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
<path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
<path d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
<path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
<path d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" fill="currentColor" fill-opacity="0.2" />
<path d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z" fill="currentColor" fill-opacity="0.2" />
<path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
</svg>
)
}
export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 512 512">
<rect
width="336"
height="336"
x="128"
y="128"
fill="none"
stroke="currentColor"
stroke-linejoin="round"
stroke-width="32"
rx="57"
ry="57"
></rect>
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="32"
d="m383.5 128l.5-24a56.16 56.16 0 0 0-56-56H112a64.19 64.19 0 0 0-64 64v216a56.16 56.16 0 0 0 56 56h24"
></path>
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
)
}
export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z"
></path>
<svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.75 15.0938L9 20.25L21.25 3.75" stroke="#03B000" stroke-width="2" stroke-linecap="square" />
</svg>
)
}
@@ -80,3 +99,116 @@ export function IconCreditCard(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg>
)
}
export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24">
<path
fill="currentColor"
d="M15.827 12.506c0 .672-.31 1.175-.771 1.175-.293 0-.468-.106-.589-.237l-.007-1.855c.13-.143.31-.247.596-.247.456-.001.771.51.771 1.164zm3.36-1.253c-.312 0-.659.236-.659.798h1.291c0-.562-.325-.798-.632-.798zm4.813-5.253v12c0 1.104-.896 2-2 2h-20c-1.104 0-2-.896-2-2v-12c0-1.104.896-2 2-2h20c1.104 0 2 .896 2 2zm-17.829 7.372c0-1.489-1.909-1.222-1.909-1.784 0-.195.162-.271.424-.271.38 0 .862.116 1.242.321v-1.176c-.414-.165-.827-.228-1.241-.228-1.012.001-1.687.53-1.687 1.414 0 1.382 1.898 1.158 1.898 1.754 0 .231-.201.305-.479.305-.414 0-.947-.171-1.366-.399v1.192c.464.2.935.283 1.365.283 1.038.001 1.753-.512 1.753-1.411zm2.422-3.054h-.949l-.001-1.084-1.219.259-.005 4.006c0 .739.556 1.285 1.297 1.285.408 0 .71-.074.876-.165v-1.016c-.16.064-.948.293-.948-.443v-1.776h.948v-1.066zm2.596 0c-.166-.06-.75-.169-1.042.369l-.078-.369h-1.079v4.377h1.248v-2.967c.295-.388.793-.313.952-.262v-1.148zm1.554 0h-1.253v4.377h1.253v-4.377zm0-1.664l-1.253.266v1.017l1.253-.266v-1.017zm4.314 3.824c0-1.454-.826-2.244-1.703-2.243-.489 0-.805.23-.978.392l-.065-.309h-1.099v5.828l1.249-.265.003-1.413c.179.131.446.316.883.316.893 0 1.71-.719 1.71-2.306zm3.943.045c0-1.279-.619-2.288-1.805-2.288-1.188 0-1.911 1.01-1.911 2.281 0 1.506.852 2.267 2.068 2.267.597 0 1.045-.136 1.384-.324v-1.006c-.34.172-.731.276-1.227.276-.487 0-.915-.172-.971-.758h2.444l.018-.448z"
/>
</svg>
)
}
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
/>
</svg>
)
}
export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
</svg>
)
}
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
<path
d="M8.43799 8.06943V6.09387C8.43799 5.92749 8.50347 5.80267 8.65601 5.71959L12.8206 3.43211C13.3875 3.1202 14.0635 2.9747 14.7611 2.9747C17.3775 2.9747 19.0347 4.9087 19.0347 6.96734C19.0347 7.11288 19.0347 7.27926 19.0128 7.44564L14.6956 5.03335C14.434 4.88785 14.1723 4.88785 13.9107 5.03335L8.43799 8.06943ZM18.1624 15.7637V11.0431C18.1624 10.7519 18.0315 10.544 17.7699 10.3984L12.2972 7.36234L14.0851 6.3849C14.2377 6.30182 14.3686 6.30182 14.5212 6.3849L18.6858 8.67238C19.8851 9.3379 20.6917 10.7519 20.6917 12.1243C20.6917 13.7047 19.7106 15.1604 18.1624 15.7636V15.7637ZM7.15158 11.6047L5.36369 10.6066C5.21114 10.5235 5.14566 10.3986 5.14566 10.2323V5.65735C5.14566 3.43233 6.93355 1.7478 9.35381 1.7478C10.2697 1.7478 11.1199 2.039 11.8396 2.55886L7.54424 4.92959C7.28268 5.07508 7.15181 5.28303 7.15181 5.57427V11.6049L7.15158 11.6047ZM11 13.7258L8.43799 12.3533V9.44209L11 8.06965L13.5618 9.44209V12.3533L11 13.7258ZM12.6461 20.0476C11.7303 20.0476 10.8801 19.7564 10.1604 19.2366L14.4557 16.8658C14.7173 16.7203 14.8482 16.5124 14.8482 16.2211V10.1905L16.658 11.1886C16.8105 11.2717 16.876 11.3965 16.876 11.563V16.1379C16.876 18.3629 15.0662 20.0474 12.6461 20.0474V20.0476ZM7.47863 15.4103L3.314 13.1229C2.11471 12.4573 1.30808 11.0433 1.30808 9.67088C1.30808 8.06965 2.31106 6.6348 3.85903 6.03168V10.773C3.85903 11.0642 3.98995 11.2721 4.25151 11.4177L9.70253 14.4328L7.91464 15.4103C7.76209 15.4934 7.63117 15.4934 7.47863 15.4103ZM7.23892 18.8207C4.77508 18.8207 2.96533 17.0531 2.96533 14.8696C2.96533 14.7032 2.98719 14.5368 3.00886 14.3704L7.30418 16.7412C7.56574 16.8867 7.82752 16.8867 8.08909 16.7412L13.5618 13.726V15.7015C13.5618 15.8679 13.4964 15.9927 13.3438 16.0758L9.17918 18.3633C8.61225 18.6752 7.93631 18.8207 7.23869 18.8207H7.23892ZM12.6461 21.2952C15.2844 21.2952 17.4865 19.5069 17.9882 17.1362C20.4301 16.5331 22 14.3495 22 12.1245C22 10.6688 21.346 9.25482 20.1685 8.23581C20.2775 7.79908 20.343 7.36234 20.343 6.92582C20.343 3.95215 17.8137 1.72691 14.892 1.72691C14.3034 1.72691 13.7365 1.80999 13.1695 1.99726C12.1882 1.08223 10.8364 0.5 9.35381 0.5C6.71557 0.5 4.51352 2.28829 4.01185 4.65902C1.56987 5.26214 0 7.44564 0 9.67067C0 11.1264 0.654039 12.5404 1.83147 13.5594C1.72246 13.9961 1.65702 14.4328 1.65702 14.8694C1.65702 17.8431 4.1863 20.0683 7.108 20.0683C7.69661 20.0683 8.26354 19.9852 8.83046 19.7979C9.81155 20.713 11.1634 21.2952 12.6461 21.2952Z"
fill="currentColor"
/>
</svg>
)
}
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
<path
fill="currentColor"
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
/>
</svg>
)
}
export function IconXai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
fill="currentColor"
/>
<path
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
fill="currentColor"
/>
</svg>
)
}
export function IconAlibaba(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="currentColor"
d="M11.6043 0.340162C11.9973 1.03016 12.3883 1.72215 12.7783 2.41514C12.7941 2.44286 12.8169 2.46589 12.8445 2.48187C12.8721 2.49786 12.9034 2.50624 12.9353 2.50614H18.4873C18.6612 2.50614 18.8092 2.61614 18.9332 2.83314L20.3872 5.40311C20.5772 5.74011 20.6272 5.88111 20.4112 6.24011C20.1512 6.6701 19.8982 7.1041 19.6512 7.54009L19.2842 8.19809C19.1782 8.39409 19.0612 8.47809 19.2442 8.71008L21.8962 13.347C22.0682 13.648 22.0072 13.841 21.8532 14.117C21.4162 14.902 20.9712 15.681 20.5182 16.457C20.3592 16.729 20.1662 16.832 19.8382 16.827C19.0612 16.811 18.2863 16.817 17.5113 16.843C17.4946 16.8439 17.4785 16.8489 17.4644 16.8576C17.4502 16.8664 17.4385 16.8785 17.4303 16.893C16.5361 18.4773 15.6344 20.0573 14.7253 21.633C14.5563 21.926 14.3453 21.996 14.0003 21.997C13.0033 22 11.9983 22.001 10.9833 21.999C10.8889 21.9987 10.7961 21.9735 10.7145 21.9259C10.6328 21.8783 10.5652 21.8101 10.5184 21.728L9.18337 19.405C9.1756 19.3898 9.16368 19.3771 9.14898 19.3684C9.13429 19.3598 9.11743 19.3554 9.10037 19.356H3.98244C3.69744 19.386 3.42944 19.355 3.17745 19.264L1.57447 16.494C1.52706 16.412 1.50193 16.319 1.50158 16.2243C1.50123 16.1296 1.52567 16.0364 1.57247 15.954L2.77945 13.834C2.79665 13.8041 2.80569 13.7701 2.80569 13.7355C2.80569 13.701 2.79665 13.667 2.77945 13.637C2.15073 12.5485 1.52573 11.4579 0.904476 10.3651L0.114486 8.97008C-0.0455115 8.66008 -0.0585113 8.47409 0.209485 8.00509C0.674479 7.1921 1.13647 6.38011 1.59647 5.56911C1.72847 5.33512 1.90046 5.23512 2.18046 5.23412C3.04344 5.23048 3.90644 5.23015 4.76943 5.23312C4.79123 5.23295 4.81259 5.22704 4.83138 5.21597C4.85016 5.20491 4.8657 5.1891 4.87643 5.17012L7.68239 0.275163C7.72491 0.200697 7.78631 0.138751 7.86039 0.0955646C7.93448 0.0523783 8.01863 0.0294762 8.10439 0.0291651C8.62838 0.0281651 9.15737 0.029165 9.68736 0.0231651L10.7044 0.000165317C11.0453 -0.00283466 11.4283 0.032165 11.6043 0.340162ZM8.17238 0.743158C8.16185 0.743152 8.15149 0.745921 8.14236 0.751187C8.13323 0.756454 8.12565 0.764031 8.12038 0.773158L5.25442 5.78811C5.24066 5.81174 5.22097 5.83137 5.19729 5.84505C5.17361 5.85873 5.14677 5.86599 5.11942 5.86611H2.25346C2.19746 5.86611 2.18346 5.89111 2.21246 5.94011L8.02239 16.096C8.04739 16.138 8.03539 16.158 7.98839 16.159L5.19342 16.174C5.15256 16.1727 5.11214 16.1828 5.07678 16.2033C5.04141 16.2238 5.01253 16.2539 4.99342 16.29L3.67344 18.6C3.62944 18.678 3.65244 18.718 3.74144 18.718L9.45737 18.726C9.50337 18.726 9.53737 18.746 9.56137 18.787L10.9643 21.241C11.0103 21.322 11.0563 21.323 11.1033 21.241L16.1093 12.481L16.8923 11.0991C16.897 11.0905 16.904 11.0834 16.9125 11.0785C16.9209 11.0735 16.9305 11.0709 16.9403 11.0709C16.9501 11.0709 16.9597 11.0735 16.9681 11.0785C16.9765 11.0834 16.9835 11.0905 16.9883 11.0991L18.4123 13.629C18.4229 13.648 18.4385 13.6637 18.4573 13.6746C18.4761 13.6855 18.4975 13.6912 18.5193 13.691L21.2822 13.671C21.2893 13.6711 21.2963 13.6693 21.3024 13.6658C21.3086 13.6623 21.3137 13.6572 21.3172 13.651C21.3206 13.6449 21.3224 13.638 21.3224 13.631C21.3224 13.624 21.3206 13.6172 21.3172 13.611L18.4173 8.52508C18.4068 8.50809 18.4013 8.48853 18.4013 8.46859C18.4013 8.44864 18.4068 8.42908 18.4173 8.41209L18.7102 7.90509L19.8302 5.92811C19.8542 5.88711 19.8422 5.86611 19.7952 5.86611H8.20038C8.14138 5.86611 8.12738 5.84011 8.15738 5.78911L9.59137 3.28413C9.60211 3.26706 9.60781 3.24731 9.60781 3.22714C9.60781 3.20697 9.60211 3.18721 9.59137 3.17014L8.22538 0.774158C8.22016 0.764697 8.21248 0.756822 8.20315 0.751365C8.19382 0.745909 8.18319 0.743073 8.17238 0.743158ZM14.4623 8.76308C14.5083 8.76308 14.5203 8.78308 14.4963 8.82308L13.6643 10.2881L11.0513 14.873C11.0464 14.8819 11.0392 14.8894 11.0304 14.8945C11.0216 14.8996 11.0115 14.9022 11.0013 14.902C10.9912 14.902 10.9813 14.8993 10.9725 14.8942C10.9637 14.8891 10.9564 14.8818 10.9513 14.873L7.49839 8.84108C7.47839 8.80708 7.48839 8.78908 7.52639 8.78708L7.74239 8.77508L14.4643 8.76308H14.4623Z"
/>
</svg>
)
}
export function IconMoonshotAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.052 6.0364C18.7527 4.28846 16.8758 2.94985 14.6066 2.34296C12.3374 1.73606 10.0411 1.95816 8.04092 2.82331L20.052 6.0364ZM3.75774 6.34071C4.64576 5.04902 5.81872 3.99836 7.16325 3.25459L16.0115 5.62191C15.4308 5.95635 14.8088 6.53268 14.3273 7.16626L21.6025 9.11263C21.809 9.79398 21.9435 10.5003 22 11.2213L3.75774 6.34071ZM21.6866 14.5876C21.584 14.9707 21.4603 15.3425 21.3172 15.7019L2.10543 10.5623C2.16147 10.1792 2.24079 9.7957 2.34339 9.41263C2.58479 8.51262 2.94172 7.67459 3.39435 6.91016L12.7957 9.42554C12.4194 9.96271 12.0766 10.5464 11.7749 11.1709L21.8517 13.8671C21.8056 14.1077 21.7504 14.3478 21.6862 14.5884L21.6866 14.5876ZM2.58134 15.3529C2.11535 14.05 1.91662 12.6408 2.03215 11.2088L10.8985 13.5808C10.8347 13.7823 10.7748 13.9863 10.7192 14.1933C10.6062 14.6147 10.514 15.0352 10.4424 15.4527L20.0166 18.0142C19.5709 18.6051 19.0631 19.1406 18.5057 19.6132L2.58134 15.3529ZM9.42338 21.6568C6.40111 20.8481 4.07415 18.7424 2.88266 16.1001L10.0976 18.0305C10.0859 18.7024 10.1278 19.3571 10.2196 19.9851L15.4619 21.3874C13.5906 22.0743 11.496 22.2112 9.42338 21.6568Z"
fill="currentColor"
/>
</svg>
)
}
export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path>
</svg>
)
}
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
<path
d="M24 15.5L18.1816 14.2871L18.1328 14.3115C16.9036 11.7879 15.6135 9.29301 14.2607 6.82812C14.0172 6.38435 13.771 5.94188 13.5234 5.5C13.6911 5.97998 13.8606 6.45942 14.0322 6.9375C14.9902 9.60529 16.012 12.2429 17.0947 14.8516L12 17.5L6.9043 14.8516C7.98712 12.2428 9.00977 9.6054 9.96777 6.9375C10.1394 6.45942 10.3089 5.97998 10.4766 5.5C10.229 5.94188 9.98281 6.38435 9.73926 6.82812C8.38629 9.29339 7.09557 11.7884 5.86621 14.3125L5.81738 14.2871L0 15.5L12 0.5L24 15.5Z"
fill="currentColor"
/>
</svg>
)
}

View File

@@ -0,0 +1,9 @@
export function Legal() {
return (
<div data-component="legal">
<span>
©{new Date().getFullYear()} <a href="https://anoma.ly">Anomaly</a>
</span>
</div>
)
}

View File

@@ -0,0 +1,66 @@
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
[data-component="modal"][data-slot="overlay"] {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.2s ease;
@media (prefers-color-scheme: dark) {
background-color: rgba(0, 0, 0, 0.7);
}
[data-slot="content"] {
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-md);
padding: var(--space-6);
min-width: 400px;
max-width: 90vw;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.2s ease;
@media (max-width: 30rem) {
min-width: 300px;
padding: var(--space-4);
}
@media (prefers-color-scheme: dark) {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
}
[data-slot="title"] {
margin: 0 0 var(--space-4) 0;
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--color-text);
}
}

View File

@@ -0,0 +1,24 @@
import { JSX, Show } from "solid-js"
import "./modal.css"
interface ModalProps {
open: boolean
onClose: () => void
title?: string
children: JSX.Element
}
export function Modal(props: ModalProps) {
return (
<Show when={props.open}>
<div data-component="modal" data-slot="overlay" onClick={props.onClose}>
<div data-slot="content" onClick={(e) => e.stopPropagation()}>
<Show when={props.title}>
<h2 data-slot="title">{props.title}</h2>
</Show>
{props.children}
</div>
</div>
</Show>
)
}

View File

@@ -1,25 +0,0 @@
export function formatDateForTable(date: Date) {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "short",
hour: "numeric",
minute: "2-digit",
hour12: true,
}
return date.toLocaleDateString("en-GB", options).replace(",", ",")
}
export function formatDateUTC(date: Date) {
const options: Intl.DateTimeFormatOptions = {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
timeZone: "UTC",
}
return date.toLocaleDateString("en-US", options)
}

View File

@@ -1,114 +0,0 @@
.root {
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
[data-slot="reload-error"] {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
p {
color: var(--color-danger);
font-size: var(--font-size-sm);
line-height: 1.4;
margin: 0;
flex: 1;
}
[data-slot="create-form"] {
display: flex;
gap: var(--space-2);
margin: 0;
flex-shrink: 0;
}
}
[data-slot="payment"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
min-width: 14.5rem;
width: fit-content;
@media (max-width: 30rem) {
width: 100%;
}
[data-slot="credit-card"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: space-between;
[data-slot="card-icon"] {
display: flex;
align-items: center;
color: var(--color-text-muted);
}
[data-slot="card-details"] {
display: flex;
align-items: baseline;
gap: var(--space-1);
[data-slot="secret"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
color: var(--color-text-muted);
font-weight: 400;
}
[data-slot="number"] {
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
[data-slot="button-row"] {
display: flex;
gap: var(--space-2);
align-items: center;
@media (max-width: 30rem) {
flex-direction: column;
> button {
width: 100%;
}
}
[data-slot="create-form"] {
margin: 0;
}
/* Make Enable Billing button full width when it's the only button */
> button {
flex: 1;
}
}
}
[data-slot="usage"] {
p {
font-size: var(--font-size-sm);
line-height: 1.5;
color: var(--color-text-secondary);
b {
font-weight: 600;
}
}
}
}

View File

@@ -1,98 +0,0 @@
import { json, query, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { withActor } from "~/context/auth.withActor"
import styles from "./billing-section.module.css"
import { Database, eq } from "@opencode/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js"
import { Show } from "solid-js"
const updateShare = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
const dataShare = form.get("dataShare")?.toString() === "true" ? true : null
return json(
await withActor(() => {
return Database.use((tx) =>
tx
.update(WorkspaceTable)
.set({
dataShare,
})
.where(eq(WorkspaceTable.id, workspaceID)),
)
}, workspaceID),
{ revalidate: getWorkspaceInfo.key },
)
}, "workspace.disableShare")
const getWorkspaceInfo = query(async (workspaceID: string) => {
"use server"
return withActor(() => {
return Database.use((tx) =>
tx
.select({
dataShare: WorkspaceTable.dataShare,
})
.from(WorkspaceTable)
.where(eq(WorkspaceTable.id, workspaceID))
.then((r) => r[0]),
)
}, workspaceID)
}, "workspace.get")
export function PrivacySection() {
const params = useParams()
const workspaceInfo = createAsync(() => getWorkspaceInfo(params.id))
const updateShareSubmission = useSubmission(updateShare)
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Privacy controls</h2>
<p>
Some providers offer data-sharing programs. If you opt in, you voluntarily <b>share your usage data</b> with
them, which they may use to improve their services, including <b>model training</b>.
</p>
<br />
<p>
By opting in, you gain access to <b>discounted pricing</b> from the provider. You can opt in or out at any
time.
</p>
<br />
<p>
<a target="_blank" href="/docs/zen">
Learn more
</a>
</p>
</div>
<Show when={workspaceInfo()?.dataShare}>
<div data-slot="payment">
<div data-slot="credit-card">
<div data-slot="card-details">
<span data-slot="number">You are currently opted in to the data-sharing program.</span>
</div>
</div>
</div>
</Show>
<div data-slot="section-content">
<div data-slot="payment">
<div data-slot="button-row">
<form action={updateShare} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="dataShare" value={workspaceInfo()?.dataShare ? "false" : "true"} />
<button data-color="ghost" type="submit" disabled={updateShareSubmission.pending}>
{workspaceInfo()?.dataShare
? updateShareSubmission.pending
? "Opting out..."
: "Opt out"
: updateShareSubmission.pending
? "Opting in..."
: "Opt in"}
</button>
</form>
</div>
</div>
</div>
</section>
)
}

View File

@@ -15,6 +15,7 @@ export function useAuthSession() {
return useSession<AuthSession>({
password: "0".repeat(32),
name: "auth",
maxAge: 60 * 60 * 24 * 365,
cookie: {
secure: false,
httpOnly: true,

View File

@@ -1,10 +1,8 @@
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray } from "@opencode/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { redirect } from "@solidjs/router"
import { AccountTable } from "@opencode/console-core/schema/account.sql.js"
import { Actor } from "@opencode/console-core/actor.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { createClient } from "@openauthjs/openauth/client"
import { useAuthSession } from "./auth.session"
@@ -54,25 +52,35 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
}
const accounts = Object.keys(auth.data.account ?? {})
if (accounts.length) {
const result = await Database.transaction(async (tx) => {
return await tx
.select({
user: UserTable,
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspace)))
const user = await Database.use((tx) =>
tx
.select()
.from(UserTable)
.where(
and(
eq(UserTable.workspaceID, workspace),
isNull(UserTable.timeDeleted),
inArray(UserTable.accountID, accounts),
),
)
.limit(1)
.execute()
.then((x) => x[0])
})
if (result) {
.then((x) => x[0]),
)
if (user) {
await Database.use((tx) =>
tx
.update(UserTable)
.set({ timeSeen: sql`now()` })
.where(and(eq(UserTable.workspaceID, workspace), eq(UserTable.id, user.id))),
)
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
userID: user.id,
workspaceID: user.workspaceID,
accountID: user.accountID,
role: user.role,
},
}
}

View File

@@ -1,4 +1,4 @@
import { Actor } from "@opencode/console-core/actor.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { getActor } from "./auth"
export async function withActor<T>(fn: () => T, workspace?: string) {

View File

@@ -0,0 +1,34 @@
import { query } from "@solidjs/router"
export const github = query(async () => {
"use server"
const headers = {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}
try {
const [meta, releases, contributors] = await Promise.all([
fetch("https://api.github.com/repos/sst/opencode", { headers }).then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/releases", { headers }).then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/contributors?per_page=1", { headers }),
])
const [release] = releases
const contributorCount = Number.parseInt(
contributors.headers
.get("Link")!
.match(/&page=(\d+)>; rel="last"/)!
.at(1)!,
)
return {
stars: meta.stargazers_count,
release: {
name: release.name,
url: release.html_url,
},
contributors: contributorCount,
}
} catch (e) {
console.error(e)
}
return undefined
}, "github")

View File

@@ -1,12 +1,11 @@
import { Account } from "@opencode/console-core/account.js"
import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server"
import { withActor } from "~/context/auth.withActor"
import { getLastSeenWorkspaceID } from "../workspace/common"
export async function GET(input: APIEvent) {
try {
const workspaces = await withActor(async () => Account.workspaces())
return redirect(`/workspace/${workspaces[0].id}`)
const workspaceID = await getLastSeenWorkspaceID()
return redirect(`/workspace/${workspaceID}`)
} catch {
return redirect("/auth/authorize")
}

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