Compare commits

..

235 Commits

Author SHA1 Message Date
Github Action
7962f774ad Update Nix flake.lock and hashes 2025-12-08 00:04:47 +00:00
GitHub Action
7d00a49c2f chore: regen sdk 2025-12-08 00:04:01 +00:00
Dax Raad
c35c15a3ac sync 2025-12-07 19:03:20 -05:00
Dax Raad
11840191f8 sync 2025-12-07 18:58:10 -05:00
Dax Raad
9672a7b056 core: test pre-release version of openapi-ts with updated parameter handling 2025-12-07 18:38:36 -05:00
Dax Raad
66a34798a4 sdk v2 2025-12-07 18:35:50 -05:00
GitHub Action
6667856ba5 chore: format code 2025-12-07 20:53:12 +00:00
Dax Raad
13b2cf50ae remove outdated SDKs 2025-12-07 15:52:27 -05:00
Github Action
93b0abfce9 Update Nix flake.lock and hashes 2025-12-07 20:49:39 +00:00
GitHub Action
f7e4c47113 chore: regen sdk 2025-12-07 20:47:51 +00:00
André Cruz
509e43d6f8 feat(mcp): add OAuth authentication support for remote MCP servers (#5014) 2025-12-07 15:47:27 -05:00
GitHub Action
e693192e06 chore: regen sdk 2025-12-07 19:23:56 +00:00
rari404
ec27759f90 feat: add uninstall command (#5208) 2025-12-07 13:23:30 -06:00
GitHub Action
9c938eec73 chore: format code 2025-12-07 19:08:48 +00:00
secretninjaman
238b907dd8 fix: use basename for shell detection to support non-standard paths (#5205)
Co-authored-by: Ayato French <a@ayatofrench.com>
2025-12-07 13:08:26 -06:00
GitHub Action
c16d8c6db8 chore: format code 2025-12-07 19:06:37 +00:00
Aiden Cline
9856e3b798 ignore: add test for provider url case 2025-12-07 13:06:12 -06:00
GitHub Action
1d089272c8 chore: regen sdk 2025-12-07 19:01:34 +00:00
Aiden Cline
c30b1130ee fix: provider url merging logic 2025-12-07 13:01:05 -06:00
GitHub Action
40ca222d09 chore: format code 2025-12-07 18:48:45 +00:00
Dax Raad
0ecccbfd17 enable zoom hotkeys 2025-12-07 13:48:12 -05:00
GitHub Action
3f4862ced6 chore: regen sdk 2025-12-07 18:42:48 +00:00
Brendan Allan
1574e2457b Desktop macOS codesigning and notarization (#5154)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-07 13:42:23 -05:00
GitHub Action
af33212f77 chore: format code 2025-12-07 18:26:05 +00:00
Mikheil Berishvili
4eb82e8c04 fix: autocomplete popup repositions on window resize (#5196) 2025-12-07 12:25:35 -06:00
GitHub Action
a45f0aac90 chore: regen sdk 2025-12-07 18:18:10 +00:00
Aiden Cline
eb4afdca65 ci: fix format 2025-12-07 12:17:40 -06:00
GitHub Action
9391749577 chore: format code 2025-12-07 15:33:51 +00:00
Carsten Kragelund Jørgensen
36a25660e9 fix: update zed agent server linux url to tar.gz (#5194) 2025-12-07 09:33:23 -06:00
GitHub Action
d9fe722da1 ignore: update download stats 2025-12-07 2025-12-07 12:04:11 +00:00
GitHub Action
da722e7db9 chore: regen sdk 2025-12-07 05:55:31 +00:00
Aiden Cline
75a4dcbce8 tweak: make bash give agent more awareness of cwd, bump default timeout, drop max timeout (#5140) 2025-12-06 23:55:07 -06:00
GitHub Action
3a179fcd34 chore: format code 2025-12-07 05:45:36 +00:00
Arindam Majumder
ad22fe9fe7 docs: Nebius Token Factory provider documentation (#2997) 2025-12-06 23:45:08 -06:00
GitHub Action
6d622d91be chore: regen sdk 2025-12-07 05:33:26 +00:00
Aiden Cline
aa884b003e core: prevent sessions from disappearing after git init
Previously, sessions created in a non-git directory would disappear from
the session picker after running git init and making the first commit.
This happened because the migration logic ran prematurely before a stable
project ID existed.
2025-12-06 23:32:47 -06:00
GitHub Action
e0f77940f9 chore: format code 2025-12-06 23:25:16 +00:00
Aiden Cline
3a1718f62b ci: fix review 2025-12-06 17:24:44 -06:00
GitHub Action
f7f9d3e5b9 chore: regen sdk 2025-12-06 23:23:24 +00:00
Dax Raad
68501d7799 ci 2025-12-06 18:22:55 -05:00
GitHub Action
f8a987b135 chore: format code 2025-12-06 23:22:04 +00:00
Dax Raad
dfea6780d9 sync 2025-12-06 18:21:32 -05:00
GitHub Action
2ac8dd6361 chore: regen sdk 2025-12-06 23:19:47 +00:00
Dax Raad
dd0945b9ca tui: add visual separator between username and timestamp for better readability 2025-12-06 18:19:18 -05:00
Dax Raad
1b05d5dd8e tui: prevent deprecated models from appearing in model picker 2025-12-06 18:18:45 -05:00
GitHub Action
6923cc4a6a chore: format code 2025-12-06 23:15:39 +00:00
Aiden Cline
3feb69e63c ci: fix format 2025-12-06 17:15:04 -06:00
GitHub Action
6723792fbb chore: regen sdk 2025-12-06 22:05:07 +00:00
Aiden Cline
3e36069f41 fix: reduce overhead of task tool metadata 2025-12-06 16:04:33 -06:00
GitHub Action
6a4ca92a6c chore: format code 2025-12-06 20:49:33 +00:00
Saatvik Arya
3ec34ee3dd feat(tui): add dynamic terminal window title (#5112) 2025-12-06 14:49:11 -06:00
GitHub Action
2e5c2d5e98 chore: regen sdk 2025-12-06 20:48:46 +00:00
Anchit Bajaj
4dbe17d4f1 fix: update description to lowercase for ACP command (to be consistent with other commands) (#5137) 2025-12-06 14:48:21 -06:00
GitHub Action
f18776cb49 chore: format code 2025-12-06 20:43:15 +00:00
Ariane Emory
429aa24275 fix: make timestamp toggle text dynamic in command list (resolves #5106) (#5108) 2025-12-06 14:42:46 -06:00
GitHub Action
741c9d3c63 chore: regen sdk 2025-12-06 20:30:00 +00:00
Ben Vargas
419983c0f1 feat: restore experimental flag for websearch/codesearch tools (#5132)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-06 14:29:34 -06:00
GitHub Action
a59c80e076 chore: format code 2025-12-06 20:28:25 +00:00
Cody Rayment
55981205da docs: update server API reference with complete endpoint list (#5136) 2025-12-06 14:28:02 -06:00
Dak Washbrook
c1e6037bda feat: add mercury.com theme (#5141) 2025-12-06 14:27:56 -06:00
Github Action
8e816a9283 Update Nix flake.lock and hashes 2025-12-06 12:45:14 +00:00
GitHub Action
d165c6b15a chore: regen sdk 2025-12-06 12:44:22 +00:00
Adam
9111005165 fix: terminal serialization and isolation 2025-12-06 06:43:53 -06:00
GitHub Action
3ceac25fb5 ignore: update download stats 2025-12-06 2025-12-06 12:04:12 +00:00
GitHub Action
8ec771d5be chore: format code 2025-12-06 11:53:20 +00:00
Adam
659e5653bc fix: exclude dist 2025-12-06 05:52:47 -06:00
Adam
3ff6de261c chore: update tauri update pub key 2025-12-06 05:49:05 -06:00
GitHub Action
25dae77fcd chore: regen sdk 2025-12-06 01:21:17 +00:00
Dax Raad
68daadcb56 sync 2025-12-05 20:20:47 -05:00
GitHub Action
c9b1bb0285 chore: format code 2025-12-06 01:14:58 +00:00
Dax
ac5809e757 Documentation edits made through Mintlify web editor 2025-12-05 20:14:29 -05:00
Dax
0db209a636 Documentation edits made through Mintlify web editor 2025-12-05 20:14:10 -05:00
GitHub Action
6c65f4acd1 chore: regen sdk 2025-12-06 01:13:23 +00:00
Dax Raad
3d447e8b12 ci 2025-12-05 20:12:52 -05:00
Dax Raad
f8807144d4 openapi route 2025-12-05 20:11:01 -05:00
GitHub Action
ebb4c8a724 chore: format code 2025-12-06 01:09:36 +00:00
Dax Raad
893f232b2f openapi json 2025-12-05 20:07:24 -05:00
GitHub Action
a6aaf5429c chore: format code 2025-12-06 01:02:41 +00:00
Dax Raad
4ef239a086 openapi generate 2025-12-05 20:01:55 -05:00
David Hill
9362368fd3 fix: add extra line break to install page 2025-12-05 23:43:26 +00:00
Adam
b35e010e2a feat: consistent (updated) social share images 2025-12-05 15:42:19 -06:00
GitHub Action
cc35e6a019 chore: format code 2025-12-05 21:42:15 +00:00
Dax Raad
3281888160 ignore: docs test 2025-12-05 16:41:35 -05:00
GitHub Action
c6d0ae892e chore: regen sdk 2025-12-05 21:02:17 +00:00
Aiden Cline
df67ae9cbe ci: regen sdk instead of failing tests 2025-12-05 15:01:41 -06:00
GitHub Action
ebe20efb29 chore: format code 2025-12-05 20:50:13 +00:00
franlol
b03b9b9017 feat: add optional scrollbar to the session chat (#5116)
Co-authored-by: Sebastian <hasta84@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 14:49:05 -06:00
opencode
73258c6193 release: v1.0.134 2025-12-05 20:46:49 +00:00
Github Action
dfe3fb8ed3 Update Nix flake.lock and hashes 2025-12-05 20:41:00 +00:00
Brendan Allan
cd6bfb3f69 OpenCode Desktop app (#5044)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-05 14:39:49 -06:00
Shantur Rathore
ba417d80b1 tweak: bash tool improve output metadata for agent consumption, fix small timeout issue (#5131) 2025-12-05 13:56:56 -06:00
Nathan Thomas
40eb8b93e1 feat: add max steps for supervisor and sub-agents (#4062)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 12:26:44 -06:00
Adam
6e6bd1e171 fix(desktop): terminal cursor position 2025-12-05 12:05:30 -06:00
Adam
81ee2d2332 fix(desktop): prompting 2025-12-05 10:51:35 -06:00
Aiden Cline
85974e9acd ignore: regen sdk 2025-12-05 10:50:14 -06:00
Noam Bressler
864c098701 add experimental.open_telemetry config option to enable OTEL spans (#4978)
Co-authored-by: noamzbr <noamzbr@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 10:48:22 -06:00
Adam
cfbaf81ef8 fix(desktop): clone pty session on reconnect 2025-12-05 10:30:48 -06:00
Adam
87a791fdb9 fix(desktop): new session not selecting tab 2025-12-05 10:30:48 -06:00
Anthony Shew
ada7cca10d feat(theme): Vercel (#5119)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-05 10:01:20 -06:00
Julian Visser
52db0f23a6 fix: #5064 ProviderInitError github-copilot-enterprise (#5123) 2025-12-05 09:53:32 -06:00
Dax Raad
60388f7f03 do not use required pty for local dev 2025-12-05 10:39:44 -05:00
Frank
53ed1c912b Zen: add codex max 2025-12-05 09:05:00 -05:00
GitHub Action
5f7ab83de4 ignore: update download stats 2025-12-05 2025-12-05 12:04:29 +00:00
Aiden Cline
05d2f70529 ignore: tweak 2025-12-05 01:00:47 -06:00
Aiden Cline
f950de95ba fix: ensure projects that go from having no commits to having commits have sessions migrated (#5105)
Co-authored-by: GitHub Action <action@github.com>
2025-12-05 00:49:07 -06:00
Aiden Cline
a4e5a72c36 ci: keybinds 2025-12-05 00:22:28 -06:00
ry2009
03324d4277 tui: wrap dialog option descriptions (#5083) 2025-12-05 00:19:48 -06:00
Aiden Cline
e53580cb68 ignore: cmd tweak 2025-12-05 00:11:47 -06:00
GitHub Action
332ebe36c3 chore: format code 2025-12-05 05:14:21 +00:00
Aiden Cline
5013d64b28 ignore: rm slop commnand (only for opencode repo this isnt shipping) 2025-12-04 23:13:41 -06:00
Aiden Cline
767a81f930 fix: ensure that vcs is still set to git even if no commits in repo 2025-12-04 23:02:11 -06:00
Aiden Cline
78046dac8b ci: review 2025-12-04 23:02:11 -06:00
Dax Raad
95168b8267 increase default scroll speed 2025-12-04 23:54:46 -05:00
Dax Raad
c264e9c364 fix 2025-12-04 23:48:32 -05:00
Dax Raad
856e1e2948 fix pty builds 2025-12-04 23:47:57 -05:00
Jérôme Benoit
bef4fdfc4b fix: add getModel to SAP AI Core provider for correct SDK initialization (#5086)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2025-12-04 22:43:22 -06:00
opencode-agent[bot]
095a1ab041 docs: llama.cpp docs: limit moved under model (#5089)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-12-04 22:39:13 -06:00
Dax Raad
71e578eac9 ignore: fix provider credentials query for BYOK
Provider credentials field was being selected from ProviderTable even when the table wasn't joined (when byokProvider was undefined). Now the join is conditional - when byokProvider exists, we join and get the credentials; when it doesn't, the join condition is always false so provider remains null.
2025-12-04 22:57:39 -05:00
Frank
4380727727 zen: fix byok 2025-12-04 21:53:33 -05:00
Github Action
392d46933b Update Nix flake.lock and hashes 2025-12-05 02:33:39 +00:00
GitHub Action
2bc0b46ff4 chore: format code 2025-12-05 02:33:01 +00:00
Adam
09f522f0aa Reapply "feat(desktop): terminal pane (#5081)"
This reverts commit f9dcd97936.
2025-12-04 20:32:08 -06:00
Github Action
d82bd430f6 Update Nix flake.lock and hashes 2025-12-04 22:02:17 +00:00
opencode
49800a00bd release: v1.0.133 2025-12-04 22:02:17 +00:00
Aiden Cline
f9dcd97936 Revert "feat(desktop): terminal pane (#5081)"
This reverts commit d763c11a6d.
2025-12-04 15:57:01 -06:00
Adam
d763c11a6d feat(desktop): terminal pane (#5081)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-04 15:37:29 -06:00
Dax Raad
b1202ac6db core: add test for custom model npm package inheritance 2025-12-04 16:30:54 -05:00
Aiden Cline
d469d7d441 tweak: bash tool description re commit stuff 2025-12-04 15:27:23 -06:00
Cason Adams
48dc520fb8 docs: add CodeCompanion.nvim integration instructions (#5079) 2025-12-04 14:49:51 -06:00
Dax Raad
668d5a76d5 core: ensure model npm package falls back to dev models config when not explicitly defined 2025-12-04 15:39:52 -05:00
Jérôme Benoit
b9c1f10016 feat: Add SAP AI Core provider support (#5023)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
2025-12-04 14:07:23 -06:00
Aiden Cline
8a0c86cbdb bump: builtin plugin versions 2025-12-04 12:37:14 -06:00
Daniel Polito
7f86fe3f61 add optional prompt Input to Github Action (#4828)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-04 12:10:56 -06:00
Aiden Cline
a32cf70d7e tui: fix /new slash command being persisted in prompt input 2025-12-04 12:01:13 -06:00
Shantur Rathore
a607f33552 tweak: bash tool messages regarding timeouts and truncation more clear for agent (#5066) 2025-12-04 11:33:00 -06:00
Aiden Cline
350a32274a fix: model not being passed correctly to tool 2025-12-04 11:15:30 -06:00
Daniel Gray
27c99b46cb Preserve prompt input when creating new session (#4993) 2025-12-04 11:12:58 -06:00
Adam
1d6e3d477b fix(tui): cursor color 2025-12-04 06:56:48 -06:00
GitHub Action
efbb973393 ignore: update download stats 2025-12-04 2025-12-04 12:04:38 +00:00
Aiden Cline
45bc7a6a9d ci: cleaner 2025-12-04 01:14:09 -06:00
Aiden Cline
088ebb967f ci: only maintainer can trigger 2025-12-04 01:07:04 -06:00
Frank
bcf740f98a zen: make session provider sticky 2025-12-03 23:33:46 -05:00
wsx99outlook
6b80fff2bb ci: use blacksmith runners in review workflow too (#5042) 2025-12-03 22:30:00 -06:00
GitHub Action
2e63fedb76 chore: format code 2025-12-04 04:29:03 +00:00
YeonGyu-Kim
5a9f4e5c60 fix: ensure checkUpgrade sets init: (#5040) 2025-12-03 22:28:35 -06:00
opencode
d0a48a09e2 release: v1.0.132 2025-12-04 04:23:39 +00:00
GitHub Action
c0a21e7025 chore: format code 2025-12-04 04:19:15 +00:00
Dax Raad
10cc15aabe fix anthropic api key error 2025-12-03 23:18:24 -05:00
opencode
adf7681100 release: v1.0.131 2025-12-04 04:11:48 +00:00
Aiden Cline
0237905b96 fix: TypeError: undefined is not an object 2025-12-03 22:03:42 -06:00
GitHub Action
e8aa79bab6 chore: format code 2025-12-04 03:42:08 +00:00
Frank
4ff5783e59 zen: fix chart loading 2025-12-03 22:41:31 -05:00
opencode
dcfeb52983 release: v1.0.130 2025-12-04 03:38:18 +00:00
Jakub Matjanowski
46790e57e9 feat: Enhance DeepSeek reasoning content handling (#4975)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-03 21:31:36 -06:00
Jack Bisceglia
4bc3fa0826 docs: remove outdated theme section as system theme is now added back (#5041) 2025-12-03 21:10:26 -06:00
Aiden Cline
88cfb979be ci: add note about iife 2025-12-03 21:08:12 -06:00
Aiden Cline
32b5db754e fix: provider id issue 2025-12-03 20:45:55 -06:00
Aiden Cline
f33f8ca109 fix: compaction type issue 2025-12-03 20:43:47 -06:00
Aiden Cline
598d63db63 fix: dax typo 2025-12-03 20:39:11 -06:00
Github Action
38bff1b372 Update Nix flake.lock and hashes 2025-12-04 02:34:26 +00:00
Aiden Cline
e8c9b21f20 bump opentui 2025-12-03 20:33:08 -06:00
Dax
6d3fc63658 core: refactor provider and model system (#5033)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: thdxr <thdxr@users.noreply.github.com>
2025-12-03 21:09:03 -05:00
Dax Raad
ee4437ff32 core: add provider test coverage for upcoming refactor
Add comprehensive test suite for Provider module to ensure safe
refactoring of provider internals. Tests cover:
- Provider loading from env vars and config
- Provider filtering (disabled_providers, enabled_providers)
- Model whitelist/blacklist
- Model aliasing and custom providers
- getModel, getProvider, closest, defaultModel functions

Also adds Env module for instance-scoped environment variable access,
enabling isolated test environments without global state pollution.
2025-12-03 18:30:42 -05:00
Frank
7a4aa68706 zen: fix chart loading
closes #5030
2025-12-03 18:12:28 -05:00
Aiden Cline
f00380d285 ci: review tweak 2025-12-03 16:16:08 -06:00
Ariane Emory
c00d4885c6 feat: add tool_details keybind w/ no default (#4976)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-03 13:38:09 -06:00
Aiden Cline
70f4722356 ci: review ready for review action 2025-12-03 13:35:49 -06:00
Aiden Cline
8898bf7ca4 ci: tweak review cmd 2025-12-03 13:34:45 -06:00
Frank
e5b13b767e zen: usage graph respect light/dark mode 2025-12-03 14:24:44 -05:00
Aiden Cline
3181c68cbb ci: make review only fire on non draft pr creation 2025-12-03 13:10:40 -06:00
Aiden Cline
c3c9003dbb ci: add pr review 2025-12-03 12:45:06 -06:00
Ariane Emory
921b98066d feat: add messages_last_user command to scroll TUI to last user message (implements #4847) (#4855)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-03 12:30:11 -06:00
Luke Parker
c5b4cc80cc fix: bunfs path on windows (#5011) 2025-12-03 11:21:13 -06:00
Spoon
0bccd1d578 feat: experimental.primary_tools, allow user to set the tools that should only be available to primary agents (#4913)
Co-authored-by: GitHub Action <action@github.com>
2025-12-03 11:19:43 -06:00
Aiden Cline
91db82c138 add retry case for grok resource exhausted 2025-12-03 11:16:27 -06:00
GitHub Action
0eb97086fc chore: format code 2025-12-03 16:47:25 +00:00
Aiden Cline
5b34636afa ignore: docs & style 2025-12-03 10:46:48 -06:00
GitHub Action
f1138b9a55 ignore: update download stats 2025-12-03 2025-12-03 12:04:36 +00:00
opencode
23ff6dbba4 release: v1.0.129 2025-12-03 11:39:04 +00:00
Aiden Cline
b457923970 core: fix GitHub Copilot Enterprise authentication failing with sdk.chat undefined error 2025-12-02 23:37:10 -06:00
Jason Cheatham
66e4a5a64e tweak: adjust light/dark theme toggle (#5007) 2025-12-02 23:03:24 -06:00
Ben Vargas
6c25e64658 fix: correct Provider type in chat.params plugin hook (#5003) 2025-12-02 22:50:21 -06:00
Jason Cheatham
f2fd0f8f00 fix: handle 0 in ANSI theme color definitions (#5009) 2025-12-02 22:42:30 -06:00
Frank
44cdde5422 zen: fix removing provider 2025-12-02 21:52:12 -05:00
Github Action
88235dc618 Update Nix flake.lock and hashes 2025-12-03 02:17:49 +00:00
Sebastian Herrlinger
4d2b671d7b actually bump opentui to v0.1.55
- fix scrollbox empty/blank last items at bottom
- fix should not insert chars with modifiers in input/textarea anymore
- do not wrap OSC4 palette sequences for tmux 3.6
2025-12-03 03:16:32 +01:00
Sebastian Herrlinger
8098031eac Revert "bump opentui to v0.1.55"
This reverts commit 80636fec43.
2025-12-03 03:14:08 +01:00
Github Action
74c882d9d0 Update Nix flake.lock and hashes 2025-12-03 02:06:35 +00:00
Sebastian Herrlinger
80636fec43 bump opentui to v0.1.55
- fix scrollbox empty/blank last items at bottom
- fix should not insert chars with modifiers in input/textarea anymore
- do not wrap OSC4 palette sequences for tmux 3.6
2025-12-03 03:03:52 +01:00
Aiden Cline
a8ad74aef3 add basic session list command 2025-12-02 19:24:05 -06:00
Aiden Cline
e2e2b7934e Make homebrew update check use homebrew registry version info 2025-12-02 17:43:33 -06:00
Frank
28c802f399 wip: zen 2025-12-02 18:36:15 -05:00
Dalton Alexandre
bcfa63aa4e fix: allow unignoring files in .ignore (#4814) 2025-12-02 17:15:12 -06:00
Jay V
d40feafb01 ignore: commands 2025-12-02 18:09:16 -05:00
Jay V
2a8473891b docs: replace deprecated opencode auth login command with /connect across all documentation
Users no longer need to exit the TUI to add providers - they can now use the /connect command directly in the terminal interface. Updated all provider setup instructions to use simplified format with /connect command instead of the deprecated opencode auth login CLI command. Added /connect to TUI commands reference and streamlined provider documentation to show clearer, more concise setup steps.
2025-12-02 18:08:39 -05:00
Aiden Cline
a4e3451d5c tweak: make message border match color of agent it was sent to 2025-12-02 16:59:31 -06:00
Aiden Cline
53a7c2885b bump default lsp server timeout 2025-12-02 16:04:08 -06:00
Adam
f354507d42 fix: session turn margins 2025-12-02 15:50:24 -06:00
Jaga Santagostino
f17e1def32 toggle to hide username in TUI (#4750)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-02 15:11:03 -06:00
GitHub Action
3183e8b7d4 chore: format code 2025-12-02 20:28:17 +00:00
Aiden Cline
9d2b9ef2d4 ci: dont forget our european designer 2025-12-02 14:27:41 -06:00
opencode-agent[bot]
733e5cd876 add OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT (#4996)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-02 20:14:23 +00:00
opencode
4ee4f7bcb3 release: v1.0.128 2025-12-02 20:14:22 +00:00
GitHub Action
da7ecda9ea chore: format code 2025-12-02 19:52:47 +00:00
Frank
1f11d4fb1a zen: data dumper 2025-12-02 14:51:27 -05:00
Adam
b308503ab5 chore: remove comment (redeploy) 2025-12-02 13:49:48 -06:00
Adam
3b10de4a28 fix: vite config 2025-12-02 13:08:59 -06:00
U Cirello
6ce1de476a fix(run): allow messages to start with dash (-) (#4904) 2025-12-02 12:52:05 -06:00
Aiden Cline
d9b0848a61 tweak: hide [REDACTED] chunks 2025-12-02 12:29:20 -06:00
Adam
46dd3b8166 chore: update landing page stats 2025-12-02 11:59:37 -06:00
Adam
eca07be072 chore: update landing page stats 2025-12-02 11:52:53 -06:00
GitHub Action
165d57b88e chore: format code 2025-12-02 17:07:27 +00:00
David Hill
28c44f7e5a Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-02 17:06:54 +00:00
David Hill
39d5bdff4b fix: add docs button 2025-12-02 17:06:37 +00:00
GitHub Action
b9f8480b2f chore: format code 2025-12-02 17:02:39 +00:00
David Hill
58b30d678a Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-02 17:00:45 +00:00
David Hill
408cdaf5e0 fix: website hero copy 2025-12-02 17:00:40 +00:00
GitHub Action
cae23cde09 chore: format code 2025-12-02 16:45:56 +00:00
Dalton Alexandre
702fb2562c fix: handle ANSI color indexes in theme resolution (#4842) 2025-12-02 10:45:22 -06:00
opencode
785d0b60b6 release: v1.0.127 2025-12-02 16:05:59 +00:00
Adam
67ab9dc4d0 fix: share page ssr 2025-12-02 07:08:25 -06:00
Github Action
77494cb7df Update Nix flake.lock and hashes 2025-12-02 12:52:28 +00:00
GitHub Action
08f8856d6c chore: format code 2025-12-02 12:51:37 +00:00
Adam
79a4c1544d fix: type error 2025-12-02 06:51:05 -06:00
Adam
c0a35141e6 feat: better code and diff rendering performance 2025-12-02 06:50:21 -06:00
GitHub Action
221bb64aeb ignore: update download stats 2025-12-02 2025-12-02 12:04:35 +00:00
David Hill
2555dba188 fix: update the install copy 2025-12-02 11:37:41 +00:00
Aiden Cline
6355ed6ae7 feat: add overridable review slash command (#4973) 2025-12-02 00:18:58 -06:00
Ariane Emory
1864e8c863 feat: toggle tool details visibility (resolves #4824) (#4882)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-01 19:38:44 -06:00
Adam
61d0d66dae fix: install orange 2025-12-01 19:21:55 -06:00
Aiden Cline
86522f1b3e fix: tui crash when no authed providers and default provider disabled (#4964) 2025-12-01 18:35:40 -06:00
Frank
dc32705bc9 zen: remove unnecessary transactions 2025-12-01 18:33:32 -05:00
Stephen Collings
1eaf5c31d3 fix(auth): Respect disabled/enabled providers config in auth login (#4940)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-01 17:01:10 -06:00
Aiden Cline
677b19e22e fix: add .quiet 2025-12-01 16:59:32 -06:00
opencode-agent[bot]
8e248ae045 fix: respect npm registry (#4958)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-01 16:58:39 -06:00
656 changed files with 41179 additions and 43425 deletions

View File

@@ -1,57 +0,0 @@
#
# This file is intentionally in the wrong dir, will move and add later....
#
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

@@ -55,4 +55,7 @@ jobs:
Feel free to ignore if none of these address your specific case.'
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
If no clear duplicates are found, do not comment."

View File

@@ -27,3 +27,5 @@ jobs:
./script/format.ts
env:
CI: true
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF_NAME: ${{ github.ref_name }}

View File

@@ -26,6 +26,7 @@ permissions:
jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
steps:
- uses: actions/checkout@v3
with:
@@ -79,3 +80,101 @@ jobs:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
publish-tauri:
strategy:
matrix:
settings:
- host: macos-latest
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-msvc
- host: ubuntu-24.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
with:
keychain: build
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Verify Certificate
if: ${{ runner.os == 'macOS' }}
run: |
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
echo "Certificate imported."
- name: Setup Apple API Key
if: ${{ runner.os == 'macOS' }}
run: |
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- run: git fetch --force --tags
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
if: startsWith(matrix.settings.host, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: packages/tauri/src-tauri
shared-key: ${{ matrix.settings.target }}
- name: Prepare
run: |
cd packages/tauri
bun ./scripts/prepare.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_CHANNEL: latest
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
if: startsWith(matrix.settings.host, 'ubuntu')
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
with:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
# releaseId: TODO

79
.github/workflows/review.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Guidelines Check
on:
issue_comment:
types: [created]
jobs:
check-guidelines:
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: write
steps:
- name: Get PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
fi
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Get PR details
id: pr-details
run: |
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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" } }'
PR_TITLE: ${{ steps.pr-details.outputs.title }}
run: |
PR_BODY=$(jq -r .body pr_data.json)
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
<pr-number>
${{ steps.pr-number.outputs.number }}
</pr-number>
<pr-description>
$PR_BODY
</pr-description>
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. 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
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
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/${{ steps.pr-number.outputs.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
\`\`\`
Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."

39
.github/workflows/sdk.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: sdk
on:
push:
branches-ignore:
- production
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
format:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: run
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
if [ -z "$(git status --porcelain)" ]; then
echo "No changes to commit"
exit 0
fi
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add -A
git commit -m "chore: regen sdk"
git push --no-verify
env:
CI: true

View File

@@ -28,9 +28,3 @@ jobs:
bun turbo test
env:
CI: true
- name: Check SDK is up to date
run: |
bun ./packages/sdk/js/script/build.ts
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
continue-on-error: false

2
.gitignore vendored
View File

@@ -6,7 +6,6 @@ node_modules
.idea
.vscode
*~
openapi.json
playground
tmp
dist
@@ -18,3 +17,4 @@ refs
Session.vim
opencode.json
a.out
target

View File

@@ -1,5 +1,5 @@
---
description: Git commit and push
description: git commit and push
---
commit and push

View File

@@ -1,8 +0,0 @@
---
description: hello world iaosd ioasjdoiasjd oisadjoisajd osiajd oisaj dosaij dsoajsajdaijdoisa jdoias jdoias jdoia jois jo jdois jdoias jdoias j djoasdj
---
hey there $ARGUMENTS
!`ls`
check out @README.md

View File

@@ -1,5 +1,5 @@
---
description: "Find issue(s) on github"
description: "find issue(s) on github"
model: opencode/claude-haiku-4-5
---

View File

@@ -0,0 +1,15 @@
---
description: Remove AI code slop
---
Check the diff against dev, and remove all AI generated slop introduced in this branch.
This includes:
- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
- Casts to any to get around type issues
- Any other style that is inconsistent with the file
- Unnecessary emoji usage
Report at the end with only a 1-3 sentence summary of what you changed

View File

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

View File

@@ -1,9 +1,10 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
"enterprise": {
"url": "https://enterprise.dev.opencode.ai",
},
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
"instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
"options": {

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
sst-env.d.ts

View File

@@ -1,16 +1,3 @@
## IMPORTANT
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()
## Debugging
- To test opencode in the `packages/opencode` directory you can run `bun dev`

View File

@@ -42,6 +42,8 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
Please try to follow the [style guide](./STYLE_GUIDE.md)
### Setting up a Debugger
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.

View File

@@ -157,3 +157,9 @@
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |

12
STYLE_GUIDE.md Normal file
View File

@@ -0,0 +1,12 @@
## Style Guide
- Try to keep things in one function unless composable or reusable
- DO NOT do unnecessary destructuring of variables
- DO NOT use `else` statements unless necessary
- DO NOT use `try`/`catch` if it can be avoided
- AVOID `try`/`catch` where possible
- AVOID `else` statements
- AVOID using `any` type
- AVOID `let` statements
- PREFER single word variable names where possible
- Use as many bun apis as possible like Bun.file()

397
bun.lock

File diff suppressed because it is too large Load Diff

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1764587062,
"narHash": "sha256-hdFa0TAVQAQLDF31cEW3enWmBP+b592OvHs6WVe3D8k=",
"lastModified": 1764947035,
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c1cb7d097cb250f6e1904aacd5f2ba5ffd8a49ce",
"rev": "a672be65651c80d3f592a89b3945466584a22069",
"type": "github"
},
"original": {

View File

@@ -13,6 +13,10 @@ inputs:
description: "Share the opencode session (defaults to true for public repos)"
required: false
prompt:
description: "Custom prompt to override the default prompt"
required: false
runs:
using: "composite"
steps:
@@ -27,3 +31,4 @@ runs:
env:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}

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

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

View File

@@ -116,7 +116,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
// CONSOLE
////////////////
const bucket = new sst.cloudflare.Bucket("ConsoleData")
const bucket = new sst.cloudflare.Bucket("ZenData")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")

10
install
View File

@@ -4,7 +4,7 @@ APP=opencode
MUTED='\033[0;2m'
RED='\033[0;31m'
ORANGE='\033[38;2;255;140;0m'
ORANGE='\033[38;5;214m'
NC='\033[0m' # No Color
requested_version=${VERSION:-}
@@ -353,10 +353,10 @@ echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
echo -e ""
echo -e ""
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
echo -e "opencode ${MUTED}Use free models${NC}"
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
echo -e "opencode help ${MUTED}List commands and options${NC}"
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
echo -e ""
echo -e "cd <project> ${MUTED}# Open directory${NC}"
echo -e "opencode ${MUTED}# Run command${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-9BfJ3dFq/UYyhsnK3Sfx6rb6CT8bCvFOFOqD2+W1WQE="
"nodeModules": "sha256-7ItLfqYrXzC6LO2iXZ8m+ZfQH1D7NWtcAcgRMO5NXZI="
}

View File

@@ -30,16 +30,16 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.5.7",
"@pierre/precision-diffs": "0.6.0-beta.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
"hono": "4.7.10",
"hono-openapi": "1.1.1",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-list": "0.3.0",
@@ -86,5 +86,8 @@
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
"patchedDependencies": {
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.126",
"version": "1.0.134",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 50 B

View File

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

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 46 B

View File

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

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 46 B

View File

@@ -9,8 +9,8 @@ export const config = {
github: {
repoUrl: "https://github.com/sst/opencode",
starsFormatted: {
compact: "30K",
full: "30,000",
compact: "35K",
full: "35,000",
},
},
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "300",
commits: "4,000",
monthlyUsers: "300,000",
contributors: "350",
commits: "5,000",
monthlyUsers: "400,000",
},
} as const

View File

@@ -338,6 +338,11 @@ body {
}
}
[data-slot="installation-instructions"] {
color: var(--color-text-strong);
margin-bottom: 8px;
}
[data-slot="installation"] {
width: 100%;
max-width: 100%;
@@ -348,6 +353,11 @@ body {
}
}
[data-slot="installation-options"] {
font-size: 13px;
margin-top: 12px;
}
[data-component="tabs"] {
[data-slot="tablist"] {
display: flex;
@@ -480,10 +490,10 @@ body {
}
h1 {
font-size: 28px;
font-size: 38px;
color: var(--color-text-strong);
font-weight: 500;
margin-bottom: 16px;
margin-bottom: 8px;
@media (max-width: 60rem) {
font-size: 22px;
@@ -492,7 +502,7 @@ body {
p {
color: var(--color-text);
margin-bottom: 24px;
margin-bottom: 40px;
max-width: 82%;
@media (max-width: 50rem) {
@@ -607,6 +617,25 @@ body {
padding: var(--vertical-padding) var(--padding);
color: var(--color-text);
a {
background: var(--color-background-strong);
padding: 8px 12px 8px 20px;
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
margin-top: 40px;
display: flex;
width: fit-content;
gap: 12px;
text-decoration: none;
}
a:hover {
background: var(--color-background-strong-hover);
}
ul {
padding: 0;
li {

View File

@@ -43,7 +43,7 @@ export default function Home() {
return (
<main data-page="opencode">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>OpenCode | The AI coding agent built for the terminal</Title>
<Title>OpenCode | The open source AI coding agent</Title>
<Link rel="canonical" href={config.baseUrl} />
<Meta property="og:image" content="/social-share.png" />
<Meta name="twitter:image" content="/social-share.png" />
@@ -56,23 +56,13 @@ export default function Home() {
<a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
Whats new in {release()?.name ?? "the latest release"}
</a>
<h1>The AI coding agent built for the terminal</h1>
<h1>The open source coding agent</h1>
<p>
OpenCode is fully open source, giving you control and freedom to use any provider, any model, and any
editor.
OpenCode includes free models or connect from any provider to <br />
use other models, including Claude, GPT, Gemini and more.
</p>
<a href="/docs">
<span>Read docs </span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</a>
</div>
<p data-slot="installation-instructions">Install and use. No account, no email, and no credit card.</p>
<div data-slot="installation">
<Tabs
as="section"
@@ -151,6 +141,11 @@ export default function Home() {
</div>
</Tabs>
</div>
<p data-slot="installation-options">
Available in terminal, web, and desktop (coming soon).
<br />
Extensions for VS Code, Cursor, Windsurf, and more.
</p>
</section>
<section data-component="video">
@@ -208,6 +203,17 @@ export default function Home() {
</div>
</li>
</ul>
<a href="/docs">
<span>Read docs </span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</a>
</section>
<section data-component="growth">

View File

@@ -0,0 +1,7 @@
export async function GET() {
const response = await fetch(
"https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/sdk/openapi.json",
)
const json = await response.json()
return json
}

View File

@@ -14,7 +14,7 @@ import "./workspace-picker.css"
const getWorkspaces = query(async () => {
"use server"
return withActor(async () => {
return Database.transaction((tx) =>
return Database.use((tx) =>
tx
.select({
id: WorkspaceTable.id,

View File

@@ -3,7 +3,7 @@ import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { useParams } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
@@ -94,8 +94,6 @@ async function getCosts(workspaceID: string, year: number, month: number) {
}, workspaceID)
}
const queryCosts = query(getCosts, "costs.get")
const MODEL_COLORS: Record<string, string> = {
"claude-sonnet-4-5": "#D4745C",
"claude-sonnet-4": "#E8B4A4",
@@ -158,32 +156,27 @@ export function GraphSection() {
model: null as string | null,
modelDropdownOpen: false,
keyDropdownOpen: false,
colorScheme: "light" as "light" | "dark",
})
const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
const onPreviousMonth = async () => {
const month = store.month === 0 ? 11 : store.month - 1
const year = store.month === 0 ? store.year - 1 : store.year
const data = await getCosts(params.id!, year, month)
setStore({ month, year, data })
setStore({ month, year })
}
const onNextMonth = async () => {
const month = store.month === 11 ? 0 : store.month + 1
const year = store.month === 11 ? store.year + 1 : store.year
setStore({ month, year, data: await getCosts(params.id!, year, month) })
setStore({ month, year })
}
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
const getData = createMemo(() => store.data ?? initialData())
const getModels = createMemo(() => {
const data = getData()
if (!data?.usage) return []
return Array.from(new Set(data.usage.map((row) => row.model))).sort()
if (!store.data?.usage) return []
return Array.from(new Set(store.data.usage.map((row) => row.model))).sort()
})
const getDates = createMemo(() => {
@@ -206,10 +199,19 @@ export function GraphSection() {
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
const chartConfig = createMemo((): ChartConfiguration | null => {
const data = getData()
const data = store.data
const dates = getDates()
if (!data?.usage?.length) return null
store.colorScheme
const styles = getComputedStyle(document.documentElement)
const colorTextMuted = styles.getPropertyValue("--color-text-muted").trim()
const colorBorderMuted = styles.getPropertyValue("--color-border-muted").trim()
const colorBgElevated = styles.getPropertyValue("--color-bg-elevated").trim()
const colorText = styles.getPropertyValue("--color-text").trim()
const colorTextSecondary = styles.getPropertyValue("--color-text-secondary").trim()
const colorBorder = styles.getPropertyValue("--color-border").trim()
const dailyData = new Map<string, Map<string, number>>()
for (const dateKey of dates) dailyData.set(dateKey, new Map())
@@ -252,7 +254,7 @@ export function GraphSection() {
ticks: {
maxRotation: 0,
autoSkipPadding: 20,
color: "rgba(255, 255, 255, 0.5)",
color: colorTextMuted,
font: {
family: "monospace",
size: 11,
@@ -263,10 +265,10 @@ export function GraphSection() {
stacked: true,
beginAtZero: true,
grid: {
color: "rgba(255, 255, 255, 0.1)",
color: colorBorderMuted,
},
ticks: {
color: "rgba(255, 255, 255, 0.5)",
color: colorTextMuted,
font: {
family: "monospace",
size: 11,
@@ -282,10 +284,10 @@ export function GraphSection() {
tooltip: {
mode: "index",
intersect: false,
backgroundColor: "rgba(0, 0, 0, 0.9)",
titleColor: "rgba(255, 255, 255, 0.9)",
bodyColor: "rgba(255, 255, 255, 0.8)",
borderColor: "rgba(255, 255, 255, 0.1)",
backgroundColor: colorBgElevated,
titleColor: colorText,
bodyColor: colorTextSecondary,
borderColor: colorBorder,
borderWidth: 1,
padding: 12,
displayColors: true,
@@ -301,7 +303,7 @@ export function GraphSection() {
display: true,
position: "bottom",
labels: {
color: "rgba(255, 255, 255, 0.7)",
color: colorTextSecondary,
font: {
size: 12,
},
@@ -339,15 +341,32 @@ export function GraphSection() {
}
})
createEffect(async () => {
const data = await getCosts(params.id!, store.year, store.month)
setStore({ data })
})
createEffect(() => {
const config = chartConfig()
if (!config || !canvasRef) return
if (chartInstance) chartInstance.destroy()
chartInstance = new Chart(canvasRef, config)
onCleanup(() => chartInstance?.destroy())
})
onCleanup(() => chartInstance?.destroy())
createEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
setStore({ colorScheme: mediaQuery.matches ? "dark" : "light" })
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
setStore({ colorScheme: e.matches ? "dark" : "light" })
}
mediaQuery.addEventListener("change", handleColorSchemeChange)
onCleanup(() => mediaQuery.removeEventListener("change", handleColorSchemeChange))
})
return (
<section class={styles.root}>
@@ -356,55 +375,53 @@ export function GraphSection() {
<p>Usage costs broken down by model.</p>
</div>
<Show when={getData()}>
<div data-slot="filter-container">
<div data-slot="month-picker">
<button data-slot="month-button" onClick={onPreviousMonth}>
<IconChevronLeft />
</button>
<span data-slot="month-label">{formatMonthYear()}</span>
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
<IconChevronRight />
</button>
</div>
<Dropdown
trigger={store.model === null ? "All Models" : store.model}
open={store.modelDropdownOpen}
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
<span>All Models</span>
</button>
<For each={getModels()}>
{(model) => (
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
<span>{model}</span>
</button>
)}
</For>
</>
</Dropdown>
<Dropdown
trigger={getKeyName(store.key)}
open={store.keyDropdownOpen}
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
<span>All Keys</span>
</button>
<For each={getData()?.keys || []}>
{(key) => (
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
<span>{key.displayName}</span>
</button>
)}
</For>
</>
</Dropdown>
<div data-slot="filter-container">
<div data-slot="month-picker">
<button data-slot="month-button" onClick={onPreviousMonth}>
<IconChevronLeft />
</button>
<span data-slot="month-label">{formatMonthYear()}</span>
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
<IconChevronRight />
</button>
</div>
</Show>
<Dropdown
trigger={store.model === null ? "All Models" : store.model}
open={store.modelDropdownOpen}
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
<span>All Models</span>
</button>
<For each={getModels()}>
{(model) => (
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
<span>{model}</span>
</button>
)}
</For>
</>
</Dropdown>
<Dropdown
trigger={getKeyName(store.key)}
open={store.keyDropdownOpen}
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
<span>All Keys</span>
</button>
<For each={store.data?.keys || []}>
{(key) => (
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
<span>{key.displayName}</span>
</button>
)}
</For>
</>
</Dropdown>
</div>
<Show
when={chartConfig()}

View File

@@ -1,25 +1,37 @@
import { Resource, waitUntil } from "@opencode-ai/console-resource"
export function createDataDumper(sessionId: string, requestId: string) {
export function createDataDumper(sessionId: string, requestId: string, projectId: string) {
if (Resource.App.stage !== "production") return
if (sessionId === "") return
let data: Record<string, any> = {}
let modelName: string | undefined
let data: Record<string, any> = { sessionId, requestId, projectId }
let metadata: Record<string, any> = { sessionId, requestId, projectId }
return {
provideModel: (model?: string) => (modelName = model),
provideModel: (model?: string) => {
data.modelName = model
metadata.modelName = model
},
provideRequest: (request: string) => (data.request = request),
provideResponse: (response: string) => (data.response = response),
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
flush: () => {
if (!modelName) return
if (!data.modelName) return
const str = new Date().toISOString().replace(/[^0-9]/g, "")
const yyyymmdd = str.substring(0, 8)
const hh = str.substring(8, 10)
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "")
waitUntil(
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
Resource.ZenData.put(
`data/${data.modelName}/${sessionId}/${requestId}.json`,
JSON.stringify({ timestamp, ...data }),
),
)
waitUntil(
Resource.ZenData.put(
`meta/${data.modelName}/${timestamp}/${requestId}.json`,
JSON.stringify({ timestamp, ...metadata }),
),
)
},
}

View File

@@ -13,13 +13,7 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
import {
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
ProviderHelper,
UsageInfo,
} from "./provider/provider"
import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
import { googleHelper } from "./provider/google"
import { openaiHelper } from "./provider/openai"
@@ -27,6 +21,7 @@ import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
import { createDataDumper } from "./dataDumper"
import { createTrialLimiter } from "./trialLimiter"
import { createStickyTracker } from "./stickyProviderTracker"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
@@ -61,6 +56,7 @@ export async function handler(
const ip = input.request.headers.get("x-real-ip") ?? ""
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
const requestId = input.request.headers.get("x-opencode-request") ?? ""
const projectId = input.request.headers.get("x-opencode-project") ?? ""
logger.metric({
is_tream: isStream,
session: sessionId,
@@ -68,15 +64,25 @@ export async function handler(
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, model)
const dataDumper = createDataDumper(sessionId, requestId)
const dataDumper = createDataDumper(sessionId, requestId, projectId)
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId)
const stickyProvider = await stickyTracker?.get()
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
const authInfo = await authenticate(modelInfo, providerInfo)
const authInfo = await authenticate(modelInfo)
const providerInfo = selectProvider(
zenData,
authInfo,
modelInfo,
sessionId,
isTrial ?? false,
retry,
stickyProvider,
)
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
@@ -126,6 +132,9 @@ export async function handler(
dataDumper?.provideModel(providerInfo.storeModel)
dataDumper?.provideRequest(reqBody)
// Store sticky provider
await stickyTracker?.set(providerInfo.id)
// Scrub response headers
const resHeaders = new Headers()
const keepHeaders = ["content-type", "cache-control"]
@@ -290,16 +299,27 @@ export async function handler(
function selectProvider(
zenData: ZenData,
authInfo: AuthInfo,
modelInfo: ModelInfo,
sessionId: string,
isTrial: boolean,
retry: RetryOptions,
stickyProvider: string | undefined,
) {
const provider = (() => {
if (authInfo?.provider?.credentials) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
}
if (isTrial) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
}
if (stickyProvider) {
const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
if (provider) return provider
}
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
}
@@ -335,7 +355,7 @@ export async function handler(
}
}
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
async function authenticate(modelInfo: ModelInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
@@ -373,7 +393,12 @@ export async function handler(
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, modelInfo.id)))
.leftJoin(
ProviderTable,
and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
modelInfo.byokProvider
? and(
eq(ProviderTable.workspaceID, KeyTable.workspaceID),
eq(ProviderTable.provider, modelInfo.byokProvider),
)
: sql`false`,
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
@@ -450,8 +475,7 @@ export async function handler(
}
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
if (!authInfo) return
if (!authInfo.provider?.credentials) return
if (!authInfo?.provider?.credentials) return
providerInfo.apiKey = authInfo.provider.credentials
}

View File

@@ -0,0 +1,16 @@
import { Resource } from "@opencode-ai/console-resource"
export function createStickyTracker(stickyProvider: boolean, session: string) {
if (!stickyProvider) return
if (!session) return
const key = `sticky:${session}`
return {
get: async () => {
return await Resource.GatewayKv.get(key)
},
set: async (providerId: string) => {
await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
},
}
}

View File

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

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.126",
"version": "1.0.134",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -11,7 +11,7 @@ export namespace Account {
id: z.string().optional(),
}),
async (input) =>
Database.transaction(async (tx) => {
Database.use(async (tx) => {
const id = input.id ?? Identifier.create("account")
await tx.insert(AccountTable).values({
id,
@@ -21,13 +21,12 @@ export namespace Account {
)
export const fromID = fn(z.string(), async (id) =>
Database.transaction(async (tx) => {
return tx
Database.use((tx) =>
tx
.select()
.from(AccountTable)
.where(eq(AccountTable.id, id))
.execute()
.then((rows) => rows[0])
}),
.then((rows) => rows[0]),
),
)
}

View File

@@ -24,6 +24,8 @@ export namespace ZenData {
cost: ModelCostSchema,
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.boolean().optional(),
trial: z
.object({
limit: z.number(),

View File

@@ -47,7 +47,7 @@ export namespace Provider {
}),
async ({ provider }) => {
Actor.assertAdmin()
return Database.transaction((tx) =>
return Database.use((tx) =>
tx
.delete(ProviderTable)
.where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),

View File

@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// 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
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}

View File

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

View File

@@ -194,7 +194,7 @@ export default {
// Get workspace
await Actor.provide("account", { accountID, email }, async () => {
await User.joinInvitedWorkspaces()
const workspaces = await Database.transaction(async (tx) =>
const workspaces = await Database.use((tx) =>
tx
.select({ id: WorkspaceTable.id })
.from(WorkspaceTable)

View File

@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// 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
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptio
import { Resource as ResourceBase } from "sst"
import Cloudflare from "cloudflare"
export const waitUntil = async (fn: () => Promise<void>) => {
await fn()
export const waitUntil = async (promise: Promise<any>) => {
await promise
}
export const Resource = new Proxy(

View File

@@ -6,126 +6,130 @@
import "sst"
declare module "sst" {
export interface Resource {
ADMIN_SECRET: {
type: "sst.sst.Secret"
value: string
"ADMIN_SECRET": {
"type": "sst.sst.Secret"
"value": string
}
AUTH_API_URL: {
type: "sst.sst.Linkable"
value: string
"AUTH_API_URL": {
"type": "sst.sst.Linkable"
"value": string
}
AWS_SES_ACCESS_KEY_ID: {
type: "sst.sst.Secret"
value: string
"AWS_SES_ACCESS_KEY_ID": {
"type": "sst.sst.Secret"
"value": string
}
AWS_SES_SECRET_ACCESS_KEY: {
type: "sst.sst.Secret"
value: string
"AWS_SES_SECRET_ACCESS_KEY": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_API_TOKEN": {
"type": "sst.sst.Secret"
"value": string
}
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
"CLOUDFLARE_DEFAULT_ACCOUNT_ID": {
"type": "sst.sst.Secret"
"value": string
}
Console: {
type: "sst.cloudflare.SolidStart"
url: string
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
}
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
}
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
}
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
}
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
"R2AccessKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
"R2SecretKey": {
"type": "sst.sst.Secret"
"value": string
}
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
}
Web: {
type: "sst.cloudflare.Astro"
url: string
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
}
ZEN_MODELS2: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
"ZEN_MODELS3": {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS4": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
import * as cloudflare from "@cloudflare/workers-types"
// 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
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
"Api": cloudflare.Service
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"EnterpriseStorage": cloudflare.R2Bucket
"GatewayKv": cloudflare.KVNamespace
"LogProcessor": cloudflare.Service
"ZenData": cloudflare.R2Bucket
}
}
import "sst"
export {}
export {}

View File

@@ -0,0 +1,2 @@
[test]
preload = ["./happydom.ts"]

View File

@@ -0,0 +1,75 @@
import { GlobalRegistrator } from "@happy-dom/global-registrator"
GlobalRegistrator.register()
const originalGetContext = HTMLCanvasElement.prototype.getContext
// @ts-expect-error - we're overriding with a simplified mock
HTMLCanvasElement.prototype.getContext = function (contextType: string, _options?: unknown) {
if (contextType === "2d") {
return {
canvas: this,
fillStyle: "#000000",
strokeStyle: "#000000",
font: "12px monospace",
textAlign: "start",
textBaseline: "alphabetic",
globalAlpha: 1,
globalCompositeOperation: "source-over",
imageSmoothingEnabled: true,
lineWidth: 1,
lineCap: "butt",
lineJoin: "miter",
miterLimit: 10,
shadowBlur: 0,
shadowColor: "rgba(0, 0, 0, 0)",
shadowOffsetX: 0,
shadowOffsetY: 0,
fillRect: () => {},
strokeRect: () => {},
clearRect: () => {},
fillText: () => {},
strokeText: () => {},
measureText: (text: string) => ({ width: text.length * 8 }),
drawImage: () => {},
save: () => {},
restore: () => {},
scale: () => {},
rotate: () => {},
translate: () => {},
transform: () => {},
setTransform: () => {},
resetTransform: () => {},
createLinearGradient: () => ({ addColorStop: () => {} }),
createRadialGradient: () => ({ addColorStop: () => {} }),
createPattern: () => null,
beginPath: () => {},
closePath: () => {},
moveTo: () => {},
lineTo: () => {},
bezierCurveTo: () => {},
quadraticCurveTo: () => {},
arc: () => {},
arcTo: () => {},
ellipse: () => {},
rect: () => {},
fill: () => {},
stroke: () => {},
clip: () => {},
isPointInPath: () => false,
isPointInStroke: () => false,
getTransform: () => ({}),
getImageData: () => ({
data: new Uint8ClampedArray(0),
width: 0,
height: 0,
}),
putImageData: () => {},
createImageData: () => ({
data: new Uint8ClampedArray(0),
width: 0,
height: 0,
}),
} as unknown as CanvasRenderingContext2D
}
return originalGetContext.call(this, contextType as "2d", _options)
}

View File

@@ -1,8 +1,12 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.126",
"version": "1.0.134",
"description": "",
"type": "module",
"exports": {
".": "./src/index.tsx",
"./vite": "./vite.js"
},
"scripts": {
"typecheck": "tsgo --noEmit",
"start": "vite",
@@ -12,8 +16,10 @@
},
"license": "MIT",
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/bun": "catalog:",
"@types/luxon": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
@@ -33,11 +39,13 @@
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "catalog:",
"fuzzysort": "catalog:",
"ghostty-web": "0.3.0",
"luxon": "catalog:",
"marked": "16.2.0",
"marked-shiki": "1.2.1",

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 43 B

View File

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

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 43 B

View File

@@ -0,0 +1,272 @@
import { describe, test, expect, beforeAll, afterEach } from "bun:test"
import { Terminal, Ghostty } from "ghostty-web"
import { SerializeAddon } from "./serialize"
let ghostty: Ghostty
beforeAll(async () => {
ghostty = await Ghostty.load()
})
const terminals: Terminal[] = []
afterEach(() => {
for (const term of terminals) {
term.dispose()
}
terminals.length = 0
document.body.innerHTML = ""
})
function createTerminal(cols = 80, rows = 24): { term: Terminal; addon: SerializeAddon; container: HTMLElement } {
const container = document.createElement("div")
document.body.appendChild(container)
const term = new Terminal({ cols, rows, ghostty })
const addon = new SerializeAddon()
term.loadAddon(addon)
term.open(container)
terminals.push(term)
return { term, addon, container }
}
function writeAndWait(term: Terminal, data: string): Promise<void> {
return new Promise((resolve) => {
term.write(data, resolve)
})
}
describe("SerializeAddon", () => {
describe("ANSI color preservation", () => {
test("should preserve text attributes (bold, italic, underline)", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[1mBOLD\x1b[0m \x1b[3mITALIC\x1b[0m \x1b[4mUNDER\x1b[0m"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
expect(origLine!.getCell(0)!.isBold()).toBe(1)
expect(origLine!.getCell(5)!.isItalic()).toBe(1)
expect(origLine!.getCell(12)!.isUnderline()).toBe(1)
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const boldCell = line!.getCell(0)
expect(boldCell!.getChars()).toBe("B")
expect(boldCell!.isBold()).toBe(1)
const italicCell = line!.getCell(5)
expect(italicCell!.getChars()).toBe("I")
expect(italicCell!.isItalic()).toBe(1)
const underCell = line!.getCell(12)
expect(underCell!.getChars()).toBe("U")
expect(underCell!.isUnderline()).toBe(1)
})
test("should preserve basic 16-color foreground colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[31mRED\x1b[32mGREEN\x1b[34mBLUE\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedFg = origLine!.getCell(0)!.getFgColor()
const origGreenFg = origLine!.getCell(3)!.getFgColor()
const origBlueFg = origLine!.getCell(8)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
expect(line).toBeDefined()
const redCell = line!.getCell(0)
expect(redCell!.getChars()).toBe("R")
expect(redCell!.getFgColor()).toBe(origRedFg)
const greenCell = line!.getCell(3)
expect(greenCell!.getChars()).toBe("G")
expect(greenCell!.getFgColor()).toBe(origGreenFg)
const blueCell = line!.getCell(8)
expect(blueCell!.getChars()).toBe("B")
expect(blueCell!.getFgColor()).toBe(origBlueFg)
})
test("should preserve 256-color palette colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[38;5;196mRED256\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedFg = origLine!.getCell(0)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const redCell = line!.getCell(0)
expect(redCell!.getChars()).toBe("R")
expect(redCell!.getFgColor()).toBe(origRedFg)
})
test("should preserve RGB/truecolor colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[38;2;255;128;64mRGB_TEXT\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRgbFg = origLine!.getCell(0)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const rgbCell = line!.getCell(0)
expect(rgbCell!.getChars()).toBe("R")
expect(rgbCell!.getFgColor()).toBe(origRgbFg)
})
test("should preserve background colors", async () => {
const { term, addon } = createTerminal()
const input = "\x1b[48;2;255;0;0mRED_BG\x1b[48;2;0;255;0mGREEN_BG\x1b[0mNORMAL"
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origRedBg = origLine!.getCell(0)!.getBgColor()
const origGreenBg = origLine!.getCell(6)!.getBgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
const line = term2.buffer.active.getLine(0)
const redBgCell = line!.getCell(0)
expect(redBgCell!.getChars()).toBe("R")
expect(redBgCell!.getBgColor()).toBe(origRedBg)
const greenBgCell = line!.getCell(6)
expect(greenBgCell!.getChars()).toBe("G")
expect(greenBgCell!.getBgColor()).toBe(origGreenBg)
})
test("should handle combined colors and attributes", async () => {
const { term, addon } = createTerminal()
const input =
"\x1b[1;38;2;255;0;0;48;2;255;255;0mCOMBO\x1b[0mNORMAL "
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origFg = origLine!.getCell(0)!.getFgColor()
const origBg = origLine!.getCell(0)!.getBgColor()
expect(origLine!.getCell(0)!.isBold()).toBe(1)
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const cleanSerialized = serialized.replace(/\x1b\[\d+X/g, "")
expect(cleanSerialized.startsWith("\x1b[1;")).toBe(true)
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, cleanSerialized)
const line = term2.buffer.active.getLine(0)
const comboCell = line!.getCell(0)
expect(comboCell!.getChars()).toBe("C")
expect(cleanSerialized).toContain("\x1b[1;38;2;255;0;0;48;2;255;255;0m")
})
})
describe("round-trip serialization", () => {
test("should not produce ECH sequences", async () => {
const { term, addon } = createTerminal()
await writeAndWait(term, "\x1b[31mHello\x1b[0m World")
const serialized = addon.serialize()
const hasECH = /\x1b\[\d+X/.test(serialized)
expect(hasECH).toBe(false)
})
test("multi-line content should not have garbage characters", async () => {
const { term, addon } = createTerminal()
const content = [
"\x1b[1;32m\x1b[0m \x1b[34mcd\x1b[0m /some/path",
"\x1b[1;32m\x1b[0m \x1b[34mls\x1b[0m -la",
"total 42",
].join("\r\n")
await writeAndWait(term, content)
const serialized = addon.serialize()
expect(/\x1b\[\d+X/.test(serialized)).toBe(false)
const { term: term2 } = createTerminal()
terminals.push(term2)
await writeAndWait(term2, serialized)
for (let row = 0; row < 3; row++) {
const line = term2.buffer.active.getLine(row)?.translateToString(true)
expect(line?.includes("𑼝")).toBe(false)
}
expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
})
test("serialized output written to new terminal should match original colors", async () => {
const { term, addon } = createTerminal(40, 5)
const input = "\x1b[38;2;255;0;0mHello\x1b[0m \x1b[38;2;0;255;0mWorld\x1b[0m! "
await writeAndWait(term, input)
const origLine = term.buffer.active.getLine(0)
const origHelloFg = origLine!.getCell(0)!.getFgColor()
const origWorldFg = origLine!.getCell(6)!.getFgColor()
const serialized = addon.serialize({ range: { start: 0, end: 0 } })
const { term: term2 } = createTerminal(40, 5)
terminals.push(term2)
await writeAndWait(term2, serialized)
const newLine = term2.buffer.active.getLine(0)
expect(newLine!.getCell(0)!.getChars()).toBe("H")
expect(newLine!.getCell(0)!.getFgColor()).toBe(origHelloFg)
expect(newLine!.getCell(6)!.getChars()).toBe("W")
expect(newLine!.getCell(6)!.getFgColor()).toBe(origWorldFg)
expect(newLine!.getCell(11)!.getChars()).toBe("!")
})
})
})

View File

@@ -0,0 +1,595 @@
/**
* SerializeAddon - Serialize terminal buffer contents
*
* Port of xterm.js addon-serialize for ghostty-web.
* Enables serialization of terminal contents to a string that can
* be written back to restore terminal state.
*
* Usage:
* ```typescript
* const serializeAddon = new SerializeAddon();
* term.loadAddon(serializeAddon);
* const content = serializeAddon.serialize();
* ```
*/
import type { ITerminalAddon, ITerminalCore, IBufferRange } from "ghostty-web"
// ============================================================================
// Buffer Types (matching ghostty-web internal interfaces)
// ============================================================================
interface IBuffer {
readonly type: "normal" | "alternate"
readonly cursorX: number
readonly cursorY: number
readonly viewportY: number
readonly baseY: number
readonly length: number
getLine(y: number): IBufferLine | undefined
getNullCell(): IBufferCell
}
interface IBufferLine {
readonly length: number
readonly isWrapped: boolean
getCell(x: number): IBufferCell | undefined
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string
}
interface IBufferCell {
getChars(): string
getCode(): number
getWidth(): number
getFgColorMode(): number
getBgColorMode(): number
getFgColor(): number
getBgColor(): number
isBold(): number
isItalic(): number
isUnderline(): number
isStrikethrough(): number
isBlink(): number
isInverse(): number
isInvisible(): number
isFaint(): number
isDim(): boolean
}
// ============================================================================
// Types
// ============================================================================
export interface ISerializeOptions {
/**
* The row range to serialize. When an explicit range is specified, the cursor
* will get its final repositioning.
*/
range?: ISerializeRange
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer. When not specified, all available
* rows in the scrollback buffer will be serialized.
*/
scrollback?: number
/**
* Whether to exclude the terminal modes from the serialization.
* Default: false
*/
excludeModes?: boolean
/**
* Whether to exclude the alt buffer from the serialization.
* Default: false
*/
excludeAltBuffer?: boolean
}
export interface ISerializeRange {
/**
* The line to start serializing (inclusive).
*/
start: number
/**
* The line to end serializing (inclusive).
*/
end: number
}
export interface IHTMLSerializeOptions {
/**
* The number of rows in the scrollback buffer to serialize, starting from
* the bottom of the scrollback buffer.
*/
scrollback?: number
/**
* Whether to only serialize the selection.
* Default: false
*/
onlySelection?: boolean
/**
* Whether to include the global background of the terminal.
* Default: false
*/
includeGlobalBackground?: boolean
/**
* The range to serialize. This is prioritized over onlySelection.
*/
range?: {
startLine: number
endLine: number
startCol: number
}
}
// ============================================================================
// Helper Functions
// ============================================================================
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high))
}
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor()
}
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor()
}
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
return (
!!cell1.isInverse() === !!cell2.isInverse() &&
!!cell1.isBold() === !!cell2.isBold() &&
!!cell1.isUnderline() === !!cell2.isUnderline() &&
!!cell1.isBlink() === !!cell2.isBlink() &&
!!cell1.isInvisible() === !!cell2.isInvisible() &&
!!cell1.isItalic() === !!cell2.isItalic() &&
!!cell1.isDim() === !!cell2.isDim() &&
!!cell1.isStrikethrough() === !!cell2.isStrikethrough()
)
}
// ============================================================================
// Base Serialize Handler
// ============================================================================
abstract class BaseSerializeHandler {
constructor(protected readonly _buffer: IBuffer) {}
private _isRealContent(codepoint: number): boolean {
if (codepoint === 0) return false
if (codepoint >= 0xf000) return false
return true
}
private _findLastContentColumn(line: IBufferLine): number {
let lastContent = -1
for (let col = 0; col < line.length; col++) {
const cell = line.getCell(col)
if (cell && this._isRealContent(cell.getCode())) {
lastContent = col
}
}
return lastContent + 1
}
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
let oldCell = this._buffer.getNullCell()
const startRow = range.start.y
const endRow = range.end.y
const startColumn = range.start.x
const endColumn = range.end.x
this._beforeSerialize(endRow - startRow, startRow, endRow)
for (let row = startRow; row <= endRow; row++) {
const line = this._buffer.getLine(row)
if (line) {
const startLineColumn = row === range.start.y ? startColumn : 0
const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
const endLineColumn = Math.min(maxColumn, line.length)
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col)
if (!c) {
continue
}
this._nextCell(c, oldCell, row, col)
oldCell = c
}
}
this._rowEnd(row, row === endRow)
}
this._afterSerialize()
return this._serializeString(excludeFinalCursorPosition)
}
protected _nextCell(_cell: IBufferCell, _oldCell: IBufferCell, _row: number, _col: number): void {}
protected _rowEnd(_row: number, _isLastRow: boolean): void {}
protected _beforeSerialize(_rows: number, _startRow: number, _endRow: number): void {}
protected _afterSerialize(): void {}
protected _serializeString(_excludeFinalCursorPosition?: boolean): string {
return ""
}
}
// ============================================================================
// String Serialize Handler
// ============================================================================
class StringSerializeHandler extends BaseSerializeHandler {
private _rowIndex: number = 0
private _allRows: string[] = []
private _allRowSeparators: string[] = []
private _currentRow: string = ""
private _nullCellCount: number = 0
private _cursorStyle: IBufferCell
private _firstRow: number = 0
private _lastCursorRow: number = 0
private _lastCursorCol: number = 0
private _lastContentCursorRow: number = 0
private _lastContentCursorCol: number = 0
constructor(
buffer: IBuffer,
private readonly _terminal: ITerminalCore,
) {
super(buffer)
this._cursorStyle = this._buffer.getNullCell()
}
protected _beforeSerialize(rows: number, start: number, _end: number): void {
this._allRows = new Array<string>(rows)
this._lastContentCursorRow = start
this._lastCursorRow = start
this._firstRow = start
}
protected _rowEnd(row: number, isLastRow: boolean): void {
let rowSeparator = ""
if (!isLastRow) {
const nextLine = this._buffer.getLine(row + 1)
if (!nextLine?.isWrapped) {
rowSeparator = "\r\n"
this._lastCursorRow = row + 1
this._lastCursorCol = 0
}
}
this._allRows[this._rowIndex] = this._currentRow
this._allRowSeparators[this._rowIndex++] = rowSeparator
this._currentRow = ""
this._nullCellCount = 0
}
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
const sgrSeq: number[] = []
const fgChanged = !equalFg(cell, oldCell)
const bgChanged = !equalBg(cell, oldCell)
const flagsChanged = !equalFlags(cell, oldCell)
if (fgChanged || bgChanged || flagsChanged) {
if (this._isAttributeDefault(cell)) {
if (!this._isAttributeDefault(oldCell)) {
sgrSeq.push(0)
}
} else {
if (flagsChanged) {
if (!!cell.isInverse() !== !!oldCell.isInverse()) {
sgrSeq.push(cell.isInverse() ? 7 : 27)
}
if (!!cell.isBold() !== !!oldCell.isBold()) {
sgrSeq.push(cell.isBold() ? 1 : 22)
}
if (!!cell.isUnderline() !== !!oldCell.isUnderline()) {
sgrSeq.push(cell.isUnderline() ? 4 : 24)
}
if (!!cell.isBlink() !== !!oldCell.isBlink()) {
sgrSeq.push(cell.isBlink() ? 5 : 25)
}
if (!!cell.isInvisible() !== !!oldCell.isInvisible()) {
sgrSeq.push(cell.isInvisible() ? 8 : 28)
}
if (!!cell.isItalic() !== !!oldCell.isItalic()) {
sgrSeq.push(cell.isItalic() ? 3 : 23)
}
if (!!cell.isDim() !== !!oldCell.isDim()) {
sgrSeq.push(cell.isDim() ? 2 : 22)
}
if (!!cell.isStrikethrough() !== !!oldCell.isStrikethrough()) {
sgrSeq.push(cell.isStrikethrough() ? 9 : 29)
}
}
if (fgChanged) {
const color = cell.getFgColor()
const mode = cell.getFgColorMode()
if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(38, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
if (color >= 16) {
sgrSeq.push(38, 5, color)
} else {
sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7))
}
} else {
sgrSeq.push(39)
}
}
if (bgChanged) {
const color = cell.getBgColor()
const mode = cell.getBgColorMode()
if (mode === 2 || mode === 3 || mode === -1) {
sgrSeq.push(48, 2, (color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff)
} else if (mode === 1) {
// Palette
if (color >= 16) {
sgrSeq.push(48, 5, color)
} else {
sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7))
}
} else {
sgrSeq.push(49)
}
}
}
}
return sgrSeq
}
private _isAttributeDefault(cell: IBufferCell): boolean {
const mode = cell.getFgColorMode()
const bgMode = cell.getBgColorMode()
if (mode === 0 && bgMode === 0) {
return (
!cell.isBold() &&
!cell.isItalic() &&
!cell.isUnderline() &&
!cell.isBlink() &&
!cell.isInverse() &&
!cell.isInvisible() &&
!cell.isDim() &&
!cell.isStrikethrough()
)
}
const fgColor = cell.getFgColor()
const bgColor = cell.getBgColor()
const nullCell = this._buffer.getNullCell()
const nullFg = nullCell.getFgColor()
const nullBg = nullCell.getBgColor()
return (
fgColor === nullFg &&
bgColor === nullBg &&
!cell.isBold() &&
!cell.isItalic() &&
!cell.isUnderline() &&
!cell.isBlink() &&
!cell.isInverse() &&
!cell.isInvisible() &&
!cell.isDim() &&
!cell.isStrikethrough()
)
}
protected _nextCell(cell: IBufferCell, _oldCell: IBufferCell, row: number, col: number): void {
const isPlaceHolderCell = cell.getWidth() === 0
if (isPlaceHolderCell) {
return
}
const codepoint = cell.getCode()
const isGarbage = codepoint >= 0xf000
const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0
if (styleChanged) {
if (this._nullCellCount > 0) {
this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
this._lastContentCursorRow = this._lastCursorRow = row
this._lastContentCursorCol = this._lastCursorCol = col
this._currentRow += `\u001b[${sgrSeq.join(";")}m`
const line = this._buffer.getLine(row)
const cellFromLine = line?.getCell(col)
if (cellFromLine) {
this._cursorStyle = cellFromLine
}
}
if (isEmptyCell) {
this._nullCellCount += cell.getWidth()
} else {
if (this._nullCellCount > 0) {
this._currentRow += `\u001b[${this._nullCellCount}C`
this._nullCellCount = 0
}
this._currentRow += cell.getChars()
this._lastContentCursorRow = this._lastCursorRow = row
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth()
}
}
protected _serializeString(excludeFinalCursorPosition?: boolean): string {
let rowEnd = this._allRows.length
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
rowEnd = this._lastContentCursorRow + 1 - this._firstRow
this._lastCursorCol = this._lastContentCursorCol
this._lastCursorRow = this._lastContentCursorRow
}
let content = ""
for (let i = 0; i < rowEnd; i++) {
content += this._allRows[i]
if (i + 1 < rowEnd) {
content += this._allRowSeparators[i]
}
}
if (!excludeFinalCursorPosition) {
const absoluteCursorRow = (this._buffer.baseY ?? 0) + this._buffer.cursorY
const cursorRow = constrain(absoluteCursorRow - this._firstRow + 1, 1, Number.MAX_SAFE_INTEGER)
const cursorCol = this._buffer.cursorX + 1
content += `\u001b[${cursorRow};${cursorCol}H`
}
return content
}
}
// ============================================================================
// SerializeAddon Class
// ============================================================================
export class SerializeAddon implements ITerminalAddon {
private _terminal?: ITerminalCore
/**
* Activate the addon (called by Terminal.loadAddon)
*/
public activate(terminal: ITerminalCore): void {
this._terminal = terminal
}
/**
* Dispose the addon and clean up resources
*/
public dispose(): void {
this._terminal = undefined
}
/**
* Serializes terminal rows into a string that can be written back to the
* terminal to restore the state. The cursor will also be positioned to the
* correct cell.
*
* @param options Custom options to allow control over what gets serialized.
*/
public serialize(options?: ISerializeOptions): string {
if (!this._terminal) {
throw new Error("Cannot use addon until it has been loaded")
}
const terminal = this._terminal as any
const buffer = terminal.buffer
if (!buffer) {
return ""
}
const normalBuffer = buffer.normal || buffer.active
const altBuffer = buffer.alternate
if (!normalBuffer) {
return ""
}
let content = options?.range
? this._serializeBufferByRange(normalBuffer, options.range, true)
: this._serializeBufferByScrollback(normalBuffer, options?.scrollback)
if (!options?.excludeAltBuffer && buffer.active?.type === "alternate" && altBuffer) {
const alternateContent = this._serializeBufferByScrollback(altBuffer, undefined)
content += `\u001b[?1049h\u001b[H${alternateContent}`
}
return content
}
/**
* Serializes terminal content as plain text (no escape sequences)
* @param options Custom options to allow control over what gets serialized.
*/
public serializeAsText(options?: { scrollback?: number; trimWhitespace?: boolean }): string {
if (!this._terminal) {
throw new Error("Cannot use addon until it has been loaded")
}
const terminal = this._terminal as any
const buffer = terminal.buffer
if (!buffer) {
return ""
}
const activeBuffer = buffer.active || buffer.normal
if (!activeBuffer) {
return ""
}
const maxRows = activeBuffer.length
const scrollback = options?.scrollback
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + this._terminal.rows, 0, maxRows)
const startRow = maxRows - correctRows
const endRow = maxRows - 1
const lines: string[] = []
for (let row = startRow; row <= endRow; row++) {
const line = activeBuffer.getLine(row)
if (line) {
const text = line.translateToString(options?.trimWhitespace ?? true)
lines.push(text)
}
}
// Trim trailing empty lines if requested
if (options?.trimWhitespace) {
while (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop()
}
}
return lines.join("\n")
}
private _serializeBufferByScrollback(buffer: IBuffer, scrollback?: number): string {
const maxRows = buffer.length
const rows = this._terminal?.rows ?? 24
const correctRows = scrollback === undefined ? maxRows : constrain(scrollback + rows, 0, maxRows)
return this._serializeBufferByRange(
buffer,
{
start: maxRows - correctRows,
end: maxRows - 1,
},
false,
)
}
private _serializeBufferByRange(
buffer: IBuffer,
range: ISerializeRange,
excludeFinalCursorPosition: boolean,
): string {
const handler = new StringSerializeHandler(buffer, this._terminal!)
const cols = this._terminal?.cols ?? 80
return handler.serialize(
{
start: { x: 0, y: range.start },
end: { x: cols, y: range.end },
},
excludeFinalCursorPosition,
)
}
}

View File

@@ -235,9 +235,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const abort = () =>
sdk.client.session.abort({
path: {
id: session.id!,
},
sessionID: session.id!,
})
const handleKeyDown = (event: KeyboardEvent) => {
@@ -329,21 +327,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
session.prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
sdk.client.session.prompt({
path: { id: existing.id },
body: {
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text,
},
...attachmentParts,
],
sessionID: existing.id,
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text,
},
...attachmentParts,
],
})
}
@@ -456,9 +452,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
<Show when={i.release_date}>
<Show when={false}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
{DateTime.fromFormat("unknown", "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>

View File

@@ -0,0 +1,150 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
onCleanup?: (pty: LocalPTY) => void
onConnectError?: (error: unknown) => void
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
let term: Term
let ghostty: Ghostty
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
onMount(async () => {
ghostty = await Ghostty.load()
ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
term = new Term({
cursorBlink: true,
fontSize: 14,
fontFamily: "TX-02, monospace",
allowTransparency: true,
theme: {
background: "#191515",
foreground: "#d4d4d4",
},
scrollback: 10_000,
ghostty,
})
term.attachCustomKeyEventHandler((event) => {
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
return true
}
return false
})
fitAddon = new FitAddon()
serializeAddon = new SerializeAddon()
term.loadAddon(serializeAddon)
term.loadAddon(fitAddon)
term.open(container)
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
term.resize(local.pty.cols, local.pty.rows)
}
term.reset()
term.write(local.pty.buffer)
if (local.pty.scrollY) {
term.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
}
container.focus()
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
term.onResize(async (size) => {
if (ws && ws.readyState === WebSocket.OPEN) {
await sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
},
})
}
})
term.onData((data) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data)
}
})
term.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
// term.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
ws.addEventListener("open", () => {
console.log("WebSocket connected")
sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: term.cols,
rows: term.rows,
},
})
})
ws.addEventListener("message", (event) => {
term.write(event.data)
})
ws.addEventListener("error", (error) => {
console.error("WebSocket error:", error)
props.onConnectError?.(error)
})
ws.addEventListener("close", () => {
console.log("WebSocket disconnected")
})
})
onCleanup(() => {
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
if (serializeAddon && props.onCleanup) {
const buffer = serializeAddon.serialize()
props.onCleanup({
...local.pty,
buffer,
rows: term.rows,
cols: term.cols,
scrollY: term.getViewportY(),
})
}
ws?.close()
term?.dispose()
})
return (
<div
ref={container}
data-component="terminal"
classList={{
...(local.classList ?? {}),
"size-full px-6 py-3 font-mono": true,
[local.class ?? ""]: !!local.class,
}}
{...others}
/>
)
}

View File

@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"

View File

@@ -12,7 +12,7 @@ import type {
FileDiff,
Todo,
SessionStatus,
} from "@opencode-ai/sdk"
} from "@opencode-ai/sdk/v2"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"

View File

@@ -15,12 +15,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: true,
width: 280,
},
terminal: {
opened: false,
height: 280,
},
review: {
state: "pane" as "pane" | "tab",
},
}),
{
name: "___default-layout",
name: "____default-layout",
},
)
@@ -61,6 +65,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("sidebar", "width", width)
},
},
terminal: {
opened: createMemo(() => store.terminal.opened),
open() {
setStore("terminal", "opened", true)
},
close() {
setStore("terminal", "opened", false)
},
toggle() {
setStore("terminal", "opened", (x) => !x)
},
height: createMemo(() => store.terminal.height),
resize(height: number) {
setStore("terminal", "height", height)
},
},
review: {
state: createMemo(() => store.review?.state ?? "closed"),
pane() {

View File

@@ -1,7 +1,7 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo } from "solid-js"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
@@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
sdk.client.file.read({ query: { path: relativePath } }).then((x) => {
sdk.client.file.read({ path: relativePath }).then((x) => {
setStore(
"node",
relativePath,
@@ -305,7 +305,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
return sdk.client.file.list({ query: { path: path + "/" } }).then((x) => {
return sdk.client.file.list({ path: path + "/" }).then((x) => {
setStore(
"node",
produce((draft) => {
@@ -318,10 +318,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
const searchFiles = (query: string) =>
sdk.client.find.files({ query: { query, dirs: "false" } }).then((x) => x.data!)
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
const searchFilesAndDirectories = (query: string) =>
sdk.client.find.files({ query: { query, dirs: "true" } }).then((x) => x.data!)
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
sdk.event.listen((e) => {
const event = e.details

View File

@@ -1,4 +1,4 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/client"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
@@ -27,6 +27,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
abort.abort()
})
return { directory: props.directory, client: sdk, event: emitter }
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url }
},
})

View File

@@ -5,17 +5,28 @@ import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@/utils"
import { useSDK } from "./sdk"
export type LocalPTY = {
id: string
title: string
rows?: number
cols?: number
buffer?: string
scrollY?: number
}
export const { use: useSession, provider: SessionProvider } = createSimpleContext({
name: "Session",
init: () => {
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `___${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v1`,
)
const [store, setStore] = makePersisted(
@@ -23,16 +34,21 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
messageId?: string
tabs: {
active?: string
opened: string[]
all: string[]
}
prompt: Prompt
cursor?: number
terminals: {
active?: string
all: LocalPTY[]
}
}>({
tabs: {
opened: [],
all: [],
},
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
terminals: { all: [] },
}),
{
name: name(),
@@ -138,7 +154,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
setStore("tabs", "active", tab)
},
setOpenedTabs(tabs: string[]) {
setStore("tabs", "opened", tabs)
setStore("tabs", "all", tabs)
},
async openTab(tab: string) {
if (tab === "chat") {
@@ -146,8 +162,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
return
}
if (tab !== "review") {
if (!store.tabs.opened.includes(tab)) {
setStore("tabs", "opened", [...store.tabs.opened, tab])
if (!store.tabs.all.includes(tab)) {
setStore("tabs", "all", [...store.tabs.all, tab])
}
}
setStore("tabs", "active", tab)
@@ -156,28 +172,99 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
batch(() => {
setStore(
"tabs",
"opened",
store.tabs.opened.filter((x) => x !== tab),
"all",
store.tabs.all.filter((x) => x !== tab),
)
if (store.tabs.active === tab) {
const index = store.tabs.opened.findIndex((f) => f === tab)
const previous = store.tabs.opened[Math.max(0, index - 1)]
const index = store.tabs.all.findIndex((f) => f === tab)
const previous = store.tabs.all[Math.max(0, index - 1)]
setStore("tabs", "active", previous)
}
})
},
moveTab(tab: string, to: number) {
const index = store.tabs.opened.findIndex((f) => f === tab)
const index = store.tabs.all.findIndex((f) => f === tab)
if (index === -1) return
setStore(
"tabs",
"opened",
"all",
produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0])
}),
)
},
},
terminal: {
all: createMemo(() => Object.values(store.terminals.all)),
active: createMemo(() => store.terminals.active),
new() {
sdk.client.pty.create({ title: `Terminal ${store.terminals.all.length + 1}` }).then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("terminals", "all", [
...store.terminals.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("terminals", "active", id)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
},
async clone(id: string) {
const index = store.terminals.all.findIndex((x) => x.id === id)
const pty = store.terminals.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
title: pty.title,
})
if (!clone.data) return
setStore("terminals", "all", index, {
...pty,
...clone.data,
})
if (store.terminals.active === pty.id) {
setStore("terminals", "active", clone.data.id)
}
},
open(id: string) {
setStore("terminals", "active", id)
},
async close(id: string) {
batch(() => {
setStore(
"terminals",
"all",
store.terminals.all.filter((x) => x.id !== id),
)
if (store.terminals.active === id) {
const index = store.terminals.all.findIndex((f) => f.id === id)
const previous = store.tabs.all[Math.max(0, index - 1)]
setStore("terminals", "active", previous)
}
})
await sdk.client.pty.remove({ ptyID: id })
},
move(id: string, to: number) {
const index = store.terminals.all.findIndex((f) => f.id === id)
if (index === -1) return
setStore(
"terminals",
"all",
produce((all) => {
all.splice(to, 0, all.splice(index, 1)[0])
}),
)
},
},
}
},
})

View File

@@ -28,7 +28,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.client.file.list({ query: { path: "/" } }).then((x) => setStore("node", x.data!)),
node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
@@ -49,10 +49,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ path: { id: sessionID }, throwOnError: true }),
sdk.client.session.messages({ path: { id: sessionID }, query: { limit: 100 } }),
sdk.client.session.todo({ path: { id: sessionID } }),
sdk.client.session.diff({ path: { id: sessionID } }),
sdk.client.session.get({ sessionID }, { throwOnError: true }),
sdk.client.session.messages({ sessionID, limit: 100 }),
sdk.client.session.todo({ sessionID }),
sdk.client.session.diff({ sessionID }),
])
setStore(
produce((draft) => {

View File

@@ -1,9 +1,9 @@
import { createMemo, For, ParentProps, Show } from "solid-js"
import { DateTime } from "luxon"
import { A, useParams } from "@solidjs/router"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Encode } from "@/utils"
import { base64Decode, base64Encode } from "@/utils"
import { Mark } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
@@ -12,11 +12,22 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { Session } from "@opencode-ai/sdk/v2/client"
export default function Layout(props: ParentProps) {
const navigate = useNavigate()
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
}
const handleOpenProject = async () => {
// layout.projects.open(dir.)
@@ -24,7 +35,7 @@ export default function Layout(props: ParentProps) {
return (
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<A
href="/"
classList={{
@@ -33,19 +44,115 @@ export default function Layout(props: ParentProps) {
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => getFilename(project.directory))}
current={getFilename(currentDirectory())}
class="text-14-regular text-text-base"
variant="ghost"
/>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="Select session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-3xs"
variant="ghost"
/>
</div>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
</div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
<div
classList={{
"@container w-12 pb-5 shrink-0 bg-background-base": true,
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
<Show when={layout.sidebar.opened()}>
<div
class="absolute inset-y-0 right-0 z-10 w-2 translate-x-1/2 cursor-ew-resize"
onMouseDown={(e) => {
e.preventDefault()
const startX = e.clientX
const startWidth = layout.sidebar.width()
const maxWidth = window.innerWidth * 0.3
const minWidth = 150
const collapseThreshold = 80
let currentWidth = startWidth
document.body.style.userSelect = "none"
document.body.style.overflow = "hidden"
const onMouseMove = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.clientX - startX
currentWidth = startWidth + deltaX
const clampedWidth = Math.min(maxWidth, Math.max(minWidth, currentWidth))
layout.sidebar.resize(clampedWidth)
}
const onMouseUp = () => {
document.body.style.userSelect = ""
document.body.style.overflow = ""
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
if (currentWidth < collapseThreshold) {
layout.sidebar.close()
}
}
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
}}
/>
</Show>
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
<Button
@@ -109,7 +216,7 @@ export default function Layout(props: ParentProps) {
>
<Tooltip placement="right" value={session.title}>
<div
class="w-full px-2 py-1 rounded-md
class="w-full px-2 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
@@ -197,7 +304,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</div>
<main class="size-full overflow-x-hidden">{props.children}</main>
<main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
</div>
</div>
)

View File

@@ -1,4 +1,4 @@
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo } from "solid-js"
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
@@ -30,6 +30,8 @@ import { useSync } from "@/context/sync"
import { useSession } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Diff } from "@opencode-ai/ui/diff"
import { Terminal } from "@/components/terminal"
export default function Page() {
const layout = useLayout()
@@ -53,6 +55,14 @@ export default function Page() {
document.removeEventListener("keydown", handleKeyDown)
})
createEffect(() => {
if (layout.terminal.opened()) {
if (session.terminal.all().length === 0) {
session.terminal.new()
}
}
})
const handleKeyDown = (event: KeyboardEvent) => {
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
event.preventDefault()
@@ -72,6 +82,20 @@ export default function Page() {
document.documentElement.setAttribute("data-theme", nextTheme)
return
}
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
if (event.shiftKey) {
session.terminal.new()
return
}
layout.terminal.toggle()
return
}
// @ts-expect-error
if (document.activeElement?.dataset?.component === "terminal") {
return
}
const focused = document.activeElement === inputRef
if (focused) {
@@ -123,7 +147,6 @@ export default function Page() {
const handleTabClick = async (tab: string) => {
if (store.clickTimer) {
resetClickTimer()
// local.file.update(file.path, { ...file, pinned: true })
} else {
if (tab.startsWith("file://")) {
local.file.open(tab.replace("file://", ""))
@@ -141,7 +164,7 @@ export default function Page() {
const handleDragOver = (event: DragEvent) => {
const { draggable, droppable } = event
if (draggable && droppable) {
const currentTabs = session.layout.tabs.opened
const currentTabs = session.layout.tabs.all
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
const toIndex = currentTabs?.indexOf(droppable.id.toString())
if (fromIndex !== toIndex && toIndex !== undefined) {
@@ -259,308 +282,401 @@ export default function Page() {
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
return (
<div class="relative bg-background-base size-full overflow-x-hidden">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
<div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(session.usage.tokens() ?? 0)} Tokens`}
class="flex items-center gap-1.5"
>
<ProgressCircle percentage={session.usage.context() ?? 0} />
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
</Tooltip>
</div>
</Tabs.Trigger>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Trigger
value="review"
closeButton={
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
}
>
<div class="flex items-center gap-3">
<Show when={session.diffs()}>
<DiffChanges changes={session.diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={session.info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{session.info()?.summary?.files ?? 0}
</div>
</Show>
</div>
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col items-start">
<div class="min-h-0 grow w-full">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List>
<Tabs.Trigger value="chat">
<div class="flex gap-x-[17px] items-center">
<div>Session</div>
<Tooltip
value={`${new Intl.NumberFormat("en-US", {
notation: "compact",
compactDisplay: "short",
}).format(session.usage.tokens() ?? 0)} Tokens`}
class="flex items-center gap-1.5"
>
<ProgressCircle percentage={session.usage.context() ?? 0} />
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
</Tooltip>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={session.layout.tabs.opened ?? []}>
<For each={session.layout.tabs.opened ?? []}>
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => setStore("fileSelectOpen", true)}
/>
</Tooltip>
</div>
</Tabs.List>
</div>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"w-full flex-1 min-h-0": true,
grid: layout.review.state() === "tab",
flex: layout.review.state() === "pane",
}}
>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Trigger
value="review"
closeButton={
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
}
>
<div class="flex items-center gap-3">
<Show when={session.diffs()}>
<DiffChanges changes={session.diffs()} variant="bars" />
</Show>
<div class="flex items-center gap-1.5">
<div>Review</div>
<Show when={session.info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{session.info()?.summary?.files ?? 0}
</div>
</Show>
</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={session.layout.tabs.all ?? []}>
<For each={session.layout.tabs.all ?? []}>
{(tab) => (
<SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />
)}
</For>
</SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
<Tooltip value="Open file" class="flex items-center">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => setStore("fileSelectOpen", true)}
/>
</Tooltip>
</div>
</Tabs.List>
</div>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-146 mx-auto": !wide(),
"w-full flex-1 min-h-0": true,
grid: layout.review.state() === "tab",
flex: layout.review.state() === "pane",
}}
>
<Switch>
<Match when={session.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
working={session.working()}
wide={wide()}
/>
<SessionTurn
sessionID={session.id!}
messageID={session.messages.active()?.id!}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container: "w-full " + (wide() ? "max-w-146 mx-auto px-6" : "pr-6 pl-18"),
<div
classList={{
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-146 mx-auto": !wide(),
}}
>
<Switch>
<Match when={session.id}>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
working={session.working()}
wide={wide()}
/>
<SessionTurn
sessionID={session.id!}
messageID={session.messages.active()?.id!}
classes={{
root: "pb-20 flex-1 min-w-0",
content: "pb-20",
container:
"w-full " +
(wide()
? "max-w-146 mx-auto px-6"
: session.messages.user().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
diffComponent={Diff}
/>
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
</div>
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div class="w-full max-w-146 px-6">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
</div>
</div>
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div class="w-full max-w-146 px-6">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
<Show when={layout.review.state() === "pane" && session.diffs().length}>
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
}}
>
<SessionReview
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
diffComponent={Diff}
actions={
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
layout.review.tab()
session.layout.setActiveTab("review")
}}
/>
</Tooltip>
}
/>
</div>
</Show>
</div>
<Show when={layout.review.state() === "pane" && session.diffs().length}>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
}}
>
<SessionReview
classes={{
root: "pb-20",
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
actions={
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
layout.review.tab()
session.layout.setActiveTab("review")
}}
/>
</Tooltip>
}
diffComponent={Diff}
split
/>
</div>
</Show>
</div>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
</Tabs.Content>
</Show>
<For each={session.layout.tabs.all}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Code
file={{ name: f().path, contents: f().content?.content ?? "" }}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
<Show when={session.layout.tabs.active}>
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</Show>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
</div>
<div class="hidden shrink-0 w-56 p-2">
<Show
when={local.file.changes().length}
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
</span>
</button>
</li>
)}
</For>
</ul>
</Show>
</div>
<Show when={store.fileSelectOpen}>
<SelectDialog
defaultOpen
title="Select file"
placeholder="Search files"
emptyMessage="No files found"
items={local.file.searchFiles}
key={(x) => x}
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
local.file.open(x)
return session.layout.openTab("file://" + x)
}
return undefined
}}
>
{(i) => (
<div
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
"w-full flex items-center justify-between rounded-md": true,
}}
>
<SessionReview
classes={{
root: "pb-40",
header: "px-6",
container: "px-6",
}}
diffs={session.diffs()}
split
/>
</div>
</Tabs.Content>
</Show>
<For each={session.layout.tabs.opened}>
{(tab) => {
const [file] = createResource(
() => tab,
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<Tabs.Content value={tab} class="select-text mt-3">
<Switch>
<Match when={file()}>
{(f) => (
<Code
file={{ name: f().path, contents: f().content?.content ?? "" }}
overflow="scroll"
class="pb-40"
/>
)}
</Match>
</Switch>
</Tabs.Content>
)
}}
</For>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedFile) => {
const [file] = createResource(
() => draggedFile(),
async (tab) => {
if (tab.startsWith("file://")) {
return local.file.node(tab.replace("file://", ""))
}
return undefined
},
)
return (
<div class="relative px-6 h-12 flex items-center bg-background-stronger border-x border-border-weak-base border-b border-b-transparent">
<Show when={file()}>{(f) => <FileVisual active file={f()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
<Show when={session.layout.tabs.active}>
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</Show>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
</div>
<div class="hidden shrink-0 w-56 p-2">
<Show when={local.file.changes().length} fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
<div class="flex items-center gap-x-2 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
</button>
</li>
)}
</For>
</ul>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</div>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
</div>
)}
</SelectDialog>
</Show>
</div>
<Show when={store.fileSelectOpen}>
<SelectDialog
defaultOpen
title="Select file"
placeholder="Search files"
emptyMessage="No files found"
items={local.file.searchFiles}
key={(x) => x}
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
local.file.open(x)
return session.layout.openTab("file://" + x)
}
return undefined
}}
<Show when={layout.terminal.opened()}>
<div
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base"
style={{ height: `${layout.terminal.height()}px` }}
>
{(i) => (
<div
classList={{
"w-full flex items-center justify-between rounded-md": true,
}}
>
<div class="flex items-center gap-x-2 grow min-w-0">
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(i)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
</div>
<div
class="absolute inset-x-0 top-0 z-10 h-2 -translate-y-1/2 cursor-ns-resize"
onMouseDown={(e) => {
e.preventDefault()
const startY = e.clientY
const startHeight = layout.terminal.height()
const maxHeight = window.innerHeight * 0.6
const minHeight = 100
const collapseThreshold = 50
let currentHeight = startHeight
document.body.style.userSelect = "none"
document.body.style.overflow = "hidden"
const onMouseMove = (moveEvent: MouseEvent) => {
const deltaY = startY - moveEvent.clientY
currentHeight = startHeight + deltaY
const clampedHeight = Math.min(maxHeight, Math.max(minHeight, currentHeight))
layout.terminal.resize(clampedHeight)
}
const onMouseUp = () => {
document.body.style.userSelect = ""
document.body.style.overflow = ""
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
if (currentHeight < collapseThreshold) {
layout.terminal.close()
}
}
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
}}
/>
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Trigger
value={terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
)
}
>
{terminal.title}
</Tabs.Trigger>
)}
</For>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
</div>
)}
</SelectDialog>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
</Tabs>
</div>
</Show>
</div>
)

View File

@@ -2,7 +2,9 @@
/* tslint:disable */
/* eslint-disable */
/// <reference types="vite/client" />
interface ImportMetaEnv {}
interface ImportMetaEnv {
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
}

View File

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

View File

@@ -16,5 +16,6 @@
"paths": {
"@/*": ["./src/*"]
}
}
},
"exclude": ["dist"]
}

View File

@@ -1,15 +1,8 @@
import { defineConfig } from "vite"
import solidPlugin from "vite-plugin-solid"
import tailwindcss from "@tailwindcss/vite"
import path from "path"
import desktopPlugin from "./vite"
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
plugins: [tailwindcss(), solidPlugin()] as any,
plugins: [desktopPlugin] as any,
server: {
host: "0.0.0.0",
allowedHosts: true,

26
packages/desktop/vite.js Normal file
View File

@@ -0,0 +1,26 @@
import solidPlugin from "vite-plugin-solid"
import tailwindcss from "@tailwindcss/vite"
import { fileURLToPath } from "url"
/**
* @type {import("vite").PluginOption}
*/
export default [
{
name: "opencode-desktop:config",
config() {
return {
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
worker: {
format: "es",
},
}
},
},
tailwindcss(),
solidPlugin(),
]

21
packages/docs/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mintlify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

44
packages/docs/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Mintlify Starter Kit
Use the starter kit to get your docs deployed and ready to customize.
Click the green **Use this template** button at the top of this repo to copy the Mintlify starter kit. The starter kit contains examples with
- Guide pages
- Navigation
- Customizations
- API reference pages
- Use of popular components
**[Follow the full quickstart guide](https://starter.mintlify.com/quickstart)**
## Development
Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview your documentation changes locally. To install, use the following command:
```
npm i -g mint
```
Run the following command at the root of your documentation, where your `docs.json` is located:
```
mint dev
```
View your local preview at `http://localhost:3000`.
## Publishing changes
Install our GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app) to propagate changes from your repo to your deployment. Changes are deployed to production automatically after pushing to the default branch.
## Need help?
### Troubleshooting
- If your dev environment isn't running: Run `mint update` to ensure you have the most recent version of the CLI.
- If a page loads as a 404: Make sure you are running in a folder with a valid `docs.json`.
### Resources
- [Mintlify documentation](https://mintlify.com/docs)

View File

@@ -0,0 +1,83 @@
---
title: "Claude Code setup"
description: "Configure Claude Code for your documentation workflow"
icon: "asterisk"
---
Claude Code is Anthropic's official CLI tool. This guide will help you set up Claude Code to help you write and maintain your documentation.
## Prerequisites
- Active Claude subscription (Pro, Max, or API access)
## Setup
1. Install Claude Code globally:
```bash
npm install -g @anthropic-ai/claude-code
```
2. Navigate to your docs directory.
3. (Optional) Add the `CLAUDE.md` file below to your project.
4. Run `claude` to start.
## Create `CLAUDE.md`
Create a `CLAUDE.md` file at the root of your documentation repository to train Claude Code on your specific documentation standards:
```markdown
# Mintlify documentation
## Working relationship
- You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so
- ALWAYS ask for clarification rather than making assumptions
- NEVER lie, guess, or make up information
## Project context
- Format: MDX files with YAML frontmatter
- Config: docs.json for navigation, theme, settings
- Components: Mintlify components
## Content strategy
- Document just enough for user success - not too much, not too little
- Prioritize accuracy and usability of information
- Make content evergreen when possible
- Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason
- Check existing patterns for consistency
- Start by making the smallest reasonable changes
## Frontmatter requirements for pages
- title: Clear, descriptive page title
- description: Concise summary for SEO/navigation
## Writing standards
- Second-person voice ("you")
- Prerequisites at start of procedural content
- Test all code examples before publishing
- Match style and formatting of existing pages
- Include both basic and advanced use cases
- Language tags on all code blocks
- Alt text on all images
- Relative paths for internal links
## Git workflow
- NEVER use --no-verify when committing
- Ask how to handle uncommitted changes before starting
- Create a new branch when no clear branch exists for changes
- Commit frequently throughout development
- NEVER skip or disable pre-commit hooks
## Do not
- Skip frontmatter on any MDX file
- Use absolute URLs for internal links
- Include untested code examples
- Make assumptions - always ask for clarification
```

View File

@@ -0,0 +1,423 @@
---
title: "Cursor setup"
description: "Configure Cursor for your documentation workflow"
icon: "arrow-pointer"
---
Use Cursor to help write and maintain your documentation. This guide shows how to configure Cursor for better results on technical writing tasks and using Mintlify components.
## Prerequisites
- Cursor editor installed
- Access to your documentation repository
## Project rules
Create project rules that all team members can use. In your documentation repository root:
```bash
mkdir -p .cursor
```
Create `.cursor/rules.md`:
````markdown
# Mintlify technical writing rule
You are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices.
## Core writing principles
### Language and style requirements
- Use clear, direct language appropriate for technical audiences
- Write in second person ("you") for instructions and procedures
- Use active voice over passive voice
- Employ present tense for current states, future tense for outcomes
- Avoid jargon unless necessary and define terms when first used
- Maintain consistent terminology throughout all documentation
- Keep sentences concise while providing necessary context
- Use parallel structure in lists, headings, and procedures
### Content organization standards
- Lead with the most important information (inverted pyramid structure)
- Use progressive disclosure: basic concepts before advanced ones
- Break complex procedures into numbered steps
- Include prerequisites and context before instructions
- Provide expected outcomes for each major step
- Use descriptive, keyword-rich headings for navigation and SEO
- Group related information logically with clear section breaks
### User-centered approach
- Focus on user goals and outcomes rather than system features
- Anticipate common questions and address them proactively
- Include troubleshooting for likely failure points
- Write for scannability with clear headings, lists, and white space
- Include verification steps to confirm success
## Mintlify component reference
### Callout components
#### Note - Additional helpful information
<Note>
Supplementary information that supports the main content without interrupting flow
</Note>
#### Tip - Best practices and pro tips
<Tip>
Expert advice, shortcuts, or best practices that enhance user success
</Tip>
#### Warning - Important cautions
<Warning>
Critical information about potential issues, breaking changes, or destructive actions
</Warning>
#### Info - Neutral contextual information
<Info>
Background information, context, or neutral announcements
</Info>
#### Check - Success confirmations
<Check>
Positive confirmations, successful completions, or achievement indicators
</Check>
### Code components
#### Single code block
Example of a single code block:
```javascript config.js
const apiConfig = {
baseURL: "https://api.example.com",
timeout: 5000,
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
}
```
#### Code group with multiple languages
Example of a code group:
<CodeGroup>
```javascript Node.js
const response = await fetch('/api/endpoint', {
headers: { Authorization: `Bearer ${apiKey}` }
});
```
```python Python
import requests
response = requests.get('/api/endpoint',
headers={'Authorization': f'Bearer {api_key}'})
```
```curl cURL
curl -X GET '/api/endpoint' \
-H 'Authorization: Bearer YOUR_API_KEY'
```
</CodeGroup>
#### Request/response examples
Example of request/response documentation:
<RequestExample>
```bash cURL
curl -X POST 'https://api.example.com/users' \
-H 'Content-Type: application/json' \
-d '{"name": "John Doe", "email": "john@example.com"}'
```
</RequestExample>
<ResponseExample>
```json Success
{
"id": "user_123",
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
}
```
</ResponseExample>
### Structural components
#### Steps for procedures
Example of step-by-step instructions:
<Steps>
<Step title="Install dependencies">
Run `npm install` to install required packages.
<Check>
Verify installation by running `npm list`.
</Check>
</Step>
<Step title="Configure environment">
Create a `.env` file with your API credentials.
```bash
API_KEY=your_api_key_here
```
<Warning>
Never commit API keys to version control.
</Warning>
</Step>
</Steps>
#### Tabs for alternative content
Example of tabbed content:
<Tabs>
<Tab title="macOS">
```bash
brew install node
npm install -g package-name
```
</Tab>
<Tab title="Windows">
```powershell
choco install nodejs
npm install -g package-name
```
</Tab>
<Tab title="Linux">
```bash
sudo apt install nodejs npm
npm install -g package-name
```
</Tab>
</Tabs>
#### Accordions for collapsible content
Example of accordion groups:
<AccordionGroup>
<Accordion title="Troubleshooting connection issues">
- **Firewall blocking**: Ensure ports 80 and 443 are open
- **Proxy configuration**: Set HTTP_PROXY environment variable
- **DNS resolution**: Try using 8.8.8.8 as DNS server
</Accordion>
<Accordion title="Advanced configuration">
```javascript
const config = {
performance: { cache: true, timeout: 30000 },
security: { encryption: 'AES-256' }
};
```
</Accordion>
</AccordionGroup>
### Cards and columns for emphasizing information
Example of cards and card groups:
<Card title="Getting started guide" icon="rocket" href="/quickstart">
Complete walkthrough from installation to your first API call in under 10 minutes.
</Card>
<CardGroup cols={2}>
<Card title="Authentication" icon="key" href="/auth">
Learn how to authenticate requests using API keys or JWT tokens.
</Card>
<Card title="Rate limiting" icon="clock" href="/rate-limits">
Understand rate limits and best practices for high-volume usage.
</Card>
</CardGroup>
### API documentation components
#### Parameter fields
Example of parameter documentation:
<ParamField path="user_id" type="string" required>
Unique identifier for the user. Must be a valid UUID v4 format.
</ParamField>
<ParamField body="email" type="string" required>
User's email address. Must be valid and unique within the system.
</ParamField>
<ParamField query="limit" type="integer" default="10">
Maximum number of results to return. Range: 1-100.
</ParamField>
<ParamField header="Authorization" type="string" required>
Bearer token for API authentication. Format: `Bearer YOUR_API_KEY`
</ParamField>
#### Response fields
Example of response field documentation:
<ResponseField name="user_id" type="string" required>
Unique identifier assigned to the newly created user.
</ResponseField>
<ResponseField name="created_at" type="timestamp">
ISO 8601 formatted timestamp of when the user was created.
</ResponseField>
<ResponseField name="permissions" type="array">
List of permission strings assigned to this user.
</ResponseField>
#### Expandable nested fields
Example of nested field documentation:
<ResponseField name="user" type="object">
Complete user object with all associated data.
<Expandable title="User properties">
<ResponseField name="profile" type="object">
User profile information including personal details.
<Expandable title="Profile details">
<ResponseField name="first_name" type="string">
User's first name as entered during registration.
</ResponseField>
<ResponseField name="avatar_url" type="string | null">
URL to user's profile picture. Returns null if no avatar is set.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
### Media and advanced components
#### Frames for images
Wrap all images in frames:
<Frame>
<img src="/images/dashboard.png" alt="Main dashboard showing analytics overview" />
</Frame>
<Frame caption="The analytics dashboard provides real-time insights">
<img src="/images/analytics.png" alt="Analytics dashboard with charts" />
</Frame>
#### Videos
Use the HTML video element for self-hosted video content:
<video
controls
className="w-full aspect-video rounded-xl"
src="link-to-your-video.com"
> </video>
Embed YouTube videos using iframe elements:
<iframe
className="w-full aspect-video rounded-xl"
src="https://www.youtube.com/embed/4KzFe50RQkQ"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
#### Tooltips
Example of tooltip usage:
<Tooltip tip="Application Programming Interface - protocols for building software">
API
</Tooltip>
#### Updates
Use updates for changelogs:
<Update label="Version 2.1.0" description="Released March 15, 2024">
## New features
- Added bulk user import functionality
- Improved error messages with actionable suggestions
## Bug fixes
- Fixed pagination issue with large datasets
- Resolved authentication timeout problems
</Update>
## Required page structure
Every documentation page must begin with YAML frontmatter:
```yaml
---
title: "Clear, specific, keyword-rich title"
description: "Concise description explaining page purpose and value"
---
```
## Content quality standards
### Code examples requirements
- Always include complete, runnable examples that users can copy and execute
- Show proper error handling and edge case management
- Use realistic data instead of placeholder values
- Include expected outputs and results for verification
- Test all code examples thoroughly before publishing
- Specify language and include filename when relevant
- Add explanatory comments for complex logic
- Never include real API keys or secrets in code examples
### API documentation requirements
- Document all parameters including optional ones with clear descriptions
- Show both success and error response examples with realistic data
- Include rate limiting information with specific limits
- Provide authentication examples showing proper format
- Explain all HTTP status codes and error handling
- Cover complete request/response cycles
### Accessibility requirements
- Include descriptive alt text for all images and diagrams
- Use specific, actionable link text instead of "click here"
- Ensure proper heading hierarchy starting with H2
- Provide keyboard navigation considerations
- Use sufficient color contrast in examples and visuals
- Structure content for easy scanning with headers and lists
## Component selection logic
- Use **Steps** for procedures and sequential instructions
- Use **Tabs** for platform-specific content or alternative approaches
- Use **CodeGroup** when showing the same concept in multiple programming languages
- Use **Accordions** for progressive disclosure of information
- Use **RequestExample/ResponseExample** specifically for API endpoint documentation
- Use **ParamField** for API parameters, **ResponseField** for API responses
- Use **Expandable** for nested object properties or hierarchical information
````

View File

@@ -0,0 +1,96 @@
---
title: "Windsurf setup"
description: "Configure Windsurf for your documentation workflow"
icon: "water"
---
Configure Windsurf's Cascade AI assistant to help you write and maintain documentation. This guide shows how to set up Windsurf specifically for your Mintlify documentation workflow.
## Prerequisites
- Windsurf editor installed
- Access to your documentation repository
## Workspace rules
Create workspace rules that provide Windsurf with context about your documentation project and standards.
Create `.windsurf/rules.md` in your project root:
````markdown
# Mintlify technical writing rule
## Project context
- This is a documentation project on the Mintlify platform
- We use MDX files with YAML frontmatter
- Navigation is configured in `docs.json`
- We follow technical writing best practices
## Writing standards
- Use second person ("you") for instructions
- Write in active voice and present tense
- Start procedures with prerequisites
- Include expected outcomes for major steps
- Use descriptive, keyword-rich headings
- Keep sentences concise but informative
## Required page structure
Every page must start with frontmatter:
```yaml
---
title: "Clear, specific title"
description: "Concise description for SEO and navigation"
---
```
## Mintlify components
### Callouts
- `<Note>` for helpful supplementary information
- `<Warning>` for important cautions and breaking changes
- `<Tip>` for best practices and expert advice
- `<Info>` for neutral contextual information
- `<Check>` for success confirmations
### Code examples
- When appropriate, include complete, runnable examples
- Use `<CodeGroup>` for multiple language examples
- Specify language tags on all code blocks
- Include realistic data, not placeholders
- Use `<RequestExample>` and `<ResponseExample>` for API docs
### Procedures
- Use `<Steps>` component for sequential instructions
- Include verification steps with `<Check>` components when relevant
- Break complex procedures into smaller steps
### Content organization
- Use `<Tabs>` for platform-specific content
- Use `<Accordion>` for progressive disclosure
- Use `<Card>` and `<CardGroup>` for highlighting content
- Wrap images in `<Frame>` components with descriptive alt text
## API documentation requirements
- Document all parameters with `<ParamField>`
- Show response structure with `<ResponseField>`
- Include both success and error examples
- Use `<Expandable>` for nested object properties
- Always include authentication examples
## Quality standards
- Test all code examples before publishing
- Use relative paths for internal links
- Include alt text for all images
- Ensure proper heading hierarchy (start with h2)
- Check existing patterns for consistency
````

View File

@@ -0,0 +1,96 @@
---
title: "Development"
description: "Preview changes locally to update your docs"
---
<Info>**Prerequisites**: - Node.js version 19 or higher - A docs repository with a `docs.json` file</Info>
Follow these steps to install and run Mintlify on your operating system.
<Steps>
<Step title="Install the Mintlify CLI">
```bash
npm i -g mint
```
</Step>
<Step title="Preview locally">
Navigate to your docs directory where your `docs.json` file is located, and run the following command:
```bash
mint dev
```
A local preview of your documentation will be available at `http://localhost:3000`.
</Step>
</Steps>
## Custom ports
By default, Mintlify uses port 3000. You can customize the port Mintlify runs on by using the `--port` flag. For example, to run Mintlify on port 3333, use this command:
```bash
mint dev --port 3333
```
If you attempt to run Mintlify on a port that's already in use, it will use the next available port:
```md
Port 3000 is already in use. Trying 3001 instead.
```
## Mintlify versions
Please note that each CLI release is associated with a specific version of Mintlify. If your local preview does not align with the production version, please update the CLI:
```bash
npm mint update
```
## Validating links
The CLI can assist with validating links in your documentation. To identify any broken links, use the following command:
```bash
mint broken-links
```
## Deployment
If the deployment is successful, you should see the following:
<Frame>
<img
src="/images/checks-passed.png"
alt="Screenshot of a deployment confirmation message that says All checks have passed."
style={{ borderRadius: "0.5rem" }}
/>
</Frame>
## Code formatting
We suggest using extensions on your IDE to recognize and format MDX. If you're a VSCode user, consider the [MDX VSCode extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) for syntax highlighting, and [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) for code formatting.
## Troubleshooting
<AccordionGroup>
<Accordion title='Error: Could not load the "sharp" module using the darwin-arm64 runtime'>
This may be due to an outdated version of node. Try the following:
1. Remove the currently-installed version of the CLI: `npm remove -g mint`
2. Upgrade to Node v19 or higher.
3. Reinstall the CLI: `npm i -g mint`
</Accordion>
<Accordion title="Issue: Encountering an unknown error">
Solution: Go to the root of your device and delete the `~/.mintlify` folder. Then run `mint dev` again.
</Accordion>
</AccordionGroup>
Curious about what changed in the latest CLI version? Check out the [CLI changelog](https://www.npmjs.com/package/mintlify?activeTab=versions).

86
packages/docs/docs.json Normal file
View File

@@ -0,0 +1,86 @@
{
"$schema": "https://mintlify.com/docs.json",
"theme": "mint",
"name": "@opencode-ai/docs",
"colors": {
"primary": "#16A34A",
"light": "#07C983",
"dark": "#15803D"
},
"favicon": "/favicon.svg",
"servers": [
{
"url": "https://opencode.ai/openapi.json"
}
],
"navigation": {
"tabs": [
{
"tab": "Guides",
"groups": [
{
"group": "Getting started",
"pages": ["index", "quickstart", "development"]
},
{
"group": "Customization",
"pages": ["essentials/settings", "essentials/navigation"]
},
{
"group": "Writing content",
"pages": ["essentials/markdown", "essentials/code", "essentials/images", "essentials/reusable-snippets"]
},
{
"group": "AI tools",
"pages": ["ai-tools/cursor", "ai-tools/claude-code", "ai-tools/windsurf"]
}
]
},
{
"tab": "API Reference",
"openapi": "https://opencode.ai/openapi.json"
}
],
"global": {
"anchors": [
{
"anchor": "Documentation",
"href": "https://mintlify.com/docs",
"icon": "book-open-cover"
},
{
"anchor": "Blog",
"href": "https://mintlify.com/blog",
"icon": "newspaper"
}
]
}
},
"logo": {
"light": "/logo/light.svg",
"dark": "/logo/dark.svg"
},
"navbar": {
"links": [
{
"label": "Support",
"href": "mailto:hi@mintlify.com"
}
],
"primary": {
"type": "button",
"label": "Dashboard",
"href": "https://dashboard.mintlify.com"
}
},
"contextual": {
"options": ["copy", "view", "chatgpt", "claude", "perplexity", "mcp", "cursor", "vscode"]
},
"footer": {
"socials": {
"x": "https://x.com/mintlify",
"github": "https://github.com/mintlify",
"linkedin": "https://linkedin.com/company/mintlify"
}
}
}

View File

@@ -0,0 +1,35 @@
---
title: "Code blocks"
description: "Display inline code and code blocks"
icon: "code"
---
## Inline code
To denote a `word` or `phrase` as code, enclose it in backticks (`).
```
To denote a `word` or `phrase` as code, enclose it in backticks (`).
```
## Code blocks
Use [fenced code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) by enclosing code in three backticks and follow the leading ticks with the programming language of your snippet to get syntax highlighting. Optionally, you can also write the name of your code after the programming language.
```java HelloWorld.java
class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
```
````md
```java HelloWorld.java
class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
```
````

View File

@@ -0,0 +1,56 @@
---
title: "Images and embeds"
description: "Add image, video, and other HTML elements"
icon: "image"
---
<img style={{ borderRadius: "0.5rem" }} src="https://mintlify-assets.b-cdn.net/bigbend.jpg" />
## Image
### Using Markdown
The [markdown syntax](https://www.markdownguide.org/basic-syntax/#images) lets you add images using the following code
```md
![title](/path/image.jpg)
```
Note that the image file size must be less than 5MB. Otherwise, we recommend hosting on a service like [Cloudinary](https://cloudinary.com/) or [S3](https://aws.amazon.com/s3/). You can then use that URL and embed.
### Using embeds
To get more customizability with images, you can also use [embeds](/writing-content/embed) to add images
```html
<img height="200" src="/path/image.jpg" />
```
## Embeds and HTML elements
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/4KzFe50RQkQ"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ width: "100%", borderRadius: "0.5rem" }}
></iframe>
<br />
<Tip>
Mintlify supports [HTML tags in Markdown](https://www.markdownguide.org/basic-syntax/#html). This is helpful if you prefer HTML tags to Markdown syntax, and lets you create documentation with infinite flexibility.
</Tip>
### iFrames
Loads another HTML page within the document. Most commonly used for embedding videos.
```html
<iframe src="https://www.youtube.com/embed/4KzFe50RQkQ"> </iframe>
```

View File

@@ -0,0 +1,88 @@
---
title: "Markdown syntax"
description: "Text, title, and styling in standard markdown"
icon: "text-size"
---
## Titles
Best used for section headers.
```md
## Titles
```
### Subtitles
Best used for subsection headers.
```md
### Subtitles
```
<Tip>
Each **title** and **subtitle** creates an anchor and also shows up on the table of contents on the right.
</Tip>
## Text formatting
We support most markdown formatting. Simply add `**`, `_`, or `~` around text to format it.
| Style | How to write it | Result |
| ------------- | ----------------- | --------------- |
| Bold | `**bold**` | **bold** |
| Italic | `_italic_` | _italic_ |
| Strikethrough | `~strikethrough~` | ~strikethrough~ |
You can combine these. For example, write `**_bold and italic_**` to get **_bold and italic_** text.
You need to use HTML to write superscript and subscript text. That is, add `<sup>` or `<sub>` around your text.
| Text Size | How to write it | Result |
| ----------- | ------------------------ | ---------------------- |
| Superscript | `<sup>superscript</sup>` | <sup>superscript</sup> |
| Subscript | `<sub>subscript</sub>` | <sub>subscript</sub> |
## Linking to pages
You can add a link by wrapping text in `[]()`. You would write `[link to google](https://google.com)` to [link to google](https://google.com).
Links to pages in your docs need to be root-relative. Basically, you should include the entire folder path. For example, `[link to text](/writing-content/text)` links to the page "Text" in our components section.
Relative links like `[link to text](../text)` will open slower because we cannot optimize them as easily.
## Blockquotes
### Singleline
To create a blockquote, add a `>` in front of a paragraph.
> Dorothy followed her through many of the beautiful rooms in her castle.
```md
> Dorothy followed her through many of the beautiful rooms in her castle.
```
### Multiline
> Dorothy followed her through many of the beautiful rooms in her castle.
>
> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
```md
> Dorothy followed her through many of the beautiful rooms in her castle.
>
> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
```
### LaTeX
Mintlify supports [LaTeX](https://www.latex-project.org) through the Latex component.
<Latex>8 x (vk x H1 - H2) = (0,1)</Latex>
```md
<Latex>8 x (vk x H1 - H2) = (0,1)</Latex>
```

View File

@@ -0,0 +1,87 @@
---
title: "Navigation"
description: "The navigation field in docs.json defines the pages that go in the navigation menu"
icon: "map"
---
The navigation menu is the list of links on every website.
You will likely update `docs.json` every time you add a new page. Pages do not show up automatically.
## Navigation syntax
Our navigation syntax is recursive which means you can make nested navigation groups. You don't need to include `.mdx` in page names.
<CodeGroup>
```json Regular Navigation
"navigation": {
"tabs": [
{
"tab": "Docs",
"groups": [
{
"group": "Getting Started",
"pages": ["quickstart"]
}
]
}
]
}
```
```json Nested Navigation
"navigation": {
"tabs": [
{
"tab": "Docs",
"groups": [
{
"group": "Getting Started",
"pages": [
"quickstart",
{
"group": "Nested Reference Pages",
"pages": ["nested-reference-page"]
}
]
}
]
}
]
}
```
</CodeGroup>
## Folders
Simply put your MDX files in folders and update the paths in `docs.json`.
For example, to have a page at `https://yoursite.com/your-folder/your-page` you would make a folder called `your-folder` containing an MDX file called `your-page.mdx`.
<Warning>
You cannot use `api` for the name of a folder unless you nest it inside another folder. Mintlify uses Next.js which reserves the top-level `api` folder for internal server calls. A folder name such as `api-reference` would be accepted.
</Warning>
```json Navigation With Folder
"navigation": {
"tabs": [
{
"tab": "Docs",
"groups": [
{
"group": "Group Name",
"pages": ["your-folder/your-page"]
}
]
}
]
}
```
## Hidden pages
MDX files not included in `docs.json` will not show up in the sidebar but are accessible through the search bar and by linking directly to them.

View File

@@ -0,0 +1,112 @@
---
title: "Reusable snippets"
description: "Reusable, custom snippets to keep content in sync"
icon: "recycle"
---
import SnippetIntro from "/snippets/snippet-intro.mdx"
<SnippetIntro />
## Creating a custom snippet
**Pre-condition**: You must create your snippet file in the `snippets` directory.
<Note>
Any page in the `snippets` directory will be treated as a snippet and will not be rendered into a standalone page. If
you want to create a standalone page from the snippet, import the snippet into another file and call it as a
component.
</Note>
### Default export
1. Add content to your snippet file that you want to re-use across multiple
locations. Optionally, you can add variables that can be filled in via props
when you import the snippet.
```mdx snippets/my-snippet.mdx
Hello world! This is my content I want to reuse across pages. My keyword of the
day is {word}.
```
<Warning>
The content that you want to reuse must be inside the `snippets` directory in order for the import to work.
</Warning>
2. Import the snippet into your destination file.
```mdx destination-file.mdx
---
title: My title
description: My Description
---
import MySnippet from "/snippets/path/to/my-snippet.mdx"
## Header
Lorem impsum dolor sit amet.
<MySnippet word="bananas" />
```
### Reusable variables
1. Export a variable from your snippet file:
```mdx snippets/path/to/custom-variables.mdx
export const myName = "my name"
export const myObject = { fruit: "strawberries" }
;
```
2. Import the snippet from your destination file and use the variable:
```mdx destination-file.mdx
---
title: My title
description: My Description
---
import { myName, myObject } from "/snippets/path/to/custom-variables.mdx"
Hello, my name is {myName} and I like {myObject.fruit}.
```
### Reusable components
1. Inside your snippet file, create a component that takes in props by exporting
your component in the form of an arrow function.
```mdx snippets/custom-component.mdx
export const MyComponent = ({ title }) => (
<div>
<h1>{title}</h1>
<p>... snippet content ...</p>
</div>
)
;
```
<Warning>
MDX does not compile inside the body of an arrow function. Stick to HTML syntax when you can or use a default export
if you need to use MDX.
</Warning>
2. Import the snippet into your destination file and pass in the props
```mdx destination-file.mdx
---
title: My title
description: My Description
---
import { MyComponent } from "/snippets/custom-component.mdx"
Lorem ipsum dolor sit amet.
<MyComponent title={"Custom title"} />
```

View File

@@ -0,0 +1,317 @@
---
title: "Global Settings"
description: "Mintlify gives you complete control over the look and feel of your documentation using the docs.json file"
icon: "gear"
---
Every Mintlify site needs a `docs.json` file with the core configuration settings. Learn more about the [properties](#properties) below.
## Properties
<ResponseField name="name" type="string" required>
Name of your project. Used for the global title.
Example: `mintlify`
</ResponseField>
<ResponseField name="navigation" type="Navigation[]" required>
An array of groups with all the pages within that group
<Expandable title="Navigation">
<ResponseField name="group" type="string">
The name of the group.
Example: `Settings`
</ResponseField>
<ResponseField name="pages" type="string[]">
The relative paths to the markdown files that will serve as pages.
Example: `["customization", "page"]`
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="logo" type="string or object">
Path to logo image or object with path to "light" and "dark" mode logo images
<Expandable title="Logo">
<ResponseField name="light" type="string">
Path to the logo in light mode
</ResponseField>
<ResponseField name="dark" type="string">
Path to the logo in dark mode
</ResponseField>
<ResponseField name="href" type="string" default="/">
Where clicking on the logo links you to
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="favicon" type="string">
Path to the favicon image
</ResponseField>
<ResponseField name="colors" type="Colors">
Hex color codes for your global theme
<Expandable title="Colors">
<ResponseField name="primary" type="string" required>
The primary color. Used for most often for highlighted content, section headers, accents, in light mode
</ResponseField>
<ResponseField name="light" type="string">
The primary color for dark mode. Used for most often for highlighted content, section headers, accents, in dark
mode
</ResponseField>
<ResponseField name="dark" type="string">
The primary color for important buttons
</ResponseField>
<ResponseField name="background" type="object">
The color of the background in both light and dark mode
<Expandable title="Object">
<ResponseField name="light" type="string" required>
The hex color code of the background in light mode
</ResponseField>
<ResponseField name="dark" type="string" required>
The hex color code of the background in dark mode
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="topbarLinks" type="TopbarLink[]">
Array of `name`s and `url`s of links you want to include in the topbar
<Expandable title="TopbarLink">
<ResponseField name="name" type="string">
The name of the button.
Example: `Contact us`
</ResponseField>
<ResponseField name="url" type="string">
The url once you click on the button. Example: `https://mintlify.com/docs`
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="topbarCtaButton" type="Call to Action">
<Expandable title="Topbar Call to Action">
<ResponseField name="type" type={'"link" or "github"'} default="link">
Link shows a button. GitHub shows the repo information at the url provided including the number of GitHub stars.
</ResponseField>
<ResponseField name="url" type="string">
If `link`: What the button links to.
If `github`: Link to the repository to load GitHub information from.
</ResponseField>
<ResponseField name="name" type="string">
Text inside the button. Only required if `type` is a `link`.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="versions" type="string[]">
Array of version names. Only use this if you want to show different versions of docs with a dropdown in the navigation
bar.
</ResponseField>
<ResponseField name="anchors" type="Anchor[]">
An array of the anchors, includes the `icon`, `color`, and `url`.
<Expandable title="Anchor">
<ResponseField name="icon" type="string">
The [Font Awesome](https://fontawesome.com/search?q=heart) icon used to feature the anchor.
Example: `comments`
</ResponseField>
<ResponseField name="name" type="string">
The name of the anchor label.
Example: `Community`
</ResponseField>
<ResponseField name="url" type="string">
The start of the URL that marks what pages go in the anchor. Generally, this is the name of the folder you put your pages in.
</ResponseField>
<ResponseField name="color" type="string">
The hex color of the anchor icon background. Can also be a gradient if you pass an object with the properties `from` and `to` that are each a hex color.
</ResponseField>
<ResponseField name="version" type="string">
Used if you want to hide an anchor until the correct docs version is selected.
</ResponseField>
<ResponseField name="isDefaultHidden" type="boolean" default="false">
Pass `true` if you want to hide the anchor until you directly link someone to docs inside it.
</ResponseField>
<ResponseField name="iconType" default="duotone" type="string">
One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin"
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="topAnchor" type="Object">
Override the default configurations for the top-most anchor.
<Expandable title="Object">
<ResponseField name="name" default="Documentation" type="string">
The name of the top-most anchor
</ResponseField>
<ResponseField name="icon" default="book-open" type="string">
Font Awesome icon.
</ResponseField>
<ResponseField name="iconType" default="duotone" type="string">
One of: "brands", "duotone", "light", "sharp-solid", "solid", or "thin"
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="tabs" type="Tabs[]">
An array of navigational tabs.
<Expandable title="Tabs">
<ResponseField name="name" type="string">
The name of the tab label.
</ResponseField>
<ResponseField name="url" type="string">
The start of the URL that marks what pages go in the tab. Generally, this is the name of the folder you put your
pages in.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="api" type="API">
Configuration for API settings. Learn more about API pages at [API Components](/api-playground/demo).
<Expandable title="API">
<ResponseField name="baseUrl" type="string">
The base url for all API endpoints. If `baseUrl` is an array, it will enable for multiple base url
options that the user can toggle.
</ResponseField>
<ResponseField name="auth" type="Auth">
<Expandable title="Auth">
<ResponseField name="method" type='"bearer" | "basic" | "key"'>
The authentication strategy used for all API endpoints.
</ResponseField>
<ResponseField name="name" type="string">
The name of the authentication parameter used in the API playground.
If method is `basic`, the format should be `[usernameName]:[passwordName]`
</ResponseField>
<ResponseField name="inputPrefix" type="string">
The default value that's designed to be a prefix for the authentication input field.
E.g. If an `inputPrefix` of `AuthKey` would inherit the default input result of the authentication field as `AuthKey`.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="playground" type="Playground">
Configurations for the API playground
<Expandable title="Playground">
<ResponseField name="mode" default="show" type='"show" | "simple" | "hide"'>
Whether the playground is showing, hidden, or only displaying the endpoint with no added user interactivity `simple`
Learn more at the [playground guides](/api-playground/demo)
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="maintainOrder" type="boolean">
Enabling this flag ensures that key ordering in OpenAPI pages matches the key ordering defined in the OpenAPI file.
<Warning>This behavior will soon be enabled by default, at which point this field will be deprecated.</Warning>
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="openapi" type="string | string[]">
A string or an array of strings of URL(s) or relative path(s) pointing to your
OpenAPI file.
Examples:
<CodeGroup>
```json Absolute
"openapi": "https://example.com/openapi.json"
```
```json Relative
"openapi": "/openapi.json"
```
```json Multiple
"openapi": ["https://example.com/openapi1.json", "/openapi2.json", "/openapi3.json"]
```
</CodeGroup>
</ResponseField>
<ResponseField name="footerSocials" type="FooterSocials">
An object of social media accounts where the key:property pair represents the social media platform and the account url.
Example:
```json
{
"x": "https://x.com/mintlify",
"website": "https://mintlify.com"
}
```
<Expandable title="FooterSocials">
<ResponseField name="[key]" type="string">
One of the following values `website`, `facebook`, `x`, `discord`, `slack`, `github`, `linkedin`, `instagram`, `hacker-news`
Example: `x`
</ResponseField>
<ResponseField name="property" type="string">
The URL to the social platform.
Example: `https://x.com/mintlify`
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="feedback" type="Feedback">
Configurations to enable feedback buttons
<Expandable title="Feedback">
<ResponseField name="suggestEdit" type="boolean" default="false">
Enables a button to allow users to suggest edits via pull requests
</ResponseField>
<ResponseField name="raiseIssue" type="boolean" default="false">
Enables a button to allow users to raise an issue about the documentation
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="modeToggle" type="ModeToggle">
Customize the dark mode toggle.
<Expandable title="ModeToggle">
<ResponseField name="default" type={'"light" or "dark"'}>
Set if you always want to show light or dark mode for new users. When not
set, we default to the same mode as the user's operating system.
</ResponseField>
<ResponseField name="isHidden" type="boolean" default="false">
Set to true to hide the dark/light mode toggle. You can combine `isHidden` with `default` to force your docs to only use light or dark mode. For example:
<CodeGroup>
```json Only Dark Mode
"modeToggle": {
"default": "dark",
"isHidden": true
}
```
```json Only Light Mode
"modeToggle": {
"default": "light",
"isHidden": true
}
```
</CodeGroup>
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="backgroundImage" type="string">
A background image to be displayed behind every page. See example with [Infisical](https://infisical.com/docs) and
[FRPC](https://frpc.io).
</ResponseField>

19
packages/docs/favicon.svg Normal file
View File

@@ -0,0 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.06145 23.1079C5.26816 22.3769 -3.39077 20.6274 1.4173 5.06384C9.6344 6.09939 16.9728 14.0644 9.06145 23.1079Z" fill="url(#paint0_linear_17557_2021)"/>
<path d="M8.91928 23.0939C5.27642 21.2223 0.78371 4.20891 17.0071 0C20.7569 7.19341 19.6212 16.5452 8.91928 23.0939Z" fill="url(#paint1_linear_17557_2021)"/>
<path d="M8.91388 23.0788C8.73534 19.8817 10.1585 9.08525 23.5699 13.1107C23.1812 20.1229 18.984 26.4182 8.91388 23.0788Z" fill="url(#paint2_linear_17557_2021)"/>
<defs>
<linearGradient id="paint0_linear_17557_2021" x1="3.77557" y1="5.91571" x2="5.23185" y2="21.5589" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1" stop-color="#15803D"/>
</linearGradient>
<linearGradient id="paint1_linear_17557_2021" x1="12.1711" y1="-0.718425" x2="10.1897" y2="22.9832" gradientUnits="userSpaceOnUse">
<stop stop-color="#16A34A"/>
<stop offset="1" stop-color="#4ADE80"/>
</linearGradient>
<linearGradient id="paint2_linear_17557_2021" x1="23.1327" y1="15.353" x2="9.33841" y2="18.5196" gradientUnits="userSpaceOnUse">
<stop stop-color="#4ADE80"/>
<stop offset="1" stop-color="#0D9373"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

56
packages/docs/index.mdx Normal file
View File

@@ -0,0 +1,56 @@
---
title: "Introduction"
description: "Welcome to the new home for your documentation"
---
## Setting up
Get your documentation site up and running in minutes.
<Card title="Start here" icon="rocket" href="/quickstart" horizontal>
Follow our three step quickstart guide.
</Card>
## Make it yours
Design a docs site that looks great and empowers your users.
<Columns cols={2}>
<Card title="Edit locally" icon="pen-to-square" href="/development">
Edit your docs locally and preview them in real time.
</Card>
<Card title="Customize your site" icon="palette" href="/essentials/settings">
Customize the design and colors of your site to match your brand.
</Card>
<Card title="Set up navigation" icon="map" href="/essentials/navigation">
Organize your docs to help users find what they need and succeed with your product.
</Card>
<Card title="API documentation" icon="terminal" href="/api-reference/introduction">
Auto-generate API documentation from OpenAPI specifications.
</Card>
</Columns>
## Create beautiful pages
Everything you need to create world-class documentation.
<Columns cols={2}>
<Card title="Write with MDX" icon="pen-fancy" href="/essentials/markdown">
Use MDX to style your docs pages.
</Card>
<Card title="Code samples" icon="code" href="/essentials/code">
Add sample code to demonstrate how to use your product.
</Card>
<Card title="Images" icon="image" href="/essentials/images">
Display images and other media.
</Card>
<Card title="Reusable snippets" icon="recycle" href="/essentials/reusable-snippets">
Write once and reuse across your docs.
</Card>
</Columns>
## Need inspiration?
<Card title="See complete examples" icon="stars" href="https://mintlify.com/customers">
Browse our showcase of exceptional documentation sites.
</Card>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,81 @@
---
title: "Quickstart"
description: "Start building awesome documentation in minutes"
---
## Get started in three steps
Get your documentation site running locally and make your first customization.
### Step 1: Set up your local environment
<AccordionGroup>
<Accordion icon="copy" title="Clone your docs locally">
During the onboarding process, you created a GitHub repository with your docs content if you didn't already have
one. You can find a link to this repository in your [dashboard](https://dashboard.mintlify.com). To clone the
repository locally so that you can make and preview changes to your docs, follow the [Cloning a
repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) guide
in the GitHub docs.
</Accordion>
<Accordion icon="rectangle-terminal" title="Start the preview server">
1. Install the Mintlify CLI: `npm i -g mint` 2. Navigate to your docs directory and run: `mint dev` 3. Open
`http://localhost:3000` to see your docs live!
<Tip>Your preview updates automatically as you edit files.</Tip>
</Accordion>
</AccordionGroup>
### Step 2: Deploy your changes
<AccordionGroup>
<Accordion icon="github" title="Install our GitHub app">
Install the Mintlify GitHub app from your [dashboard](https://dashboard.mintlify.com/settings/organization/github-app).
Our GitHub app automatically deploys your changes to your docs site, so you don't need to manage deployments yourself.
</Accordion>
<Accordion icon="palette" title="Update your site name and colors">
For a first change, let's update the name and colors of your docs site.
1. Open `docs.json` in your editor.
2. Change the `"name"` field to your project name.
3. Update the `"colors"` to match your brand.
4. Save and see your changes instantly at `http://localhost:3000`.
<Tip>Try changing the primary color to see an immediate difference!</Tip>
</Accordion>
</AccordionGroup>
### Step 3: Go live
<Accordion icon="rocket" title="Publish your docs">
1. Commit and push your changes. 2. Your docs will update and be live in moments!
</Accordion>
## Next steps
Now that you have your docs running, explore these key features:
<CardGroup cols={2}>
<Card title="Write Content" icon="pen-to-square" href="/essentials/markdown">
Learn MDX syntax and start writing your documentation.
</Card>
<Card title="Customize style" icon="palette" href="/essentials/settings">
Make your docs match your brand perfectly.
</Card>
<Card title="Add code examples" icon="square-code" href="/essentials/code">
Include syntax-highlighted code blocks.
</Card>
<Card title="API documentation" icon="code" href="/api-reference/introduction">
Auto-generate API docs from OpenAPI specs.
</Card>
</CardGroup>
<Note>
**Need help?** See our [full documentation](https://mintlify.com/docs) or join our
[community](https://mintlify.com/community).
</Note>

View File

@@ -0,0 +1,4 @@
One of the core principles of software development is DRY (Don't Repeat
Yourself). This is a principle that applies to documentation as
well. If you find yourself repeating the same content in multiple places, you
should consider creating a custom snippet to keep your content in sync.

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