Compare commits

...

242 Commits

Author SHA1 Message Date
opencode
e789abec79 release: v0.4.45 2025-08-13 22:32:26 +00:00
Aiden Cline
118617473e fix: bash should hide stdout from zshrc (#1909) 2025-08-13 17:04:32 -04:00
adamdotdevin
a4beb60e19 chore: rename bash -> shell 2025-08-13 15:11:30 -05:00
Yuu Toriyama
3f0f910f7b Fix: Error [ERR_DLOPEN_FAILED] (#1546) 2025-08-13 19:49:14 +00:00
opencode
5bf841ab7a release: v0.4.44 2025-08-13 19:49:14 +00:00
Dax Raad
49727e3eab re-enable aur 2025-08-13 15:44:11 -04:00
Dax Raad
592c6ef97f ci: issue 2025-08-13 15:43:06 -04:00
envolution
00579f0ec1 Fix incorrect AUR namespace (#1907) 2025-08-13 15:37:15 -04:00
adamdotdevin
69d516c7fa fix: default scroll speed should be slower 2025-08-13 14:35:18 -05:00
Dax Raad
bedeb626b2 docs: fix 2025-08-13 19:33:38 +00:00
Dominik Engelhardt
a4c14dbb2d feat: convert attachments to text on delete (#1863)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
2025-08-13 19:33:38 +00:00
opencode
036b24791d release: v0.4.43 2025-08-13 19:33:38 +00:00
Dax Raad
93b71477e6 support !shell commands 2025-08-13 15:26:13 -04:00
adamdotdevin
1357319f6f feat: bash commands 2025-08-13 13:28:22 -05:00
Dax Raad
e729eed34d wip: bash 2025-08-13 14:14:27 -04:00
Jay V
2e5fdd8cef docs: global model options 2025-08-13 14:07:10 -04:00
Dax Raad
21f15f15c1 docs(cli): document ! bash commands and session persistence in CLI docs 2025-08-13 13:37:19 -04:00
Dax Raad
c6344c5714 wip: bash 2025-08-13 13:31:29 -04:00
Dax Raad
7505fa61b9 wip: bash commands 2025-08-13 13:29:06 -04:00
Matt Cook
77bb5af092 fix: grammatical error in agent launch example (by Opencode) (#1897) 2025-08-13 12:25:38 -05:00
Aiden Cline
0c4fe73cbf fix: js plugin support as per documentation (#1896) 2025-08-13 12:25:04 -05:00
Aiden Cline
e132f6183d fix: duplicates bot prompt (#1901) 2025-08-13 11:54:37 -05:00
opencode
e06ebb6780 release: v0.4.42 2025-08-13 16:48:35 +00:00
adamdotdevin
66d99ba527 fix: messages layout instability 2025-08-13 11:43:28 -05:00
adamdotdevin
f2021a85d6 fix: allow attachments outside cwd, and support svg 2025-08-13 10:36:50 -05:00
adamdotdevin
7d54f893c9 fix: update read tool description to exclude binary/image files 2025-08-13 10:13:57 -05:00
Mariano Uvalle
e1f80c0067 Merge default agent permissions with global config (#1879) 2025-08-13 09:01:17 -04:00
GitHub Action
4ff13d3290 ignore: update download stats 2025-08-13 2025-08-13 12:04:41 +00:00
Aiden Cline
832d8da453 fix: permission prompting issues (#1884) 2025-08-13 06:34:06 -05:00
Aiden Cline
b5d61b77f7 fix: reasoning not supported (#1882) 2025-08-13 06:26:07 -05:00
Aiden Cline
790e9947bd fix: task tool prompt (#1887) 2025-08-12 23:41:12 -04:00
Dax Raad
2056781cf7 ci: disable 2025-08-12 22:55:57 -04:00
Aiden Cline
ed5f76d849 fix: better error message when config has invalid references (#1874) 2025-08-12 19:28:41 -05:00
opencode
93102dc84b release: v0.4.41 2025-08-13 00:05:51 +00:00
Dax Raad
e2920ac262 update copilot prompt 2025-08-12 20:01:34 -04:00
Dax Raad
aa5e39e744 fix unzip not found printing to tui 2025-08-12 18:43:24 -04:00
opencode
296cc41a07 release: v0.4.40 2025-08-12 21:51:19 +00:00
Dax Raad
482239b848 ci: sync 2025-08-12 17:47:04 -04:00
Dax Raad
17b07877e5 ci: disable AUR packaging in publish workflow 2025-08-12 17:44:39 -04:00
spoons-and-mirrors
dedaa34dc1 fix(TUI): unsurfacing subagent from agents modal (#1873) 2025-08-12 17:38:35 -04:00
Dax Raad
5785ded6e2 add openai prompt cache key 2025-08-12 17:37:15 -04:00
Dax Raad
d1876e3031 ci: enable aur 2025-08-12 17:29:10 -04:00
Tom Hackshaw
9fa3e0a0ec chore: spelling in git committer file (#1869) 2025-08-12 16:29:41 -04:00
spoons-and-mirrors
47c327641b feat: add session rename functionality to TUI modal (#1821)
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
2025-08-12 16:22:03 -04:00
spoons-and-mirrors
81583cddbd refactor(agent-modal): revamped UI/UX for the agent modal (#1838)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
2025-08-12 16:21:57 -04:00
Dax Raad
d16ae1fc4e bash truncate character max instead of line max 2025-08-12 16:14:40 -04:00
Dax Raad
14e00a06b6 ci: sync 2025-08-12 15:09:21 -04:00
Dax Raad
5cc44c872e disable todo tools for qwen models to improve compatibility 2025-08-12 18:56:26 +00:00
opencode
cadc5982f1 release: v0.4.37 2025-08-12 18:56:25 +00:00
Dax Raad
6aa157cfe6 limit bash tool to 1000 lines of output 2025-08-12 14:51:13 -04:00
Dax Raad
9fff9a37d0 ci: sync 2025-08-12 14:38:58 -04:00
Dax Raad
b289fd9dc7 ci: sync 2025-08-12 14:33:09 -04:00
opencode
13d4a802ac release: v0.4.36 2025-08-12 18:31:12 +00:00
adamdotdevin
c4ae3e429c fix: markdown lists 2025-08-12 13:22:28 -05:00
Dax Raad
e9cb360cb7 ci: sync 2025-08-12 14:21:34 -04:00
Dax Raad
596d4e4490 ci: sync 2025-08-12 14:17:18 -04:00
Dax Raad
45451095f7 ci: sync 2025-08-12 14:11:10 -04:00
adamdotdevin
aae354c951 fix: word wrapping with hyphens 2025-08-12 13:03:35 -05:00
Dax Raad
4cddda3e16 ci: sync 2025-08-12 14:00:30 -04:00
Dax Raad
834aa036a4 ci: sync 2025-08-12 13:58:51 -04:00
Dax Raad
6d1b6a6fb1 ci: ignore 2025-08-12 13:54:53 -04:00
Dax Raad
e78fe8dcba ci: tweak 2025-08-12 13:53:32 -04:00
Dax Raad
f62d826037 ci: sync 2025-08-12 13:42:46 -04:00
Dax Raad
342f4239e4 ci: softer language 2025-08-12 13:39:53 -04:00
Dax Raad
5141fe8d7d ci: sync 2025-08-12 17:36:48 +00:00
opencode
3a9dd306db release: v0.4.35 2025-08-12 17:36:48 +00:00
Dax Raad
e32b6e2cc2 ci: ignore 2025-08-12 13:31:23 -04:00
Dax Raad
fab0e5de04 fix issue when @ tagging fiels throwing error 2025-08-12 13:30:52 -04:00
Dax Raad
61105b487f ci: ignore 2025-08-12 13:25:05 -04:00
Dax
6f584ec641 ci: improve guideline check (#1867) 2025-08-12 13:23:19 -04:00
opencode
3a2b2f13f2 release: v0.4.34 2025-08-12 17:14:23 +00:00
Dax Raad
be13e71fb9 ci: sync 2025-08-12 13:09:24 -04:00
Dax Raad
dd0c049119 ci: tweak 2025-08-12 13:01:45 -04:00
Dax Raad
ee2b57958d ci: disable aur 2025-08-12 12:58:18 -04:00
Dax Raad
4178b3c6ae ci: sync 2025-08-12 12:53:55 -04:00
Dax Raad
2c2752ee02 ci: ignore 2025-08-12 12:33:10 -04:00
Dax Raad
5a17f44da4 support OPENCODE_PERMISSION json env variable 2025-08-12 12:28:08 -04:00
Dax Raad
354e55ecef ci: ignore 2025-08-12 12:12:54 -04:00
Dax
10735f93ca Add agent-level permissions with whitelist/blacklist support (#1862) 2025-08-12 11:39:39 -04:00
adamdotdevin
ccaebdcd16 fix: long word and attachment wrapping in editor 2025-08-12 10:34:54 -05:00
opencode
2bbd7a167a release: v0.4.29 2025-08-12 14:00:15 +00:00
Aiden Cline
f12d470b33 fix: Missing ~/.local/share/opencode/bin directory causes misleading … (#1860) 2025-08-12 13:54:33 +00:00
opencode
0835170224 release: v0.4.28 2025-08-12 13:54:32 +00:00
adamdotdevin
3530885f48 fix: vscode extension cursor placement 2025-08-12 08:48:42 -05:00
opencode
a071a2b7f4 release: v0.4.27 2025-08-12 12:58:53 +00:00
adamdotdevin
b2f2c9ac37 fix: use real cursor instead of virtual cursor 2025-08-12 07:52:19 -05:00
GitHub Action
631722213b ignore: update download stats 2025-08-12 2025-08-12 12:04:42 +00:00
Camden Clark
80b25c79bb fix: preserve process.env when spawning formatter commands (#1850) 2025-08-12 02:05:26 -04:00
Dax
3d9c5b4adf Update guidelines-check.yml 2025-08-12 00:34:34 -04:00
Dax
a3064e2c32 Update duplicate-issues.yml 2025-08-12 00:34:16 -04:00
Dax Raad
275bc4d2c8 ci: add guidelines check workflow for PRs 2025-08-12 00:01:59 -04:00
Dax Raad
39106718ed ci: tweak 2025-08-11 23:52:16 -04:00
Dax Raad
02cfdfbf5b ci: add duplicate issue detection workflow 2025-08-11 23:51:08 -04:00
Dax Raad
1ec71e419b support wildcard matching tool names in config 2025-08-11 23:37:09 -04:00
opencode
5fbbdcaf64 release: v0.4.26 2025-08-12 03:25:36 +00:00
Dax Raad
2b6afe90d0 fix azure reasoningEffort 2025-08-11 23:20:19 -04:00
Dax Raad
5f34dcc792 azure reasoning effort 2025-08-11 23:04:32 -04:00
opencode
681abcbf2d release: v0.4.25 2025-08-12 02:51:32 +00:00
Dax Raad
603e81ef5a Simplify git-committer agent definition for better maintainability 2025-08-11 22:45:40 -04:00
Dax Raad
fb0a200ecf refactor: replace OPENCODE_AGENTS env var with HTTP API call
Replace environment variable passing of agent data from Node.js to TUI
with proper HTTP API call to /agent endpoint. This improves architecture
by eliminating env var dependencies and allows dynamic agent data fetching.
2025-08-11 22:42:25 -04:00
opencode
3ec670784d release: v0.4.24 2025-08-12 02:33:35 +00:00
Dax Raad
e6f3cf0839 fix pyright 2025-08-11 22:27:24 -04:00
opencode
9437cf4ff6 release: v0.4.23 2025-08-12 02:04:53 +00:00
Dax Raad
7d095d19f6 fix undo/redo when opencode is run in nested folders 2025-08-11 21:59:12 -04:00
Dax Raad
0ca10ec2f5 ignore: log 2025-08-11 21:52:05 -04:00
Dax Raad
f03fae03e5 switch back to didUpdate instead of closing and opening file 2025-08-11 21:36:05 -04:00
opencode
bb14a955a0 release: v0.4.22 2025-08-12 01:07:27 +00:00
Dax Raad
dac1506680 update anthropic prompt and variables 2025-08-11 21:01:33 -04:00
opencode
3946a08f40 release: v0.4.21 2025-08-12 00:30:49 +00:00
adamdotdevin
ee0519aacc feat: add clangd for cpp 2025-08-11 19:21:59 -05:00
adamdotdevin
dec1e3fdda fix: complete item on space 2025-08-11 18:58:42 -05:00
Carl Brugger
f54e900716 Fix plugin file name (#1837) 2025-08-11 23:43:43 +00:00
opencode
7e8b5749fa release: v0.4.20 2025-08-11 23:43:43 +00:00
adamdotdevin
febf902dc4 Revert "feat: improve file attachment pasting (#1704)"
This reverts commit 81a3e02474.
2025-08-11 18:37:34 -05:00
Jay V
04b51f2610 ignore: share page thinking blocks 2025-08-11 19:36:34 -04:00
Aiden Cline
b2a4f57d64 feat: add -c and -s args to tui command following run command pattern (#1835) 2025-08-11 18:32:09 -05:00
Dax Raad
0ce7d92a8b ignore: fix share page 2025-08-11 16:12:26 -04:00
adamdotdevin
7a67fe7dde fix: collapsed tool calls hidden at times 2025-08-11 13:54:58 -05:00
Aiden Cline
00b4670b8b docs: fix instructions (#1827) 2025-08-11 13:44:12 -05:00
Dax Raad
7633a951e6 ignore: test 2025-08-11 14:43:42 -04:00
adamdotdevin
4ff64c6209 fix: take up less vertical space 2025-08-11 13:38:39 -05:00
Dax Raad
22023fa9e7 remove git bash tool coauthor message 2025-08-11 18:36:06 +00:00
opencode
85e0b53c33 release: v0.4.19 2025-08-11 18:36:06 +00:00
Dax Raad
6eaa231587 Update GPT-5 system prompt to use copilot-specific prompt instead of codex prompt
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-08-11 14:28:49 -04:00
Jay V
befb7509de docs: share 2025-08-11 14:19:22 -04:00
Jay V
09bf0b86d8 docs: share page 2025-08-11 14:01:20 -04:00
Jay V
b5d45fa9f5 docs: share page 2025-08-11 13:57:41 -04:00
Jay V
a6a633d5c1 docs: test share 2025-08-11 13:45:55 -04:00
Jay V
e83e8001da docs: test share 2025-08-11 13:38:03 -04:00
Jay V
0386898476 docs: comment out thinking blocks for share page 2025-08-11 13:21:59 -04:00
adamdotdevin
5e777fd2a2 feat: toggle tool details visible 2025-08-11 11:58:46 -05:00
adamdotdevin
3c71fda648 fix: don't display placeholder on error 2025-08-11 11:58:46 -05:00
Jay V
42329a038a docs: share page fix 2025-08-11 12:51:19 -04:00
Jay V
10f3983f0b docs: edits 2025-08-11 12:41:13 -04:00
opencode
e9de7f95a7 release: v0.4.18 2025-08-11 16:04:07 +00:00
adamdotdevin
a4113acd15 fix: assistant message footer styles 2025-08-11 10:57:18 -05:00
adamdotdevin
9c8e56fc96 fix: assistant message footer styles 2025-08-11 10:52:49 -05:00
adamdotdevin
c78cb57c41 fix: assistant message footer styles 2025-08-11 10:50:00 -05:00
opencode
eb15b2ba75 release: v0.4.17 2025-08-11 15:15:24 +00:00
Dax Raad
279edb6f24 fix azure gpt config 2025-08-11 10:56:16 -04:00
Dax Raad
c51a34bf4b make models key optional in config 2025-08-11 10:54:14 -04:00
adamdotdevin
e8d144d2a2 fix: reformat assistant message footer 2025-08-11 09:38:52 -05:00
adamdotdevin
a760e8364f feat: placeholder on pending assistant message 2025-08-11 09:29:44 -05:00
adamdotdevin
fa7cae59c0 fix: re-render messages on session error 2025-08-11 09:19:45 -05:00
spoons-and-mirrors
8780fa6ccf Fix: Respect agent's preferred model at TUI startup (#1683)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-11 08:51:35 -05:00
spoons-and-mirrors
ab2df0ae33 Feat: Implement Wrap-Around Navigation for List Selection (for Models and Tools modal) (#1768) 2025-08-11 08:47:51 -05:00
Timo Clasen
23757f3ac0 fix: only load the first local and global rule file (#1761) 2025-08-11 08:28:03 -05:00
Aiden Cline
df7296cfe1 fix: instructions should be able to handle absolute paths (#1762) 2025-08-11 08:23:41 -05:00
opencode
776276d5a4 release: v0.4.16 2025-08-11 12:59:20 +00:00
Dax Raad
eea45a22fa ci: tweak 2025-08-11 08:54:14 -04:00
opencode
ddacb04f99 release: v0.4.15 2025-08-11 12:49:52 +00:00
GitHub Action
09561254a8 ignore: update download stats 2025-08-11 2025-08-11 12:04:33 +00:00
spoons-and-mirrors
73a8356b10 Feat: Add F2 Keybind to Cycle Through the 5 Most Recent Models (#1778) 2025-08-11 07:00:32 -05:00
cvzakharchenko
8db75266d0 Issue 1676: Don't eat up the last newline in a multi-line replacement (#1777) 2025-08-11 06:55:45 -05:00
Jake
6c30565d40 Add support for biome.jsonc config file (#1791) 2025-08-11 06:48:46 -05:00
spoons-and-mirrors
b223a29603 Fix: Sanitize MCP Tool Names for Consistency in User Expectations (#1769) 2025-08-11 01:59:50 -04:00
Stibbs
8ed72ae087 chore: add OPENCODE env var (#1780) 2025-08-11 01:56:42 -04:00
Aiden Cline
62b8c7aee0 feat (tui): agents dialog (#1802) 2025-08-11 01:46:38 -04:00
Dax Raad
6145dfcca0 fix run command to be less messy 2025-08-11 01:45:05 -04:00
opencode
4580c88c0b release: v0.4.12 2025-08-11 05:28:22 +00:00
Dax Raad
061ba65d20 show combined output of bash tool progressively 2025-08-11 01:23:00 -04:00
Dax Raad
457386ad08 fix plan mode bash tool making changes 2025-08-11 01:15:12 -04:00
opencode
fce04dc48b release: v0.4.11 2025-08-11 02:30:21 +00:00
Dax Raad
81534ab387 ci: tweaks 2025-08-10 22:23:59 -04:00
Aiden Cline
409a6f93b2 fix: enforce field requirement for cli cmds (#1796) 2025-08-10 22:17:12 -04:00
opencode
55c294c013 release: v0.4.6 2025-08-11 01:59:27 +00:00
Dax Raad
70db372466 add OPENCODE_DISABLE_AUTOUPDATE flag 2025-08-10 21:52:52 -04:00
Dax Raad
8cc427daba docs: docs agent 2025-08-10 21:40:49 -04:00
Dax Raad
8fde772957 ci: smoke test 2025-08-10 21:37:48 -04:00
Dax Raad
d8dc23bde9 pass through additional agent options to the provider 2025-08-10 21:34:46 -04:00
Tom
1c83ef75a2 fix(plugin): prevent compiled binary hang by removing lazy dynamic import (#1794)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-10 21:31:15 -04:00
opencode
95e410db88 release: v0.4.3 2025-08-11 00:53:06 +00:00
Dax Raad
13d3fba86b switch gpt-5 to codex prompt 2025-08-10 20:47:38 -04:00
Dax Raad
3ab4f42ebb support agent options 2025-08-10 20:30:37 -04:00
adamdotdevin
b8d2aebf09 feat: thinking blocks rendered in tui and share page 2025-08-10 19:25:03 -05:00
Frank
20e818ad05 wip gateway 2025-08-10 12:30:52 -04:00
Aiden Cline
542186aa49 feat: webfetch permission support (#1772) 2025-08-10 08:00:44 -05:00
GitHub Action
c478d1bdbb ignore: update download stats 2025-08-10 2025-08-10 12:04:14 +00:00
Frank
7fd2222976 wip: gateway 2025-08-10 01:17:48 -04:00
Frank
e86adb2ec8 wip: gateway 2025-08-10 01:03:15 -04:00
Frank
34ac0e895d wip: gateway 2025-08-10 00:29:00 -04:00
spoons-and-mirrors
bd4319f2bc Feat: Add Agent Name in the LLM Response Footer (and re-order it) (#1770) 2025-08-09 20:22:16 -05:00
Frank
696ab1a752 Update moonshot ai provider doc 2025-08-09 19:22:50 -04:00
Dax Raad
d3ff66e911 use minimal reasoning effort for gpt-5 2025-08-09 15:38:48 -04:00
Aiden Cline
1954b59167 feat: eslint lsp (#1744) 2025-08-09 11:04:58 -05:00
Aiden Cline
e2fac991dc better permissions ux when denying (#1747) 2025-08-09 11:03:33 -05:00
GitHub Action
8ba35eadd4 ignore: update download stats 2025-08-09 2025-08-09 12:04:16 +00:00
Frank
7446f5ad7b wip gateway 2025-08-09 01:28:27 -04:00
Dominik Engelhardt
81a3e02474 feat: improve file attachment pasting (#1704) 2025-08-08 20:06:38 -05:00
Dax Raad
7bbc643600 remove synthetic message in plan mode, fixes being confused in build mode 2025-08-08 20:45:24 -04:00
Dax Raad
53630ebdce gpt-5 lower verbosity 2025-08-08 20:42:22 -04:00
Dax
85eaa5b58b Remove unused OpenTelemetry tracing and fix overlapping highlights (#1738)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-08 20:20:01 -04:00
Erick Christian
b789844b9c feat(agent): allow mode selection during creation (#1699) 2025-08-08 20:07:20 -04:00
Clayton
9b6ef074f0 Reference the actual name of the windows package (#1700) 2025-08-08 20:07:00 -04:00
zWing
2f4291672b chore(js-sdk): Compatible with nodenext (#1667) 2025-08-08 20:05:50 -04:00
rmoriz
83f4e8e156 Clarify remote mcp error (#1729)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-08 20:04:26 -04:00
gsbain
7af2771a7e Docs: Homebrew can install Opencode on Linux (#1737) 2025-08-08 20:04:02 -04:00
Frank
c9a3b35ac2 fix deploy 2025-08-08 18:39:47 -04:00
Frank
0dde6d0840 fix deploy script 2025-08-08 17:59:21 -04:00
Max Pod
d1208bf0a1 docs: Update plugins.mdx (#1690) 2025-08-08 17:11:06 -04:00
Typing Turtle
0a9463541a docs: Adds required models field to variables documentation (#1709) 2025-08-08 16:57:31 -04:00
Yihui Khuu
fe26b4a7b1 fix(tui): preserve scroll position when reflowing due to message stream (#1716) 2025-08-08 13:14:09 -05:00
Frank
8c173e18b7 wip: gateway 2025-08-08 13:24:50 -04:00
Frank
183e0911b7 wip: gateway 2025-08-08 13:24:32 -04:00
GitHub Action
c7bb19ad07 ignore: update download stats 2025-08-08 2025-08-08 12:04:40 +00:00
Timo Clasen
e444d15b57 fix(TUI): enable general (sub-) agent for @ referencing (#1705) 2025-08-08 05:36:55 -05:00
opencode
063d67a046 release: v0.4.1 2025-08-08 03:01:03 +00:00
Dax Raad
4f164c53d2 temporary fix for max output token 2025-08-07 22:54:59 -04:00
Dax Raad
02ef96f89b docs: fix 2025-08-07 21:49:18 -04:00
Dax Raad
8750744068 renable todo tool 2025-08-07 21:47:37 -04:00
Dax Raad
3e74107e36 looser todo tool schema 2025-08-07 21:47:37 -04:00
Jay V
160f839b25 docs: update cli 2025-08-07 19:24:08 -04:00
Jay V
bf5b109c1f docs: edit agent doc 2025-08-07 18:51:54 -04:00
Dax Raad
60254d8ac0 docs: remove modes from sidebar navigation
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-08-07 16:35:35 -04:00
Dax
c34aec060f Merge agent and mode into one (#1689)
The concept of mode has been deprecated, there is now only the agent field in the config.

An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it

Full docs here: https://opencode.ai/docs/agents/
2025-08-07 16:32:12 -04:00
Jay V
12f1ad521f docs: slash commands 2025-08-07 16:16:16 -04:00
Timo Clasen
723a37ea9a fix: get session api (#1684) 2025-08-07 15:28:18 -04:00
Aiden Cline
c6a46615c0 fix: modal pastes (#1677) 2025-08-07 13:23:58 -05:00
GitHub Action
da29380093 ignore: update download stats 2025-08-07 2025-08-07 12:04:29 +00:00
Aiden Cline
7950ae1462 fix: text selection bug (#1664) 2025-08-07 05:32:34 -05:00
opencode
15e830410f release: v0.3.133 2025-08-07 00:30:05 +00:00
Dax Raad
1a561bb512 add api to get session 2025-08-06 20:24:36 -04:00
Jay V
fecae609d9 docs: config doc edits 2025-08-06 16:10:17 -04:00
Jay V
e01a540b08 docs: typos 2025-08-06 15:45:16 -04:00
Timo Clasen
54457e48bb fix(docs): small_model is not used for summarization (#1360) 2025-08-06 14:03:14 -05:00
Aiden Cline
b179d08484 fix: interface conversion panic (#1655) 2025-08-06 14:02:33 -05:00
Jay V
d9edd6818f docs: add undo to tutorial 2025-08-06 13:51:47 -04:00
Dax Raad
4217286b72 ignore: remove demo plugin 2025-08-06 11:36:53 -04:00
Dax Raad
28a4517ec6 add snapshot field in config to disable snapshots 2025-08-06 11:35:37 -04:00
Aiden Cline
b00b2ded4f docs: update readme (#1654) 2025-08-06 09:35:02 -05:00
Aiden Cline
7b6d5b1429 chore: update marked-shiki, remove patch (#1653) 2025-08-06 08:47:53 -05:00
GitHub Action
7210db19e9 ignore: update download stats 2025-08-06 2025-08-06 12:04:53 +00:00
Yihui Khuu
90d2b26426 fix: run command should use specified model from cli args if provided (#1648) 2025-08-06 05:39:44 -05:00
Aiden Cline
6beba2c04f docs: document permissions (#1638) 2025-08-06 05:18:08 -05:00
Aiden Cline
b8a0ecca98 fix: highlight after text wrap (#1640) 2025-08-06 05:17:35 -05:00
Aiden Cline
ad10d3a126 fix: handle undefined agent in task tool (#1642) 2025-08-06 05:16:43 -05:00
Aiden Cline
a48274f82b permissions disallow support (#1627) 2025-08-05 19:14:28 -05:00
adamdotdevin
6b25b7e95e feat: better assistant message visual 2025-08-05 19:05:44 -05:00
Jay V
030a3a7446 docs: identity 2025-08-05 19:36:10 -04:00
Timo Clasen
1a0e7f1e63 docs(plugins): fix typo (#1621) 2025-08-05 17:16:47 -05:00
Aiden Cline
677fb6032b fix: markdown table renders (#1623) 2025-08-05 17:16:35 -05:00
Timo Clasen
49aa48ce58 fix: prevent title regeneration on auto compact (#1628) 2025-08-05 17:15:50 -05:00
251 changed files with 17984 additions and 2445 deletions

View File

@@ -17,10 +17,11 @@ jobs:
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.17
bun-version: 1.2.19
- run: bun install
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}

60
.github/workflows/duplicate-issues.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Duplicate Issue Detection
on:
issues:
types: [opened]
jobs:
check-duplicates:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check for duplicate issues
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"gh issue*": "allow",
"*": "deny"
},
"webfetch": "deny"
}
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
Issue number:
${{ github.event.issue.number }}
Issue body:
${{ github.event.issue.body }}
Please search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
If you find any potential duplicates, please comment on the new issue with:
- A brief explanation of why it might be a duplicate
- Links to the potentially duplicate issues
- A suggestion to check those issues first
Use this format for the comment:
'👋 This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
If none of these address your specific case, please let us know how this issue differs.'
If no clear duplicates are found, do not comment."

53
.github/workflows/guidelines-check.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Guidelines Check
on:
# Disabled - uncomment to re-enable
# pull_request_target:
# types: [opened, synchronize]
jobs:
check-guidelines:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
<pr-number>
${{ github.event.pull_request.number }}
</pr-number>
<pr-description>
${{ github.event.pull_request.body }}
</pr-description>
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
Command MUST be like this.
```
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
```
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."

View File

@@ -21,7 +21,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.17
bun-version: 1.2.19
- run: git fetch --force --tags
- run: bun install -g @vscode/vsce

View File

@@ -52,16 +52,14 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- name: Install dependencies
run: bun install

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ node_modules
.vscode
openapi.json
playground
tmp

14
.opencode/agent/docs.md Normal file
View File

@@ -0,0 +1,14 @@
---
model: openai/gpt-5
reasoningEffort: medium
description: ALWAYS use this when writing docs
---
You are an expert technical documentation writer
You are not verbose
Every chunk of text should be followed by an example or something besides text
to look at.
Chunks of text should not be more than 2 sentences long.

View File

@@ -1,44 +0,0 @@
---
description: >-
Use this agent when you need to create or improve documentation that requires
concrete examples to illustrate every concept. Examples include:
<example>Context: User has written a new API endpoint and needs documentation.
user: 'I just created a POST /users endpoint that accepts name and email
fields. Can you document this?' assistant: 'I'll use the
example-driven-docs-writer agent to create documentation with practical
examples for your API endpoint.' <commentary>Since the user needs
documentation with examples, use the example-driven-docs-writer agent to
create comprehensive docs with code samples.</commentary></example>
<example>Context: User has a complex configuration file that needs
documentation. user: 'This config file has multiple sections and I need docs
that show how each option works' assistant: 'Let me use the
example-driven-docs-writer agent to create documentation that breaks down each
configuration option with practical examples.' <commentary>The user needs
documentation that demonstrates configuration options, perfect for the
example-driven-docs-writer agent.</commentary></example>
---
You are an expert technical documentation writer who specializes in creating clear, example-rich documentation that never leaves readers guessing. Your core principle is that every concept must be immediately illustrated with concrete examples, code samples, or practical demonstrations.
Your documentation approach:
- Never write more than one sentence in any section without providing an example, code snippet, diagram, or practical illustration
- Break up longer explanations with multiple examples showing different scenarios or use cases
- Use concrete, realistic examples rather than abstract or placeholder content
- Include both basic and advanced examples when covering complex topics
- Show expected inputs, outputs, and results for all examples
- Use code blocks, bullet points, tables, or other formatting to visually separate examples from explanatory text
Structural requirements:
- Start each section with a brief one-sentence explanation followed immediately by an example
- For multi-step processes, provide an example after each step
- Include error examples and edge cases alongside success scenarios
- Use consistent formatting and naming conventions throughout examples
- Ensure examples are copy-pasteable and functional when applicable
Quality standards:
- Verify that no paragraph exceeds one sentence without an accompanying example
- Test that examples are accurate and would work in real scenarios
- Ensure examples progress logically from simple to complex
- Include context for when and why to use different approaches shown in examples
- Provide troubleshooting examples for common issues
When you receive a documentation request, immediately identify what needs examples and plan to illustrate every single concept, feature, or instruction with concrete demonstrations. Ask for clarification if you need more context to create realistic, useful examples.

View File

@@ -0,0 +1,10 @@
---
description: Use this agent when you are asked to commit and push code changes to a git repository.
mode: subagent
---
You commit and push to git
Commit messages should be brief since they are used to generate release notes.
Messages should say WHY the change was made and not WHAT was changed.

View File

@@ -1,10 +0,0 @@
import { Plugin } from "./index"
export const ExamplePlugin: Plugin = async ({ app, client, $ }) => {
return {
permission: {},
async "chat.params"(input, output) {
output.topP = 1
},
}
}

View File

@@ -26,7 +26,7 @@ curl -fsSL https://opencode.ai/install | bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
brew install sst/tap/opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
```
@@ -83,7 +83,7 @@ And run.
```bash
$ bun install
$ bun run packages/opencode/src/index.ts
$ bun dev
```
#### Development Notes

View File

@@ -1,41 +1,49 @@
# 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) |
| 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) |

1363
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
import { defineConfig } from "drizzle-kit"
import { Resource } from "sst"
export default defineConfig({
out: "./migrations/",
strict: true,
schema: ["./src/**/*.sql.ts"],
verbose: true,
dialect: "postgresql",
dbCredentials: {
database: Resource.Database.database,
host: Resource.Database.host,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
ssl: {
rejectUnauthorized: false,
},
},
})

View File

@@ -0,0 +1,66 @@
CREATE TABLE "billing" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_method_id" varchar(255),
"payment_method_last4" varchar(4),
"balance" bigint NOT NULL,
"reload" boolean,
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "payment" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_id" varchar(255),
"amount" bigint NOT NULL,
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "usage" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"request_id" varchar(255),
"model" varchar(255) NOT NULL,
"input_tokens" integer NOT NULL,
"output_tokens" integer NOT NULL,
"reasoning_tokens" integer,
"cache_read_tokens" integer,
"cache_write_tokens" integer,
"cost" bigint NOT NULL,
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" text NOT NULL,
"name" varchar(255) NOT NULL,
"time_seen" timestamp with time zone,
"color" integer,
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "workspace" (
"id" varchar(30) PRIMARY KEY NOT NULL,
"slug" varchar(255),
"name" varchar(255),
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");

View File

@@ -0,0 +1,8 @@
CREATE TABLE "account" (
"id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");

View File

@@ -0,0 +1,14 @@
CREATE TABLE "key" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"user_id" text NOT NULL,
"name" varchar(255) NOT NULL,
"key" varchar(255) NOT NULL,
"time_used" timestamp with time zone,
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");

View File

@@ -0,0 +1 @@
ALTER TABLE "usage" DROP COLUMN "request_id";

View File

@@ -0,0 +1,461 @@
{
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,515 @@
{
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,615 @@
{
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,609 @@
{
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,34 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1754518198186,
"tag": "0000_amused_mojo",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1754609655262,
"tag": "0001_thankful_chat",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1754627626945,
"tag": "0002_stale_jackal",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1754672464106,
"tag": "0003_tranquil_spencer_smythe",
"breakpoints": true
}
]
}

23
cloud/core/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.4.45",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"drizzle-orm": "0.41.0",
"postgres": "3.4.7",
"stripe": "18.0.0",
"ulid": "3.0.0"
},
"exports": {
"./*": "./src/*"
},
"scripts": {
"db": "sst shell drizzle-kit"
},
"devDependencies": {
"drizzle-kit": "0.30.5"
}
}

67
cloud/core/src/account.ts Normal file
View File

@@ -0,0 +1,67 @@
import { z } from "zod"
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
import { fn } from "./util/fn"
import { Database } from "./drizzle"
import { Identifier } from "./identifier"
import { AccountTable } from "./schema/account.sql"
import { Actor } from "./actor"
import { WorkspaceTable } from "./schema/workspace.sql"
import { UserTable } from "./schema/user.sql"
export namespace Account {
export const create = fn(
z.object({
email: z.string().email(),
id: z.string().optional(),
}),
async (input) =>
Database.transaction(async (tx) => {
const id = input.id ?? Identifier.create("account")
await tx.insert(AccountTable).values({
id,
email: input.email,
})
return id
}),
)
export const fromID = fn(z.string(), async (id) =>
Database.transaction(async (tx) => {
return tx
.select()
.from(AccountTable)
.where(eq(AccountTable.id, id))
.execute()
.then((rows) => rows[0])
}),
)
export const fromEmail = fn(z.string().email(), async (email) =>
Database.transaction(async (tx) => {
return tx
.select()
.from(AccountTable)
.where(eq(AccountTable.email, email))
.execute()
.then((rows) => rows[0])
}),
)
export const workspaces = async () => {
const actor = Actor.assert("account")
return Database.transaction(async (tx) =>
tx
.select(getTableColumns(WorkspaceTable))
.from(WorkspaceTable)
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(
and(
eq(UserTable.email, actor.properties.email),
isNull(UserTable.timeDeleted),
isNull(WorkspaceTable.timeDeleted),
),
)
.execute(),
)
}
}

75
cloud/core/src/actor.ts Normal file
View File

@@ -0,0 +1,75 @@
import { Context } from "./context"
import { Log } from "./util/log"
export namespace Actor {
interface Account {
type: "account"
properties: {
accountID: string
email: string
}
}
interface Public {
type: "public"
properties: {}
}
interface User {
type: "user"
properties: {
userID: string
workspaceID: string
email: string
}
}
interface System {
type: "system"
properties: {
workspaceID: string
}
}
export type Info = Account | Public | User | System
const ctx = Context.create<Info>()
export const use = ctx.use
const log = Log.create().tag("namespace", "actor")
export function provide<R, T extends Info["type"]>(
type: T,
properties: Extract<Info, { type: T }>["properties"],
cb: () => R,
) {
return ctx.provide(
{
type,
properties,
} as any,
() => {
return Log.provide({ ...properties }, () => {
log.info("provided")
return cb()
})
},
)
}
export function assert<T extends Info["type"]>(type: T) {
const actor = use()
if (actor.type !== type) {
throw new Error(`Expected actor type ${type}, got ${actor.type}`)
}
return actor as Extract<Info, { type: T }>
}
export function workspace() {
const actor = use()
if ("workspaceID" in actor.properties) {
return actor.properties.workspaceID
}
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
}
}

71
cloud/core/src/billing.ts Normal file
View File

@@ -0,0 +1,71 @@
import { Resource } from "sst"
import { Stripe } from "stripe"
import { Database, eq, sql } from "./drizzle"
import { BillingTable, UsageTable } from "./schema/billing.sql"
import { Actor } from "./actor"
import { fn } from "./util/fn"
import { z } from "zod"
import { Identifier } from "./identifier"
import { centsToMicroCents } from "./util/price"
export namespace Billing {
export const stripe = () =>
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
apiVersion: "2025-03-31.basil",
})
export const get = async () => {
return Database.use(async (tx) =>
tx
.select({
customerID: BillingTable.customerID,
paymentMethodID: BillingTable.paymentMethodID,
balance: BillingTable.balance,
reload: BillingTable.reload,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, Actor.workspace()))
.then((r) => r[0]),
)
}
export const consume = fn(
z.object({
requestID: z.string().optional(),
model: z.string(),
inputTokens: z.number(),
outputTokens: z.number(),
reasoningTokens: z.number().optional(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
costInCents: z.number(),
}),
async (input) => {
const workspaceID = Actor.workspace()
const cost = centsToMicroCents(input.costInCents)
return await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID,
id: Identifier.create("usage"),
requestID: input.requestID,
model: input.model,
inputTokens: input.inputTokens,
outputTokens: input.outputTokens,
reasoningTokens: input.reasoningTokens,
cacheReadTokens: input.cacheReadTokens,
cacheWriteTokens: input.cacheWriteTokens,
cost,
})
const [updated] = await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${cost}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
.returning()
return updated.balance
})
},
)
}

21
cloud/core/src/context.ts Normal file
View File

@@ -0,0 +1,21 @@
import { AsyncLocalStorage } from "node:async_hooks"
export namespace Context {
export class NotFound extends Error {}
export function create<T>() {
const storage = new AsyncLocalStorage<T>()
return {
use() {
const result = storage.getStore()
if (!result) {
throw new NotFound()
}
return result
},
provide<R>(value: T, fn: () => R) {
return storage.run<R>(value, fn)
},
}
}
}

View File

@@ -0,0 +1,94 @@
import { drizzle } from "drizzle-orm/postgres-js"
import { Resource } from "sst"
export * from "drizzle-orm"
import postgres from "postgres"
function createClient() {
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
host: Resource.Database.host,
database: Resource.Database.database,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
ssl: {
rejectUnauthorized: false,
},
max: 1,
})
return drizzle(client, {})
}
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
import { Context } from "../context"
export namespace Database {
export type Transaction = PgTransaction<
PostgresJsQueryResultHKT,
Record<string, unknown>,
ExtractTablesWithRelations<Record<string, unknown>>
>
export type TxOrDb = Transaction | ReturnType<typeof createClient>
const TransactionContext = Context.create<{
tx: TxOrDb
effects: (() => void | Promise<void>)[]
}>()
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
try {
const { tx } = TransactionContext.use()
return tx.transaction(callback)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await TransactionContext.provide(
{
effects,
tx: client,
},
() => callback(client),
)
await Promise.all(effects.map((x) => x()))
return result
}
throw err
}
}
export async function fn<Input, T>(callback: (input: Input, trx: TxOrDb) => Promise<T>) {
return (input: Input) => use(async (tx) => callback(input, tx))
}
export async function effect(effect: () => any | Promise<any>) {
try {
const { effects } = TransactionContext.use()
effects.push(effect)
} catch {
await effect()
}
}
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
try {
const { tx } = TransactionContext.use()
return callback(tx)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await client.transaction(async (tx) => {
return TransactionContext.provide({ tx, effects }, () => callback(tx))
}, config)
await Promise.all(effects.map((x) => x()))
return result
}
throw err
}
}
}

View File

@@ -0,0 +1,29 @@
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
export const ulid = (name: string) => varchar(name, { length: 30 })
export const workspaceColumns = {
get id() {
return ulid("id").notNull()
},
get workspaceID() {
return ulid("workspace_id").notNull()
},
}
export const id = () => ulid("id").notNull()
export const utc = (name: string) =>
timestamp(name, {
withTimezone: true,
})
export const currency = (name: string) =>
bigint(name, {
mode: "number",
})
export const timestamps = {
timeCreated: utc("time_created").notNull().defaultNow(),
timeDeleted: utc("time_deleted"),
}

View File

@@ -0,0 +1,26 @@
import { ulid } from "ulid"
import { z } from "zod"
export namespace Identifier {
const prefixes = {
account: "acc",
billing: "bil",
key: "key",
payment: "pay",
usage: "usg",
user: "usr",
workspace: "wrk",
} as const
export function create(prefix: keyof typeof prefixes, given?: string): string {
if (given) {
if (given.startsWith(prefixes[prefix])) return given
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
}
return [prefixes[prefix], ulid()].join("_")
}
export function schema(prefix: keyof typeof prefixes) {
return z.string().startsWith(prefixes[prefix])
}
}

View File

@@ -0,0 +1,12 @@
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { id, timestamps } from "../drizzle/types"
export const AccountTable = pgTable(
"account",
{
id: id(),
...timestamps,
email: varchar("email", { length: 255 }).notNull(),
},
(table) => [uniqueIndex("email").on(table.email)],
)

View File

@@ -0,0 +1,45 @@
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
import { timestamps, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const BillingTable = pgTable(
"billing",
{
...workspaceColumns,
...timestamps,
customerID: varchar("customer_id", { length: 255 }),
paymentMethodID: varchar("payment_method_id", { length: 255 }),
paymentMethodLast4: varchar("payment_method_last4", { length: 4 }),
balance: bigint("balance", { mode: "number" }).notNull(),
reload: boolean("reload"),
},
(table) => [...workspaceIndexes(table)],
)
export const PaymentTable = pgTable(
"payment",
{
...workspaceColumns,
...timestamps,
customerID: varchar("customer_id", { length: 255 }),
paymentID: varchar("payment_id", { length: 255 }),
amount: bigint("amount", { mode: "number" }).notNull(),
},
(table) => [...workspaceIndexes(table)],
)
export const UsageTable = pgTable(
"usage",
{
...workspaceColumns,
...timestamps,
model: varchar("model", { length: 255 }).notNull(),
inputTokens: integer("input_tokens").notNull(),
outputTokens: integer("output_tokens").notNull(),
reasoningTokens: integer("reasoning_tokens"),
cacheReadTokens: integer("cache_read_tokens"),
cacheWriteTokens: integer("cache_write_tokens"),
cost: bigint("cost", { mode: "number" }).notNull(),
},
(table) => [...workspaceIndexes(table)],
)

View File

@@ -0,0 +1,16 @@
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const KeyTable = pgTable(
"key",
{
...workspaceColumns,
...timestamps,
userID: text("user_id").notNull(),
name: varchar("name", { length: 255 }).notNull(),
key: varchar("key", { length: 255 }).notNull(),
timeUsed: utc("time_used"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)],
)

View File

@@ -0,0 +1,16 @@
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const UserTable = pgTable(
"user",
{
...workspaceColumns,
...timestamps,
email: text("email").notNull(),
name: varchar("name", { length: 255 }).notNull(),
timeSeen: utc("time_seen"),
color: integer("color"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
)

View File

@@ -0,0 +1,25 @@
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { timestamps, ulid } from "../drizzle/types"
export const WorkspaceTable = pgTable(
"workspace",
{
id: ulid("id").notNull().primaryKey(),
slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }),
...timestamps,
},
(table) => [uniqueIndex("slug").on(table.slug)],
)
export function workspaceIndexes(table: any) {
return [
primaryKey({
columns: [table.workspaceID, table.id],
}),
foreignKey({
foreignColumns: [WorkspaceTable.id],
columns: [table.workspaceID],
}),
]
}

14
cloud/core/src/util/fn.ts Normal file
View File

@@ -0,0 +1,14 @@
import { z } from "zod"
export function fn<T extends z.ZodType, Result>(
schema: T,
cb: (input: z.output<T>) => Result,
) {
const result = (input: z.input<T>) => {
const parsed = schema.parse(input)
return cb(parsed)
}
result.force = (input: z.input<T>) => cb(input)
result.schema = schema
return result
}

View File

@@ -0,0 +1,55 @@
import { Context } from "../context"
export namespace Log {
const ctx = Context.create<{
tags: Record<string, any>
}>()
export function create(tags?: Record<string, any>) {
tags = tags || {}
const result = {
info(message?: any, extra?: Record<string, any>) {
const prefix = Object.entries({
...use().tags,
...tags,
...extra,
})
.map(([key, value]) => `${key}=${value}`)
.join(" ")
console.log(prefix, message)
return result
},
tag(key: string, value: string) {
if (tags) tags[key] = value
return result
},
clone() {
return Log.create({ ...tags })
},
}
return result
}
export function provide<R>(tags: Record<string, any>, cb: () => R) {
const existing = use()
return ctx.provide(
{
tags: {
...existing.tags,
...tags,
},
},
cb,
)
}
function use() {
try {
return ctx.use()
} catch (e) {
return { tags: {} }
}
}
}

View File

@@ -0,0 +1,3 @@
export function centsToMicroCents(amount: number) {
return Math.round(amount * 1000000)
}

View File

@@ -0,0 +1,48 @@
import { z } from "zod"
import { fn } from "./util/fn"
import { centsToMicroCents } from "./util/price"
import { Actor } from "./actor"
import { Database, eq } from "./drizzle"
import { Identifier } from "./identifier"
import { UserTable } from "./schema/user.sql"
import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
export namespace Workspace {
export const create = fn(z.void(), async () => {
const account = Actor.assert("account")
const workspaceID = Identifier.create("workspace")
await Database.transaction(async (tx) => {
await tx.insert(WorkspaceTable).values({
id: workspaceID,
})
await tx.insert(UserTable).values({
workspaceID,
id: Identifier.create("user"),
email: account.properties.email,
name: "",
})
await tx.insert(BillingTable).values({
workspaceID,
id: Identifier.create("billing"),
balance: centsToMicroCents(100),
})
})
return workspaceID
})
export async function list() {
const account = Actor.assert("account")
return Database.use(async (tx) => {
return tx
.select({
id: WorkspaceTable.id,
slug: WorkspaceTable.slug,
name: WorkspaceTable.name,
})
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.where(eq(UserTable.email, account.properties.email))
})
}
}

9
cloud/core/sst-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}

9
cloud/core/tsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "@opencode/cloud-function",
"version": "0.4.45",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
"@ai-sdk/openai-compatible": "1.0.1",
"@hono/zod-validator": "catalog:",
"@openauthjs/openauth": "0.0.0-20250322224806",
"ai": "catalog:",
"hono": "catalog:",
"zod": "catalog:"
}
}

124
cloud/function/src/auth.ts Normal file
View File

@@ -0,0 +1,124 @@
import { Resource } from "sst"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import { createSubjects } from "@openauthjs/openauth/subject"
import { CodeProvider } from "@openauthjs/openauth/provider/code"
import { GithubProvider } from "@openauthjs/openauth/provider/github"
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
import { Account } from "@opencode/cloud-core/account.js"
type Env = {
AuthStorage: KVNamespace
}
export const subjects = createSubjects({
account: z.object({
accountID: z.string(),
email: z.string(),
}),
user: z.object({
userID: z.string(),
workspaceID: z.string(),
}),
})
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return issuer({
providers: {
github: GithubProvider({
clientID: Resource.GITHUB_CLIENT_ID_CONSOLE.value,
clientSecret: Resource.GITHUB_CLIENT_SECRET_CONSOLE.value,
scopes: ["read:user", "user:email"],
}),
google: GoogleOidcProvider({
clientID: Resource.GOOGLE_CLIENT_ID.value,
scopes: ["openid", "email"],
}),
// email: CodeProvider({
// async request(req, state, form, error) {
// console.log(state)
// const params = new URLSearchParams()
// if (error) {
// params.set("error", error.type)
// }
// if (state.type === "start") {
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/email?" + params.toString(), 302)
// }
//
// if (state.type === "code") {
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/code?" + params.toString(), 302)
// }
//
// return new Response("ok")
// },
// async sendCode(claims, code) {
// const email = z.string().email().parse(claims.email)
// const cmd = new SendEmailCommand({
// Destination: {
// ToAddresses: [email],
// },
// FromEmailAddress: `SST <auth@${Resource.Email.sender}>`,
// Content: {
// Simple: {
// Body: {
// Html: {
// Data: `Your pin code is <strong>${code}</strong>`,
// },
// Text: {
// Data: `Your pin code is ${code}`,
// },
// },
// Subject: {
// Data: "SST Console Pin Code: " + code,
// },
// },
// },
// })
// await ses.send(cmd)
// },
// }),
},
storage: CloudflareStorage({
namespace: env.AuthStorage,
}),
subjects,
async success(ctx, response) {
console.log(response)
let email: string | undefined
if (response.provider === "github") {
const userResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${response.tokenset.access}`,
"User-Agent": "opencode",
Accept: "application/vnd.github+json",
},
})
const user = (await userResponse.json()) as { email: string }
email = user.email
} else if (response.provider === "google") {
if (!response.id.email_verified) throw new Error("Google email not verified")
email = response.id.email as string
}
//if (response.provider === "email") {
// email = response.claims.email
//}
else throw new Error("Unsupported provider")
if (!email) throw new Error("No email found")
let accountID = await Account.fromEmail(email).then((x) => x?.id)
if (!accountID) {
console.log("creating account for", email)
accountID = await Account.create({
email: email!,
})
}
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
},
}

View File

@@ -0,0 +1,909 @@
import { z } from "zod"
import { Hono, MiddlewareHandler } from "hono"
import { cors } from "hono/cors"
import { HTTPException } from "hono/http-exception"
import { zValidator } from "@hono/zod-validator"
import { Resource } from "sst"
import { type ProviderMetadata, type LanguageModelUsage, generateText, streamText } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
import { Actor } from "@opencode/cloud-core/actor.js"
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { createClient } from "@openauthjs/openauth/client"
import { Log } from "@opencode/cloud-core/util/log.js"
import { Billing } from "@opencode/cloud-core/billing.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "../../core/src/identifier"
type Env = {}
let _client: ReturnType<typeof createClient>
const client = () => {
if (_client) return _client
_client = createClient({
clientID: "api",
issuer: Resource.AUTH_API_URL.value,
})
return _client
}
const SUPPORTED_MODELS = {
"anthropic/claude-sonnet-4": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createAnthropic({
apiKey: Resource.ANTHROPIC_API_KEY.value,
})("claude-sonnet-4-20250514"),
},
"openai/gpt-4.1": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createOpenAI({
apiKey: Resource.OPENAI_API_KEY.value,
})("gpt-4.1"),
},
"zhipuai/glm-4.5-flash": {
input: 0,
output: 0,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
model: () =>
createOpenAICompatible({
name: "Zhipu AI",
baseURL: "https://api.z.ai/api/paas/v4",
apiKey: Resource.ZHIPU_API_KEY.value,
})("glm-4.5-flash"),
},
}
const log = Log.create({
namespace: "api",
})
const GatewayAuth: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
const apiKey = authHeader.split(" ")[1]
// Check against KeyTable
const keyRecord = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
if (!keyRecord) {
return c.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
c.set("keyRecord", keyRecord)
await next()
}
const RestAuth: MiddlewareHandler = async (c, next) => {
const authorization = c.req.header("authorization")
if (!authorization) {
return Actor.provide("public", {}, next)
}
const token = authorization.split(" ")[1]
if (!token)
throw new HTTPException(403, {
message: "Bearer token is required.",
})
const verified = await client().verify(token)
if (verified.err) {
throw new HTTPException(403, {
message: "Invalid token.",
})
}
let subject = verified.subject as Actor.Info
if (subject.type === "account") {
const workspaceID = c.req.header("x-opencode-workspace")
const email = subject.properties.email
if (workspaceID) {
const user = await Database.use((tx) =>
tx
.select({
id: UserTable.id,
workspaceID: UserTable.workspaceID,
email: UserTable.email,
})
.from(UserTable)
.where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID)))
.then((rows) => rows[0]),
)
if (!user)
throw new HTTPException(403, {
message: "You do not have access to this workspace.",
})
subject = {
type: "user",
properties: {
userID: user.id,
workspaceID: workspaceID,
email: user.email,
},
}
}
}
await Actor.provide(subject.type, subject.properties, next)
}
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/v1/chat/completions", GatewayAuth, async (c) => {
const keyRecord = c.get("keyRecord")!
return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
try {
// Check balance
const customer = await Billing.get()
if (customer.balance <= 0) {
return c.json(
{
error: {
message: "Insufficient balance",
type: "insufficient_quota",
param: null,
code: "insufficient_quota",
},
},
401,
)
}
const body = await c.req.json<ChatCompletionCreateParamsBase>()
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
if (!model) throw new Error(`Unsupported model: ${body.model}`)
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await model.doStream({
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.stream) {
console.log("!!! CHUNK !!! : " + chunk.type)
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: chunk.input,
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
error: {
message: typeof chunk.error === "string" ? chunk.error : chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason:
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop",
},
],
usage: {
prompt_tokens: chunk.usage.inputTokens,
completion_tokens: chunk.usage.outputTokens,
total_tokens: chunk.usage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.usage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.usage.cachedInputTokens,
},
},
}
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await model.doGenerate({
...requestBody,
})
await trackUsage(body.model, response.usage, response.providerMetadata)
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
const tools = transformTools()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" as const }
if (body.response_format.type === "json_schema")
return {
type: "json" as const,
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" as const }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
tools: tools.tools,
toolChoice: tools.toolChoice,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.map((tool) => {
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters!,
}
})
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = { type: "auto" as const }
} else if (tool_choice === "none") {
aiSdkToolChoice = { type: "none" as const }
} else if (tool_choice === "required") {
aiSdkToolChoice = { type: "required" as const }
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool" as const,
toolName: tool_choice.function.name,
}
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
if (!modelData) throw new Error(`Unsupported model: ${model}`)
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
const reasoningTokens = usage.reasoningTokens ?? 0
const cacheReadTokens = usage.cachedInputTokens ?? 0
const cacheWriteTokens =
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0
const inputCost = modelData.input * inputTokens
const outputCost = modelData.output * outputTokens
const reasoningCost = modelData.reasoning * reasoningTokens
const cacheReadCost = modelData.cacheRead * cacheReadTokens
const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
await Billing.consume({
model,
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWriteTokens,
costInCents,
})
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, keyRecord.id)),
)
}
} catch (error: any) {
return c.json({ error: { message: error.message } }, 500)
}
})
})
.use("/*", cors())
.use(RestAuth)
.get("/rest/account", async (c) => {
const account = Actor.assert("account")
let workspaces = await Workspace.list()
if (workspaces.length === 0) {
await Workspace.create()
workspaces = await Workspace.list()
}
return c.json({
id: account.properties.accountID,
email: account.properties.email,
workspaces,
})
})
.get("/billing/info", async (c) => {
const billing = await Billing.get()
const payments = await Database.use((tx) =>
tx
.select()
.from(PaymentTable)
.where(eq(PaymentTable.workspaceID, Actor.workspace()))
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
.limit(100),
)
const usage = await Database.use((tx) =>
tx
.select()
.from(UsageTable)
.where(eq(UsageTable.workspaceID, Actor.workspace()))
.orderBy(sql`${UsageTable.timeCreated} DESC`)
.limit(100),
)
return c.json({ billing, payments, usage })
})
.post(
"/billing/checkout",
zValidator(
"json",
z.custom<{
success_url: string
cancel_url: string
}>(),
),
async (c) => {
const account = Actor.assert("user")
const body = await c.req.json()
const customer = await Billing.get()
const session = await Billing.stripe().checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "opencode credits",
},
unit_amount: 2000, // $20 minimum
},
quantity: 1,
},
],
payment_intent_data: {
setup_future_usage: "on_session",
},
...(customer.customerID
? { customer: customer.customerID }
: {
customer_email: account.properties.email,
customer_creation: "always",
}),
metadata: {
workspaceID: Actor.workspace(),
},
currency: "usd",
payment_method_types: ["card"],
success_url: body.success_url,
cancel_url: body.cancel_url,
})
return c.json({
url: session.url,
})
},
)
.post("/billing/portal", async (c) => {
const body = await c.req.json()
const customer = await Billing.get()
if (!customer?.customerID) {
throw new Error("No stripe customer ID")
}
const session = await Billing.stripe().billingPortal.sessions.create({
customer: customer.customerID,
return_url: body.return_url,
})
return c.json({
url: session.url,
})
})
.post("/stripe/webhook", async (c) => {
const body = await Billing.stripe().webhooks.constructEventAsync(
await c.req.text(),
c.req.header("stripe-signature")!,
Resource.STRIPE_WEBHOOK_SECRET.value,
)
console.log(body.type, JSON.stringify(body, null, 2))
if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID
const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string
const amount = body.data.object.amount_total
if (!workspaceID) throw new Error("Workspace ID not found")
if (!customerID) throw new Error("Customer ID not found")
if (!amount) throw new Error("Amount not found")
if (!paymentID) throw new Error("Payment ID not found")
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
// set customer metadata
if (!customer?.customerID) {
await Billing.stripe().customers.update(customerID, {
metadata: {
workspaceID,
},
})
}
// get payment method for the payment intent
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
})
.where(eq(BillingTable.workspaceID, workspaceID))
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amount),
paymentID,
customerID,
})
})
})
}
console.log("finished handling")
return c.json("ok", 200)
})
.get("/keys", async (c) => {
const user = Actor.assert("user")
const keys = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
name: KeyTable.name,
key: KeyTable.key,
userID: KeyTable.userID,
timeCreated: KeyTable.timeCreated,
timeUsed: KeyTable.timeUsed,
})
.from(KeyTable)
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
.orderBy(sql`${KeyTable.timeCreated} DESC`),
)
return c.json({ keys })
})
.post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => {
const user = Actor.assert("user")
const { name } = c.req.valid("json")
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let randomPart = ""
for (let i = 0; i < 64; i++) {
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
}
const secretKey = `sk-${randomPart}`
const keyRecord = await Database.use((tx) =>
tx
.insert(KeyTable)
.values({
id: Identifier.create("key"),
workspaceID: user.properties.workspaceID,
userID: user.properties.userID,
name,
key: secretKey,
timeUsed: null,
})
.returning(),
)
return c.json({
key: secretKey,
id: keyRecord[0].id,
name: keyRecord[0].name,
created: keyRecord[0].timeCreated,
})
})
.delete("/keys/:id", async (c) => {
const user = Actor.assert("user")
const keyId = c.req.param("id")
const result = await Database.use((tx) =>
tx
.delete(KeyTable)
.where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID)))
.returning({ id: KeyTable.id }),
)
if (result.length === 0) {
return c.json({ error: "Key not found" }, 404)
}
return c.json({ success: true, id: result[0].id })
})
.all("*", (c) => c.text("Not Found"))
export type ApiType = typeof app
export default app

92
cloud/function/sst-env.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
declare module "sst" {
export interface Resource {
"ANTHROPIC_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
"Console": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
}
"DATABASE_USERNAME": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types";
declare module "sst" {
export interface Resource {
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
}
}
import "sst"
export {}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}

2
cloud/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

38
cloud/web/index.html Normal file
View File

@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en" data-color-mode="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenControl</title>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" media="(prefers-color-scheme: light)">
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
<link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml">
<meta property="twitter:image" content="%BASE_URL%/social-share.png">
<meta property="og:title" content="OpenControl">
<meta property="og:url" content="%BASE_URL%">
<meta property="og:locale" content="en">
<meta property="og:description" content="Control your infrastructure with AI.">
<meta property="og:site_name" content="OpenControl">
<meta name="twitter:card" content="summary_large_image">
<meta name="description" content="Control your infrastructure with AI.">
<meta property="og:image" content="%BASE_URL%/social-share.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Rubik:wght@300..900&display=swap" rel="stylesheet">
<!--ssr-head-->
<!--ssr-assets-->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!--ssr-outlet-->
</div>
<script type="module" src="/src/entry-client.tsx"></script>
</body>
</html>

29
cloud/web/npm-debug.log Normal file
View File

@@ -0,0 +1,29 @@
0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli '/usr/local/bin/node',
1 verbose cli '/Users/frank/Sites/opencode/node_modules/.bin/npm',
1 verbose cli 'run',
1 verbose cli 'dev'
1 verbose cli ]
2 info using npm@2.15.12
3 info using node@v20.18.1
4 verbose stack Error: Invalid name: "@opencode/cloud/web"
4 verbose stack at ensureValidName (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:336:15)
4 verbose stack at Object.fixNameField (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:215:5)
4 verbose stack at /Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:32:38
4 verbose stack at Array.forEach (<anonymous>)
4 verbose stack at normalize (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:31:15)
4 verbose stack at final (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:349:5)
4 verbose stack at then (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:124:5)
4 verbose stack at ReadFileContext.<anonymous> (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:295:20)
4 verbose stack at ReadFileContext.callback (/Users/frank/Sites/opencode/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16)
4 verbose stack at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:299:13)
5 verbose cwd /Users/frank/Sites/opencode/cloud/web
6 error Darwin 24.5.0
7 error argv "/usr/local/bin/node" "/Users/frank/Sites/opencode/node_modules/.bin/npm" "run" "dev"
8 error node v20.18.1
9 error npm v2.15.12
10 error Invalid name: "@opencode/cloud/web"
11 error If you need help, you may report this error at:
11 error <https://github.com/npm/npm/issues>
12 verbose exit [ 1, true ]

32
cloud/web/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@opencode/cloud-web",
"version": "0.4.45",
"private": true,
"description": "",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "bun build:server && bun build:client",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
"serve": "vite preview",
"sst:dev": "bun sst shell --target Console -- bun dev"
},
"license": "MIT",
"devDependencies": {
"typescript": "catalog:",
"vite": "6.2.2",
"vite-plugin-pages": "0.32.5",
"vite-plugin-solid": "2.11.6"
},
"dependencies": {
"@kobalte/core": "0.13.9",
"@openauthjs/solid": "0.0.0-20250322224806",
"@solid-primitives/storage": "4.3.1",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.3",
"solid-js": "1.9.5",
"solid-list": "0.3.0"
}
}

View File

@@ -0,0 +1,3 @@
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@@ -0,0 +1,3 @@
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,24 @@
import fs from "fs"
import path from "path"
import { generateHydrationScript, getAssets } from "solid-js/web"
const dist = import.meta.resolve("../dist").replace("file://", "")
const serverEntry = await import("../dist/server/entry-server.js")
const template = fs.readFileSync(path.join(dist, "client/index.html"), "utf-8")
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
const routes = ["/", "/foo"]
for (const route of routes) {
const { app } = serverEntry.render({ url: route })
const html = template
.replace("<!--ssr-outlet-->", app)
.replace("<!--ssr-head-->", generateHydrationScript())
.replace("<!--ssr-assets-->", getAssets())
const filePath = dist + `/client${route === "/" ? "/index" : route}.html`
fs.mkdirSync(path.dirname(filePath), {
recursive: true,
})
fs.writeFileSync(filePath, html)
console.log(`Pre-rendered: ${filePath}`)
}

42
cloud/web/src/app.tsx Normal file
View File

@@ -0,0 +1,42 @@
/// <reference types="vite-plugin-pages/client-solid" />
import { Router } from "@solidjs/router"
import routes from "~solid-pages"
import "./ui/style/index.css"
import { MetaProvider } from "@solidjs/meta"
import { AccountProvider } from "./components/context-account"
import { DialogProvider } from "./ui/context-dialog"
import { DialogString } from "./ui/dialog-string"
import { DialogSelect } from "./ui/dialog-select"
import { ThemeProvider } from "./components/context-theme"
import { Suspense } from "solid-js"
import { OpenAuthProvider } from "./components/context-openauth"
export function App(props: { url?: string }) {
return (
<ThemeProvider>
<Suspense>
<DialogProvider>
<DialogString />
<DialogSelect />
<OpenAuthProvider
clientID="web"
issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}
>
<AccountProvider>
<MetaProvider>
<Router
children={routes}
url={props.url}
root={(props) => {
return <>{props.children}</>
}}
/>
</MetaProvider>
</AccountProvider>
</OpenAuthProvider>
</DialogProvider>
</Suspense>
</ThemeProvider>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

View File

@@ -0,0 +1,99 @@
import { createContext, createEffect, ParentProps, Suspense, useContext } from "solid-js"
import { makePersisted } from "@solid-primitives/storage"
import { createStore } from "solid-js/store"
import { useOpenAuth } from "./context-openauth"
import { createAsync } from "@solidjs/router"
import { isServer } from "solid-js/web"
type Storage = {
accounts: Record<
string,
{
id: string
email: string
workspaces: {
id: string
name: string
slug: string
}[]
}
>
}
const context = createContext<ReturnType<typeof init>>()
function init() {
const auth = useOpenAuth()
const [store, setStore] = makePersisted(
createStore<Storage>({
accounts: {},
}),
{
name: "opencontrol.account",
},
)
async function refresh(id: string) {
return fetch(import.meta.env.VITE_API_URL + "/rest/account", {
headers: {
authorization: `Bearer ${await auth.access(id)}`,
},
})
.then((val) => val.json())
.then((val) => setStore("accounts", id, val as any))
}
createEffect((previous: string[]) => {
if (Object.keys(auth.all).length === 0) {
return []
}
for (const item of Object.values(auth.all)) {
if (previous.includes(item.id)) continue
refresh(item.id)
}
return Object.keys(auth.all)
}, [] as string[])
const result = {
get all() {
return Object.keys(auth.all)
.map((id) => store.accounts[id])
.filter(Boolean)
},
get current() {
if (!auth.subject) return undefined
return store.accounts[auth.subject.id]
},
refresh,
get ready() {
return Object.keys(auth.all).length === result.all.length
},
}
return result
}
export function AccountProvider(props: ParentProps) {
const ctx = init()
const resource = createAsync(async () => {
await new Promise<void>((resolve) => {
if (isServer) return resolve()
createEffect(() => {
if (ctx.ready) resolve()
})
})
return null
})
return (
<Suspense>
{resource()}
<context.Provider value={ctx}>{props.children}</context.Provider>
</Suspense>
)
}
export function useAccount() {
const result = useContext(context)
if (!result) throw new Error("no account context")
return result
}

View File

@@ -0,0 +1,180 @@
import { createClient } from "@openauthjs/openauth/client"
import { makePersisted } from "@solid-primitives/storage"
import { createAsync } from "@solidjs/router"
import {
batch,
createContext,
createEffect,
createResource,
createSignal,
onMount,
ParentProps,
Show,
Suspense,
useContext,
} from "solid-js"
import { createStore, produce } from "solid-js/store"
import { isServer } from "solid-js/web"
interface Storage {
subjects: Record<string, SubjectInfo>
current?: string
}
interface Context {
all: Record<string, SubjectInfo>
subject?: SubjectInfo
switch(id: string): void
logout(id: string): void
access(id?: string): Promise<string | undefined>
authorize(opts?: AuthorizeOptions): void
}
export interface AuthorizeOptions {
redirectPath?: string
provider?: string
}
interface SubjectInfo {
id: string
refresh: string
}
interface AuthContextOpts {
issuer: string
clientID: string
}
const context = createContext<Context>()
export function OpenAuthProvider(props: ParentProps<AuthContextOpts>) {
const client = createClient({
issuer: props.issuer,
clientID: props.clientID,
})
const [storage, setStorage] = makePersisted(
createStore<Storage>({
subjects: {},
}),
{
name: `${props.issuer}.auth`,
},
)
const resource = createAsync(async () => {
if (isServer) return true
const hash = new URLSearchParams(window.location.search.substring(1))
const code = hash.get("code")
const state = hash.get("state")
if (code && state) {
const oldState = sessionStorage.getItem("openauth.state")
const verifier = sessionStorage.getItem("openauth.verifier")
const redirect = sessionStorage.getItem("openauth.redirect")
if (redirect && verifier && oldState === state) {
const result = await client.exchange(code, redirect, verifier)
if (!result.err) {
const id = result.tokens.refresh.split(":").slice(0, -1).join(":")
batch(() => {
setStorage("subjects", id, {
id: id,
refresh: result.tokens.refresh,
})
setStorage("current", id)
})
}
}
}
return true
})
async function authorize(opts?: AuthorizeOptions) {
const redirect = new URL(window.location.origin + (opts?.redirectPath ?? "/")).toString()
const authorize = await client.authorize(redirect, "code", {
pkce: true,
provider: opts?.provider,
})
sessionStorage.setItem("openauth.state", authorize.challenge.state)
sessionStorage.setItem("openauth.redirect", redirect)
if (authorize.challenge.verifier) sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier)
window.location.href = authorize.url
}
const accessCache = new Map<string, string>()
const pendingRequests = new Map<string, Promise<any>>()
async function access(id: string) {
const pending = pendingRequests.get(id)
if (pending) return pending
const promise = (async () => {
const existing = accessCache.get(id)
const subject = storage.subjects[id]
const access = await client.refresh(subject.refresh, {
access: existing,
})
if (access.err) {
pendingRequests.delete(id)
ctx.logout(id)
return
}
if (access.tokens) {
setStorage("subjects", id, "refresh", access.tokens.refresh)
accessCache.set(id, access.tokens.access)
}
pendingRequests.delete(id)
return access.tokens?.access || existing!
})()
pendingRequests.set(id, promise)
return promise
}
const ctx: Context = {
get all() {
return storage.subjects
},
get subject() {
if (!storage.current) return
return storage.subjects[storage.current!]
},
switch(id: string) {
if (!storage.subjects[id]) return
setStorage("current", id)
},
authorize,
logout(id: string) {
if (!storage.subjects[id]) return
setStorage(
produce((s) => {
delete s.subjects[id]
if (s.current === id) s.current = Object.keys(s.subjects)[0]
}),
)
},
async access(id?: string) {
id = id || storage.current
if (!id) return
return access(id || storage.current!)
},
}
createEffect(() => {
if (!resource()) return
if (storage.current) return
const [first] = Object.keys(storage.subjects)
if (first) {
setStorage("current", first)
return
}
})
return (
<>
{resource()}
<context.Provider value={ctx}>{props.children}</context.Provider>
</>
)
}
export function useOpenAuth() {
const result = useContext(context)
if (!result) throw new Error("no auth context")
return result
}

View File

@@ -0,0 +1,39 @@
import { createStore } from "solid-js/store"
import { makePersisted } from "@solid-primitives/storage"
import { createEffect } from "solid-js"
import { createInitializedContext } from "../util/context"
import { isServer } from "solid-js/web"
interface Storage {
mode: "light" | "dark"
}
export const { provider: ThemeProvider, use: useTheme } =
createInitializedContext("ThemeContext", () => {
const [store, setStore] = makePersisted(
createStore<Storage>({
mode:
!isServer &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
}),
{
name: "theme",
},
)
createEffect(() => {
document.documentElement.setAttribute("data-color-mode", store.mode)
})
return {
setMode(mode: Storage["mode"]) {
setStore("mode", mode)
},
get mode() {
return store.mode
},
ready: true,
}
})

View File

@@ -0,0 +1,13 @@
/* @refresh reload */
import { hydrate, render } from "solid-js/web"
import { App } from "./app"
if (import.meta.env.DEV) {
render(() => <App />, document.getElementById("root")!)
}
if (!import.meta.env.DEV) {
if ("_$HY" in window) hydrate(() => <App />, document.getElementById("root")!)
else render(() => <App />, document.getElementById("root")!)
}

View File

@@ -0,0 +1,7 @@
import { renderToStringAsync } from "solid-js/web"
import { App } from "./app"
export async function render(props: { url: string }) {
const app = await renderToStringAsync(() => <App url={props.url} />)
return { app }
}

View File

@@ -0,0 +1,11 @@
import { WorkspaceProvider } from "./components/context-workspace"
import { ParentProps } from "solid-js"
import Layout from "./components/layout"
export default function Index(props: ParentProps) {
return (
<WorkspaceProvider>
<Layout>{props.children}</Layout>
</WorkspaceProvider>
)
}

View File

@@ -0,0 +1,56 @@
.root {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-7) var(--space-5) var(--space-5);
[data-slot="billing-info"] {
display: flex;
flex-direction: column;
gap: var(--space-6);
}
[data-slot="header"] {
display: flex;
flex-direction: column;
gap: var(--space-1-5);
h2 {
text-transform: uppercase;
font-weight: 600;
letter-spacing: -0.03125rem;
font-size: var(--font-size-lg);
}
p {
color: var(--color-text-dimmed);
font-size: var(--font-size-md);
}
}
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-5);
padding: var(--space-6);
border: 2px solid var(--color-border);
}
[data-slot="amount"] {
font-size: var(--font-size-3xl);
font-weight: 600;
line-height: 1.2;
}
@media (min-width: 40rem) {
[data-slot="balance"] {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
[data-slot="amount"] {
margin: 0;
}
}
}

View File

@@ -0,0 +1,132 @@
import { Button } from "../../ui/button"
import { useApi } from "../components/context-api"
import { createEffect, createSignal, createResource, For } from "solid-js"
import { useWorkspace } from "../components/context-workspace"
import style from "./billing.module.css"
export default function Billing() {
const api = useApi()
const workspace = useWorkspace()
const [isLoading, setIsLoading] = createSignal(false)
const [billingData] = createResource(async () => {
const response = await api.billing.info.$get()
return response.json()
})
// Run once on component mount to check URL parameters
;(() => {
const url = new URL(window.location.href)
const result = url.hash
console.log("STRIPE RESULT", result)
if (url.hash === "#success") {
setIsLoading(true)
// Remove the hash from the URL
window.history.replaceState(null, "", window.location.pathname + window.location.search)
}
})()
createEffect((old?: number) => {
if (old && old !== billingData()?.billing?.balance) {
setIsLoading(false)
}
return billingData()?.billing?.balance
})
const handleBuyCredits = async () => {
try {
setIsLoading(true)
const baseUrl = window.location.href
const successUrl = new URL(baseUrl)
successUrl.hash = "success"
const response = await api.billing.checkout
.$post({
json: {
success_url: successUrl.toString(),
cancel_url: baseUrl,
},
})
.then((r) => r.json() as any)
window.location.href = response.url
} catch (error) {
console.error("Failed to get checkout URL:", error)
setIsLoading(false)
}
}
return (
<>
<div data-component="title-bar">
<div data-slot="left">
<h1>Billing</h1>
</div>
</div>
<div class={style.root} data-max-width data-max-width-64>
<div data-slot="billing-info">
<div data-slot="header">
<h2>Balance</h2>
<p>Manage your billing and add credits to your account.</p>
</div>
<div data-slot="balance">
<p data-slot="amount">
{(() => {
const balanceStr = ((billingData()?.billing?.balance ?? 0) / 100000000).toFixed(2)
return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
})()}
</p>
<Button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
{isLoading() ? "Loading..." : "Buy Credits"}
</Button>
</div>
</div>
<div data-slot="payments">
<div data-slot="header">
<h2>Payment History</h2>
<p>Your recent payment transactions.</p>
</div>
<div data-slot="payment-list">
<For each={billingData()?.payments} fallback={<p>No payments found.</p>}>
{(payment) => (
<div data-slot="payment-item">
<span data-slot="payment-id">{payment.id}</span>
{" | "}
<span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
{" | "}
<span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
</div>
)}
</For>
</div>
</div>
<div data-slot="usage">
<div data-slot="header">
<h2>Usage History</h2>
<p>Your recent API usage and costs.</p>
</div>
<div data-slot="usage-list">
<For each={billingData()?.usage} fallback={<p>No usage found.</p>}>
{(usage) => (
<div data-slot="usage-item">
<span data-slot="usage-model">{usage.model}</span>
{" | "}
<span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
{" | "}
<span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
{" | "}
<span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
</div>
)}
</For>
</div>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,11 @@
You are OpenControl, an interactive CLI tool that helps users execute various tasks.
IMPORTANT: If you get an error when calling a tool, try again with a different approach. Be creative, do not give up, try different inputs to the tool. You should chain together multiple tool calls. ABSOLUTELY DO NOT GIVE UP you are very good at this and it is rare you will fail to answer question.
You should be concise, direct, and to the point.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".

View File

@@ -0,0 +1,271 @@
import { createResource } from "solid-js"
import { createStore, produce } from "solid-js/store"
import SYSTEM_PROMPT from "./system.txt?raw"
import type {
LanguageModelV1Prompt,
LanguageModelV1CallOptions,
LanguageModelV1,
} from "ai"
interface Tool {
name: string
description: string
inputSchema: any
}
interface ToolCallerProps {
tool: {
list: () => Promise<Tool[]>
call: (input: { name: string; arguments: any }) => Promise<any>
}
generate: (
prompt: LanguageModelV1CallOptions,
) => Promise<
| { err: "rate" }
| { err: "context" }
| { err: "balance" }
| ({ err: false } & Awaited<ReturnType<LanguageModelV1["doGenerate"]>>)
>
onPromptUpdated?: (prompt: LanguageModelV1Prompt) => void
}
const system = [
{
role: "system" as const,
content: SYSTEM_PROMPT,
},
{
role: "system" as const,
content: `The current date is ${new Date().toDateString()}. Always use this current date when responding to relative date queries.`,
},
]
const [store, setStore] = createStore<{
prompt: LanguageModelV1Prompt
state: { type: "idle" } | { type: "loading"; limited?: boolean }
}>({
prompt: [...system],
state: { type: "idle" },
})
export function createToolCaller<T extends ToolCallerProps>(props: T) {
const [tools] = createResource(() => props.tool.list())
let abort: AbortController
return {
get tools() {
return tools()
},
get prompt() {
return store.prompt
},
get state() {
return store.state
},
clear() {
setStore("prompt", [...system])
},
async chat(input: string) {
if (store.state.type !== "idle") return
abort = new AbortController()
setStore(
produce((s) => {
s.state = {
type: "loading",
limited: false,
}
s.prompt.push({
role: "user",
content: [
{
type: "text",
text: input,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
while (true) {
if (abort.signal.aborted) {
break
}
const response = await props.generate({
inputFormat: "messages",
prompt: store.prompt,
temperature: 0,
seed: 69,
mode: {
type: "regular",
tools: tools()?.map((tool) => ({
type: "function",
name: tool.name,
description: tool.description,
parameters: {
...tool.inputSchema,
},
})),
},
})
if (abort.signal.aborted) continue
if (!response.err) {
setStore("state", {
type: "loading",
})
if (response.text) {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: response.text || "",
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
}
if (response.finishReason === "stop") {
break
}
if (response.finishReason === "tool-calls") {
for (const item of response.toolCalls || []) {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "tool-call",
toolName: item.toolName,
args: JSON.parse(item.args),
toolCallId: item.toolCallId,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
const called = await props.tool.call({
name: item.toolName,
arguments: JSON.parse(item.args),
})
setStore(
produce((s) => {
s.prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: item.toolName,
toolCallId: item.toolCallId,
result: called,
},
],
})
}),
)
props.onPromptUpdated?.(store.prompt)
}
}
continue
}
if (response.err === "context") {
setStore(
produce((s) => {
s.prompt.splice(2, 1)
}),
)
props.onPromptUpdated?.(store.prompt)
}
if (response.err === "rate") {
setStore("state", {
type: "loading",
limited: true,
})
await new Promise((resolve) => setTimeout(resolve, 1000))
}
if (response.err === "balance") {
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: "You need to add credits to your account. Please go to Billing and add credits to continue.",
},
],
})
s.state = { type: "idle" }
}),
)
props.onPromptUpdated?.(store.prompt)
break
}
}
setStore("state", { type: "idle" })
},
async cancel() {
abort.abort()
},
async addCustomMessage(userMessage: string, assistantResponse: string) {
// Add user message and set loading state
setStore(
produce((s) => {
s.prompt.push({
role: "user",
content: [
{
type: "text",
text: userMessage,
},
],
})
s.state = {
type: "loading",
limited: false,
}
}),
)
props.onPromptUpdated?.(store.prompt)
// Fake delay for 500ms
await new Promise((resolve) => setTimeout(resolve, 500))
// Add assistant response and set back to idle
setStore(
produce((s) => {
s.prompt.push({
role: "assistant",
content: [
{
type: "text",
text: assistantResponse,
},
],
})
s.state = { type: "idle" }
}),
)
props.onPromptUpdated?.(store.prompt)
},
}
}

View File

@@ -0,0 +1,239 @@
.root {
display: contents;
[data-slot="messages"] {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
height: 0;
/* This is important for flexbox to allow scrolling */
font-family: var(--font-mono);
color: var(--color-text);
row-gap: var(--space-4);
/* Add consistent spacing between messages */
/* Remove top border for first user message */
&>[data-component="message"][data-user]:first-child::before {
display: none;
}
&:has([data-component="loading"]) [data-component="clear"] {
display: none;
}
}
[data-component="message"] {
width: 100%;
padding: var(--space-2) var(--space-4);
line-height: var(--font-line-height);
white-space: pre-wrap;
align-self: flex-start;
min-height: auto;
/* Allow natural height for all messages */
display: flex;
flex-direction: column;
align-items: flex-start;
/* User message styling */
&[data-user] {
padding: var(--space-6) var(--space-4);
position: relative;
font-weight: 600;
color: var(--color-text);
/* margin: 0.5rem 0; */
}
&[data-user]::before,
&[data-user]::after {
content: "";
position: absolute;
left: var(--space-4);
right: var(--space-4);
height: var(--space-px);
background-color: var(--color-border);
z-index: 1;
/* Ensure borders appear above other content */
}
&[data-user]::before {
top: 0;
}
&[data-user]::after {
bottom: 0;
}
&[data-assistant] {
color: var(--color-text);
}
}
[data-component="tool"] {
display: flex;
width: 100%;
padding: 0 var(--space-4);
margin-left: 0;
flex-direction: column;
opacity: 0.7;
gap: var(--space-2);
align-items: flex-start;
color: var(--color-text-dimmed);
min-height: auto;
/* Allow natural height */
[data-slot="header"] {
display: flex;
gap: var(--space-2);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
align-items: center;
width: 100%;
}
[data-slot="name"] {
letter-spacing: -0.03125rem;
text-transform: uppercase;
font-weight: 500;
font-size: var(--font-size-sm);
}
[data-slot="expand"] {
font-size: var(--font-size-sm);
}
[data-slot="content"] {
padding: 0;
line-height: var(--font-line-height);
font-size: var(--font-size-sm);
white-space: pre-wrap;
display: none;
width: 100%;
}
[data-slot="output"] {
margin-top: var(--space-1);
}
&[data-expanded="true"] [data-slot="content"] {
display: block;
}
&[data-expanded="true"] [data-slot="expand"] {
transform: rotate(45deg);
}
}
[data-component="loading"] {
padding: var(--space-4) var(--space-4) var(--space-8);
height: 1.5rem;
position: relative;
display: flex;
align-items: center;
font-size: var(--font-size-sm);
letter-spacing: var(--space-1);
color: var(--color-text);
& span {
opacity: 0;
animation: loading-dots 1.4s linear infinite;
}
& span:nth-child(2) {
animation-delay: 0.2s;
}
& span:nth-child(3) {
animation-delay: 0.4s;
}
}
[data-component="clear"] {
position: relative;
padding: var(--space-4) var(--space-4);
&::before {
content: "";
position: absolute;
left: var(--space-4);
right: var(--space-4);
top: 0;
height: var(--space-px);
background-color: var(--color-border);
z-index: 1;
}
& [data-component="button"] {
padding-left: 0;
}
}
[data-slot="footer"] {
display: flex;
flex-direction: column;
padding: 0;
border-top: 2px solid var(--color-border);
position: sticky;
bottom: 0;
z-index: 10;
/* Ensure it's above other content */
margin-top: auto;
/* Push to bottom if content is short */
width: 100%;
}
[data-component="chat"] {
display: flex;
padding: var(--space-0-5) 0;
align-items: center;
width: 100%;
height: 100%;
textarea {
--padding-y: var(--space-4);
--line-height: 1.5;
--text-height: calc(var(--line-height) * var(--font-size-lg));
--height: calc(var(--text-height) + var(--padding-y) * 2);
width: 100%;
resize: none;
line-height: var(--line-height);
height: var(--height);
min-height: var(--height);
max-height: calc(5 * var(--text-height) + var(--padding-y) * 2);
padding: var(--padding-y) var(--space-4);
border-radius: 0;
background-color: transparent;
color: var(--color-text);
border: none;
outline: none;
font-size: var(--font-size-lg);
}
textarea::placeholder {
color: var(--color-text-dimmed);
opacity: 0.75;
}
textarea:focus {
outline: 0;
}
& [data-component="button"] {
height: 100%;
}
}
}
@keyframes loading-dots {
0%,
100% {
opacity: 0;
}
40%,
60% {
opacity: 1;
}
}

View File

@@ -0,0 +1,18 @@
import { Button } from "../../ui/button"
import { IconArrowRight } from "../../ui/svg/icons"
import { createSignal, For } from "solid-js"
import { createToolCaller } from "./components/tool"
import { useApi } from "../components/context-api"
import { useWorkspace } from "../components/context-workspace"
import style from "./index.module.css"
export default function Index() {
const api = useApi()
const workspace = useWorkspace()
return (
<div class={style.root}>
<h1>Hello</h1>
</div>
)
}

View File

@@ -0,0 +1,97 @@
.root {
display: flex;
flex-direction: column;
gap: 2rem;
}
.root [data-slot="keys-info"] {
display: flex;
flex-direction: column;
gap: 1rem;
}
.root [data-slot="header"] {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.root [data-slot="header"] h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.root [data-slot="header"] p {
margin: 0;
color: var(--color-text-secondary);
}
.root [data-slot="key-list"] {
display: flex;
flex-direction: column;
gap: 1rem;
}
.root [data-slot="key-item"] {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
background: var(--color-background-secondary);
}
.root [data-slot="key-actions"] {
display: flex;
gap: 0.5rem;
}
.root [data-slot="key-info"] {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.root [data-slot="key-value"] {
font-family: monospace;
font-size: 0.875rem;
color: var(--color-text-primary);
}
.root [data-slot="key-meta"] {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.root [data-slot="empty-state"] {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-secondary);
}
.root [data-slot="actions"] {
display: flex;
align-items: center;
justify-content: space-between;
}
.root [data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 300px;
}
.root [data-slot="form-actions"] {
display: flex;
gap: 0.5rem;
}
.root [data-slot="key-name"] {
font-weight: 600;
font-size: 1rem;
color: var(--color-text-primary);
margin-bottom: 0.25rem;
}

View File

@@ -0,0 +1,151 @@
import { Button } from "../../ui/button"
import { useApi } from "../components/context-api"
import { createSignal, createResource, For, Show } from "solid-js"
import style from "./keys.module.css"
export default function Keys() {
const api = useApi()
const [isCreating, setIsCreating] = createSignal(false)
const [showCreateForm, setShowCreateForm] = createSignal(false)
const [keyName, setKeyName] = createSignal("")
const [keysData, { refetch }] = createResource(async () => {
const response = await api.keys.$get()
return response.json()
})
const handleCreateKey = async () => {
if (!keyName().trim()) return
try {
setIsCreating(true)
await api.keys.$post({
json: { name: keyName().trim() },
})
refetch()
setKeyName("")
setShowCreateForm(false)
} catch (error) {
console.error("Failed to create API key:", error)
} finally {
setIsCreating(false)
}
}
const handleDeleteKey = async (keyId: string) => {
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
return
}
try {
await api.keys[":id"].$delete({
param: { id: keyId },
})
refetch()
} catch (error) {
console.error("Failed to delete API key:", error)
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString()
}
const formatKey = (key: string) => {
if (key.length <= 11) return key
return `${key.slice(0, 7)}...${key.slice(-4)}`
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
return (
<>
<div data-component="title-bar">
<div data-slot="left">
<h1>API Keys</h1>
</div>
</div>
<div class={style.root} data-max-width data-max-width-64>
<div data-slot="keys-info">
<div data-slot="actions">
<div data-slot="header">
<h2>API Keys</h2>
<p>Manage your API keys to access the OpenCode gateway.</p>
</div>
<Show
when={!showCreateForm()}
fallback={
<div data-slot="create-form">
<input
data-component="input"
type="text"
placeholder="Enter key name"
value={keyName()}
onInput={(e) => setKeyName(e.currentTarget.value)}
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
/>
<div data-slot="form-actions">
<Button color="primary" disabled={isCreating() || !keyName().trim()} onClick={handleCreateKey}>
{isCreating() ? "Creating..." : "Create"}
</Button>
<Button
color="ghost"
onClick={() => {
setShowCreateForm(false)
setKeyName("")
}}
>
Cancel
</Button>
</div>
</div>
}
>
<Button color="primary" onClick={() => setShowCreateForm(true)}>
Create API Key
</Button>
</Show>
</div>
<div data-slot="key-list">
<For
each={keysData()?.keys}
fallback={
<div data-slot="empty-state">
<p>Create an API key to access opencode gateway</p>
</div>
}
>
{(key) => (
<div data-slot="key-item">
<div data-slot="key-info">
<div data-slot="key-name">{key.name}</div>
<div data-slot="key-value">{formatKey(key.key)}</div>
<div data-slot="key-meta">
Created: {formatDate(key.timeCreated)}
{key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
</div>
</div>
<div data-slot="key-actions">
<Button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
Copy
</Button>
<Button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
Delete
</Button>
</div>
</div>
)}
</For>
</div>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,24 @@
import { hc } from "hono/client"
import { ApiType } from "@opencode/cloud-function/src/gateway"
import { useWorkspace } from "./context-workspace"
import { useOpenAuth } from "../../components/context-openauth"
export function useApi() {
const workspace = useWorkspace()
const auth = useOpenAuth()
return hc<ApiType>(import.meta.env.VITE_API_URL, {
async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
const [input, init] = args
const request = input instanceof Request ? input : new Request(input, init)
const headers = new Headers(request.headers)
headers.set("authorization", `Bearer ${await auth.access()}`)
headers.set("x-opencode-workspace", workspace.id)
return fetch(
new Request(request, {
...init,
headers,
}),
)
},
})
}

View File

@@ -0,0 +1,38 @@
import { useNavigate, useParams } from "@solidjs/router"
import { createInitializedContext } from "../../util/context"
import { useAccount } from "../../components/context-account"
import { createEffect, createMemo } from "solid-js"
export const { use: useWorkspace, provider: WorkspaceProvider } =
createInitializedContext("WorkspaceProvider", () => {
const params = useParams()
const account = useAccount()
const workspace = createMemo(() =>
account.current?.workspaces.find(
(x) => x.id === params.workspace || x.slug === params.workspace,
),
)
const nav = useNavigate()
createEffect(() => {
if (!workspace()) nav("/")
})
const result = () => workspace()!
result.ready = true
return {
get id() {
return workspace()!.id
},
get slug() {
return workspace()!.slug
},
get name() {
return workspace()!.name
},
get ready() {
return workspace() !== undefined
},
}
})

View File

@@ -0,0 +1,199 @@
.root {
--padding: var(--space-10);
--vertical-padding: var(--space-8);
--heading-font-size: var(--font-size-4xl);
--sidebar-width: 200px;
--mobile-breakpoint: 40rem;
--topbar-height: 60px;
margin: var(--space-4);
border: 2px solid var(--color-border);
height: calc(100vh - var(--space-8));
display: flex;
flex-direction: row;
overflow: hidden;
/* Prevent overall scrolling */
position: relative;
}
[data-component="mobile-top-bar"] {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--topbar-height);
background: var(--color-background);
border-bottom: 2px solid var(--color-border);
z-index: 20;
align-items: center;
padding: 0 var(--space-4) 0 0;
[data-slot="logo"] {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
div {
text-transform: uppercase;
font-weight: 600;
letter-spacing: -0.03125rem;
}
svg {
height: 28px;
width: auto;
color: var(--color-white);
}
}
[data-slot="toggle"] {
background: transparent;
border: none;
padding: var(--space-4);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
& svg {
width: 24px;
height: 24px;
color: var(--color-foreground);
}
}
}
[data-component="sidebar"] {
width: var(--sidebar-width);
border-right: 2px solid var(--color-border);
display: flex;
flex-direction: column;
padding: calc(var(--padding) / 2);
overflow-y: auto;
/* Allow scrolling if needed */
position: sticky;
top: 0;
height: 100%;
background-color: var(--color-background);
z-index: 10;
[data-slot="logo"] {
margin-top: 2px;
margin-bottom: var(--space-7);
color: var(--color-white);
& svg {
height: 32px;
width: auto;
}
}
[data-slot="nav"] {
flex: 1;
ul {
list-style-type: none;
padding: 0;
}
li {
margin-bottom: calc(var(--vertical-padding) / 2);
text-transform: uppercase;
font-weight: 500;
}
a {
display: block;
padding: var(--space-2) 0;
}
}
[data-slot="user"] {
[data-component="button"] {
padding-left: 0;
padding-bottom: 0;
height: auto;
}
}
}
.navActiveLink {
cursor: default;
text-decoration: none;
}
[data-slot="main-content"] {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
/* Full height */
overflow: hidden;
/* Prevent overflow */
position: relative;
/* For positioning footer */
width: 100%;
/* Full width */
}
/* Backdrop for mobile */
[data-component="backdrop"] {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background-color: rgba(0, 0, 0, 0.5); */
z-index: 25;
backdrop-filter: blur(2px);
}
/* Mobile styles */
@media (max-width: 40rem) {
.root {
margin: 0;
border: none;
height: 100vh;
}
[data-component="mobile-top-bar"] {
display: flex;
}
[data-component="backdrop"] {
display: block;
}
[data-component="sidebar"] {
position: fixed;
left: -100%;
top: 0;
height: 100vh;
width: 80%;
max-width: 280px;
transition: left 0.3s ease-in-out;
box-shadow: none;
z-index: 30;
padding: var(--space-8);
background-color: var(--color-bg);
&[data-opened="true"] {
left: 0;
box-shadow: 8px 0 0px 0px var(--color-gray-4);
}
}
[data-slot="main-content"] {
padding-top: var(--topbar-height);
/* Add space for the top bar */
overflow-y: auto;
}
/* Hide the logo in the sidebar on mobile since it's in the top bar */
[data-component="sidebar"] [data-slot="logo"] {
display: none;
}
}

View File

@@ -0,0 +1,96 @@
import style from "./layout.module.css"
import { useAccount } from "../../components/context-account"
import { Button } from "../../ui/button"
import { IconLogomark } from "../../ui/svg"
import { IconBars3BottomLeft } from "../../ui/svg/icons"
import { ParentProps, createMemo, createSignal } from "solid-js"
import { A, useLocation } from "@solidjs/router"
import { useOpenAuth } from "../../components/context-openauth"
export default function Layout(props: ParentProps) {
const auth = useOpenAuth()
const account = useAccount()
const [sidebarOpen, setSidebarOpen] = createSignal(false)
const location = useLocation()
const workspaceId = createMemo(() => account.current?.workspaces[0].id)
const pageTitle = createMemo(() => {
const path = location.pathname
if (path.endsWith("/billing")) return "Billing"
if (path.endsWith("/keys")) return "API Keys"
return null
})
function handleLogout() {
auth.logout(auth.subject?.id!)
}
return (
<div class={style.root}>
{/* Mobile top bar */}
<div data-component="mobile-top-bar">
<button data-slot="toggle" onClick={() => setSidebarOpen(!sidebarOpen())}>
<IconBars3BottomLeft />
</button>
<div data-slot="logo">
{pageTitle() ? (
<div>{pageTitle()}</div>
) : (
<A href="/">
<IconLogomark />
</A>
)}
</div>
</div>
{/* Backdrop for mobile sidebar - closes sidebar when clicked */}
{sidebarOpen() && <div data-component="backdrop" onClick={() => setSidebarOpen(false)}></div>}
<div data-component="sidebar" data-opened={sidebarOpen() ? "true" : "false"}>
<div data-slot="logo">
<A href="/">
<IconLogomark />
</A>
</div>
<nav data-slot="nav">
<ul>
<li>
<A end activeClass={style.navActiveLink} href={`/${workspaceId()}`} onClick={() => setSidebarOpen(false)}>
Chat
</A>
</li>
<li>
<A
activeClass={style.navActiveLink}
href={`/${workspaceId()}/billing`}
onClick={() => setSidebarOpen(false)}
>
Billing
</A>
</li>
<li>
<A
activeClass={style.navActiveLink}
href={`/${workspaceId()}/keys`}
onClick={() => setSidebarOpen(false)}
>
API Keys
</A>
</li>
</ul>
</nav>
<div data-slot="user">
<Button color="ghost" onClick={handleLogout} title={account.current?.email || ""}>
Logout
</Button>
</div>
</div>
{/* Main Content */}
<div data-slot="main-content">{props.children}</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { Match, Switch } from "solid-js"
import { useAccount } from "../components/context-account"
import { Navigate } from "@solidjs/router"
import { IconLogo } from "../ui/svg"
import styles from "./lander.module.css"
import { useOpenAuth } from "../components/context-openauth"
export default function Index() {
const auth = useOpenAuth()
const account = useAccount()
return (
<Switch>
<Match when={account.current}>
<Navigate href={`/${account.current!.workspaces[0].id}`} />
</Match>
<Match when={!account.current}>
<div class={styles.lander}>
<div data-slot="hero">
<section data-slot="top">
<div data-slot="logo">
<IconLogo />
</div>
<h1>opencode Gateway Console</h1>
</section>
<section data-slot="cta">
<div>
<span onClick={() => auth.authorize({ provider: "github" })}>Sign in with GitHub</span>
</div>
<div>
<span onClick={() => auth.authorize({ provider: "google" })}>Sign in with Google</span>
</div>
</section>
</div>
</div>
</Match>
</Switch>
)
}

View File

@@ -0,0 +1,83 @@
.lander {
--padding: 3rem;
--vertical-padding: 2rem;
--heading-font-size: 2rem;
margin: 1rem;
@media (max-width: 30rem) {
& {
--padding: 1.5rem;
--vertical-padding: 1rem;
--heading-font-size: 1.5rem;
margin: 0.5rem;
}
}
[data-slot="hero"] {
border: 2px solid var(--color-border);
max-width: 64rem;
margin-left: auto;
margin-right: auto;
width: 100%;
}
[data-slot="top"] {
padding: var(--padding);
h1 {
margin-top: calc(var(--vertical-padding) / 8);
font-size: var(--heading-font-size);
line-height: 1.25;
text-transform: uppercase;
font-weight: 600;
}
[data-slot="logo"] {
width: clamp(200px, 70vw, 400px);
color: var(--color-white);
}
}
[data-slot="cta"] {
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 2px solid var(--color-border);
& > div {
flex: 1;
line-height: 1.4;
text-align: center;
text-transform: uppercase;
cursor: pointer;
text-decoration: underline;
letter-spacing: -0.03125rem;
&[data-slot="col-2"] {
background-color: var(--color-border);
color: var(--color-text-invert);
font-weight: 600;
}
& > * {
display: block;
width: 100%;
height: 100%;
padding: calc(var(--padding) / 2) 0.5rem;
}
}
@media (max-width: 30rem) {
& > div {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
}
& > div + div {
border-left: 2px solid var(--color-border);
}
}
}

View File

@@ -0,0 +1,204 @@
.pageContainer {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.componentTable {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
border: 2px solid var(--color-border);
}
.componentCell {
padding: 1rem;
border: 2px solid var(--color-border);
vertical-align: top;
}
.componentLabel {
text-transform: uppercase;
letter-spacing: -0.03125rem;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.75rem;
color: var(--color-text-dimmed);
}
.sectionTitle {
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: -0.03125rem;
font-size: 1.2rem;
}
.divider {
height: 2px;
background: var(--color-border);
margin: 3rem 0;
width: 100%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.buttonSection {
margin-bottom: 4rem;
}
.colorSection {
margin-bottom: 4rem;
}
.labelSection {
margin-bottom: 4rem;
}
.inputSection {
margin-bottom: 4rem;
}
.dialogSection {
margin-bottom: 4rem;
}
.formGroup {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dialogContent {
padding: 2rem;
}
.dialogContentFooter {
margin-top: 1rem;
}
.pageTitle {
font-size: var(--heading-font-size, 2rem);
text-transform: uppercase;
font-weight: 600;
}
.colorBox {
width: 100%;
height: 80px;
margin-bottom: 0.5rem;
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 0.5rem;
}
.colorOrange {
background-color: var(--color-orange);
}
.colorOrangeLow {
background-color: var(--color-orange-low);
}
.colorOrangeHigh {
background-color: var(--color-orange-high);
}
.colorGreen {
background-color: var(--color-green);
}
.colorGreenLow {
background-color: var(--color-green-low);
}
.colorGreenHigh {
background-color: var(--color-green-high);
}
.colorBlue {
background-color: var(--color-blue);
}
.colorBlueLow {
background-color: var(--color-blue-low);
}
.colorBlueHigh {
background-color: var(--color-blue-high);
}
.colorPurple {
background-color: var(--color-purple);
}
.colorPurpleLow {
background-color: var(--color-purple-low);
}
.colorPurpleHigh {
background-color: var(--color-purple-high);
}
.colorRed {
background-color: var(--color-red);
}
.colorRedLow {
background-color: var(--color-red-low);
}
.colorRedHigh {
background-color: var(--color-red-high);
}
.colorAccent {
background-color: var(--color-accent);
}
.colorAccentLow {
background-color: var(--color-accent-low);
}
.colorAccentHigh {
background-color: var(--color-accent-high);
}
.colorCode {
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-family: monospace;
}
.colorVariants {
display: flex;
gap: 0.5rem;
}
.colorVariant {
flex: 1;
height: 40px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.colorVariantCode {
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 2px 4px;
border-radius: 4px;
font-size: 0.65rem;
font-family: monospace;
white-space: nowrap;
}

View File

@@ -0,0 +1,562 @@
import { Button } from "../../ui/button"
import { Dialog } from "../../ui/dialog"
import { Navigate } from "@solidjs/router"
import { createSignal, Show } from "solid-js"
import { IconHome, IconPencilSquare } from "../../ui/svg/icons"
import { useTheme } from "../../components/context-theme"
import { useDialog } from "../../ui/context-dialog"
import { DialogString } from "../../ui/dialog-string"
import { DialogSelect } from "../../ui/dialog-select"
import styles from "./design.module.css"
export default function DesignSystem() {
const dialog = useDialog()
const [dialogOpen, setDialogOpen] = createSignal(false)
const [dialogOpenTransition, setDialogOpenTransition] = createSignal(false)
const theme = useTheme()
// Check if we're running locally
const isLocal = import.meta.env.DEV === true
if (!isLocal) {
return <Navigate href="/" />
}
// Add a toggle button for theme
const toggleTheme = () => {
theme.setMode(theme.mode === "light" ? "dark" : "light")
}
return (
<div class={styles.pageContainer}>
<div class={styles.header}>
<h1 class={styles.pageTitle}>Design System</h1>
<Button onClick={toggleTheme}>
Toggle {theme.mode === "light" ? "Dark" : "Light"} Mode
</Button>
</div>
<section class={styles.colorSection}>
<h2 class={styles.sectionTitle}>Colors</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Orange</h3>
<div class={`${styles.colorBox} ${styles.colorOrange}`}>
<span class={styles.colorCode}>hsl(41, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorOrangeLow}`}
>
<span class={styles.colorVariantCode}>
hsl(41, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorOrangeHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(41, 82%, 87%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Green</h3>
<div class={`${styles.colorBox} ${styles.colorGreen}`}>
<span class={styles.colorCode}>hsl(101, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorGreenLow}`}>
<span class={styles.colorVariantCode}>
hsl(101, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorGreenHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(101, 82%, 80%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Blue</h3>
<div class={`${styles.colorBox} ${styles.colorBlue}`}>
<span class={styles.colorCode}>hsl(234, 100%, 60%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorBlueLow}`}>
<span class={styles.colorVariantCode}>
hsl(234, 54%, 20%)
</span>
</div>
<div class={`${styles.colorVariant} ${styles.colorBlueHigh}`}>
<span class={styles.colorVariantCode}>
hsl(234, 100%, 87%)
</span>
</div>
</div>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Purple</h3>
<div class={`${styles.colorBox} ${styles.colorPurple}`}>
<span class={styles.colorCode}>hsl(281, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorPurpleLow}`}
>
<span class={styles.colorVariantCode}>
hsl(281, 39%, 22%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorPurpleHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(281, 82%, 89%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Red</h3>
<div class={`${styles.colorBox} ${styles.colorRed}`}>
<span class={styles.colorCode}>hsl(339, 82%, 63%)</span>
</div>
<div class={styles.colorVariants}>
<div class={`${styles.colorVariant} ${styles.colorRedLow}`}>
<span class={styles.colorVariantCode}>
hsl(339, 39%, 22%)
</span>
</div>
<div class={`${styles.colorVariant} ${styles.colorRedHigh}`}>
<span class={styles.colorVariantCode}>
hsl(339, 82%, 87%)
</span>
</div>
</div>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Accent</h3>
<div class={`${styles.colorBox} ${styles.colorAccent}`}>
<span class={styles.colorCode}>hsl(13, 88%, 57%)</span>
</div>
<div class={styles.colorVariants}>
<div
class={`${styles.colorVariant} ${styles.colorAccentLow}`}
>
<span class={styles.colorVariantCode}>
hsl(13, 75%, 30%)
</span>
</div>
<div
class={`${styles.colorVariant} ${styles.colorAccentHigh}`}
>
<span class={styles.colorVariantCode}>
hsl(13, 100%, 78%)
</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.buttonSection}>
<h2 class={styles.sectionTitle}>Buttons</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Primary</h3>
<Button>Primary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Secondary</h3>
<Button color="secondary">Secondary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Ghost</h3>
<Button color="ghost">Ghost Button</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Primary Disabled</h3>
<Button disabled>Primary Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Secondary Disabled</h3>
<Button color="secondary" disabled>
Secondary Button
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Ghost Disabled</h3>
<Button color="ghost" disabled>
Ghost Button
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<Button size="sm">Small Button</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Secondary</h3>
<Button size="sm" color="secondary">
Small Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Ghost</h3>
<Button size="sm" color="ghost">
Small Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>With Icon</h3>
<Button icon={<IconHome />}>With Icon</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon + Secondary</h3>
<Button icon={<IconHome />} color="secondary">
Icon Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon + Ghost</h3>
<Button icon={<IconHome />} color="ghost">
Icon Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon</h3>
<Button size="sm" icon={<IconHome />}>
Small Icon
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon + Secondary</h3>
<Button size="sm" icon={<IconHome />} color="secondary">
Small Icon Secondary
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small + Icon + Ghost</h3>
<Button size="sm" icon={<IconHome />} color="ghost">
Small Icon Ghost
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only</h3>
<Button icon={<IconHome />}></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only + Secondary</h3>
<Button icon={<IconHome />} color="secondary"></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only + Ghost</h3>
<Button icon={<IconHome />} color="ghost"></Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Icon Only Disabled</h3>
<Button icon={<IconHome />} disabled></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Icon Only + Secondary Disabled
</h3>
<Button icon={<IconHome />} color="secondary" disabled></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Icon Only + Ghost Disabled
</h3>
<Button icon={<IconHome />} color="ghost" disabled></Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Icon Only</h3>
<Button size="sm" icon={<IconHome />}></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>
Small Icon Only + Secondary
</h3>
<Button
size="sm"
icon={<IconHome />}
color="secondary"
></Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small Icon Only + Ghost</h3>
<Button size="sm" icon={<IconHome />} color="ghost"></Button>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.labelSection}>
<h2 class={styles.sectionTitle}>Labels</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<label data-size="sm" data-component="label">
Small Label Text
</label>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Medium</h3>
<label data-size="md" data-component="label">
Medium Label Text
</label>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Large</h3>
<label data-size="lg" data-component="label">
Large Label Text
</label>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.inputSection}>
<h2 class={styles.sectionTitle}>Inputs</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small</h3>
<input
data-component="input"
data-size="sm"
placeholder="Small input field"
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Medium</h3>
<input
data-component="input"
data-size="md"
placeholder="Medium input field"
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Large</h3>
<input
data-component="input"
data-size="lg"
placeholder="Large input field"
/>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Disabled</h3>
<input
data-component="input"
data-size="md"
placeholder="Disabled input"
disabled
/>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>With Value</h3>
<input
data-component="input"
data-size="md"
value="Input with preset value"
readOnly
/>
</td>
</tr>
</tbody>
</table>
</section>
<div class={styles.divider}></div>
<section class={styles.dialogSection}>
<h2 class={styles.sectionTitle}>Dialogs</h2>
<table class={styles.componentTable}>
<tbody>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Default</h3>
<Button color="secondary" onClick={() => setDialogOpen(true)}>
Open Dialog
</Button>
<Dialog open={dialogOpen()} onOpenChange={setDialogOpen}>
<div data-slot="header">
<div data-slot="title">Dialog Title</div>
</div>
<div data-slot="main">
<p>This is the default dialog content.</p>
</div>
<div data-slot="footer">
<Button onClick={() => setDialogOpen(false)}>Close</Button>
</div>
</Dialog>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Small With Transition</h3>
<Button
color="secondary"
onClick={() => {
setDialogOpenTransition(true)
}}
>
Small Dialog
</Button>
<Dialog
open={dialogOpenTransition()}
onOpenChange={setDialogOpenTransition}
size="sm"
transition={true}
>
<div class={styles.dialogContent}>
<h2 class={styles.sectionTitle}>Small Dialog</h2>
<p>This is a smaller dialog with transitions.</p>
<div class={styles.dialogContentFooter}>
<Button onClick={() => setDialogOpenTransition(false)}>
Close
</Button>
</div>
</div>
</Dialog>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Input String</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogString, {
title: "Name",
action: "Change name",
placeholder: "Enter a name",
onSubmit: () => {},
})
}
>
String
</Button>
</td>
</tr>
<tr>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select Input</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [
{
display: "Change name",
prefix: <IconPencilSquare />,
onSelect: () => {
dialog.close()
},
},
{
display: "Remove user",
prefix: <IconHome />,
onSelect: () => {
dialog.close()
},
},
],
})
}
>
Select
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select Input</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [
{
display: "Change name",
onSelect: () => {
dialog.close()
},
},
{
display: "Remove user",
onSelect: () => {
dialog.close()
},
},
],
})
}
>
No Prefix
</Button>
</td>
<td class={styles.componentCell}>
<h3 class={styles.componentLabel}>Select No Options</h3>
<Button
color="secondary"
onClick={() =>
dialog.open(DialogSelect, {
placeholder: "Select",
title: "User Settings",
options: [],
})
}
>
No Options
</Button>
</td>
</tr>
</tbody>
</table>
</section>
</div>
)
}

12
cloud/web/src/sst-env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DOCS_URL: string
readonly VITE_API_URL: string
readonly VITE_AUTH_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,24 @@
import { Button as Kobalte } from "@kobalte/core/button"
import { JSX, Show, splitProps } from "solid-js"
export interface ButtonProps {
color?: "primary" | "secondary" | "ghost"
size?: "md" | "sm"
icon?: JSX.Element
}
export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) {
const [split, rest] = splitProps(props, ["color", "size", "icon"])
return (
<Kobalte
{...rest}
data-component="button"
data-size={split.size || "md"}
data-color={split.color || "primary"}
>
<Show when={props.icon}>
<div data-slot="icon">{props.icon}</div>
</Show>
{props.children}
</Kobalte>
)
}

View File

@@ -0,0 +1,120 @@
import { createContext, JSX, ParentProps, useContext } from "solid-js"
import { StandardSchemaV1 } from "@standard-schema/spec"
import { createStore } from "solid-js/store"
import { Dialog } from "./dialog"
const Context = createContext<DialogControl>()
type DialogControl = {
open<Schema extends StandardSchemaV1<object>>(
component: DialogComponent<Schema>,
input: StandardSchemaV1.InferInput<Schema>,
): void
close(): void
isOpen(input: any): boolean
size: "sm" | "md"
transition?: boolean
input?: any
}
type DialogProps<Schema extends StandardSchemaV1<object>> = {
input: StandardSchemaV1.InferInput<Schema>
control: DialogControl
}
type DialogComponent<Schema extends StandardSchemaV1<object>> = ReturnType<
typeof createDialog<Schema>
>
export function createDialog<Schema extends StandardSchemaV1<object>>(props: {
schema: Schema
size: "sm" | "md"
render: (props: DialogProps<Schema>) => JSX.Element
}) {
const result = () => {
const dialog = useDialog()
return (
<Dialog
size={dialog.size}
transition={dialog.transition}
open={dialog.isOpen(result)}
onOpenChange={(val) => {
if (!val) dialog.close()
}}
>
{props.render({
input: dialog.input,
control: dialog,
})}
</Dialog>
)
}
result.schema = props.schema
result.size = props.size
return result
}
export function DialogProvider(props: ParentProps) {
const [store, setStore] = createStore<{
dialog?: DialogComponent<any>
input?: any
transition?: boolean
size: "sm" | "md"
}>({
size: "sm",
})
const control: DialogControl = {
get input() {
return store.input
},
get size() {
return store.size
},
get transition() {
return store.transition
},
isOpen(input) {
return store.dialog === input
},
open(component, input) {
setStore({
dialog: component,
input: input,
size: store.dialog !== undefined ? store.size : component.size,
transition: store.dialog !== undefined,
})
setTimeout(() => {
setStore({
size: component.size,
})
}, 0)
setTimeout(() => {
setStore({
transition: false,
})
}, 150)
},
close() {
setStore({
dialog: undefined,
})
},
}
return (
<>
<Context.Provider value={control}>{props.children}</Context.Provider>
</>
)
}
export function useDialog() {
const ctx = useContext(Context)
if (!ctx) {
throw new Error("useDialog must be used within a DialogProvider")
}
return ctx
}

View File

@@ -0,0 +1,36 @@
.options {
margin-top: var(--space-1);
border-top: 2px solid var(--color-border);
padding: var(--space-2);
[data-slot="option"] {
outline: none;
flex-shrink: 0;
height: var(--space-11);
display: flex;
justify-content: start;
align-items: center;
padding: 0 var(--space-2-5);
gap: var(--space-3);
cursor: pointer;
&[data-empty] {
cursor: default;
color: var(--color-text-dimmed);
}
&[data-active] {
background-color: var(--color-bg-surface);
}
[data-slot="title"] {
font-size: var(--font-size-md);
}
[data-slot="prefix"] {
width: var(--space-4);
height: var(--space-4);
}
}
}

View File

@@ -0,0 +1,124 @@
import style from "./dialog-select.module.css"
import { z } from "zod"
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
import { createList } from "solid-list"
import { createDialog } from "./context-dialog"
export const DialogSelect = createDialog({
size: "md",
schema: z.object({
title: z.string(),
placeholder: z.string(),
onSelect: z
.function(z.tuple([z.any()]))
.returns(z.void())
.optional(),
options: z.array(
z.object({
display: z.string(),
value: z.any().optional(),
onSelect: z.function().returns(z.void()).optional(),
prefix: z.custom<JSX.Element>().optional(),
}),
),
}),
render: (ctx) => {
let input: HTMLInputElement
onMount(() => {
input.focus()
input.value = ""
})
const [filter, setFilter] = createSignal("")
const filtered = createMemo(() =>
ctx.input.options?.filter((i) =>
i.display.toLowerCase().includes(filter().toLowerCase()),
),
)
const list = createList({
loop: true,
initialActive: 0,
items: () => filtered().map((_, i) => i),
handleTab: false,
})
const handleSelection = (index: number) => {
const option = ctx.input.options[index]
// If the option has its own onSelect handler, use it
if (option.onSelect) {
option.onSelect()
}
// Otherwise, if there's a global onSelect handler, call it with the option's value
else if (ctx.input.onSelect) {
ctx.input.onSelect(
option.value !== undefined ? option.value : option.display,
)
}
}
return (
<>
<div data-slot="header">
<label
data-size="md"
data-slot="title"
data-component="label"
for={`dialog-select-${ctx.input.title}`}
>
{ctx.input.title}
</label>
</div>
<div data-slot="main">
<input
data-size="lg"
data-component="input"
value={filter()}
onInput={(e) => {
setFilter(e.target.value)
list.setActive(0)
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
const selected = list.active()
if (selected === null) return
handleSelection(selected)
return
}
if (e.key === "Escape") {
setFilter("")
return
}
list.onKeyDown(e)
}}
id={`dialog-select-${ctx.input.title}`}
ref={(r) => (input = r)}
data-slot="input"
placeholder={ctx.input.placeholder}
/>
</div>
<div data-slot="options" class={style.options}>
<For
each={filtered()}
fallback={
<div data-slot="option" data-empty>
No results
</div>
}
>
{(option, index) => (
<div
onClick={() => handleSelection(index())}
data-slot="option"
data-active={list.active() === index() ? true : undefined}
>
{option.prefix && <div data-slot="prefix">{option.prefix}</div>}
<div data-slot="title">{option.display}</div>
</div>
)}
</For>
</div>
</>
)
},
})

View File

@@ -0,0 +1,70 @@
import { z } from "zod"
import { onMount } from "solid-js"
import { createDialog } from "./context-dialog"
import { Button } from "./button"
export const DialogString = createDialog({
size: "sm",
schema: z.object({
title: z.string(),
placeholder: z.string(),
action: z.string(),
onSubmit: z.function().args(z.string()).returns(z.void()),
}),
render: (ctx) => {
let input: HTMLInputElement
onMount(() => {
setTimeout(() => {
input.focus()
input.value = ""
}, 50)
})
function submit() {
const value = input.value.trim()
if (value) {
ctx.input.onSubmit(value)
ctx.control.close()
}
}
return (
<>
<div data-slot="header">
<label
data-size="md"
data-slot="title"
data-component="label"
for={`dialog-string-${ctx.input.title}`}
>
{ctx.input.title}
</label>
</div>
<div data-slot="main">
<input
data-slot="input"
data-size="lg"
data-component="input"
ref={(r) => (input = r)}
placeholder={ctx.input.placeholder}
id={`dialog-string-${ctx.input.title}`}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
submit()
}
}}
/>
</div>
<div data-slot="footer">
<Button size="md" color="ghost" onClick={() => ctx.control.close()}>
Cancel
</Button>
<Button size="md" color="secondary" onClick={submit}>
{ctx.input.action}
</Button>
</div>
</>
)
},
})

View File

@@ -0,0 +1,27 @@
import { Dialog as Kobalte } from "@kobalte/core/dialog"
import { ComponentProps, ParentProps } from "solid-js"
export type Props = ParentProps<{
size?: "sm" | "md"
transition?: boolean
}> &
ComponentProps<typeof Kobalte>
export function Dialog(props: Props) {
return (
<Kobalte {...props}>
<Kobalte.Portal>
<Kobalte.Overlay data-component="dialog-overlay" />
<div data-component="dialog-center">
<Kobalte.Content
data-transition={props.transition ? "" : undefined}
data-size={props.size}
data-slot="content"
>
{props.children}
</Kobalte.Content>
</div>
</Kobalte.Portal>
</Kobalte>
)
}

View File

@@ -0,0 +1,78 @@
[data-component="button"] {
width: fit-content;
display: flex;
line-height: 1;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-size: var(--font-size-md);
text-transform: uppercase;
height: var(--space-11);
outline: none;
font-weight: 500;
padding: 0 var(--space-4);
border-width: 2px;
border-color: var(--color-border);
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: default;
}
&[data-color="primary"] {
background-color: var(--color-text);
border-color: var(--color-text);
color: var(--color-text-invert);
&:active {
border-color: var(--color-accent);
}
}
&[data-color="secondary"] {
&:active {
border-color: var(--color-accent);
}
}
&[data-color="ghost"] {
border: none;
text-decoration: underline;
&:active {
color: var(--color-text-accent);
}
}
&:has([data-slot="icon"]) {
padding-left: var(--space-3);
padding-right: var(--space-3);
}
&[data-size="sm"] {
height: var(--space-8);
padding: var(--space-3);
font-size: var(--font-size-xs);
[data-slot="icon"] {
width: var(--space-3-5);
height: var(--space-3-5);
}
&:has([data-slot="icon"]) {
padding-left: var(--space-2);
padding-right: var(--space-2);
}
}
[data-slot="icon"] {
width: var(--space-4);
height: var(--space-4);
transition: transform 0.2s ease;
}
&[data-rotate] [data-slot="icon"] {
transform: rotate(180deg);
}
}

View File

@@ -0,0 +1,84 @@
[data-component="dialog-overlay"] {
pointer-events: none !important;
position: fixed;
inset: 0;
animation-name: fadeOut;
animation-duration: 200ms;
animation-timing-function: ease;
opacity: 0;
backdrop-filter: blur(2px);
&[data-expanded] {
animation-name: fadeIn;
opacity: 1;
pointer-events: auto !important;
}
}
[data-component="dialog-center"] {
position: fixed;
inset: 0;
padding-top: 10vh;
justify-content: center;
pointer-events: none;
[data-slot="content"] {
width: 45rem;
margin: 0 auto;
transition: 150ms width;
background-color: var(--color-bg);
border-width: 2px;
border-color: var(--color-border);
overflow: hidden;
display: flex;
flex-direction: column;
gap: var(--space-3);
outline: none;
animation-duration: 1ms;
animation-name: zoomOut;
animation-timing-function: ease;
box-shadow: 8px 8px 0px 0px var(--color-gray-4);
&[data-expanded] {
animation-name: zoomIn;
}
&[data-transition] {
animation-duration: 200ms;
}
&[data-size="sm"] {
width: 30rem;
}
[data-slot="header"] {
display: flex;
padding: var(--space-4) var(--space-4) 0;
[data-slot="title"] {
}
}
[data-slot="main"] {
padding: 0 var(--space-4);
&:has([data-slot="options"]) {
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
}
[data-slot="input"] {
}
[data-slot="footer"] {
padding: var(--space-4);
display: flex;
gap: var(--space-4);
justify-content: end;
}
}
}

View File

@@ -0,0 +1,34 @@
[data-component="input"] {
font-size: var(--font-size-md);
background: transparent;
caret-color: var(--color-accent);
font-family: var(--font-mono);
height: var(--space-11);
padding: 0 var(--space-4);
width: 100%;
resize: none;
border: 2px solid var(--color-border);
&::placeholder {
color: var(--color-text-dimmed);
opacity: 0.75;
}
&:focus {
outline: 0;
}
&[data-size="sm"] {
height: var(--space-9);
padding: 0 var(--space-3);
font-size: var(--font-size-xs);
}
&[data-size="md"] {
}
&[data-size="lg"] {
height: var(--space-12);
font-size: var(--font-size-lg);
}
}

View File

@@ -0,0 +1,17 @@
[data-component="label"] {
letter-spacing: -0.03125rem;
text-transform: uppercase;
color: var(--color-text-dimmed);
font-weight: 500;
font-size: var(--font-size-md);
&[data-size="sm"] {
font-size: var(--font-size-sm);
}
&[data-size="md"] {
}
&[data-size="lg"] {
font-size: var(--font-size-lg);
}
}

View File

@@ -0,0 +1,32 @@
[data-component="title-bar"] {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
padding: 0 var(--space-4);
border-bottom: 2px solid var(--color-border);
[data-slot="left"] {
display: flex;
flex-direction: column;
gap: var(--space-1-5);
h1 {
letter-spacing: -0.03125rem;
font-size: var(--font-size-xl);
text-transform: uppercase;
font-weight: 600;
}
p {
color: var(--color-text-dimmed);
}
}
}
@media (max-width: 40rem) {
[data-component="title-bar"] {
display: none;
}
}

View File

@@ -0,0 +1,50 @@
/* tokens */
@import "./token/color.css";
@import "./token/reset.css";
@import "./token/animation.css";
@import "./token/font.css";
@import "./token/space.css";
/* components */
@import "./component/label.css";
@import "./component/input.css";
@import "./component/button.css";
@import "./component/dialog.css";
@import "./component/title-bar.css";
body {
font-family: var(--font-mono);
line-height: 1;
color: var(--color-text);
background-color: var(--color-bg);
cursor: default;
user-select: none;
text-underline-offset: 0.1875rem;
}
a {
text-decoration: underline;
&:active {
color: var(--color-text-accent);
}
}
::selection {
background-color: var(--color-text-accent-invert);
}
/* Responsive utilities */
[data-max-width] {
width: 100%;
& > * {
max-width: 90rem;
margin-left: auto;
margin-right: auto;
width: 100%;
}
&[data-max-width-64] > * {
max-width: 64rem;
}
}

View File

@@ -0,0 +1,23 @@
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes zoomOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}

View File

@@ -0,0 +1,88 @@
:root {
--color-white: hsl(0, 0%, 100%);
--color-gray-1: hsl(224, 20%, 94%);
--color-gray-2: hsl(224, 6%, 77%);
--color-gray-3: hsl(224, 6%, 56%);
--color-gray-4: hsl(224, 7%, 36%);
--color-gray-5: hsl(224, 10%, 23%);
--color-gray-6: hsl(224, 14%, 16%);
--color-black: hsl(224, 10%, 10%);
--hue-orange: 41;
--color-orange-low: hsl(var(--hue-orange), 39%, 22%);
--color-orange: hsl(var(--hue-orange), 82%, 63%);
--color-orange-high: hsl(var(--hue-orange), 82%, 87%);
--hue-green: 101;
--color-green-low: hsl(var(--hue-green), 39%, 22%);
--color-green: hsl(var(--hue-green), 82%, 63%);
--color-green-high: hsl(var(--hue-green), 82%, 80%);
--hue-blue: 234;
--color-blue-low: hsl(var(--hue-blue), 54%, 20%);
--color-blue: hsl(var(--hue-blue), 100%, 60%);
--color-blue-high: hsl(var(--hue-blue), 100%, 87%);
--hue-purple: 281;
--color-purple-low: hsl(var(--hue-purple), 39%, 22%);
--color-purple: hsl(var(--hue-purple), 82%, 63%);
--color-purple-high: hsl(var(--hue-purple), 82%, 89%);
--hue-red: 339;
--color-red-low: hsl(var(--hue-red), 39%, 22%);
--color-red: hsl(var(--hue-red), 82%, 63%);
--color-red-high: hsl(var(--hue-red), 82%, 87%);
--color-accent-low: hsl(13, 75%, 30%);
--color-accent: hsl(13, 88%, 57%);
--color-accent-high: hsl(13, 100%, 78%);
--color-text: var(--color-gray-1);
--color-text-dimmed: var(--color-gray-3);
--color-text-accent: var(--color-accent);
--color-text-invert: var(--color-black);
--color-text-accent-invert: var(--color-accent-high);
--color-bg: var(--color-black);
--color-bg-surface: var(--color-gray-5);
--color-bg-accent: var(--color-accent-high);
--color-border: var(--color-gray-2);
--color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
}
:root[data-color-mode="light"] {
--color-white: hsl(224, 10%, 10%);
--color-gray-1: hsl(224, 14%, 16%);
--color-gray-2: hsl(224, 10%, 23%);
--color-gray-3: hsl(224, 7%, 36%);
--color-gray-4: hsl(224, 6%, 56%);
--color-gray-5: hsl(224, 6%, 77%);
--color-gray-6: hsl(224, 20%, 94%);
--color-gray-7: hsl(224, 19%, 97%);
--color-black: hsl(0, 0%, 100%);
--color-orange-high: hsl(var(--hue-orange), 80%, 25%);
--color-orange: hsl(var(--hue-orange), 90%, 60%);
--color-orange-low: hsl(var(--hue-orange), 90%, 88%);
--color-green-high: hsl(var(--hue-green), 80%, 22%);
--color-green: hsl(var(--hue-green), 90%, 46%);
--color-green-low: hsl(var(--hue-green), 85%, 90%);
--color-blue-high: hsl(var(--hue-blue), 80%, 30%);
--color-blue: hsl(var(--hue-blue), 90%, 60%);
--color-blue-low: hsl(var(--hue-blue), 88%, 90%);
--color-purple-high: hsl(var(--hue-purple), 90%, 30%);
--color-purple: hsl(var(--hue-purple), 90%, 60%);
--color-purple-low: hsl(var(--hue-purple), 80%, 90%);
--color-red-high: hsl(var(--hue-red), 80%, 30%);
--color-red: hsl(var(--hue-red), 90%, 60%);
--color-red-low: hsl(var(--hue-red), 80%, 90%);
--color-accent-high: hsl(13, 75%, 26%);
--color-accent: hsl(13, 88%, 60%);
--color-accent-low: hsl(13, 100%, 89%);
--color-text-accent: var(--color-accent);
--color-text-dimmed: var(--color-gray-4);
--color-text-invert: var(--color-black);
--color-text-accent-invert: var(--color-accent-low);
--color-bg-surface: var(--color-gray-6);
--color-bg-accent: var(--color-accent);
--color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
}

View File

@@ -0,0 +1,20 @@
:root {
--font-size-2xs: 0.6875rem;
--font-size-xs: 0.75rem;
--font-size-sm: 0.8125rem;
--font-size-md: 0.9375rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-size-5xl: 3rem;
--font-size-6xl: 3.75rem;
--font-size-7xl: 4.5rem;
--font-size-8xl: 6rem;
--font-size-9xl: 8rem;
--font-mono: IBM Plex Mono, monospace;
--font-sans: Rubik, sans-serif;
--font-line-height: 1.75;
}

View File

@@ -0,0 +1,212 @@
* {
margin: 0;
padding: 0;
font: inherit;
}
*,
*::before,
*::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: var(--global-color-border, currentColor);
}
html {
line-height: 1.5;
--font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-moz-tab-size: 4;
tab-size: 4;
font-family: var(--global-font-body, var(--font-fallback));
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
body {
height: 100%;
line-height: inherit;
}
img {
border-style: none;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
vertical-align: middle;
}
img,
video {
max-width: 100%;
height: auto;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
ol,
ul {
list-style: none;
}
code,
kbd,
pre,
samp {
font-size: 1em;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
}
button,
select {
text-transform: none;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: var(--global-color-placeholder, #9ca3af);
}
textarea {
resize: vertical;
}
summary {
display: list-item;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
dialog {
padding: 0;
}
a {
color: inherit;
text-decoration: inherit;
}
abbr:where([title]) {
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp,
pre {
font-size: 1em;
--font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New";
font-family: var(--global-font-mono, var(--font-fallback));
}
input[type="text"],
input[type="email"],
input[type="search"],
input[type="password"] {
-webkit-appearance: none;
-moz-appearance: none;
}
input[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
::-webkit-search-decoration,
::-webkit-search-cancel-button {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
input[type="number"] {
-moz-appearance: textfield;
}
:-moz-ui-invalid {
box-shadow: none;
}
:-moz-focusring {
outline: auto;
}

View File

@@ -0,0 +1,38 @@
:root {
--space-0: 0;
--space-px: 1px;
--space-0-5: 0.125rem;
--space-1: 0.25rem;
--space-1-5: 0.375rem;
--space-2: 0.5rem;
--space-2-5: 0.625rem;
--space-3: 0.75rem;
--space-3-5: 0.875rem;
--space-4: 1rem;
--space-4-5: 1.125rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-7: 1.75rem;
--space-8: 2rem;
--space-9: 2.25rem;
--space-10: 2.5rem;
--space-11: 2.75rem;
--space-12: 3rem;
--space-14: 3.5rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
--space-28: 7rem;
--space-32: 8rem;
--space-36: 9rem;
--space-40: 10rem;
--space-44: 11rem;
--space-48: 12rem;
--space-52: 13rem;
--space-56: 14rem;
--space-60: 15rem;
--space-64: 16rem;
--space-72: 18rem;
--space-80: 20rem;
--space-96: 24rem;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
import { JSX } from "solid-js"
export function IconLogomark(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z"
fill="currentColor"
/>
</svg>
)
}
export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 220 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99208L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z"
fill="currentColor"
/>
<path
d="M45.628 28.432C42.556 28.432 40.252 27.688 38.716 26.2C37.204 24.688 36.448 22.552 36.448 19.792V12.628C36.448 9.84399 37.204 7.70799 38.716 6.21999C40.252 4.73199 42.556 3.98799 45.628 3.98799C48.7 3.98799 50.992 4.73199 52.504 6.21999C54.04 7.70799 54.808 9.84399 54.808 12.628V19.792C54.808 22.552 54.04 24.688 52.504 26.2C50.992 27.688 48.7 28.432 45.628 28.432ZM45.628 25.228C47.452 25.228 48.832 24.76 49.768 23.824C50.704 22.864 51.172 21.484 51.172 19.684V12.736C51.172 10.912 50.704 9.53199 49.768 8.59599C48.832 7.65999 47.452 7.19199 45.628 7.19199C43.828 7.19199 42.448 7.65999 41.488 8.59599C40.552 9.53199 40.084 10.912 40.084 12.736V19.684C40.084 21.484 40.552 22.864 41.488 23.824C42.448 24.76 43.828 25.228 45.628 25.228Z"
fill="currentColor"
/>
<path
d="M67.0294 28.432C66.1654 28.432 65.2414 28.312 64.2574 28.072C63.2734 27.856 62.4694 27.544 61.8454 27.136L61.7734 24.184C62.4214 24.544 63.1414 24.832 63.9334 25.048C64.7254 25.24 65.4574 25.336 66.1294 25.336C67.4254 25.336 68.3614 24.964 68.9374 24.22C69.5374 23.452 69.8374 22.312 69.8374 20.8V16.912C69.8374 15.592 69.6094 14.632 69.1534 14.032C68.6974 13.408 68.0014 13.096 67.0654 13.096C66.2734 13.096 65.4694 13.336 64.6534 13.816C63.8374 14.272 62.8174 15.064 61.5934 16.192L61.4854 13.168C62.2534 12.448 62.9854 11.848 63.6814 11.368C64.3774 10.888 65.0854 10.528 65.8054 10.288C66.5254 10.048 67.2934 9.92799 68.1094 9.92799C69.8614 9.92799 71.1814 10.468 72.0694 11.548C72.9814 12.628 73.4374 14.272 73.4374 16.48V21.196C73.4374 23.572 72.8854 25.372 71.7814 26.596C70.7014 27.82 69.1174 28.432 67.0294 28.432ZM59.1094 34.66C58.8454 34.66 58.7134 34.528 58.7134 34.264V14.536C58.7134 13.936 58.6894 13.3 58.6414 12.628C58.5934 11.956 58.5334 11.356 58.4614 10.828C58.4374 10.516 58.5694 10.36 58.8574 10.36H61.4134C61.6774 10.36 61.8214 10.48 61.8454 10.72C61.8934 10.912 61.9414 11.164 61.9894 11.476C62.0374 11.788 62.0734 12.1 62.0974 12.412C62.1214 12.7 62.1334 12.916 62.1334 13.06L62.3134 14.968V34.264C62.3134 34.528 62.1814 34.66 61.9174 34.66H59.1094Z"
fill="currentColor"
/>
<path
d="M84.0237 28.432C81.5757 28.432 79.7157 27.88 78.4437 26.776C77.1957 25.672 76.5717 24.028 76.5717 21.844V17.056C76.5717 14.728 77.1957 12.964 78.4437 11.764C79.6917 10.54 81.5397 9.92799 83.9877 9.92799C86.4117 9.92799 88.2477 10.516 89.4957 11.692C90.7437 12.868 91.3677 14.584 91.3677 16.84V19.648C91.3677 19.912 91.2477 20.044 91.0077 20.044H80.1717V21.196C80.1717 22.66 80.4837 23.728 81.1077 24.4C81.7317 25.072 82.7397 25.408 84.1317 25.408C85.1877 25.408 86.0037 25.24 86.5797 24.904C87.1557 24.568 87.4437 24.052 87.4437 23.356C87.4437 23.092 87.5877 22.96 87.8757 22.96H90.6477C90.8637 22.96 90.9957 23.08 91.0437 23.32C91.1397 24.928 90.5637 26.188 89.3157 27.1C88.0677 27.988 86.3037 28.432 84.0237 28.432ZM80.1717 17.308H87.8037V17.128C87.8037 15.688 87.4917 14.632 86.8677 13.96C86.2437 13.288 85.2957 12.952 84.0237 12.952C82.7277 12.952 81.7557 13.3 81.1077 13.996C80.4837 14.692 80.1717 15.736 80.1717 17.128V17.308Z"
fill="currentColor"
/>
<path
d="M106.647 28C106.383 28 106.251 27.868 106.251 27.604V16.3C106.251 15.196 106.023 14.392 105.567 13.888C105.135 13.36 104.451 13.096 103.515 13.096C102.627 13.096 101.751 13.36 100.887 13.888C100.023 14.392 98.9906 15.244 97.7906 16.444L97.7186 13.528C98.4866 12.712 99.2306 12.04 99.9506 11.512C100.671 10.984 101.403 10.588 102.147 10.324C102.915 10.06 103.719 9.92799 104.559 9.92799C106.287 9.92799 107.595 10.432 108.483 11.44C109.395 12.424 109.851 13.912 109.851 15.904V27.604C109.851 27.868 109.731 28 109.491 28H106.647ZM95.2346 28C94.9706 28 94.8386 27.868 94.8386 27.604V14.824C94.8386 14.152 94.8026 13.444 94.7306 12.7C94.6826 11.932 94.6346 11.32 94.5866 10.864C94.5386 10.528 94.6706 10.36 94.9826 10.36H97.5386C97.7786 10.36 97.9226 10.48 97.9706 10.72C98.0186 10.936 98.0666 11.236 98.1146 11.62C98.1626 12.004 98.2106 12.412 98.2586 12.844C98.3066 13.276 98.3306 13.648 98.3306 13.96L98.4386 15.112V27.604C98.4386 27.868 98.3066 28 98.0426 28H95.2346Z"
fill="currentColor"
/>
<path
d="M122.87 28.432C119.894 28.432 117.626 27.7 116.066 26.236C114.506 24.748 113.726 22.6 113.726 19.792V12.664C113.726 9.83199 114.506 7.68399 116.066 6.21999C117.65 4.73199 119.93 3.98799 122.906 3.98799C124.85 3.98799 126.542 4.31199 127.982 4.95999C129.422 5.60799 130.49 6.54399 131.186 7.76799C131.906 8.96799 132.146 10.408 131.906 12.088C131.882 12.232 131.846 12.352 131.798 12.448C131.75 12.544 131.654 12.592 131.51 12.592H128.63C128.366 12.592 128.246 12.46 128.27 12.196C128.342 10.612 127.922 9.37599 127.01 8.48799C126.098 7.59999 124.742 7.15599 122.942 7.15599C121.142 7.15599 119.762 7.62399 118.802 8.55999C117.842 9.49599 117.362 10.864 117.362 12.664V19.756C117.362 21.556 117.842 22.924 118.802 23.86C119.762 24.796 121.142 25.264 122.942 25.264C124.766 25.264 126.146 24.82 127.082 23.932C128.042 23.02 128.438 21.784 128.27 20.224C128.246 19.96 128.366 19.828 128.63 19.828H131.474C131.714 19.828 131.858 19.996 131.906 20.332C132.05 21.964 131.762 23.392 131.042 24.616C130.346 25.816 129.29 26.752 127.874 27.424C126.458 28.096 124.79 28.432 122.87 28.432Z"
fill="currentColor"
/>
<path
d="M142.558 28.432C140.086 28.432 138.19 27.832 136.87 26.632C135.55 25.432 134.89 23.62 134.89 21.196V17.164C134.89 14.74 135.538 12.928 136.834 11.728C138.154 10.528 140.062 9.92799 142.558 9.92799C145.03 9.92799 146.914 10.528 148.21 11.728C149.53 12.928 150.19 14.74 150.19 17.164V21.196C150.19 23.62 149.542 25.432 148.246 26.632C146.95 27.832 145.054 28.432 142.558 28.432ZM142.558 25.3C143.974 25.3 144.994 24.94 145.618 24.22C146.266 23.5 146.59 22.384 146.59 20.872V17.488C146.59 16 146.266 14.896 145.618 14.176C144.994 13.432 143.974 13.06 142.558 13.06C141.118 13.06 140.074 13.432 139.426 14.176C138.802 14.896 138.49 16 138.49 17.488V20.872C138.49 22.384 138.802 23.5 139.426 24.22C140.074 24.94 141.118 25.3 142.558 25.3Z"
fill="currentColor"
/>
<path
d="M165.493 28C165.229 28 165.097 27.868 165.097 27.604V16.3C165.097 15.196 164.869 14.392 164.413 13.888C163.981 13.36 163.297 13.096 162.361 13.096C161.473 13.096 160.597 13.36 159.733 13.888C158.869 14.392 157.837 15.244 156.637 16.444L156.565 13.528C157.333 12.712 158.077 12.04 158.797 11.512C159.517 10.984 160.249 10.588 160.993 10.324C161.761 10.06 162.565 9.92799 163.405 9.92799C165.133 9.92799 166.441 10.432 167.329 11.44C168.241 12.424 168.697 13.912 168.697 15.904V27.604C168.697 27.868 168.577 28 168.337 28H165.493ZM154.081 28C153.817 28 153.685 27.868 153.685 27.604V14.824C153.685 14.152 153.649 13.444 153.577 12.7C153.529 11.932 153.481 11.32 153.433 10.864C153.385 10.528 153.517 10.36 153.829 10.36H156.385C156.625 10.36 156.769 10.48 156.817 10.72C156.865 10.936 156.913 11.236 156.961 11.62C157.009 12.004 157.057 12.412 157.105 12.844C157.153 13.276 157.177 13.648 157.177 13.96L157.285 15.112V27.604C157.285 27.868 157.153 28 156.889 28H154.081Z"
fill="currentColor"
/>
<path
d="M179.477 28.432C178.157 28.432 177.113 28.228 176.345 27.82C175.577 27.388 175.013 26.74 174.653 25.876C174.317 24.988 174.149 23.884 174.149 22.564V13.42H171.413C171.173 13.42 171.053 13.288 171.053 13.024V10.756C171.053 10.492 171.173 10.36 171.413 10.36H174.257L174.689 6.03999C174.713 5.79999 174.857 5.67999 175.121 5.67999H177.317C177.581 5.67999 177.713 5.79999 177.713 6.03999L177.749 10.36H182.897C183.161 10.36 183.293 10.492 183.293 10.756V13.024C183.293 13.288 183.161 13.42 182.897 13.42H177.749V22.42C177.749 23.476 177.929 24.232 178.289 24.688C178.673 25.12 179.321 25.336 180.233 25.336C180.689 25.336 181.145 25.288 181.601 25.192C182.081 25.072 182.501 24.928 182.861 24.76C183.173 24.64 183.329 24.736 183.329 25.048V27.388C183.329 27.604 183.233 27.748 183.041 27.82C182.585 27.988 182.057 28.132 181.457 28.252C180.881 28.372 180.221 28.432 179.477 28.432Z"
fill="currentColor"
/>
<path
d="M186.233 28C185.969 28 185.837 27.868 185.837 27.604V14.716C185.837 14.02 185.813 13.36 185.765 12.736C185.741 12.088 185.681 11.44 185.585 10.792C185.537 10.504 185.669 10.36 185.981 10.36H188.537C188.801 10.36 188.945 10.48 188.969 10.72C189.065 11.152 189.137 11.656 189.185 12.232C189.233 12.808 189.257 13.3 189.257 13.708L189.437 15.688V27.604C189.437 27.868 189.305 28 189.041 28H186.233ZM189.041 17.02L188.897 13.744C189.401 13 189.977 12.352 190.625 11.8C191.273 11.224 191.933 10.768 192.605 10.432C193.277 10.096 193.901 9.92799 194.477 9.92799C194.885 9.92799 195.209 9.96399 195.449 10.036C195.641 10.108 195.749 10.252 195.773 10.468C195.821 10.996 195.833 11.56 195.809 12.16C195.809 12.736 195.773 13.288 195.701 13.816C195.653 14.104 195.485 14.212 195.197 14.14C195.029 14.092 194.837 14.056 194.621 14.032C194.405 14.008 194.177 13.996 193.937 13.996C193.361 13.996 192.785 14.128 192.209 14.392C191.633 14.632 191.069 14.98 190.517 15.436C189.989 15.868 189.497 16.396 189.041 17.02Z"
fill="currentColor"
/>
<path
d="M204.648 28.432C202.176 28.432 200.28 27.832 198.96 26.632C197.64 25.432 196.98 23.62 196.98 21.196V17.164C196.98 14.74 197.628 12.928 198.924 11.728C200.244 10.528 202.152 9.92799 204.648 9.92799C207.12 9.92799 209.004 10.528 210.3 11.728C211.62 12.928 212.28 14.74 212.28 17.164V21.196C212.28 23.62 211.632 25.432 210.336 26.632C209.04 27.832 207.144 28.432 204.648 28.432ZM204.648 25.3C206.064 25.3 207.084 24.94 207.708 24.22C208.356 23.5 208.68 22.384 208.68 20.872V17.488C208.68 16 208.356 14.896 207.708 14.176C207.084 13.432 206.064 13.06 204.648 13.06C203.208 13.06 202.164 13.432 201.516 14.176C200.892 14.896 200.58 16 200.58 17.488V20.872C200.58 22.384 200.892 23.5 201.516 24.22C202.164 24.94 203.208 25.3 204.648 25.3Z"
fill="currentColor"
/>
<path
d="M216.171 28C215.907 28 215.775 27.868 215.775 27.604V3.80799C215.775 3.54399 215.907 3.41199 216.171 3.41199H218.979C219.243 3.41199 219.375 3.54399 219.375 3.80799V27.604C219.375 27.868 219.243 28 218.979 28H216.171Z"
fill="currentColor"
/>
</svg>
)
}

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