Compare commits

..
117 Commits
Author SHA1 Message Date
opencode 1fd676528d release: v1.0.48 2025-11-09 01:44:10 +00:00
GitHub Action 0a2801444b chore: format code 2025-11-09 01:38:01 +00:00
Dax Raad c9adbc7c21 tui: add logging when creating project instances to help users debug startup issues 2025-11-08 20:37:08 -05:00
opencode ba8de38435 release: v1.0.47 2025-11-09 01:34:24 +00:00
Dax Raad 8166612467 tui: fix continue session navigation to use most recent session instead of oldest session 2025-11-08 20:28:23 -05:00
Dax Raad 4d20e1c3c6 Merge remote-tracking branch 'origin/dev' into dev 2025-11-08 20:21:02 -05:00
Dax Raad 4bb7ea9127 improve startup speed 2025-11-08 20:18:36 -05:00
GitHub Action 969af4d541 chore: format code 2025-11-08 22:32:25 +00:00
Christopher SaccaandGitHub 271b679058 fix(lsp): handle optional requests to avoid MethodNotFound (-32601) with MATLAB Language Server (#4007) 2025-11-08 16:31:39 -06:00
GitHub Action 83b16cb18e chore: format code 2025-11-08 22:27:51 +00:00
Kamaal FarahGitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
431ffc94f5 fix(theme): filter out null values from theme palette (#4083)
Signed-off-by: Kamaal Farah <kamaal.f1@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-11-08 16:27:07 -06:00
opencode 47b4cc6d53 release: v1.0.46 2025-11-08 21:48:37 +00:00
Dax Raad ce9b758d0a tui: display 'Free' badge for zero-cost models in model selection dialog 2025-11-08 16:20:30 -05:00
Frank f8a1a0b26f zen: fix billing button 2025-11-08 10:33:56 -05:00
GitHub Action 6ecaf83f76 chore: format code 2025-11-08 15:20:53 +00:00
Frank 30b1ae5d4b zen: rate limit 2025-11-08 10:18:21 -05:00
GitHub Action 9cd465f9c0 ignore: update download stats 2025-11-08 2025-11-08 12:04:10 +00:00
opencode 1747979568 release: v1.0.45 2025-11-08 02:25:51 +00:00
Dax Raad 062023fa06 chore(prompt): refine polaris instructions for practical behavior 2025-11-07 21:18:49 -05:00
Dax Raad 954a796b8a core: route polaris-alpha models to polaris system prompt 2025-11-07 21:07:59 -05:00
GitHub Action 34ff87d504 chore: format code 2025-11-08 01:59:02 +00:00
Dax Raad 16357e8041 chore: standardize prettier printWidth to 120 2025-11-07 20:58:17 -05:00
Dax Raad dabb1aa719 fix(prompt): prevent title generation refusal responses 2025-11-07 20:50:15 -05:00
Aiden Cline 7af3380455 chore: adjust cfg code 2025-11-07 19:18:57 -06:00
GitHub Action dcaa90808e chore: format code 2025-11-08 00:59:52 +00:00
Jay V 01705fd467 ignore: docs: fix type errors in sitemap generation script 2025-11-07 19:55:25 -05:00
Jay V 006f3bdeb6 ignore: docs: add sitemap.xml to gitignore 2025-11-07 19:51:47 -05:00
Jay V 1d43b4e6d7 ignore: docs: add automated sitemap generation for main app and docs routes 2025-11-07 19:51:46 -05:00
Aiden Cline 8cef7940fe allow reading of opencode.json(c) from .opencode/ 2025-11-07 17:35:08 -06:00
Sebastian Herrlinger b2dd9fdfdf upgrade to opentui 0.1.39 supporting shift/ctrl+return/esc custom csi in ghostty 2025-11-08 00:17:07 +01:00
Aiden Cline b82a52cb85 ignore: update log 2025-11-07 15:56:37 -06:00
Aiden Cline 7294d86778 ignore: symlink zed extension license 2025-11-07 15:41:01 -06:00
Aiden Cline 3bb3f4f2c9 ignore: fix 2025-11-07 15:32:19 -06:00
GitHub Action d31f97343c chore: format code 2025-11-07 21:28:26 +00:00
Jay V 536d10e5ab ignore: lander add canonical urls and h1 tags to all landing pages 2025-11-07 16:27:35 -05:00
Jay V 9885c716f3 ignore: lander use h1 tags for main headings on landing and zen pages 2025-11-07 16:27:34 -05:00
opencode 39461fbbce release: v1.0.44 2025-11-07 21:24:15 +00:00
Dax Raad 1a2b3701f2 tui: show more sessions in list and fix sync timing to prevent race conditions 2025-11-07 16:19:44 -05:00
Aiden Cline 0a395d8783 ignore: update version 2025-11-07 15:18:44 -06:00
Aiden Cline 79bb22a573 ci: auto update zed extension 2025-11-07 15:17:26 -06:00
GitHub Action 4271df96d2 chore: format code 2025-11-07 21:09:47 +00:00
Aiden Cline aa07be09e1 ignore: update zed extension 2025-11-07 15:08:50 -06:00
opencode 5d6bdca6d0 release: v1.0.43 2025-11-07 21:04:26 +00:00
Dax Raad 58bbe9e689 ci: add optional version parameter to publish workflow
Allows overriding the version when publishing releases instead of only using semantic bumping. This gives maintainers more control over release versioning for special cases or hotfixes.
2025-11-07 15:58:51 -05:00
Sebastian Herrlinger b5a035ceab upgrade to opentui to fix disappearing content (again) and sticky scroll 2025-11-07 21:38:40 +01:00
Aiden Cline b3c6d0b08a fix formatters 2025-11-07 14:10:20 -06:00
Aiden Cline 090d27df11 chore: rm debug logs 2025-11-07 14:00:18 -06:00
Adam b374a6cac9 fix(desktop): stop icon size 2025-11-07 13:58:52 -06:00
Aiden ClineandGitHub 73cd8a334c rework acp to compensate for changes in Zed IDE (#4050) 2025-11-07 13:57:12 -06:00
Adam b46c3f2a26 fix(desktop): prompt input issues (wip) 2025-11-07 13:54:53 -06:00
Adam 45fabec091 fix(desktop): prompt input on non-chat tabs 2025-11-07 13:54:53 -06:00
Dax Raad a96365fd81 Add command bar action to rename sessions 2025-11-07 14:51:44 -05:00
GitHub Action 5f7e1e099b chore: format code 2025-11-07 19:47:53 +00:00
Dax Raad d462e380f4 fix: update references after moving message functions to MessageV2 namespace 2025-11-07 14:47:05 -05:00
Adam c5a558f3da chore(desktop): remove dead code 2025-11-07 13:34:41 -06:00
Adam 7f51b181d4 chore(desktop): cleanup shiki theme stuff 2025-11-07 13:30:07 -06:00
Adam 7adbc3ad44 fix(desktop): code tab padding 2025-11-07 13:14:57 -06:00
Adam 89922a8598 fix(desktop): prompt input missing on new session 2025-11-07 12:56:07 -06:00
Adam 3a1d1a6284 feat(desktop): custom syntax colors 2025-11-07 12:48:17 -06:00
Dax Raad 4463d319c9 fix scroll when no session exists 2025-11-07 13:22:03 -05:00
GitHub Actionandopencode c6eea0343d chore: format code 2025-11-07 18:11:40 +00:00
opencode e317e7e481 release: v1.0.41 2025-11-07 18:11:39 +00:00
Dax Raad 287855336d allow not specifying a limit on messages endpoint 2025-11-07 13:05:19 -05:00
opencode d55f4f3322 release: v1.0.40 2025-11-07 17:37:31 +00:00
Dax Raad b708d0ecec disable scrollbar temporarily because of text wrap issues 2025-11-07 12:31:28 -05:00
Jinhyeok LeeandGitHub afb831c93c vscode: Add VS Code Insiders support (#4019) 2025-11-07 10:31:16 -06:00
Adam 14397651b5 ignore: test file 2025-11-07 09:05:09 -06:00
Adam e5804f64f9 fix(desktop): layout quirks 2025-11-07 06:26:26 -06:00
GitHub Action ce7b73170f ignore: update download stats 2025-11-07 2025-11-07 12:04:25 +00:00
Dax Raad 9554abb56e message storage performance improvements 2025-11-07 01:11:47 -05:00
Charles David MupendeandGitHub d0f5c825bd feat: implement network IP retrieval for remote access in web command (#3945) 2025-11-06 19:37:23 -06:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-nodeGitHub ActionAiden Cline
9f603e39a6 Fixed ACP to respect user's default model config. (#4006)
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: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 19:07:27 -06:00
GitHub Action da51c9dfac chore: format code 2025-11-07 01:06:38 +00:00
9e04ff013c fix: resolve Mistral API compatibility issues (#2440)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 20:05:59 -05:00
opencode 6bfccace0c release: v1.0.39 2025-11-07 01:04:05 +00:00
Dax Raad b25d4f9dfb fix issue with input randomly breaking 2025-11-06 19:48:10 -05:00
GitHub Action d1962ca5a7 chore: format code 2025-11-07 00:14:59 +00:00
Dax Raad 25f31f3096 codex tweaks 2025-11-06 19:14:01 -05:00
opencode 11a6f0886e release: v1.0.37 2025-11-06 23:14:32 +00:00
DaxandGitHub 3ba7e243d0 system theme (#4010) 2025-11-06 18:00:09 -05:00
a2ab019317 fix: resolve Mistral API compatibility issues (#2440)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 16:53:02 -06:00
Jay V 21957406ff docs: add Deep Infra provider documentation 2025-11-06 17:08:55 -05:00
Adam 61c4747fbe fix(desktop): diff highlight rendering 2025-11-06 15:58:45 -06:00
Adam 957c43aa09 fix(desktop): review tab padding 2025-11-06 15:34:47 -06:00
Adam 96c57418f3 feat(desktop): review flow 2025-11-06 15:13:06 -06:00
b8c51e307f Fix: Auto-scroll to bottom when selecting session from list (#3988)
Co-authored-by: shanjairaj7 <shanjairaj7@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-06 14:29:39 -06:00
Frank 6791233ca0 Add desktop-feedback 2025-11-06 14:29:11 -05:00
GitHub Action cd6072ec58 chore: format code 2025-11-06 18:59:34 +00:00
Jay V 017e42bbcd docs: align web package favicon with console design for consistency 2025-11-06 13:58:52 -05:00
opencode 2d20582802 release: v1.0.36 2025-11-06 18:55:54 +00:00
GitHub Action 2bcc00dbf0 chore: format code 2025-11-06 18:21:59 +00:00
Dax Raad e45e94634f only display last 100 messages in TUI 2025-11-06 13:21:15 -05:00
Aiden Cline de1278414f fix: opencode run not parsing model string 2025-11-06 12:09:13 -06:00
Dax Raad 3c2803fd9a flaky test 2025-11-06 13:05:53 -05:00
GitHub Action 90c2b26733 chore: format code 2025-11-06 18:03:50 +00:00
Dax Raad 1ea3a8eb9b big format 2025-11-06 13:03:12 -05:00
Aiden Cline 8729edc5e0 update import command to accept share links 2025-11-06 11:55:57 -06:00
Aiden Cline d8bcf1f5f3 ci: update auto label 2025-11-06 11:46:42 -06:00
Dax Raad 67f3c934fe fix tests 2025-11-06 11:42:46 -05:00
Aiden Cline 065f656fb0 chore: rm hanging test 2025-11-06 10:39:32 -06:00
Dax Raad f636d937c4 fix undo command breaking other commands 2025-11-06 11:25:37 -05:00
Adam 492bf51a0d fix(desktop): sidebar collapsed width 2025-11-06 09:56:46 -06:00
Adam 81ab127f63 fix(desktop): demo type error 2025-11-06 09:49:39 -06:00
Adam 6ba7c54bab feat(desktop): collapsible sidebar 2025-11-06 09:48:51 -06:00
Adam 146bae82cb fix(desktop): button styles 2025-11-06 09:48:50 -06:00
Adam ab345cf0da feat(desktop): better tooltips 2025-11-06 09:48:50 -06:00
GitHub Action a1836527ce ignore: update download stats 2025-11-06 2025-11-06 12:04:56 +00:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-nodeAiden Cline
49e4cfb286 Added big dot (●) indicator for current session in modal (#3980)
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-11-06 01:09:59 -06:00
Frankandopencode e52bfab79d Update sst 2025-11-06 05:42:25 +00:00
opencode cc6d5c8ddd release: v1.0.35 2025-11-06 05:42:24 +00:00
Dax Raad afe8508949 fix homebrew upgrade 2025-11-06 00:37:44 -05:00
opencode 7c098c8849 release: v1.0.34 2025-11-06 02:35:17 +00:00
Dax Raad 11d6005b77 tui: reduce scrollbar visual prominence for less distracting interface 2025-11-05 21:30:40 -05:00
Dax Raad 2cc072b3dc enable scrollbar 2025-11-05 21:27:23 -05:00
opencode 86247b8ea9 release: v1.0.33 2025-11-06 02:18:41 +00:00
Dax Raad 0a5a02043c tui: move debug shortcuts to command palette for better discoverability 2025-11-05 21:13:35 -05:00
Sebastian Herrlingerandopencode 6e553f7e20 upgrade to opentui v0.1.36 2025-11-06 01:43:35 +00:00
267 changed files with 7315 additions and 11585 deletions
+2 -2
View File
@@ -20,8 +20,8 @@ jobs:
const title = issue.title;
const description = issue.body || '';
// Check for "opencode web" or desktop keywords
const webPattern = /(opencode web|\bdesktop\b)/i;
// Check for "opencode web" keyword
const webPattern = /(opencode web)/i;
const isWebRelated = webPattern.test(title) || webPattern.test(description);
// Check for version patterns like v1.0.x or 1.0.x
+5
View File
@@ -12,6 +12,10 @@ on:
- major
- minor
- patch
version:
description: "Override version (optional)"
required: false
type: string
concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -62,6 +66,7 @@ jobs:
./script/publish.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 }}
+23 -23
View File
@@ -21,27 +21,27 @@
json
{
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
"recipient_name": "multi_tool_use.parallel",
"parameters": {
"tool_uses": [
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.tsx"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.ts"
}
},
{
"recipient_name": "functions.read",
"parameters": {
"filePath": "path/to/file.md"
}
}
]
}
}
+3
View File
@@ -131,3 +131,6 @@
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
+59 -40
View File
@@ -11,7 +11,7 @@
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.19",
"sst": "3.17.23",
"turbo": "2.5.6",
},
},
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -106,12 +106,15 @@
"@cloudflare/workers-types": "catalog:",
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/node": "catalog:",
"cloudflare": "5.2.0",
},
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -151,7 +154,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -167,7 +170,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.32",
"version": "1.0.48",
"bin": {
"opencode": "./bin/opencode",
},
@@ -185,8 +188,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opentui/core": "0.1.35",
"@opentui/solid": "0.1.35",
"@opentui/core": "0.0.0-20251108-0c7899b1",
"@opentui/solid": "0.0.0-20251108-0c7899b1",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -245,7 +248,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -265,7 +268,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.32",
"version": "1.0.48",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -276,7 +279,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -289,7 +292,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -319,7 +322,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -365,7 +368,7 @@
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.2",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
@@ -962,21 +965,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.35", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.35", "@opentui/core-darwin-x64": "0.1.35", "@opentui/core-linux-arm64": "0.1.35", "@opentui/core-linux-x64": "0.1.35", "@opentui/core-win32-arm64": "0.1.35", "@opentui/core-win32-x64": "0.1.35", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-t7xUaie+ri7ROXcWWLrQ0XY0Mo2kH6gqyagZrNRgUjH1zudqPBYJddiw7Kc9LMuPtV8usPzlzjXIny+EzuawkA=="],
"@opentui/core": ["@opentui/core@0.0.0-20251108-0c7899b1", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-darwin-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-x64": "0.0.0-20251108-0c7899b1", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-uJ7wbVw2v5NnL6g3v72SjPLUwMl2wqOejUEo8t4NeBA8nsboSxggqkrqOYf6OOmCADoAqyFDY7akZMsz6HMZtg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.35", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mSu6sS4a8DBejMqr06xtRdLtS+8m4oRzFVNtiKuY1T9PL74SnRmUl3VFxseY19ZMSVVAz/7hHPFX+ttmoT2pEw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.35", "", { "os": "darwin", "cpu": "x64" }, "sha512-HqIbf25OSJgsxfe+G6ZvpsGolj1V8yTIXrAUcRSqCmw/Xdp82JqTnI6zx1cxQSllVmF4SxT+WwTRG216DE5MMw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "x64" }, "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.35", "", { "os": "linux", "cpu": "arm64" }, "sha512-zNZpKg2fxZTsMJAjW3M9Il+1if61oe+d3m98hPm1i/UDtM+Fj8pkU0XBnsIJHfP5GVNnVHuLksXDpqmHquNmOg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.35", "", { "os": "linux", "cpu": "x64" }, "sha512-KKLw9Po+KC0u0JWG9p1bCWKro3dzSg56QQ8Y9CR/wT2ywTatNrtiYeymoeft3BogWSz65PxQr7Dw8kwdUOD3yQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "x64" }, "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.35", "", { "os": "win32", "cpu": "arm64" }, "sha512-5ycmzf0D8SkaR4xlTZqneegFt4w4RK2Q9yZC3ZiAclNMtffVnWOkK/3zIpCwTdi2I1wHPVG0doX6pi0EUM7f2A=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "arm64" }, "sha512-C7JLWuNN3w2txiVx3demwNwogVi4DQB5ZNHy2b09++kd2m449/RwGPyLcKpuoTzU4s/usYOeY4TxKIAd8cKedQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.35", "", { "os": "win32", "cpu": "x64" }, "sha512-m/l9NkzT/asdpPPN0525z5JcYiHc++feIisqmHS1+IfqirTDuReyKk7HxU3MydxrFcsp1LBMCxFipWiYV2V9hw=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "x64" }, "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg=="],
"@opentui/solid": ["@opentui/solid@0.1.35", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.35", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-VIhPmnZstthNpDWZzouOE6YzEnLHI3HbuT9wL6wf0K7Fp0kJEGUA7dlYscSpX9ZBY15B7COnIkSagC/C7hUQkw=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251108-0c7899b1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251108-0c7899b1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-tcsYnFGH/KBlQNG0IyZE2bisnm5NwN/w7theuWga3L1zoXqZqA5dQHutAVg4zkq5l/YKULeDI4jBlvz0lzH88A=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1034,7 +1037,7 @@
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.4.2", "", { "dependencies": { "@shikijs/core": "3.14.0", "@shikijs/transformers": "3.14.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.14.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-C6LbruH24BCp4awI47D5iMtVaZZD6GkzqBoDw+Sfu7DB3hC9y/rZr1C2BD7AUzAKwByTfFnh16Zp11ipfPqLKw=="],
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.4.4", "", { "dependencies": { "@shikijs/core": "3.14.0", "@shikijs/transformers": "3.14.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "shiki": "3.14.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-9bhWs+hsz1i0/SMIrzce+fFrSec8aLIFrJYTGHATlynmQovngIWz1Gc+XwGigvY4+zSMksrGPzO5HiaNlvRqtQ=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -1432,6 +1435,8 @@
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"@types/promise.allsettled": ["@types/promise.allsettled@1.0.6", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
@@ -1518,6 +1523,8 @@
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
@@ -1742,6 +1749,8 @@
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
"cloudflare": ["cloudflare@5.2.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-dVzqDpPFYR9ApEC9e+JJshFJZXcw4HzM8W+3DHzO5oy9+8rLC53G7x6fEf9A7/gSuSCxuvndzui5qJKftfIM9A=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
@@ -2068,7 +2077,11 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
@@ -2258,6 +2271,8 @@
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
@@ -2716,6 +2731,8 @@
"node-addon-api": ["node-addon-api@6.1.0", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -3178,23 +3195,23 @@
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"sst": ["sst@3.17.19", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.19", "sst-darwin-x64": "3.17.19", "sst-linux-arm64": "3.17.19", "sst-linux-x64": "3.17.19", "sst-linux-x86": "3.17.19", "sst-win32-arm64": "3.17.19", "sst-win32-x64": "3.17.19", "sst-win32-x86": "3.17.19" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-j0FlQhFZW+QWCczzqfPr6fZAF0Um7lP1tbGdd7zkbjFlxdk9BUBI4CYXUnopC6KaTMtjvpfg3XRF7v0bDc9g+A=="],
"sst": ["sst@3.17.23", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.23", "sst-darwin-x64": "3.17.23", "sst-linux-arm64": "3.17.23", "sst-linux-x64": "3.17.23", "sst-linux-x86": "3.17.23", "sst-win32-arm64": "3.17.23", "sst-win32-x64": "3.17.23", "sst-win32-x86": "3.17.23" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-TwKgUgDnZdc1Swe+bvCNeyO4dQnYz5cTodMpYj3jlXZdK9/KNz0PVxT1f0u5E76i1pmilXrUBL/f7iiMPw4RDg=="],
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6FeEgPqXkRT3o5qV0xktJ1eUiscJiPLBcGaxOxIEClpkVggZM83hO7Nizx/cAaAMhr1XQhbOZcKYueDHPdUY+Q=="],
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="],
"sst-darwin-x64": ["sst-darwin-x64@3.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-/z78dxfLHG8FtOhpjMnYSpKSdQjfdyKbq+cL3eud2+g2BQr7IyQ8BWNGimk2oadh38V3r6dO1/5aVJh3x3l1rg=="],
"sst-darwin-x64": ["sst-darwin-x64@3.17.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="],
"sst-linux-arm64": ["sst-linux-arm64@3.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-vbcMjiuLVxZ7352ajGlMqsS4J5AkAYvjLmsEALySUBVQhJUO9U7pk2P+Orfn702ZcO+6+NkGG9AL/g3K9EM1Tg=="],
"sst-linux-arm64": ["sst-linux-arm64@3.17.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="],
"sst-linux-x64": ["sst-linux-x64@3.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-gkNNmuHyvKjcb7RwMyoUH4wtgd7/bH7vUlMbcVsDzwt38y7+iTxyPMbcihucw42wDQRaDJtkDneSqj08U+MTFQ=="],
"sst-linux-x64": ["sst-linux-x64@3.17.23", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="],
"sst-linux-x86": ["sst-linux-x86@3.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-Bsvunkh4onZRVv4Rxq7bT/63qQOg2KJoQKhAQtFkJdbri/cOA2QWkzqH8+pC5Sv9rSvbcIJAEIhMXILC0pqCJw=="],
"sst-linux-x86": ["sst-linux-x86@3.17.23", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="],
"sst-win32-arm64": ["sst-win32-arm64@3.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-dKxR4v24AODJLHiT9yNena0JUgyz3cHyCi6HZyxyG3dXyWncMe1ZXMXIgs1ZEUcU4XeYM2HVy+Nnz4KB1US1Kg=="],
"sst-win32-arm64": ["sst-win32-arm64@3.17.23", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="],
"sst-win32-x64": ["sst-win32-x64@3.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-zgxSkGWZ1dewAr4R3slN/d3X9yumQDvAUOlJiX/6QE9Z67t/XNlow4+5i3L2oz4WHAFi59Un12YxbfM+RsBDmA=="],
"sst-win32-x64": ["sst-win32-x64@3.17.23", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="],
"sst-win32-x86": ["sst-win32-x86@3.17.19", "", { "os": "win32", "cpu": "none" }, "sha512-z8S0kyb0ibz9Q3cNYDpcKYX47jys7j/mdebC8HUhtED1qKEAfqQ1vsR+zvWyN64Z9Ijj7aPi1KwNV6Et3d7F8g=="],
"sst-win32-x86": ["sst-win32-x86@3.17.23", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
@@ -3480,6 +3497,8 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -3754,6 +3773,8 @@
"@slack/web-api/eventemitter3": ["eventemitter3@3.1.2", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
"@slack/web-api/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
"@slack/web-api/p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"@solidjs/start/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
@@ -3814,8 +3835,6 @@
"astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"axios/form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
@@ -4350,6 +4369,8 @@
"@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.14.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ=="],
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"@slack/web-api/p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"@slack/web-api/p-queue/p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
@@ -4402,8 +4423,6 @@
"astro/shiki/@shikijs/types": ["@shikijs/types@3.14.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ=="],
"axios/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
@@ -4700,6 +4719,8 @@
"@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"@vercel/nft/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -4708,8 +4729,6 @@
"archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"axios/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
+14 -43
View File
@@ -171,9 +171,7 @@ try {
const summary = await summarize(response)
await pushToLocalBranch(summary)
}
const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
await updateComment(`${response}${footer({ image: !hasShared })}`)
}
// Fork PR
@@ -185,9 +183,7 @@ try {
const summary = await summarize(response)
await pushToForkBranch(summary, prData)
}
const hasShared = prData.comments.nodes.some((c) =>
c.body.includes(`${useShareUrl()}/s/${shareId}`),
)
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${useShareUrl()}/s/${shareId}`))
await updateComment(`${response}${footer({ image: !hasShared })}`)
}
}
@@ -368,9 +364,7 @@ async function getAccessToken() {
if (!response.ok) {
const responseJson = (await response.json()) as { error?: string }
throw new Error(
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`,
)
throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
}
const responseJson = (await response.json()) as { token: string }
@@ -411,12 +405,8 @@ async function getUserPrompt() {
// ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
// ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
// ie. ![Image](https://github.com/user-attachments/assets/xxxx)
const mdMatches = prompt.matchAll(
/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi,
)
const tagMatches = prompt.matchAll(
/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi,
)
const mdMatches = prompt.matchAll(/!?\[.*?\]\((https:\/\/github\.com\/user-attachments\/[^)]+)\)/gi)
const tagMatches = prompt.matchAll(/<img .*?src="(https:\/\/github\.com\/user-attachments\/[^"]+)" \/>/gi)
const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
console.log("Images", JSON.stringify(matches, null, 2))
@@ -443,8 +433,7 @@ async function getUserPrompt() {
// Replace img tag with file path, ie. @image.png
const replacement = `@${filename}`
prompt =
prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
prompt = prompt.slice(0, start + offset) + replacement + prompt.slice(start + offset + tag.length)
offset += replacement.length - tag.length
const contentType = res.headers.get("content-type")
@@ -512,12 +501,7 @@ async function subscribeSessionEvents() {
? JSON.stringify(part.state.input)
: "Unknown"
console.log()
console.log(
color + `|`,
"\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`,
"",
"\x1b[0m" + title,
)
console.log(color + `|`, "\x1b[0m\x1b[2m" + ` ${tool.padEnd(7, " ")}`, "", "\x1b[0m" + title)
}
if (part.type === "text") {
@@ -729,8 +713,7 @@ async function assertPermissions() {
throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
}
if (!["admin", "write"].includes(permission))
throw new Error(`User ${actor} does not have write permissions`)
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
}
async function updateComment(body: string) {
@@ -774,9 +757,7 @@ function footer(opts?: { image?: boolean }) {
return `<a href="${useShareUrl()}/s/${shareId}"><img width="200" alt="${titleAlt}" src="https://social-cards.sst.dev/opencode-share/${title64}.png?model=${providerID}/${modelID}&version=${session.version}&id=${shareId}" /></a>\n`
})()
const shareUrl = shareId
? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
: ""
const shareUrl = shareId ? `[opencode session](${useShareUrl()}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
return `\n\n${image}${shareUrl}[github run](${useEnvRunUrl()})`
}
@@ -959,13 +940,9 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
})
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
const files = (pr.files.nodes || []).map(
(f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`,
)
const files = (pr.files.nodes || []).map((f) => `- ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
const reviewData = (pr.reviews.nodes || []).map((r) => {
const comments = (r.comments.nodes || []).map(
(c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`,
)
const comments = (r.comments.nodes || []).map((c) => ` - ${c.path}:${c.line ?? "?"}: ${c.body}`)
return [
`- ${r.author.login} at ${r.submittedAt}:`,
` - Review body: ${r.body}`,
@@ -987,15 +964,9 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
`Deletions: ${pr.deletions}`,
`Total Commits: ${pr.commits.totalCount}`,
`Changed Files: ${pr.files.nodes.length} files`,
...(comments.length > 0
? ["<pull_request_comments>", ...comments, "</pull_request_comments>"]
: []),
...(files.length > 0
? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"]
: []),
...(reviewData.length > 0
? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"]
: []),
...(comments.length > 0 ? ["<pull_request_comments>", ...comments, "</pull_request_comments>"] : []),
...(files.length > 0 ? ["<pull_request_changed_files>", ...files, "</pull_request_changed_files>"] : []),
...(reviewData.length > 0 ? ["<pull_request_reviews>", ...reviewData, "</pull_request_reviews>"] : []),
"</pull_request>",
].join("\n")
}
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../sst-env.d.ts" />
import "sst"
export {}
export {}
+9 -7
View File
@@ -61,13 +61,7 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
domain: `auth.${domain}`,
handler: "packages/console/function/src/auth.ts",
url: true,
link: [
database,
authStorage,
GITHUB_CLIENT_ID_CONSOLE,
GITHUB_CLIENT_SECRET_CONSOLE,
GOOGLE_CLIENT_ID,
],
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
})
////////////////
@@ -112,6 +106,7 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
properties: { value: stripeWebhook.secret },
})
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
////////////////
// CONSOLE
@@ -142,6 +137,13 @@ new sst.cloudflare.x.SolidStart("Console", {
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
...($dev
? [
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
new sst.Secret("CLOUDFLARE_API_TOKEN", process.env.CLOUDFLARE_API_TOKEN!),
]
: []),
gatewayKv,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),
@@ -1,15 +1,15 @@
{
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}
"keep": {
"days": true,
"amount": 14
},
"auditLog": "/home/thdxr/dev/projects/sst/opencode/logs/.2c5480b3b2480f80fa29b850af461dce619c0b2f-audit.json",
"files": [
{
"date": 1759827172859,
"name": "/home/thdxr/dev/projects/sst/opencode/logs/mcp-puppeteer-2025-10-07.log",
"hash": "a3d98b26edd793411b968a0d24cfeee8332138e282023c3b83ec169d55c67f16"
}
],
"hashType": "sha256"
}
+3 -3
View File
@@ -28,7 +28,7 @@
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.2",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
@@ -52,7 +52,7 @@
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.19",
"sst": "3.17.23",
"turbo": "2.5.6"
},
"dependencies": {
@@ -66,7 +66,7 @@
"license": "MIT",
"prettier": {
"semi": false,
"printWidth": 100
"printWidth": 120
},
"trustedDependencies": [
"esbuild",
+3
View File
@@ -23,6 +23,9 @@ app.config.timestamp_*.js
# Temp
gitignore
# Generated files
public/sitemap.xml
# System Files
.DS_Store
Thumbs.db
+2 -2
View File
@@ -5,9 +5,9 @@
"typecheck": "tsgo --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.32"
"version": "1.0.48"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bun
import { readdir, writeFile } from "fs/promises"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
import { config } from "../src/config.js"
const __dirname = dirname(fileURLToPath(import.meta.url))
const BASE_URL = config.baseUrl
const PUBLIC_DIR = join(__dirname, "../public")
const ROUTES_DIR = join(__dirname, "../src/routes")
const DOCS_DIR = join(__dirname, "../../../web/src/content/docs")
interface SitemapEntry {
url: string
priority: number
changefreq: string
}
async function getMainRoutes(): Promise<SitemapEntry[]> {
const routes: SitemapEntry[] = []
// Add main static routes
const staticRoutes = [
{ path: "/", priority: 1.0, changefreq: "daily" },
{ path: "/enterprise", priority: 0.8, changefreq: "weekly" },
{ path: "/brand", priority: 0.6, changefreq: "monthly" },
{ path: "/zen", priority: 0.8, changefreq: "weekly" },
]
for (const route of staticRoutes) {
routes.push({
url: `${BASE_URL}${route.path}`,
priority: route.priority,
changefreq: route.changefreq,
})
}
return routes
}
async function getDocsRoutes(): Promise<SitemapEntry[]> {
const routes: SitemapEntry[] = []
try {
const files = await readdir(DOCS_DIR)
for (const file of files) {
if (!file.endsWith(".mdx")) continue
const slug = file.replace(".mdx", "")
const path = slug === "index" ? "/docs/" : `/docs/${slug}`
routes.push({
url: `${BASE_URL}${path}`,
priority: slug === "index" ? 0.9 : 0.7,
changefreq: "weekly",
})
}
} catch (error) {
console.error("Error reading docs directory:", error)
}
return routes
}
function generateSitemapXML(entries: SitemapEntry[]): string {
const urls = entries
.map(
(entry) => ` <url>
<loc>${entry.url}</loc>
<changefreq>${entry.changefreq}</changefreq>
<priority>${entry.priority}</priority>
</url>`,
)
.join("\n")
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
}
async function main() {
console.log("Generating sitemap...")
const mainRoutes = await getMainRoutes()
const docsRoutes = await getDocsRoutes()
const allRoutes = [...mainRoutes, ...docsRoutes]
console.log(`Found ${mainRoutes.length} main routes`)
console.log(`Found ${docsRoutes.length} docs routes`)
console.log(`Total: ${allRoutes.length} routes`)
const xml = generateSitemapXML(allRoutes)
const outputPath = join(PUBLIC_DIR, "sitemap.xml")
await writeFile(outputPath, xml, "utf-8")
console.log(`✓ Sitemap generated at ${outputPath}`)
}
main()
+1 -4
View File
@@ -12,10 +12,7 @@ export default function App() {
root={(props) => (
<MetaProvider>
<Title>opencode</Title>
<Meta
name="description"
content="OpenCode - The AI coding agent built for the terminal."
/>
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
@@ -77,4 +77,4 @@
background-color: var(--color-accent-alpha);
}
}
}
}
+1 -1
View File
@@ -63,4 +63,4 @@
font-weight: 600;
color: var(--color-text);
}
}
}
+3
View File
@@ -2,6 +2,9 @@
* Application-wide constants and configuration
*/
export const config = {
// Base URL
baseUrl: "https://opencode.ai",
// GitHub
github: {
repoUrl: "https://github.com/sst/opencode",
+1 -4
View File
@@ -7,10 +7,7 @@ export const github = query(async () => {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
}
const apiBaseUrl = config.github.repoUrl.replace(
"https://github.com/",
"https://api.github.com/repos/",
)
const apiBaseUrl = config.github.repoUrl.replace("https://github.com/", "https://api.github.com/repos/")
try {
const [meta, releases, contributors] = await Promise.all([
fetch(apiBaseUrl, { headers }).then((res) => res.json()),
@@ -264,7 +264,7 @@
[data-component="brand-content"] {
padding: 4rem 5rem;
h2 {
h1 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
@@ -299,7 +299,6 @@
transition: all 0.2s ease;
text-decoration: none;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
@@ -385,23 +384,21 @@
0 1px 2px -1px rgba(19, 16, 16, 0.12);
@media (max-width: 40rem) {
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.16)
box-shadow: 0 0 0 1px rgba(19, 16, 16, 0.16);
}
&:hover {
background: var(--color-background);
}
&:active {
transform: scale(0.98);
box-shadow: 0 0 0 1px rgba(19, 16, 16, 0.08), 0 6px 8px -8px rgba(19, 16, 16, 0.50);
box-shadow:
0 0 0 1px rgba(19, 16, 16, 0.08),
0 6px 8px -8px rgba(19, 16, 16, 0.5);
}
}
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
+25 -125
View File
@@ -1,6 +1,7 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { Title, Meta, Link } from "@solidjs/meta"
import { Header } from "~/component/header"
import { config } from "~/config"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
@@ -53,26 +54,21 @@ export default function Brand() {
return (
<main data-page="enterprise">
<Title>OpenCode | Brand</Title>
<Link rel="canonical" href={`${config.baseUrl}/brand`} />
<Meta name="description" content="OpenCode brand guidelines" />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="brand-content">
<h2>Brand guidelines</h2>
<h1>Brand guidelines</h1>
<p>Resources and assets to help you work with the OpenCode brand.</p>
<button
data-component="download-button"
onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")}
>
Download all assets
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -88,13 +84,7 @@ export default function Brand() {
<div data-component="actions">
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -105,13 +95,7 @@ export default function Brand() {
</button>
<button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -127,13 +111,7 @@ export default function Brand() {
<div data-component="actions">
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -144,13 +122,7 @@ export default function Brand() {
</button>
<button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -164,17 +136,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}
>
<button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -183,17 +147,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}
>
<button onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -207,17 +163,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}
>
<button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -226,17 +174,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}
>
<button onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -250,19 +190,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")
}
>
<button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -271,19 +201,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")
}
>
<button onClick={() => downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -297,19 +217,9 @@ export default function Brand() {
<div>
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
<div data-component="actions">
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")
}
>
<button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
PNG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -318,19 +228,9 @@ export default function Brand() {
/>
</svg>
</button>
<button
onClick={() =>
downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")
}
>
<button onClick={() => downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")}>
SVG
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
@@ -0,0 +1,5 @@
import { redirect } from "@solidjs/router"
export async function GET() {
return redirect("https://discord.gg/h5TNnkFVNy")
}
@@ -287,7 +287,7 @@
}
[data-component="enterprise-column-1"] {
h2 {
h1 {
font-size: 1.5rem;
font-weight: 500;
color: var(--color-text-strong);
@@ -1,6 +1,7 @@
import "./index.css"
import { Title, Meta } from "@solidjs/meta"
import { Title, Meta, Link } from "@solidjs/meta"
import { createSignal, Show } from "solid-js"
import { config } from "~/config"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
@@ -54,6 +55,7 @@ export default function Enterprise() {
return (
<main data-page="enterprise">
<Title>OpenCode | Enterprise solutions for your organisation</Title>
<Link rel="canonical" href={`${config.baseUrl}/enterprise`} />
<Meta name="description" content="Contact OpenCode for enterprise solutions" />
<div data-component="container">
<Header />
@@ -62,41 +64,28 @@ export default function Enterprise() {
<section data-component="enterprise-content">
<div data-component="enterprise-columns">
<div data-component="enterprise-column-1">
<h2>Your code is yours</h2>
<h1>Your code is yours</h1>
<p>
OpenCode operates securely inside your organization with no data or context stored
and no licensing restrictions or ownership claims. Start a trial with your team,
then deploy it across your organization by integrating it with your SSO and
internal AI gateway.
OpenCode operates securely inside your organization with no data or context stored and no licensing
restrictions or ownership claims. Start a trial with your team, then deploy it across your
organization by integrating it with your SSO and internal AI gateway.
</p>
<p>Let us know and how we can help.</p>
<Show when={false}>
<div data-component="testimonial">
<div data-component="quotation">
<svg
width="20"
height="17"
viewBox="0 0 20 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
fill="currentColor"
/>
</svg>
</div>
Thanks to OpenCode, we found a way to create software to track all our assets
even the imaginary ones.
Thanks to OpenCode, we found a way to create software to track all our assets even the imaginary
ones.
<div data-component="testimonial-logo">
<svg
width="80"
height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="80" height="79" viewBox="0 0 80 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -213,11 +202,7 @@ export default function Enterprise() {
</button>
</form>
{showSuccess() && (
<div data-component="success-message">
Message sent, we'll be in touch soon.
</div>
)}
{showSuccess() && <div data-component="success-message">Message sent, we'll be in touch soon.</div>}
</div>
</div>
</div>
@@ -230,31 +215,29 @@ export default function Enterprise() {
<ul>
<li>
<Faq question="What is OpenCode Enterprise?">
OpenCode Enterprise is for organizations that want to ensure that their code and
data never leaves their infrastructure. It can do this by using a centralized
config that integrates with your SSO and internal AI gateway.
OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves
their infrastructure. It can do this by using a centralized config that integrates with your SSO and
internal AI gateway.
</Faq>
</li>
<li>
<Faq question="How do I get started with OpenCode Enterprise?">
Simply start with an internal trial with your team. OpenCode by default does not
store your code or context data, making it easy to get started. Then contact us to
discuss pricing and implementation options.
Simply start with an internal trial with your team. OpenCode by default does not store your code or
context data, making it easy to get started. Then contact us to discuss pricing and implementation
options.
</Faq>
</li>
<li>
<Faq question="How does enterprise pricing work?">
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not
charge for tokens used. For further details, contact us for a custom quote based
on your organization's needs.
We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens
used. For further details, contact us for a custom quote based on your organization's needs.
</Faq>
</li>
<li>
<Faq question="Is my data secure with OpenCode Enterprise?">
Yes. OpenCode does not store your code or context data. All processing happens
locally or through direct API calls to your AI provider. With central config and
SSO integration, your data remains secure within your organization's
infrastructure.
Yes. OpenCode does not store your code or context data. All processing happens locally or through
direct API calls to your AI provider. With central config and SSO integration, your data remains
secure within your organization's infrastructure.
</Faq>
</li>
</ul>
+1 -1
View File
@@ -479,7 +479,7 @@ body {
border-bottom: 1px solid var(--color-border-weak);
}
strong {
h1 {
font-size: 28px;
color: var(--color-text-strong);
font-weight: 500;
File diff suppressed because it is too large Load Diff
@@ -41,8 +41,7 @@ export async function POST(input: APIEvent) {
}
if (body.type === "checkout.session.completed") {
const workspaceID = body.data.object.metadata?.workspaceID
const amountInCents =
body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
const customerID = body.data.object.customer as string
const paymentID = body.data.object.payment_intent as string
const invoiceID = body.data.object.invoice as string
@@ -55,8 +54,7 @@ export async function POST(input: APIEvent) {
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID)
throw new Error("Customer ID mismatch")
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
// set customer metadata
if (!customer?.customerID) {
@@ -72,8 +70,7 @@ export async function POST(input: APIEvent) {
expand: ["payment_method"],
})
const paymentMethod = paymentIntent.payment_method
if (!paymentMethod || typeof paymentMethod === "string")
throw new Error("Payment method not expanded")
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
await Database.transaction(async (tx) => {
await tx
@@ -128,12 +125,7 @@ export async function POST(input: APIEvent) {
amount: PaymentTable.amount,
})
.from(PaymentTable)
.where(
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
.then((rows) => rows[0]?.amount),
)
if (!amount) throw new Error("Payment not found")
@@ -144,12 +136,7 @@ export async function POST(input: APIEvent) {
.set({
timeRefunded: new Date(body.created * 1000),
})
.where(
and(
eq(PaymentTable.paymentID, paymentIntentID),
eq(PaymentTable.workspaceID, workspaceID),
),
)
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
await tx
.update(BillingTable)
+4 -6
View File
@@ -79,19 +79,17 @@ export default function Home() {
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
</li>
<li>
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a>{" "}
provided by opencode <label>New</label>
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
<label>New</label>
</li>
<li>
<strong>Multi-session</strong> Start multiple agents in parallel on the same project
</li>
<li>
<strong>Shareable links</strong> Share a link to any sessions for reference or to
debug
<strong>Shareable links</strong> Share a link to any sessions for reference or to debug
</li>
<li>
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max
account
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
</li>
<li>
<strong>Use any model</strong> Supports 75+ LLM providers through{" "}
@@ -14,4 +14,4 @@
color: var(--color-danger);
}
}
}
}
@@ -71,4 +71,4 @@
color: var(--color-text-muted);
}
}
}
}
@@ -104,4 +104,4 @@
}
}
}
}
}
@@ -182,4 +182,4 @@
padding: var(--space-4);
min-width: 150px;
}
}
}
@@ -34,10 +34,17 @@ export function BillingSection() {
const sessionSubmission = useSubmission(createSessionUrl)
const [store, setStore] = createStore({
showAddBalanceForm: false,
addBalanceAmount: "",
addBalanceAmount: billingInfo()?.reloadAmount.toString() ?? "",
checkoutRedirecting: false,
sessionRedirecting: false,
})
createEffect(() => {
const info = billingInfo()
if (info) {
setStore("addBalanceAmount", info.reloadAmount.toString())
}
})
const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
async function onClickCheckout() {
@@ -67,7 +74,6 @@ export function BillingSection() {
}
setStore({
showAddBalanceForm: true,
addBalanceAmount: billingInfo()!.reloadAmount.toString(),
})
}
@@ -133,8 +139,7 @@ export function BillingSection() {
<div data-slot="section-title">
<h2>Billing</h2>
<p>
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any
questions.
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any questions.
</p>
</div>
<div data-slot="section-content">
@@ -164,32 +169,20 @@ export function BillingSection() {
placeholder="Enter amount"
/>
<div data-slot="form-actions">
<button
data-color="ghost"
type="button"
onClick={() => hideAddBalanceForm()}
>
<button data-color="ghost" type="button" onClick={() => hideAddBalanceForm()}>
Cancel
</button>
<button
data-color="primary"
type="button"
disabled={
!store.addBalanceAmount ||
checkoutSubmission.pending ||
store.checkoutRedirecting
}
disabled={!store.addBalanceAmount || checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Add"}
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Add"}
</button>
</div>
</div>
<Show
when={checkoutSubmission.result && (checkoutSubmission.result as any).error}
>
<Show when={checkoutSubmission.result && (checkoutSubmission.result as any).error}>
{(err: any) => <div data-slot="form-error">{err()}</div>}
</Show>
</div>
@@ -210,10 +203,7 @@ export function BillingSection() {
<div data-slot="card-details">
<Switch>
<Match when={billingInfo()?.paymentMethodType === "card"}>
<Show
when={billingInfo()?.paymentMethodLast4}
fallback={<span data-slot="number">----</span>}
>
<Show when={billingInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
<span data-slot="secret"></span>
<span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
</Show>
@@ -241,9 +231,7 @@ export function BillingSection() {
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Enable Billing"}
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable Billing"}
</button>
</Show>
</div>
@@ -93,4 +93,4 @@
margin: 0;
line-height: 1.4;
}
}
}
@@ -104,13 +104,9 @@ export function MonthlyLimitSection() {
</button>
</Show>
</div>
<Show
when={billingInfo()?.monthlyLimit}
fallback={<p data-slot="usage-status">No usage limit set.</p>}
>
<Show when={billingInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No usage limit set.</p>}>
<p data-slot="usage-status">
Current usage for{" "}
{new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
{(() => {
const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
if (!dateLastUsed) return "0"
@@ -69,11 +69,7 @@ export function ReloadSection() {
})
createEffect(() => {
if (
!setReloadSubmission.pending &&
setReloadSubmission.result &&
!(setReloadSubmission.result as any).error
) {
if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
setStore("show", false)
}
})
@@ -108,8 +104,8 @@ export function ReloadSection() {
}
>
<p>
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b>{" "}
(+$1.23 processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+$1.23 processing fee)
when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
</p>
</Show>
<button data-color="primary" type="button" onClick={() => show()}>
@@ -194,8 +190,8 @@ export function ReloadSection() {
minute: "2-digit",
second: "2-digit",
})}
. Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment
method and try again.
. Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
again.
</p>
<form action={reload} method="post" data-slot="create-form">
<input type="hidden" name="workspaceID" value={params.id} />
@@ -51,9 +51,7 @@ export default function () {
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
onClick={onClickCheckout}
>
{checkoutSubmission.pending || store.checkoutRedirecting
? "Loading..."
: "Enable billing"}
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable billing"}
</button>
}
>
@@ -171,7 +171,6 @@
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -181,8 +180,7 @@
th {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
@@ -190,11 +188,10 @@
td {
&:nth-child(3)
/* Date */
{
/* Date */ {
display: none;
}
}
}
}
}
}
@@ -69,4 +69,4 @@
}
}
}
}
}
@@ -5,15 +5,7 @@ import { withActor } from "~/context/auth.withActor"
import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import {
IconAlibaba,
IconAnthropic,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
@@ -76,8 +68,7 @@ export function ModelSection() {
<div data-slot="section-title">
<h2>Models</h2>
<p>
Manage which models workspace members can access.{" "}
<a href="/docs/zen#pricing ">Learn more</a>.
Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
</p>
</div>
<div data-slot="models-list">
@@ -140,4 +140,4 @@
}
}
}
}
}
@@ -128,7 +128,6 @@
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -136,4 +135,4 @@
}
}
}
}
}
@@ -22,7 +22,9 @@ const removeProvider = action(async (form: FormData) => {
if (!provider) return { error: "Provider is required" }
const workspaceID = form.get("workspaceID")?.toString()
if (!workspaceID) return { error: "Workspace ID is required" }
return json(await withActor(() => Provider.remove({ provider }), workspaceID), { revalidate: listProviders.key })
return json(await withActor(() => Provider.remove({ provider }), workspaceID), {
revalidate: listProviders.key,
})
}, "provider.remove")
const saveProvider = action(async (form: FormData) => {
@@ -31,7 +31,7 @@
margin: 0;
}
>button {
> button {
align-self: flex-start;
}
}
@@ -80,7 +80,7 @@
}
}
>button[type="reset"] {
> button[type="reset"] {
align-self: flex-start;
}
@@ -91,4 +91,4 @@
margin-top: calc(var(--space-1) * -1);
}
}
}
}
@@ -67,10 +67,7 @@ export const querySessionInfo = query(async (workspaceID: string) => {
return withActor(() => {
return {
isAdmin: Actor.userRole() === "admin",
isBeta:
Resource.App.stage === "production"
? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y"
: true,
isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
}
}, workspaceID)
}, "session.get")
@@ -277,7 +277,7 @@ body {
margin-bottom: 24px;
}
strong {
h1 {
font-size: 28px;
color: var(--color-text-strong);
font-weight: 500;
@@ -3,6 +3,7 @@ import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta, Link } from "@solidjs/meta"
import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg"
import { config } from "~/config"
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
import compareVideo from "../../asset/lander/opencode-comparison-min.mp4"
import compareVideoPoster from "../../asset/lander/opencode-comparison-poster.png"
@@ -30,6 +31,7 @@ export default function Home() {
<main data-page="zen">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<Meta property="og:image" content="/social-share-zen.png" />
<Meta name="twitter:image" content="/social-share-zen.png" />
@@ -42,7 +44,7 @@ export default function Home() {
<div data-slot="hero-copy">
<img data-slot="zen logo light" src={zenLogoLight} alt="zen logo light" />
<img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" />
<strong>Reliable optimized models for coding agents</strong>
<h1>Reliable optimized models for coding agents</h1>
<p>
Zen gives you access to a curated set of AI models that OpenCode has tested and benchmarked specifically
for coding agents. No need to worry about inconsistent performance and quality, use validated models
@@ -3,3 +3,4 @@ export class CreditsError extends Error {}
export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
export class RateLimitError extends Error {}
@@ -12,18 +12,14 @@ import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError } from "./error"
import {
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
} from "./provider/provider"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type Model = ZenData["models"][string]
export async function handler(
input: APIEvent,
@@ -32,6 +28,10 @@ export async function handler(
parseApiKey: (headers: Headers) => string | undefined
},
) {
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
type ModelInfo = Awaited<ReturnType<typeof validateModel>>
type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
@@ -39,6 +39,7 @@ export async function handler(
try {
const body = await input.request.json()
const ip = input.request.headers.get("x-real-ip") ?? ""
logger.metric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
@@ -46,13 +47,11 @@ export async function handler(
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model)
const providerInfo = selectProvider(
zenData,
modelInfo,
input.request.headers.get("x-real-ip") ?? "",
)
const providerInfo = selectProvider(zenData, modelInfo, ip)
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(modelInfo, authInfo)
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
updateProviderKey(authInfo, providerInfo)
logger.metric({ provider: providerInfo.id })
@@ -67,7 +66,7 @@ export async function handler(
}),
)
logger.debug("REQUEST URL: " + reqUrl)
logger.debug("REQUEST: " + reqBody)
logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
const res = await fetch(reqUrl, {
method: "POST",
headers: (() => {
@@ -92,9 +91,6 @@ export async function handler(
}
}
logger.debug("STATUS: " + res.status + " " + res.statusText)
if (res.status === 400 || res.status === 503) {
logger.debug("RESPONSE: " + (await res.text()))
}
// Handle non-streaming response
if (!body.stream) {
@@ -103,6 +99,7 @@ export async function handler(
const body = JSON.stringify(responseConverter(json))
logger.metric({ response_length: body.length })
logger.debug("RESPONSE: " + body)
await rateLimiter?.track()
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
await reload(authInfo)
return new Response(body, {
@@ -131,6 +128,7 @@ export async function handler(
response_length: responseLength,
"timestamp.last_byte": Date.now(),
})
await rateLimiter?.track()
const usage = usageParser.retrieve()
if (usage) {
await trackUsage(authInfo, modelInfo, providerInfo, usage)
@@ -205,6 +203,15 @@ export async function handler(
{ status: 401 },
)
if (error instanceof RateLimitError)
return new Response(
JSON.stringify({
type: "error",
error: { type: error.constructor.name, message: error.message },
}),
{ status: 429 },
)
return new Response(
JSON.stringify({
type: "error",
@@ -229,12 +236,8 @@ export async function handler(
return { id: modelId, ...modelData }
}
function selectProvider(
zenData: ZenData,
model: Awaited<ReturnType<typeof validateModel>>,
ip: string,
) {
const providers = model.providers
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string) {
const providers = modelInfo.providers
.filter((provider) => !provider.disabled)
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
@@ -247,26 +250,22 @@ export async function handler(
throw new ModelError(`Provider ${provider.id} not supported`)
}
const format = zenData.providers[provider.id].format
return {
...provider,
...zenData.providers[provider.id],
...(format === "anthropic"
? anthropicHelper
: format === "openai"
? openaiHelper
: oaCompatHelper),
...(() => {
const format = zenData.providers[provider.id].format
if (format === "anthropic") return anthropicHelper
if (format === "openai") return openaiHelper
return oaCompatHelper
})(),
}
}
async function authenticate(
model: Awaited<ReturnType<typeof validateModel>>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
) {
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey) {
if (model.allowAnonymous) return
if (modelInfo.allowAnonymous) return
throw new AuthError("Missing API key.")
}
@@ -297,20 +296,11 @@ export async function handler(
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
.innerJoin(
UserTable,
and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)),
)
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, model.id)),
)
.innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
.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),
),
and(eq(ProviderTable.workspaceID, KeyTable.workspaceID), eq(ProviderTable.provider, providerInfo.id)),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
@@ -333,11 +323,11 @@ export async function handler(
}
}
function validateBilling(model: Model, authInfo: Awaited<ReturnType<typeof authenticate>>) {
function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) {
if (!authInfo) return
if (authInfo.provider?.credentials) return
if (authInfo.isFree) return
if (model.allowAnonymous) return
if (modelInfo.allowAnonymous) return
const billing = authInfo.billing
if (!billing.paymentMethodID)
@@ -381,39 +371,24 @@ export async function handler(
}
}
function validateModelSettings(authInfo: Awaited<ReturnType<typeof authenticate>>) {
function validateModelSettings(authInfo: AuthInfo) {
if (!authInfo) return
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
}
function updateProviderKey(
authInfo: Awaited<ReturnType<typeof authenticate>>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
) {
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
if (!authInfo) return
if (!authInfo.provider?.credentials) return
providerInfo.apiKey = authInfo.provider.credentials
}
async function trackUsage(
authInfo: Awaited<ReturnType<typeof authenticate>>,
modelInfo: ReturnType<typeof validateModel>,
providerInfo: Awaited<ReturnType<typeof selectProvider>>,
usage: any,
) {
const {
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens,
cacheWrite1hTokens,
} = providerInfo.normalizeUsage(usage)
async function trackUsage(authInfo: AuthInfo, modelInfo: ModelInfo, providerInfo: ProviderInfo, usage: any) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
providerInfo.normalizeUsage(usage)
const modelCost =
modelInfo.cost200K &&
inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) >
200_000
inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
? modelInfo.cost200K
: modelInfo.cost
@@ -464,8 +439,7 @@ export async function handler(
if (!authInfo) return
const cost =
authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
const cost = authInfo.isFree || authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID: authInfo.workspaceID,
@@ -505,9 +479,7 @@ export async function handler(
`,
timeMonthlyUsageUpdated: sql`now()`,
})
.where(
and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)),
)
.where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id)))
})
await Database.use((tx) =>
@@ -518,7 +490,7 @@ export async function handler(
)
}
async function reload(authInfo: Awaited<ReturnType<typeof authenticate>>) {
async function reload(authInfo: AuthInfo) {
if (!authInfo) return
if (authInfo.isFree) return
if (authInfo.provider?.credentials) return
@@ -537,10 +509,7 @@ export async function handler(
BillingTable.balance,
centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100),
),
or(
isNull(BillingTable.timeReloadLockedTill),
lt(BillingTable.timeReloadLockedTill, sql`now()`),
),
or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
),
),
)
@@ -98,7 +98,10 @@ export function fromAnthropicRequest(body: any): CommonRequest {
typeof (src as any).media_type === "string" &&
typeof (src as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` } }
return {
type: "image_url",
image_url: { url: `data:${(src as any).media_type};base64,${(src as any).data}` },
}
return undefined
}
@@ -165,7 +168,11 @@ export function fromAnthropicRequest(body: any): CommonRequest {
.filter((t: any) => t && typeof t === "object" && "input_schema" in t)
.map((t: any) => ({
type: "function",
function: { name: (t as any).name, description: (t as any).description, parameters: (t as any).input_schema },
function: {
name: (t as any).name,
description: (t as any).description,
parameters: (t as any).input_schema,
},
}))
: undefined
@@ -452,7 +459,12 @@ export function toAnthropicResponse(resp: CommonResponse) {
} catch {
input = (tc as any).function.arguments
}
content.push({ type: "tool_use", id: (tc as any).id, name: (tc as any).function.name, input })
content.push({
type: "tool_use",
id: (tc as any).id,
name: (tc as any).function.name,
input,
})
}
}
}
@@ -511,13 +523,22 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
if (json.type === "content_block_start") {
const cb = json.content_block
if (cb?.type === "text") {
out.choices.push({ index: json.index ?? 0, delta: { role: "assistant", content: "" }, finish_reason: null })
out.choices.push({
index: json.index ?? 0,
delta: { role: "assistant", content: "" },
finish_reason: null,
})
} else if (cb?.type === "tool_use") {
out.choices.push({
index: json.index ?? 0,
delta: {
tool_calls: [
{ index: json.index ?? 0, id: cb.id, type: "function", function: { name: cb.name, arguments: "" } },
{
index: json.index ?? 0,
id: cb.id,
type: "function",
function: { name: cb.name, arguments: "" },
},
],
},
finish_reason: null,
@@ -532,7 +553,9 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
} else if (d?.type === "input_json_delta") {
out.choices.push({
index: json.index ?? 0,
delta: { tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }] },
delta: {
tool_calls: [{ index: json.index ?? 0, function: { arguments: d.partial_json } }],
},
finish_reason: null,
})
}
@@ -532,7 +532,9 @@ export function toOaCompatibleChunk(chunk: CommonChunk): string {
total_tokens: chunk.usage.total_tokens,
...(chunk.usage.prompt_tokens_details?.cached_tokens
? {
prompt_tokens_details: { cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens },
prompt_tokens_details: {
cached_tokens: chunk.usage.prompt_tokens_details.cached_tokens,
},
}
: {}),
}
@@ -77,7 +77,10 @@ export function fromOpenaiRequest(body: any): CommonRequest {
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "image_url", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return {
type: "image_url",
image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` },
}
return undefined
}
@@ -153,7 +156,11 @@ export function fromOpenaiRequest(body: any): CommonRequest {
}
if ((m as any).role === "tool") {
msgs.push({ role: "tool", tool_call_id: (m as any).tool_call_id, content: (m as any).content })
msgs.push({
role: "tool",
tool_call_id: (m as any).tool_call_id,
content: (m as any).content,
})
continue
}
}
@@ -210,7 +217,10 @@ export function toOpenaiRequest(body: CommonRequest) {
typeof (s as any).media_type === "string" &&
typeof (s as any).data === "string"
)
return { type: "input_image", image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` } }
return {
type: "input_image",
image_url: { url: `data:${(s as any).media_type};base64,${(s as any).data}` },
}
return undefined
}
@@ -498,7 +508,9 @@ export function fromOpenaiChunk(chunk: string): CommonChunk | string {
if (typeof name === "string" && name.length > 0) {
out.choices.push({
index: 0,
delta: { tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }] },
delta: {
tool_calls: [{ index: 0, id, type: "function", function: { name, arguments: "" } }],
},
finish_reason: null,
})
}
@@ -555,7 +567,12 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
const model = chunk.model
if (d.content) {
const data = { id, type: "response.output_text.delta", delta: d.content, response: { id, model } }
const data = {
id,
type: "response.output_text.delta",
delta: d.content,
response: { id, model },
}
return `event: response.output_text.delta\ndata: ${JSON.stringify(data)}`
}
@@ -565,7 +582,13 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
const data = {
type: "response.output_item.added",
output_index: 0,
item: { id: tc.id, type: "function_call", name: tc.function.name, call_id: tc.id, arguments: "" },
item: {
id: tc.id,
type: "function_call",
name: tc.function.name,
call_id: tc.id,
arguments: "",
},
}
return `event: response.output_item.added\ndata: ${JSON.stringify(data)}`
}
@@ -593,7 +616,11 @@ export function toOpenaiChunk(chunk: CommonChunk): string {
}
: undefined
const data: any = { id, type: "response.completed", response: { id, model, ...(usage ? { usage } : {}) } }
const data: any = {
id,
type: "response.completed",
response: { id, model, ...(usage ? { usage } : {}) },
}
return `event: response.completed\ndata: ${JSON.stringify(data)}`
}
@@ -0,0 +1,35 @@
import { Resource } from "@opencode-ai/console-resource"
import { RateLimitError } from "./error"
import { logger } from "./logger"
export function createRateLimiter(model: string, limit: number | undefined, ip: string) {
if (!limit) return
const now = Date.now()
const currKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now)}`
const prevKey = `usage:${ip}:${model}:${buildYYYYMMDDHH(now - 3_600_000)}`
let currRate: number
let prevRate: number
return {
track: async () => {
await Resource.GatewayKv.put(currKey, currRate + 1, { expirationTtl: 3600 })
},
check: async () => {
const values = await Resource.GatewayKv.get([currKey, prevKey])
const prevValue = values?.get(prevKey)
const currValue = values?.get(currKey)
prevRate = prevValue ? parseInt(prevValue) : 0
currRate = currValue ? parseInt(currValue) : 0
logger.debug(`rate limit ${model} prev/curr: ${prevRate}/${currRate}`)
if (prevRate + currRate >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
},
}
}
function buildYYYYMMDDHH(timestamp: number) {
return new Date(timestamp)
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 10)
}
@@ -50,10 +50,7 @@ export async function GET(input: APIEvent) {
})
.from(KeyTable)
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
.leftJoin(
ModelTable,
and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)),
)
.leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows.map((row) => row.model)),
)
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
export {}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -592,10 +571,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -603,10 +579,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -663,9 +636,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -673,9 +644,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -692,4 +661,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,10 +578,7 @@
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -610,10 +586,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -670,9 +643,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -680,9 +651,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -699,4 +668,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -613,18 +592,12 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
@@ -632,10 +605,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -692,9 +662,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -702,9 +670,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -721,4 +687,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -613,32 +592,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -646,10 +615,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -706,9 +672,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -716,9 +680,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -735,4 +697,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,17 +473,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -504,10 +486,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -599,32 +578,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -632,10 +601,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -692,9 +658,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -702,9 +666,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -721,4 +683,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -493,17 +480,12 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": [
"workspace_id",
"name"
],
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
@@ -511,10 +493,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -606,32 +585,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -639,10 +608,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -699,9 +665,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -709,9 +673,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -728,4 +690,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -493,9 +480,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -503,10 +488,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -598,32 +580,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -631,10 +603,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -691,9 +660,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -701,9 +668,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -720,4 +685,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -584,32 +566,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -617,10 +589,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -677,9 +646,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -687,9 +654,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -706,4 +671,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -584,32 +566,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -617,10 +589,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -677,9 +646,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -687,9 +654,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -706,4 +671,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -605,32 +587,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -638,10 +610,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -698,9 +667,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -708,9 +675,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -727,4 +692,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -549,10 +531,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -560,10 +539,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -676,32 +652,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -709,10 +675,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -769,9 +732,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -779,9 +740,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -798,4 +757,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -398,10 +388,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -479,9 +466,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -489,10 +474,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -549,10 +531,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -560,10 +539,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -627,10 +603,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -638,10 +611,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -754,32 +724,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -787,10 +747,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -847,9 +804,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -857,9 +812,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -876,4 +829,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -180,9 +178,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -190,10 +186,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -280,10 +273,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -405,10 +395,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -486,9 +473,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -496,10 +481,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -556,10 +538,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -567,10 +546,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -634,10 +610,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -645,10 +618,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -761,32 +731,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -794,10 +754,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -854,9 +811,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -864,9 +819,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -883,4 +836,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -187,9 +185,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -197,10 +193,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -287,10 +280,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -412,10 +402,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -493,9 +480,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -503,10 +488,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -563,10 +545,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -574,10 +553,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -641,10 +617,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -652,10 +625,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -768,32 +738,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -801,10 +761,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -861,9 +818,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -871,9 +826,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -890,4 +843,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -48,9 +48,7 @@
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": true
}
},
@@ -117,10 +115,7 @@
"indexes": {
"provider": {
"name": "provider",
"columns": [
"provider",
"subject"
],
"columns": ["provider", "subject"],
"isUnique": true
}
},
@@ -257,9 +252,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -267,10 +260,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -357,10 +347,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -482,10 +469,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -563,9 +547,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -573,10 +555,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -633,10 +612,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -644,10 +620,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -711,10 +684,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -722,10 +692,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -838,32 +805,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -871,10 +828,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -931,9 +885,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -941,9 +893,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -960,4 +910,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -102,17 +102,12 @@
"indexes": {
"provider": {
"name": "provider",
"columns": [
"provider",
"subject"
],
"columns": ["provider", "subject"],
"isUnique": true
},
"account_id": {
"name": "account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
}
},
@@ -249,9 +244,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -259,10 +252,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -349,10 +339,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -474,10 +461,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -555,9 +539,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -565,10 +547,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -625,10 +604,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -636,10 +612,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -703,10 +676,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -714,10 +684,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -830,32 +797,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -863,10 +820,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -923,9 +877,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -933,9 +885,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -952,4 +902,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -43,9 +43,7 @@
"compositePrimaryKeys": {
"account_id_pk": {
"name": "account_id_pk",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -109,17 +107,12 @@
"indexes": {
"provider": {
"name": "provider",
"columns": [
"provider",
"subject"
],
"columns": ["provider", "subject"],
"isUnique": true
},
"account_id": {
"name": "account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
}
},
@@ -127,9 +120,7 @@
"compositePrimaryKeys": {
"auth_id_pk": {
"name": "auth_id_pk",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -263,9 +254,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -273,10 +262,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -363,10 +349,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -488,10 +471,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -569,9 +549,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -579,10 +557,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -639,10 +614,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -650,10 +622,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -717,10 +686,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -728,10 +694,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -844,32 +807,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -877,10 +830,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -937,9 +887,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -947,9 +895,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -966,4 +912,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -43,9 +43,7 @@
"compositePrimaryKeys": {
"account_id_pk": {
"name": "account_id_pk",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -109,17 +107,12 @@
"indexes": {
"provider": {
"name": "provider",
"columns": [
"provider",
"subject"
],
"columns": ["provider", "subject"],
"isUnique": true
},
"account_id": {
"name": "account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
}
},
@@ -127,9 +120,7 @@
"compositePrimaryKeys": {
"auth_id_pk": {
"name": "auth_id_pk",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -277,9 +268,7 @@
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": [
"customer_id"
],
"columns": ["customer_id"],
"isUnique": true
}
},
@@ -287,10 +276,7 @@
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -377,10 +363,7 @@
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -502,10 +485,7 @@
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -583,9 +563,7 @@
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"columns": ["key"],
"isUnique": true
}
},
@@ -593,10 +571,7 @@
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -653,10 +628,7 @@
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": [
"workspace_id",
"model"
],
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
@@ -664,10 +636,7 @@
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -731,10 +700,7 @@
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": [
"workspace_id",
"provider"
],
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
@@ -742,10 +708,7 @@
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -858,32 +821,22 @@
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": [
"workspace_id",
"account_id"
],
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": [
"workspace_id",
"email"
],
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": [
"account_id"
],
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": [
"email"
],
"columns": ["email"],
"isUnique": false
}
},
@@ -891,10 +844,7 @@
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
@@ -951,9 +901,7 @@
"indexes": {
"slug": {
"name": "slug",
"columns": [
"slug"
],
"columns": ["slug"],
"isUnique": true
}
},
@@ -961,9 +909,7 @@
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
"columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -980,4 +926,4 @@
"tables": {},
"indexes": {}
}
}
}
@@ -269,4 +269,4 @@
"breakpoints": true
}
]
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.32",
"version": "1.0.48",
"private": true,
"type": "module",
"dependencies": {
+3 -10
View File
@@ -8,22 +8,15 @@ if (!email) {
process.exit(1)
}
const authData = await printTable("Auth", (tx) =>
tx.select().from(AuthTable).where(eq(AuthTable.subject, email)),
)
const authData = await printTable("Auth", (tx) => tx.select().from(AuthTable).where(eq(AuthTable.subject, email)))
if (authData.length === 0) {
console.error("User not found")
process.exit(1)
}
await printTable("Auth", (tx) =>
tx.select().from(AuthTable).where(eq(AuthTable.accountID, authData[0].accountID)),
)
await printTable("Auth", (tx) => tx.select().from(AuthTable).where(eq(AuthTable.accountID, authData[0].accountID)))
function printTable(
title: string,
callback: (tx: Database.TxOrDb) => Promise<any[]>,
): Promise<any[]> {
function printTable(title: string, callback: (tx: Database.TxOrDb) => Promise<any[]>): Promise<any[]> {
return Database.use(async (tx) => {
const data = await callback(tx)
console.log(`== ${title} ==`)
+1 -9
View File
@@ -8,14 +8,6 @@ import { KeyTable } from "../src/schema/key.sql.js"
if (Resource.App.stage !== "frank") throw new Error("This script is only for frank")
for (const table of [
AccountTable,
BillingTable,
KeyTable,
PaymentTable,
UsageTable,
UserTable,
WorkspaceTable,
]) {
for (const table of [AccountTable, BillingTable, KeyTable, PaymentTable, UsageTable, UserTable, WorkspaceTable]) {
await Database.use((tx) => tx.delete(table))
}
@@ -7,7 +7,6 @@ import { ZenData } from "../src/model"
const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
console.log("models", models)
// read the line starting with "ZEN_MODELS"
const oldValue1 = models
+2 -3
View File
@@ -24,6 +24,7 @@ export namespace ZenData {
cost: ModelCostSchema,
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
rateLimit: z.number().optional(),
providers: z.array(
z.object({
id: z.string(),
@@ -60,9 +61,7 @@ export namespace Model {
export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
Actor.assertAdmin()
return Database.use((db) =>
db
.delete(ModelTable)
.where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
db.delete(ModelTable).where(and(eq(ModelTable.workspaceID, Actor.workspace()), eq(ModelTable.model, model))),
)
})
+78 -69
View File
@@ -6,99 +6,108 @@
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
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
}
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
}
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
Console: {
type: "sst.cloudflare.SolidStart"
url: string
}
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
}
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
}
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
Web: {
type: "sst.cloudflare.Astro"
url: string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
}
ZEN_MODELS2: {
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
"LogProcessor": cloudflare.Service
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
}
}
import "sst"
export {}
export {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.32",
"version": "1.0.48",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+78 -69
View File
@@ -6,99 +6,108 @@
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
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
}
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
}
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
Console: {
type: "sst.cloudflare.SolidStart"
url: string
}
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
}
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
}
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
Web: {
type: "sst.cloudflare.Astro"
url: string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
}
ZEN_MODELS2: {
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
"LogProcessor": cloudflare.Service
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
}
}
import "sst"
export {}
export {}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.32",
"version": "1.0.48",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -6,4 +6,4 @@
/// <reference path="../../../sst-env.d.ts" />
import "sst"
export {}
export {}
+4 -1
View File
@@ -13,6 +13,9 @@
}
},
"devDependencies": {
"@tsconfig/node22": "22.0.2"
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/node": "catalog:",
"cloudflare": "5.2.0"
}
}
+58 -1
View File
@@ -1 +1,58 @@
export { Resource } from "sst"
import type { KVNamespaceListOptions, KVNamespaceListResult, KVNamespacePutOptions } from "@cloudflare/workers-types"
import { Resource as ResourceBase } from "sst"
import Cloudflare from "cloudflare"
export const Resource = new Proxy(
{},
{
get(_target, prop: keyof typeof ResourceBase) {
const value = ResourceBase[prop]
// @ts-ignore
if ("type" in value && value.type === "sst.cloudflare.Kv") {
const client = new Cloudflare({
apiToken: ResourceBase.CLOUDFLARE_API_TOKEN.value,
})
// @ts-ignore
const namespaceId = value.namespaceId
const accountId = ResourceBase.CLOUDFLARE_DEFAULT_ACCOUNT_ID.value
return {
get: (k: string | string[]) => {
const isMulti = Array.isArray(k)
return client.kv.namespaces
.bulkGet(namespaceId, {
keys: Array.isArray(k) ? k : [k],
account_id: accountId,
})
.then((result) => (isMulti ? new Map(Object.entries(result?.values ?? {})) : result?.values?.[k]))
},
put: (k: string, v: string, opts?: KVNamespacePutOptions) =>
client.kv.namespaces.values.update(namespaceId, k, {
account_id: accountId,
value: v,
expiration: opts?.expiration,
expiration_ttl: opts?.expirationTtl,
metadata: opts?.metadata,
}),
delete: (k: string) =>
client.kv.namespaces.values.delete(namespaceId, k, {
account_id: accountId,
}),
list: (opts?: KVNamespaceListOptions): Promise<KVNamespaceListResult<unknown, string>> =>
client.kv.namespaces.keys
.list(namespaceId, {
account_id: accountId,
prefix: opts?.prefix ?? undefined,
})
.then((result) => {
return {
keys: result.result,
list_complete: true,
cacheStatus: null,
}
}),
}
}
return value
},
},
) as Record<string, any>
+78 -69
View File
@@ -6,99 +6,108 @@
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
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
CLOUDFLARE_API_TOKEN: {
type: "sst.sst.Secret"
value: string
}
"Database": {
"database": string
"host": string
"password": string
"port": number
"type": "sst.sst.Linkable"
"username": string
CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
type: "sst.sst.Secret"
value: string
}
"Desktop": {
"type": "sst.cloudflare.StaticSite"
"url": string
Console: {
type: "sst.cloudflare.SolidStart"
url: string
}
"EMAILOCTOPUS_API_KEY": {
"type": "sst.sst.Secret"
"value": string
Database: {
database: string
host: string
password: string
port: number
type: "sst.sst.Linkable"
username: string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
Desktop: {
type: "sst.cloudflare.StaticSite"
url: string
}
"GITHUB_APP_PRIVATE_KEY": {
"type": "sst.sst.Secret"
"value": string
EMAILOCTOPUS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_ID_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_ID: {
type: "sst.sst.Secret"
value: string
}
"GITHUB_CLIENT_SECRET_CONSOLE": {
"type": "sst.sst.Secret"
"value": string
GITHUB_APP_PRIVATE_KEY: {
type: "sst.sst.Secret"
value: string
}
"GOOGLE_CLIENT_ID": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_ID_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
GITHUB_CLIENT_SECRET_CONSOLE: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_SECRET_KEY": {
"type": "sst.sst.Secret"
"value": string
GOOGLE_CLIENT_ID: {
type: "sst.sst.Secret"
value: string
}
"STRIPE_WEBHOOK_SECRET": {
"type": "sst.sst.Linkable"
"value": string
HONEYCOMB_API_KEY: {
type: "sst.sst.Secret"
value: string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
}
"ZEN_MODELS1": {
"type": "sst.sst.Secret"
"value": string
STRIPE_WEBHOOK_SECRET: {
type: "sst.sst.Linkable"
value: string
}
"ZEN_MODELS2": {
"type": "sst.sst.Secret"
"value": string
Web: {
type: "sst.cloudflare.Astro"
url: string
}
ZEN_MODELS1: {
type: "sst.sst.Secret"
value: string
}
ZEN_MODELS2: {
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
"LogProcessor": cloudflare.Service
Api: cloudflare.Service
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
}
}
import "sst"
export {}
export {}
+1 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.32",
"version": "1.0.48",
"description": "",
"type": "module",
"scripts": {
@@ -46,9 +46,5 @@
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:"
},
"prettier": {
"semi": false,
"printWidth": 120
}
}
-846
View File
@@ -1,846 +0,0 @@
import { bundledLanguages, type BundledLanguage, type ShikiTransformer } from "shiki"
import { splitProps, type ComponentProps, createEffect, onMount, onCleanup, createMemo, createResource } from "solid-js"
import { useLocal, type TextSelection } from "@/context/local"
import { getFileExtension, getNodeOffsetInLine, getSelectionInContainer } from "@/utils"
import { useShiki } from "@opencode-ai/ui"
type DefinedSelection = Exclude<TextSelection, undefined>
interface Props extends ComponentProps<"div"> {
code: string
path: string
}
export function Code(props: Props) {
const ctx = useLocal()
const highlighter = useShiki()
const [local, others] = splitProps(props, ["class", "classList", "code", "path"])
const lang = createMemo(() => {
const ext = getFileExtension(local.path)
if (ext in bundledLanguages) return ext
return "text"
})
let container: HTMLDivElement | undefined
let isProgrammaticSelection = false
const ranges = createMemo<DefinedSelection[]>(() => {
const items = ctx.context.all() as Array<{ type: "file"; path: string; selection?: DefinedSelection }>
const result: DefinedSelection[] = []
for (const item of items) {
if (item.path !== local.path) continue
const selection = item.selection
if (!selection) continue
result.push(selection)
}
return result
})
const createLineNumberTransformer = (selections: DefinedSelection[]): ShikiTransformer => {
const highlighted = new Set<number>()
for (const selection of selections) {
const startLine = selection.startLine
const endLine = selection.endLine
const start = Math.max(1, Math.min(startLine, endLine))
const end = Math.max(start, Math.max(startLine, endLine))
const count = end - start + 1
if (count <= 0) continue
const values = Array.from({ length: count }, (_, index) => start + index)
for (const value of values) highlighted.add(value)
}
return {
name: "line-number-highlight",
line(node, index) {
if (!highlighted.has(index)) return
this.addClassToHast(node, "line-number-highlight")
const children = node.children
if (!Array.isArray(children)) return
for (const child of children) {
if (!child || typeof child !== "object") continue
const element = child as { type?: string; properties?: { className?: string[] } }
if (element.type !== "element") continue
const className = element.properties?.className
if (!Array.isArray(className)) continue
const matches = className.includes("diff-oldln") || className.includes("diff-newln")
if (!matches) continue
if (className.includes("line-number-highlight")) continue
className.push("line-number-highlight")
}
},
}
}
const [html] = createResource(
() => ranges(),
async (activeRanges) => {
if (!highlighter.getLoadedLanguages().includes(lang())) {
await highlighter.loadLanguage(lang() as BundledLanguage)
}
return highlighter.codeToHtml(local.code || "", {
lang: lang() && lang() in bundledLanguages ? lang() : "text",
theme: "opencode",
transformers: [transformerUnifiedDiff(), transformerDiffGroups(), createLineNumberTransformer(activeRanges)],
}) as string
},
)
onMount(() => {
if (!container) return
let ticking = false
const onScroll = () => {
if (!container) return
// if (ctx.file.active()?.path !== local.path) return
if (ticking) return
ticking = true
requestAnimationFrame(() => {
ticking = false
ctx.file.scroll(local.path, container!.scrollTop)
})
}
const onSelectionChange = async () => {
if (!container) return
if (isProgrammaticSelection) return
// if (ctx.file.active()?.path !== local.path) return
const d = getSelectionInContainer(container)
if (!d) return
const p = (await ctx.file.node(local.path))?.selection
if (p && p.startLine === d.sl && p.endLine === d.el && p.startChar === d.sch && p.endChar === d.ech) return
ctx.file.select(local.path, { startLine: d.sl, startChar: d.sch, endLine: d.el, endChar: d.ech })
}
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
const onKeyDown = (e: KeyboardEvent) => {
// if (ctx.file.active()?.path !== local.path) return
const ae = document.activeElement as HTMLElement | undefined
const tag = (ae?.tagName || "").toLowerCase()
const inputFocused = !!ae && (tag === "input" || tag === "textarea" || ae.isContentEditable)
if (inputFocused) return
if (e.getModifierState(MOD) && e.key.toLowerCase() === "a") {
e.preventDefault()
if (!container) return
const element = container.querySelector("code") as HTMLElement | undefined
if (!element) return
const lines = Array.from(element.querySelectorAll(".line"))
if (!lines.length) return
const r = document.createRange()
const last = lines[lines.length - 1]
r.selectNodeContents(last)
const lastLen = r.toString().length
ctx.file.select(local.path, { startLine: 1, startChar: 0, endLine: lines.length, endChar: lastLen })
}
}
container.addEventListener("scroll", onScroll)
document.addEventListener("selectionchange", onSelectionChange)
document.addEventListener("keydown", onKeyDown)
onCleanup(() => {
container?.removeEventListener("scroll", onScroll)
document.removeEventListener("selectionchange", onSelectionChange)
document.removeEventListener("keydown", onKeyDown)
})
})
// Restore scroll position from store when content is ready
createEffect(async () => {
const content = html()
if (!container || !content) return
const top = (await ctx.file.node(local.path))?.scrollTop
if (top !== undefined && container.scrollTop !== top) container.scrollTop = top
})
// Sync selection from store -> DOM
createEffect(async () => {
const content = html()
if (!container || !content) return
// if (ctx.file.active()?.path !== local.path) return
const codeEl = container.querySelector("code") as HTMLElement | undefined
if (!codeEl) return
const target = (await ctx.file.node(local.path))?.selection
const current = getSelectionInContainer(container)
const sel = window.getSelection()
if (!sel) return
if (!target) {
if (current) {
isProgrammaticSelection = true
sel.removeAllRanges()
queueMicrotask(() => {
isProgrammaticSelection = false
})
}
return
}
const matches = !!(
current &&
current.sl === target.startLine &&
current.sch === target.startChar &&
current.el === target.endLine &&
current.ech === target.endChar
)
if (matches) return
const lines = Array.from(codeEl.querySelectorAll(".line"))
if (lines.length === 0) return
let sIdx = Math.max(0, target.startLine - 1)
let eIdx = Math.max(0, target.endLine - 1)
let sChar = Math.max(0, target.startChar || 0)
let eChar = Math.max(0, target.endChar || 0)
if (sIdx > eIdx || (sIdx === eIdx && sChar > eChar)) {
const ti = sIdx
sIdx = eIdx
eIdx = ti
const tc = sChar
sChar = eChar
eChar = tc
}
if (eChar === 0 && eIdx > sIdx) {
eIdx = eIdx - 1
eChar = Number.POSITIVE_INFINITY
}
if (sIdx >= lines.length) return
if (eIdx >= lines.length) eIdx = lines.length - 1
const s = getNodeOffsetInLine(lines[sIdx], sChar) ?? { node: lines[sIdx], offset: 0 }
const e = getNodeOffsetInLine(lines[eIdx], eChar) ?? { node: lines[eIdx], offset: lines[eIdx].childNodes.length }
const range = document.createRange()
range.setStart(s.node, s.offset)
range.setEnd(e.node, e.offset)
isProgrammaticSelection = true
sel.removeAllRanges()
sel.addRange(range)
queueMicrotask(() => {
isProgrammaticSelection = false
})
})
// Build/toggle split layout and apply folding (both unified and split)
createEffect(() => {
const content = html()
if (!container || !content) return
const view = ctx.file.view(local.path)
const pres = Array.from(container.querySelectorAll<HTMLPreElement>("pre"))
if (pres.length === 0) return
const originalPre = pres[0]
const split = container.querySelector<HTMLElement>(".diff-split")
if (view === "diff-split") {
applySplitDiff(container)
const next = container.querySelector<HTMLElement>(".diff-split")
if (next) next.style.display = ""
originalPre.style.display = "none"
} else {
if (split) split.style.display = "none"
originalPre.style.display = ""
}
const expanded = ctx.file.folded(local.path)
if (view === "diff-split") {
const left = container.querySelector<HTMLElement>(".diff-split pre:nth-child(1) code")
const right = container.querySelector<HTMLElement>(".diff-split pre:nth-child(2) code")
if (left)
applyDiffFolding(left, 3, { expanded, onExpand: (key) => ctx.file.unfold(local.path, key), side: "left" })
if (right)
applyDiffFolding(right, 3, { expanded, onExpand: (key) => ctx.file.unfold(local.path, key), side: "right" })
} else {
const code = container.querySelector<HTMLElement>("pre code")
if (code)
applyDiffFolding(code, 3, {
expanded,
onExpand: (key) => ctx.file.unfold(local.path, key),
})
}
})
// Highlight groups + scroll coupling
const clearHighlights = () => {
if (!container) return
container.querySelectorAll<HTMLElement>(".diff-selected").forEach((el) => el.classList.remove("diff-selected"))
}
const applyHighlight = (idx: number, scroll?: boolean) => {
if (!container) return
const view = ctx.file.view(local.path)
if (view === "raw") return
clearHighlights()
const nodes: HTMLElement[] = []
if (view === "diff-split") {
const left = container.querySelector<HTMLElement>(".diff-split pre:nth-child(1) code")
const right = container.querySelector<HTMLElement>(".diff-split pre:nth-child(2) code")
if (left)
nodes.push(...Array.from(left.querySelectorAll<HTMLElement>(`[data-chgrp="${idx}"][data-diff="remove"]`)))
if (right)
nodes.push(...Array.from(right.querySelectorAll<HTMLElement>(`[data-chgrp="${idx}"][data-diff="add"]`)))
} else {
const code = container.querySelector<HTMLElement>("pre code")
if (code) nodes.push(...Array.from(code.querySelectorAll<HTMLElement>(`[data-chgrp="${idx}"]`)))
}
for (const n of nodes) n.classList.add("diff-selected")
if (scroll && nodes.length) nodes[0].scrollIntoView({ block: "center", behavior: "smooth" })
}
const countGroups = () => {
if (!container) return 0
const code = container.querySelector<HTMLElement>("pre code")
if (!code) return 0
const set = new Set<string>()
for (const el of Array.from(code.querySelectorAll<HTMLElement>(".diff-line[data-chgrp]"))) {
const v = el.getAttribute("data-chgrp")
if (v != undefined) set.add(v)
}
return set.size
}
let lastIdx: number | undefined = undefined
let lastView: string | undefined
let lastContent: string | undefined
let lastRawIdx: number | undefined = undefined
createEffect(() => {
const content = html()
if (!container || !content) return
const view = ctx.file.view(local.path)
const raw = ctx.file.changeIndex(local.path)
if (raw === undefined) return
const total = countGroups()
if (total <= 0) return
const next = ((raw % total) + total) % total
const navigated = lastRawIdx !== undefined && lastRawIdx !== raw
if (next !== raw) {
ctx.file.setChangeIndex(local.path, next)
applyHighlight(next, true)
} else {
if (lastView !== view || lastContent !== content) applyHighlight(next)
if ((lastIdx !== undefined && lastIdx !== next) || navigated) applyHighlight(next, true)
}
lastRawIdx = raw
lastIdx = next
lastView = view
lastContent = content
})
return (
<div
ref={(el) => {
container = el
}}
innerHTML={html()}
class="
font-mono text-xs tracking-wide overflow-y-auto h-full
[&]:[counter-reset:line]
[&_pre]:focus-visible:outline-none
[&_pre]:overflow-x-auto [&_pre]:no-scrollbar
[&_code]:min-w-full [&_code]:inline-block
[&_.tab]:relative
[&_.tab::before]:content['⇥']
[&_.tab::before]:absolute
[&_.tab::before]:opacity-0
[&_.space]:relative
[&_.space::before]:content-['·']
[&_.space::before]:absolute
[&_.space::before]:opacity-0
[&_.line]:inline-block [&_.line]:w-full
[&_.line]:hover:bg-background-element
[&_.line::before]:sticky [&_.line::before]:left-0
[&_.line::before]:w-12 [&_.line::before]:pr-4
[&_.line::before]:z-10
[&_.line::before]:bg-background-panel
[&_.line::before]:text-text-muted/60
[&_.line::before]:text-right [&_.line::before]:inline-block
[&_.line::before]:select-none
[&_.line::before]:[counter-increment:line]
[&_.line::before]:content-[counter(line)]
[&_.line-number-highlight]:bg-accent/20
[&_.line-number-highlight::before]:bg-accent/40!
[&_.line-number-highlight::before]:text-background-panel!
[&_code.code-diff_.line::before]:content-['']
[&_code.code-diff_.line::before]:w-0
[&_code.code-diff_.line::before]:pr-0
[&_.diff-split_code.code-diff::before]:w-10
[&_.diff-split_.diff-newln]:left-0
[&_.diff-oldln]:sticky [&_.diff-oldln]:left-0
[&_.diff-oldln]:w-10 [&_.diff-oldln]:pr-2
[&_.diff-oldln]:z-40
[&_.diff-oldln]:text-text-muted/60
[&_.diff-oldln]:text-right [&_.diff-oldln]:inline-block
[&_.diff-oldln]:select-none
[&_.diff-oldln]:bg-background-panel
[&_.diff-newln]:sticky [&_.diff-newln]:left-10
[&_.diff-newln]:w-10 [&_.diff-newln]:pr-2
[&_.diff-newln]:z-40
[&_.diff-newln]:text-text-muted/60
[&_.diff-newln]:text-right [&_.diff-newln]:inline-block
[&_.diff-newln]:select-none
[&_.diff-newln]:bg-background-panel
[&_.diff-add]:bg-success/20!
[&_.diff-add.diff-selected]:bg-success/50!
[&_.diff-add_.diff-oldln]:bg-success!
[&_.diff-add_.diff-oldln]:text-background-panel!
[&_.diff-add_.diff-newln]:bg-success!
[&_.diff-add_.diff-newln]:text-background-panel!
[&_.diff-remove]:bg-error/20!
[&_.diff-remove.diff-selected]:bg-error/50!
[&_.diff-remove_.diff-newln]:bg-error!
[&_.diff-remove_.diff-newln]:text-background-panel!
[&_.diff-remove_.diff-oldln]:bg-error!
[&_.diff-remove_.diff-oldln]:text-background-panel!
[&_.diff-sign]:inline-block [&_.diff-sign]:px-2 [&_.diff-sign]:select-none
[&_.diff-blank]:bg-background-element
[&_.diff-blank_.diff-oldln]:bg-background-element
[&_.diff-blank_.diff-newln]:bg-background-element
[&_.diff-collapsed]:block! [&_.diff-collapsed]:w-full [&_.diff-collapsed]:relative
[&_.diff-collapsed]:select-none
[&_.diff-collapsed]:bg-info/20 [&_.diff-collapsed]:hover:bg-info/40!
[&_.diff-collapsed]:text-info/80 [&_.diff-collapsed]:hover:text-info
[&_.diff-collapsed]:text-xs
[&_.diff-collapsed_.diff-oldln]:bg-info!
[&_.diff-collapsed_.diff-newln]:bg-info!
"
classList={{
...(local.classList || {}),
[local.class ?? ""]: !!local.class,
}}
{...others}
></div>
)
}
function transformerUnifiedDiff(): ShikiTransformer {
const kinds = new Map<number, string>()
const meta = new Map<number, { old?: number; new?: number; sign?: string }>()
let isDiff = false
return {
name: "unified-diff",
preprocess(input) {
kinds.clear()
meta.clear()
isDiff = false
const ls = input.split(/\r?\n/)
const out: Array<string> = []
let oldNo = 0
let newNo = 0
let inHunk = false
for (let i = 0; i < ls.length; i++) {
const s = ls[i]
const m = s.match(/^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@/)
if (m) {
isDiff = true
inHunk = true
oldNo = parseInt(m[1], 10)
newNo = parseInt(m[3], 10)
continue
}
if (
/^diff --git /.test(s) ||
/^Index: /.test(s) ||
/^--- /.test(s) ||
/^\+\+\+ /.test(s) ||
/^[=]{3,}$/.test(s) ||
/^\*{3,}$/.test(s) ||
/^\\ No newline at end of file$/.test(s)
) {
isDiff = true
continue
}
if (!inHunk) {
out.push(s)
continue
}
if (/^\+/.test(s)) {
out.push(s)
const ln = out.length
kinds.set(ln, "add")
meta.set(ln, { new: newNo, sign: "+" })
newNo++
continue
}
if (/^-/.test(s)) {
out.push(s)
const ln = out.length
kinds.set(ln, "remove")
meta.set(ln, { old: oldNo, sign: "-" })
oldNo++
continue
}
if (/^ /.test(s)) {
out.push(s)
const ln = out.length
kinds.set(ln, "context")
meta.set(ln, { old: oldNo, new: newNo })
oldNo++
newNo++
continue
}
// fallback in hunks
out.push(s)
}
return out.join("\n").trimEnd()
},
code(node) {
if (isDiff) this.addClassToHast(node, "code-diff")
},
pre(node) {
if (isDiff) this.addClassToHast(node, "code-diff")
},
line(node, line) {
if (!isDiff) return
const kind = kinds.get(line)
if (!kind) return
const m = meta.get(line) || {}
this.addClassToHast(node, "diff-line")
this.addClassToHast(node, `diff-${kind}`)
node.properties = node.properties || {}
;(node.properties as any)["data-diff"] = kind
if (m.old != undefined) (node.properties as any)["data-old"] = String(m.old)
if (m.new != undefined) (node.properties as any)["data-new"] = String(m.new)
const oldSpan = {
type: "element",
tagName: "span",
properties: { className: ["diff-oldln"] },
children: [{ type: "text", value: m.old != undefined ? String(m.old) : " " }],
}
const newSpan = {
type: "element",
tagName: "span",
properties: { className: ["diff-newln"] },
children: [{ type: "text", value: m.new != undefined ? String(m.new) : " " }],
}
if (kind === "add" || kind === "remove" || kind === "context") {
const first = (node.children && (node.children as any[])[0]) as any
if (first && first.type === "element" && first.children && first.children.length > 0) {
const t = first.children[0]
if (t && t.type === "text" && typeof t.value === "string" && t.value.length > 0) {
const ch = t.value[0]
if (ch === "+" || ch === "-" || ch === " ") t.value = t.value.slice(1)
}
}
}
const signSpan = {
type: "element",
tagName: "span",
properties: { className: ["diff-sign"] },
children: [{ type: "text", value: (m as any).sign || " " }],
}
// @ts-expect-error hast typing across versions
node.children = [oldSpan, newSpan, signSpan, ...(node.children || [])]
},
}
}
function transformerDiffGroups(): ShikiTransformer {
let group = -1
let inGroup = false
return {
name: "diff-groups",
pre() {
group = -1
inGroup = false
},
line(node) {
const props = (node.properties || {}) as any
const kind = props["data-diff"] as string | undefined
if (kind === "add" || kind === "remove") {
if (!inGroup) {
group += 1
inGroup = true
}
;(node.properties as any)["data-chgrp"] = String(group)
} else {
inGroup = false
}
},
}
}
function applyDiffFolding(
root: HTMLElement,
context = 3,
options?: { expanded?: string[]; onExpand?: (key: string) => void; side?: "left" | "right" },
) {
if (!root.classList.contains("code-diff")) return
// Cleanup: unwrap previous collapsed blocks and remove toggles
const blocks = Array.from(root.querySelectorAll<HTMLElement>(".diff-collapsed-block"))
for (const block of blocks) {
const p = block.parentNode
if (!p) {
block.remove()
continue
}
while (block.firstChild) p.insertBefore(block.firstChild, block)
block.remove()
}
const toggles = Array.from(root.querySelectorAll<HTMLElement>(".diff-collapsed"))
for (const t of toggles) t.remove()
const lines = Array.from(root.querySelectorAll<HTMLElement>(".diff-line"))
if (lines.length === 0) return
const n = lines.length
const isChange = lines.map((l) => l.dataset["diff"] === "add" || l.dataset["diff"] === "remove")
const isContext = lines.map((l) => l.dataset["diff"] === "context")
if (!isChange.some(Boolean)) return
const visible = new Array(n).fill(false) as boolean[]
for (let i = 0; i < n; i++) if (isChange[i]) visible[i] = true
for (let i = 0; i < n; i++) {
if (isChange[i]) {
const s = Math.max(0, i - context)
const e = Math.min(n - 1, i + context)
for (let j = s; j <= e; j++) if (isContext[j]) visible[j] = true
}
}
type Range = { start: number; end: number }
const ranges: Range[] = []
let i = 0
while (i < n) {
if (!visible[i] && isContext[i]) {
let j = i
while (j + 1 < n && !visible[j + 1] && isContext[j + 1]) j++
ranges.push({ start: i, end: j })
i = j + 1
} else {
i++
}
}
for (const r of ranges) {
const start = lines[r.start]
const end = lines[r.end]
const count = r.end - r.start + 1
const minCollapse = 20
if (count < minCollapse) {
continue
}
// Wrap the entire collapsed chunk (including trailing newline) so it takes no space
const block = document.createElement("span")
block.className = "diff-collapsed-block"
start.parentElement?.insertBefore(block, start)
let cur: Node | undefined = start
while (cur) {
const next: Node | undefined = cur.nextSibling || undefined
block.appendChild(cur)
if (cur === end) {
// Also move the newline after the last line into the block
if (next && next.nodeType === Node.TEXT_NODE && (next.textContent || "").startsWith("\n")) {
block.appendChild(next)
}
break
}
cur = next
}
block.style.display = "none"
const row = document.createElement("span")
row.className = "line diff-collapsed"
row.setAttribute("data-kind", "collapsed")
row.setAttribute("data-count", String(count))
row.setAttribute("tabindex", "0")
row.setAttribute("role", "button")
const oldln = document.createElement("span")
oldln.className = "diff-oldln"
oldln.textContent = " "
const newln = document.createElement("span")
newln.className = "diff-newln"
newln.textContent = " "
const sign = document.createElement("span")
sign.className = "diff-sign"
sign.textContent = "…"
const label = document.createElement("span")
label.textContent = `show ${count} unchanged line${count > 1 ? "s" : ""}`
const key = `o${start.dataset["old"] || ""}-${end.dataset["old"] || ""}:n${start.dataset["new"] || ""}-${end.dataset["new"] || ""}`
const show = (record = true) => {
if (record) options?.onExpand?.(key)
const p = block.parentNode
if (p) {
while (block.firstChild) p.insertBefore(block.firstChild, block)
block.remove()
}
row.remove()
}
row.addEventListener("click", () => show(true))
row.addEventListener("keydown", (ev) => {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault()
show(true)
}
})
block.parentElement?.insertBefore(row, block)
if (!options?.side || options.side === "left") row.appendChild(oldln)
if (!options?.side || options.side === "right") row.appendChild(newln)
row.appendChild(sign)
row.appendChild(label)
if (options?.expanded && options.expanded.includes(key)) {
show(false)
}
}
}
function applySplitDiff(container: HTMLElement) {
const pres = Array.from(container.querySelectorAll<HTMLPreElement>("pre"))
if (pres.length === 0) return
const originalPre = pres[0]
const originalCode = originalPre.querySelector("code") as HTMLElement | undefined
if (!originalCode || !originalCode.classList.contains("code-diff")) return
// Rebuild split each time to match current content
const existing = container.querySelector<HTMLElement>(".diff-split")
if (existing) existing.remove()
const grid = document.createElement("div")
grid.className = "diff-split grid grid-cols-2 gap-x-6"
const makeColumn = () => {
const pre = document.createElement("pre")
pre.className = originalPre.className
const code = document.createElement("code")
code.className = originalCode.className
pre.appendChild(code)
return { pre, code }
}
const left = makeColumn()
const right = makeColumn()
// Helpers
const cloneSide = (line: HTMLElement, side: "old" | "new"): HTMLElement => {
const clone = line.cloneNode(true) as HTMLElement
const oldln = clone.querySelector(".diff-oldln")
const newln = clone.querySelector(".diff-newln")
if (side === "old") {
if (newln) newln.remove()
} else {
if (oldln) oldln.remove()
}
return clone
}
const blankLine = (side: "old" | "new", kind: "add" | "remove"): HTMLElement => {
const span = document.createElement("span")
span.className = "line diff-line diff-blank"
span.setAttribute("data-diff", kind)
const ln = document.createElement("span")
ln.className = side === "old" ? "diff-oldln" : "diff-newln"
ln.textContent = " "
span.appendChild(ln)
return span
}
const lines = Array.from(originalCode.querySelectorAll<HTMLElement>(".diff-line"))
let i = 0
while (i < lines.length) {
const cur = lines[i]
const kind = cur.dataset["diff"]
if (kind === "context") {
left.code.appendChild(cloneSide(cur, "old"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(cloneSide(cur, "new"))
right.code.appendChild(document.createTextNode("\n"))
i++
continue
}
if (kind === "remove") {
// Batch consecutive removes and following adds, then pair
const removes: HTMLElement[] = []
const adds: HTMLElement[] = []
let j = i
while (j < lines.length && lines[j].dataset["diff"] === "remove") {
removes.push(lines[j])
j++
}
let k = j
while (k < lines.length && lines[k].dataset["diff"] === "add") {
adds.push(lines[k])
k++
}
const pairs = Math.min(removes.length, adds.length)
for (let p = 0; p < pairs; p++) {
left.code.appendChild(cloneSide(removes[p], "old"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(cloneSide(adds[p], "new"))
right.code.appendChild(document.createTextNode("\n"))
}
for (let p = pairs; p < removes.length; p++) {
left.code.appendChild(cloneSide(removes[p], "old"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(blankLine("new", "remove"))
right.code.appendChild(document.createTextNode("\n"))
}
for (let p = pairs; p < adds.length; p++) {
left.code.appendChild(blankLine("old", "add"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(cloneSide(adds[p], "new"))
right.code.appendChild(document.createTextNode("\n"))
}
i = k
continue
}
if (kind === "add") {
// Run of adds not preceded by removes
const adds: HTMLElement[] = []
let j = i
while (j < lines.length && lines[j].dataset["diff"] === "add") {
adds.push(lines[j])
j++
}
for (let p = 0; p < adds.length; p++) {
left.code.appendChild(blankLine("old", "add"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(cloneSide(adds[p], "new"))
right.code.appendChild(document.createTextNode("\n"))
}
i = j
continue
}
// Any other kind: mirror as context
left.code.appendChild(cloneSide(cur, "old"))
left.code.appendChild(document.createTextNode("\n"))
right.code.appendChild(cloneSide(cur, "new"))
right.code.appendChild(document.createTextNode("\n"))
i++
}
grid.appendChild(left.pre)
grid.appendChild(right.pre)
container.appendChild(grid)
}
@@ -1,6 +1,6 @@
import { Button, Icon, IconButton, Select, SelectDialog } from "@opencode-ai/ui"
import { Button, Icon, IconButton, Select, SelectDialog, Tooltip } from "@opencode-ai/ui"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup } from "solid-js"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
import { createStore } from "solid-js/store"
import { FileIcon } from "@/ui"
import { getDirectory, getFilename } from "@/utils"
@@ -174,10 +174,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const addPart = (part: ContentPart) => {
const cursorPosition = getCursorPosition(editorRef)
const rawText = session.prompt
.current()
.map((p) => p.content)
.join("")
const prompt = session.prompt.current()
const rawText = prompt.map((p) => p.content).join("")
const textBeforeCursor = rawText.substring(0, cursorPosition)
const atMatch = textBeforeCursor.match(/@(\S*)$/)
@@ -203,7 +201,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
parts: nextParts,
inserted,
cursorPositionAfter,
} = session.prompt.current().reduce(
} = prompt.reduce(
(acc, item) => {
if (acc.inserted) {
acc.parts.push({ ...item, start: acc.runningIndex, end: acc.runningIndex + item.content.length })
@@ -262,7 +260,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
)
if (!inserted) {
const baseParts = session.prompt.current().filter((item) => !(item.type === "text" && item.content === ""))
const baseParts = prompt.filter((item) => !(item.type === "text" && item.content === ""))
const runningIndex = baseParts.reduce((sum, p) => sum + p.content.length, 0)
const appendedAcc = { parts: [...baseParts] as ContentPart[], runningIndex }
if (part.type === "text") {
@@ -316,10 +314,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleSubmit = async (event: Event) => {
event.preventDefault()
const text = session.prompt
.current()
.map((part) => part.content)
.join("")
const prompt = session.prompt.current()
const text = prompt.map((part) => part.content).join("")
if (text.trim().length === 0) {
if (session.working()) abort()
return
@@ -329,18 +325,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!existing) {
const created = await sdk.client.session.create()
existing = created.data ?? undefined
if (existing) navigate(`/session/${existing.id}`)
}
if (!existing) return
navigate(`/session/${existing.id}`)
if (!session.id) {
// session.layout.setOpenedTabs(
// session.layout.copyTabs("", session.id)
}
session.layout.setActiveTab(undefined)
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
// if (!session.id) {
// session.layout.setOpenedTabs(
// session.layout.copyTabs("", session.id)
// }
const attachments = session.prompt.current().filter((part) => part.type === "file")
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
const attachments = prompt.filter((part) => part.type === "file")
// const activeFile = local.context.active()
// if (activeFile) {
@@ -381,9 +376,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
})
session.layout.setActiveTab(undefined)
session.messages.setActive(undefined)
session.prompt.set(DEFAULT_PROMPT, 0)
await sdk.client.session.prompt({
sdk.client.session.prompt({
path: { id: existing.id },
body: {
agent: local.agent.current()!.name,
@@ -519,12 +516,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
)}
</SelectDialog>
</div>
<IconButton
type="submit"
disabled={!session.prompt.dirty() && !session.working()}
icon={session.working() ? "stop" : "arrow-up"}
variant="primary"
/>
<Tooltip
placement="top"
value={
<Switch>
<Match when={session.working()}>
<div class="flex items-center gap-2">
<span>Stop</span>
<span class="text-icon-base text-12-medium text-[10px]!">ESC</span>
</div>
</Match>
<Match when={true}>
<div class="flex items-center gap-2">
<span>Send</span>
<Icon name="enter" size="small" class="text-icon-base" />
</div>
</Match>
</Switch>
}
>
<IconButton
type="submit"
disabled={!session.prompt.dirty() && !session.working()}
icon={session.working() ? "stop" : "arrow-up"}
variant="primary"
class="rounded-full"
/>
</Tooltip>
</div>
</form>
</div>
-558
View File
@@ -1,558 +0,0 @@
{
"colors": {
"actionBar.toggledBackground": "var(--surface-raised-base)",
"activityBarBadge.background": "var(--surface-brand-base)",
"checkbox.border": "var(--border-base)",
"editor.background": "transparent",
"editor.foreground": "var(--text-base)",
"editor.inactiveSelectionBackground": "var(--surface-raised-base)",
"editor.selectionHighlightBackground": "var(--border-active)",
"editorIndentGuide.activeBackground1": "var(--border-weak-base)",
"editorIndentGuide.background1": "var(--border-weak-base)",
"input.placeholderForeground": "var(--text-weak)",
"list.activeSelectionIconForeground": "var(--text-base)",
"list.dropBackground": "var(--surface-raised-base)",
"menu.background": "var(--surface-base)",
"menu.border": "var(--border-base)",
"menu.foreground": "var(--text-base)",
"menu.selectionBackground": "var(--surface-interactive-base)",
"menu.separatorBackground": "var(--border-base)",
"ports.iconRunningProcessForeground": "var(--icon-success-base)",
"sideBarSectionHeader.background": "transparent",
"sideBarSectionHeader.border": "var(--border-weak-base)",
"sideBarTitle.foreground": "var(--text-weak)",
"statusBarItem.remoteBackground": "var(--surface-success-base)",
"statusBarItem.remoteForeground": "var(--text-base)",
"tab.lastPinnedBorder": "var(--border-weak-base)",
"tab.selectedBackground": "var(--surface-raised-base)",
"tab.selectedForeground": "var(--text-weak)",
"terminal.inactiveSelectionBackground": "var(--surface-raised-base)",
"widget.border": "var(--border-base)"
},
"displayName": "opencode",
"name": "opencode",
"semanticHighlighting": true,
"semanticTokenColors": {
"customLiteral": "var(--syntax-function)",
"newOperator": "var(--syntax-operator)",
"numberLiteral": "var(--syntax-number)",
"stringLiteral": "var(--syntax-string)"
},
"tokenColors": [
{
"scope": [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python"
],
"settings": {
"foreground": "var(--text-base)"
}
},
{
"scope": "emphasis",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "strong",
"settings": {
"fontStyle": "bold"
}
},
{
"scope": "header",
"settings": {
"foreground": "var(--markdown-heading)"
}
},
{
"scope": "comment",
"settings": {
"foreground": "var(--syntax-comment)"
}
},
{
"scope": "constant.language",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent"
],
"settings": {
"foreground": "var(--syntax-number)"
}
},
{
"scope": "constant.regexp",
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": "entity.name.tag",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": ["entity.name.tag.css", "entity.name.tag.less"],
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": "entity.other.attribute-name",
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss"
],
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": "invalid",
"settings": {
"foreground": "var(--syntax-critical)"
}
},
{
"scope": "markup.underline",
"settings": {
"fontStyle": "underline"
}
},
{
"scope": "markup.bold",
"settings": {
"fontStyle": "bold",
"foreground": "var(--markdown-strong)"
}
},
{
"scope": "markup.heading",
"settings": {
"fontStyle": "bold",
"foreground": "var(--theme-markdown-heading)"
}
},
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
}
},
{
"scope": "markup.strikethrough",
"settings": {
"fontStyle": "strikethrough"
}
},
{
"scope": "markup.inserted",
"settings": {
"foreground": "var(--text-diff-add-base)"
}
},
{
"scope": "markup.deleted",
"settings": {
"foreground": "var(--text-diff-delete-base)"
}
},
{
"scope": "markup.changed",
"settings": {
"foreground": "var(--text-base)"
}
},
{
"scope": "punctuation.definition.quote.begin.markdown",
"settings": {
"foreground": "var(--markdown-block-quote)"
}
},
{
"scope": "punctuation.definition.list.begin.markdown",
"settings": {
"foreground": "var(--markdown-list-enumeration)"
}
},
{
"scope": "markup.inline.raw",
"settings": {
"foreground": "var(--markdown-code)"
}
},
{
"scope": "punctuation.definition.tag",
"settings": {
"foreground": "var(--syntax-punctuation)"
}
},
{
"scope": ["meta.preprocessor", "entity.name.function.preprocessor"],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "meta.preprocessor.string",
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": "meta.preprocessor.numeric",
"settings": {
"foreground": "var(--syntax-number)"
}
},
{
"scope": "meta.structure.dictionary.key.python",
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": "meta.diff.header",
"settings": {
"foreground": "var(--text-weak)"
}
},
{
"scope": "storage",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "storage.type",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": ["storage.modifier", "keyword.operator.noexcept"],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": ["string", "meta.embedded.assembly"],
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": "string.tag",
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": "string.value",
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": "string.regexp",
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded"
],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": ["meta.template.expression"],
"settings": {
"foreground": "var(--text-base)"
}
},
{
"scope": [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded"
],
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": "keyword",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "keyword.control",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "keyword.operator",
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike"
],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "keyword.other.unit",
"settings": {
"foreground": "var(--syntax-number)"
}
},
{
"scope": ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "support.function.git-rebase",
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": "constant.sha.git-rebase",
"settings": {
"foreground": "var(--syntax-number)"
}
},
{
"scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
"settings": {
"foreground": "var(--text-base)"
}
},
{
"scope": "variable.language",
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal"
],
"settings": {
"foreground": "var(--syntax-function)"
}
},
{
"scope": [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy"
],
"settings": {
"foreground": "var(--syntax-type)"
}
},
{
"scope": [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby"
],
"settings": {
"foreground": "var(--syntax-type)"
}
},
{
"scope": [
"keyword.control",
"source.cpp keyword.operator.new",
"keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator"
],
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder"
],
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": ["variable.other.constant", "variable.other.enummember"],
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": ["meta.object-literal.key"],
"settings": {
"foreground": "var(--syntax-variable)"
}
},
{
"scope": [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color"
],
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp"
],
"settings": {
"foreground": "var(--syntax-string)"
}
},
{
"scope": [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp"
],
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": "keyword.operator.quantifier.regexp",
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": ["constant.character", "constant.other.option"],
"settings": {
"foreground": "var(--syntax-keyword)"
}
},
{
"scope": "constant.character.escape",
"settings": {
"foreground": "var(--syntax-operator)"
}
},
{
"scope": "entity.name.label",
"settings": {
"foreground": "var(--text-weak)"
}
}
],
"type": "dark"
}
+50
View File
@@ -5,6 +5,7 @@ import type { FileContent, FileNode, Model, Provider, File as FileStatus } from
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { makePersisted } from "@solid-primitives/storage"
export type LocalFile = FileNode &
Partial<{
@@ -456,11 +457,60 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})()
const layout = (() => {
const [store, setStore] = makePersisted(
createStore({
sidebar: {
opened: true,
width: 240,
},
review: {
state: "closed" as "open" | "closed" | "tab",
},
}),
{
name: "default-layout",
},
)
return {
sidebar: {
opened: createMemo(() => store.sidebar.opened),
open() {
setStore("sidebar", "opened", true)
},
close() {
setStore("sidebar", "opened", false)
},
toggle() {
setStore("sidebar", "opened", (x) => !x)
},
width: createMemo(() => store.sidebar.width),
resize(width: number) {
setStore("sidebar", "width", width)
},
},
review: {
state: createMemo(() => store.review?.state ?? "closed"),
open() {
setStore("review", "state", "open")
},
close() {
setStore("review", "state", "closed")
},
tab() {
setStore("review", "state", "tab")
},
},
}
})()
const result = {
model,
agent,
file,
context,
layout,
}
return result
},
+6 -2
View File
@@ -80,6 +80,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const model = createMemo(() =>
last() ? sync.data.provider.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
)
const diffs = createMemo(() => (props.sessionId ? (sync.data.session_diff[props.sessionId] ?? []) : []))
const tokens = createMemo(() => {
if (!last()) return
@@ -98,6 +99,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
id: props.sessionId,
info,
working,
diffs,
prompt: {
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursorPosition),
@@ -139,8 +141,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
if (tab.startsWith("file://")) {
await local.file.open(tab.replace("file://", ""))
}
if (!store.tabs.opened.includes(tab)) {
setStore("tabs", "opened", [...store.tabs.opened, tab])
if (tab !== "review") {
if (!store.tabs.opened.includes(tab)) {
setStore("tabs", "opened", [...store.tabs.opened, tab])
}
}
setStore("tabs", "active", tab)
},
+39 -16
View File
@@ -1,4 +1,17 @@
import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode, Project } from "@opencode-ai/sdk"
import type {
Message,
Agent,
Provider,
Session,
Part,
Config,
Path,
File,
FileNode,
Project,
FileDiff,
Todo,
} from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { createMemo } from "solid-js"
import { Binary } from "@/utils/binary"
@@ -16,8 +29,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
config: Config
path: Path
session: Session[]
session_diff: {
[sessionID: string]: FileDiff[]
}
todo: {
[sessionID: string]: Todo[]
}
limit: number
more: boolean
message: {
[sessionID: string]: Message[]
}
@@ -34,8 +52,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: [],
provider: [],
session: [],
session_diff: {},
todo: {},
limit: 10,
more: false,
message: {},
part: {},
node: [],
@@ -60,6 +79,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
)
break
}
case "session.diff":
setStore("session_diff", event.properties.sessionID, event.properties.diff)
break
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
@@ -116,7 +141,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", sessions)
setStore("more", sessions.length === store.limit)
}),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
@@ -161,22 +185,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) return store.session[match.index]
return undefined
},
async sync(sessionID: string, isRetry = false) {
const [session, messages] = await Promise.all([
sdk.client.session.get({ path: { id: sessionID } }),
sdk.client.session.messages({ path: { id: sessionID } }),
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 } }),
])
// If no messages and this might be a new session, retry after a delay
if (!isRetry && messages.data!.length === 0) {
setTimeout(() => this.sync(sessionID, true), 500)
return
}
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
draft.session[match.index] = session.data!
if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? []
draft.message[sessionID] = messages
.data!.map((x) => x.info)
.slice()
@@ -187,6 +208,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.map(sanitizePart)
.sort((a, b) => a.id.localeCompare(b.id))
}
draft.session_diff[sessionID] = diff.data ?? []
}),
)
},
@@ -194,6 +216,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore("limit", (x) => x + count)
await load.session()
},
more: createMemo(() => store.session.length === store.limit),
},
load,
absolute,
+17 -19
View File
@@ -3,7 +3,7 @@ import "@/index.css"
import { render } from "solid-js/web"
import { Router, Route } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Fonts, ShikiProvider, MarkedProvider } from "@opencode-ai/ui"
import { Fonts, MarkedProvider } from "@opencode-ai/ui"
import { SDKProvider } from "./context/sdk"
import { SyncProvider } from "./context/sync"
import { LocalProvider } from "./context/local"
@@ -29,24 +29,22 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
render(
() => (
<ShikiProvider>
<MarkedProvider>
<SDKProvider url={url}>
<SyncProvider>
<LocalProvider>
<MetaProvider>
<Fonts />
<Router root={Layout}>
<Route path={["/", "/session"]} component={SessionLayout}>
<Route path="/:id?" component={Session} />
</Route>
</Router>
</MetaProvider>
</LocalProvider>
</SyncProvider>
</SDKProvider>
</MarkedProvider>
</ShikiProvider>
<MarkedProvider>
<SDKProvider url={url}>
<SyncProvider>
<LocalProvider>
<MetaProvider>
<Fonts />
<Router root={Layout}>
<Route path={["/", "/session"]} component={SessionLayout}>
<Route path="/:id?" component={Session} />
</Route>
</Router>
</MetaProvider>
</LocalProvider>
</SyncProvider>
</SDKProvider>
</MarkedProvider>
),
root!,
)
+49 -14
View File
@@ -1,30 +1,42 @@
import { Button, Tooltip, DiffChanges } from "@opencode-ai/ui"
import { Button, Tooltip, DiffChanges, IconButton } from "@opencode-ai/ui"
import { createMemo, For, ParentProps, Show } from "solid-js"
import { getFilename } from "@/utils"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { VList } from "virtua/solid"
import { A, useParams } from "@solidjs/router"
import { useLocal } from "@/context/local"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useSync()
const local = useLocal()
return (
<div class="relative h-screen flex flex-col">
<header class="hidden h-12 shrink-0 bg-background-strong border-b border-border-weak-base"></header>
<div class="h-[calc(100vh-0rem)] flex">
<div class="w-70 shrink-0 bg-background-weak border-r border-border-weak-base flex flex-col items-start">
<div class="h-10 shrink-0 flex items-center self-stretch px-5 border-b border-border-weak-base">
<span class="text-14-regular overflow-hidden text-ellipsis">{getFilename(sync.data.path.directory)}</span>
</div>
<div class="flex flex-col items-start gap-4 self-stretch flex-1 py-4 px-3 overflow-hidden">
<A href="/session" class="w-full">
<Button class="w-full" size="large" icon="edit-small-2">
<div
classList={{
"@container w-14 pb-4 shrink-0 bg-background-weak": true,
"flex flex-col items-start self-stretch justify-between": true,
"border-r border-border-weak-base": true,
"w-70": local.layout.sidebar.opened(),
}}
>
<div class="flex flex-col justify-center items-start gap-4 self-stretch py-2 overflow-hidden mx-auto @[4rem]:mx-0">
<div class="h-8 shrink-0 flex items-center self-stretch px-3">
<Tooltip placement="right" value="Collapse sidebar">
<IconButton icon="layout-left" variant="ghost" size="large" onClick={local.layout.sidebar.toggle} />
</Tooltip>
</div>
<div class="w-full px-3">
<Button as={A} href="/session" class="hidden @[4rem]:flex w-full" size="large" icon="edit-small-2">
New Session
</Button>
</A>
<div class="w-full h-full overflow-y-auto no-scrollbar flex flex-col flex-1">
<Tooltip placement="right" value="New session">
<IconButton as={A} href="/session" icon="edit-small-2" size="large" class="@[4rem]:hidden" />
</Tooltip>
</div>
<div class="hidden @[4rem]:flex size-full overflow-y-auto no-scrollbar flex-col flex-1 px-3">
<nav class="w-full">
<For each={sync.data.session}>
{(session) => {
@@ -33,7 +45,7 @@ export default function Layout(props: ParentProps) {
<A
data-active={session.id === params.id}
href={`/session/${session.id}`}
class="group/session focus:outline-none"
class="group/session focus:outline-none cursor-default"
>
<Tooltip placement="right" value={session.title}>
<div
@@ -68,7 +80,7 @@ export default function Layout(props: ParentProps) {
}}
</For>
</nav>
<Show when={sync.data.more}>
<Show when={sync.session.more()}>
<button
class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong"
onClick={() => sync.session.fetch()}
@@ -78,6 +90,29 @@ export default function Layout(props: ParentProps) {
</Show>
</div>
</div>
<div class="flex flex-col items-start shrink-0 px-3 py-1 mx-auto @[4rem]:mx-0">
<Button
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
class="hidden @[4rem]:flex w-full text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
icon="speech-bubble"
>
Share feedback
</Button>
<Tooltip placement="right" value="Share feedback">
<IconButton
as={"a"}
href="https://opencode.ai/desktop-feedback"
target="_blank"
icon="speech-bubble"
variant="ghost"
size="large"
class="@[4rem]:hidden stroke-[1.5px]"
/>
</Tooltip>
</div>
</div>
<main class="size-full overflow-x-hidden">{props.children}</main>
</div>
+469 -263
View File
@@ -13,6 +13,7 @@ import {
Code,
Tooltip,
ProgressCircle,
Button,
} from "@opencode-ai/ui"
import { FileIcon } from "@/ui"
import { MessageProgress } from "@/components/message-progress"
@@ -221,9 +222,9 @@ export default function Page() {
</Switch>
<IconButton
icon="close"
class="mt-0.5 opacity-0 text-text-muted/60 group-data-[selected]/tab:opacity-100
group-data-[selected]/tab:text-text group-data-[selected]/tab:hover:bg-border-subtle
hover:opacity-100 group-hover/tab:opacity-100"
class="mt-0.5 opacity-0 group-data-[selected]/tab:opacity-100
hover:bg-transparent
hover:opacity-100 group-hover/tab:opacity-100"
variant="ghost"
onClick={() => props.onTabClose(props.tab)}
/>
@@ -289,105 +290,79 @@ export default function Page() {
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
</Tooltip>
</Tabs.Trigger>
{/* <Tabs.Trigger value="review">Review</Tabs.Trigger> */}
<Show when={local.layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Trigger value="review" class="flex gap-3 items-center group/tab pr-1">
<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>
<IconButton
icon="close"
class="mt-0.5 -ml-1 opacity-0 group-data-[selected]/tab:opacity-100
hover:bg-transparent hover:opacity-100 group-hover/tab:opacity-100"
variant="ghost"
onClick={local.layout.review.close}
/>
</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">
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
onClick={() => setStore("fileSelectOpen", true)}
/>
<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 class="relative px-6 pt-12 max-w-2xl w-full mx-auto flex flex-col flex-1 min-h-0">
<Show
when={session.id}
fallback={
<div class="flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch">
<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
classList={{
"w-full grid flex-1 min-h-0": true,
"grid-cols-2": local.layout.review.state() === "open",
}}
>
<div class="relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 max-w-2xl mx-auto">
<Switch>
<Match when={session.id}>
<div class="h-8 flex shrink-0 self-stretch items-center justify-end">
<Show when={local.layout.review.state() === "closed" && session.diffs().length}>
<Button icon="layout-right" onClick={local.layout.review.open}>
Review
</Button>
</Show>
</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>
}
>
{(_) => {
return (
<div class="pt-3 flex flex-col flex-1 min-h-0">
<div class="flex-1 min-h-0">
<Show when={session.messages.user().length > 1}>
<ul
role="list"
class="absolute right-full mr-8 hidden w-60 shrink-0 @7xl:flex flex-col items-start gap-1"
>
<For each={session.messages.user()}>
{(message) => {
const assistantMessages = createMemo(() => {
if (!session.id) return []
return sync.data.message[session.id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const working = createMemo(() => !message.summary?.body && !error())
return (
<li class="group/li flex items-center self-stretch">
<button
class="flex items-center self-stretch w-full gap-x-2 py-1 cursor-default"
onClick={() => session.messages.setActive(message.id)}
>
<Switch>
<Match when={working()}>
<Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
</Match>
<Match when={true}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
</Match>
</Switch>
<div
data-active={session.messages.active()?.id === message.id}
classList={{
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}}
>
<Show when={message.summary?.title} fallback="New message">
{message.summary?.title}
</Show>
</div>
</button>
</li>
)
}}
</For>
</ul>
</Show>
<div ref={messageScrollElement} class="grow min-w-0 h-full overflow-y-auto no-scrollbar">
<div
classList={{
"flex-1 min-h-0 pb-20": true,
"flex items-start justify-start": local.layout.review.state() === "open",
}}
>
<Show when={session.messages.user().length > 1}>
<ul
role="list"
classList={{
"mr-8 shrink-0 flex flex-col items-start": true,
"absolute right-full w-60 @7xl:gap-2": true, // local.layout.review.state() !== "open",
"": local.layout.review.state() === "open",
}}
>
<For each={session.messages.user()}>
{(message) => {
const isActive = createMemo(() => session.messages.active()?.id === message.id)
const [titled, setTitled] = createSignal(!!message.summary?.title)
const assistantMessages = createMemo(() => {
if (!session.id) return []
return sync.data.message[session.id]?.filter(
@@ -395,186 +370,415 @@ export default function Page() {
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id])
const hasToolPart = createMemo(() =>
assistantMessages()
?.flatMap((m) => sync.data.part[m.id])
.some((p) => p?.type === "tool"),
)
const working = createMemo(() => !message.summary?.body && !error())
// allowing time for the animations to finish
createEffect(() => {
const title = message.summary?.title
setTimeout(() => setTitled(!!title), 10_000)
})
createEffect(() => {
const summary = message.summary?.body
const complete = !!summary || !!error()
setTimeout(() => setCompleted(complete), 1200)
})
const handleClick = () => session.messages.setActive(message.id)
return (
<Show when={isActive()}>
<div
data-message={message.id}
class="flex flex-col items-start self-stretch gap-8 pb-50"
<li
classList={{
"group/li flex items-center self-stretch justify-end": true,
"@7xl:justify-start": local.layout.review.state() !== "open",
}}
>
<Tooltip
placement="right"
gutter={8}
value={
<div class="flex items-center gap-2">
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
{message.summary?.title}
</div>
}
>
{/* Title */}
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
<div class="w-full text-14-medium text-text-strong">
<Show
when={titled()}
fallback={
<Typewriter
as="h1"
text={message.summary?.title}
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
}
>
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">
{message.summary?.title}
</h1>
</Show>
</div>
<button
data-active={session.messages.active()?.id === message.id}
onClick={handleClick}
classList={{
"group/tick flex items-center justify-start h-2 w-8 -mr-3": true,
"data-[active=true]:[&>div]:bg-icon-strong-base data-[active=true]:[&>div]:w-full": true,
"@7xl:hidden": local.layout.review.state() !== "open",
}}
>
<div class="h-px w-5 bg-icon-base group-hover/tick:w-full group-hover/tick:bg-icon-strong-base" />
</button>
</Tooltip>
<button
classList={{
"hidden items-center self-stretch w-full gap-x-2 cursor-default": true,
"@7xl:flex": local.layout.review.state() !== "open",
}}
onClick={handleClick}
>
<Switch>
<Match when={working()}>
<Spinner class="text-text-base shrink-0 w-[18px] aspect-square" />
</Match>
<Match when={true}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
</Match>
</Switch>
<div
data-active={session.messages.active()?.id === message.id}
classList={{
"text-14-regular text-text-weak whitespace-nowrap truncate min-w-0": true,
"text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true,
}}
>
<Show when={message.summary?.title} fallback="New message">
{message.summary?.title}
</Show>
</div>
<div class="-mt-8">
<Message message={message} parts={parts()} />
</div>
{/* Summary */}
<Show when={completed()}>
<div class="w-full flex flex-col gap-6 items-start self-stretch">
<div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak">
<Switch>
<Match when={message.summary?.diffs?.length}>Summary</Match>
<Match when={true}>Response</Match>
</Switch>
</h2>
<Show when={message.summary?.body}>
{(summary) => (
<Markdown
classList={{
"text-14-regular": !!message.summary?.diffs?.length,
"[&>*]:fade-up-text": !message.summary?.diffs?.length,
}}
text={summary()}
/>
)}
</Show>
</div>
<Accordion class="w-full" multiple>
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<Accordion.Header>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
<FileIcon
node={{ path: diff.file, type: "file" }}
class="shrink-0 size-4"
/>
<div class="flex grow min-w-0">
<Show when={diff.file.includes("/")}>
<span class="text-text-base truncate-start">
{getDirectory(diff.file)}&lrm;
</span>
</Show>
<span class="text-text-strong shrink-0">
{getFilename(diff.file)}
</span>
</div>
</div>
<div class="shrink-0 flex gap-4 items-center justify-end">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="max-h-[300px] overflow-y-auto no-scrollbar">
<Diff
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !detailsExpanded()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
{/* Response */}
<div class="w-full">
<Switch>
<Match when={!completed()}>
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
</Match>
<Match when={completed() && hasToolPart()}>
<Collapsible
variant="ghost"
open={detailsExpanded()}
onOpenChange={setDetailsExpanded}
>
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch">
<div class="text-12-medium">
<Switch>
<Match when={detailsExpanded()}>Hide details</Match>
<Match when={!detailsExpanded()}>Show details</Match>
</Switch>
</div>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="w-full flex flex-col items-start self-stretch gap-3">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
return <Message message={assistantMessage} parts={parts()} />
}}
</For>
<Show when={error()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Collapsible.Content>
</Collapsible>
</Match>
</Switch>
</div>
</div>
</Show>
</button>
</li>
)
}}
</For>
</ul>
</Show>
<div ref={messageScrollElement} class="grow w-full min-w-0 h-full overflow-y-auto no-scrollbar">
<For each={session.messages.user()}>
{(message) => {
const isActive = createMemo(() => session.messages.active()?.id === message.id)
const [titled, setTitled] = createSignal(!!message.summary?.title)
const assistantMessages = createMemo(() => {
if (!session.id) return []
return sync.data.message[session.id]?.filter(
(m) => m.role === "assistant" && m.parentID == message.id,
) as AssistantMessageType[]
})
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
const [completed, setCompleted] = createSignal(!!message.summary?.body || !!error())
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id])
const hasToolPart = createMemo(() =>
assistantMessages()
?.flatMap((m) => sync.data.part[m.id])
.some((p) => p?.type === "tool"),
)
const working = createMemo(() => !message.summary?.body && !error())
// allowing time for the animations to finish
createEffect(() => {
const title = message.summary?.title
setTimeout(() => setTitled(!!title), 10_000)
})
createEffect(() => {
const summary = message.summary?.body
const complete = !!summary || !!error()
setTimeout(() => setCompleted(complete), 1200)
})
return (
<Show when={isActive()}>
<div
data-message={message.id}
class="flex flex-col items-start self-stretch gap-8 pb-20"
>
{/* Title */}
<div class="flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10 pb-1">
<div class="w-full text-14-medium text-text-strong">
<Show
when={titled()}
fallback={
<Typewriter
as="h1"
text={message.summary?.title}
class="overflow-hidden text-ellipsis min-w-0 text-nowrap"
/>
}
>
<h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">
{message.summary?.title}
</h1>
</Show>
</div>
</div>
<div class="-mt-9">
<Message message={message} parts={parts()} />
</div>
{/* Summary */}
<Show when={completed()}>
<div class="w-full flex flex-col gap-6 items-start self-stretch">
<div class="flex flex-col items-start gap-1 self-stretch">
<h2 class="text-12-medium text-text-weak">
<Switch>
<Match when={message.summary?.diffs?.length}>Summary</Match>
<Match when={true}>Response</Match>
</Switch>
</h2>
<Show when={message.summary?.body}>
{(summary) => (
<Markdown
classList={{
"text-14-regular": !!message.summary?.diffs?.length,
"[&>*]:fade-up-text": !message.summary?.diffs?.length,
}}
text={summary()}
/>
)}
</Show>
</div>
<Accordion class="w-full" multiple>
<For each={message.summary?.diffs ?? []}>
{(diff) => (
<Accordion.Item value={diff.file}>
<Accordion.Header>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
<FileIcon
node={{ path: diff.file, type: "file" }}
class="shrink-0 size-4"
/>
<div class="flex grow min-w-0">
<Show when={diff.file.includes("/")}>
<span class="text-text-base truncate-start">
{getDirectory(diff.file)}&lrm;
</span>
</Show>
<span class="text-text-strong shrink-0">
{getFilename(diff.file)}
</span>
</div>
</div>
<div class="shrink-0 flex gap-4 items-center justify-end">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="max-h-[360px] overflow-y-auto no-scrollbar">
<Diff
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</Show>
<Show when={error() && !detailsExpanded()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
{/* Response */}
<div class="w-full">
<Switch>
<Match when={!completed()}>
<MessageProgress assistantMessages={assistantMessages} done={!working()} />
</Match>
<Match when={completed() && hasToolPart()}>
<Collapsible
variant="ghost"
open={detailsExpanded()}
onOpenChange={setDetailsExpanded}
>
<Collapsible.Trigger class="text-text-weak hover:text-text-strong">
<div class="flex items-center gap-1 self-stretch">
<div class="text-12-medium">
<Switch>
<Match when={detailsExpanded()}>Hide details</Match>
<Match when={!detailsExpanded()}>Show details</Match>
</Switch>
</div>
<Collapsible.Arrow />
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="w-full flex flex-col items-start self-stretch gap-3">
<For each={assistantMessages()}>
{(assistantMessage) => {
const parts = createMemo(() => sync.data.part[assistantMessage.id])
return <Message message={assistantMessage} parts={parts()} />
}}
</For>
<Show when={error()}>
<Card variant="error" class="text-text-on-critical-base">
{error()?.data?.message as string}
</Card>
</Show>
</div>
</Collapsible.Content>
</Collapsible>
</Match>
</Switch>
</div>
</div>
</Show>
)
}}
</For>
</div>
</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">
<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 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
</div>
<Show when={local.layout.review.state() === "open"}>
<div
classList={{
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0 border-l border-border-weak-base": true,
}}
>
<div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch">
<div class="flex items-center gap-x-3">
<Tooltip value="Close">
<IconButton icon="align-right" variant="ghost" onClick={local.layout.review.close} />
</Tooltip>
<Tooltip value="Open in tab">
<IconButton
icon="expand"
variant="ghost"
onClick={() => {
local.layout.review.tab()
session.layout.setActiveTab("review")
}}
/>
</Tooltip>
</div>
</div>
<div class="text-14-medium text-text-strong">All changes</div>
<div class="h-full pb-40 overflow-y-auto no-scrollbar">
<Accordion class="w-full" multiple>
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
<FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" />
<div class="flex grow min-w-0">
<Show when={diff.file.includes("/")}>
<span class="text-text-base truncate-start">
{getDirectory(diff.file)}&lrm;
</span>
</Show>
<span class="text-text-strong shrink-0">{getFilename(diff.file)}</span>
</div>
</div>
<div class="shrink-0 flex gap-4 items-center justify-end">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<Diff
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</div>
</Show>
</div>
</Tabs.Content>
{/* <Tabs.Content value="review" class="select-text"></Tabs.Content> */}
<Show when={local.layout.review.state() === "tab" && session.diffs().length}>
<Tabs.Content value="review" class="select-text">
<div
classList={{
"relative px-6 py-2 w-full flex flex-col gap-6 flex-1 min-h-0": true,
}}
>
<div class="h-8 w-full flex items-center justify-between shrink-0 self-stretch sticky top-0 bg-background-stronger z-100">
<div class="flex items-center gap-x-3"></div>
</div>
<div class="text-14-medium text-text-strong">All changes</div>
<div class="h-full pb-40 overflow-y-auto no-scrollbar">
<Accordion class="w-full" multiple>
<For each={session.diffs()}>
{(diff) => (
<Accordion.Item value={diff.file} defaultOpen>
<Accordion.Header>
<Accordion.Trigger>
<div class="flex items-center justify-between w-full gap-5">
<div class="grow flex items-center gap-5 min-w-0">
<FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" />
<div class="flex grow min-w-0">
<Show when={diff.file.includes("/")}>
<span class="text-text-base truncate-start">{getDirectory(diff.file)}&lrm;</span>
</Show>
<span class="text-text-strong shrink-0">{getFilename(diff.file)}</span>
</div>
</div>
<div class="shrink-0 flex gap-4 items-center justify-end">
<DiffChanges changes={diff} />
<Icon name="chevron-grabber-vertical" size="small" />
</div>
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<Diff
diffStyle="split"
before={{
name: diff.file!,
contents: diff.before!,
}}
after={{
name: diff.file!,
contents: diff.after!,
}}
/>
</Accordion.Content>
</Accordion.Item>
)}
</For>
</Accordion>
</div>
</div>
</Tabs.Content>
</Show>
<For each={session.layout.tabs.opened}>
{(tab) => {
const [file] = createResource(
@@ -587,14 +791,14 @@ export default function Page() {
},
)
return (
<Tabs.Content value={tab} class="select-text">
<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="pt-3 pb-40"
class="pb-40"
/>
)}
</Match>
@@ -625,13 +829,15 @@ export default function Page() {
</Show>
</DragOverlay>
</DragDropProvider>
<div class="absolute inset-x-0 px-6 max-w-2xl flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
}}
/>
</div>
<Show when={session.layout.tabs.active}>
<div class="absolute inset-x-0 px-6 max-w-2xl 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>

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