Просмотр исходного кода

Merge remote-tracking branch 'origin/dev' into dev

Dax Raad 3 месяцев назад
Родитель
Сommit
4d20e1c3c6
100 измененных файлов с 742 добавлено и 2078 удалено
  1. 1 0
      STATS.md
  2. 47 28
      bun.lock
  3. 14 43
      github/index.ts
  4. 9 7
      infra/console.ts
  5. 2 2
      package.json
  6. 1 1
      packages/console/app/package.json
  7. 1 4
      packages/console/app/src/app.tsx
  8. 1 4
      packages/console/app/src/component/faq.tsx
  9. 20 113
      packages/console/app/src/component/icon.tsx
  10. 1 4
      packages/console/app/src/lib/github.ts
  11. 1 4
      packages/console/app/src/routes/auth/authorize.ts
  12. 21 123
      packages/console/app/src/routes/brand/index.tsx
  13. 19 38
      packages/console/app/src/routes/enterprise/index.tsx
  14. 109 653
      packages/console/app/src/routes/index.tsx
  15. 5 18
      packages/console/app/src/routes/stripe/webhook.ts
  16. 4 6
      packages/console/app/src/routes/temp.tsx
  17. 2 9
      packages/console/app/src/routes/workspace-picker.tsx
  18. 15 27
      packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx
  19. 2 6
      packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx
  20. 1 4
      packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx
  21. 5 9
      packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx
  22. 1 3
      packages/console/app/src/routes/workspace/[id]/index.tsx
  23. 2 8
      packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx
  24. 1 6
      packages/console/app/src/routes/workspace/[id]/members/member-section.tsx
  25. 2 11
      packages/console/app/src/routes/workspace/[id]/model-section.tsx
  26. 3 12
      packages/console/app/src/routes/workspace/[id]/new-user-section.tsx
  27. 3 13
      packages/console/app/src/routes/workspace/[id]/provider-section.tsx
  28. 1 4
      packages/console/app/src/routes/workspace/common.tsx
  29. 36 96
      packages/console/app/src/routes/zen/index.tsx
  30. 1 0
      packages/console/app/src/routes/zen/util/error.ts
  31. 49 80
      packages/console/app/src/routes/zen/util/handler.ts
  32. 7 19
      packages/console/app/src/routes/zen/util/provider/anthropic.ts
  33. 10 21
      packages/console/app/src/routes/zen/util/provider/openai-compatible.ts
  34. 9 29
      packages/console/app/src/routes/zen/util/provider/openai.ts
  35. 35 0
      packages/console/app/src/routes/zen/util/rateLimiter.ts
  36. 1 4
      packages/console/app/src/routes/zen/v1/models.ts
  37. 1 2
      packages/console/app/src/style/token/font.css
  38. 1 1
      packages/console/core/package.json
  39. 3 10
      packages/console/core/script/lookup-user.ts
  40. 1 9
      packages/console/core/script/reset-db.ts
  41. 0 1
      packages/console/core/script/update-models.ts
  42. 25 28
      packages/console/core/src/aws.ts
  43. 2 8
      packages/console/core/src/drizzle/index.ts
  44. 2 8
      packages/console/core/src/key.ts
  45. 2 3
      packages/console/core/src/model.ts
  46. 2 9
      packages/console/core/src/provider.ts
  47. 1 8
      packages/console/core/src/schema/auth.sql.ts
  48. 1 4
      packages/console/core/src/schema/model.sql.ts
  49. 1 4
      packages/console/core/src/schema/provider.sql.ts
  50. 1 9
      packages/console/core/src/schema/user.sql.ts
  51. 5 23
      packages/console/core/src/user.ts
  52. 9 0
      packages/console/core/sst-env.d.ts
  53. 1 1
      packages/console/function/package.json
  54. 9 0
      packages/console/function/sst-env.d.ts
  55. 4 21
      packages/console/mail/emails/templates/InviteEmail.tsx
  56. 1 1
      packages/console/mail/package.json
  57. 4 1
      packages/console/resource/package.json
  58. 58 1
      packages/console/resource/resource.node.ts
  59. 9 0
      packages/console/resource/sst-env.d.ts
  60. 1 5
      packages/desktop/package.json
  61. 6 6
      packages/extensions/zed/extension.toml
  62. 1 1
      packages/function/package.json
  63. 1 5
      packages/function/src/api.ts
  64. 9 0
      packages/function/sst-env.d.ts
  65. 1 1
      packages/opencode/package.json
  66. 2 6
      packages/opencode/script/build.ts
  67. 2 5
      packages/opencode/script/postinstall.mjs
  68. 4 12
      packages/opencode/script/publish.ts
  69. 2 13
      packages/opencode/script/schema.ts
  70. 7 17
      packages/opencode/src/acp/agent.ts
  71. 1 5
      packages/opencode/src/acp/session.ts
  72. 2 16
      packages/opencode/src/agent/agent.ts
  73. 1 4
      packages/opencode/src/bun/index.ts
  74. 2 8
      packages/opencode/src/bus/index.ts
  75. 4 13
      packages/opencode/src/cli/cmd/auth.ts
  76. 1 5
      packages/opencode/src/cli/cmd/debug/lsp.ts
  77. 2 5
      packages/opencode/src/cli/cmd/debug/ripgrep.ts
  78. 1 2
      packages/opencode/src/cli/cmd/debug/snapshot.ts
  79. 15 45
      packages/opencode/src/cli/cmd/github.ts
  80. 5 18
      packages/opencode/src/cli/cmd/run.ts
  81. 5 14
      packages/opencode/src/cli/cmd/stats.ts
  82. 4 26
      packages/opencode/src/cli/cmd/tui/app.tsx
  83. 10 5
      packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
  84. 1 3
      packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  85. 1 4
      packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx
  86. 2 12
      packages/opencode/src/cli/cmd/tui/component/logo.tsx
  87. 7 26
      packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
  88. 10 39
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  89. 2 6
      packages/opencode/src/cli/cmd/tui/context/local.tsx
  90. 2 12
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  91. 2 8
      packages/opencode/src/cli/cmd/tui/context/theme.tsx
  92. 2 13
      packages/opencode/src/cli/cmd/tui/routes/home.tsx
  93. 1 3
      packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
  94. 2 10
      packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx
  95. 3 15
      packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
  96. 26 103
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  97. 4 14
      packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
  98. 1 6
      packages/opencode/src/cli/cmd/tui/spawn.ts
  99. 1 3
      packages/opencode/src/cli/cmd/tui/thread.ts
  100. 1 3
      packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx

+ 1 - 0
STATS.md

@@ -133,3 +133,4 @@
 | 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
 | 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-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-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) |

+ 47 - 28
bun.lock

@@ -11,7 +11,7 @@
         "@tsconfig/bun": "catalog:",
         "@tsconfig/bun": "catalog:",
         "husky": "9.1.7",
         "husky": "9.1.7",
         "prettier": "3.6.2",
         "prettier": "3.6.2",
-        "sst": "3.17.22",
+        "sst": "3.17.23",
         "turbo": "2.5.6",
         "turbo": "2.5.6",
       },
       },
     },
     },
@@ -39,7 +39,7 @@
     },
     },
     "packages/console/core": {
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
       "name": "@opencode-ai/console-core",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
         "@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
     },
     },
     "packages/console/function": {
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
       "name": "@opencode-ai/console-function",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
         "@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
     },
     },
     "packages/console/mail": {
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
         "@jsx-email/cli": "1.4.3",
@@ -106,12 +106,15 @@
         "@cloudflare/workers-types": "catalog:",
         "@cloudflare/workers-types": "catalog:",
       },
       },
       "devDependencies": {
       "devDependencies": {
+        "@cloudflare/workers-types": "catalog:",
         "@tsconfig/node22": "22.0.2",
         "@tsconfig/node22": "22.0.2",
+        "@types/node": "catalog:",
+        "cloudflare": "5.2.0",
       },
       },
     },
     },
     "packages/desktop": {
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
       "name": "@opencode-ai/desktop",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -151,7 +154,7 @@
     },
     },
     "packages/function": {
     "packages/function": {
       "name": "@opencode-ai/function",
       "name": "@opencode-ai/function",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
         "@octokit/rest": "22.0.0",
@@ -167,7 +170,7 @@
     },
     },
     "packages/opencode": {
     "packages/opencode": {
       "name": "opencode",
       "name": "opencode",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "bin": {
       "bin": {
         "opencode": "./bin/opencode",
         "opencode": "./bin/opencode",
       },
       },
@@ -245,7 +248,7 @@
     },
     },
     "packages/plugin": {
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
       "name": "@opencode-ai/plugin",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
         "zod": "catalog:",
@@ -265,7 +268,7 @@
     },
     },
     "packages/sdk/js": {
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
       "name": "@opencode-ai/sdk",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "devDependencies": {
       "devDependencies": {
         "@hey-api/openapi-ts": "0.81.0",
         "@hey-api/openapi-ts": "0.81.0",
         "@tsconfig/node22": "catalog:",
         "@tsconfig/node22": "catalog:",
@@ -276,7 +279,7 @@
     },
     },
     "packages/slack": {
     "packages/slack": {
       "name": "@opencode-ai/slack",
       "name": "@opencode-ai/slack",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
         "@slack/bolt": "^3.17.1",
@@ -289,7 +292,7 @@
     },
     },
     "packages/ui": {
     "packages/ui": {
       "name": "@opencode-ai/ui",
       "name": "@opencode-ai/ui",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -319,7 +322,7 @@
     },
     },
     "packages/web": {
     "packages/web": {
       "name": "@opencode-ai/web",
       "name": "@opencode-ai/web",
-      "version": "1.0.44",
+      "version": "1.0.46",
       "dependencies": {
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
         "@astrojs/markdown-remark": "6.3.1",
@@ -1432,6 +1435,8 @@
 
 
     "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
     "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
 
 
+    "@types/node-fetch": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
+
     "@types/promise.allsettled": ["@types/[email protected]", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="],
     "@types/promise.allsettled": ["@types/[email protected]", "", {}, "sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg=="],
 
 
     "@types/prop-types": ["@types/[email protected]", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
     "@types/prop-types": ["@types/[email protected]", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
@@ -1518,6 +1523,8 @@
 
 
     "agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
     "agent-base": ["[email protected]", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
 
 
+    "agentkeepalive": ["[email protected]", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
+
     "ai": ["[email protected]", "", { "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=="],
     "ai": ["[email protected]", "", { "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": ["[email protected]", "", { "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=="],
     "ajv": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
     "clone": ["[email protected]", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
 
 
+    "cloudflare": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
     "clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 
 
     "cluster-key-slot": ["[email protected]", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
     "cluster-key-slot": ["[email protected]", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
@@ -2068,7 +2077,11 @@
 
 
     "foreground-child": ["[email protected]", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
     "foreground-child": ["[email protected]", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
 
 
-    "form-data": ["[email protected]", "", { "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": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
+
+    "formdata-node": ["[email protected]", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
 
 
     "forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
     "forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
 
 
@@ -2258,6 +2271,8 @@
 
 
     "human-signals": ["[email protected]", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
     "human-signals": ["[email protected]", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
 
 
+    "humanize-ms": ["[email protected]", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
+
     "husky": ["[email protected]", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
     "husky": ["[email protected]", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
 
 
     "i18next": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
     "i18next": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
@@ -2716,6 +2731,8 @@
 
 
     "node-addon-api": ["[email protected]", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
     "node-addon-api": ["[email protected]", "", {}, "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="],
 
 
+    "node-domexception": ["[email protected]", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
     "node-fetch": ["[email protected]", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
     "node-fetch": ["[email protected]", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
 
 
     "node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
     "node-fetch-native": ["[email protected]", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
@@ -3178,23 +3195,23 @@
 
 
     "sqlstring": ["[email protected]", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
     "sqlstring": ["[email protected]", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
 
 
-    "sst": ["[email protected]2", "", { "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.22", "sst-darwin-x64": "3.17.22", "sst-linux-arm64": "3.17.22", "sst-linux-x64": "3.17.22", "sst-linux-x86": "3.17.22", "sst-win32-arm64": "3.17.22", "sst-win32-x64": "3.17.22", "sst-win32-x86": "3.17.22" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-C+XMTbm6fx+7eT+ESAMATqG7qV7+pyVfxYQb6osdH3jd4u91QW1VU/xlEru+RU1rs1ZE58ixXdRP75UGPn+gog=="],
+    "sst": ["[email protected]3", "", { "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": ["[email protected]2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B2pKq1dWc60+7HfXQ6/9etskxxNv9axxlQKveCLQAuG2a3mmtv2/jcR0Ch3mvSTGtW+KfhzUXda2kj7nZ/phBA=="],
+    "sst-darwin-arm64": ["[email protected]3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R6kvmF+rUideOoU7KBs2SdvrIupoE+b+Dor/eq9Uo4Dojj7KvYDZI/EDm8sSCbbcx/opiWeyNqKtlnLEdCxE6g=="],
 
 
-    "sst-darwin-x64": ["[email protected]2", "", { "os": "darwin", "cpu": "x64" }, "sha512-flikYqXvhwwrS6x2FDOde+MQODHaZCIbUkVHYO3/gYo99rbAMQ8VpC/3LXnmnPEQkLOwWCSzLp4S4F9nG/PW2g=="],
+    "sst-darwin-x64": ["[email protected]3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WW4P1S35iYCifQXxD+sE3wuzcN+LHLpuKMaNoaBqEcWGZnH3IPaDJ7rpLF0arkDAo/z3jZmWWzOCkr0JuqJ8vQ=="],
 
 
-    "sst-linux-arm64": ["[email protected]2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pyD8Oej9js8XeCCebiEIde02vC5hc+bLl2/jR02K+9gYkGVJ6n5bkT8AlR8zWdS4FJKPyeJYUfjliT1T33j+g=="],
+    "sst-linux-arm64": ["[email protected]3", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjtNqgIh7RlAWgPLFCAt0mXvIB+J7WjmRvIRrAdX0mXsndOiBJ/DMOgXSLVsIWHCfPj8MIEot/hWpnJgXgIeag=="],
 
 
-    "sst-linux-x64": ["[email protected]2", "", { "os": "linux", "cpu": "x64" }, "sha512-A5p941edP9wgfgsbLUMeEPvi9JExj0OSaxgtFAC6/6BYoW4zruGAPzq206Ln6dNYP3gRdo5TJbSjio3F0ot8qg=="],
+    "sst-linux-x64": ["[email protected]3", "", { "os": "linux", "cpu": "x64" }, "sha512-qdqJiEbYfCjZlI3F/TA6eoIU7JXVkEEI/UMILNf2JWhky0KQdCW2Xyz+wb6c0msVJCWdUM/uj+1DaiP2eXvghw=="],
 
 
-    "sst-linux-x86": ["[email protected]2", "", { "os": "linux", "cpu": "none" }, "sha512-pFDIi+ZwH8GOvy5He9wsbAjRGf/sTGhGE/V480w0A6itb9BC4jQ9sblJkk3Jx/fP2g27pKN2RNz+ifOU+GrUYQ=="],
+    "sst-linux-x86": ["[email protected]3", "", { "os": "linux", "cpu": "none" }, "sha512-aGmUujIvoNlmAABEGsOgfY1rxD9koC6hN8bnTLbDI+oI/u/zjHYh50jsbL0p3TlaHpwF/lxP3xFSuT6IKp+KgA=="],
 
 
-    "sst-win32-arm64": ["[email protected]2", "", { "os": "win32", "cpu": "arm64" }, "sha512-9KaIrk+Z6hLDNi9GShf9NLrZi9jC/NNGpUAn6HvTXr8c6HUyQzg6takMH8nrISGCPn92y+IYWqdglaqbgnJTog=="],
+    "sst-win32-arm64": ["[email protected]3", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZxdkGqYDrrZGz98rijDCN+m5yuCcwD6Bc9/6hubLsvdpNlVorUqzpg801Ec97xSK0nIC9g6pNiRyxAcsQQstUg=="],
 
 
-    "sst-win32-x64": ["[email protected]2", "", { "os": "win32", "cpu": "x64" }, "sha512-cvzyet4octGHK7w05jPUSPmUdlAWyh8IzjB8Pcs873K9AUGJEtQCftOKZjXaFdIG9DTvFWCCBi9zdzClxT9jJg=="],
+    "sst-win32-x64": ["[email protected]3", "", { "os": "win32", "cpu": "x64" }, "sha512-yc9cor4MS49Ccy2tQCF1tf6M81yLeSGzGL+gjhUxpVKo2pN3bxl3w70eyU/mTXSEeyAmG9zEfbt6FNu4sy5cUA=="],
 
 
-    "sst-win32-x86": ["[email protected]2", "", { "os": "win32", "cpu": "none" }, "sha512-ol5icDJuHzG+AjbGbCIQoF8z3oiikTF9CtccdK/udqEF861DnngWzM99IY5TJvmJlN+38yOV0MY4XI5hM6SEQA=="],
+    "sst-win32-x86": ["[email protected]3", "", { "os": "win32", "cpu": "none" }, "sha512-DIp3s54IpNAfdYjSRt6McvkbEPQDMxUu6RUeRAd2C+FcTJgTloon/ghAPQBaDgu2VoVgymjcJARO/XyfKcCLOQ=="],
 
 
     "stackframe": ["[email protected]", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
     "stackframe": ["[email protected]", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
 
 
@@ -3480,6 +3497,8 @@
 
 
     "web-namespaces": ["[email protected]", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
     "web-namespaces": ["[email protected]", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
 
 
+    "web-streams-polyfill": ["[email protected]", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
+
     "web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
     "web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
 
 
     "webidl-conversions": ["[email protected]", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
     "webidl-conversions": ["[email protected]", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -3754,6 +3773,8 @@
 
 
     "@slack/web-api/eventemitter3": ["[email protected]", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
     "@slack/web-api/eventemitter3": ["[email protected]", "", {}, "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="],
 
 
+    "@slack/web-api/form-data": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
     "@slack/web-api/p-queue": ["[email protected]", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
 
 
     "@solidjs/start/shiki": ["[email protected]", "", { "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=="],
     "@solidjs/start/shiki": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
     "astro/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
 
 
-    "axios/form-data": ["[email protected]", "", { "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/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
     "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
 
 
     "babel-plugin-module-resolver/glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
     "babel-plugin-module-resolver/glob": ["[email protected]", "", { "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/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ=="],
     "@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
     "@slack/web-api/p-queue/eventemitter3": ["[email protected]", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
     "@slack/web-api/p-queue/eventemitter3": ["[email protected]", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
 
 
     "@slack/web-api/p-queue/p-timeout": ["[email protected]", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
     "@slack/web-api/p-queue/p-timeout": ["[email protected]", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
@@ -4402,8 +4423,6 @@
 
 
     "astro/shiki/@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ=="],
     "astro/shiki/@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ=="],
 
 
-    "axios/form-data/mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
-
     "babel-plugin-module-resolver/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
     "babel-plugin-module-resolver/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
 
 
     "babel-plugin-module-resolver/glob/minipass": ["[email protected]", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
     "babel-plugin-module-resolver/glob/minipass": ["[email protected]", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
@@ -4700,6 +4719,8 @@
 
 
     "@modelcontextprotocol/sdk/express/type-is/media-typer": ["[email protected]", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
     "@modelcontextprotocol/sdk/express/type-is/media-typer": ["[email protected]", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
 
 
+    "@slack/web-api/form-data/mime-types/mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["[email protected]", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["[email protected]", "", { "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": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
     "@vercel/nft/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -4708,8 +4729,6 @@
 
 
     "archiver-utils/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
     "archiver-utils/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
 
-    "axios/form-data/mime-types/mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
-
     "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
     "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
 
     "babel-plugin-module-resolver/glob/path-scurry/minipass": ["[email protected]", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
     "babel-plugin-module-resolver/glob/path-scurry/minipass": ["[email protected]", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],

+ 14 - 43
github/index.ts

@@ -171,9 +171,7 @@ try {
         const summary = await summarize(response)
         const summary = await summarize(response)
         await pushToLocalBranch(summary)
         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 })}`)
       await updateComment(`${response}${footer({ image: !hasShared })}`)
     }
     }
     // Fork PR
     // Fork PR
@@ -185,9 +183,7 @@ try {
         const summary = await summarize(response)
         const summary = await summarize(response)
         await pushToForkBranch(summary, prData)
         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 })}`)
       await updateComment(`${response}${footer({ image: !hasShared })}`)
     }
     }
   }
   }
@@ -368,9 +364,7 @@ async function getAccessToken() {
 
 
   if (!response.ok) {
   if (!response.ok) {
     const responseJson = (await response.json()) as { error?: string }
     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 }
   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. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
   // ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
   // ie. [api.json](https://github.com/user-attachments/files/21433810/api.json)
   // ie. ![Image](https://github.com/user-attachments/assets/xxxx)
   // 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)
   const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
   console.log("Images", JSON.stringify(matches, null, 2))
   console.log("Images", JSON.stringify(matches, null, 2))
 
 
@@ -443,8 +433,7 @@ async function getUserPrompt() {
 
 
     // Replace img tag with file path, ie. @image.png
     // Replace img tag with file path, ie. @image.png
     const replacement = `@${filename}`
     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
     offset += replacement.length - tag.length
 
 
     const contentType = res.headers.get("content-type")
     const contentType = res.headers.get("content-type")
@@ -512,12 +501,7 @@ async function subscribeSessionEvents() {
                     ? JSON.stringify(part.state.input)
                     ? JSON.stringify(part.state.input)
                     : "Unknown"
                     : "Unknown"
                 console.log()
                 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") {
               if (part.type === "text") {
@@ -729,8 +713,7 @@ async function assertPermissions() {
     throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
     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) {
 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`
     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()})`
   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}`)
     .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 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 [
     return [
       `- ${r.author.login} at ${r.submittedAt}:`,
       `- ${r.author.login} at ${r.submittedAt}:`,
       `  - Review body: ${r.body}`,
       `  - Review body: ${r.body}`,
@@ -987,15 +964,9 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
     `Deletions: ${pr.deletions}`,
     `Deletions: ${pr.deletions}`,
     `Total Commits: ${pr.commits.totalCount}`,
     `Total Commits: ${pr.commits.totalCount}`,
     `Changed Files: ${pr.files.nodes.length} files`,
     `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>",
     "</pull_request>",
   ].join("\n")
   ].join("\n")
 }
 }

+ 9 - 7
infra/console.ts

@@ -61,13 +61,7 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
   domain: `auth.${domain}`,
   domain: `auth.${domain}`,
   handler: "packages/console/function/src/auth.ts",
   handler: "packages/console/function/src/auth.ts",
   url: true,
   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", {
 const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
   properties: { value: stripeWebhook.secret },
   properties: { value: stripeWebhook.secret },
 })
 })
+const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
 
 
 ////////////////
 ////////////////
 // CONSOLE
 // CONSOLE
@@ -142,6 +137,13 @@ new sst.cloudflare.x.SolidStart("Console", {
     EMAILOCTOPUS_API_KEY,
     EMAILOCTOPUS_API_KEY,
     AWS_SES_ACCESS_KEY_ID,
     AWS_SES_ACCESS_KEY_ID,
     AWS_SES_SECRET_ACCESS_KEY,
     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: {
   environment: {
     //VITE_DOCS_URL: web.url.apply((url) => url!),
     //VITE_DOCS_URL: web.url.apply((url) => url!),

+ 2 - 2
package.json

@@ -52,7 +52,7 @@
     "@tsconfig/bun": "catalog:",
     "@tsconfig/bun": "catalog:",
     "husky": "9.1.7",
     "husky": "9.1.7",
     "prettier": "3.6.2",
     "prettier": "3.6.2",
-    "sst": "3.17.22",
+    "sst": "3.17.23",
     "turbo": "2.5.6"
     "turbo": "2.5.6"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -66,7 +66,7 @@
   "license": "MIT",
   "license": "MIT",
   "prettier": {
   "prettier": {
     "semi": false,
     "semi": false,
-    "printWidth": 100
+    "printWidth": 120
   },
   },
   "trustedDependencies": [
   "trustedDependencies": [
     "esbuild",
     "esbuild",

+ 1 - 1
packages/console/app/package.json

@@ -7,7 +7,7 @@
     "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
     "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
     "build": "./script/generate-sitemap.ts && 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",
     "start": "vinxi start",
-    "version": "1.0.44"
+    "version": "1.0.46"
   },
   },
   "dependencies": {
   "dependencies": {
     "@ibm/plex": "6.4.1",
     "@ibm/plex": "6.4.1",

+ 1 - 4
packages/console/app/src/app.tsx

@@ -12,10 +12,7 @@ export default function App() {
       root={(props) => (
       root={(props) => (
         <MetaProvider>
         <MetaProvider>
           <Title>opencode</Title>
           <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>
           <Suspense>{props.children}</Suspense>
         </MetaProvider>
         </MetaProvider>
       )}
       )}

+ 1 - 4
packages/console/app/src/component/faq.tsx

@@ -13,10 +13,7 @@ export function Faq(props: ParentProps & { question: string }) {
           fill="currentColor"
           fill="currentColor"
           xmlns="http://www.w3.org/2000/svg"
           xmlns="http://www.w3.org/2000/svg"
         >
         >
-          <path
-            d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z"
-            fill="currentColor"
-          />
+          <path d="M12.5 11.5H19V12.5H12.5V19H11.5V12.5H5V11.5H11.5V5H12.5V11.5Z" fill="currentColor" />
         </svg>
         </svg>
         <svg
         <svg
           data-slot="faq-icon-minus"
           data-slot="faq-icon-minus"

+ 20 - 113
packages/console/app/src/component/icon.tsx

@@ -9,23 +9,10 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
       <path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
       <path d="M13.7124 9.14333V4.5719H18.2838V9.14333H13.7124Z" fill="currentColor" />
       <path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
       <path d="M13.7124 13.7136V9.14221H18.2838V13.7136H13.7124Z" fill="currentColor" />
       <path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M0 18.2857V13.7142H4.57143V18.2857H0Z" fill="currentColor" fill-opacity="0.2" />
-      <rect
-        width="4.57143"
-        height="4.57143"
-        transform="translate(4.57178 13.7141)"
-        fill="currentColor"
-      />
-      <path
-        d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <rect width="4.57143" height="4.57143" transform="translate(4.57178 13.7141)" fill="currentColor" />
+      <path d="M4.57178 18.2855V13.7141H9.14321V18.2855H4.57178Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
       <path d="M9.1438 18.2855V13.7141H13.7152V18.2855H9.1438Z" fill="currentColor" />
-      <path
-        d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M13.7156 18.2855V13.7141H18.287V18.2855H13.7156Z" fill="currentColor" fill-opacity="0.2" />
       <rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
       <rect width="4.57143" height="4.57143" transform="translate(0 18.2859)" fill="currentColor" />
       <path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M0 22.8572V18.2858H4.57143V22.8572H0Z" fill="currentColor" fill-opacity="0.2" />
       <rect
       <rect
@@ -36,16 +23,8 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
         fill-opacity="0.2"
         fill-opacity="0.2"
       />
       />
       <path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
       <path d="M4.57178 22.8573V18.2859H9.14321V22.8573H4.57178Z" fill="currentColor" />
-      <path
-        d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M9.1438 22.8573V18.2859H13.7152V22.8573H9.1438Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M13.7156 22.8573V18.2859H18.287V22.8573H13.7156Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
       <path d="M0 27.4292V22.8578H4.57143V27.4292H0Z" fill="currentColor" />
       <path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
       <path d="M4.57178 27.4292V22.8578H9.14321V27.4292H4.57178Z" fill="currentColor" />
       <path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
       <path d="M9.1438 27.4276V22.8562H13.7152V27.4276H9.1438Z" fill="currentColor" />
@@ -61,21 +40,9 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
       <path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
       <path d="M32.001 18.2855V13.7141H36.5724V18.2855H32.001Z" fill="currentColor" />
       <path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
       <path d="M36.5698 18.2855V13.7141H41.1413V18.2855H36.5698Z" fill="currentColor" />
       <path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
       <path d="M22.8572 22.8573V18.2859H27.4286V22.8573H22.8572Z" fill="currentColor" />
-      <path
-        d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M27.4292 22.8573V18.2859H32.0006V22.8573H27.4292Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M32.001 22.8573V18.2859H36.5724V22.8573H32.001Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M36.5698 22.8573V18.2859H41.1413V22.8573H36.5698Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
       <path d="M22.8572 27.4292V22.8578H27.4286V27.4292H22.8572Z" fill="currentColor" />
       <path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
       <path d="M27.4292 27.4276V22.8562H32.0006V27.4276H27.4292Z" fill="currentColor" />
       <path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
       <path d="M32.001 27.4276V22.8562H36.5724V27.4276H32.001Z" fill="currentColor" />
@@ -86,40 +53,16 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
       <path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
       <path d="M45.7144 13.7136V9.14221H50.2858V13.7136H45.7144Z" fill="currentColor" />
       <path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
       <path d="M59.4299 13.7152V9.1438H64.0014V13.7152H59.4299Z" fill="currentColor" />
       <path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
       <path d="M45.7144 18.2855V13.7141H50.2858V18.2855H45.7144Z" fill="currentColor" />
-      <path
-        d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M50.2861 18.2857V13.7142H54.8576V18.2857H50.2861Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M54.8579 18.2855V13.7141H59.4293V18.2855H54.8579Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
       <path d="M59.4299 18.2855V13.7141H64.0014V18.2855H59.4299Z" fill="currentColor" />
       <path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
       <path d="M45.7144 22.8573V18.2859H50.2858V22.8573H45.7144Z" fill="currentColor" />
-      <path
-        d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M50.2861 22.8572V18.2858H54.8576V22.8572H50.2861Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M54.8579 22.8573V18.2859H59.4293V22.8573H54.8579Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
       <path d="M59.4299 22.8573V18.2859H64.0014V22.8573H59.4299Z" fill="currentColor" />
       <path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
       <path d="M45.7144 27.4292V22.8578H50.2858V27.4292H45.7144Z" fill="currentColor" />
-      <path
-        d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
-      <path
-        d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z"
-        fill="currentColor"
-        fill-opacity="0.2"
-      />
+      <path d="M50.2861 27.4286V22.8572H54.8576V27.4286H50.2861Z" fill="currentColor" fill-opacity="0.2" />
+      <path d="M54.8579 27.4285V22.8571H59.4293V27.4285H54.8579Z" fill="currentColor" fill-opacity="0.2" />
       <path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
       <path d="M59.4299 27.4292V22.8578H64.0014V27.4292H59.4299Z" fill="currentColor" />
     </svg>
     </svg>
   )
   )
@@ -127,14 +70,7 @@ export function IconLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 
 
 export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
-    <svg
-      {...props}
-      width="24"
-      height="24"
-      viewBox="0 0 24 24"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-    >
+    <svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
       <path
       <path
         d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
         d="M8.75 8.75V2.75H21.25V15.25H15.25M15.25 8.75H2.75V21.25H15.25V8.75Z"
         stroke="currentColor"
         stroke="currentColor"
@@ -147,20 +83,8 @@ export function IconCopy(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 
 
 export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconCheck(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
-    <svg
-      {...props}
-      width="24"
-      height="24"
-      viewBox="0 0 24 24"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-    >
-      <path
-        d="M2.75 15.0938L9 20.25L21.25 3.75"
-        stroke="#03B000"
-        stroke-width="2"
-        stroke-linecap="square"
-      />
+    <svg {...props} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <path d="M2.75 15.0938L9 20.25L21.25 3.75" stroke="#03B000" stroke-width="2" stroke-linecap="square" />
     </svg>
     </svg>
   )
   )
 }
 }
@@ -189,14 +113,7 @@ export function IconStripe(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 
 
 export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
-    <svg
-      {...props}
-      width="8"
-      height="6"
-      viewBox="0 0 8 6"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-    >
+    <svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
       <path
       <path
         fill="currentColor"
         fill="currentColor"
         d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
         d="M4.00024 5.04041L7.37401 1.66663L6.66691 0.959525L4.00024 3.62619L1.33357 0.959525L0.626465 1.66663L4.00024 5.04041Z"
@@ -207,14 +124,7 @@ export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 
 
 export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
-    <svg
-      {...props}
-      width="24"
-      height="30"
-      viewBox="0 0 24 30"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-    >
+    <svg {...props} width="24" height="30" viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
       <path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
       <path d="M18 6H6V24H18V6ZM24 30H0V0H24V30Z" fill="currentColor" />
     </svg>
     </svg>
   )
   )
@@ -234,10 +144,7 @@ export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
 export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
   return (
   return (
     <svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
     <svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <path
-        fill="currentColor"
-        d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z"
-      />
+      <path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
       <path
       <path
         fill="currentColor"
         fill="currentColor"
         d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
         d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"

+ 1 - 4
packages/console/app/src/lib/github.ts

@@ -7,10 +7,7 @@ export const github = query(async () => {
     "User-Agent":
     "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",
       "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 {
   try {
     const [meta, releases, contributors] = await Promise.all([
     const [meta, releases, contributors] = await Promise.all([
       fetch(apiBaseUrl, { headers }).then((res) => res.json()),
       fetch(apiBaseUrl, { headers }).then((res) => res.json()),

+ 1 - 4
packages/console/app/src/routes/auth/authorize.ts

@@ -2,9 +2,6 @@ import type { APIEvent } from "@solidjs/start/server"
 import { AuthClient } from "~/context/auth"
 import { AuthClient } from "~/context/auth"
 
 
 export async function GET(input: APIEvent) {
 export async function GET(input: APIEvent) {
-  const result = await AuthClient.authorize(
-    new URL("./callback", input.request.url).toString(),
-    "code",
-  )
+  const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
   return Response.redirect(result.url, 302)
   return Response.redirect(result.url, 302)
 }
 }

+ 21 - 123
packages/console/app/src/routes/brand/index.tsx

@@ -68,13 +68,7 @@ export default function Brand() {
               onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")}
               onClick={() => downloadFile(brandAssets, "opencode-brand-assets.zip")}
             >
             >
               Download all assets
               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
                 <path
                   d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                   d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                   stroke="currentColor"
                   stroke="currentColor"
@@ -90,13 +84,7 @@ export default function Brand() {
                 <div data-component="actions">
                 <div data-component="actions">
                   <button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
                   <button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -107,13 +95,7 @@ export default function Brand() {
                   </button>
                   </button>
                   <button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}>
                   <button onClick={() => downloadFile(logoLightSvg, "opencode-logo-light.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -129,13 +111,7 @@ export default function Brand() {
                 <div data-component="actions">
                 <div data-component="actions">
                   <button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
                   <button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -146,13 +122,7 @@ export default function Brand() {
                   </button>
                   </button>
                   <button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}>
                   <button onClick={() => downloadFile(logoDarkSvg, "opencode-logo-dark.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -166,17 +136,9 @@ export default function Brand() {
               <div>
               <div>
                 <img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
                 <img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
                 <div data-component="actions">
                 <div data-component="actions">
-                  <button
-                    onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}
-                  >
+                  <button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -185,17 +147,9 @@ export default function Brand() {
                       />
                       />
                     </svg>
                     </svg>
                   </button>
                   </button>
-                  <button
-                    onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}
-                  >
+                  <button onClick={() => downloadFile(wordmarkLightSvg, "opencode-wordmark-light.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -209,17 +163,9 @@ export default function Brand() {
               <div>
               <div>
                 <img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
                 <img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
                 <div data-component="actions">
                 <div data-component="actions">
-                  <button
-                    onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}
-                  >
+                  <button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -228,17 +174,9 @@ export default function Brand() {
                       />
                       />
                     </svg>
                     </svg>
                   </button>
                   </button>
-                  <button
-                    onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}
-                  >
+                  <button onClick={() => downloadFile(wordmarkDarkSvg, "opencode-wordmark-dark.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -252,19 +190,9 @@ export default function Brand() {
               <div>
               <div>
                 <img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
                 <img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
                 <div data-component="actions">
                 <div data-component="actions">
-                  <button
-                    onClick={() =>
-                      downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")
-                    }
-                  >
+                  <button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -273,19 +201,9 @@ export default function Brand() {
                       />
                       />
                     </svg>
                     </svg>
                   </button>
                   </button>
-                  <button
-                    onClick={() =>
-                      downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")
-                    }
-                  >
+                  <button onClick={() => downloadFile(wordmarkSimpleLightSvg, "opencode-wordmark-simple-light.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -299,19 +217,9 @@ export default function Brand() {
               <div>
               <div>
                 <img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
                 <img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
                 <div data-component="actions">
                 <div data-component="actions">
-                  <button
-                    onClick={() =>
-                      downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")
-                    }
-                  >
+                  <button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"
@@ -320,19 +228,9 @@ export default function Brand() {
                       />
                       />
                     </svg>
                     </svg>
                   </button>
                   </button>
-                  <button
-                    onClick={() =>
-                      downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")
-                    }
-                  >
+                  <button onClick={() => downloadFile(wordmarkSimpleDarkSvg, "opencode-wordmark-simple-dark.svg")}>
                     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
                       <path
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
                         stroke="currentColor"
                         stroke="currentColor"

+ 19 - 38
packages/console/app/src/routes/enterprise/index.tsx

@@ -66,39 +66,26 @@ export default function Enterprise() {
               <div data-component="enterprise-column-1">
               <div data-component="enterprise-column-1">
                 <h1>Your code is yours</h1>
                 <h1>Your code is yours</h1>
                 <p>
                 <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>
                 <p>Let us know and how we can help.</p>
                 <p>Let us know and how we can help.</p>
 
 
                 <Show when={false}>
                 <Show when={false}>
                   <div data-component="testimonial">
                   <div data-component="testimonial">
                     <div data-component="quotation">
                     <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
                         <path
                           d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
                           d="M19.4118 0L16.5882 9.20833H20V17H12.2353V10.0938L16 0H19.4118ZM7.17647 0L4.35294 9.20833H7.76471V17H0V10.0938L3.76471 0H7.17647Z"
                           fill="currentColor"
                           fill="currentColor"
                         />
                         />
                       </svg>
                       </svg>
                     </div>
                     </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">
                     <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
                         <path
                           fill-rule="evenodd"
                           fill-rule="evenodd"
                           clip-rule="evenodd"
                           clip-rule="evenodd"
@@ -215,11 +202,7 @@ export default function Enterprise() {
                     </button>
                     </button>
                   </form>
                   </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>
               </div>
             </div>
             </div>
@@ -232,31 +215,29 @@ export default function Enterprise() {
             <ul>
             <ul>
               <li>
               <li>
                 <Faq question="What is OpenCode Enterprise?">
                 <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>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="How do I get started with OpenCode Enterprise?">
                 <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>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="How does enterprise pricing work?">
                 <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>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="Is my data secure with OpenCode Enterprise?">
                 <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>
                 </Faq>
               </li>
               </li>
             </ul>
             </ul>

Разница между файлами не показана из-за своего большого размера
+ 109 - 653
packages/console/app/src/routes/index.tsx


+ 5 - 18
packages/console/app/src/routes/stripe/webhook.ts

@@ -41,8 +41,7 @@ export async function POST(input: APIEvent) {
     }
     }
     if (body.type === "checkout.session.completed") {
     if (body.type === "checkout.session.completed") {
       const workspaceID = body.data.object.metadata?.workspaceID
       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 customerID = body.data.object.customer as string
       const paymentID = body.data.object.payment_intent as string
       const paymentID = body.data.object.payment_intent as string
       const invoiceID = body.data.object.invoice 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 () => {
       await Actor.provide("system", { workspaceID }, async () => {
         const customer = await Billing.get()
         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
         // set customer metadata
         if (!customer?.customerID) {
         if (!customer?.customerID) {
@@ -72,8 +70,7 @@ export async function POST(input: APIEvent) {
           expand: ["payment_method"],
           expand: ["payment_method"],
         })
         })
         const paymentMethod = paymentIntent.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 Database.transaction(async (tx) => {
           await tx
           await tx
@@ -128,12 +125,7 @@ export async function POST(input: APIEvent) {
             amount: PaymentTable.amount,
             amount: PaymentTable.amount,
           })
           })
           .from(PaymentTable)
           .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),
           .then((rows) => rows[0]?.amount),
       )
       )
       if (!amount) throw new Error("Payment not found")
       if (!amount) throw new Error("Payment not found")
@@ -144,12 +136,7 @@ export async function POST(input: APIEvent) {
           .set({
           .set({
             timeRefunded: new Date(body.created * 1000),
             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
         await tx
           .update(BillingTable)
           .update(BillingTable)

+ 4 - 6
packages/console/app/src/routes/temp.tsx

@@ -79,19 +79,17 @@ export default function Home() {
               <strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
               <strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
             </li>
             </li>
             <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>
             <li>
             <li>
               <strong>Multi-session</strong> Start multiple agents in parallel on the same project
               <strong>Multi-session</strong> Start multiple agents in parallel on the same project
             </li>
             </li>
             <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>
             <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>
             <li>
             <li>
               <strong>Use any model</strong> Supports 75+ LLM providers through{" "}
               <strong>Use any model</strong> Supports 75+ LLM providers through{" "}

+ 2 - 9
packages/console/app/src/routes/workspace-picker.tsx

@@ -85,10 +85,7 @@ export function WorkspacePicker() {
       <Dropdown trigger={currentWorkspace()} align="left">
       <Dropdown trigger={currentWorkspace()} align="left">
         <For each={workspaces()}>
         <For each={workspaces()}>
           {(workspace) => (
           {(workspace) => (
-            <DropdownItem
-              selected={workspace.id === params.id}
-              onClick={() => handleSelectWorkspace(workspace.id)}
-            >
+            <DropdownItem selected={workspace.id === params.id} onClick={() => handleSelectWorkspace(workspace.id)}>
               {workspace.name || workspace.slug}
               {workspace.name || workspace.slug}
             </DropdownItem>
             </DropdownItem>
           )}
           )}
@@ -98,11 +95,7 @@ export function WorkspacePicker() {
         </button>
         </button>
       </Dropdown>
       </Dropdown>
 
 
-      <Modal
-        open={store.showForm}
-        onClose={() => setStore("showForm", false)}
-        title="Create New Workspace"
-      >
+      <Modal open={store.showForm} onClose={() => setStore("showForm", false)} title="Create New Workspace">
         <form data-slot="create-form" action={createWorkspace} method="post">
         <form data-slot="create-form" action={createWorkspace} method="post">
           <div data-slot="create-input-group">
           <div data-slot="create-input-group">
             <input
             <input

+ 15 - 27
packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx

@@ -34,10 +34,17 @@ export function BillingSection() {
   const sessionSubmission = useSubmission(createSessionUrl)
   const sessionSubmission = useSubmission(createSessionUrl)
   const [store, setStore] = createStore({
   const [store, setStore] = createStore({
     showAddBalanceForm: false,
     showAddBalanceForm: false,
-    addBalanceAmount: "",
+    addBalanceAmount: billingInfo()?.reloadAmount.toString() ?? "",
     checkoutRedirecting: false,
     checkoutRedirecting: false,
     sessionRedirecting: false,
     sessionRedirecting: false,
   })
   })
+
+  createEffect(() => {
+    const info = billingInfo()
+    if (info) {
+      setStore("addBalanceAmount", info.reloadAmount.toString())
+    }
+  })
   const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
   const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
 
 
   async function onClickCheckout() {
   async function onClickCheckout() {
@@ -67,7 +74,6 @@ export function BillingSection() {
     }
     }
     setStore({
     setStore({
       showAddBalanceForm: true,
       showAddBalanceForm: true,
-      addBalanceAmount: billingInfo()!.reloadAmount.toString(),
     })
     })
   }
   }
 
 
@@ -133,8 +139,7 @@ export function BillingSection() {
       <div data-slot="section-title">
       <div data-slot="section-title">
         <h2>Billing</h2>
         <h2>Billing</h2>
         <p>
         <p>
-          Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any
-          questions.
+          Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any questions.
         </p>
         </p>
       </div>
       </div>
       <div data-slot="section-content">
       <div data-slot="section-content">
@@ -164,32 +169,20 @@ export function BillingSection() {
                         placeholder="Enter amount"
                         placeholder="Enter amount"
                       />
                       />
                       <div data-slot="form-actions">
                       <div data-slot="form-actions">
-                        <button
-                          data-color="ghost"
-                          type="button"
-                          onClick={() => hideAddBalanceForm()}
-                        >
+                        <button data-color="ghost" type="button" onClick={() => hideAddBalanceForm()}>
                           Cancel
                           Cancel
                         </button>
                         </button>
                         <button
                         <button
                           data-color="primary"
                           data-color="primary"
                           type="button"
                           type="button"
-                          disabled={
-                            !store.addBalanceAmount ||
-                            checkoutSubmission.pending ||
-                            store.checkoutRedirecting
-                          }
+                          disabled={!store.addBalanceAmount || checkoutSubmission.pending || store.checkoutRedirecting}
                           onClick={onClickCheckout}
                           onClick={onClickCheckout}
                         >
                         >
-                          {checkoutSubmission.pending || store.checkoutRedirecting
-                            ? "Loading..."
-                            : "Add"}
+                          {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Add"}
                         </button>
                         </button>
                       </div>
                       </div>
                     </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>}
                       {(err: any) => <div data-slot="form-error">{err()}</div>}
                     </Show>
                     </Show>
                   </div>
                   </div>
@@ -210,10 +203,7 @@ export function BillingSection() {
                 <div data-slot="card-details">
                 <div data-slot="card-details">
                   <Switch>
                   <Switch>
                     <Match when={billingInfo()?.paymentMethodType === "card"}>
                     <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="secret">••••</span>
                         <span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
                         <span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
                       </Show>
                       </Show>
@@ -241,9 +231,7 @@ export function BillingSection() {
             disabled={checkoutSubmission.pending || store.checkoutRedirecting}
             disabled={checkoutSubmission.pending || store.checkoutRedirecting}
             onClick={onClickCheckout}
             onClick={onClickCheckout}
           >
           >
-            {checkoutSubmission.pending || store.checkoutRedirecting
-              ? "Loading..."
-              : "Enable Billing"}
+            {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable Billing"}
           </button>
           </button>
         </Show>
         </Show>
       </div>
       </div>

+ 2 - 6
packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx

@@ -104,13 +104,9 @@ export function MonthlyLimitSection() {
             </button>
             </button>
           </Show>
           </Show>
         </div>
         </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">
           <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
               const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
               if (!dateLastUsed) return "0"
               if (!dateLastUsed) return "0"

+ 1 - 4
packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx

@@ -89,10 +89,7 @@ export function PaymentSection() {
                       <td data-slot="payment-receipt">
                       <td data-slot="payment-receipt">
                         <button
                         <button
                           onClick={async () => {
                           onClick={async () => {
-                            const receiptUrl = await downloadReceiptAction(
-                              params.id,
-                              payment.paymentID!,
-                            )
+                            const receiptUrl = await downloadReceiptAction(params.id, payment.paymentID!)
                             if (receiptUrl) {
                             if (receiptUrl) {
                               window.open(receiptUrl, "_blank")
                               window.open(receiptUrl, "_blank")
                             }
                             }

+ 5 - 9
packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx

@@ -69,11 +69,7 @@ export function ReloadSection() {
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
-    if (
-      !setReloadSubmission.pending &&
-      setReloadSubmission.result &&
-      !(setReloadSubmission.result as any).error
-    ) {
+    if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
       setStore("show", false)
       setStore("show", false)
     }
     }
   })
   })
@@ -108,8 +104,8 @@ export function ReloadSection() {
             }
             }
           >
           >
             <p>
             <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>
             </p>
           </Show>
           </Show>
           <button data-color="primary" type="button" onClick={() => show()}>
           <button data-color="primary" type="button" onClick={() => show()}>
@@ -194,8 +190,8 @@ export function ReloadSection() {
                 minute: "2-digit",
                 minute: "2-digit",
                 second: "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>
             </p>
             <form action={reload} method="post" data-slot="create-form">
             <form action={reload} method="post" data-slot="create-form">
               <input type="hidden" name="workspaceID" value={params.id} />
               <input type="hidden" name="workspaceID" value={params.id} />

+ 1 - 3
packages/console/app/src/routes/workspace/[id]/index.tsx

@@ -51,9 +51,7 @@ export default function () {
                     disabled={checkoutSubmission.pending || store.checkoutRedirecting}
                     disabled={checkoutSubmission.pending || store.checkoutRedirecting}
                     onClick={onClickCheckout}
                     onClick={onClickCheckout}
                   >
                   >
-                    {checkoutSubmission.pending || store.checkoutRedirecting
-                      ? "Loading..."
-                      : "Enable billing"}
+                    {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable billing"}
                   </button>
                   </button>
                 }
                 }
               >
               >

+ 2 - 8
packages/console/app/src/routes/workspace/[id]/keys/key-section.tsx

@@ -146,20 +146,14 @@ export function KeySection() {
                             title="Copy API key"
                             title="Copy API key"
                           >
                           >
                             <span>{key.keyDisplay}</span>
                             <span>{key.keyDisplay}</span>
-                            <Show
-                              when={copied()}
-                              fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
-                            >
+                            <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
                               <IconCheck style={{ width: "14px", height: "14px" }} />
                               <IconCheck style={{ width: "14px", height: "14px" }} />
                             </Show>
                             </Show>
                           </button>
                           </button>
                         </Show>
                         </Show>
                       </td>
                       </td>
                       <td data-slot="key-user-email">{key.email}</td>
                       <td data-slot="key-user-email">{key.email}</td>
-                      <td
-                        data-slot="key-last-used"
-                        title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}
-                      >
+                      <td data-slot="key-last-used" title={key.timeUsed ? formatDateUTC(key.timeUsed) : undefined}>
                         {key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
                         {key.timeUsed ? formatDateForTable(key.timeUsed) : "-"}
                       </td>
                       </td>
                       <td data-slot="key-actions">
                       <td data-slot="key-actions">

+ 1 - 6
packages/console/app/src/routes/workspace/[id]/members/member-section.tsx

@@ -85,12 +85,7 @@ const updateMember = action(async (form: FormData) => {
   )
   )
 }, "member.update")
 }, "member.update")
 
 
-function MemberRow(props: {
-  member: any
-  workspaceID: string
-  actorID: string
-  actorRole: string
-}) {
+function MemberRow(props: { member: any; workspaceID: string; actorID: string; actorRole: string }) {
   const submission = useSubmission(updateMember)
   const submission = useSubmission(updateMember)
   const isCurrentUser = () => props.actorID === props.member.id
   const isCurrentUser = () => props.actorID === props.member.id
   const isAdmin = () => props.actorRole === "admin"
   const isAdmin = () => props.actorRole === "admin"

+ 2 - 11
packages/console/app/src/routes/workspace/[id]/model-section.tsx

@@ -5,15 +5,7 @@ import { withActor } from "~/context/auth.withActor"
 import { ZenData } from "@opencode-ai/console-core/model.js"
 import { ZenData } from "@opencode-ai/console-core/model.js"
 import styles from "./model-section.module.css"
 import styles from "./model-section.module.css"
 import { querySessionInfo } from "../common"
 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) => {
 const getModelLab = (modelId: string) => {
   if (modelId.startsWith("claude")) return "Anthropic"
   if (modelId.startsWith("claude")) return "Anthropic"
@@ -76,8 +68,7 @@ export function ModelSection() {
       <div data-slot="section-title">
       <div data-slot="section-title">
         <h2>Models</h2>
         <h2>Models</h2>
         <p>
         <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>
         </p>
       </div>
       </div>
       <div data-slot="models-list">
       <div data-slot="models-list">

+ 3 - 12
packages/console/app/src/routes/workspace/[id]/new-user-section.tsx

@@ -43,24 +43,15 @@ export function NewUserSection() {
         <div data-component="feature-grid">
         <div data-component="feature-grid">
           <div data-slot="feature">
           <div data-slot="feature">
             <h3>Tested & Verified Models</h3>
             <h3>Tested & Verified Models</h3>
-            <p>
-              We've benchmarked and tested models specifically for coding agents to ensure the best
-              performance.
-            </p>
+            <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
           </div>
           </div>
           <div data-slot="feature">
           <div data-slot="feature">
             <h3>Highest Quality</h3>
             <h3>Highest Quality</h3>
-            <p>
-              Access models configured for optimal performance - no downgrades or routing to cheaper
-              providers.
-            </p>
+            <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
           </div>
           </div>
           <div data-slot="feature">
           <div data-slot="feature">
             <h3>No Lock-in</h3>
             <h3>No Lock-in</h3>
-            <p>
-              Use Zen with any coding agent, and continue using other providers with opencode
-              whenever you want.
-            </p>
+            <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
           </div>
           </div>
         </div>
         </div>
 
 

+ 3 - 13
packages/console/app/src/routes/workspace/[id]/provider-section.tsx

@@ -55,10 +55,7 @@ const listProviders = query(async (workspaceID: string) => {
 function ProviderRow(props: { provider: Provider }) {
 function ProviderRow(props: { provider: Provider }) {
   const params = useParams()
   const params = useParams()
   const providers = createAsync(() => listProviders(params.id))
   const providers = createAsync(() => listProviders(params.id))
-  const saveSubmission = useSubmission(
-    saveProvider,
-    ([fd]) => fd.get("provider")?.toString() === props.provider.key,
-  )
+  const saveSubmission = useSubmission(saveProvider, ([fd]) => fd.get("provider")?.toString() === props.provider.key)
   const removeSubmission = useSubmission(
   const removeSubmission = useSubmission(
     removeProvider,
     removeProvider,
     ([fd]) => fd.get("provider")?.toString() === props.provider.key,
     ([fd]) => fd.get("provider")?.toString() === props.provider.key,
@@ -94,16 +91,9 @@ function ProviderRow(props: { provider: Provider }) {
       <td data-slot="provider-key">
       <td data-slot="provider-key">
         <Show
         <Show
           when={store.editing}
           when={store.editing}
-          fallback={
-            <span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>
-          }
+          fallback={<span>{providerData() ? maskCredentials(providerData()!.credentials) : "-"}</span>}
         >
         >
-          <form
-            id={`provider-form-${props.provider.key}`}
-            action={saveProvider}
-            method="post"
-            data-slot="edit-form"
-          >
+          <form id={`provider-form-${props.provider.key}`} action={saveProvider} method="post" data-slot="edit-form">
             <div data-slot="input-wrapper">
             <div data-slot="input-wrapper">
               <input
               <input
                 ref={(r) => (input = r)}
                 ref={(r) => (input = r)}

+ 1 - 4
packages/console/app/src/routes/workspace/common.tsx

@@ -67,10 +67,7 @@ export const querySessionInfo = query(async (workspaceID: string) => {
   return withActor(() => {
   return withActor(() => {
     return {
     return {
       isAdmin: Actor.userRole() === "admin",
       isAdmin: Actor.userRole() === "admin",
-      isBeta:
-        Resource.App.stage === "production"
-          ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y"
-          : true,
+      isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
     }
     }
   }, workspaceID)
   }, workspaceID)
 }, "session.get")
 }, "session.get")

+ 36 - 96
packages/console/app/src/routes/zen/index.tsx

@@ -29,10 +29,7 @@ export default function Home() {
   createAsync(() => checkLoggedIn())
   createAsync(() => checkLoggedIn())
   return (
   return (
     <main data-page="zen">
     <main data-page="zen">
-      <HttpHeader
-        name="Cache-Control"
-        value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400"
-      />
+      <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>
       <Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
       <Link rel="canonical" href={`${config.baseUrl}/zen`} />
       <Link rel="canonical" href={`${config.baseUrl}/zen`} />
       <Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
       <Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
@@ -49,19 +46,13 @@ export default function Home() {
               <img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" />
               <img data-slot="zen logo dark" src={zenLogoDark} alt="zen logo dark" />
               <h1>Reliable optimized models for coding agents</h1>
               <h1>Reliable optimized models for coding agents</h1>
               <p>
               <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 that work.
+                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
+                that work.
               </p>
               </p>
               <div data-slot="model-logos">
               <div data-slot="model-logos">
                 <div>
                 <div>
-                  <svg
-                    width="24"
-                    height="24"
-                    viewBox="0 0 24 24"
-                    fill="none"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <mask
                     <mask
                       id="mask0_79_128586"
                       id="mask0_79_128586"
                       style="mask-type:luminance"
                       style="mask-type:luminance"
@@ -82,17 +73,8 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
                 <div>
                 <div>
-                  <svg
-                    width="24"
-                    height="24"
-                    viewBox="0 0 24 24"
-                    fill="none"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
-                    <path
-                      d="M13.7891 3.93164L20.2223 20.0677H23.7502L17.317 3.93164H13.7891Z"
-                      fill="currentColor"
-                    />
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <path d="M13.7891 3.93164L20.2223 20.0677H23.7502L17.317 3.93164H13.7891Z" fill="currentColor" />
                     <path
                     <path
                       d="M6.32538 13.6824L8.52662 8.01177L10.7279 13.6824H6.32538ZM6.68225 3.93164L0.25 20.0677H3.84652L5.16202 16.6791H11.8914L13.2067 20.0677H16.8033L10.371 3.93164H6.68225Z"
                       d="M6.32538 13.6824L8.52662 8.01177L10.7279 13.6824H6.32538ZM6.68225 3.93164L0.25 20.0677H3.84652L5.16202 16.6791H11.8914L13.2067 20.0677H16.8033L10.371 3.93164H6.68225Z"
                       fill="currentColor"
                       fill="currentColor"
@@ -100,13 +82,7 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
                 <div>
                 <div>
-                  <svg
-                    width="24"
-                    height="24"
-                    viewBox="0 0 24 24"
-                    fill="none"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <path
                     <path
                       d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
                       d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
                       fill="currentColor"
                       fill="currentColor"
@@ -118,13 +94,7 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
                 <div>
                 <div>
-                  <svg
-                    width="24"
-                    height="24"
-                    viewBox="0 0 24 24"
-                    fill="none"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <path
                     <path
                       fill-rule="evenodd"
                       fill-rule="evenodd"
                       clip-rule="evenodd"
                       clip-rule="evenodd"
@@ -134,13 +104,7 @@ export default function Home() {
                   </svg>
                   </svg>
                 </div>
                 </div>
                 <div>
                 <div>
-                  <svg
-                    width="24"
-                    height="24"
-                    viewBox="0 0 24 24"
-                    fill="none"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
+                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <path
                     <path
                       d="M12.6241 11.346L20.3848 3.44816C20.5309 3.29931 20.4487 3 20.2601 3H16.0842C16.0388 3 15.9949 3.01897 15.9594 3.05541L7.59764 11.5629C7.46721 11.6944 7.27446 11.5771 7.27446 11.3666V3.25183C7.27446 3.11242 7.18515 3 7.07594 3H4.19843C4.08932 3 4 3.11242 4 3.25183V20.7482C4 20.8876 4.08932 21 4.19843 21H7.07594C7.18515 21 7.27446 20.8876 7.27446 20.7482V17.1834C7.27446 17.1073 7.30136 17.0344 7.34815 16.987L9.94075 14.3486C10.0031 14.2853 10.0895 14.2757 10.159 14.3232L17.0934 19.5573C18.2289 20.3412 19.4975 20.8226 20.786 20.9652C20.9008 20.9778 21 20.8606 21 20.7133V17.3559C21 17.2276 20.9249 17.1232 20.8243 17.1073C20.0659 16.9853 19.326 16.6845 18.6569 16.222L12.6538 11.764C12.5291 11.6785 12.5135 11.4584 12.6241 11.346Z"
                       d="M12.6241 11.346L20.3848 3.44816C20.5309 3.29931 20.4487 3 20.2601 3H16.0842C16.0388 3 15.9949 3.01897 15.9594 3.05541L7.59764 11.5629C7.46721 11.6944 7.27446 11.5771 7.27446 11.3666V3.25183C7.27446 3.11242 7.18515 3 7.07594 3H4.19843C4.08932 3 4 3.11242 4 3.25183V20.7482C4 20.8876 4.08932 21 4.19843 21H7.07594C7.18515 21 7.27446 20.8876 7.27446 20.7482V17.1834C7.27446 17.1073 7.30136 17.0344 7.34815 16.987L9.94075 14.3486C10.0031 14.2853 10.0895 14.2757 10.159 14.3232L17.0934 19.5573C18.2289 20.3412 19.4975 20.8226 20.786 20.9652C20.9008 20.9778 21 20.8606 21 20.7133V17.3559C21 17.2276 20.9249 17.1232 20.8243 17.1073C20.0659 16.9853 19.326 16.6845 18.6569 16.222L12.6538 11.764C12.5291 11.6785 12.5135 11.4584 12.6241 11.346Z"
                       fill="currentColor"
                       fill="currentColor"
@@ -150,13 +114,7 @@ export default function Home() {
               </div>
               </div>
               <a href="/auth">
               <a href="/auth">
                 <span>Get started with Zen </span>
                 <span>Get started with Zen </span>
-                <svg
-                  width="24"
-                  height="24"
-                  viewBox="0 0 24 24"
-                  fill="none"
-                  xmlns="http://www.w3.org/2000/svg"
-                >
+                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                   <path
                   <path
                     d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
                     d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
                     stroke="currentColor"
                     stroke="currentColor"
@@ -168,23 +126,14 @@ export default function Home() {
             </div>
             </div>
             <div data-slot="pricing-copy">
             <div data-slot="pricing-copy">
               <p>
               <p>
-                <strong>Add $20 Pay as you go balance</strong>{" "}
-                <span>(+$1.23 card processing fee)</span>
+                <strong>Add $20 Pay as you go balance</strong> <span>(+$1.23 card processing fee)</span>
               </p>
               </p>
               <p>Use with any agent. Set monthly spend limits. Cancel any time.</p>
               <p>Use with any agent. Set monthly spend limits. Cancel any time.</p>
             </div>
             </div>
           </section>
           </section>
 
 
           <section data-component="comparison">
           <section data-component="comparison">
-            <video
-              src={compareVideo}
-              autoplay
-              playsinline
-              loop
-              muted
-              preload="auto"
-              poster={compareVideoPoster}
-            >
+            <video src={compareVideo} autoplay playsinline loop muted preload="auto" poster={compareVideoPoster}>
               Your browser does not support the video tag.
               Your browser does not support the video tag.
             </video>
             </video>
           </section>
           </section>
@@ -193,8 +142,8 @@ export default function Home() {
             <div data-slot="section-title">
             <div data-slot="section-title">
               <h3>What problem is Zen solving?</h3>
               <h3>What problem is Zen solving?</h3>
               <p>
               <p>
-                There are so many models available, but only a few work well with coding agents.
-                Most providers configure them differently with varying results.
+                There are so many models available, but only a few work well with coding agents. Most providers
+                configure them differently with varying results.
               </p>
               </p>
             </div>
             </div>
             <p>We're fixing this for everyone, not just OpenCode users.</p>
             <p>We're fixing this for everyone, not just OpenCode users.</p>
@@ -229,15 +178,14 @@ export default function Home() {
               <li>
               <li>
                 <span>[2]</span>
                 <span>[2]</span>
                 <div>
                 <div>
-                  <strong>Use Zen with transparent pricing</strong> -{" "}
-                  <a href="/docs/zen/#pricing">pay per request</a> with zero markups
+                  <strong>Use Zen with transparent pricing</strong> - <a href="/docs/zen/#pricing">pay per request</a>{" "}
+                  with zero markups
                 </div>
                 </div>
               </li>
               </li>
               <li>
               <li>
                 <span>[3]</span>
                 <span>[3]</span>
                 <div>
                 <div>
-                  <strong>Auto-top up</strong> - when your balance reaches $5 we’ll automatically
-                  add $20
+                  <strong>Auto-top up</strong> - when your balance reaches $5 we’ll automatically add $20
                 </div>
                 </div>
               </li>
               </li>
             </ul>
             </ul>
@@ -249,9 +197,8 @@ export default function Home() {
               <div>
               <div>
                 <span>[*]</span>
                 <span>[*]</span>
                 <p>
                 <p>
-                  All Zen models are hosted in the US. Providers follow a zero-retention policy and
-                  do not use your data for model training, with the{" "}
-                  <a href="/docs/zen/#privacy">following exceptions</a>.
+                  All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data
+                  for model training, with the <a href="/docs/zen/#privacy">following exceptions</a>.
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -306,8 +253,7 @@ export default function Home() {
                   <span>ex-Head of Design, Laravel</span>
                   <span>ex-Head of Design, Laravel</span>
                 </div>
                 </div>
                 <div data-slot="quote">
                 <div data-slot="quote">
-                  With <span>@OpenCode</span> Zen I know all the models are tested and perfect for
-                  coding agents.
+                  With <span>@OpenCode</span> Zen I know all the models are tested and perfect for coding agents.
                 </div>
                 </div>
               </div>
               </div>
             </a>
             </a>
@@ -331,44 +277,38 @@ export default function Home() {
             <ul>
             <ul>
               <li>
               <li>
                 <Faq question="What is OpenCode Zen?">
                 <Faq question="What is OpenCode Zen?">
-                  Zen is a curated set of AI models tested and benchmarked for coding agents created
-                  by the team behind OpenCode.
+                  Zen is a curated set of AI models tested and benchmarked for coding agents created by the team behind
+                  OpenCode.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="What makes Zen more accurate?">
                 <Faq question="What makes Zen more accurate?">
-                  Zen only provides models that have been specifically tested and benchmarked for
-                  coding agents. You wouldn’t use a butter knife to cut steak, don’t use poor models
-                  for coding.
+                  Zen only provides models that have been specifically tested and benchmarked for coding agents. You
+                  wouldn’t use a butter knife to cut steak, don’t use poor models for coding.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="Is Zen cheaper?">
                 <Faq question="Is Zen cheaper?">
-                  Zen is not for profit. Zen passes through the costs from the model providers to
-                  you. The higher Zen’s usage the more OpenCode can negotiate better rates and pass
-                  those to you.
+                  Zen is not for profit. Zen passes through the costs from the model providers to you. The higher Zen’s
+                  usage the more OpenCode can negotiate better rates and pass those to you.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="How much does Zen cost?">
                 <Faq question="How much does Zen cost?">
-                  Zen <a href="/docs/zen/#pricing">charges per request</a> with zero markups, so you
-                  pay exactly what the model provider charges. Your total cost depends on usage, and
-                  you can set monthly spend limits in your <a href="/auth">account</a>. To cover
-                  costs, OpenCode adds only a small payment processing fee of $1.23 per $20 balance
-                  top-up.
+                  Zen <a href="/docs/zen/#pricing">charges per request</a> with zero markups, so you pay exactly what
+                  the model provider charges. Your total cost depends on usage, and you can set monthly spend limits in
+                  your <a href="/auth">account</a>. To cover costs, OpenCode adds only a small payment processing fee of
+                  $1.23 per $20 balance top-up.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="What about data and privacy?">
                 <Faq question="What about data and privacy?">
-                  All Zen models are hosted in the US. Providers follow a zero-retention policy and
-                  do not use your data for model training, with the{" "}
-                  <a href="/docs/zen/#privacy">following exceptions</a>.
+                  All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data
+                  for model training, with the <a href="/docs/zen/#privacy">following exceptions</a>.
                 </Faq>
                 </Faq>
               </li>
               </li>
               <li>
               <li>
-                <Faq question="Can I set spend limits?">
-                  Yes, you can set monthly spending limits in your account.
-                </Faq>
+                <Faq question="Can I set spend limits?">Yes, you can set monthly spending limits in your account.</Faq>
               </li>
               </li>
               <li>
               <li>
                 <Faq question="Can I cancel?">
                 <Faq question="Can I cancel?">
@@ -377,8 +317,8 @@ export default function Home() {
               </li>
               </li>
               <li>
               <li>
                 <Faq question="Can I use Zen with other coding agents?">
                 <Faq question="Can I use Zen with other coding agents?">
-                  While Zen works great with OpenCode, you can use Zen with any agent. Follow the
-                  setup instructions in your preferred coding agent.
+                  While Zen works great with OpenCode, you can use Zen with any agent. Follow the setup instructions in
+                  your preferred coding agent.
                 </Faq>
                 </Faq>
               </li>
               </li>
             </ul>
             </ul>

+ 1 - 0
packages/console/app/src/routes/zen/util/error.ts

@@ -3,3 +3,4 @@ export class CreditsError extends Error {}
 export class MonthlyLimitError extends Error {}
 export class MonthlyLimitError extends Error {}
 export class UserLimitError extends Error {}
 export class UserLimitError extends Error {}
 export class ModelError extends Error {}
 export class ModelError extends Error {}
+export class RateLimitError extends Error {}

+ 49 - 80
packages/console/app/src/routes/zen/util/handler.ts

@@ -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 { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
 import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
 import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
 import { logger } from "./logger"
 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 { anthropicHelper } from "./provider/anthropic"
 import { openaiHelper } from "./provider/openai"
 import { openaiHelper } from "./provider/openai"
 import { oaCompatHelper } from "./provider/openai-compatible"
 import { oaCompatHelper } from "./provider/openai-compatible"
+import { createRateLimiter } from "./rateLimiter"
 
 
 type ZenData = Awaited<ReturnType<typeof ZenData.list>>
 type ZenData = Awaited<ReturnType<typeof ZenData.list>>
-type Model = ZenData["models"][string]
 
 
 export async function handler(
 export async function handler(
   input: APIEvent,
   input: APIEvent,
@@ -32,6 +28,10 @@ export async function handler(
     parseApiKey: (headers: Headers) => string | undefined
     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 = [
   const FREE_WORKSPACES = [
     "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
     "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
     "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
     "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
@@ -39,6 +39,7 @@ export async function handler(
 
 
   try {
   try {
     const body = await input.request.json()
     const body = await input.request.json()
+    const ip = input.request.headers.get("x-real-ip") ?? ""
     logger.metric({
     logger.metric({
       is_tream: !!body.stream,
       is_tream: !!body.stream,
       session: input.request.headers.get("x-opencode-session"),
       session: input.request.headers.get("x-opencode-session"),
@@ -46,13 +47,11 @@ export async function handler(
     })
     })
     const zenData = ZenData.list()
     const zenData = ZenData.list()
     const modelInfo = validateModel(zenData, body.model)
     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)
     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)
     validateModelSettings(authInfo)
     updateProviderKey(authInfo, providerInfo)
     updateProviderKey(authInfo, providerInfo)
     logger.metric({ provider: providerInfo.id })
     logger.metric({ provider: providerInfo.id })
@@ -67,7 +66,7 @@ export async function handler(
       }),
       }),
     )
     )
     logger.debug("REQUEST URL: " + reqUrl)
     logger.debug("REQUEST URL: " + reqUrl)
-    logger.debug("REQUEST: " + reqBody)
+    logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
     const res = await fetch(reqUrl, {
     const res = await fetch(reqUrl, {
       method: "POST",
       method: "POST",
       headers: (() => {
       headers: (() => {
@@ -92,9 +91,6 @@ export async function handler(
       }
       }
     }
     }
     logger.debug("STATUS: " + res.status + " " + res.statusText)
     logger.debug("STATUS: " + res.status + " " + res.statusText)
-    if (res.status === 400 || res.status === 503) {
-      logger.debug("RESPONSE: " + (await res.text()))
-    }
 
 
     // Handle non-streaming response
     // Handle non-streaming response
     if (!body.stream) {
     if (!body.stream) {
@@ -103,6 +99,7 @@ export async function handler(
       const body = JSON.stringify(responseConverter(json))
       const body = JSON.stringify(responseConverter(json))
       logger.metric({ response_length: body.length })
       logger.metric({ response_length: body.length })
       logger.debug("RESPONSE: " + body)
       logger.debug("RESPONSE: " + body)
+      await rateLimiter?.track()
       await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
       await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
       await reload(authInfo)
       await reload(authInfo)
       return new Response(body, {
       return new Response(body, {
@@ -131,6 +128,7 @@ export async function handler(
                   response_length: responseLength,
                   response_length: responseLength,
                   "timestamp.last_byte": Date.now(),
                   "timestamp.last_byte": Date.now(),
                 })
                 })
+                await rateLimiter?.track()
                 const usage = usageParser.retrieve()
                 const usage = usageParser.retrieve()
                 if (usage) {
                 if (usage) {
                   await trackUsage(authInfo, modelInfo, providerInfo, usage)
                   await trackUsage(authInfo, modelInfo, providerInfo, usage)
@@ -205,6 +203,15 @@ export async function handler(
         { status: 401 },
         { 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(
     return new Response(
       JSON.stringify({
       JSON.stringify({
         type: "error",
         type: "error",
@@ -229,12 +236,8 @@ export async function handler(
     return { id: modelId, ...modelData }
     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)
       .filter((provider) => !provider.disabled)
       .flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
       .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`)
       throw new ModelError(`Provider ${provider.id} not supported`)
     }
     }
 
 
-    const format = zenData.providers[provider.id].format
-
     return {
     return {
       ...provider,
       ...provider,
       ...zenData.providers[provider.id],
       ...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)
     const apiKey = opts.parseApiKey(input.request.headers)
     if (!apiKey) {
     if (!apiKey) {
-      if (model.allowAnonymous) return
+      if (modelInfo.allowAnonymous) return
       throw new AuthError("Missing API key.")
       throw new AuthError("Missing API key.")
     }
     }
 
 
@@ -297,20 +296,11 @@ export async function handler(
         .from(KeyTable)
         .from(KeyTable)
         .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
         .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
         .innerJoin(BillingTable, eq(BillingTable.workspaceID, 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(
         .leftJoin(
           ProviderTable,
           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)))
         .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
         .then((rows) => rows[0]),
         .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) return
     if (authInfo.provider?.credentials) return
     if (authInfo.provider?.credentials) return
     if (authInfo.isFree) return
     if (authInfo.isFree) return
-    if (model.allowAnonymous) return
+    if (modelInfo.allowAnonymous) return
 
 
     const billing = authInfo.billing
     const billing = authInfo.billing
     if (!billing.paymentMethodID)
     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) return
     if (authInfo.isDisabled) throw new ModelError("Model is disabled")
     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) return
     if (!authInfo.provider?.credentials) return
     if (!authInfo.provider?.credentials) return
     providerInfo.apiKey = authInfo.provider.credentials
     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 =
     const modelCost =
       modelInfo.cost200K &&
       modelInfo.cost200K &&
-      inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) >
-        200_000
+      inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
         ? modelInfo.cost200K
         ? modelInfo.cost200K
         : modelInfo.cost
         : modelInfo.cost
 
 
@@ -464,8 +439,7 @@ export async function handler(
 
 
     if (!authInfo) return
     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 Database.transaction(async (tx) => {
       await tx.insert(UsageTable).values({
       await tx.insert(UsageTable).values({
         workspaceID: authInfo.workspaceID,
         workspaceID: authInfo.workspaceID,
@@ -505,9 +479,7 @@ export async function handler(
             `,
             `,
           timeMonthlyUsageUpdated: sql`now()`,
           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) =>
     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) return
     if (authInfo.isFree) return
     if (authInfo.isFree) return
     if (authInfo.provider?.credentials) return
     if (authInfo.provider?.credentials) return
@@ -537,10 +509,7 @@ export async function handler(
               BillingTable.balance,
               BillingTable.balance,
               centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100),
               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()`)),
           ),
           ),
         ),
         ),
     )
     )

+ 7 - 19
packages/console/app/src/routes/zen/util/provider/anthropic.ts

@@ -123,15 +123,12 @@ export function fromAnthropicRequest(body: any): CommonRequest {
         if ((p as any).type === "tool_result") {
         if ((p as any).type === "tool_result") {
           const id = (p as any).tool_use_id
           const id = (p as any).tool_use_id
           const content =
           const content =
-            typeof (p as any).content === "string"
-              ? (p as any).content
-              : JSON.stringify((p as any).content)
+            typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
           msgs.push({ role: "tool", tool_call_id: id, content })
           msgs.push({ role: "tool", tool_call_id: id, content })
         }
         }
       }
       }
       if (partsOut.length > 0) {
       if (partsOut.length > 0) {
-        if (partsOut.length === 1 && partsOut[0].type === "text")
-          msgs.push({ role: "user", content: partsOut[0].text })
+        if (partsOut.length === 1 && partsOut[0].type === "text") msgs.push({ role: "user", content: partsOut[0].text })
         else msgs.push({ role: "user", content: partsOut })
         else msgs.push({ role: "user", content: partsOut })
       }
       }
       continue
       continue
@@ -143,8 +140,7 @@ export function fromAnthropicRequest(body: any): CommonRequest {
       const tcs: any[] = []
       const tcs: any[] = []
       for (const p of partsIn) {
       for (const p of partsIn) {
         if (!p || !(p as any).type) continue
         if (!p || !(p as any).type) continue
-        if ((p as any).type === "text" && typeof (p as any).text === "string")
-          texts.push((p as any).text)
+        if ((p as any).type === "text" && typeof (p as any).text === "string") texts.push((p as any).text)
         if ((p as any).type === "tool_use") {
         if ((p as any).type === "tool_use") {
           const name = (p as any).name
           const name = (p as any).name
           const id = (p as any).id
           const id = (p as any).id
@@ -214,9 +210,7 @@ export function fromAnthropicRequest(body: any): CommonRequest {
 export function toAnthropicRequest(body: CommonRequest) {
 export function toAnthropicRequest(body: CommonRequest) {
   if (!body || typeof body !== "object") return body
   if (!body || typeof body !== "object") return body
 
 
-  const sysIn = Array.isArray(body.messages)
-    ? body.messages.filter((m: any) => m && m.role === "system")
-    : []
+  const sysIn = Array.isArray(body.messages) ? body.messages.filter((m: any) => m && m.role === "system") : []
   let ccCount = 0
   let ccCount = 0
   const cc = () => {
   const cc = () => {
     ccCount++
     ccCount++
@@ -367,9 +361,7 @@ export function fromAnthropicResponse(resp: any): CommonResponse {
 
 
   const idIn = (resp as any).id
   const idIn = (resp as any).id
   const id =
   const id =
-    typeof idIn === "string"
-      ? idIn.replace(/^msg_/, "chatcmpl_")
-      : `chatcmpl_${Math.random().toString(36).slice(2)}`
+    typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
   const model = (resp as any).model
   const model = (resp as any).model
 
 
   const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
   const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
@@ -412,9 +404,7 @@ export function fromAnthropicResponse(resp: any): CommonResponse {
     const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined
     const ct = typeof (u as any).output_tokens === "number" ? (u as any).output_tokens : undefined
     const total = pt != null && ct != null ? pt + ct : undefined
     const total = pt != null && ct != null ? pt + ct : undefined
     const cached =
     const cached =
-      typeof (u as any).cache_read_input_tokens === "number"
-        ? (u as any).cache_read_input_tokens
-        : undefined
+      typeof (u as any).cache_read_input_tokens === "number" ? (u as any).cache_read_input_tokens : undefined
     const details = cached != null ? { cached_tokens: cached } : undefined
     const details = cached != null ? { cached_tokens: cached } : undefined
     return {
     return {
       prompt_tokens: pt,
       prompt_tokens: pt,
@@ -591,9 +581,7 @@ export function fromAnthropicChunk(chunk: string): CommonChunk | string {
       prompt_tokens: u.input_tokens,
       prompt_tokens: u.input_tokens,
       completion_tokens: u.output_tokens,
       completion_tokens: u.output_tokens,
       total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0),
       total_tokens: (u.input_tokens || 0) + (u.output_tokens || 0),
-      ...(u.cache_read_input_tokens
-        ? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } }
-        : {}),
+      ...(u.cache_read_input_tokens ? { prompt_tokens_details: { cached_tokens: u.cache_read_input_tokens } } : {}),
     }
     }
   }
   }
 
 

+ 10 - 21
packages/console/app/src/routes/zen/util/provider/openai-compatible.ts

@@ -57,8 +57,7 @@ export const oaCompatHelper = {
     const inputTokens = usage.prompt_tokens ?? 0
     const inputTokens = usage.prompt_tokens ?? 0
     const outputTokens = usage.completion_tokens ?? 0
     const outputTokens = usage.completion_tokens ?? 0
     const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
     const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? undefined
-    const cacheReadTokens =
-      usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
+    const cacheReadTokens = usage.cached_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? undefined
     return {
     return {
       inputTokens: inputTokens - (cacheReadTokens ?? 0),
       inputTokens: inputTokens - (cacheReadTokens ?? 0),
       outputTokens,
       outputTokens,
@@ -80,8 +79,7 @@ export function fromOaCompatibleRequest(body: any): CommonRequest {
     if (!m || !m.role) continue
     if (!m || !m.role) continue
 
 
     if (m.role === "system") {
     if (m.role === "system") {
-      if (typeof m.content === "string" && m.content.length > 0)
-        msgsOut.push({ role: "system", content: m.content })
+      if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
       continue
       continue
     }
     }
 
 
@@ -92,12 +90,10 @@ export function fromOaCompatibleRequest(body: any): CommonRequest {
         const parts: any[] = []
         const parts: any[] = []
         for (const p of m.content) {
         for (const p of m.content) {
           if (!p || !p.type) continue
           if (!p || !p.type) continue
-          if (p.type === "text" && typeof p.text === "string")
-            parts.push({ type: "text", text: p.text })
+          if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
           if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url })
           if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url })
         }
         }
-        if (parts.length === 1 && parts[0].type === "text")
-          msgsOut.push({ role: "user", content: parts[0].text })
+        if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
         else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
         else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
       }
       }
       continue
       continue
@@ -141,8 +137,7 @@ export function toOaCompatibleRequest(body: CommonRequest) {
     if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url }
     if (p.type === "image_url" && p.image_url) return { type: "image_url", image_url: p.image_url }
     const s = (p as any).source
     const s = (p as any).source
     if (!s || typeof s !== "object") return undefined
     if (!s || typeof s !== "object") return undefined
-    if (s.type === "url" && typeof s.url === "string")
-      return { type: "image_url", image_url: { url: s.url } }
+    if (s.type === "url" && typeof s.url === "string") return { type: "image_url", image_url: { url: s.url } }
     if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string")
     if (s.type === "base64" && typeof s.media_type === "string" && typeof s.data === "string")
       return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } }
       return { type: "image_url", image_url: { url: `data:${s.media_type};base64,${s.data}` } }
     return undefined
     return undefined
@@ -152,8 +147,7 @@ export function toOaCompatibleRequest(body: CommonRequest) {
     if (!m || !m.role) continue
     if (!m || !m.role) continue
 
 
     if (m.role === "system") {
     if (m.role === "system") {
-      if (typeof m.content === "string" && m.content.length > 0)
-        msgsOut.push({ role: "system", content: m.content })
+      if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content })
       continue
       continue
     }
     }
 
 
@@ -166,13 +160,11 @@ export function toOaCompatibleRequest(body: CommonRequest) {
         const parts: any[] = []
         const parts: any[] = []
         for (const p of m.content) {
         for (const p of m.content) {
           if (!p || !p.type) continue
           if (!p || !p.type) continue
-          if (p.type === "text" && typeof p.text === "string")
-            parts.push({ type: "text", text: p.text })
+          if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text })
           const ip = toImg(p)
           const ip = toImg(p)
           if (ip) parts.push(ip)
           if (ip) parts.push(ip)
         }
         }
-        if (parts.length === 1 && parts[0].type === "text")
-          msgsOut.push({ role: "user", content: parts[0].text })
+        if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text })
         else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
         else if (parts.length > 0) msgsOut.push({ role: "user", content: parts })
       }
       }
       continue
       continue
@@ -325,9 +317,7 @@ export function toOaCompatibleResponse(resp: CommonResponse) {
 
 
   const idIn = (resp as any).id
   const idIn = (resp as any).id
   const id =
   const id =
-    typeof idIn === "string"
-      ? idIn.replace(/^msg_/, "chatcmpl_")
-      : `chatcmpl_${Math.random().toString(36).slice(2)}`
+    typeof idIn === "string" ? idIn.replace(/^msg_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
   const model = (resp as any).model
   const model = (resp as any).model
 
 
   const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
   const blocks: any[] = Array.isArray((resp as any).content) ? (resp as any).content : []
@@ -369,8 +359,7 @@ export function toOaCompatibleResponse(resp: CommonResponse) {
     const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined
     const pt = typeof u.input_tokens === "number" ? u.input_tokens : undefined
     const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined
     const ct = typeof u.output_tokens === "number" ? u.output_tokens : undefined
     const total = pt != null && ct != null ? pt + ct : undefined
     const total = pt != null && ct != null ? pt + ct : undefined
-    const cached =
-      typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined
+    const cached = typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : undefined
     const details = cached != null ? { cached_tokens: cached } : undefined
     const details = cached != null ? { cached_tokens: cached } : undefined
     return {
     return {
       prompt_tokens: pt,
       prompt_tokens: pt,

+ 9 - 29
packages/console/app/src/routes/zen/util/provider/openai.ts

@@ -86,11 +86,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
 
 
   const msgs: any[] = []
   const msgs: any[] = []
 
 
-  const inMsgs = Array.isArray(body.input)
-    ? body.input
-    : Array.isArray(body.messages)
-      ? body.messages
-      : []
+  const inMsgs = Array.isArray(body.input) ? body.input : Array.isArray(body.messages) ? body.messages : []
 
 
   for (const m of inMsgs) {
   for (const m of inMsgs) {
     if (!m) continue
     if (!m) continue
@@ -103,9 +99,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
         const args = typeof a === "string" ? a : JSON.stringify(a ?? {})
         const args = typeof a === "string" ? a : JSON.stringify(a ?? {})
         msgs.push({
         msgs.push({
           role: "assistant",
           role: "assistant",
-          tool_calls: [
-            { id: (m as any).id, type: "function", function: { name, arguments: args } },
-          ],
+          tool_calls: [{ id: (m as any).id, type: "function", function: { name, arguments: args } }],
         })
         })
       }
       }
       if ((m as any).type === "function_call_output") {
       if ((m as any).type === "function_call_output") {
@@ -122,8 +116,7 @@ export function fromOpenaiRequest(body: any): CommonRequest {
       if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c })
       if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c })
       if (Array.isArray(c)) {
       if (Array.isArray(c)) {
         const t = c.find((p: any) => p && typeof p.text === "string")
         const t = c.find((p: any) => p && typeof p.text === "string")
-        if (t && typeof t.text === "string" && t.text.length > 0)
-          msgs.push({ role: "system", content: t.text })
+        if (t && typeof t.text === "string" && t.text.length > 0) msgs.push({ role: "system", content: t.text })
       }
       }
       continue
       continue
     }
     }
@@ -136,24 +129,18 @@ export function fromOpenaiRequest(body: any): CommonRequest {
         const parts: any[] = []
         const parts: any[] = []
         for (const p of c) {
         for (const p of c) {
           if (!p || !(p as any).type) continue
           if (!p || !(p as any).type) continue
-          if (
-            ((p as any).type === "text" || (p as any).type === "input_text") &&
-            typeof (p as any).text === "string"
-          )
+          if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string")
             parts.push({ type: "text", text: (p as any).text })
             parts.push({ type: "text", text: (p as any).text })
           const ip = toImg(p)
           const ip = toImg(p)
           if (ip) parts.push(ip)
           if (ip) parts.push(ip)
           if ((p as any).type === "tool_result") {
           if ((p as any).type === "tool_result") {
             const id = (p as any).tool_call_id
             const id = (p as any).tool_call_id
             const content =
             const content =
-              typeof (p as any).content === "string"
-                ? (p as any).content
-                : JSON.stringify((p as any).content)
+              typeof (p as any).content === "string" ? (p as any).content : JSON.stringify((p as any).content)
             msgs.push({ role: "tool", tool_call_id: id, content })
             msgs.push({ role: "tool", tool_call_id: id, content })
           }
           }
         }
         }
-        if (parts.length === 1 && parts[0].type === "text")
-          msgs.push({ role: "user", content: parts[0].text })
+        if (parts.length === 1 && parts[0].type === "text") msgs.push({ role: "user", content: parts[0].text })
         else if (parts.length > 0) msgs.push({ role: "user", content: parts })
         else if (parts.length > 0) msgs.push({ role: "user", content: parts })
       }
       }
       continue
       continue
@@ -280,10 +267,7 @@ export function toOpenaiRequest(body: CommonRequest) {
     }
     }
 
 
     if ((m as any).role === "tool") {
     if ((m as any).role === "tool") {
-      const out =
-        typeof (m as any).content === "string"
-          ? (m as any).content
-          : JSON.stringify((m as any).content)
+      const out = typeof (m as any).content === "string" ? (m as any).content : JSON.stringify((m as any).content)
       input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out })
       input.push({ type: "function_call_output", call_id: (m as any).tool_call_id, output: out })
       continue
       continue
     }
     }
@@ -351,9 +335,7 @@ export function fromOpenaiResponse(resp: any): CommonResponse {
 
 
   const idIn = (r as any).id
   const idIn = (r as any).id
   const id =
   const id =
-    typeof idIn === "string"
-      ? idIn.replace(/^resp_/, "chatcmpl_")
-      : `chatcmpl_${Math.random().toString(36).slice(2)}`
+    typeof idIn === "string" ? idIn.replace(/^resp_/, "chatcmpl_") : `chatcmpl_${Math.random().toString(36).slice(2)}`
   const model = (r as any).model ?? (resp as any).model
   const model = (r as any).model ?? (resp as any).model
 
 
   const out = Array.isArray((r as any).output) ? (r as any).output : []
   const out = Array.isArray((r as any).output) ? (r as any).output : []
@@ -480,9 +462,7 @@ export function toOpenaiResponse(resp: CommonResponse) {
   })()
   })()
 
 
   return {
   return {
-    id:
-      (resp as any).id?.replace(/^chatcmpl_/, "resp_") ??
-      `resp_${Math.random().toString(36).slice(2)}`,
+    id: (resp as any).id?.replace(/^chatcmpl_/, "resp_") ?? `resp_${Math.random().toString(36).slice(2)}`,
     object: "response",
     object: "response",
     model: (resp as any).model,
     model: (resp as any).model,
     output: outputItems,
     output: outputItems,

+ 35 - 0
packages/console/app/src/routes/zen/util/rateLimiter.ts

@@ -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)
+}

+ 1 - 4
packages/console/app/src/routes/zen/v1/models.ts

@@ -50,10 +50,7 @@ export async function GET(input: APIEvent) {
         })
         })
         .from(KeyTable)
         .from(KeyTable)
         .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
         .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)))
         .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
         .then((rows) => rows.map((row) => row.model)),
         .then((rows) => rows.map((row) => row.model)),
     )
     )

+ 1 - 2
packages/console/app/src/style/token/font.css

@@ -15,7 +15,6 @@ body {
   --font-size-9xl: 8rem;
   --font-size-9xl: 8rem;
 
 
   --font-mono:
   --font-mono:
-    "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
-    "Courier New", monospace;
+    "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
   --font-sans: var(--font-mono);
   --font-sans: var(--font-mono);
 }
 }

+ 1 - 1
packages/console/core/package.json

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

+ 3 - 10
packages/console/core/script/lookup-user.ts

@@ -8,22 +8,15 @@ if (!email) {
   process.exit(1)
   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) {
 if (authData.length === 0) {
   console.error("User not found")
   console.error("User not found")
   process.exit(1)
   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) => {
   return Database.use(async (tx) => {
     const data = await callback(tx)
     const data = await callback(tx)
     console.log(`== ${title} ==`)
     console.log(`== ${title} ==`)

+ 1 - 9
packages/console/core/script/reset-db.ts

@@ -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")
 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))
   await Database.use((tx) => tx.delete(table))
 }
 }

+ 0 - 1
packages/console/core/script/update-models.ts

@@ -7,7 +7,6 @@ import { ZenData } from "../src/model"
 
 
 const root = path.resolve(process.cwd(), "..", "..", "..")
 const root = path.resolve(process.cwd(), "..", "..", "..")
 const models = await $`bun sst secret list`.cwd(root).text()
 const models = await $`bun sst secret list`.cwd(root).text()
-console.log("models", models)
 
 
 // read the line starting with "ZEN_MODELS"
 // read the line starting with "ZEN_MODELS"
 const oldValue1 = models
 const oldValue1 = models

+ 25 - 28
packages/console/core/src/aws.ts

@@ -24,40 +24,37 @@ export namespace AWS {
       body: z.string(),
       body: z.string(),
     }),
     }),
     async (input) => {
     async (input) => {
-      const res = await createClient().fetch(
-        "https://email.us-east-1.amazonaws.com/v2/email/outbound-emails",
-        {
-          method: "POST",
-          headers: {
-            "X-Amz-Target": "SES.SendEmail",
-            "Content-Type": "application/json",
+      const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
+        method: "POST",
+        headers: {
+          "X-Amz-Target": "SES.SendEmail",
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+          FromEmailAddress: `OpenCode Zen <[email protected]>`,
+          Destination: {
+            ToAddresses: [input.to],
           },
           },
-          body: JSON.stringify({
-            FromEmailAddress: `OpenCode Zen <[email protected]>`,
-            Destination: {
-              ToAddresses: [input.to],
-            },
-            Content: {
-              Simple: {
-                Subject: {
+          Content: {
+            Simple: {
+              Subject: {
+                Charset: "UTF-8",
+                Data: input.subject,
+              },
+              Body: {
+                Text: {
                   Charset: "UTF-8",
                   Charset: "UTF-8",
-                  Data: input.subject,
+                  Data: input.body,
                 },
                 },
-                Body: {
-                  Text: {
-                    Charset: "UTF-8",
-                    Data: input.body,
-                  },
-                  Html: {
-                    Charset: "UTF-8",
-                    Data: input.body,
-                  },
+                Html: {
+                  Charset: "UTF-8",
+                  Data: input.body,
                 },
                 },
               },
               },
             },
             },
-          }),
-        },
-      )
+          },
+        }),
+      })
       if (!res.ok) {
       if (!res.ok) {
         throw new Error(`Failed to send email: ${res.statusText}`)
         throw new Error(`Failed to send email: ${res.statusText}`)
       }
       }

+ 2 - 8
packages/console/core/src/drizzle/index.ts

@@ -5,10 +5,7 @@ import { Client } from "@planetscale/database"
 
 
 import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
 import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
 import type { ExtractTablesWithRelations } from "drizzle-orm"
 import type { ExtractTablesWithRelations } from "drizzle-orm"
-import type {
-  PlanetScalePreparedQueryHKT,
-  PlanetscaleQueryResultHKT,
-} from "drizzle-orm/planetscale-serverless"
+import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
 import { Context } from "../context"
 import { Context } from "../context"
 import { memo } from "../util/memo"
 import { memo } from "../util/memo"
 
 
@@ -70,10 +67,7 @@ export namespace Database {
     }
     }
   }
   }
 
 
-  export async function transaction<T>(
-    callback: (tx: TxOrDb) => Promise<T>,
-    config?: MySqlTransactionConfig,
-  ) {
+  export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: MySqlTransactionConfig) {
     try {
     try {
       const { tx } = TransactionContext.use()
       const { tx } = TransactionContext.use()
       return callback(tx)
       return callback(tx)

+ 2 - 8
packages/console/core/src/key.ts

@@ -20,14 +20,8 @@ export namespace Key {
           email: AuthTable.subject,
           email: AuthTable.subject,
         })
         })
         .from(KeyTable)
         .from(KeyTable)
-        .innerJoin(
-          UserTable,
-          and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)),
-        )
-        .innerJoin(
-          AuthTable,
-          and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
-        )
+        .innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
+        .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
         .where(
         .where(
           and(
           and(
             ...[
             ...[

+ 2 - 3
packages/console/core/src/model.ts

@@ -24,6 +24,7 @@ export namespace ZenData {
     cost: ModelCostSchema,
     cost: ModelCostSchema,
     cost200K: ModelCostSchema.optional(),
     cost200K: ModelCostSchema.optional(),
     allowAnonymous: z.boolean().optional(),
     allowAnonymous: z.boolean().optional(),
+    rateLimit: z.number().optional(),
     providers: z.array(
     providers: z.array(
       z.object({
       z.object({
         id: z.string(),
         id: z.string(),
@@ -60,9 +61,7 @@ export namespace Model {
   export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
   export const enable = fn(z.object({ model: z.string() }), ({ model }) => {
     Actor.assertAdmin()
     Actor.assertAdmin()
     return Database.use((db) =>
     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))),
     )
     )
   })
   })
 
 

+ 2 - 9
packages/console/core/src/provider.ts

@@ -11,9 +11,7 @@ export namespace Provider {
       tx
       tx
         .select()
         .select()
         .from(ProviderTable)
         .from(ProviderTable)
-        .where(
-          and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted)),
-        ),
+        .where(and(eq(ProviderTable.workspaceID, Actor.workspace()), isNull(ProviderTable.timeDeleted))),
     ),
     ),
   )
   )
 
 
@@ -52,12 +50,7 @@ export namespace Provider {
       return Database.transaction((tx) =>
       return Database.transaction((tx) =>
         tx
         tx
           .delete(ProviderTable)
           .delete(ProviderTable)
-          .where(
-            and(
-              eq(ProviderTable.provider, provider),
-              eq(ProviderTable.workspaceID, Actor.workspace()),
-            ),
-          ),
+          .where(and(eq(ProviderTable.provider, provider), eq(ProviderTable.workspaceID, Actor.workspace()))),
       )
       )
     },
     },
   )
   )

+ 1 - 8
packages/console/core/src/schema/auth.sql.ts

@@ -1,11 +1,4 @@
-import {
-  index,
-  mysqlEnum,
-  mysqlTable,
-  primaryKey,
-  uniqueIndex,
-  varchar,
-} from "drizzle-orm/mysql-core"
+import { index, mysqlEnum, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
 import { id, timestamps, ulid } from "../drizzle/types"
 import { id, timestamps, ulid } from "../drizzle/types"
 
 
 export const AuthProvider = ["email", "github", "google"] as const
 export const AuthProvider = ["email", "github", "google"] as const

+ 1 - 4
packages/console/core/src/schema/model.sql.ts

@@ -9,8 +9,5 @@ export const ModelTable = mysqlTable(
     ...timestamps,
     ...timestamps,
     model: varchar("model", { length: 64 }).notNull(),
     model: varchar("model", { length: 64 }).notNull(),
   },
   },
-  (table) => [
-    ...workspaceIndexes(table),
-    uniqueIndex("model_workspace_model").on(table.workspaceID, table.model),
-  ],
+  (table) => [...workspaceIndexes(table), uniqueIndex("model_workspace_model").on(table.workspaceID, table.model)],
 )
 )

+ 1 - 4
packages/console/core/src/schema/provider.sql.ts

@@ -10,8 +10,5 @@ export const ProviderTable = mysqlTable(
     provider: varchar("provider", { length: 64 }).notNull(),
     provider: varchar("provider", { length: 64 }).notNull(),
     credentials: text("credentials").notNull(),
     credentials: text("credentials").notNull(),
   },
   },
-  (table) => [
-    ...workspaceIndexes(table),
-    uniqueIndex("workspace_provider").on(table.workspaceID, table.provider),
-  ],
+  (table) => [...workspaceIndexes(table), uniqueIndex("workspace_provider").on(table.workspaceID, table.provider)],
 )
 )

+ 1 - 9
packages/console/core/src/schema/user.sql.ts

@@ -1,12 +1,4 @@
-import {
-  mysqlTable,
-  uniqueIndex,
-  varchar,
-  int,
-  mysqlEnum,
-  index,
-  bigint,
-} from "drizzle-orm/mysql-core"
+import { mysqlTable, uniqueIndex, varchar, int, mysqlEnum, index, bigint } from "drizzle-orm/mysql-core"
 import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
 import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
 import { workspaceIndexes } from "./workspace.sql"
 import { workspaceIndexes } from "./workspace.sql"
 
 

+ 5 - 23
packages/console/core/src/user.ts

@@ -26,10 +26,7 @@ export namespace User {
           authEmail: AuthTable.subject,
           authEmail: AuthTable.subject,
         })
         })
         .from(UserTable)
         .from(UserTable)
-        .leftJoin(
-          AuthTable,
-          and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
-        )
+        .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
         .where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))),
         .where(and(eq(UserTable.workspaceID, Actor.workspace()), isNull(UserTable.timeDeleted))),
     ),
     ),
   )
   )
@@ -39,13 +36,7 @@ export namespace User {
       tx
       tx
         .select()
         .select()
         .from(UserTable)
         .from(UserTable)
-        .where(
-          and(
-            eq(UserTable.workspaceID, Actor.workspace()),
-            eq(UserTable.id, id),
-            isNull(UserTable.timeDeleted),
-          ),
-        )
+        .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id), isNull(UserTable.timeDeleted)))
         .then((rows) => rows[0]),
         .then((rows) => rows[0]),
     ),
     ),
   )
   )
@@ -57,10 +48,7 @@ export namespace User {
           email: AuthTable.subject,
           email: AuthTable.subject,
         })
         })
         .from(UserTable)
         .from(UserTable)
-        .leftJoin(
-          AuthTable,
-          and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
-        )
+        .leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
         .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id)))
         .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.id, id)))
         .then((rows) => rows[0]?.email),
         .then((rows) => rows[0]?.email),
     ),
     ),
@@ -142,16 +130,10 @@ export namespace User {
               workspaceName: WorkspaceTable.name,
               workspaceName: WorkspaceTable.name,
             })
             })
             .from(UserTable)
             .from(UserTable)
-            .innerJoin(
-              AuthTable,
-              and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")),
-            )
+            .innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
             .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
             .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, workspaceID))
             .where(
             .where(
-              and(
-                eq(UserTable.workspaceID, workspaceID),
-                eq(UserTable.id, Actor.assert("user").properties.userID),
-              ),
+              and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, Actor.assert("user").properties.userID)),
             )
             )
             .then((rows) => rows[0]),
             .then((rows) => rows[0]),
         )
         )

+ 9 - 0
packages/console/core/sst-env.d.ts

@@ -22,6 +22,14 @@ declare module "sst" {
       type: "sst.sst.Secret"
       type: "sst.sst.Secret"
       value: string
       value: string
     }
     }
+    CLOUDFLARE_API_TOKEN: {
+      type: "sst.sst.Secret"
+      value: string
+    }
+    CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
+      type: "sst.sst.Secret"
+      value: string
+    }
     Console: {
     Console: {
       type: "sst.cloudflare.SolidStart"
       type: "sst.cloudflare.SolidStart"
       url: string
       url: string
@@ -96,6 +104,7 @@ declare module "sst" {
     AuthApi: cloudflare.Service
     AuthApi: cloudflare.Service
     AuthStorage: cloudflare.KVNamespace
     AuthStorage: cloudflare.KVNamespace
     Bucket: cloudflare.R2Bucket
     Bucket: cloudflare.R2Bucket
+    GatewayKv: cloudflare.KVNamespace
     LogProcessor: cloudflare.Service
     LogProcessor: cloudflare.Service
   }
   }
 }
 }

+ 1 - 1
packages/console/function/package.json

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

+ 9 - 0
packages/console/function/sst-env.d.ts

@@ -22,6 +22,14 @@ declare module "sst" {
       type: "sst.sst.Secret"
       type: "sst.sst.Secret"
       value: string
       value: string
     }
     }
+    CLOUDFLARE_API_TOKEN: {
+      type: "sst.sst.Secret"
+      value: string
+    }
+    CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
+      type: "sst.sst.Secret"
+      value: string
+    }
     Console: {
     Console: {
       type: "sst.cloudflare.SolidStart"
       type: "sst.cloudflare.SolidStart"
       url: string
       url: string
@@ -96,6 +104,7 @@ declare module "sst" {
     AuthApi: cloudflare.Service
     AuthApi: cloudflare.Service
     AuthStorage: cloudflare.KVNamespace
     AuthStorage: cloudflare.KVNamespace
     Bucket: cloudflare.R2Bucket
     Bucket: cloudflare.R2Bucket
+    GatewayKv: cloudflare.KVNamespace
     LogProcessor: cloudflare.Service
     LogProcessor: cloudflare.Service
   }
   }
 }
 }

+ 4 - 21
packages/console/mail/emails/templates/InviteEmail.tsx

@@ -1,18 +1,6 @@
 // @ts-nocheck
 // @ts-nocheck
 import React from "react"
 import React from "react"
-import {
-  Img,
-  Row,
-  Html,
-  Link,
-  Body,
-  Head,
-  Button,
-  Column,
-  Preview,
-  Section,
-  Container,
-} from "@jsx-email/all"
+import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
 import { Text, Fonts, Title, A, Span } from "../components"
 import { Text, Fonts, Title, A, Span } from "../components"
 import {
 import {
   unit,
   unit,
@@ -64,8 +52,8 @@ export const InviteEmail = ({
             <Section style={{ padding: `${unit * 2}px 0 0 0` }}>
             <Section style={{ padding: `${unit * 2}px 0 0 0` }}>
               <Text style={headingText}>Join your team's OpenCode workspace</Text>
               <Text style={headingText}>Join your team's OpenCode workspace</Text>
               <Text style={contentText}>
               <Text style={contentText}>
-                You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join
-                the <Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
+                You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join the{" "}
+                <Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
               </Text>
               </Text>
             </Section>
             </Section>
 
 
@@ -73,12 +61,7 @@ export const InviteEmail = ({
               <Button style={button} href={url}>
               <Button style={button} href={url}>
                 <Text style={buttonText}>
                 <Text style={buttonText}>
                   Join workspace
                   Join workspace
-                  <Img
-                    width="24"
-                    height="24"
-                    src={`${assetsUrl}/right-arrow.png`}
-                    alt="Arrow right"
-                  />
+                  <Img width="24" height="24" src={`${assetsUrl}/right-arrow.png`} alt="Arrow right" />
                 </Text>
                 </Text>
               </Button>
               </Button>
             </Section>
             </Section>

+ 1 - 1
packages/console/mail/package.json

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

+ 4 - 1
packages/console/resource/package.json

@@ -13,6 +13,9 @@
     }
     }
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@tsconfig/node22": "22.0.2"
+    "@cloudflare/workers-types": "catalog:",
+    "@tsconfig/node22": "22.0.2",
+    "@types/node": "catalog:",
+    "cloudflare": "5.2.0"
   }
   }
 }
 }

+ 58 - 1
packages/console/resource/resource.node.ts

@@ -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>

+ 9 - 0
packages/console/resource/sst-env.d.ts

@@ -22,6 +22,14 @@ declare module "sst" {
       type: "sst.sst.Secret"
       type: "sst.sst.Secret"
       value: string
       value: string
     }
     }
+    CLOUDFLARE_API_TOKEN: {
+      type: "sst.sst.Secret"
+      value: string
+    }
+    CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
+      type: "sst.sst.Secret"
+      value: string
+    }
     Console: {
     Console: {
       type: "sst.cloudflare.SolidStart"
       type: "sst.cloudflare.SolidStart"
       url: string
       url: string
@@ -96,6 +104,7 @@ declare module "sst" {
     AuthApi: cloudflare.Service
     AuthApi: cloudflare.Service
     AuthStorage: cloudflare.KVNamespace
     AuthStorage: cloudflare.KVNamespace
     Bucket: cloudflare.R2Bucket
     Bucket: cloudflare.R2Bucket
+    GatewayKv: cloudflare.KVNamespace
     LogProcessor: cloudflare.Service
     LogProcessor: cloudflare.Service
   }
   }
 }
 }

+ 1 - 5
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@opencode-ai/desktop",
   "name": "@opencode-ai/desktop",
-  "version": "1.0.44",
+  "version": "1.0.46",
   "description": "",
   "description": "",
   "type": "module",
   "type": "module",
   "scripts": {
   "scripts": {
@@ -46,9 +46,5 @@
     "solid-list": "catalog:",
     "solid-list": "catalog:",
     "tailwindcss": "catalog:",
     "tailwindcss": "catalog:",
     "virtua": "catalog:"
     "virtua": "catalog:"
-  },
-  "prettier": {
-    "semi": false,
-    "printWidth": 120
   }
   }
 }
 }

+ 6 - 6
packages/extensions/zed/extension.toml

@@ -1,7 +1,7 @@
 id = "opencode"
 id = "opencode"
 name = "OpenCode"
 name = "OpenCode"
 description = "The AI coding agent built for the terminal"
 description = "The AI coding agent built for the terminal"
-version = "1.0.44"
+version = "1.0.46"
 schema_version = 1
 schema_version = 1
 authors = ["Anomaly"]
 authors = ["Anomaly"]
 repository = "https://github.com/sst/opencode"
 repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
 icon = "./icons/opencode.svg"
 icon = "./icons/opencode.svg"
 
 
 [agent_servers.opencode.targets.darwin-aarch64]
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.44/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.46/opencode-darwin-arm64.zip"
 cmd = "./opencode"
 cmd = "./opencode"
 args = ["acp"]
 args = ["acp"]
 
 
 [agent_servers.opencode.targets.darwin-x86_64]
 [agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.44/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.46/opencode-darwin-x64.zip"
 cmd = "./opencode"
 cmd = "./opencode"
 args = ["acp"]
 args = ["acp"]
 
 
 [agent_servers.opencode.targets.linux-aarch64]
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.44/opencode-linux-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.46/opencode-linux-arm64.zip"
 cmd = "./opencode"
 cmd = "./opencode"
 args = ["acp"]
 args = ["acp"]
 
 
 [agent_servers.opencode.targets.linux-x86_64]
 [agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.44/opencode-linux-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.46/opencode-linux-x64.zip"
 cmd = "./opencode"
 cmd = "./opencode"
 args = ["acp"]
 args = ["acp"]
 
 
 [agent_servers.opencode.targets.windows-x86_64]
 [agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.44/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.46/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 cmd = "./opencode.exe"
 args = ["acp"]
 args = ["acp"]

+ 1 - 1
packages/function/package.json

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

+ 1 - 5
packages/function/src/api.ts

@@ -268,11 +268,7 @@ export default new Hono<{ Bindings: Env }>()
       // Verify permissions
       // Verify permissions
       const userClient = new Octokit({ auth: token })
       const userClient = new Octokit({ auth: token })
       const { data: repoData } = await userClient.repos.get({ owner, repo })
       const { data: repoData } = await userClient.repos.get({ owner, repo })
-      if (
-        !repoData.permissions.admin &&
-        !repoData.permissions.push &&
-        !repoData.permissions.maintain
-      )
+      if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
         throw new Error("User does not have write permissions")
         throw new Error("User does not have write permissions")
 
 
       // Get installation token
       // Get installation token

+ 9 - 0
packages/function/sst-env.d.ts

@@ -22,6 +22,14 @@ declare module "sst" {
       type: "sst.sst.Secret"
       type: "sst.sst.Secret"
       value: string
       value: string
     }
     }
+    CLOUDFLARE_API_TOKEN: {
+      type: "sst.sst.Secret"
+      value: string
+    }
+    CLOUDFLARE_DEFAULT_ACCOUNT_ID: {
+      type: "sst.sst.Secret"
+      value: string
+    }
     Console: {
     Console: {
       type: "sst.cloudflare.SolidStart"
       type: "sst.cloudflare.SolidStart"
       url: string
       url: string
@@ -96,6 +104,7 @@ declare module "sst" {
     AuthApi: cloudflare.Service
     AuthApi: cloudflare.Service
     AuthStorage: cloudflare.KVNamespace
     AuthStorage: cloudflare.KVNamespace
     Bucket: cloudflare.R2Bucket
     Bucket: cloudflare.R2Bucket
+    GatewayKv: cloudflare.KVNamespace
     LogProcessor: cloudflare.Service
     LogProcessor: cloudflare.Service
   }
   }
 }
 }

+ 1 - 1
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
 {
   "$schema": "https://json.schemastore.org/package.json",
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.0.44",
+  "version": "1.0.46",
   "name": "opencode",
   "name": "opencode",
   "type": "module",
   "type": "module",
   "private": true,
   "private": true,

+ 2 - 6
packages/opencode/script/build.ts

@@ -41,9 +41,7 @@ for (const [os, arch] of targets) {
 
 
   const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
   const opentui = `@opentui/core-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}`
   await $`mkdir -p ../../node_modules/${opentui}`
   await $`mkdir -p ../../node_modules/${opentui}`
-  await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(
-    path.join(dir, "../../node_modules"),
-  )
+  await $`npm pack ${opentui}@${pkg.dependencies["@opentui/core"]}`.cwd(path.join(dir, "../../node_modules"))
   await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
   await $`tar -xf ../../node_modules/${opentui.replace("@opentui/", "opentui-")}-*.tgz -C ../../node_modules/${opentui} --strip-components=1`
 
 
   const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
   const watcher = `@parcel/watcher-${os === "windows" ? "win32" : os}-${arch.replace("-baseline", "")}${os === "linux" ? "-glibc" : ""}`
@@ -51,9 +49,7 @@ for (const [os, arch] of targets) {
   await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
   await $`npm pack ${watcher}`.cwd(path.join(dir, "../../node_modules")).quiet()
   await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
   await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
 
 
-  const parserWorker = fs.realpathSync(
-    path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"),
-  )
+  const parserWorker = fs.realpathSync(path.resolve(dir, "./node_modules/@opentui/core/parser.worker.js"))
   const workerPath = "./src/cli/cmd/tui/worker.ts"
   const workerPath = "./src/cli/cmd/tui/worker.ts"
 
 
   await Bun.build({
   await Bun.build({

+ 2 - 5
packages/opencode/script/postinstall.mjs

@@ -77,8 +77,7 @@ async function regenerateWindowsCmdWrappers() {
 
 
     // npm_config_global is string | undefined
     // npm_config_global is string | undefined
     // if it exists, the value is true
     // if it exists, the value is true
-    const isGlobal =
-      process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
+    const isGlobal = process.env.npm_config_global === "true" || pkgPath.includes(path.join("npm", "node_modules"))
 
 
     // The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links
     // The npm rebuild command does 2 things - Execute lifecycle scripts and rebuild bin links
     // We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts
     // We want to skip lifecycle scripts to avoid infinite loops, so we use --ignore-scripts
@@ -94,9 +93,7 @@ async function regenerateWindowsCmdWrappers() {
     console.log("Successfully rebuilt npm bin links")
     console.log("Successfully rebuilt npm bin links")
   } catch (error) {
   } catch (error) {
     console.error("Error rebuilding npm links:", error.message)
     console.error("Error rebuilding npm links:", error.message)
-    console.error(
-      "npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts",
-    )
+    console.error("npm rebuild failed. You may need to manually run: npm rebuild opencode-ai --ignore-scripts")
   }
   }
 }
 }
 
 

+ 4 - 12
packages/opencode/script/publish.ts

@@ -55,18 +55,10 @@ if (!Script.preview) {
   }
   }
 
 
   // Calculate SHA values
   // Calculate SHA values
-  const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
-    .text()
-    .then((x) => x.trim())
-  const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
-    .text()
-    .then((x) => x.trim())
-  const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
-    .text()
-    .then((x) => x.trim())
-  const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
-    .text()
-    .then((x) => x.trim())
+  const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
+  const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
+  const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
+  const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
 
 
   const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
   const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2)
 
 

+ 2 - 13
packages/opencode/script/schema.ts

@@ -19,23 +19,12 @@ const result = z.toJSONSchema(Config.Info, {
     const schema = ctx.jsonSchema
     const schema = ctx.jsonSchema
 
 
     // Preserve strictness: set additionalProperties: false for objects
     // Preserve strictness: set additionalProperties: false for objects
-    if (
-      schema &&
-      typeof schema === "object" &&
-      schema.type === "object" &&
-      schema.additionalProperties === undefined
-    ) {
+    if (schema && typeof schema === "object" && schema.type === "object" && schema.additionalProperties === undefined) {
       schema.additionalProperties = false
       schema.additionalProperties = false
     }
     }
 
 
     // Add examples and default descriptions for string fields with defaults
     // Add examples and default descriptions for string fields with defaults
-    if (
-      schema &&
-      typeof schema === "object" &&
-      "type" in schema &&
-      schema.type === "string" &&
-      schema?.default
-    ) {
+    if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
       if (!schema.examples) {
       if (!schema.examples) {
         schema.examples = [schema.default]
         schema.examples = [schema.default]
       }
       }

+ 7 - 17
packages/opencode/src/acp/agent.ts

@@ -199,10 +199,8 @@ export namespace ACP {
 
 
                       if (kind === "edit") {
                       if (kind === "edit") {
                         const input = part.state.input
                         const input = part.state.input
-                        const filePath =
-                          typeof input["filePath"] === "string" ? input["filePath"] : ""
-                        const oldText =
-                          typeof input["oldString"] === "string" ? input["oldString"] : ""
+                        const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
+                        const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
                         const newText =
                         const newText =
                           typeof input["newString"] === "string"
                           typeof input["newString"] === "string"
                             ? input["newString"]
                             ? input["newString"]
@@ -218,9 +216,7 @@ export namespace ACP {
                       }
                       }
 
 
                       if (part.tool === "todowrite") {
                       if (part.tool === "todowrite") {
-                        const parsedTodos = z
-                          .array(Todo.Info)
-                          .safeParse(JSON.parse(part.state.output))
+                        const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
                         if (parsedTodos.success) {
                         if (parsedTodos.success) {
                           await this.connection
                           await this.connection
                             .sessionUpdate({
                             .sessionUpdate({
@@ -229,9 +225,7 @@ export namespace ACP {
                                 sessionUpdate: "plan",
                                 sessionUpdate: "plan",
                                 entries: parsedTodos.data.map((todo) => {
                                 entries: parsedTodos.data.map((todo) => {
                                   const status: PlanEntry["status"] =
                                   const status: PlanEntry["status"] =
-                                    todo.status === "cancelled"
-                                      ? "completed"
-                                      : (todo.status as PlanEntry["status"])
+                                    todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
                                   return {
                                   return {
                                     priority: "medium",
                                     priority: "medium",
                                     status,
                                     status,
@@ -481,8 +475,7 @@ export namespace ACP {
           description: agent.description,
           description: agent.description,
         }))
         }))
 
 
-      const currentModeId =
-        availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
+      const currentModeId = availableModes.find((m) => m.name === "build")?.id ?? availableModes[0].id
 
 
       const mcpServers: Record<string, Config.Mcp> = {}
       const mcpServers: Record<string, Config.Mcp> = {}
       for (const server of params.mcpServers) {
       for (const server of params.mcpServers) {
@@ -587,8 +580,7 @@ export namespace ACP {
       const agent = session.modeId ?? "build"
       const agent = session.modeId ?? "build"
 
 
       const parts: Array<
       const parts: Array<
-        | { type: "text"; text: string }
-        | { type: "file"; url: string; filename: string; mime: string }
+        { type: "text"; text: string } | { type: "file"; url: string; filename: string; mime: string }
       > = []
       > = []
       for (const part of params.prompt) {
       for (const part of params.prompt) {
         switch (part.type) {
         switch (part.type) {
@@ -794,9 +786,7 @@ export namespace ACP {
 
 
   function parseUri(
   function parseUri(
     uri: string,
     uri: string,
-  ):
-    | { type: "file"; url: string; filename: string; mime: string }
-    | { type: "text"; text: string } {
+  ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
     try {
     try {
       if (uri.startsWith("file://")) {
       if (uri.startsWith("file://")) {
         const path = uri.slice(7)
         const path = uri.slice(7)

+ 1 - 5
packages/opencode/src/acp/session.ts

@@ -13,11 +13,7 @@ export class ACPSessionManager {
     this.sdk = sdk
     this.sdk = sdk
   }
   }
 
 
-  async create(
-    cwd: string,
-    mcpServers: McpServer[],
-    model?: ACPSessionState["model"],
-  ): Promise<ACPSessionState> {
+  async create(cwd: string, mcpServers: McpServer[], model?: ACPSessionState["model"]): Promise<ACPSessionState> {
     const session = await this.sdk.session
     const session = await this.sdk.session
       .create({
       .create({
         body: {
         body: {

+ 2 - 16
packages/opencode/src/agent/agent.ts

@@ -143,18 +143,7 @@ export namespace Agent {
           tools: {},
           tools: {},
           builtIn: false,
           builtIn: false,
         }
         }
-      const {
-        name,
-        model,
-        prompt,
-        tools,
-        description,
-        temperature,
-        top_p,
-        mode,
-        permission,
-        ...extra
-      } = value
+      const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
       item.options = {
       item.options = {
         ...item.options,
         ...item.options,
         ...extra,
         ...extra,
@@ -223,10 +212,7 @@ export namespace Agent {
   }
   }
 }
 }
 
 
-function mergeAgentPermissions(
-  basePermission: any,
-  overridePermission: any,
-): Agent.Info["permission"] {
+function mergeAgentPermissions(basePermission: any, overridePermission: any): Agent.Info["permission"] {
   if (typeof basePermission.bash === "string") {
   if (typeof basePermission.bash === "string") {
     basePermission.bash = {
     basePermission.bash = {
       "*": basePermission.bash,
       "*": basePermission.bash,

+ 1 - 4
packages/opencode/src/bun/index.ts

@@ -8,10 +8,7 @@ import { readableStreamToText } from "bun"
 export namespace BunProc {
 export namespace BunProc {
   const log = Log.create({ service: "bun" })
   const log = Log.create({ service: "bun" })
 
 
-  export async function run(
-    cmd: string[],
-    options?: Bun.SpawnOptions.OptionsObject<any, any, any>,
-  ) {
+  export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
     log.info("running", {
     log.info("running", {
       cmd: [which(), ...cmd],
       cmd: [which(), ...cmd],
       ...options,
       ...options,

+ 2 - 8
packages/opencode/src/bus/index.ts

@@ -19,10 +19,7 @@ export namespace Bus {
 
 
   const registry = new Map<string, EventDefinition>()
   const registry = new Map<string, EventDefinition>()
 
 
-  export function event<Type extends string, Properties extends ZodType>(
-    type: Type,
-    properties: Properties,
-  ) {
+  export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
     const result = {
     const result = {
       type,
       type,
       properties,
       properties,
@@ -73,10 +70,7 @@ export namespace Bus {
 
 
   export function subscribe<Definition extends EventDefinition>(
   export function subscribe<Definition extends EventDefinition>(
     def: Definition,
     def: Definition,
-    callback: (event: {
-      type: Definition["type"]
-      properties: z.infer<Definition["properties"]>
-    }) => void,
+    callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
   ) {
   ) {
     return raw(def.type, callback)
     return raw(def.type, callback)
   }
   }

+ 4 - 13
packages/opencode/src/cli/cmd/auth.ts

@@ -14,11 +14,7 @@ export const AuthCommand = cmd({
   command: "auth",
   command: "auth",
   describe: "manage credentials",
   describe: "manage credentials",
   builder: (yargs) =>
   builder: (yargs) =>
-    yargs
-      .command(AuthLoginCommand)
-      .command(AuthLogoutCommand)
-      .command(AuthListCommand)
-      .demandCommand(),
+    yargs.command(AuthLoginCommand).command(AuthLogoutCommand).command(AuthListCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 
@@ -64,9 +60,7 @@ export const AuthListCommand = cmd({
         prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
         prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
       }
       }
 
 
-      prompts.outro(
-        `${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"),
-      )
+      prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
     }
     }
   },
   },
 })
 })
@@ -86,9 +80,7 @@ export const AuthLoginCommand = cmd({
         UI.empty()
         UI.empty()
         prompts.intro("Add credential")
         prompts.intro("Add credential")
         if (args.url) {
         if (args.url) {
-          const wellknown = await fetch(`${args.url}/.well-known/opencode`).then(
-            (x) => x.json() as any,
-          )
+          const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
           prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
           prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
           const proc = Bun.spawn({
           const proc = Bun.spawn({
             cmd: wellknown.auth.command,
             cmd: wellknown.auth.command,
@@ -290,8 +282,7 @@ export const AuthLoginCommand = cmd({
         if (provider === "other") {
         if (provider === "other") {
           provider = await prompts.text({
           provider = await prompts.text({
             message: "Enter provider id",
             message: "Enter provider id",
-            validate: (x) =>
-              x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only",
+            validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
           })
           })
           if (prompts.isCancel(provider)) throw new UI.CancelledError()
           if (prompts.isCancel(provider)) throw new UI.CancelledError()
           provider = provider.replace(/^@ai-sdk\//, "")
           provider = provider.replace(/^@ai-sdk\//, "")

+ 1 - 5
packages/opencode/src/cli/cmd/debug/lsp.ts

@@ -7,11 +7,7 @@ import { EOL } from "os"
 export const LSPCommand = cmd({
 export const LSPCommand = cmd({
   command: "lsp",
   command: "lsp",
   builder: (yargs) =>
   builder: (yargs) =>
-    yargs
-      .command(DiagnosticsCommand)
-      .command(SymbolsCommand)
-      .command(DocumentSymbolsCommand)
-      .demandCommand(),
+    yargs.command(DiagnosticsCommand).command(SymbolsCommand).command(DocumentSymbolsCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 

+ 2 - 5
packages/opencode/src/cli/cmd/debug/ripgrep.ts

@@ -6,8 +6,7 @@ import { cmd } from "../cmd"
 
 
 export const RipgrepCommand = cmd({
 export const RipgrepCommand = cmd({
   command: "rg",
   command: "rg",
-  builder: (yargs) =>
-    yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
+  builder: (yargs) => yargs.command(TreeCommand).command(FilesCommand).command(SearchCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 
@@ -19,9 +18,7 @@ const TreeCommand = cmd({
     }),
     }),
   async handler(args) {
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
     await bootstrap(process.cwd(), async () => {
-      process.stdout.write(
-        (await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL,
-      )
+      process.stdout.write((await Ripgrep.tree({ cwd: Instance.directory, limit: args.limit })) + EOL)
     })
     })
   },
   },
 })
 })

+ 1 - 2
packages/opencode/src/cli/cmd/debug/snapshot.ts

@@ -4,8 +4,7 @@ import { cmd } from "../cmd"
 
 
 export const SnapshotCommand = cmd({
 export const SnapshotCommand = cmd({
   command: "snapshot",
   command: "snapshot",
-  builder: (yargs) =>
-    yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
+  builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
   async handler() {},
   async handler() {},
 })
 })
 
 

+ 15 - 45
packages/opencode/src/cli/cmd/github.ts

@@ -189,9 +189,7 @@ export const GithubInstallCommand = cmd({
           async function getAppInfo() {
           async function getAppInfo() {
             const project = Instance.project
             const project = Instance.project
             if (project.vcs !== "git") {
             if (project.vcs !== "git") {
-              prompts.log.error(
-                `Could not find git repository. Please run this command from a git repository.`,
-              )
+              prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
               throw new UI.CancelledError()
               throw new UI.CancelledError()
             }
             }
 
 
@@ -204,13 +202,9 @@ export const GithubInstallCommand = cmd({
             // ie. [email protected]:sst/opencode
             // ie. [email protected]:sst/opencode
             // ie. ssh://[email protected]/sst/opencode.git
             // ie. ssh://[email protected]/sst/opencode.git
             // ie. ssh://[email protected]/sst/opencode
             // ie. ssh://[email protected]/sst/opencode
-            const parsed = info.match(
-              /^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/,
-            )
+            const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
             if (!parsed) {
             if (!parsed) {
-              prompts.log.error(
-                `Could not find git repository. Please run this command from a git repository.`,
-              )
+              prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
               throw new UI.CancelledError()
               throw new UI.CancelledError()
             }
             }
             const [, owner, repo] = parsed
             const [, owner, repo] = parsed
@@ -451,9 +445,7 @@ export const GithubRunCommand = cmd({
               const summary = await summarize(response)
               const summary = await summarize(response)
               await pushToLocalBranch(summary)
               await pushToLocalBranch(summary)
             }
             }
-            const hasShared = prData.comments.nodes.some((c) =>
-              c.body.includes(`${shareBaseUrl}/s/${shareId}`),
-            )
+            const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
             await updateComment(`${response}${footer({ image: !hasShared })}`)
             await updateComment(`${response}${footer({ image: !hasShared })}`)
           }
           }
           // Fork PR
           // Fork PR
@@ -465,9 +457,7 @@ export const GithubRunCommand = cmd({
               const summary = await summarize(response)
               const summary = await summarize(response)
               await pushToForkBranch(summary, prData)
               await pushToForkBranch(summary, prData)
             }
             }
-            const hasShared = prData.comments.nodes.some((c) =>
-              c.body.includes(`${shareBaseUrl}/s/${shareId}`),
-            )
+            const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
             await updateComment(`${response}${footer({ image: !hasShared })}`)
             await updateComment(`${response}${footer({ image: !hasShared })}`)
           }
           }
         }
         }
@@ -557,12 +547,8 @@ export const GithubRunCommand = cmd({
         // ie. <img alt="Image" src="https://github.com/user-attachments/assets/xxxx" />
         // 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. [api.json](https://github.com/user-attachments/files/21433810/api.json)
         // ie. ![Image](https://github.com/user-attachments/assets/xxxx)
         // 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)
         const matches = [...mdMatches, ...tagMatches].sort((a, b) => a.index - b.index)
         console.log("Images", JSON.stringify(matches, null, 2))
         console.log("Images", JSON.stringify(matches, null, 2))
 
 
@@ -587,10 +573,7 @@ export const GithubRunCommand = cmd({
 
 
           // Replace img tag with file path, ie. @image.png
           // Replace img tag with file path, ie. @image.png
           const replacement = `@${filename}`
           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
           offset += replacement.length - tag.length
 
 
           const contentType = res.headers.get("content-type")
           const contentType = res.headers.get("content-type")
@@ -873,8 +856,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
           throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
           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 createComment() {
       async function createComment() {
@@ -922,9 +904,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
 
 
           return `<a href="${shareBaseUrl}/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`
           return `<a href="${shareBaseUrl}/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](${shareBaseUrl}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;`
-          : ""
+        const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId})&nbsp;&nbsp;|&nbsp;&nbsp;` : ""
         return `\n\n${image}${shareUrl}[github run](${runUrl})`
         return `\n\n${image}${shareUrl}[github run](${runUrl})`
       }
       }
 
 
@@ -1100,13 +1080,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
           })
           })
           .map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
           .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 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 [
           return [
             `- ${r.author.login} at ${r.submittedAt}:`,
             `- ${r.author.login} at ${r.submittedAt}:`,
             `  - Review body: ${r.body}`,
             `  - Review body: ${r.body}`,
@@ -1128,15 +1104,9 @@ query($owner: String!, $repo: String!, $number: Int!) {
           `Deletions: ${pr.deletions}`,
           `Deletions: ${pr.deletions}`,
           `Total Commits: ${pr.commits.totalCount}`,
           `Total Commits: ${pr.commits.totalCount}`,
           `Changed Files: ${pr.files.nodes.length} files`,
           `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>",
           "</pull_request>",
         ].join("\n")
         ].join("\n")
       }
       }

+ 5 - 18
packages/opencode/src/cli/cmd/run.ts

@@ -138,9 +138,7 @@ export const RunCommand = cmd({
 
 
       const outputJsonEvent = (type: string, data: any) => {
       const outputJsonEvent = (type: string, data: any) => {
         if (args.format === "json") {
         if (args.format === "json") {
-          process.stdout.write(
-            JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL,
-          )
+          process.stdout.write(JSON.stringify({ type, timestamp: Date.now(), sessionID, ...data }) + EOL)
           return true
           return true
         }
         }
         return false
         return false
@@ -160,9 +158,7 @@ export const RunCommand = cmd({
               const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
               const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
               const title =
               const title =
                 part.state.title ||
                 part.state.title ||
-                (Object.keys(part.state.input).length > 0
-                  ? JSON.stringify(part.state.input)
-                  : "Unknown")
+                (Object.keys(part.state.input).length > 0 ? JSON.stringify(part.state.input) : "Unknown")
               printEvent(color, tool, title)
               printEvent(color, tool, title)
               if (part.tool === "bash" && part.state.output?.trim()) {
               if (part.tool === "bash" && part.state.output?.trim()) {
                 UI.println()
                 UI.println()
@@ -215,10 +211,7 @@ export const RunCommand = cmd({
               ],
               ],
               initialValue: "once",
               initialValue: "once",
             }).catch(() => "reject")
             }).catch(() => "reject")
-            const response = (result.toString().includes("cancel") ? "reject" : result) as
-              | "once"
-              | "always"
-              | "reject"
+            const response = (result.toString().includes("cancel") ? "reject" : result) as "once" | "always" | "reject"
             await sdk.postSessionIdPermissionsPermissionId({
             await sdk.postSessionIdPermissionsPermissionId({
               path: { id: sessionID, permissionID: permission.id },
               path: { id: sessionID, permissionID: permission.id },
               body: { response },
               body: { response },
@@ -280,10 +273,7 @@ export const RunCommand = cmd({
       }
       }
 
 
       const cfgResult = await sdk.config.get()
       const cfgResult = await sdk.config.get()
-      if (
-        cfgResult.data &&
-        (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
-      ) {
+      if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
         const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
         const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
           if (error instanceof Error && error.message.includes("disabled")) {
           if (error instanceof Error && error.message.includes("disabled")) {
             UI.println(UI.Style.TEXT_DANGER_BOLD + "!  " + error.message)
             UI.println(UI.Style.TEXT_DANGER_BOLD + "!  " + error.message)
@@ -336,10 +326,7 @@ export const RunCommand = cmd({
       }
       }
 
 
       const cfgResult = await sdk.config.get()
       const cfgResult = await sdk.config.get()
-      if (
-        cfgResult.data &&
-        (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)
-      ) {
+      if (cfgResult.data && (cfgResult.data.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share)) {
         const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
         const shareResult = await sdk.session.share({ path: { id: sessionID } }).catch((error) => {
           if (error instanceof Error && error.message.includes("disabled")) {
           if (error instanceof Error && error.message.includes("disabled")) {
             UI.println(UI.Style.TEXT_DANGER_BOLD + "!  " + error.message)
             UI.println(UI.Style.TEXT_DANGER_BOLD + "!  " + error.message)

+ 5 - 14
packages/opencode/src/cli/cmd/stats.ts

@@ -68,9 +68,7 @@ async function getAllSessions(): Promise<Session.Info[]> {
     if (!project) continue
     if (!project) continue
 
 
     const sessionKeys = await Storage.list(["session", project.id])
     const sessionKeys = await Storage.list(["session", project.id])
-    const projectSessions = await Promise.all(
-      sessionKeys.map((key) => Storage.read<Session.Info>(key)),
-    )
+    const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
 
 
     for (const session of projectSessions) {
     for (const session of projectSessions) {
       if (session) {
       if (session) {
@@ -87,16 +85,12 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
   const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
   const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
   const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
   const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
 
 
-  let filteredSessions = days
-    ? sessions.filter((session) => session.time.updated >= cutoffTime)
-    : sessions
+  let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
 
 
   if (projectFilter !== undefined) {
   if (projectFilter !== undefined) {
     if (projectFilter === "") {
     if (projectFilter === "") {
       const currentProject = await getCurrentProject()
       const currentProject = await getCurrentProject()
-      filteredSessions = filteredSessions.filter(
-        (session) => session.projectID === currentProject.id,
-      )
+      filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
     } else {
     } else {
       filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
       filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
     }
     }
@@ -125,9 +119,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
   }
   }
 
 
   if (filteredSessions.length > 1000) {
   if (filteredSessions.length > 1000) {
-    console.log(
-      `Large dataset detected (${filteredSessions.length} sessions). This may take a while...`,
-    )
+    console.log(`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`)
   }
   }
 
 
   if (filteredSessions.length === 0) {
   if (filteredSessions.length === 0) {
@@ -262,8 +254,7 @@ export function displayStats(stats: SessionStats, toolLimit?: number) {
       const percentage = ((count / totalToolUsage) * 100).toFixed(1)
       const percentage = ((count / totalToolUsage) * 100).toFixed(1)
 
 
       const maxToolLength = 18
       const maxToolLength = 18
-      const truncatedTool =
-        tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
+      const truncatedTool = tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
       const toolName = truncatedTool.padEnd(maxToolLength)
       const toolName = truncatedTool.padEnd(maxToolLength)
 
 
       const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`
       const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`

+ 4 - 26
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -2,16 +2,7 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
 import { Clipboard } from "@tui/util/clipboard"
 import { Clipboard } from "@tui/util/clipboard"
 import { TextAttributes } from "@opentui/core"
 import { TextAttributes } from "@opentui/core"
 import { RouteProvider, useRoute } from "@tui/context/route"
 import { RouteProvider, useRoute } from "@tui/context/route"
-import {
-  Switch,
-  Match,
-  createEffect,
-  untrack,
-  ErrorBoundary,
-  createSignal,
-  onMount,
-  batch,
-} from "solid-js"
+import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch } from "solid-js"
 import { Installation } from "@/installation"
 import { Installation } from "@/installation"
 import { Global } from "@/global"
 import { Global } from "@/global"
 import { DialogProvider, useDialog } from "@tui/ui/dialog"
 import { DialogProvider, useDialog } from "@tui/ui/dialog"
@@ -111,11 +102,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
     render(
     render(
       () => {
       () => {
         return (
         return (
-          <ErrorBoundary
-            fallback={(error, reset) => (
-              <ErrorComponent error={error} reset={reset} onExit={onExit} />
-            )}
-          >
+          <ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
             <ArgsProvider {...input.args}>
             <ArgsProvider {...input.args}>
               <ExitProvider onExit={onExit}>
               <ExitProvider onExit={onExit}>
                 <KVProvider>
                 <KVProvider>
@@ -440,12 +427,7 @@ function App() {
         flexShrink={0}
         flexShrink={0}
       >
       >
         <box flexDirection="row">
         <box flexDirection="row">
-          <box
-            flexDirection="row"
-            backgroundColor={theme.backgroundElement}
-            paddingLeft={1}
-            paddingRight={1}
-          >
+          <box flexDirection="row" backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
             <text fg={theme.textMuted}>open</text>
             <text fg={theme.textMuted}>open</text>
             <text fg={theme.text} attributes={TextAttributes.BOLD}>
             <text fg={theme.text} attributes={TextAttributes.BOLD}>
               code{" "}
               code{" "}
@@ -461,11 +443,7 @@ function App() {
             tab
             tab
           </text>
           </text>
           <text fg={local.agent.color(local.agent.current().name)}>{""}</text>
           <text fg={local.agent.color(local.agent.current().name)}>{""}</text>
-          <text
-            bg={local.agent.color(local.agent.current().name)}
-            fg={theme.background}
-            wrapMode={undefined}
-          >
+          <text bg={local.agent.color(local.agent.current().name)} fg={theme.background} wrapMode={undefined}>
             <span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
             <span style={{ bold: true }}> {local.agent.current().name.toUpperCase()}</span>
             <span> AGENT </span>
             <span> AGENT </span>
           </text>
           </text>

+ 10 - 5
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx

@@ -4,12 +4,19 @@ import { useSync } from "@tui/context/sync"
 import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda"
 import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda"
 import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
 import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
 import { useDialog } from "@tui/ui/dialog"
 import { useDialog } from "@tui/ui/dialog"
+import { useTheme } from "../context/theme"
+
+function Free() {
+  const { theme } = useTheme()
+  return <span style={{ fg: theme.secondary }}>Free</span>
+}
 
 
 export function DialogModel() {
 export function DialogModel() {
   const local = useLocal()
   const local = useLocal()
   const sync = useSync()
   const sync = useSync()
   const dialog = useDialog()
   const dialog = useDialog()
   const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
   const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
+  const { theme } = useTheme()
 
 
   const options = createMemo(() => {
   const options = createMemo(() => {
     return [
     return [
@@ -29,6 +36,7 @@ export function DialogModel() {
                 title: model.name ?? item.modelID,
                 title: model.name ?? item.modelID,
                 description: provider.name,
                 description: provider.name,
                 category: "Recent",
                 category: "Recent",
+                footer: model.cost.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
               },
               },
             ]
             ]
           })
           })
@@ -51,12 +59,9 @@ export function DialogModel() {
               title: info.name ?? model,
               title: info.name ?? model,
               description: provider.name,
               description: provider.name,
               category: provider.name,
               category: provider.name,
+              footer: info.cost.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
             })),
             })),
-            filter(
-              (x) =>
-                Boolean(ref()?.filter) ||
-                !local.model.recent().find((y) => isDeepEqual(y, x.value)),
-            ),
+            filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
           ),
           ),
         ),
         ),
       ),
       ),

+ 1 - 3
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -20,9 +20,7 @@ export function DialogSessionList() {
 
 
   const deleteKeybind = "ctrl+d"
   const deleteKeybind = "ctrl+d"
 
 
-  const currentSessionID = createMemo(() =>
-    route.data.type === "session" ? route.data.sessionID : undefined,
-  )
+  const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
 
 
   const options = createMemo(() => {
   const options = createMemo(() => {
     const today = new Date().toDateString()
     const today = new Date().toDateString()

+ 1 - 4
packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx

@@ -77,10 +77,7 @@ export function DialogStatus() {
           </For>
           </For>
         </box>
         </box>
       )}
       )}
-      <Show
-        when={enabledFormatters().length > 0}
-        fallback={<text fg={theme.text}>No Formatters</text>}
-      >
+      <Show when={enabledFormatters().length > 0} fallback={<text fg={theme.text}>No Formatters</text>}>
         <box>
         <box>
           <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
           <text fg={theme.text}>{enabledFormatters().length} Formatters</text>
           <For each={enabledFormatters()}>
           <For each={enabledFormatters()}>

+ 2 - 12
packages/opencode/src/cli/cmd/tui/component/logo.tsx

@@ -3,19 +3,9 @@ import { TextAttributes } from "@opentui/core"
 import { For } from "solid-js"
 import { For } from "solid-js"
 import { useTheme } from "@tui/context/theme"
 import { useTheme } from "@tui/context/theme"
 
 
-const LOGO_LEFT = [
-  `                   `,
-  `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`,
-  `█░░█ █░░█ █▀▀▀ █░░█`,
-  `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀  ▀`,
-]
+const LOGO_LEFT = [`                   `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀  ▀`]
 
 
-const LOGO_RIGHT = [
-  `             ▄     `,
-  `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`,
-  `█░░░ █░░█ █░░█ █▀▀▀`,
-  `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
-]
+const LOGO_RIGHT = [`             ▄     `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`]
 
 
 export function Logo() {
 export function Logo() {
   const { theme } = useTheme()
   const { theme } = useTheme()

+ 7 - 26
packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

@@ -83,12 +83,7 @@ export function Autocomplete(props: {
     const extmarkStart = store.index
     const extmarkStart = store.index
     const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
     const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
 
 
-    const styleId =
-      part.type === "file"
-        ? props.fileStyleId
-        : part.type === "agent"
-          ? props.agentStyleId
-          : undefined
+    const styleId = part.type === "file" ? props.fileStyleId : part.type === "agent" ? props.agentStyleId : undefined
 
 
     const extmarkId = input.extmarks.create({
     const extmarkId = input.extmarks.create({
       start: extmarkStart,
       start: extmarkStart,
@@ -185,9 +180,7 @@ export function Autocomplete(props: {
       )
       )
   })
   })
 
 
-  const session = createMemo(() =>
-    props.sessionID ? sync.session.get(props.sessionID) : undefined,
-  )
+  const session = createMemo(() => (props.sessionID ? sync.session.get(props.sessionID) : undefined))
   const commands = createMemo((): AutocompleteOption[] => {
   const commands = createMemo((): AutocompleteOption[] => {
     const results: AutocompleteOption[] = []
     const results: AutocompleteOption[] = []
     const s = session()
     const s = session()
@@ -324,9 +317,7 @@ export function Autocomplete(props: {
 
 
   const options = createMemo(() => {
   const options = createMemo(() => {
     const mixed: AutocompleteOption[] = (
     const mixed: AutocompleteOption[] = (
-      store.visible === "@"
-        ? [...agents(), ...(files.loading ? files.latest || [] : files())]
-        : [...commands()]
+      store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
     ).filter((x) => x.disabled !== true)
     ).filter((x) => x.disabled !== true)
     const currentFilter = filter()
     const currentFilter = filter()
     if (!currentFilter) return mixed.slice(0, 10)
     if (!currentFilter) return mixed.slice(0, 10)
@@ -393,9 +384,7 @@ export function Autocomplete(props: {
             return
             return
           }
           }
           // Check if a space was typed after the trigger character
           // Check if a space was typed after the trigger character
-          const currentText = props
-            .input()
-            .getTextRange(store.index + 1, props.input().cursorOffset + 1)
+          const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
           if (currentText.includes(" ")) {
           if (currentText.includes(" ")) {
             hide()
             hide()
           }
           }
@@ -433,13 +422,8 @@ export function Autocomplete(props: {
           if (e.name === "@") {
           if (e.name === "@") {
             const cursorOffset = props.input().cursorOffset
             const cursorOffset = props.input().cursorOffset
             const charBeforeCursor =
             const charBeforeCursor =
-              cursorOffset === 0
-                ? undefined
-                : props.input().getTextRange(cursorOffset - 1, cursorOffset)
-            const canTrigger =
-              charBeforeCursor === undefined ||
-              charBeforeCursor === "" ||
-              /\s/.test(charBeforeCursor)
+              cursorOffset === 0 ? undefined : props.input().getTextRange(cursorOffset - 1, cursorOffset)
+            const canTrigger = charBeforeCursor === undefined || charBeforeCursor === "" || /\s/.test(charBeforeCursor)
             if (canTrigger) show("@")
             if (canTrigger) show("@")
           }
           }
 
 
@@ -487,10 +471,7 @@ export function Autocomplete(props: {
                 {option.display}
                 {option.display}
               </text>
               </text>
               <Show when={option.description}>
               <Show when={option.description}>
-                <text
-                  fg={index() === store.selected ? theme.background : theme.textMuted}
-                  wrapMode="none"
-                >
+                <text fg={index() === store.selected ? theme.background : theme.textMuted} wrapMode="none">
                   {option.description}
                   {option.description}
                 </text>
                 </text>
               </Show>
               </Show>

+ 10 - 39
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -324,9 +324,7 @@ export function Prompt(props: PromptProps) {
 
 
     // Expand pasted text inline before submitting
     // Expand pasted text inline before submitting
     const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
     const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
-    const sortedExtmarks = allExtmarks.sort(
-      (a: { start: number }, b: { start: number }) => b.start - a.start,
-    )
+    const sortedExtmarks = allExtmarks.sort((a: { start: number }, b: { start: number }) => b.start - a.start)
 
 
     for (const extmark of sortedExtmarks) {
     for (const extmark of sortedExtmarks) {
       const partIndex = store.extmarkToPartIndex.get(extmark.id)
       const partIndex = store.extmarkToPartIndex.get(extmark.id)
@@ -489,28 +487,15 @@ export function Prompt(props: PromptProps) {
         <box
         <box
           flexDirection="row"
           flexDirection="row"
           {...SplitBorder}
           {...SplitBorder}
-          borderColor={
-            keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border
-          }
+          borderColor={keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border}
           justifyContent="space-evenly"
           justifyContent="space-evenly"
         >
         >
-          <box
-            backgroundColor={theme.backgroundElement}
-            width={3}
-            height="100%"
-            alignItems="center"
-            paddingTop={1}
-          >
+          <box backgroundColor={theme.backgroundElement} width={3} height="100%" alignItems="center" paddingTop={1}>
             <text attributes={TextAttributes.BOLD} fg={theme.primary}>
             <text attributes={TextAttributes.BOLD} fg={theme.primary}>
               {store.mode === "normal" ? ">" : "!"}
               {store.mode === "normal" ? ">" : "!"}
             </text>
             </text>
           </box>
           </box>
-          <box
-            paddingTop={1}
-            paddingBottom={1}
-            backgroundColor={theme.backgroundElement}
-            flexGrow={1}
-          >
+          <box paddingTop={1} paddingBottom={1} backgroundColor={theme.backgroundElement} flexGrow={1}>
             <textarea
             <textarea
               placeholder={
               placeholder={
                 props.showPlaceholder
                 props.showPlaceholder
@@ -565,10 +550,7 @@ export function Prompt(props: PromptProps) {
                   return
                   return
                 }
                 }
                 if (store.mode === "shell") {
                 if (store.mode === "shell") {
-                  if (
-                    (e.name === "backspace" && input.visualCursor.offset === 0) ||
-                    e.name === "escape"
-                  ) {
+                  if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
                     setStore("mode", "normal")
                     setStore("mode", "normal")
                     e.preventDefault()
                     e.preventDefault()
                     return
                     return
@@ -578,8 +560,7 @@ export function Prompt(props: PromptProps) {
                 if (!autocomplete.visible) {
                 if (!autocomplete.visible) {
                   if (
                   if (
                     (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
                     (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
-                    (keybind.match("history_next", e) &&
-                      input.cursorOffset === input.plainText.length)
+                    (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
                   ) {
                   ) {
                     const direction = keybind.match("history_previous", e) ? -1 : 1
                     const direction = keybind.match("history_previous", e) ? -1 : 1
                     const item = history.move(direction, input.plainText)
                     const item = history.move(direction, input.plainText)
@@ -595,12 +576,8 @@ export function Prompt(props: PromptProps) {
                     return
                     return
                   }
                   }
 
 
-                  if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0)
-                    input.cursorOffset = 0
-                  if (
-                    keybind.match("history_next", e) &&
-                    input.visualCursor.visualRow === input.height - 1
-                  )
+                  if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
+                  if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
                     input.cursorOffset = input.plainText.length
                     input.cursorOffset = input.plainText.length
                 }
                 }
               }}
               }}
@@ -691,12 +668,7 @@ export function Prompt(props: PromptProps) {
               syntaxStyle={syntax()}
               syntaxStyle={syntax()}
             />
             />
           </box>
           </box>
-          <box
-            backgroundColor={theme.backgroundElement}
-            width={1}
-            justifyContent="center"
-            alignItems="center"
-          ></box>
+          <box backgroundColor={theme.backgroundElement} width={1} justifyContent="center" alignItems="center"></box>
         </box>
         </box>
         <box flexDirection="row" justifyContent="space-between">
         <box flexDirection="row" justifyContent="space-between">
           <text flexShrink={0} wrapMode="none" fg={theme.text}>
           <text flexShrink={0} wrapMode="none" fg={theme.text}>
@@ -717,8 +689,7 @@ export function Prompt(props: PromptProps) {
             <Match when={props.hint}>{props.hint!}</Match>
             <Match when={props.hint}>{props.hint!}</Match>
             <Match when={true}>
             <Match when={true}>
               <text fg={theme.text}>
               <text fg={theme.text}>
-                {keybind.print("command_list")}{" "}
-                <span style={{ fg: theme.textMuted }}>commands</span>
+                {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
               </text>
               </text>
             </Match>
             </Match>
           </Switch>
           </Switch>

+ 2 - 6
packages/opencode/src/cli/cmd/tui/context/local.tsx

@@ -22,9 +22,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       return !!provider?.models[model.modelID]
       return !!provider?.models[model.modelID]
     }
     }
 
 
-    function getFirstValidModel(
-      ...modelFns: (() => { providerID: string; modelID: string } | undefined)[]
-    ) {
+    function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
       for (const modelFn of modelFns) {
       for (const modelFn of modelFns) {
         const model = modelFn()
         const model = modelFn()
         if (!model) continue
         if (!model) continue
@@ -195,9 +193,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           const current = currentModel()
           const current = currentModel()
           if (!current) return
           if (!current) return
           const recent = modelStore.recent
           const recent = modelStore.recent
-          const index = recent.findIndex(
-            (x) => x.providerID === current.providerID && x.modelID === current.modelID,
-          )
+          const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
           if (index === -1) return
           if (index === -1) return
           let next = index + direction
           let next = index + direction
           if (next < 0) next = recent.length - 1
           if (next < 0) next = recent.length - 1

+ 2 - 12
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -146,12 +146,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
           }
           }
           const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
           const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
           if (result.found) {
           if (result.found) {
-            setStore(
-              "message",
-              event.properties.info.sessionID,
-              result.index,
-              reconcile(event.properties.info),
-            )
+            setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
             break
             break
           }
           }
           setStore(
           setStore(
@@ -186,12 +181,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
           }
           }
           const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
           const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
           if (result.found) {
           if (result.found) {
-            setStore(
-              "part",
-              event.properties.part.messageID,
-              result.index,
-              reconcile(event.properties.part),
-            )
+            setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
             break
             break
           }
           }
           setStore(
           setStore(

+ 2 - 8
packages/opencode/src/cli/cmd/tui/context/theme.tsx

@@ -196,7 +196,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
 function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
 function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
   const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
   const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
   const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
   const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
-  const palette = colors.palette.map((x) => RGBA.fromHex(x!))
+  const palette = colors.palette.filter((x) => x !== null).map((x) => RGBA.fromHex(x))
   const isDark = mode == "dark"
   const isDark = mode == "dark"
 
 
   // Generate gray scale based on terminal background
   // Generate gray scale based on terminal background
@@ -528,13 +528,7 @@ function generateSyntax(theme: Theme) {
       },
       },
     },
     },
     {
     {
-      scope: [
-        "variable.builtin",
-        "type.builtin",
-        "function.builtin",
-        "module.builtin",
-        "constant.builtin",
-      ],
+      scope: ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin"],
       style: {
       style: {
         foreground: theme.error,
         foreground: theme.error,
       },
       },

+ 2 - 13
packages/opencode/src/cli/cmd/tui/routes/home.tsx

@@ -30,11 +30,7 @@ export function Home() {
             </Match>
             </Match>
             <Match when={true}>
             <Match when={true}>
               <span style={{ fg: theme.success }}>•</span>{" "}
               <span style={{ fg: theme.success }}>•</span>{" "}
-              {Locale.pluralize(
-                Object.values(sync.data.mcp).length,
-                "{} mcp server",
-                "{} mcp servers",
-              )}
+              {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")}
             </Match>
             </Match>
           </Switch>
           </Switch>
         </text>
         </text>
@@ -53,14 +49,7 @@ export function Home() {
   })
   })
 
 
   return (
   return (
-    <box
-      flexGrow={1}
-      justifyContent="center"
-      alignItems="center"
-      paddingLeft={2}
-      paddingRight={2}
-      gap={1}
-    >
+    <box flexGrow={1} justifyContent="center" alignItems="center" paddingLeft={2} paddingRight={2} gap={1}>
       <Logo />
       <Logo />
       <box width={39}>
       <box width={39}>
         <HelpRow keybind="command_list">Commands</HelpRow>
         <HelpRow keybind="command_list">Commands</HelpRow>

+ 1 - 3
packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx

@@ -7,9 +7,7 @@ import { useRoute } from "@tui/context/route"
 export function DialogMessage(props: { messageID: string; sessionID: string }) {
 export function DialogMessage(props: { messageID: string; sessionID: string }) {
   const sync = useSync()
   const sync = useSync()
   const sdk = useSDK()
   const sdk = useSDK()
-  const message = createMemo(() =>
-    sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID),
-  )
+  const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
   const route = useRoute()
   const route = useRoute()
 
 
   return (
   return (

+ 2 - 10
packages/opencode/src/cli/cmd/tui/routes/session/dialog-timeline.tsx

@@ -19,9 +19,7 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
     const result = [] as DialogSelectOption<string>[]
     const result = [] as DialogSelectOption<string>[]
     for (const message of messages) {
     for (const message of messages) {
       if (message.role !== "user") continue
       if (message.role !== "user") continue
-      const part = (sync.data.part[message.id] ?? []).find(
-        (x) => x.type === "text" && !x.synthetic,
-      ) as TextPart
+      const part = (sync.data.part[message.id] ?? []).find((x) => x.type === "text" && !x.synthetic) as TextPart
       if (!part) continue
       if (!part) continue
       result.push({
       result.push({
         title: part.text.replace(/\n/g, " "),
         title: part.text.replace(/\n/g, " "),
@@ -35,11 +33,5 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
     return result
     return result
   })
   })
 
 
-  return (
-    <DialogSelect
-      onMove={(option) => props.onMove(option.value)}
-      title="Timeline"
-      options={options()}
-    />
-  )
+  return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Timeline" options={options()} />
 }
 }

+ 3 - 15
packages/opencode/src/cli/cmd/tui/routes/session/header.tsx

@@ -46,16 +46,10 @@ export function Header() {
   })
   })
 
 
   const context = createMemo(() => {
   const context = createMemo(() => {
-    const last = messages().findLast(
-      (x) => x.role === "assistant" && x.tokens.output > 0,
-    ) as AssistantMessage
+    const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
     if (!last) return
     if (!last) return
     const total =
     const total =
-      last.tokens.input +
-      last.tokens.output +
-      last.tokens.reasoning +
-      last.tokens.cache.read +
-      last.tokens.cache.write
+      last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
     const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
     const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
     let result = total.toLocaleString()
     let result = total.toLocaleString()
     if (model?.limit.context) {
     if (model?.limit.context) {
@@ -67,13 +61,7 @@ export function Header() {
   const { theme } = useTheme()
   const { theme } = useTheme()
 
 
   return (
   return (
-    <box
-      paddingLeft={1}
-      paddingRight={1}
-      {...SplitBorder}
-      borderColor={theme.backgroundElement}
-      flexShrink={0}
-    >
+    <box paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={theme.backgroundElement} flexShrink={0}>
       <Show
       <Show
         when={shareEnabled()}
         when={shareEnabled()}
         fallback={
         fallback={

+ 26 - 103
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -19,14 +19,7 @@ import { SplitBorder } from "@tui/component/border"
 import { useTheme } from "@tui/context/theme"
 import { useTheme } from "@tui/context/theme"
 import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
 import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core"
 import { Prompt, type PromptRef } from "@tui/component/prompt"
 import { Prompt, type PromptRef } from "@tui/component/prompt"
-import type {
-  AssistantMessage,
-  Part,
-  ToolPart,
-  UserMessage,
-  TextPart,
-  ReasoningPart,
-} from "@opencode-ai/sdk"
+import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk"
 import { useLocal } from "@tui/context/local"
 import { useLocal } from "@tui/context/local"
 import { Locale } from "@/util/locale"
 import { Locale } from "@/util/locale"
 import type { Tool } from "@/tool/tool"
 import type { Tool } from "@/tool/tool"
@@ -41,13 +34,7 @@ import type { EditTool } from "@/tool/edit"
 import type { PatchTool } from "@/tool/patch"
 import type { PatchTool } from "@/tool/patch"
 import type { WebFetchTool } from "@/tool/webfetch"
 import type { WebFetchTool } from "@/tool/webfetch"
 import type { TaskTool } from "@/tool/task"
 import type { TaskTool } from "@/tool/task"
-import {
-  useKeyboard,
-  useRenderer,
-  useTerminalDimensions,
-  type BoxProps,
-  type JSX,
-} from "@opentui/solid"
+import { useKeyboard, useRenderer, useTerminalDimensions, type BoxProps, type JSX } from "@opentui/solid"
 import { useSDK } from "@tui/context/sdk"
 import { useSDK } from "@tui/context/sdk"
 import { useCommandDialog } from "@tui/component/dialog-command"
 import { useCommandDialog } from "@tui/component/dialog-command"
 import { Shimmer } from "@tui/ui/shimmer"
 import { Shimmer } from "@tui/ui/shimmer"
@@ -653,14 +640,7 @@ export function Session() {
         conceal,
         conceal,
       }}
       }}
     >
     >
-      <box
-        flexDirection="row"
-        paddingBottom={1}
-        paddingTop={1}
-        paddingLeft={2}
-        paddingRight={2}
-        gap={2}
-      >
+      <box flexDirection="row" paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={2}>
         <box flexGrow={1} gap={1}>
         <box flexGrow={1} gap={1}>
           <Show when={session()}>
           <Show when={session()}>
             <Show when={session().parentID}>
             <Show when={session().parentID}>
@@ -675,19 +655,13 @@ export function Session() {
                 paddingRight={2}
                 paddingRight={2}
               >
               >
                 <text fg={theme.text}>
                 <text fg={theme.text}>
-                  Previous{" "}
-                  <span style={{ fg: theme.textMuted }}>
-                    {keybind.print("session_child_cycle_reverse")}
-                  </span>
+                  Previous <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
                 </text>
                 </text>
                 <text fg={theme.text}>
                 <text fg={theme.text}>
                   <b>Viewing subagent session</b>
                   <b>Viewing subagent session</b>
                 </text>
                 </text>
                 <text fg={theme.text}>
                 <text fg={theme.text}>
-                  <span style={{ fg: theme.textMuted }}>
-                    {keybind.print("session_child_cycle")}
-                  </span>{" "}
-                  Next
+                  <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span> Next
                 </text>
                 </text>
               </box>
               </box>
             </Show>
             </Show>
@@ -743,18 +717,12 @@ export function Session() {
                               paddingTop={1}
                               paddingTop={1}
                               paddingBottom={1}
                               paddingBottom={1}
                               paddingLeft={2}
                               paddingLeft={2}
-                              backgroundColor={
-                                hover() ? theme.backgroundElement : theme.backgroundPanel
-                              }
+                              backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel}
                             >
                             >
+                              <text fg={theme.textMuted}>{revert()!.reverted.length} message reverted</text>
                               <text fg={theme.textMuted}>
                               <text fg={theme.textMuted}>
-                                {revert()!.reverted.length} message reverted
-                              </text>
-                              <text fg={theme.textMuted}>
-                                <span style={{ fg: theme.text }}>
-                                  {keybind.print("messages_redo")}
-                                </span>{" "}
-                                or /redo to restore
+                                <span style={{ fg: theme.text }}>{keybind.print("messages_redo")}</span> or /redo to
+                                restore
                               </text>
                               </text>
                               <Show when={revert()!.diffFiles?.length}>
                               <Show when={revert()!.diffFiles?.length}>
                                 <box marginTop={1}>
                                 <box marginTop={1}>
@@ -763,16 +731,10 @@ export function Session() {
                                       <text>
                                       <text>
                                         {file.filename}
                                         {file.filename}
                                         <Show when={file.additions > 0}>
                                         <Show when={file.additions > 0}>
-                                          <span style={{ fg: theme.diffAdded }}>
-                                            {" "}
-                                            +{file.additions}
-                                          </span>
+                                          <span style={{ fg: theme.diffAdded }}> +{file.additions}</span>
                                         </Show>
                                         </Show>
                                         <Show when={file.deletions > 0}>
                                         <Show when={file.deletions > 0}>
-                                          <span style={{ fg: theme.diffRemoved }}>
-                                            {" "}
-                                            -{file.deletions}
-                                          </span>
+                                          <span style={{ fg: theme.diffRemoved }}> -{file.deletions}</span>
                                         </Show>
                                         </Show>
                                       </text>
                                       </text>
                                     )}
                                     )}
@@ -792,9 +754,7 @@ export function Session() {
                         index={index()}
                         index={index()}
                         onMouseUp={() => {
                         onMouseUp={() => {
                           if (renderer.getSelection()?.getSelectedText()) return
                           if (renderer.getSelection()?.getSelectedText()) return
-                          dialog.replace(() => (
-                            <DialogMessage messageID={message.id} sessionID={route.sessionID} />
-                          ))
+                          dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
                         }}
                         }}
                         message={message as UserMessage}
                         message={message as UserMessage}
                         parts={sync.data.part[message.id] ?? []}
                         parts={sync.data.part[message.id] ?? []}
@@ -850,9 +810,7 @@ function UserMessage(props: {
   index: number
   index: number
   pending?: string
   pending?: string
 }) {
 }) {
-  const text = createMemo(
-    () => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0],
-  )
+  const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0])
   const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
   const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : [])))
   const sync = useSync()
   const sync = useSync()
   const { theme } = useTheme()
   const { theme } = useTheme()
@@ -893,14 +851,8 @@ function UserMessage(props: {
                 })
                 })
                 return (
                 return (
                   <text fg={theme.text}>
                   <text fg={theme.text}>
-                    <span style={{ bg: bg(), fg: theme.background }}>
-                      {" "}
-                      {MIME_BADGE[file.mime] ?? file.mime}{" "}
-                    </span>
-                    <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}>
-                      {" "}
-                      {file.filename}{" "}
-                    </span>
+                    <span style={{ bg: bg(), fg: theme.background }}> {MIME_BADGE[file.mime] ?? file.mime} </span>
+                    <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.filename} </span>
                   </text>
                   </text>
                 )
                 )
               }}
               }}
@@ -911,16 +863,9 @@ function UserMessage(props: {
           {sync.data.config.username ?? "You"}{" "}
           {sync.data.config.username ?? "You"}{" "}
           <Show
           <Show
             when={queued()}
             when={queued()}
-            fallback={
-              <span style={{ fg: theme.textMuted }}>
-                ({Locale.time(props.message.time.created)})
-              </span>
-            }
+            fallback={<span style={{ fg: theme.textMuted }}>({Locale.time(props.message.time.created)})</span>}
           >
           >
-            <span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}>
-              {" "}
-              QUEUED{" "}
-            </span>
+            <span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
           </Show>
           </Show>
         </text>
         </text>
       </box>
       </box>
@@ -960,8 +905,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
       <Show
       <Show
         when={
         when={
           !props.message.time.completed ||
           !props.message.time.completed ||
-          (props.last &&
-            props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
+          (props.last && props.parts.some((item) => item.type === "step-finish" && item.reason === "tool-calls"))
         }
         }
       >
       >
         <box
         <box
@@ -973,9 +917,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
           customBorderChars={SplitBorder.customBorderChars}
           customBorderChars={SplitBorder.customBorderChars}
           borderColor={theme.backgroundElement}
           borderColor={theme.backgroundElement}
         >
         >
-          <text fg={local.agent.color(props.message.mode)}>
-            {Locale.titlecase(props.message.mode)}
-          </text>
+          <text fg={local.agent.color(props.message.mode)}>{Locale.titlecase(props.message.mode)}</text>
           <Shimmer text={`${props.message.modelID}`} color={theme.text} />
           <Shimmer text={`${props.message.modelID}`} color={theme.text} />
         </box>
         </box>
       </Show>
       </Show>
@@ -987,9 +929,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
       >
       >
         <box paddingLeft={3}>
         <box paddingLeft={3}>
           <text marginTop={1}>
           <text marginTop={1}>
-            <span style={{ fg: local.agent.color(props.message.mode) }}>
-              {Locale.titlecase(props.message.mode)}
-            </span>{" "}
+            <span style={{ fg: local.agent.color(props.message.mode) }}>{Locale.titlecase(props.message.mode)}</span>{" "}
             <span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
             <span style={{ fg: theme.textMuted }}>{props.message.modelID}</span>
           </text>
           </text>
         </box>
         </box>
@@ -1016,12 +956,7 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }
         customBorderChars={SplitBorder.customBorderChars}
         customBorderChars={SplitBorder.customBorderChars}
         borderColor={theme.backgroundPanel}
         borderColor={theme.backgroundPanel}
       >
       >
-        <box
-          paddingTop={1}
-          paddingBottom={1}
-          paddingLeft={2}
-          backgroundColor={theme.backgroundPanel}
-        >
+        <box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
           <text fg={theme.text}>{props.part.text.trim()}</text>
           <text fg={theme.text}>{props.part.text.trim()}</text>
         </box>
         </box>
       </box>
       </box>
@@ -1261,16 +1196,10 @@ ToolRegistry.register<typeof WriteTool>({
         </ToolTitle>
         </ToolTitle>
         <box flexDirection="row">
         <box flexDirection="row">
           <box flexShrink={0}>
           <box flexShrink={0}>
-            <For each={numbers()}>
-              {(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}
-            </For>
+            <For each={numbers()}>{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}</For>
           </box>
           </box>
           <box paddingLeft={1} flexGrow={1}>
           <box paddingLeft={1} flexGrow={1}>
-            <code
-              filetype={filetype(props.input.filePath!)}
-              syntaxStyle={syntax()}
-              content={code()}
-            />
+            <code filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
           </box>
           </box>
         </box>
         </box>
       </>
       </>
@@ -1285,8 +1214,7 @@ ToolRegistry.register<typeof GlobTool>({
     return (
     return (
       <>
       <>
         <ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}>
         <ToolTitle icon="✱" fallback="Finding files..." when={props.input.pattern}>
-          Glob "{props.input.pattern}"{" "}
-          <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
+          Glob "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
           <Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
           <Show when={props.metadata.count}>({props.metadata.count} matches)</Show>
         </ToolTitle>
         </ToolTitle>
       </>
       </>
@@ -1300,8 +1228,7 @@ ToolRegistry.register<typeof GrepTool>({
   render(props) {
   render(props) {
     return (
     return (
       <ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
       <ToolTitle icon="✱" fallback="Searching content..." when={props.input.pattern}>
-        Grep "{props.input.pattern}"{" "}
-        <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
+        Grep "{props.input.pattern}" <Show when={props.input.path}>in {normalizePath(props.input.path)} </Show>
         <Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
         <Show when={props.metadata.matches}>({props.metadata.matches} matches)</Show>
       </ToolTitle>
       </ToolTitle>
     )
     )
@@ -1337,11 +1264,7 @@ ToolRegistry.register<typeof TaskTool>({
 
 
     return (
     return (
       <>
       <>
-        <ToolTitle
-          icon="%"
-          fallback="Delegating..."
-          when={props.input.subagent_type ?? props.input.description}
-        >
+        <ToolTitle icon="%" fallback="Delegating..." when={props.input.subagent_type ?? props.input.description}>
           Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
           Task [{props.input.subagent_type ?? "unknown"}] {props.input.description}
         </ToolTitle>
         </ToolTitle>
         <Show when={props.metadata.summary?.length}>
         <Show when={props.metadata.summary?.length}>

+ 4 - 14
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

@@ -22,16 +22,10 @@ export function Sidebar(props: { sessionID: string }) {
   })
   })
 
 
   const context = createMemo(() => {
   const context = createMemo(() => {
-    const last = messages().findLast(
-      (x) => x.role === "assistant" && x.tokens.output > 0,
-    ) as AssistantMessage
+    const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
     if (!last) return
     if (!last) return
     const total =
     const total =
-      last.tokens.input +
-      last.tokens.output +
-      last.tokens.reasoning +
-      last.tokens.cache.read +
-      last.tokens.cache.write
+      last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
     const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
     const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
     return {
     return {
       tokens: total.toLocaleString(),
       tokens: total.toLocaleString(),
@@ -84,9 +78,7 @@ export function Sidebar(props: { sessionID: string }) {
                       <span style={{ fg: theme.textMuted }}>
                       <span style={{ fg: theme.textMuted }}>
                         <Switch>
                         <Switch>
                           <Match when={item.status === "connected"}>Connected</Match>
                           <Match when={item.status === "connected"}>Connected</Match>
-                          <Match when={item.status === "failed" && item}>
-                            {(val) => <i>{val().error}</i>}
-                          </Match>
+                          <Match when={item.status === "failed" && item}>{(val) => <i>{val().error}</i>}</Match>
                           <Match when={item.status === "disabled"}>Disabled in configuration</Match>
                           <Match when={item.status === "disabled"}>Disabled in configuration</Match>
                         </Switch>
                         </Switch>
                       </span>
                       </span>
@@ -162,9 +154,7 @@ export function Sidebar(props: { sessionID: string }) {
               </text>
               </text>
               <For each={todo()}>
               <For each={todo()}>
                 {(todo) => (
                 {(todo) => (
-                  <text
-                    style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}
-                  >
+                  <text style={{ fg: todo.status === "in_progress" ? theme.success : theme.textMuted }}>
                     [{todo.status === "completed" ? "✓" : " "}] {todo.content}
                     [{todo.status === "completed" ? "✓" : " "}] {todo.content}
                   </text>
                   </text>
                 )}
                 )}

+ 1 - 6
packages/opencode/src/cli/cmd/tui/spawn.ts

@@ -41,12 +41,7 @@ export const TuiSpawnCommand = cmd({
       )
       )
       cwd = new URL("../../../../", import.meta.url).pathname
       cwd = new URL("../../../../", import.meta.url).pathname
     } else cmd.push(process.execPath)
     } else cmd.push(process.execPath)
-    cmd.push(
-      "attach",
-      server.url.toString(),
-      "--dir",
-      args.project ? path.resolve(args.project) : process.cwd(),
-    )
+    cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
     const proc = Bun.spawn({
     const proc = Bun.spawn({
       cmd,
       cmd,
       cwd,
       cwd,

+ 1 - 3
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -71,9 +71,7 @@ export const TuiThreadCommand = cmd({
 
 
     const worker = new Worker(workerPath, {
     const worker = new Worker(workerPath, {
       env: Object.fromEntries(
       env: Object.fromEntries(
-        Object.entries(process.env).filter(
-          (entry): entry is [string, string] => entry[1] !== undefined,
-        ),
+        Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
       ),
       ),
     })
     })
     worker.onerror = console.error
     worker.onerror = console.error

+ 1 - 3
packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx

@@ -53,9 +53,7 @@ export function DialogConfirm(props: DialogConfirmProps) {
                 dialog.clear()
                 dialog.clear()
               }}
               }}
             >
             >
-              <text fg={key === store.active ? theme.background : theme.textMuted}>
-                {Locale.titlecase(key)}
-              </text>
+              <text fg={key === store.active ? theme.background : theme.textMuted}>{Locale.titlecase(key)}</text>
             </box>
             </box>
           )}
           )}
         </For>
         </For>

Некоторые файлы не были показаны из-за большого количества измененных файлов