Compare commits

...

547 Commits

Author SHA1 Message Date
opencode
5e792d7ac5 release: v0.15.26 2025-10-29 16:59:45 +00:00
David Hill
4a77e94e3c Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-10-29 16:16:56 +00:00
David Hill
4c563ea405 Enron update 2025-10-29 16:16:48 +00:00
Aiden Cline
5875257462 ignore: add label 2025-10-29 11:08:46 -05:00
David Hill
9701891e94 testimonial and other polish 2025-10-29 16:02:25 +00:00
opencode
a2ab37c1b6 release: v0.15.25 2025-10-29 16:01:53 +00:00
Dax Raad
4d6e2d8efc autoupgrade latest major only 2025-10-29 11:53:25 -04:00
Aiden Cline
4407d5d96f fix: ensure tool inputs are zod validated 2025-10-29 10:46:57 -05:00
Adam
244945c0e7 fix: desktop error 2025-10-29 10:43:34 -05:00
opencode
c652b2b4e8 release: v0.15.24 2025-10-29 15:38:55 +00:00
David Hill
aabeeb1431 Adding polish 2025-10-29 15:34:45 +00:00
David Hill
0fbedc5e19 Fix form submission 2025-10-29 15:34:39 +00:00
Dax Raad
12782fff14 remove log 2025-10-29 11:27:51 -04:00
Dax Raad
ca463a2346 session diff only include modified files 2025-10-29 11:26:21 -04:00
Aiden Cline
7265cdf817 ignore: rm 2025-10-29 10:19:02 -05:00
David Hill
7baa751351 First pass at adding an enterprise page 2025-10-29 15:16:17 +00:00
Adam
5b86fa9109 wip: desktop work 2025-10-29 07:32:01 -05:00
Adam
aa7e008fe1 wip: desktop work 2025-10-29 07:32:00 -05:00
GitHub Action
792664071c ignore: update download stats 2025-10-29 2025-10-29 12:04:57 +00:00
Aiden Cline
a0541ba57a zen: fix models endpoint to be openai compatible 2025-10-28 22:48:57 -05:00
Aiden Cline
4994bf1b46 ignore: rename type 2025-10-28 22:39:57 -05:00
Tyler Gannon
1e24514d61 add OpenAPI annotations to tui.ts control endpoints (#3519) 2025-10-28 22:39:22 -05:00
opencode
4b1c6300a0 release: v0.15.23 2025-10-29 01:35:27 +00:00
Dax Raad
db3fb9d316 ci: stuff 2025-10-28 21:28:44 -04:00
Dax Raad
cd79676b42 sync 2025-10-28 20:35:18 -04:00
Dax Raad
09e7e0ab70 tag majors 2025-10-28 20:28:30 -04:00
Kevin King
0e60f66604 ignore: python sdk (#2779)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-28 18:32:45 -05:00
Aiden Cline
fc8db6cdf9 fix: ensure timeout param passed to bash tool is positive 2025-10-28 17:32:39 -05:00
kcrommett
5cc37c4ea0 mcp: fix status() to not overwrite connected with failed (#3514) 2025-10-28 16:16:03 -05:00
Adam
46ad456718 wip: desktop work 2025-10-28 15:39:41 -05:00
Haris Gušić
832ffd2303 fix: Use process.stdout.write instead of console.log (#3508) 2025-10-28 15:38:08 -05:00
GitHub Action
b261430880 chore: format code 2025-10-28 20:29:47 +00:00
Adam
545f345848 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam
77ae0b527e wip: desktop work 2025-10-28 15:29:16 -05:00
Adam
c1278109c9 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam
a7a88d01ef wip: desktop work 2025-10-28 15:29:16 -05:00
Adam
4e0ab6b634 wip: desktop work 2025-10-28 15:29:16 -05:00
Adam
d36485b7af wip: desktop work 2025-10-28 15:29:15 -05:00
Adam
1da24f6adb wip: desktop work 2025-10-28 15:29:15 -05:00
Frank
e29dd27632 zen: provider affinity 2025-10-28 15:55:03 -04:00
Aiden Cline
37380e1f94 add --title flag to opencode run (#3507) 2025-10-28 13:32:36 -05:00
Dax Raad
1309ca7a81 ignore 2025-10-28 14:13:47 -04:00
Dax Raad
c1515316f5 core: fix additions and deletions counting in edit tool filediff 2025-10-28 14:08:10 -04:00
Danilo Favato
b66e7b6fce tweak: add experimental chatMaxRetries to config (#2116)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-28 12:09:41 -05:00
oribi
eb398f1951 add OPENCODE_CONFIG_DIR to allow loading a custom config directory (#3504)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-28 11:50:09 -05:00
Aiden Cline
643c22d21f add catch for mcp tool execution 2025-10-28 10:23:04 -05:00
Aiden Cline
74acd08ead add catch for mcp tool execution 2025-10-28 10:23:03 -05:00
opencode
49ea5aa2ad release: v0.15.20 2025-10-28 15:12:37 +00:00
Aiden Cline
ee1af0fe80 fix: blank version issue 2025-10-28 10:03:53 -05:00
GitHub Action
dfebf40471 ignore: update download stats 2025-10-28 2025-10-28 12:04:31 +00:00
opencode
6af6a1295f release: v0.15.19 2025-10-28 08:12:32 +00:00
Dax Raad
22821744ef feat: add OPENCODE_FAKE_VCS flag for VCS testing and update todo tracking instructions 2025-10-28 02:54:29 -04:00
Aiden Cline
872c9467b2 chore: rm unused import 2025-10-28 00:43:29 -05:00
Dax Raad
d8249f32a8 do not set temperature for claude models 2025-10-28 01:14:25 -04:00
Aiden Cline
982954cc1b feat (acp): mcp server support, file diffs, some default slash commands (/init, /compact), show todos properly (#3490)
The mcp server support does not mean acp didn't allow u to use mcp servers previously, it means that now you can connect new servers via ACP instead of relying on the opencode defined ones
2025-10-28 00:08:30 -05:00
Frank
4caa458232 acp: fix type error 2025-10-27 21:40:08 -04:00
Frank
6fe8e3973c zen: support 1M claude context 2025-10-27 21:36:10 -04:00
Frank
7816901713 wip: zen doc 2025-10-27 21:25:56 -04:00
Frank
71abca9571 wip: zen 2025-10-27 19:26:28 -04:00
kcrommett
7216a8c86d fix: editor paste functionality for text attachments (#3489) 2025-10-27 17:51:33 -05:00
Jay V
e3e16e58c5 docs: edit 2025-10-27 18:16:48 -04:00
Dax Raad
a2951a2702 Remove typecheck script from desktop package 2025-10-27 18:03:32 -04:00
Jay V
55453dc606 Add missing dependencies for desktop package 2025-10-27 17:49:31 -04:00
Jay V
198d7f7e5f Merge branch 'doc-acp' into dev 2025-10-27 17:49:24 -04:00
Jay V
e3e9fd7aa8 docs: edit 2025-10-27 17:48:17 -04:00
Aiden Cline
3c56dbcf58 chore: rm comment 2025-10-27 16:15:13 -05:00
Aiden Cline
ee07ed2dc4 chore: delete unused file 2025-10-27 15:44:12 -05:00
Adam
485e4520e7 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam
fc115ea367 wip: desktop work 2025-10-27 15:37:07 -05:00
Adam
d03b79e61e wip: desktop work 2025-10-27 15:37:06 -05:00
Adam
0acae8211a wip: desktop work 2025-10-27 15:37:06 -05:00
Aiden Cline
0af4505756 fix: litellm error tool= param must be specified 2025-10-27 14:03:42 -05:00
Aurelien Ribon
a606e1d2ec fix: dont set reasoning effort to medium for gpt-5-pro (#3474) 2025-10-27 10:50:57 -05:00
Aiden Cline
0e65700183 update sdk 2025-10-27 10:47:04 -05:00
Aiden Cline
e6301ca5d5 tweak: rename event 2025-10-27 10:42:47 -05:00
Bernat Pericàs
b562863fcc feat: add session.started event that triggers when a new session is created (#3413) 2025-10-27 07:18:23 -05:00
GitHub Action
db85f01eff ignore: update download stats 2025-10-27 2025-10-27 12:04:50 +00:00
Aiden Cline
1a6fd018f6 Revert "fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)"
This reverts commit a9624c0fff.
2025-10-27 01:30:13 -05:00
Aiden Cline
fdb5bae3c6 docs: acp 2025-10-27 00:56:00 -05:00
Haris Gušić
a9624c0fff fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-27 00:03:10 -05:00
Aiden Cline
316d4c9197 wip 2025-10-26 23:49:56 -05:00
Denys Rybalka
5e886c35d5 chore: use stable URLs in PKGBUILD (#3448) 2025-10-26 19:50:45 -05:00
Aiden Cline
5162268f9d docs: update agent frontmatter permission example 2025-10-26 15:04:48 -05:00
Jérôme Benoit
0eb899a950 chore: cleanup versioned zod imports (#3460)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-26 14:50:41 -05:00
Dan McGuirk
3241f6b8bb docs: fix typos (#3454) 2025-10-26 14:37:25 -05:00
GitHub Action
2c792f17e6 ignore: update download stats 2025-10-26 2025-10-26 12:04:22 +00:00
Joscha Götzer
7d0c6860cd fix: make build script work cross-platform (#3430)
Co-authored-by: JosXa <info@josxa.dev>
2025-10-26 01:40:17 -05:00
Aiden Cline
c70e393c81 Remove claude-haiku-4.5 from default priority for GitHub Copilot session title generation 2025-10-26 01:21:34 -05:00
opencode
20963c4186 release: v0.15.18 2025-10-26 03:49:21 +00:00
Aiden Cline
0a778a2789 make title gen more reliable 2025-10-25 22:14:29 -05:00
Aiden Cline
42c1e61bf4 fix: $ invocation not .quiet() (#3449) 2025-10-25 16:27:09 -05:00
Dax Raad
795b845782 update anthropic prompt 2025-10-25 17:26:27 -04:00
Mohammad Alhashemi
2e434a459a feat: add noReply parameter (#3433)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-25 14:56:54 -05:00
Paulo Edgar Castro
ae62bc8b1f fix: timeout param that allows user to disable provider timeout (#3443) 2025-10-25 12:08:27 -05:00
GitHub Action
187a5fe301 ignore: update download stats 2025-10-25 2025-10-25 12:04:18 +00:00
opencode
fc2afdc92f release: v0.15.17 2025-10-25 06:45:23 +00:00
Aiden Cline
fe5e7cfd1b ignore: rm change 2025-10-25 01:35:43 -05:00
Aiden Cline
98d51dde6a acp: slash commands, agents, permissions, @ references, code cleanup (#3403)
Co-authored-by: yetone <yetoneful@gmail.com>
2025-10-25 01:32:46 -05:00
Aiden Cline
5fec5ff424 fix: bedrock reasoning 2025-10-24 17:35:08 -05:00
Adam
fea6a357bc wip: desktop work 2025-10-24 16:04:44 -05:00
Adam
6b82153263 wip: desktop work 2025-10-24 15:51:31 -05:00
Adam
fa8e714d69 wip: desktop work 2025-10-24 15:43:47 -05:00
Adam
90515bc8c3 wip: desktop work 2025-10-24 15:02:31 -05:00
Adam
e34042e17a wip: accordion css not going to keep me down 2025-10-24 15:01:28 -05:00
Dax Raad
6ff0ce8bc5 ignore: improve session timeline debugging and message display functionality 2025-10-24 15:45:37 -04:00
Aiden Cline
e88b659545 make plan agent whitelist more conservative (#3424) 2025-10-24 14:40:36 -05:00
Dax Raad
74048ece2d ignore: fix new session message loading with retry logic to handle server processing delays 2025-10-24 15:35:53 -04:00
Dax Raad
6646f7264a ignore: highlight active session in sidebar to improve visual feedback 2025-10-24 15:27:20 -04:00
Dax Raad
18e549a474 ignore: fix session activation after creation to ensure proper state management 2025-10-24 15:24:17 -04:00
Adam
82249754e7 fix: pierre dep 2025-10-24 13:51:24 -05:00
Aiden Cline
5a0228897b ignore: reword 2025-10-24 13:46:47 -05:00
Adam
e2920c06a3 wip: desktop work 2025-10-24 13:01:22 -05:00
Dax Raad
4da3aa2eb2 add missing dep 2025-10-24 13:43:33 -04:00
Adam
efe7f01f41 wip: desktop work 2025-10-24 12:38:00 -05:00
Adam
9ae3d74adc wip: desktop work 2025-10-24 12:32:02 -05:00
Adam
477b6c584d wip: desktop work 2025-10-24 12:16:33 -05:00
Adam
86447b5764 wip: desktop work 2025-10-24 12:16:33 -05:00
Adam
fe8f6d7a3e wip: desktop work 2025-10-24 12:16:33 -05:00
Adam
59b5f53509 wip: desktop work 2025-10-24 12:16:32 -05:00
Adam
3eb2db98ed wip: desktop work 2025-10-24 12:16:32 -05:00
Adam
35dec0649d wip: desktop work 2025-10-24 12:16:32 -05:00
Adam
78a7f79143 wip: ui package demo page 2025-10-24 12:16:32 -05:00
Aiden Cline
707ed72381 adjust edit tool multiple matches error wording (#3418) 2025-10-24 12:14:49 -05:00
Dax Raad
21880e199d mroe summary tweaks 2025-10-24 12:37:23 -04:00
Dax Raad
736a85d427 track finish reason 2025-10-24 12:23:24 -04:00
Dax Raad
fb40dc6b20 generate user message title and body 2025-10-24 11:50:42 -04:00
Hieu Nguyen
483fcdaddb feat: support lua lsp (#3402) 2025-10-24 10:37:11 -05:00
Andrew Pashynnyk
883b71ac36 fix: respect local config for autoupdate settings (#3408)
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 09:50:09 -05:00
Dax Raad
3e574c71cb potentially fix fetch failed timeout errors 2025-10-24 09:46:28 -04:00
Dax Raad
4cab66da6c test cleanup 2025-10-24 09:29:38 -04:00
Dax Raad
7003efd2da fix summary 2025-10-24 09:24:58 -04:00
Dax Raad
06fe87b361 fix failing migration 2025-10-24 09:20:15 -04:00
GitHub Action
944fda45e6 ignore: update download stats 2025-10-24 2025-10-24 12:05:02 +00:00
opencode
343471b98d release: v0.15.16 2025-10-24 04:52:27 +00:00
Dax Raad
56528493dc codex does not have reasoning effort set 2025-10-24 00:29:34 -04:00
opencode
e66156c86e release: v0.15.15 2025-10-24 03:51:17 +00:00
Dax Raad
8b9b8ca15b update codex prompt 2025-10-23 23:36:32 -04:00
Haris Gušić
50cc641288 fix: Opencode fails with ENOENT posix_spawn '/usr/bin/rg' (#3396) 2025-10-23 18:05:01 -05:00
Bernat Pericàs
4c90bf3e07 refactor: whitelist some safe bash tools in Plan agent (#3288)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-23 17:11:08 -05:00
Aiden Cline
4216c1c2a9 adjust changelog generation 2025-10-23 16:16:11 -05:00
Dax Raad
4bd7646ccb regen sdk 2025-10-23 16:33:00 -04:00
Dax Raad
cee7106054 session summaries in data 2025-10-23 16:28:20 -04:00
Dax Raad
f4dfae0bb0 ignore: diff stuff 2025-10-23 16:04:58 -04:00
Aiden Cline
9b5fe10df6 add flag wildcard parsing support for bash tool (#3390) 2025-10-23 13:35:09 -05:00
Aiden Cline
b5f336c0ea test: rm flaky test 2025-10-23 12:52:08 -05:00
Aiden Cline
913c3ae799 tweak: split out title before newline 2025-10-23 12:44:43 -05:00
Thierry Delafontaine
a68111ca77 fix: move zod-to-json-schema to dependencies (#3387) 2025-10-23 12:16:03 -05:00
Aiden Cline
5f8a3a574e docs: fix numbers 2025-10-23 11:53:21 -05:00
Aiden Cline
d69e8e5528 docs: tweak google vertex 2025-10-23 11:49:53 -05:00
Yuku Kotani
e5df43f9b7 docs: Add Google Vertex AI provider documentation (#3349) 2025-10-23 11:44:06 -05:00
Andrew Pashynnyk
3c7b229d8b fix: allow tool.execute.after hook to modify MCP tool output (#3381) 2025-10-23 10:38:55 -05:00
Mani Sundararajan
9ab4414aef docs: rm winget as a recommended installation method under windows (#3382)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-23 09:25:42 -05:00
GitHub Action
c2cf6fb904 ignore: update download stats 2025-10-23 2025-10-23 12:04:55 +00:00
opencode
5e69bdbef4 release: v0.15.14 2025-10-23 05:55:28 +00:00
Yesh Yendamuri
f81e28c673 feat: add model management to ACP sessions (#3358) 2025-10-23 00:43:28 -05:00
Aiden Cline
61899d4fa7 regen sdk 2025-10-22 23:00:03 -05:00
Aiden Cline
7c7ebb0a9d feat: retry parts (#3369) 2025-10-22 18:31:36 -05:00
Dax Raad
9def7cff2d summary tweaks 2025-10-22 19:03:08 -04:00
geril07
c2ef930d2a add option to allow agent switches to not change model (#3356) 2025-10-22 17:51:46 -05:00
Adam
3c3d2f5a6e wip: desktop work 2025-10-22 17:51:16 -05:00
Dax Raad
f435049d36 sync 2025-10-22 18:49:57 -04:00
Dax Raad
1f80de2fa6 core: add experimental turn summarization to compact conversation history 2025-10-22 18:33:46 -04:00
Adam
f194a784b0 wip: desktop work 2025-10-22 17:33:08 -05:00
Adam
89b703c387 wip: desktop work 2025-10-22 17:31:49 -05:00
Aiden Cline
eff12cb484 vscode: eslint fix 2025-10-22 17:17:47 -05:00
Aiden Cline
593e89b4f4 vscode: fix script 2025-10-22 17:11:51 -05:00
Aiden Cline
4d3f703715 vscode: adjust tsconfig 2025-10-22 16:43:06 -05:00
Aiden Cline
123dcc10cc ignore: cleanup bun.lock w/ bun i 2025-10-22 15:34:41 -05:00
Dax Raad
28d8af48a0 add parent id to assistant messages 2025-10-22 15:01:13 -04:00
Kyle Galbraith
10ff6e9830 docs: fix typo in SDK documentation (#3355) 2025-10-22 11:43:28 -05:00
theVinchi
a7b43d82ab add Amazon Nova models to us-* prefix requirement list (#3357) 2025-10-22 11:41:38 -05:00
Aiden Cline
9005fd31ed tweak 2025-10-22 11:29:10 -05:00
Aiden Cline
d2bded23c3 tweak 2025-10-22 11:28:06 -05:00
Aiden Cline
c0cbc37f85 tweak: model priority 2025-10-22 11:12:32 -05:00
Aiden Cline
9df61055e2 change default title model 2025-10-22 10:41:08 -05:00
GitHub Action
074136b1e8 ignore: update download stats 2025-10-22 2025-10-22 12:04:58 +00:00
Affaan Mustafa
8db5951287 feat: Improve editor detection with auto-discovery and better error messages (#3155)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-10-21 20:49:42 -05:00
Haris Gušić
97c7e941eb fix: opencode run shouldn't print to stderr (#3341) 2025-10-21 19:53:09 -05:00
Aiden Cline
354f5c3281 ignore: fix acp test (#3339) 2025-10-21 15:27:59 -05:00
opencode
833706cda4 release: v0.15.13 2025-10-21 20:24:20 +00:00
Aiden Cline
2a951cea38 ignore: reword 2025-10-21 14:43:34 -05:00
Dax Raad
d9a8d2032a fix sourcemapping so errors show proper stack trace 2025-10-21 15:32:24 -04:00
Dax Raad
d7cdabe8b7 refactor acp args 2025-10-21 15:24:09 -04:00
Aiden Cline
e7c74d13cc ignore: reword contributing.md 2025-10-21 14:12:33 -05:00
opencode
6ac5a447c2 release: v0.15.12 2025-10-21 17:31:56 +00:00
Aiden Cline
cb4670e6de ignore: add label 2025-10-21 11:12:52 -05:00
Aiden Cline
ca0f3902b7 fix: provider option transforms (#3331) 2025-10-21 11:08:21 -05:00
Dax Raad
e9996342a7 core: provide line-level statistics in file diffs to help users understand the scale of changes 2025-10-21 11:54:41 -04:00
opencode
a84826061d release: v0.15.11 2025-10-21 15:04:22 +00:00
Dax Raad
7a20f77ebf core: improve error handling with console output for debugging 2025-10-21 10:55:10 -04:00
Giuseppe Rota
731122bf99 fix(acp): use newer acp package (#3317) 2025-10-21 09:35:09 -05:00
GitHub Action
f9036734eb ignore: update download stats 2025-10-21 2025-10-21 12:06:20 +00:00
kcrommett
a99bd3aa2c tweak: adjust file api to encode images (#3292) 2025-10-21 00:52:39 -05:00
zWing
96efede846 fix: openai options when using @ai-sdk/azure (#3315) 2025-10-20 23:30:13 -05:00
Cheol Kang
2f66055d25 feat: add -f/--file flag to opencode run command (#3295)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-20 23:25:54 -05:00
opencode
6995dab1dc release: v0.15.10 2025-10-20 22:21:03 +00:00
Dax Raad
a0a09f421c core: add session diff API to show file changes between snapshots 2025-10-20 17:59:26 -04:00
Joe Schmitt
f3f21194ae feat: Add ACP (Agent Client Protocol) support (#2947)
Co-authored-by: opencode-bot <devnull@opencode.local>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
2025-10-20 16:55:22 -05:00
opencode
835fa9fb81 release: v0.15.9 2025-10-20 21:16:57 +00:00
Aiden Cline
96ae6d51aa ignore: link labels 2025-10-20 16:02:23 -05:00
Frank
075ef0fa34 wip: share 2025-10-20 16:17:51 -04:00
Aiden Cline
89b72e4442 fix: bash tool permission merges (#3302) 2025-10-20 15:15:14 -05:00
Frank
7a7b3c6315 wip: zen 2025-10-20 13:22:43 -04:00
Rui Ferrão
3d48c14d29 docs: clarify the effects of the subtask command option with primary … (#3299) 2025-10-20 12:16:09 -05:00
GitHub Action
bfa79ed44b ignore: update download stats 2025-10-20 2025-10-20 12:04:59 +00:00
Hieu Nguyen
4d8268c818 feat: support astro lsp (#3242) 2025-10-19 22:49:06 -05:00
Squibid
95d413bec6 theme: add mellow theme (#3198) 2025-10-19 22:34:04 -05:00
Joscha Götzer
1cb5a70382 feat(theme): Add Night Owl theme (#3269) 2025-10-19 22:30:04 -05:00
Walter
6adc16ca8a fix(tui): Allow modals to handle ESC key before force closing (#3279) 2025-10-19 22:16:54 -05:00
Zeldris
10ebe9ae09 docs: add scoop & winget to readme (#3215) 2025-10-19 21:50:51 -05:00
Aiden Cline
43a07c6aca hide invalid option 2025-10-19 20:51:33 -05:00
Aiden Cline
5d3a88f34f fix: snapshot undo bug (#3290) 2025-10-19 18:42:34 -05:00
Ivan Uhalin
e47edfffe4 docs: fix Grep by Vercel link in documentation (#3280) 2025-10-19 11:51:57 -05:00
GitHub Action
141097fc73 ignore: update download stats 2025-10-19 2025-10-19 12:04:24 +00:00
Aiden Cline
c8898463a7 ignore: adjust template 2025-10-18 23:45:01 -05:00
Aiden Cline
1c7bd6365e replace all $ARGUMENTS (#3276) 2025-10-18 23:37:17 -05:00
Aiden Cline
290d15a80f ignore: add version to bug template 2025-10-18 14:56:06 -05:00
Aiden Cline
233a018fe5 docs: contributing.md (#3248)
Co-authored-by: Jay <air@live.ca>
2025-10-18 14:10:14 -05:00
opencode
d69beec087 release: v0.15.8 2025-10-18 18:36:52 +00:00
Aiden Cline
1f869bccc1 make compact interruptable (#3251) 2025-10-18 11:49:29 -05:00
GitHub Action
8da8c9e78c ignore: update download stats 2025-10-18 2025-10-18 12:04:14 +00:00
Adam
335d833655 wip: desktop work 2025-10-17 15:22:11 -05:00
Haris Gušić
1dba01e057 Improve typo directory error (#3247) 2025-10-17 14:34:37 -05:00
Jay V
a3de43f3de ignore: clarify that workspaces are free for teams during beta 2025-10-17 15:19:19 -04:00
Jay V
22ad4f5365 ignore: help users discover that teams feature is free during beta 2025-10-17 14:33:21 -04:00
Jay V
5bfbec60b5 docs: clarify team collaboration features and pricing 2025-10-17 13:58:10 -04:00
Adam
cc18b58ff9 wip: desktop work 2025-10-17 12:07:58 -05:00
Adam
887a819f24 wip: desktop work 2025-10-17 12:06:36 -05:00
mgrachten
fe8b3a2515 set 755 permissions (#3237) 2025-10-17 10:42:42 -05:00
GitHub Action
86079353ef ignore: update download stats 2025-10-17 2025-10-17 12:04:43 +00:00
Frank
b7c8690414 wip: zen 2025-10-17 02:23:45 -04:00
Frank
c25b9bf65a wip: zen 2025-10-17 01:15:25 -04:00
Matt Gillard
ddb2e6957c added AU inference for bedrock haiku 4.5 (#3206)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
2025-10-16 23:48:41 -05:00
Frank
a590b32a10 wip: zen 2025-10-17 00:26:00 -04:00
Frank
5f7bba11fd wip: zen 2025-10-16 23:43:49 -04:00
Frank
4663ea5faa wip: zen 2025-10-16 23:19:25 -04:00
Frank
dd581e8577 wip: zen 2025-10-17 03:17:07 +00:00
opencode
bad01d76de release: v0.15.7 2025-10-17 03:17:07 +00:00
Aiden Cline
d69366b00c Revert "try to avoid persisting empty thinking/text blocks"
This reverts commit d8a15e7bc9.
2025-10-16 22:04:55 -05:00
Frank
1947580b08 wip: zen 2025-10-16 22:28:34 -04:00
Frank
ca9b13e8a2 wip: zen 2025-10-16 22:28:09 -04:00
Hieu Nguyen
92d9a0ec61 feat: deno lsp (#3210)
Co-authored-by: hiunguynx <hieu.nm1@teko.vn>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-10-16 18:08:36 -05:00
opencode
2be9ed2590 release: v0.15.6 2025-10-16 23:02:59 +00:00
Aiden Cline
25861f6d0d sync 2025-10-16 17:49:02 -05:00
Aiden Cline
b24f4e3d2c fix: timeout option (#3229) 2025-10-16 17:47:41 -05:00
Frank
729ad1cb75 wip: zen 2025-10-16 17:50:32 -04:00
Haris Gušić
fb4105a46c docs: fix Ctrl+K styling in documentation (#3021) 2025-10-16 15:30:28 -05:00
Frank
7abc3e9794 ci: fix 2025-10-16 16:04:25 -04:00
Frank
88fef05923 wip: zen 2025-10-16 15:58:51 -04:00
GitHub Action
8552f3555e chore: format code 2025-10-16 19:55:30 +00:00
Adam
47d9e01765 wip: css/ui and desktop work 2025-10-16 14:53:44 -05:00
Aiden Cline
fc18fc8a08 fix: bash hangs & orphans (#3225) 2025-10-16 14:39:36 -05:00
Dax Raad
7474788778 ci: fix 2025-10-16 15:36:12 -04:00
GitHub Action
26d0d20e4d chore: format code 2025-10-16 19:08:21 +00:00
Adam
20229f147b wip: css/ui and desktop work 2025-10-16 14:07:37 -05:00
Aiden Cline
149cb6a9ec fix: connection closed 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
1834 changed files with 62417 additions and 7911 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

71
.github/publish-python-sdk.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
#
# This file is intentionally in the wrong dir, will move and add later....
#
# name: publish-python-sdk
# on:
# release:
# types: [published]
# workflow_dispatch:
# jobs:
# publish:
# runs-on: ubuntu-latest
# permissions:
# contents: read
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Setup Bun
# uses: oven-sh/setup-bun@v1
# with:
# bun-version: 1.2.21
# - name: Install dependencies (JS/Bun)
# run: bun install
# - name: Install uv
# shell: bash
# run: curl -LsSf https://astral.sh/uv/install.sh | sh
# - name: Generate Python SDK from OpenAPI (CLI)
# shell: bash
# run: |
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/generate.py --source cli
# - name: Sync Python dependencies
# shell: bash
# run: |
# ~/.local/bin/uv sync --dev --project packages/sdk/python
# - name: Set version from release tag
# shell: bash
# run: |
# TAG="${GITHUB_REF_NAME:-}"
# if [ -z "$TAG" ]; then
# TAG="$(git describe --tags --abbrev=0 || echo 0.0.0)"
# fi
# echo "Using version: $TAG"
# VERSION="$TAG" ~/.local/bin/uv run --project packages/sdk/python python - <<'PY'
# import os, re, pathlib
# root = pathlib.Path('packages/sdk/python')
# pt = (root / 'pyproject.toml').read_text()
# version = os.environ.get('VERSION','0.0.0').lstrip('v')
# pt = re.sub(r'(?m)^(version\s*=\s*")[^"]+("\s*)$', f"\\1{version}\\2", pt)
# (root / 'pyproject.toml').write_text(pt)
# # Also update generator config override for consistency
# cfgp = root / 'openapi-python-client.yaml'
# if cfgp.exists():
# cfg = cfgp.read_text()
# cfg = re.sub(r'(?m)^(package_version_override:\s*)\S+$', f"\\1{version}", cfg)
# cfgp.write_text(cfg)
# PY
# - name: Build and publish to PyPI
# env:
# PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
# shell: bash
# run: |
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/publish.py

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,17 @@ 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: Install extension dependencies
run: bun install
working-directory: ./sdks/vscode
- 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,18 +49,21 @@ 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: Setup npm auth
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Publish
run: |
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}

View File

@@ -24,27 +24,11 @@ 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-
- name: Install dependencies
run: bun install
- uses: ./.github/actions/setup-bun
- name: Publish
run: |
./packages/opencode/script/publish.ts
./script/publish.ts
env:
OPENCODE_SNAPSHOT: true
OPENCODE_TAG: ${{ github.ref_name }}
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

View File

@@ -18,15 +18,13 @@ 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: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun install
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

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
@@ -8,5 +12,9 @@ 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

@@ -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"
}
}
]
}
}

68
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,68 @@
# Contributing to OpenCode
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
- Bug fixes
- Additional LSPs / Formatters
- Improvements to LLM performance
- Support for new providers
- Fixes for environment-specific quirks
- Missing standard behavior
- Documentation improvements
However, any UI or core product feature must go through a design review with the core team before implementation.
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
## Developing OpenCode
- Requirements: Bun 1.3+, 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
choco 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?

219
STATS.md
View File

@@ -1,98 +1,125 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ----------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ----------------- | ----------------- | ------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |

1002
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -104,7 +104,7 @@ To test locally:
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
- `MOCK_TOKEN`: A GitHub persontal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
- `MOCK_EVENT`: Mock GitHub event payload (see templates below).
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`.
@@ -118,7 +118,7 @@ Replace:
- `"owner":"sst"` with repo owner
- `"repo":"hello-world"` with repo name
- `"actor":"fwang"` with the GitHub username of commentor
- `"actor":"fwang"` with the GitHub username of commenter
- `"number":4` with the GitHub issue id
- `"body":"hey opencode, summarize thread"` with comment body

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:*"
}
}

View File

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

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

@@ -6,8 +6,8 @@ export const domain = (() => {
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
// new cloudflare.RegionalHostname("RegionalHostname", {
// hostname: domain,
// regionKey: "us",
// zoneId: zoneID,
// })
new cloudflare.RegionalHostname("RegionalHostname", {
hostname: domain,
regionKey: "us",
zoneId: zoneID,
})

View File

@@ -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,7 +3,7 @@
"name": "opencode",
"private": true,
"type": "module",
"packageManager": "bun@1.2.21",
"packageManager": "bun@1.3.0",
"scripts": {
"dev": "bun run packages/opencode/src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -13,30 +13,49 @@
"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",
"@pierre/precision-diffs": "0.3.6",
"@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"
}
},
"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/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*"
},
"repository": {
"type": "git",
"url": "https://github.com/sst/opencode"
@@ -44,7 +63,7 @@
"license": "MIT",
"prettier": {
"semi": false,
"printWidth": 120
"printWidth": 100
},
"trustedDependencies": [
"esbuild",
@@ -56,5 +75,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.toLowerCase(), light: value.toLowerCase() }
}
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.toLowerCase()
}
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.toLowerCase()
}
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}`)
}
},
}
}

View File

@@ -1,225 +0,0 @@
import { createEffect, Show, For, createMemo, type JSX, createResource } from "solid-js"
import { Dialog } from "@kobalte/core/dialog"
import { Icon, IconButton } from "@/ui"
import { createStore } from "solid-js/store"
import { entries, flatMap, groupBy, map, pipe } from "remeda"
import { createList } from "solid-list"
import fuzzysort from "fuzzysort"
interface SelectDialogProps<T> {
items: T[] | ((filter: string) => Promise<T[]>)
key: (item: T) => string
render: (item: T) => JSX.Element
filter?: string[]
current?: T
placeholder?: string
groupBy?: (x: T) => string
onSelect?: (value: T | undefined) => void
onClose?: () => void
}
export function SelectDialog<T>(props: SelectDialogProps<T>) {
let scrollRef: HTMLDivElement | undefined
const [store, setStore] = createStore({
filter: "",
mouseActive: false,
})
const [grouped] = createResource(
() => store.filter,
async (filter) => {
const needle = filter.toLowerCase()
const all = (typeof props.items === "function" ? await props.items(needle) : props.items) || []
const result = pipe(
all,
(x) => {
if (!needle) return x
if (!props.filter && Array.isArray(x) && x.every((e) => typeof e === "string")) {
return fuzzysort.go(needle, x).map((x) => x.target) as T[]
}
return fuzzysort.go(needle, x, { keys: props.filter! }).map((x) => x.obj)
},
groupBy((x) => (props.groupBy ? props.groupBy(x) : "")),
// mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))),
entries(),
map(([k, v]) => ({ category: k, items: v })),
)
return result
},
)
const flat = createMemo(() => {
return pipe(
grouped() || [],
flatMap((x) => x.items),
)
})
const list = createList({
items: () => flat().map(props.key),
initialActive: props.current ? props.key(props.current) : undefined,
loop: true,
})
const resetSelection = () => {
const all = flat()
if (all.length === 0) return
list.setActive(props.key(all[0]))
}
createEffect(() => {
store.filter
scrollRef?.scrollTo(0, 0)
resetSelection()
})
createEffect(() => {
const all = flat()
if (store.mouseActive || all.length === 0) return
if (list.active() === props.key(all[0])) {
scrollRef?.scrollTo(0, 0)
return
}
const element = scrollRef?.querySelector(`[data-key="${list.active()}"]`)
element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
})
const handleInput = (value: string) => {
setStore("filter", value)
resetSelection()
}
const handleSelect = (item: T) => {
props.onSelect?.(item)
props.onClose?.()
}
const handleKey = (e: KeyboardEvent) => {
setStore("mouseActive", false)
if (e.key === "Enter") {
e.preventDefault()
const selected = flat().find((x) => props.key(x) === list.active())
if (selected) handleSelect(selected)
} else if (e.key === "Escape") {
e.preventDefault()
props.onClose?.()
} else {
list.onKeyDown(e)
}
}
return (
<Dialog defaultOpen modal onOpenChange={(open) => open || props.onClose?.()}>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/50 backdrop-blur-sm z-[100]" />
<Dialog.Content
class="fixed top-[20%] left-1/2 -translate-x-1/2 w-[90vw] max-w-2xl
shadow-[0_0_33px_rgba(0,0,0,0.8)]
bg-background border border-border-subtle/30 rounded-lg z-[101]
max-h-[60vh] flex flex-col"
>
<div class="border-b border-border-subtle/30">
<div class="relative">
<Icon name="command" size={16} class="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted/80" />
<input
type="text"
value={store.filter}
onInput={(e) => handleInput(e.currentTarget.value)}
onKeyDown={handleKey}
placeholder={props.placeholder}
class="w-full pl-10 pr-4 py-2 rounded-t-md
text-sm text-text placeholder-text-muted/70
focus:outline-none"
autofocus
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
/>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
{/* <Show when={fileResults.loading && mode() === "files"}>
<div class="text-text-muted">
<Icon name="refresh" size={14} class="animate-spin" />
</div>
</Show> */}
<Show when={store.filter}>
<IconButton
size="xs"
variant="ghost"
class="text-text-muted hover:text-text"
onClick={() => {
setStore("filter", "")
resetSelection()
}}
>
<Icon name="close" size={14} />
</IconButton>
</Show>
</div>
</div>
</div>
<div ref={(el) => (scrollRef = el)} class="relative flex-1 overflow-y-auto">
<Show
when={flat().length > 0}
fallback={<div class="text-center py-8 text-text-muted text-sm">No results</div>}
>
<For each={grouped()}>
{(group) => (
<>
<Show when={group.category}>
<div class="top-0 sticky z-10 bg-background-panel p-2 text-xs text-text-muted/60 tracking-wider uppercase">
{group.category}
</div>
</Show>
<div class="p-2">
<For each={group.items}>
{(item) => (
<button
data-key={props.key(item)}
onClick={() => handleSelect(item)}
onMouseMove={() => {
setStore("mouseActive", true)
list.setActive(props.key(item))
}}
classList={{
"w-full px-3 py-2 flex items-center gap-3": true,
"rounded-md text-left transition-colors group": true,
"bg-background-element": props.key(item) === list.active(),
}}
>
{props.render(item)}
</button>
)}
</For>
</div>
</>
)}
</For>
</Show>
</div>
<div class="p-3 border-t border-border-subtle/30 flex items-center justify-between text-xs text-text-muted">
<div class="flex items-center gap-5">
<span class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-background-element border border-border-subtle/30 rounded text-[10px]">
</kbd>
Navigate
</span>
<span class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-background-element border border-border-subtle/30 rounded text-[10px]">
</kbd>
Select
</span>
<span class="flex items-center gap-1.5">
<kbd class="px-1.5 py-0.5 bg-background-element border border-border-subtle/30 rounded text-[10px]">
ESC
</kbd>
Close
</span>
</div>
<span>{`${flat().length} results`}</span>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
)
}

View File

@@ -1,107 +0,0 @@
import { Select as KobalteSelect } from "@kobalte/core/select"
import { createMemo } from "solid-js"
import type { ComponentProps } from "solid-js"
import { Icon } from "@/ui/icon"
import { pipe, groupBy, entries, map } from "remeda"
import { Button, type ButtonProps } from "@/ui"
export interface SelectProps<T> {
placeholder?: string
options: T[]
current?: T
value?: (x: T) => string
label?: (x: T) => string
groupBy?: (x: T) => string
onSelect?: (value: T | undefined) => void
class?: ComponentProps<"div">["class"]
classList?: ComponentProps<"div">["classList"]
}
export function Select<T>(props: SelectProps<T> & ButtonProps) {
const grouped = createMemo(() => {
const result = pipe(
props.options,
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
})
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 class="ml-auto">
<Icon name="checkmark" size={16} />
</KobalteSelect.ItemIndicator>
</KobalteSelect.Item>
)}
onChange={(v) => {
props.onSelect?.(v ?? undefined)
}}
>
<KobalteSelect.Trigger
as={Button}
size={props.size || "sm"}
variant={props.variant || "secondary"}
classList={{
...(props.classList ?? {}),
[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
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,
}}
>
<KobalteSelect.Listbox class="overflow-y-auto max-h-48" />
</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">
{(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,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,34 +0,0 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { createEventBus } from "@solid-primitives/event-bus"
import type { Event as SDKEvent } from "@opencode-ai/sdk"
import { useSDK } from "@/context"
export type Event = SDKEvent // can extend with custom events later
function init() {
const sdk = useSDK()
const bus = createEventBus<Event>()
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
bus.emit(event)
}
})
return bus
}
type EventContext = ReturnType<typeof init>
const ctx = createContext<EventContext>()
export function EventProvider(props: ParentProps) {
const value = init()
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useEvent() {
const value = useContext(ctx)
if (!value) {
throw new Error("useEvent must be used within a EventProvider")
}
return value
}

View File

@@ -1,7 +0,0 @@
export { EventProvider, useEvent } from "./event"
export { LocalProvider, useLocal } from "./local"
export { MarkedProvider, useMarked } from "./marked"
export { SDKProvider, useSDK } from "./sdk"
export { ShikiProvider, useShiki } from "./shiki"
export { SyncProvider, useSync } from "./sync"
export { ThemeProvider, useTheme } from "./theme"

View File

@@ -1,434 +0,0 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
import { useSDK, useEvent, useSync } from "@/context"
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
status: FileStatus
}>
export type TextSelection = LocalFile["selection"]
export type View = LocalFile["view"]
export type LocalModel = Omit<Model, "provider"> & {
provider: Provider
}
export type ModelKey = { providerID: string; modelID: string }
function init() {
const sdk = useSDK()
const sync = useSync()
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
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 list = createMemo(() =>
sync.data.provider.flatMap((p) => Object.values(p.models).map((m) => ({ ...m, provider: p }) as LocalModel)),
)
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
const [store, setStore] = createStore<{
model: Record<string, ModelKey>
recent: ModelKey[]
}>({
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 { modelID: model.id, providerID: provider.id }
})
const current = createMemo(() => {
const a = agent.current()
return find(store.model[agent.current().name]) ?? find(a.model ?? fallback())
})
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
return {
list,
current,
recent,
set(model: ModelKey | 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 changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
createEffect((prev: FileStatus[]) => {
const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
for (const p of removed) {
setStore(
"node",
p.path,
produce((draft) => {
draft.status = undefined
draft.view = "raw"
}),
)
load(p.path)
}
for (const p of sync.data.changes) {
if (store.node[p.path] === undefined) {
fetch(p.path).then(() => setStore("node", p.path, "status", p))
} else {
setStore("node", p.path, "status", p)
}
}
return sync.data.changes
}, sync.data.changes)
const changed = (path: string) => {
const node = store.node[path]
if (node?.status) return true
const set = changeset()
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, undefined!)
}
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
const load = async (path: string) => {
const relativePath = relative(path)
sdk.file.read({ query: { path: relativePath } }).then((x) => {
setStore(
"node",
relativePath,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
}
const fetch = async (path: string) => {
const relativePath = relative(path)
const parent = relativePath.split("/").slice(0, -1).join("/")
if (parent) {
await list(parent)
}
}
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
setStore("opened", (x) => {
if (x.includes(relativePath)) return x
return [
...opened()
.filter((x) => x.pinned)
.map((x) => x.path),
relativePath,
]
})
setStore("active", relativePath)
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
if (store.node[relativePath].loaded) return
return load(relativePath)
}
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
})
}),
)
})
}
const search = (query: string) => sdk.find.files({ query: { query } }).then((x) => x.data!)
const bus = useEvent()
bus.listen((event) => {
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":
break
case "edit":
// load(part.state.input["filePath"] as string)
break
default:
break
}
}
break
case "file.watcher.updated":
setTimeout(sync.load.changes, 1000)
const relativePath = relative(event.properties.file)
if (relativePath.startsWith(".git/")) return
load(relativePath)
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)
},
changes,
changed,
children(path: string) {
return Object.values(store.node).filter(
(x) =>
x.path.startsWith(path) &&
x.path !== path &&
!x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
)
},
search,
relative,
}
})()
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,43 +0,0 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { useShiki } from "@/context"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import { bundledLanguages, type BundledLanguage } from "shiki"
function init(highlighter: ReturnType<typeof useShiki>) {
return marked.use(
markedShiki({
async highlight(code, lang) {
if (!(lang in bundledLanguages)) {
lang = "text"
}
if (!highlighter.getLoadedLanguages().includes(lang)) {
await highlighter.loadLanguage(lang as BundledLanguage)
}
return highlighter.codeToHtml(code, {
lang: lang || "text",
theme: "opencode",
tabindex: false,
})
},
}),
)
}
type MarkedContext = ReturnType<typeof init>
const ctx = createContext<MarkedContext>()
export function MarkedProvider(props: ParentProps) {
const highlighter = useShiki()
const value = init(highlighter)
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useMarked() {
const value = useContext(ctx)
if (!value) {
throw new Error("useMarked must be used within a MarkedProvider")
}
return value
}

View File

@@ -1,29 +0,0 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { createOpencodeClient } from "@opencode-ai/sdk/client"
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
function init() {
const client = createOpencodeClient({
baseUrl: `http://${host}:${port}`,
})
return client
}
type SDKContext = ReturnType<typeof init>
const ctx = createContext<SDKContext>()
export function SDKProvider(props: ParentProps) {
const value = init()
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useSDK() {
const value = useContext(ctx)
if (!value) {
throw new Error("useSDK must be used within a SDKProvider")
}
return value
}

View File

@@ -1,172 +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 { createContext, createMemo, Show, useContext, type ParentProps } from "solid-js"
import { useSDK, useEvent } from "@/context"
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 bus = useEvent()
bus.listen((event) => {
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
}
}
})
const sdk = useSDK()
const load = {
provider: () => sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () =>
sdk.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).slice().sort((a, b) => a.id.localeCompare(b.id)),
),
),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
}
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g"))
const sanitize = (text: string) => text.replace(sanitizer(), "")
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))
}
}),
)
},
},
load,
sanitize,
}
}
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,168 +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); */
}
::-webkit-scrollbar-track {
background: var(--theme-background-panel);
}
::-webkit-scrollbar-thumb {
background-color: var(--theme-border-subtle);
border-radius: 6px;
}
* {
scrollbar-color: var(--theme-border-subtle) var(--theme-background-panel);
}
.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,48 +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 {
EventProvider,
SDKProvider,
SyncProvider,
LocalProvider,
ThemeProvider,
ShikiProvider,
MarkedProvider,
} 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}>
<ShikiProvider>
<MarkedProvider>
<SDKProvider>
<EventProvider>
<SyncProvider>
<LocalProvider>
<Router root={Layout}>
<Route path="/" component={Home} />
</Router>
</LocalProvider>
</SyncProvider>
</EventProvider>
</SDKProvider>
</MarkedProvider>
</ShikiProvider>
</ThemeProvider>
</div>
),
root!,
)

View File

@@ -1,708 +0,0 @@
import { Button, 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 { SelectDialog } from "@/components/select-dialog"
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"
import { getDirectory, getFilename } from "@/utils"
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,
modelSelectOpen: false,
fileSelectOpen: false,
})
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) => {
if (e.getModifierState(MOD) && e.shiftKey && e.key.toLowerCase() === "p") {
e.preventDefault()
// TODO: command palette
return
}
if (e.getModifierState(MOD) && e.key.toLowerCase() === "p") {
e.preventDefault()
setStore("fileSelectOpen", true)
return
}
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: { modelID: local.model.current()!.id, providerID: local.model.current()!.provider.id },
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 bg-background z-10"
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">
<Show
when={local.file.changes().length}
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
</span>
</button>
</li>
)}
</For>
</ul>
</Show>
</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 bg-background z-10"
style={`width: ${local.layout.rightWidth()}px`}
>
<div class="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
<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="Placeholder text..."
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}
class="uppercase"
/>
<Button onClick={() => setStore("modelSelectOpen", true)}>
{local.model.current()?.name ?? "Select model"}
<Icon name="chevron-down" size={24} class="text-text-muted" />
</Button>
<span class="text-text-muted/70 whitespace-nowrap">{local.model.current()?.provider.name}</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>
<Show when={store.modelSelectOpen}>
<SelectDialog
key={(x) => `${x.provider.id}:${x.id}`}
items={local.model.list()}
current={local.model.current()}
render={(i) => (
<div class="w-full flex items-center justify-between">
<div class="flex items-center gap-x-2 text-text-muted grow min-w-0">
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-4 invert opacity-40" />
<span class="text-xs text-text whitespace-nowrap">{i.name}</span>
<span class="text-xs text-text-muted/80 whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{i.id}
</span>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0">
<Tooltip forceMount={false} value="Reasoning">
<Icon name="brain" size={16} classList={{ "text-accent": i.reasoning }} />
</Tooltip>
<Tooltip forceMount={false} value="Tools">
<Icon name="hammer" size={16} classList={{ "text-secondary": i.tool_call }} />
</Tooltip>
<Tooltip forceMount={false} value="Attachments">
<Icon name="photo" size={16} classList={{ "text-success": i.attachment }} />
</Tooltip>
<div class="rounded-full bg-text-muted/20 text-text-muted/80 w-9 h-4 flex items-center justify-center text-[10px]">
{new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(i.limit.context)}
</div>
<Tooltip forceMount={false} value={`$${i.cost?.input}/1M input, $${i.cost?.output}/1M output`}>
<div class="rounded-full bg-success/20 text-success/80 w-9 h-4 flex items-center justify-center text-[10px]">
<Switch fallback="FREE">
<Match when={i.cost?.input > 10}>$$$</Match>
<Match when={i.cost?.input > 1}>$$</Match>
<Match when={i.cost?.input > 0.1}>$</Match>
</Switch>
</div>
</Tooltip>
</div>
</div>
)}
filter={["provider.name", "name", "id"]}
groupBy={(x) => x.provider.name}
onClose={() => setStore("modelSelectOpen", false)}
onSelect={(x) => local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined)}
/>
</Show>
<Show when={store.fileSelectOpen}>
<SelectDialog
items={local.file.search}
key={(x) => x}
render={(i) => (
<div class="w-full flex items-center justify-between">
<div class="flex items-center gap-x-2 text-text-muted grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(i)}</span>
<span class="text-xs text-text-muted/80 whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
</div>
)}
onClose={() => setStore("fileSelectOpen", false)}
onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
/>
</Show>
</div>
)
}
const TabVisual = (props: { file: LocalFile }) => {
return (
<div class="flex items-center gap-x-1.5">
<FileIcon node={props.file} class="" />
<span classList={{ "text-xs": true, "text-primary": !!props.file.status?.status, italic: !props.file.pinned }}>
{props.file.name}
</span>
<span class="text-xs opacity-70">
<Switch>
<Match when={props.file.status?.status === "modified"}>
<span class="text-primary">M</span>
</Match>
<Match when={props.file.status?.status === "added"}>
<span class="text-success">A</span>
</Match>
<Match when={props.file.status?.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,36 +0,0 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { type ComponentProps, splitProps } from "solid-js"
export interface ButtonProps {
variant?: "primary" | "secondary" | "ghost"
size?: "sm" | "md" | "lg"
}
export function Button(props: ComponentProps<"button"> & ButtonProps) {
const [split, rest] = splitProps(props, ["variant", "size", "class", "classList"])
return (
<Kobalte
{...rest}
data-size={split.size || "sm"}
data-variant={split.variant || "secondary"}
class="inline-flex items-center justify-center rounded-md cursor-pointer font-medium transition-colors
min-w-0 whitespace-nowrap truncate
data-[size=sm]:h-6 data-[size=sm]:pl-2 data-[size=sm]:text-xs
data-[size=md]:h-8 data-[size=md]:pl-3 data-[size=md]:text-sm
data-[size=lg]:h-10 data-[size=lg]:pl-4 data-[size=lg]:text-base
data-[variant=primary]:bg-primary data-[variant=primary]:text-background
data-[variant=primary]:hover:bg-secondary data-[variant=primary]:focus-visible:ring-primary
data-[variant=secondary]:bg-background-element data-[variant=secondary]:text-text
data-[variant=secondary]:hover:bg-background-element data-[variant=secondary]:focus-visible:ring-secondary
data-[variant=ghost]:text-text data-[variant=ghost]:hover:bg-background-panel data-[variant=ghost]:focus-visible:ring-border-active
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-transparent
disabled:pointer-events-none disabled:opacity-50"
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
>
{props.children}
</Kobalte>
)
}

View File

@@ -1,38 +0,0 @@
import { Button as KobalteButton } from "@kobalte/core/button"
import { splitProps } from "solid-js"
import type { ComponentProps, JSX } from "solid-js"
export interface IconButtonProps extends ComponentProps<typeof KobalteButton> {
variant?: "primary" | "secondary" | "outline" | "ghost"
size?: "xs" | "sm" | "md" | "lg"
children: JSX.Element
}
export function IconButton(props: IconButtonProps) {
const [local, others] = splitProps(props, ["variant", "size", "class", "classList"])
return (
<KobalteButton
classList={{
...(local.classList || {}),
"inline-flex items-center justify-center rounded-md font-medium cursor-pointer": true,
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2": true,
"disabled:pointer-events-none disabled:opacity-50": true,
"bg-primary text-background hover:bg-secondary focus-visible:ring-primary data-[disabled]:opacity-50":
(local.variant || "primary") === "primary",
"bg-background-panel text-text hover:bg-background-element focus-visible:ring-secondary data-[disabled]:opacity-50":
local.variant === "secondary",
"border border-border bg-transparent text-text hover:bg-background-panel": local.variant === "outline",
"focus-visible:ring-border-active data-[disabled]:border-border-subtle data-[disabled]:text-text-muted":
local.variant === "outline",
"text-text hover:bg-background-panel focus-visible:ring-border-active data-[disabled]:text-text-muted":
local.variant === "ghost",
"h-5 w-5 text-xs": local.size === "xs",
"h-8 w-8 text-sm": local.size === "sm",
"h-10 w-10 text-sm": (local.size || "md") === "md",
"h-12 w-12 text-base": local.size === "lg",
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}

View File

@@ -1,13 +0,0 @@
export { Button, type ButtonProps } from "./button"
export {
Collapsible,
type CollapsibleProps,
type CollapsibleTriggerProps,
type CollapsibleContentProps,
} from "./collapsible"
export { FileIcon, type FileIconProps } from "./file-icon"
export { Icon, type IconProps } from "./icon"
export { IconButton, type IconButtonProps } from "./icon-button"
export { Link, type LinkProps } from "./link"
export { Logo, type LogoProps } from "./logo"
export { Tooltip, type TooltipProps } from "./tooltip"

View File

@@ -1,13 +0,0 @@
import { A } from "@solidjs/router"
import { splitProps } from "solid-js"
import type { ComponentProps } from "solid-js"
export interface LinkProps extends ComponentProps<typeof A> {
variant?: "primary" | "secondary" | "ghost"
size?: "sm" | "md" | "lg"
}
export function Link(props: LinkProps) {
const [, others] = splitProps(props, ["variant", "size", "class"])
return <A {...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,14 +0,0 @@
export function getFilename(path: string) {
const parts = path.split("/")
return parts[parts.length - 1]
}
export function getDirectory(path: string) {
const parts = path.split("/")
return parts.slice(0, parts.length - 1).join("/")
}
export function getFileExtension(path: string) {
const parts = path.split(".")
return parts[parts.length - 1]
}

View File

@@ -1,13 +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"],
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,21 +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.14.0"
"version": "0.15.26"
},
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
"@jsx-email/render": "1.1.1",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
"@opencode/console-mail": "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",
@@ -23,6 +22,10 @@
"vinxi": "^0.5.7",
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
},
"engines": {
"node": ">=22"
}

View File

@@ -1,5 +1,4 @@
<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 d="M252 278H148V174H252V278Z" fill="#6A6565"/>
<path d="M252 122H148V278H252V122ZM304 330H96V70H304V330Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M312 340H88V60H312V340ZM256 116H144V284H256V116Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 13 KiB

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

@@ -1,6 +1,6 @@
import { action, useSubmission } from "@solidjs/router"
import dock from "../asset/lander/dock.png"
import { Resource } from "sst"
import { Resource } from "@opencode-ai/console-resource"
import { Show } from "solid-js"
const emailSignup = action(async (formData: FormData) => {
@@ -39,7 +39,9 @@ export function EmailSignup() {
</button>
</form>
<Show when={submission.result}>
<div style="color: #03B000; margin-top: 24px;">Almost done, check your inbox and confirm your email address</div>
<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>

View File

@@ -10,20 +10,20 @@ export function Faq(props: ParentProps & { question: string }) {
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z" fill="#6D717D" />
<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="none"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 11.5H19V12.5H5Z" fill="#6D717D" />
<path d="M5 11.5H19V12.5H5Z" fill="currentColor" />
</svg>
<div data-slot="faq-question-text">{props.question}</div>
</Collapsible.Trigger>

View File

@@ -1,4 +1,4 @@
import { A, createAsync } from "@solidjs/router"
import { createAsync } from "@solidjs/router"
import { createMemo } from "solid-js"
import { github } from "~/lib/github"
@@ -16,18 +16,18 @@ export function Footer() {
return (
<footer data-component="footer">
<div data-slot="cell">
<A href="https://github.com/sst/opencode" target="_blank">
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</A>
</a>
</div>
<div data-slot="cell">
<A href="/docs">Docs</A>
<a href="/docs">Docs</a>
</div>
<div data-slot="cell">
<A href="/discord">Discord</A>
<a href="/discord">Discord</a>
</div>
<div data-slot="cell">
<A href="https://x.com/opencode">X</A>
<a href="https://x.com/opencode">X</a>
</div>
</footer>
)

View File

@@ -29,13 +29,16 @@ export function Header(props: { zen?: boolean }) {
<nav data-component="nav-desktop">
<ul>
<li>
<A href="https://github.com/sst/opencode" target="_blank">
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</A>
</a>
</li>
<li>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
@@ -100,17 +103,20 @@ export function Header(props: { zen?: boolean }) {
<A href="/">Home</A>
</li>
<li>
<A href="https://github.com/sst/opencode" target="_blank">
<a href="https://github.com/sst/opencode" target="_blank">
GitHub <span>[{starCount()}]</span>
</A>
</a>
</li>
<li>
<A href="/docs">Docs</A>
<a href="/docs">Docs</a>
</li>
<li>
<A href="/enterprise">Enterprise</A>
</li>
<li>
<Switch>
<Match when={props.zen}>
<A href="/auth">Login</A>
<a href="/auth">Login</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>

View File

@@ -2,32 +2,68 @@ 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>
)
}
@@ -37,7 +73,7 @@ export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
<svg {...props} 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="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
@@ -63,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,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,10 +1,8 @@
import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray, sql } 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,32 +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.use((tx) =>
const user = await Database.use((tx) =>
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)))
.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) {
if (user) {
await Database.use((tx) =>
tx
.update(UserTable)
.set({ timeSeen: sql`now()` })
.where(eq(UserTable.id, result.user.id)),
.where(and(eq(UserTable.workspaceID, workspace), eq(UserTable.id, user.id))),
)
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
role: result.user.role,
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

@@ -2,11 +2,15 @@ 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").then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/releases").then((res) => res.json()),
fetch("https://api.github.com/repos/sst/opencode/contributors?per_page=1"),
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(
@@ -23,6 +27,8 @@ export const github = query(async () => {
},
contributors: contributorCount,
}
} catch {}
} catch (e) {
console.error(e)
}
return undefined
}, "github")

View File

@@ -0,0 +1,50 @@
import type { APIEvent } from "@solidjs/start/server"
import { AWS } from "@opencode-ai/console-core/aws.js"
interface EnterpriseFormData {
name: string
role: string
email: string
message: string
}
export async function POST(event: APIEvent) {
try {
const body = (await event.request.json()) as EnterpriseFormData
// Validate required fields
if (!body.name || !body.role || !body.email || !body.message) {
return Response.json({ error: "All fields are required" }, { status: 400 })
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(body.email)) {
return Response.json({ error: "Invalid email format" }, { status: 400 })
}
// Create email content
const emailContent = `
New Enterprise Inquiry
Name: ${body.name}
Role: ${body.role}
Email: ${body.email}
Message:
${body.message}
`.trim()
// Send email using AWS SES
await AWS.sendEmail({
to: "enterprise@opencode.ai",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
} catch (error) {
console.error("Error processing enterprise form:", error)
return Response.json({ error: "Internal server error" }, { status: 500 })
}
}

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")
}

View File

@@ -1,7 +1,7 @@
import type { APIEvent } from "@solidjs/start/server"
import { json } from "@solidjs/router"
import { Database } from "@opencode/console-core/drizzle/index.js"
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
export async function GET(evt: APIEvent) {
return json({

View File

@@ -0,0 +1,544 @@
::selection {
background: var(--color-background-interactive);
color: var(--color-text-strong);
@media (prefers-color-scheme: dark) {
background: var(--color-background-interactive);
color: var(--color-text-inverted);
}
}
[data-page="enterprise"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
--color-background-weak-hover: hsl(0, 8%, 94%);
--color-background-strong: hsl(0, 5%, 12%);
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
--color-text-weaker: hsl(30, 2%, 81%);
--color-text-strong: hsl(0, 5%, 12%);
--color-text-inverted: hsl(0, 20%, 99%);
--color-text-success: hsl(119, 100%, 35%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-icon: hsl(0, 1%, 55%);
--color-success: hsl(142, 76%, 36%);
background: var(--color-background);
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
--color-background-weak: hsl(0, 6%, 10%);
--color-background-weak-hover: hsl(0, 6%, 15%);
--color-background-strong: hsl(0, 15%, 94%);
--color-background-strong-hover: hsl(0, 15%, 97%);
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
--color-text-strong: hsl(0, 15%, 94%);
--color-text-inverted: hsl(0, 9%, 7%);
--color-text-success: hsl(119, 60%, 72%);
--color-border: hsl(0, 3%, 28%);
--color-border-weak: hsl(0, 4%, 23%);
--color-icon: hsl(10, 3%, 43%);
--color-success: hsl(142, 76%, 46%);
}
/* Header and Footer styles - copied from index.css */
[data-component="top"] {
padding: 24px 5rem;
height: 80px;
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-background);
border-bottom: 1px solid var(--color-border-weak);
z-index: 10;
@media (max-width: 60rem) {
padding: 24px 1.5rem;
}
img {
height: 34px;
width: auto;
}
[data-component="nav-desktop"] {
ul {
display: flex;
justify-content: space-between;
gap: 48px;
li {
display: inline-block;
a {
text-decoration: none;
span {
color: var(--color-text-weak);
}
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
}
@media (max-width: 40rem) {
display: none;
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
outline: none;
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
background: var(--color-background-weak);
}
[data-component="nav-mobile"] {
display: none;
@media (max-width: 40rem) {
display: block;
[data-component="nav-mobile-icon"] {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
[data-component="nav-mobile-menu-list"] {
position: fixed;
background: var(--color-background);
top: 80px;
left: 0;
right: 0;
height: 100vh;
ul {
list-style: none;
padding: 20px 0;
li {
a {
text-decoration: none;
padding: 20px;
display: block;
span {
color: var(--color-text-weak);
}
}
a:hover {
background: var(--color-background-weak);
}
}
}
}
}
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
}
[data-component="footer"] {
border-top: 1px solid var(--color-border-weak);
display: flex;
flex-direction: row;
@media (max-width: 65rem) {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
width: 100%;
display: block;
span {
color: var(--color-text-weak);
@media (max-width: 40rem) {
display: none;
}
}
}
a:hover {
background: var(--color-background-weak);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
/* Mobile: third column on its own row */
@media (max-width: 25rem) {
flex-wrap: wrap;
[data-slot="cell"] {
flex: 1 0 100%;
border-left: none;
border-top: 1px solid var(--color-border-weak);
}
[data-slot="cell"]:nth-child(1) {
border-top: none;
}
}
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
border: 1px solid var(--color-border-weak);
border-top: none;
@media (max-width: 65rem) {
border: none;
}
}
[data-component="content"] {
}
[data-component="enterprise-content"] {
padding: 4rem 0;
@media (max-width: 60rem) {
padding: 2rem 0;
}
}
[data-component="enterprise-columns"] {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
padding: 4rem 5rem;
@media (max-width: 80rem) {
gap: 3rem;
}
@media (max-width: 60rem) {
grid-template-columns: 1fr;
gap: 3rem;
padding: 2rem 1.5rem;
}
}
[data-component="enterprise-column-1"] {
h2 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 1rem;
}
h3 {
font-size: 1.25rem;
font-weight: 500;
color: var(--color-text-strong);
margin: 2rem 0 1rem 0;
}
p {
line-height: 1.6;
margin-bottom: 1.5rem;
color: var(--color-text);
}
[data-component="testimonial"] {
margin-top: 4rem;
font-weight: 500;
color: var(--color-text-strong);
[data-component="quotation"] {
svg {
margin-bottom: 1rem;
opacity: 20%;
}
}
[data-component="testimonial-logo"] {
svg {
margin-top: 1.5rem;
}
}
}
}
[data-component="enterprise-column-2"] {
[data-component="enterprise-form"] {
padding: 0;
h2 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 1.5rem;
}
[data-component="form-group"] {
margin-bottom: 1.5rem;
label {
display: block;
font-weight: 500;
color: var(--color-text-weak);
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition: background-color 5000000s ease-in-out 0s;
}
input:-webkit-autofill {
-webkit-text-fill-color: var(--color-text-strong) !important;
}
input:-moz-autofill {
-moz-text-fill-color: var(--color-text-strong) !important;
}
input,
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border-weak);
border-radius: 4px;
background: var(--color-background-weak);
color: var(--color-text-strong);
font-family: inherit;
&::placeholder {
color: var(--color-text-weak);
}
&:focus {
background: var(--color-background-interactive-weaker);
outline: none;
border: none;
color: var(--color-text-strong);
border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
@media (prefers-color-scheme: dark) {
box-shadow: none;
border: 1px solid var(--color-background-interactive)
}
}
}
textarea {
resize: vertical;
min-height: 120px;
}
}
[data-component="submit-button"] {
padding: 0.5rem 1.5rem;
background: var(--color-background-strong);
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
[data-component="success-message"] {
margin-top: 1rem;
padding: 1rem 0;
color: var(--color-text-success);
text-align: left;
}
}
}
[data-component="faq"] {
border-top: 1px solid var(--color-border-weak);
padding: 4rem 5rem;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
[data-slot="section-title"] {
margin-bottom: 24px;
h3 {
font-size: 16px;
font-weight: 500;
color: var(--color-text-strong);
margin-bottom: 12px;
}
p {
margin-bottom: 12px;
color: var(--color-text);
}
}
ul {
padding: 0;
li {
list-style: none;
margin-bottom: 24px;
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
}
}
[data-slot="faq-question"] {
display: flex;
gap: 16px;
margin-bottom: 8px;
color: var(--color-text-strong);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: block;
}
[data-expanded] & {
display: none;
}
}
[data-slot="faq-icon-minus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: none;
}
[data-expanded] & {
display: block;
}
}
[data-slot="faq-question-text"] {
flex-grow: 1;
text-align: left;
}
}
[data-slot="faq-answer"] {
margin-left: 40px;
margin-bottom: 32px;
color: var(--color-text);
}
}
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
padding: 2rem 5rem;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
a {
color: var(--color-text-weak);
text-decoration: none;
}
}
a {
color: var(--color-text-strong);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
&:hover {
text-decoration-thickness: 2px;
}
}
}

View File

@@ -0,0 +1,254 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { createSignal } from "solid-js"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { Faq } from "~/component/faq"
export default function Enterprise() {
const [formData, setFormData] = createSignal({
name: "",
role: "",
email: "",
message: "",
})
const [isSubmitting, setIsSubmitting] = createSignal(false)
const [showSuccess, setShowSuccess] = createSignal(false)
const handleInputChange = (field: string) => (e: Event) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement
setFormData((prev) => ({ ...prev, [field]: target.value }))
}
const handleSubmit = async (e: Event) => {
e.preventDefault()
setIsSubmitting(true)
try {
const response = await fetch("/api/enterprise", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData()),
})
if (response.ok) {
setShowSuccess(true)
setFormData({
name: "",
role: "",
email: "",
message: "",
})
setTimeout(() => setShowSuccess(false), 5000)
}
} catch (error) {
console.error("Failed to submit form:", error)
} finally {
setIsSubmitting(false)
}
}
return (
<main data-page="enterprise">
<Title>OpenCode | Enterprise solutions for your organisation</Title>
<Meta name="description" content="Contact OpenCode for enterprise solutions" />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="enterprise-content">
<div data-component="enterprise-columns">
<div data-component="enterprise-column-1">
<h2>Your code is yours</h2>
<p>
OpenCode operates securely inside your organization with no data or context stored and no licensing restrictions or ownership claims. Start a trial with your team today, then scale confidently with enterprise-grade features including SSO, private registries, and self-hosting.
</p>
<p>
Let us know and how we can help.
</p>
<div data-component="testimonial">
<div data-component="quotation">
<svg width="20" height="17" viewBox="0 0 20 17" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor" />
</svg>
</div>
Thanks to OpenCode, we found a way to create software to track all our assets even the imaginary ones.
<div data-component="testimonial-logo">
<svg width="80" height="79" viewBox="0 0 80 79" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 39.3087L10.0579 29.251L15.6862 34.7868L13.7488 36.7248L10.3345 33.2186L8.48897 35.0639L11.8111 38.4781L9.96557 40.4156L6.55181 37.0018L4.06028 39.4928L7.56674 42.9991L5.62884 44.845L0 39.3087Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M17.7182 36.8164L20.2094 39.4003L16.6108 46.9666L22.2393 41.3374L24.3615 43.46L14.2118 53.5179L11.9047 51.1187L15.4112 43.3677L9.78254 49.0888L7.66016 46.9666L17.7182 36.8164Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M42.8139 61.915L45.3055 64.4064L41.6145 71.9731L47.243 66.3441L49.3652 68.4663L39.3077 78.5244L36.9088 76.1252L40.5072 68.374L34.7866 74.0953L32.6641 71.9731L42.8139 61.915Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M16.4258 55.7324L26.4833 45.582L28.6061 47.7042C31.0049 50.1034 32.3892 51.9497 30.1746 54.1642C28.7902 55.548 27.6831 56.0094 26.1145 54.9016L26.0222 54.994C27.2218 56.1941 26.9448 57.1162 25.4688 58.5931L23.9 60.1615C23.4383 60.6232 22.8847 61.2693 22.7927 62.0067L20.6705 59.8845C20.7625 59.146 21.3161 58.5008 21.778 58.1316L23.5307 56.3788C24.269 55.6403 23.715 54.2555 23.254 53.8872L22.8847 53.4256L18.548 57.7623L16.4258 55.7324ZM24.3611 51.9495C25.4689 53.0563 26.4833 53.3332 27.4984 52.3178C28.5134 51.3957 28.2367 50.3802 27.1295 49.1812L24.3611 51.9495Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M33.4952 66.9899C31.096 69.3891 28.8815 68.4659 27.4047 66.9899C26.021 65.6062 25.0978 63.3907 27.4972 60.9003L31.8336 56.6548C34.2333 54.2556 36.4478 55.0864 37.9241 56.5635C39.308 58.0396 40.2311 60.2541 37.8315 62.6531L33.4952 66.9899ZM29.0659 63.5752C28.6048 64.0369 28.6048 64.7753 29.1583 65.3292C29.6196 65.8821 30.4502 65.7897 30.8194 65.4215L36.2633 59.9769C36.7246 59.6076 36.7246 58.7779 36.171 58.3164C35.7097 57.7626 34.8791 57.7626 34.5101 58.2241L29.0659 63.5752Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M78.5267 39.308L68.2845 29.0654L47.5231 49.735L49.6453 51.8572L68.2845 33.2179L74.3746 39.308L47.2461 66.3435L49.3683 68.4657L78.5267 39.308Z"
fill="#0083C6" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M49.6443 51.8577L43.3695 45.4902L64.0386 24.8215L53.7969 14.4873L33.0352 35.2482L35.1574 37.3705L53.7969 18.7315L59.7947 24.8215L39.1251 45.4902L47.5221 53.9799L49.6443 51.8577Z"
fill="#2D9C5C" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M35.1564 37.3706L28.7896 31.0038L49.5515 10.3347L39.3088 0L10.0586 29.2507L12.1804 31.2804L39.3088 4.24476L45.3066 10.3347L24.6377 31.0038L33.0342 39.4008L35.1564 37.3706Z"
fill="#E92A35" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M77.2332 52.4105C76.0336 52.4105 75.111 51.4884 75.111 50.196C75.111 48.9046 76.0336 47.9814 77.2332 47.9814C78.3405 47.9814 79.263 48.9046 79.263 50.196C79.263 51.4884 78.3405 52.4105 77.2332 52.4105ZM77.2332 52.9643C78.7098 52.9643 80.0015 51.6729 80.0015 50.196C80.0015 48.6276 78.7096 47.4287 77.2332 47.4287C75.6644 47.4287 74.4648 48.6278 74.4648 50.196C74.4647 51.6731 75.6643 52.9643 77.2332 52.9643ZM76.1259 51.7653H76.6797V50.3804H77.0485L77.8788 51.7653H78.4332L77.6023 50.3804C78.1558 50.2881 78.4332 50.0122 78.4332 49.5507C78.4332 48.9046 78.0633 48.6276 77.3253 48.6276H76.1257V51.7653H76.1259ZM76.6797 49.0892H77.2332C77.5102 49.0892 77.8788 49.0892 77.8788 49.4586C77.8788 49.9202 77.6023 49.9202 77.2332 49.9202H76.6797V49.0892Z"
fill="#0083C6" />
</svg>
</div>
</div>
</div>
<div data-component="enterprise-column-2">
<div data-component="enterprise-form">
<form onSubmit={handleSubmit}>
<div data-component="form-group">
<label for="name">Full name</label>
<input
id="name"
type="text"
required
value={formData().name}
onInput={handleInputChange('name')}
placeholder="Jeff Bezos"
/>
</div>
<div data-component="form-group">
<label for="role">Role</label>
<input
id="role"
type="text"
required
value={formData().role}
onInput={handleInputChange('role')}
placeholder="Executive Chairman"
/>
</div>
<div data-component="form-group">
<label for="email">Company email</label>
<input
id="email"
type="email"
required
value={formData().email}
onInput={handleInputChange('email')}
placeholder="jeff@amazon.com"
/>
</div>
<div data-component="form-group">
<label for="message">What problem are you trying to
solve?</label>
<textarea
id="message"
required
rows={5}
value={formData().message}
onInput={handleInputChange('message')}
placeholder="We need help with..."
/>
</div>
<button type="submit" disabled={isSubmitting()}
data-component="submit-button">
{isSubmitting() ? 'Sending...' : 'Send'}
</button>
</form>
{showSuccess() && (
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
</div>
</div>
</div>
</section>
<section data-component="faq">
<div data-slot="section-title">
<h3>FAQ</h3>
</div>
<ul>
<li>
<Faq question="Does Opencode store our code or context data?">
No. OpenCode never stores your code or context data. All
processing happens locally or directly with your AI provider.
</Faq>
</li>
<li>
<Faq question="Who owns the code generated with OpenCode?">
You do. All code produced is yours, with no licensing
restrictions or ownership claims.
</Faq>
</li>
<li>
<Faq
question="How can we trial OpenCode inside our organization?">
Simply install and run an internal trial with your team. Since
OpenCode doesnt store any data, your developers can get
started right away.
</Faq>
</li>
<li>
<Faq
question="What happens if someone uses the `/share` feature?">
By default, sharing is disabled. If enabled, conversations are
sent to our share service and cached through our CDN. For
enterprise use, we recommend disabling or self-hosting this
feature.
</Faq>
</li>
<li>
<Faq question="Can OpenCode integrate with our companys SSO?">
Yes. Enterprise deployments can include SSO integration so all
sessions and shared conversations are protected by your
authentication system.
</Faq>
</li>
<li>
<Faq question="Can OpenCode be self-hosted?">
Absolutely. You can fully self-host OpenCode, including the
share feature, ensuring that data and pages are accessible
only after authentication.
</Faq>
</li>
<li>
<Faq
question="How do we get started with enterprise deployment?">
Contact us to discuss pricing, implementation, and enterprise
options like SSO, private registries, and self-hosting.
</Faq>
</li>
</ul>
</section>
</div>
<Footer />
</div>
<Legal />
</main>
)
}

View File

@@ -29,6 +29,10 @@
--color-icon: hsl(0, 1%, 55%);
}
body {
background: var(--color-background);
}
[data-page="opencode"] {
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
@@ -52,12 +56,18 @@
}
}
@supports (background: -webkit-named-image(i)) {
[data-page="opencode"] {
border-top: 1px solid var(--color-border-weak);
}
}
[data-page="opencode"] {
background: var(--color-background);
--padding: 5rem;
--vertical-padding: 4rem;
--heading-font-size: 1.375rem;
border-top: 1px solid var(--color-border-weak);
@media (max-width: 60rem) {
--padding: 1.5rem;
@@ -80,6 +90,10 @@
p {
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
}
@media (max-width: 60rem) {
@@ -162,6 +176,15 @@
transition: background-color 5000000s ease-in-out 0s;
}
input:-webkit-autofill {
-webkit-text-fill-color: var(--color-text-strong) !important;
}
input:-moz-autofill {
-moz-text-fill-color: var(--color-text-strong) !important;
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
@@ -220,6 +243,12 @@
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
@@ -227,6 +256,7 @@
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
@@ -549,9 +579,6 @@
margin-bottom: 24px;
max-width: 100%;
@media (max-width: 30Rem) {
margin-bottom: 0;
}
div {
display: flex;
@@ -561,6 +588,10 @@
span {
color: var(--color-icon);
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
}
h3 {
@@ -595,6 +626,10 @@
gap: 12px;
line-height: 200%;
@media (max-width: 60rem) {
line-height: 180%;
}
span {
color: var(--color-icon);
}
@@ -762,15 +797,29 @@
border-radius: 6px;
border: 1px solid var(--color-border-weak);
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
/* Use color, not -moz-text-fill-color, for normal text */
color: var(--color-text-strong);
@media (max-width: 30rem) {
padding-bottom: 80px;
}
&:not(:focus) {
color: var(--color-text-strong);
}
&::placeholder {
color: var(--color-text-weak);
opacity: 1;
}
/* Optional legacy */
&::-moz-placeholder {
color: var(--color-text-weak);
opacity: 1;
}
}
input:focus {
@@ -778,12 +827,8 @@
outline: none;
border: none;
color: var(--color-text-strong);
border: 1px solid var(--color-background-strong); /* Tailwind blue-600 as example */
/* Tailwind-style ring */
border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
/* mimics "ring-2 ring-blue-600/50" */
@media (prefers-color-scheme: dark) {
box-shadow: none;
@@ -850,6 +895,9 @@
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: block;
}
@@ -859,6 +907,9 @@
}
[data-slot="faq-icon-minus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: none;
}
@@ -1035,6 +1086,10 @@
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
/* Mobile: third column on its own row */

View File

@@ -4,10 +4,7 @@ import { HttpHeader } from "@solidjs/start"
import video from "../asset/lander/opencode-min.mp4"
import videoPoster from "../asset/lander/opencode-poster.png"
import { IconCopy, IconCheck } from "../component/icon"
import { A, createAsync, query } from "@solidjs/router"
import { getActor } from "~/context/auth"
import { withActor } from "~/context/auth.withActor"
import { Account } from "@opencode/console-core/account.js"
import { A, createAsync } from "@solidjs/router"
import { EmailSignup } from "~/component/email-signup"
import { Tabs } from "@kobalte/core/tabs"
import { Faq } from "~/component/faq"
@@ -26,18 +23,8 @@ function CopyStatus() {
)
}
const defaultWorkspace = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
return workspaces[0].id
}
}, "defaultWorkspace")
export default function Home() {
const githubData = createAsync(() => github())
const workspace = createAsync(() => defaultWorkspace())
const release = createMemo(() => githubData()?.release)
const handleCopyClick = (event: Event) => {
@@ -647,7 +634,7 @@ export default function Home() {
<p>
OpenCode does not store any of your code or context data, so that it can operate in privacy sensitive
environments. Learn more about <a href="/docs/enterprise/ ">privacy and enterprise</a>.
environments. Learn more about <a href="/docs/enterprise/ ">privacy</a>.
</p>
</div>
</div>
@@ -799,6 +786,8 @@ export default function Home() {
</div>
</section>
<EmailSignup />
<Footer />
</div>
</div>

View File

@@ -1,11 +1,11 @@
import { Billing } from "@opencode/console-core/billing.js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode/console-core/schema/billing.sql.js"
import { Identifier } from "@opencode/console-core/identifier.js"
import { centsToMicroCents } from "@opencode/console-core/util/price.js"
import { Actor } from "@opencode/console-core/actor.js"
import { Resource } from "@opencode/console-resource"
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Resource } from "@opencode-ai/console-resource"
export async function POST(input: APIEvent) {
const body = await Billing.stripe().webhooks.constructEventAsync(
@@ -32,7 +32,8 @@ export async function POST(input: APIEvent) {
.update(BillingTable)
.set({
paymentMethodID,
paymentMethodLast4: paymentMethod.card!.last4,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
})
.where(eq(BillingTable.customerID, customerID))
})
@@ -77,7 +78,8 @@ export async function POST(input: APIEvent) {
balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
paymentMethodType: paymentMethod.type,
reload: true,
reloadError: null,
timeReloadError: null,

View File

@@ -5,10 +5,6 @@ import logoLight from "../asset/logo-ornate-light.svg"
import logoDark from "../asset/logo-ornate-dark.svg"
import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
import { IconCopy, IconCheck } from "../component/icon"
import { createAsync, query } from "@solidjs/router"
import { getActor } from "~/context/auth"
import { withActor } from "~/context/auth.withActor"
import { Account } from "@opencode/console-core/account.js"
function CopyStatus() {
return (
@@ -19,17 +15,7 @@ function CopyStatus() {
)
}
const defaultWorkspace = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
return workspaces[0].id
}
}, "defaultWorkspace")
export default function Home() {
const workspace = createAsync(() => defaultWorkspace())
onMount(() => {
const commands = document.querySelectorAll("[data-copy]")
for (const button of commands) {

View File

@@ -0,0 +1,17 @@
[data-component="user-menu"] {
[data-component="dropdown"] {
[data-slot="trigger"] span {
color: var(--color-text-muted);
}
[data-slot="dropdown"] {
form {
width: 100%;
}
}
[data-slot="item"] {
color: var(--color-danger);
}
}
}

View File

@@ -0,0 +1,35 @@
import { action, redirect } from "@solidjs/router"
import { getRequestEvent } from "solid-js/web"
import { useAuthSession } from "~/context/auth.session"
import { Dropdown } from "~/component/dropdown"
import "./user-menu.css"
const logout = action(async () => {
"use server"
const auth = await useAuthSession()
const event = getRequestEvent()
const current = auth.data.current
if (current)
await auth.update((val) => {
delete val.account?.[current]
const first = Object.keys(val.account ?? {})[0]
val.current = first
event!.locals.actor = undefined
return val
})
throw redirect("/zen")
})
export function UserMenu(props: { email: string | null | undefined }) {
return (
<div data-component="user-menu">
<Dropdown trigger={props.email ?? ""} align="right">
<form action={logout} method="post">
<button type="submit" formaction={logout} data-slot="item">
Logout
</button>
</form>
</Dropdown>
</div>
)
}

View File

@@ -0,0 +1,74 @@
[data-component="workspace-picker"] {
[data-component="dropdown"] {
[data-slot="trigger"] {
/* Override blue accent colors with neutral colors for dropdown trigger */
--color-accent: var(--color-border);
--color-accent-hover: var(--color-border);
--color-accent-active: var(--color-border);
--color-primary: var(--color-border);
--color-primary-hover: var(--color-border);
--color-primary-active: var(--color-border);
--color-primary-alpha-20: transparent;
}
[data-slot="dropdown"] {
max-height: 240px;
overflow-y: auto;
min-width: 200px;
}
}
[data-slot="create-item"] {
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-slot="create-form"] {
width: 100%;
}
[data-slot="create-input-group"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
[data-slot="button-group"] {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
}
[data-slot="create-input"] {
flex: 1;
padding: var(--space-2-5) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
&:focus {
outline: none;
border-color: var(--color-border);
box-shadow: none;
}
&::placeholder {
color: var(--color-text-muted);
}
}
}

View File

@@ -0,0 +1,122 @@
import { query, useParams, action, createAsync, redirect, useSubmission } from "@solidjs/router"
import { For, Show, createEffect } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { Dropdown, DropdownItem } from "~/component/dropdown"
import { Modal } from "~/component/modal"
import "./workspace-picker.css"
const getWorkspaces = query(async () => {
"use server"
return withActor(async () => {
return Database.transaction((tx) =>
tx
.select({
id: WorkspaceTable.id,
name: WorkspaceTable.name,
slug: WorkspaceTable.slug,
})
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(
and(
eq(UserTable.accountID, Actor.account()),
isNull(WorkspaceTable.timeDeleted),
isNull(UserTable.timeDeleted),
),
),
)
})
}, "workspaces")
const createWorkspace = action(async (form: FormData) => {
"use server"
const name = form.get("workspaceName") as string
if (name?.trim()) {
return withActor(async () => {
const workspaceID = await Workspace.create({ name: name.trim() })
return redirect(`/workspace/${workspaceID}`)
})
}
}, "createWorkspace")
export function WorkspacePicker() {
const params = useParams()
const workspaces = createAsync(() => getWorkspaces())
const submission = useSubmission(createWorkspace)
const [store, setStore] = createStore({
showForm: false,
})
let inputRef: HTMLInputElement | undefined
const currentWorkspace = () => {
const ws = workspaces()?.find((w) => w.id === params.id)
return ws ? ws.name : "Select workspace"
}
const handleWorkspaceNew = () => {
setStore("showForm", true)
}
createEffect(() => {
if (store.showForm && inputRef) {
setTimeout(() => inputRef?.focus(), 0)
}
})
const handleSelectWorkspace = (workspaceID: string) => {
if (workspaceID === params.id) return
window.location.href = `/workspace/${workspaceID}`
}
// Reset signals when workspace ID changes
createEffect(() => {
params.id
setStore("showForm", false)
})
return (
<div data-component="workspace-picker">
<Dropdown trigger={currentWorkspace()} align="left">
<For each={workspaces()}>
{(workspace) => (
<DropdownItem selected={workspace.id === params.id} onClick={() => handleSelectWorkspace(workspace.id)}>
{workspace.name || workspace.slug}
</DropdownItem>
)}
</For>
<button data-slot="create-item" type="button" onClick={() => handleWorkspaceNew()}>
+ Create New Workspace
</button>
</Dropdown>
<Modal open={store.showForm} onClose={() => setStore("showForm", false)} title="Create New Workspace">
<form data-slot="create-form" action={createWorkspace} method="post">
<div data-slot="create-input-group">
<input
ref={inputRef}
data-slot="create-input"
type="text"
name="workspaceName"
placeholder="Enter workspace name"
required
/>
<div data-slot="button-group">
<button type="button" data-color="ghost" onClick={() => setStore("showForm", false)}>
Cancel
</button>
<button type="submit" data-color="primary" disabled={submission.pending}>
{submission.pending ? "Creating..." : "Create"}
</button>
</div>
</div>
</form>
</Modal>
</div>
)
}

View File

@@ -11,7 +11,6 @@
font-size: var(--font-size-sm);
font-family: var(--font-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
@@ -55,9 +54,6 @@
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
/* Workspace Header */
@@ -80,16 +76,14 @@
[data-slot="header-brand"] {
flex: 0 0 auto;
padding-top: 4px;
svg {
width: 138px;
}
display: flex;
align-items: center;
gap: var(--space-4);
[data-component="site-title"] {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--color-text);
text-decoration: none;
letter-spacing: -0.02em;
}
}
@@ -109,19 +103,5 @@
display: none;
}
}
a,
button {
appearance: none;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
text-transform: uppercase;
}
}
}
}

View File

@@ -1,64 +1,37 @@
import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
import "./workspace.css"
import { useAuthSession } from "~/context/auth.session"
import { IconLogo } from "../component/icon"
import { IconWorkspaceLogo } from "../component/icon"
import { WorkspacePicker } from "./workspace-picker"
import { UserMenu } from "./user-menu"
import { withActor } from "~/context/auth.withActor"
import {
query,
action,
redirect,
createAsync,
RouteSectionProps,
Navigate,
useNavigate,
useParams,
A,
} from "@solidjs/router"
import { User } from "@opencode/console-core/user.js"
import { Actor } from "@opencode/console-core/actor.js"
import { getRequestEvent } from "solid-js/web"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Link } from "@solidjs/meta"
const getUserInfo = query(async (workspaceID: string) => {
const getUserEmail = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
return await User.fromID(actor.properties.userID)
const email = await User.getAuthEmail(actor.properties.userID)
return email
}, workspaceID)
}, "userInfo")
const logout = action(async () => {
"use server"
const auth = await useAuthSession()
const event = getRequestEvent()
const current = auth.data.current
if (current)
await auth.update((val) => {
delete val.account?.[current]
const first = Object.keys(val.account ?? {})[0]
val.current = first
event!.locals.actor = undefined
return val
})
throw redirect("/")
})
}, "userEmail")
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userInfo = createAsync(() => getUserInfo(params.id))
const userEmail = createAsync(() => getUserEmail(params.id))
return (
<main data-page="workspace">
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<header data-component="workspace-header">
<div data-slot="header-brand">
<A href="/" data-component="site-title">
<IconLogo />
<IconWorkspaceLogo />
</A>
<WorkspacePicker />
</div>
<div data-slot="header-actions">
<span data-slot="user">{userInfo()?.email}</span>
<form action={logout} method="post">
<button type="submit" formaction={logout}>
Logout
</button>
</form>
<UserMenu email={userEmail()} />
</div>
</header>
<div>{props.children}</div>

View File

@@ -1,7 +1,144 @@
[data-page="workspace"] {
line-height: 1;
}
/* Workspace Layout */
[data-component="workspace-container"] {
display: flex;
height: calc(100vh - 73px);
}
[data-component="workspace-nav"] {
width: 240px;
flex-shrink: 0;
padding: var(--space-6) var(--space-4);
display: flex;
justify-content: flex-end;
}
/* Desktop Navigation */
[data-component="nav-desktop"] {
display: block;
@media (max-width: 48rem) {
display: none;
}
[data-component="workspace-nav-items"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
[data-nav-button] {
padding: var(--space-3) var(--space-4);
border-radius: var(--border-radius-sm);
color: var(--color-text-muted);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: all 0.15s ease;
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-text);
font-weight: 700;
position: relative;
&::before {
content: "";
position: absolute;
left: calc(-1 * var(--space-0-5));
top: 0;
bottom: 0;
width: 2px;
background-color: var(--color-text);
border-radius: 0 2px 2px 0;
}
}
}
}
}
/* Mobile Navigation */
[data-component="nav-mobile"] {
display: none;
@media (max-width: 48rem) {
display: flex;
align-items: stretch;
justify-content: flex-start;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
[data-component="workspace-nav-items"] {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--space-1);
min-width: max-content;
height: 100%;
[data-nav-button] {
padding: var(--space-2) var(--space-3);
padding-bottom: calc(var(--space-2) + 4px);
border-radius: var(--border-radius-sm);
color: var(--color-text-muted);
text-decoration: none;
font-size: var(--font-size-sm);
font-weight: 500;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
position: relative;
&:hover {
color: var(--color-text);
}
&.active {
color: var(--color-text);
font-weight: 700;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: var(--color-text);
border-radius: 2px 2px 0 0;
}
}
}
}
}
[data-component="workspace-content"] {
flex: 1;
padding: var(--space-6) var(--space-8);
overflow-y: auto;
@media (max-width: 48rem) {
padding: var(--space-6) var(--space-4);
}
}
[data-page="workspace-[id]"] {
max-width: 64rem;
padding: var(--space-10) var(--space-4);
margin: 0 auto;
padding: var(--space-2) var(--space-4);
margin: 0;
width: 100%;
display: flex;
flex-direction: column;
@@ -32,7 +169,6 @@
gap: var(--space-6);
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
@@ -44,8 +180,7 @@
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
color: var(--color-text);
@media (max-width: 30rem) {
font-size: var(--font-size-md);
@@ -66,7 +201,15 @@
}
}
}
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-top: var(--space-8);
}
}
section:not(:last-child) {
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-16);
@@ -78,7 +221,7 @@
}
/* Title section */
[data-component="title-section"] {
[data-component="header-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -105,11 +248,61 @@
p {
line-height: 1.5;
font-size: var(--font-size-md);
color: var(--color-text-muted);
color: var(--color-text);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
@media (max-width: 48rem) {
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
}
a {
color: var(--color-text-muted);
}
[data-slot="billing-info"] {
flex-shrink: 0;
margin-left: auto;
}
[data-slot="balance"] {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
b {
font-weight: 600;
color: var(--color-text);
}
}
}
}
}
@media (max-width: 48rem) {
[data-component="workspace-container"] {
flex-direction: column;
}
[data-component="workspace-nav"] {
width: 100%;
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--color-border);
padding: var(--space-4);
justify-content: flex-start;
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
min-height: fit-content;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
}

View File

@@ -1,75 +1,62 @@
import "./[id].css"
import { MonthlyLimitSection } from "./monthly-limit-section"
import { NewUserSection } from "./new-user-section"
import { BillingSection } from "./billing-section"
import { PaymentSection } from "./payment-section"
import { UsageSection } from "./usage-section"
import { KeySection } from "./key-section"
import { MemberSection } from "./member-section"
import { Show } from "solid-js"
import { createAsync, query, useParams } from "@solidjs/router"
import { Actor } from "@opencode/console-core/actor.js"
import { withActor } from "~/context/auth.withActor"
import { and, Database, eq } from "@opencode/console-core/drizzle/index.js"
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
import { createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
import { querySessionInfo } from "./common"
import "./[id].css"
const getUser = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.use()
const isAdmin = await (async () => {
if (actor.type !== "user") return false
const role = await Database.use((tx) =>
tx
.select({ role: UserTable.role })
.from(UserTable)
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))),
).then((x) => x[0]?.role)
return role === "admin"
})()
return { isAdmin }
}, workspaceID)
}, "user.get")
export default function () {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const data = createAsync(() => getUser(params.id))
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
<h1>Zen</h1>
<p>
Curated list of models provided by opencode.{" "}
<a target="_blank" href="/docs/zen">
Learn more
</a>
.
</p>
</section>
const userInfo = createAsync(() => querySessionInfo(params.id))
<div data-slot="sections">
<NewUserSection />
<KeySection />
<Show when={data()?.isAdmin}>
<Show when={isBeta(params.id)}>
<MemberSection />
</Show>
<BillingSection />
<MonthlyLimitSection />
</Show>
<UsageSection />
<Show when={data()?.isAdmin}>
<PaymentSection />
</Show>
return (
<main data-page="workspace">
<div data-component="workspace-container">
<nav data-component="workspace-nav">
<nav data-component="nav-desktop">
<div data-component="workspace-nav-items">
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
Zen
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
API Keys
</A>
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
Members
</A>
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
Billing
</A>
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
Settings
</A>
</Show>
</div>
</nav>
<nav data-component="nav-mobile">
<div data-component="workspace-nav-items">
<A href={`/workspace/${params.id}`} end activeClass="active" data-nav-button>
Zen
</A>
<A href={`/workspace/${params.id}/keys`} activeClass="active" data-nav-button>
API Keys
</A>
<A href={`/workspace/${params.id}/members`} activeClass="active" data-nav-button>
Members
</A>
<Show when={userInfo()?.isAdmin}>
<A href={`/workspace/${params.id}/billing`} activeClass="active" data-nav-button>
Billing
</A>
<A href={`/workspace/${params.id}/settings`} activeClass="active" data-nav-button>
Settings
</A>
</Show>
</div>
</nav>
</nav>
<div data-component="workspace-content">{props.children}</div>
</div>
</div>
</main>
)
}
export function isBeta(workspaceID: string) {
return [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
"wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
"wrk_01K6G7HBZ7C046A4XK01CVD0NS", // frank
].includes(workspaceID)
}

View File

@@ -1,10 +1,4 @@
.root {
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
[data-slot="reload-error"] {
display: flex;
align-items: center;
@@ -29,6 +23,7 @@
flex-shrink: 0;
}
}
[data-slot="payment"] {
display: flex;
flex-direction: column;
@@ -75,6 +70,12 @@
font-weight: 500;
color: var(--color-text);
}
[data-slot="type"] {
font-size: var(--font-size-sm);
font-weight: 400;
color: var(--color-text-muted);
}
}
}
@@ -86,7 +87,7 @@
@media (max-width: 30rem) {
flex-direction: column;
> button {
>button {
width: 100%;
}
}
@@ -96,19 +97,21 @@
}
/* Make Enable Billing button full width when it's the only button */
> 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,16 +1,12 @@
import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
import { createMemo, Show } from "solid-js"
import { Billing } from "@opencode/console-core/billing.js"
import { createMemo, Match, Show, Switch } from "solid-js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { withActor } from "~/context/auth.withActor"
import { IconCreditCard } from "~/component/icon"
import { IconCreditCard, IconStripe } from "~/component/icon"
import styles from "./billing-section.module.css"
import { Database, eq } from "@opencode/console-core/drizzle/index.js"
import { BillingTable } from "@opencode/console-core/schema/billing.sql.js"
const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
"use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
}, "checkoutUrl")
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { createCheckoutUrl } from "../../common"
const reload = action(async (form: FormData) => {
"use server"
@@ -65,6 +61,7 @@ export function BillingSection() {
// Scenario 1: User has not added billing details and has no balance
// const balanceInfo = () => ({
// balance: 0,
// paymentMethodType: null as string | null,
// paymentMethodLast4: null as string | null,
// reload: false,
// reloadError: null as string | null,
@@ -74,6 +71,7 @@ export function BillingSection() {
// Scenario 2: User has not added billing details but has a balance
// const balanceInfo = () => ({
// balance: 1500000000, // $15.00
// paymentMethodType: null as string | null,
// paymentMethodLast4: null as string | null,
// reload: false,
// reloadError: null as string | null,
@@ -83,6 +81,7 @@ export function BillingSection() {
// Scenario 3: User has added billing details (reload enabled)
// const balanceInfo = () => ({
// balance: 750000000, // $7.50
// paymentMethodType: "card",
// paymentMethodLast4: "4242",
// reload: true,
// reloadError: null as string | null,
@@ -92,12 +91,23 @@ export function BillingSection() {
// Scenario 4: User has billing details but reload failed
// const balanceInfo = () => ({
// balance: 250000000, // $2.50
// paymentMethodType: "card",
// paymentMethodLast4: "4242",
// reload: true,
// reloadError: "Your card was declined." as string,
// timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago
// })
// Scenario 5: User has Link payment method
// const balanceInfo = () => ({
// balance: 500000000, // $5.00
// paymentMethodType: "link",
// paymentMethodLast4: null as string | null,
// reload: true,
// reloadError: null as string | null,
// timeReloadError: null as Date | null
// })
const balanceAmount = createMemo(() => {
return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
})
@@ -140,13 +150,25 @@ export function BillingSection() {
<div data-slot="payment">
<div data-slot="credit-card">
<div data-slot="card-icon">
<IconCreditCard style={{ width: "32px", height: "32px" }} />
<Switch fallback={<IconCreditCard style={{ width: "32px", height: "32px" }} />}>
<Match when={balanceInfo()?.paymentMethodType === "link"}>
<IconStripe style={{ width: "32px", height: "32px" }} />
</Match>
</Switch>
</div>
<div data-slot="card-details">
<Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
<span data-slot="secret"></span>
<span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
</Show>
<Switch
fallback={
<Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
<span data-slot="secret"></span>
<span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
</Show>
}
>
<Match when={balanceInfo()?.paymentMethodType === "link"}>
<span data-slot="type">Linked to Stripe</span>
</Match>
</Switch>
</div>
</div>
<div data-slot="button-row">

View File

@@ -0,0 +1,23 @@
import { MonthlyLimitSection } from "./monthly-limit-section"
import { BillingSection } from "./billing-section"
import { PaymentSection } from "./payment-section"
import { Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { querySessionInfo } from "../../common"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
return (
<div data-page="workspace-[id]">
<div data-slot="sections">
<Show when={userInfo()?.isAdmin}>
<BillingSection />
<MonthlyLimitSection />
<PaymentSection />
</Show>
</div>
</div>
)
}

View File

@@ -1,10 +1,4 @@
.root {
[data-slot="section-content"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
[data-slot="balance"] {
display: flex;
flex-direction: column;
@@ -99,4 +93,4 @@
margin: 0;
line-height: 1.4;
}
}
}

View File

@@ -2,7 +2,7 @@ import { json, query, action, useParams, createAsync, useSubmission } from "@sol
import { createEffect, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode/console-core/billing.js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import styles from "./monthly-limit-section.module.css"
const getBillingInfo = query(async (workspaceID: string) => {

View File

@@ -1,8 +1,8 @@
import { Billing } from "@opencode/console-core/billing.js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
import { For } from "solid-js"
import { For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { formatDateUTC, formatDateForTable } from "./common"
import { formatDateUTC, formatDateForTable } from "../../common"
import styles from "./payment-section.module.css"
const getPaymentsInfo = query(async (workspaceID: string) => {
@@ -19,7 +19,6 @@ const downloadReceipt = action(async (workspaceID: string, paymentID: string) =>
export function PaymentSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const payments = createAsync(() => getPaymentsInfo(params.id))
const downloadReceiptAction = useAction(downloadReceipt)
@@ -58,8 +57,7 @@ export function PaymentSection() {
// ]
return (
payments() &&
payments()!.length > 0 && (
<Show when={payments() && payments()!.length > 0}>
<section class={styles.root}>
<div data-slot="section-title">
<h2>Payments History</h2>
@@ -98,7 +96,7 @@ export function PaymentSection() {
}}
data-slot="receipt-button"
>
view
View
</button>
</td>
</tr>
@@ -109,6 +107,6 @@ export function PaymentSection() {
</table>
</div>
</section>
)
</Show>
)
}

View File

@@ -0,0 +1,73 @@
import { NewUserSection } from "./new-user-section"
import { UsageSection } from "./usage-section"
import { ModelSection } from "./model-section"
import { ProviderSection } from "./provider-section"
import { IconLogo } from "~/component/icon"
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl } from "../common"
import { Show, createMemo } from "solid-js"
export default function () {
const params = useParams()
const userInfo = createAsync(() => querySessionInfo(params.id))
const billingInfo = createAsync(() => queryBillingInfo(params.id))
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const balanceAmount = createMemo(() => {
return ((billingInfo()?.balance ?? 0) / 100000000).toFixed(2)
})
return (
<div data-page="workspace-[id]">
<section data-component="header-section">
<IconLogo />
<p>
<span>
Reliable optimized models for coding agents.{" "}
<a target="_blank" href="/docs/zen">
Learn more
</a>
.
</span>
<Show when={userInfo()?.isAdmin}>
<span data-slot="billing-info">
<Show
when={billingInfo()?.reload}
fallback={
<button
data-color="primary"
data-size="sm"
disabled={createCheckoutUrlSubmission.pending}
onClick={async () => {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
}}
>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Enable billing"}
</button>
}
>
<span data-slot="balance">
Current balance <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
</span>
</Show>
</span>
</Show>
</p>
</section>
<div data-slot="sections">
<NewUserSection />
<ModelSection />
<Show when={userInfo()?.isAdmin}>
<ProviderSection />
</Show>
<UsageSection />
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { KeySection } from "./key-section"
export default function () {
return (
<div data-page="workspace-[id]">
<div data-slot="sections">
<KeySection />
</div>
</div>
)
}

View File

@@ -1,4 +1,11 @@
.root {
[data-slot="title-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
}
[data-component="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
@@ -107,6 +114,7 @@
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
margin-left: calc(-1 * var(--space-3));
font-size: var(--font-size-sm);
font-weight: 400;
border: none;
@@ -140,16 +148,30 @@
&[data-slot="key-actions"] {
font-family: var(--font-sans);
button {
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
}
}
}
tbody tr {
&:hover {
[data-slot="key-actions"] button {
opacity: 1;
pointer-events: auto;
}
}
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -157,16 +179,22 @@
}
th {
&:nth-child(3) /* Date */ {
&:nth-child(3)
/* Date */
{
display: none;
}
}
td {
&:nth-child(3) /* Date */ {
&:nth-child(3)
/* Date */
{
display: none;
}
}
}
}
}
}

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