Преглед на файлове

Merge branch 'dev' into opencode-remote-voice

Brendan Allan преди 3 месеца
родител
ревизия
ce209e22a2
променени са 100 файла, в които са добавени 4370 реда и са изтрити 1085 реда
  1. 59 153
      bun.lock
  2. 0 1
      infra/console.ts
  3. 4 4
      nix/hashes.json
  4. 19 18
      nix/opencode.nix
  5. 1 1
      package.json
  6. 1 1
      packages/app/package.json
  7. BIN
      packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2
  8. 27 0
      packages/app/src/components/settings-general.tsx
  9. 3 3
      packages/app/src/components/terminal.tsx
  10. 18 0
      packages/app/src/context/settings.tsx
  11. 3 1
      packages/app/src/i18n/ar.ts
  12. 3 1
      packages/app/src/i18n/br.ts
  13. 3 1
      packages/app/src/i18n/bs.ts
  14. 3 1
      packages/app/src/i18n/da.ts
  15. 3 1
      packages/app/src/i18n/de.ts
  16. 3 1
      packages/app/src/i18n/en.ts
  17. 3 1
      packages/app/src/i18n/es.ts
  18. 3 1
      packages/app/src/i18n/fr.ts
  19. 3 1
      packages/app/src/i18n/ja.ts
  20. 3 1
      packages/app/src/i18n/ko.ts
  21. 3 1
      packages/app/src/i18n/no.ts
  22. 3 1
      packages/app/src/i18n/pl.ts
  23. 3 1
      packages/app/src/i18n/ru.ts
  24. 3 1
      packages/app/src/i18n/th.ts
  25. 3 1
      packages/app/src/i18n/tr.ts
  26. 3 1
      packages/app/src/i18n/zh.ts
  27. 3 1
      packages/app/src/i18n/zht.ts
  28. 7 0
      packages/app/src/index.css
  29. 1 1
      packages/console/app/package.json
  30. 8 1
      packages/console/app/src/i18n/ar.ts
  31. 8 1
      packages/console/app/src/i18n/br.ts
  32. 8 1
      packages/console/app/src/i18n/da.ts
  33. 8 1
      packages/console/app/src/i18n/de.ts
  34. 8 1
      packages/console/app/src/i18n/en.ts
  35. 8 1
      packages/console/app/src/i18n/es.ts
  36. 8 1
      packages/console/app/src/i18n/fr.ts
  37. 8 1
      packages/console/app/src/i18n/it.ts
  38. 8 1
      packages/console/app/src/i18n/ja.ts
  39. 8 1
      packages/console/app/src/i18n/ko.ts
  40. 8 1
      packages/console/app/src/i18n/no.ts
  41. 8 1
      packages/console/app/src/i18n/pl.ts
  42. 8 1
      packages/console/app/src/i18n/ru.ts
  43. 8 1
      packages/console/app/src/i18n/th.ts
  44. 8 1
      packages/console/app/src/i18n/tr.ts
  45. 8 1
      packages/console/app/src/i18n/zh.ts
  46. 8 1
      packages/console/app/src/i18n/zht.ts
  47. 69 2
      packages/console/app/src/routes/api/enterprise.ts
  48. 7 0
      packages/console/app/src/routes/stripe/webhook.ts
  49. 2 0
      packages/console/app/src/routes/workspace/[id]/billing/index.tsx
  50. 61 0
      packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css
  51. 71 0
      packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx
  52. 2 1
      packages/console/app/src/routes/zen/util/handler.ts
  53. 6 0
      packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql
  54. 2619 0
      packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json
  55. 1 1
      packages/console/core/package.json
  56. 24 0
      packages/console/core/script/create-coupon.ts
  57. 49 5
      packages/console/core/src/billing.ts
  58. 2 6
      packages/console/core/src/lite.ts
  59. 23 1
      packages/console/core/src/schema/billing.sql.ts
  60. 0 4
      packages/console/core/sst-env.d.ts
  61. 1 1
      packages/console/function/package.json
  62. 0 4
      packages/console/function/sst-env.d.ts
  63. 1 1
      packages/console/mail/package.json
  64. 0 4
      packages/console/resource/sst-env.d.ts
  65. 3 2
      packages/desktop-electron/package.json
  66. 26 10
      packages/desktop-electron/src/main/index.ts
  67. 4 4
      packages/desktop-electron/src/main/migrate.ts
  68. 8 8
      packages/desktop-electron/src/main/server.ts
  69. 4 2
      packages/desktop-electron/src/main/store.ts
  70. 1 1
      packages/desktop/package.json
  71. 1 1
      packages/enterprise/package.json
  72. 0 4
      packages/enterprise/sst-env.d.ts
  73. 6 6
      packages/extensions/zed/extension.toml
  74. 1 1
      packages/function/package.json
  75. 0 4
      packages/function/sst-env.d.ts
  76. 2 3
      packages/opencode/package.json
  77. 1 9
      packages/opencode/script/build.ts
  78. 23 8
      packages/opencode/script/publish.ts
  79. 32 49
      packages/opencode/specs/effect/facades.md
  80. 27 27
      packages/opencode/specs/effect/http-api.md
  81. 10 10
      packages/opencode/specs/effect/instance-context.md
  82. 6 8
      packages/opencode/specs/effect/loose-ends.md
  83. 30 40
      packages/opencode/specs/effect/migration.md
  84. 16 18
      packages/opencode/specs/effect/routes.md
  85. 254 28
      packages/opencode/specs/effect/schema.md
  86. 19 17
      packages/opencode/specs/effect/server-package.md
  87. 3 5
      packages/opencode/specs/effect/tools.md
  88. 13 1
      packages/opencode/src/cli/cmd/generate.ts
  89. 0 1
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  90. 18 7
      packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
  91. 2 1
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  92. 96 81
      packages/opencode/src/config/agent.ts
  93. 185 166
      packages/opencode/src/config/config.ts
  94. 124 163
      packages/opencode/src/config/keybinds.ts
  95. 10 0
      packages/opencode/src/config/layout.ts
  96. 45 40
      packages/opencode/src/config/permission.ts
  97. 107 113
      packages/opencode/src/config/provider.ts
  98. 20 0
      packages/opencode/src/config/server.ts
  99. 1 0
      packages/opencode/src/control-plane/workspace.ts
  100. 7 1
      packages/opencode/src/effect/logger.ts

+ 59 - 153
bun.lock

@@ -47,7 +47,7 @@
     },
     "packages/app": {
       "name": "@opencode-ai/app",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -101,7 +101,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -135,7 +135,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -162,7 +162,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@ai-sdk/anthropic": "3.0.64",
         "@ai-sdk/openai": "3.0.48",
@@ -186,7 +186,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -210,7 +210,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@opencode-ai/app": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -243,8 +243,9 @@
     },
     "packages/desktop-electron": {
       "name": "@opencode-ai/desktop-electron",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
+        "drizzle-orm": "catalog:",
         "effect": "catalog:",
         "electron-context-menu": "4.1.2",
         "electron-log": "^5",
@@ -266,7 +267,7 @@
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
         "@valibot/to-json-schema": "1.6.0",
-        "electron": "40.4.1",
+        "electron": "41.2.1",
         "electron-builder": "^26",
         "electron-vite": "^5",
         "solid-js": "catalog:",
@@ -286,7 +287,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@opencode-ai/shared": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -315,7 +316,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "catalog:",
@@ -383,7 +384,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -392,7 +393,7 @@
         "@actions/github": "6.0.1",
         "@agentclientprotocol/sdk": "0.16.1",
         "@ai-sdk/alibaba": "1.0.17",
-        "@ai-sdk/amazon-bedrock": "4.0.95",
+        "@ai-sdk/amazon-bedrock": "4.0.96",
         "@ai-sdk/anthropic": "3.0.71",
         "@ai-sdk/azure": "3.0.49",
         "@ai-sdk/cerebras": "2.0.41",
@@ -475,7 +476,6 @@
         "partial-json": "0.1.7",
         "qrcode": "1.5.4",
         "remeda": "catalog:",
-        "ripgrep": "0.3.1",
         "semver": "^7.6.3",
         "solid-js": "catalog:",
         "strip-ansi": "7.1.2",
@@ -530,7 +530,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "effect": "catalog:",
@@ -565,7 +565,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "cross-spawn": "catalog:",
       },
@@ -580,7 +580,7 @@
     },
     "packages/shared": {
       "name": "@opencode-ai/shared",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -604,7 +604,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -639,7 +639,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -688,7 +688,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.4.10",
+      "version": "1.14.18",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -812,7 +812,7 @@
 
     "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
 
-    "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qJKWEy+cNx3bLSJi/XpIVhv0P8KO0JFB1SvEroNWN8gKm820SIglBmXS10DTeXJdM5PPbQX4i/wJj5BHEk2LRQ=="],
+    "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
 
     "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
 
@@ -1356,21 +1356,17 @@
 
     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
 
-    "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
-
-    "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
-
-    "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+    "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
 
-    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
+    "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="],
 
-    "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
+    "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
 
-    "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+    "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
 
-    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
 
-    "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.31", "", {}, "sha512-IKuqICW5oWpBSsr7zvteSKuPNLkpKVmKaZC6AvFMeMfpAHW606wNI9iAdu1ENMpnYw+RHrhQlQC5OxOl9VNikQ=="],
+    "@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.32", "", {}, "sha512-+RtX6GNteOQEycWKcliKtHYQ3NvFamuD4ZCb7MLwzImkZwxRXjUO1kSVV5FFB2koK5WNYwUCye2KkDp8LFD1SQ=="],
 
     "@expo/cli": ["@expo/cli@55.0.24", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~55.0.15", "@expo/config-plugins": "~55.0.8", "@expo/devcert": "^1.2.1", "@expo/env": "~2.1.1", "@expo/image-utils": "^0.8.13", "@expo/json-file": "^10.0.13", "@expo/log-box": "55.0.10", "@expo/metro": "~55.0.0", "@expo/metro-config": "~55.0.16", "@expo/osascript": "^2.4.2", "@expo/package-manager": "^1.10.4", "@expo/plist": "^0.5.2", "@expo/prebuild-config": "^55.0.15", "@expo/require-utils": "^55.0.4", "@expo/router-server": "^55.0.14", "@expo/schema-utils": "^55.0.3", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.0", "@react-native/dev-middleware": "0.83.4", "accepts": "^1.3.8", "arg": "^5.0.2", "better-opn": "~3.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^55.0.7", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^0.2.3", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.3", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "source-map-support": "~0.5.21", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "build/bin/cli" } }, "sha512-Z6Xh0WNTg1LvoZQ77zO3snF2cFiv1xf0VguDlwTL1Ql87oMOp30f7mjl9jeaSHqoWkgiAbmxgCKKIGjVX/keiA=="],
 
@@ -1494,9 +1490,11 @@
 
     "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
 
-    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+    "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
 
-    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+    "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
+
+    "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
 
     "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 
@@ -2668,6 +2666,8 @@
 
     "@types/emscripten": ["@types/emscripten@1.41.5", "", {}, "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q=="],
 
+    "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
+
     "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 
     "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
@@ -3040,7 +3040,7 @@
 
     "babel-plugin-react-native-web": ["babel-plugin-react-native-web@0.21.2", "", {}, "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA=="],
 
-    "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
+    "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.1", "", { "dependencies": { "hermes-parser": "0.32.1" } }, "sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ=="],
 
     "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="],
 
@@ -3182,8 +3182,6 @@
 
     "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
 
-    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
-
     "camel-case": ["camel-case@4.1.2", "", { "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw=="],
 
     "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
@@ -3488,7 +3486,7 @@
 
     "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
 
-    "electron": ["electron@40.4.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-N1ZXybQZL8kYemO8vAeh9nrk4mSvqlAO8xs0QCHkXIvRnuB/7VGwEehjvQbsU5/f4bmTKpG+2GQERe/zmKpudQ=="],
+    "electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="],
 
     "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
 
@@ -3580,15 +3578,15 @@
 
     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 
-    "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
+    "eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
 
-    "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+    "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
 
-    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+    "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
 
     "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
 
-    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+    "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
 
     "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
 
@@ -3884,7 +3882,7 @@
 
     "getenv": ["getenv@2.0.0", "", {}, "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ=="],
 
-    "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
+    "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#20bd361", {}, "anomalyco-ghostty-web-20bd361", "sha512-dW0nwaiBBcun9y5WJSvm3HxDLe5o9V0xLCndQvWonRVubU8CS1PHxZpLffyPt1YujPWC13ez03aWxcuKBPYYGQ=="],
 
     "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
 
@@ -3902,8 +3900,6 @@
 
     "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
 
-    "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
-
     "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
 
     "globby": ["globby@11.0.4", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.1.1", "ignore": "^5.1.4", "merge2": "^1.3.0", "slash": "^3.0.0" } }, "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg=="],
@@ -4058,8 +4054,6 @@
 
     "immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
 
-    "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
-
     "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
 
     "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
@@ -4384,8 +4378,6 @@
 
     "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
 
-    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
-
     "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
 
     "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
@@ -4844,8 +4836,6 @@
 
     "param-case": ["param-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A=="],
 
-    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
-
     "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="],
 
     "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="],
@@ -5066,7 +5056,7 @@
 
     "react-native-gesture-handler": ["react-native-gesture-handler@2.30.1", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA=="],
 
-    "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
+    "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
 
     "react-native-reanimated": ["react-native-reanimated@4.2.1", "", { "dependencies": { "react-native-is-edge-to-edge": "1.2.1", "semver": "7.7.3" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-worklets": ">=0.7.0" } }, "sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg=="],
 
@@ -5230,8 +5220,6 @@
 
     "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
 
-    "ripgrep": ["ripgrep@0.3.1", "", { "bin": { "rg": "lib/rg.mjs", "ripgrep": "lib/rg.mjs" } }, "sha512-6bDtNIBh1qPviVIU685/4uv0Ap5t8eS4wiJhy/tR2LdIeIey9CVasENlGS+ul3HnTmGANIp7AjnfsztsRmALfQ=="],
-
     "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
 
     "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
@@ -5486,8 +5474,6 @@
 
     "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
 
-    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
-
     "stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
 
     "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
@@ -6282,14 +6268,6 @@
 
     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 
-    "@eslint/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
-
-    "@eslint/eslintrc/ajv": ["ajv@6.14.0", "", { "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-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
-
-    "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
-
-    "@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
-
     "@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
 
     "@expo/cli/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -6330,7 +6308,7 @@
 
     "@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
-    "@expo/metro-config/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+    "@expo/metro-config/hermes-parser": ["hermes-parser@0.32.1", "", { "dependencies": { "hermes-estree": "0.32.1" } }, "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q=="],
 
     "@expo/metro-config/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
 
@@ -6610,6 +6588,8 @@
 
     "@radix-ui/react-visually-hidden/react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
 
+    "@react-native/babel-preset/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
+
     "@react-native/babel-preset/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
 
     "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -6618,12 +6598,6 @@
 
     "@react-native/codegen/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 
-    "@react-native/community-cli-plugin/metro": ["metro@0.83.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.6", "metro-cache": "0.83.6", "metro-cache-key": "0.83.6", "metro-config": "0.83.6", "metro-core": "0.83.6", "metro-file-map": "0.83.6", "metro-resolver": "0.83.6", "metro-runtime": "0.83.6", "metro-source-map": "0.83.6", "metro-symbolicate": "0.83.6", "metro-transform-plugins": "0.83.6", "metro-transform-worker": "0.83.6", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-pbdndsAZ2F/ceopDdhVbttpa/hfLzXPJ/husc+QvQ33R0D9UXJKzTn5+OzOXx4bpQNtAKF2bY88cCI3Zl44xDQ=="],
-
-    "@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.6", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.6", "metro-cache": "0.83.6", "metro-core": "0.83.6", "metro-runtime": "0.83.6", "yaml": "^2.6.1" } }, "sha512-G5622400uNtnAMlppEA5zkFAZltEf7DSGhOu09BkisCxOlVMWfdosD/oPyh4f2YVQsc1MBYyp4w6OzbExTYarg=="],
-
-    "@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.6" } }, "sha512-l+yQ2fuIgR//wszUlMrrAa9+Z+kbKazd0QOh0VQY7jC4ghb7yZBBSla/UMYRBZZ6fPg9IM+wD3+h+37a5f9etw=="],
-
     "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
 
     "@react-native/dev-middleware/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
@@ -6776,7 +6750,7 @@
 
     "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
-    "babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+    "babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.1", "", { "dependencies": { "hermes-estree": "0.32.1" } }, "sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q=="],
 
     "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
 
@@ -6872,20 +6846,12 @@
 
     "eslint/ajv": ["ajv@6.14.0", "", { "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-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
 
-    "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
-    "eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
-
     "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 
     "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 
-    "eslint/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
-
     "eslint-plugin-react-hooks/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
 
-    "espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
-
     "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
 
     "execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
@@ -6956,8 +6922,6 @@
 
     "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
 
-    "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
-
     "istanbul-lib-instrument/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
     "jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -7142,6 +7106,8 @@
 
     "react-native/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
+    "react-native/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
+
     "react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
 
     "react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -7154,6 +7120,8 @@
 
     "react-native/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 
+    "react-native-reanimated/react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
+
     "react-native-reanimated/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
 
     "react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
@@ -7296,7 +7264,7 @@
 
     "zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
 
-    "zxing-wasm/type-fest": ["type-fest@5.5.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="],
+    "zxing-wasm/type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="],
 
     "@actions/artifact/@actions/core/@actions/exec": ["@actions/exec@2.0.0", "", { "dependencies": { "@actions/io": "^2.0.0" } }, "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw=="],
 
@@ -7568,12 +7536,6 @@
 
     "@electron/windows-sign/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
 
-    "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
-
-    "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
-
-    "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
-
     "@expo/cli/npm-package-arg/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
 
     "@expo/cli/npm-package-arg/proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="],
@@ -7586,7 +7548,7 @@
 
     "@expo/config-plugins/xml2js/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
 
-    "@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
+    "@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.32.1", "", {}, "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg=="],
 
     "@expo/metro/metro-source-map/ob1": ["ob1@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg=="],
 
@@ -7872,6 +7834,8 @@
 
     "@radix-ui/react-visually-hidden/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
 
+    "@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+
     "@react-native/codegen/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
 
     "@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
@@ -7880,42 +7844,6 @@
 
     "@react-native/codegen/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 
-    "@react-native/community-cli-plugin/metro/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
-
-    "@react-native/community-cli-plugin/metro/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
-    "@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
-
-    "@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
-
-    "@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.83.6", "nullthrows": "^1.1.1" } }, "sha512-1AnuazBpzY3meRMr04WUw14kRBkV0W3Ez+AA75FAeNpRyWNN5S3M3PHLUbZw7IXq7ZeOzceyRsHStaFrnWd+8w=="],
-
-    "@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.6" } }, "sha512-DpvZE32feNkqfZkI4Fic7YI/Kw8QP9wdl1rC4YKPrA77wQbI9vXbxjmfkCT/EGwBTFOPKqvIXo+H3BNe93YyiQ=="],
-
-    "@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-5gdK4PVpgNOHi7xCGrgesNP1AuOA2TiPqpcirGXZi4RLLzX1VMowpkgTVtBfpQQCqWoosQF9yrSo9/KDQg1eBg=="],
-
-    "@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.6", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-Jg3oN604C7GWbQwFAUXt8KsbMXeKfsxbZ5HFy4XFM3ggTS+ja9QgUmq9B613kgXv3G4M6rwiI6cvh9TRly4x3w=="],
-
-    "@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-lAwR/FsT1uJ5iCt4AIsN3boKfJ88aN8bjvDT5FwBS0tKeKw4/sbdSTWlFxc7W/MUTN5RekJ3nQkJRIWsvs28tA=="],
-
-    "@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-4nvkmv9T7ozhprlPwk/+xm0SVPsxly5kYyMHdNaOlFemFz4df9BanvD46Ac6OISu/4Idinzfk2KVb++6OfzPAQ=="],
-
-    "@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-V+zoY2Ul0v0BW6IokJkTud3raXmDdbdwkUQ/5eiSoy0jKuKMhrDjdH+H5buCS5iiJdNbykOn69Eip+Sqymkodg=="],
-
-    "@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.6", "metro-babel-transformer": "0.83.6", "metro-cache": "0.83.6", "metro-cache-key": "0.83.6", "metro-minify-terser": "0.83.6", "metro-source-map": "0.83.6", "metro-transform-plugins": "0.83.6", "nullthrows": "^1.1.1" } }, "sha512-G5kDJ/P0ZTIf57t3iyAd5qIXbj2Wb1j7WtIDh82uTFQHe2Mq2SO9aXG9j1wI+kxZlIe58Z22XEXIKMl89z0ibQ=="],
-
-    "@react-native/community-cli-plugin/metro/serialize-error": ["serialize-error@2.1.0", "", {}, "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw=="],
-
-    "@react-native/community-cli-plugin/metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
-
-    "@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
-
-    "@react-native/community-cli-plugin/metro/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
-
-    "@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.6" } }, "sha512-DpvZE32feNkqfZkI4Fic7YI/Kw8QP9wdl1rC4YKPrA77wQbI9vXbxjmfkCT/EGwBTFOPKqvIXo+H3BNe93YyiQ=="],
-
-    "@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-lAwR/FsT1uJ5iCt4AIsN3boKfJ88aN8bjvDT5FwBS0tKeKw4/sbdSTWlFxc7W/MUTN5RekJ3nQkJRIWsvs28tA=="],
-
     "@react-native/dev-middleware/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
 
     "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@@ -7992,7 +7920,7 @@
 
     "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
+    "babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.1", "", {}, "sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg=="],
 
     "better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
 
@@ -8042,8 +7970,6 @@
 
     "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
 
-    "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
-
     "expo-router/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
 
     "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -8150,6 +8076,8 @@
 
     "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
 
+    "react-native/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
+
     "react-native/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
 
     "react-native/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
@@ -8348,10 +8276,6 @@
 
     "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
-    "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
-    "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
     "@expo/cli/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
     "@expo/cli/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -8440,6 +8364,8 @@
 
     "@radix-ui/react-tabs/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
 
+    "@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
+
     "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
 
     "@react-native/codegen/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -8450,14 +8376,6 @@
 
     "@react-native/codegen/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
-    "@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
-
-    "@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Vx3/Ne9Q+EIEDLfKzZUOtn/rxSNa/QjlYxc42nvK4Mg8mB6XUgd3LXX5ZZVq7lzQgehgEqLrbgShJPGfeF8PnQ=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
     "@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
@@ -8504,8 +8422,6 @@
 
     "esbuild-plugin-copy/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
 
-    "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
     "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
     "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
@@ -8578,6 +8494,8 @@
 
     "qrcode/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
 
+    "react-native/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
+
     "react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
 
     "react-native/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -8668,14 +8586,6 @@
 
     "@react-native/codegen/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
-    "@react-native/community-cli-plugin/metro/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
@@ -8742,10 +8652,6 @@
 
     "@electron/rebuild/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
 
-    "@react-native/community-cli-plugin/metro/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "@react-native/community-cli-plugin/metro/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
     "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
 
     "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],

+ 0 - 1
infra/console.ts

@@ -236,7 +236,6 @@ new sst.cloudflare.x.SolidStart("Console", {
     SALESFORCE_INSTANCE_URL,
     ZEN_BLACK_PRICE,
     ZEN_LITE_PRICE,
-    new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
     new sst.Secret("ZEN_LIMITS"),
     new sst.Secret("ZEN_SESSION_SECRET"),
     ...ZEN_MODELS,

+ 4 - 4
nix/hashes.json

@@ -1,8 +1,8 @@
 {
   "nodeModules": {
-    "x86_64-linux": "sha256-GjpBQhvGLTM6NWX29b/mS+KjrQPl0w9VjQHH5jaK9SM=",
-    "aarch64-linux": "sha256-F5h9p+iZ8CASdUYaYR7O22NwBRa/iT+ZinUxO8lbPTc=",
-    "aarch64-darwin": "sha256-jWo5yvCtjVKRf9i5XUcTTaLtj2+G6+T1Td2llO/cT5I=",
-    "x86_64-darwin": "sha256-LzV+5/8P2mkiFHmt+a8zDeJjRbU8z9nssSA4tzv1HxA="
+    "x86_64-linux": "sha256-i9TxYwWkJAR+kW6pbvhgQbRW9UYPtdrPQAGic4zPoa4=",
+    "aarch64-linux": "sha256-RYc/OYlETXUwkWBRDas+/P4cBW6zde4FqxxnMARu5vs=",
+    "aarch64-darwin": "sha256-jIhUOIRIQEa2WT62TVIedmRIhl/edhK8sbiAFvU3yCM=",
+    "x86_64-darwin": "sha256-xLGzaX7OofFlZzVgpORJR5QXD2u+54hp+t3cCfUtO84="
   }
 }

+ 19 - 18
nix/opencode.nix

@@ -7,6 +7,7 @@
   sysctl,
   makeBinaryWrapper,
   models-dev,
+  ripgrep,
   installShellFiles,
   versionCheckHook,
   writableTmpDirAsHomeHook,
@@ -51,25 +52,25 @@ stdenvNoCC.mkDerivation (finalAttrs: {
     runHook postBuild
   '';
 
-  installPhase =
-    ''
-      runHook preInstall
-
-      install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
-      install -Dm644 schema.json $out/share/opencode/schema.json
-    ''
-    # bun runs sysctl to detect if dunning on rosetta2
-    + lib.optionalString stdenvNoCC.hostPlatform.isDarwin ''
-      wrapProgram $out/bin/opencode \
-        --prefix PATH : ${
-          lib.makeBinPath [
-            sysctl
+  installPhase = ''
+    runHook preInstall
+
+    install -Dm755 dist/opencode-*/bin/opencode $out/bin/opencode
+    install -Dm644 schema.json $out/share/opencode/schema.json
+
+    wrapProgram $out/bin/opencode \
+      --prefix PATH : ${
+        lib.makeBinPath (
+          [
+            ripgrep
           ]
-        }
-    ''
-    + ''
-      runHook postInstall
-    '';
+          # bun runs sysctl to detect if dunning on rosetta2
+          ++ lib.optional stdenvNoCC.hostPlatform.isDarwin sysctl
+        )
+      }
+
+    runHook postInstall
+  '';
 
   postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
     # trick yargs into also generating zsh completions

+ 1 - 1
package.json

@@ -7,7 +7,7 @@
   "packageManager": "bun@1.3.11",
   "scripts": {
     "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
-    "dev:desktop": "bun --cwd packages/desktop tauri dev",
+    "dev:desktop": "bun --cwd packages/desktop-electron dev",
     "dev:web": "bun --cwd packages/app dev",
     "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
     "dev:storybook": "bun --cwd packages/storybook storybook",

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/app",
-  "version": "1.4.10",
+  "version": "1.14.18",
   "description": "",
   "type": "module",
   "exports": {

BIN
packages/app/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2


+ 27 - 0
packages/app/src/components/settings-general.tsx

@@ -19,6 +19,9 @@ import {
   sansDefault,
   sansFontFamily,
   sansInput,
+  terminalDefault,
+  terminalFontFamily,
+  terminalInput,
   useSettings,
 } from "@/context/settings"
 import { decode64 } from "@/utils/base64"
@@ -181,6 +184,7 @@ export const SettingsGeneral: Component = () => {
   const soundOptions = [noneSound, ...SOUND_OPTIONS]
   const mono = () => monoInput(settings.appearance.font())
   const sans = () => sansInput(settings.appearance.uiFont())
+  const terminal = () => terminalInput(settings.appearance.terminalFont())
 
   const soundSelectProps = (
     enabled: () => boolean,
@@ -451,6 +455,29 @@ export const SettingsGeneral: Component = () => {
             />
           </div>
         </SettingsRow>
+
+        <SettingsRow
+          title={language.t("settings.general.row.terminalFont.title")}
+          description={language.t("settings.general.row.terminalFont.description")}
+        >
+          <div class="w-full sm:w-[220px]">
+            <TextField
+              data-action="settings-terminal-font"
+              label={language.t("settings.general.row.terminalFont.title")}
+              hideLabel
+              type="text"
+              value={terminal()}
+              onChange={(value) => settings.appearance.setTerminalFont(value)}
+              placeholder={terminalDefault}
+              spellcheck={false}
+              autocorrect="off"
+              autocomplete="off"
+              autocapitalize="off"
+              class="text-12-regular"
+              style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
+            />
+          </div>
+        </SettingsRow>
       </SettingsList>
     </div>
   )

+ 3 - 3
packages/app/src/components/terminal.tsx

@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { useSDK } from "@/context/sdk"
 import { useServer } from "@/context/server"
-import { monoFontFamily, useSettings } from "@/context/settings"
+import { terminalFontFamily, useSettings } from "@/context/settings"
 import type { LocalPTY } from "@/context/terminal"
 import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
 import { terminalWriter } from "@/utils/terminal-writer"
@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
   })
 
   createEffect(() => {
-    const font = monoFontFamily(settings.appearance.font())
+    const font = terminalFontFamily(settings.appearance.terminalFont())
     if (!term) return
     setOptionIfSupported(term, "fontFamily", font)
     scheduleFit()
@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
         cols: restoreSize?.cols,
         rows: restoreSize?.rows,
         fontSize: 14,
-        fontFamily: monoFontFamily(settings.appearance.font()),
+        fontFamily: terminalFontFamily(settings.appearance.terminalFont()),
         allowTransparency: false,
         convertEol: false,
         theme: terminalColors(),

+ 18 - 0
packages/app/src/context/settings.tsx

@@ -39,6 +39,7 @@ export interface Settings {
     fontSize: number
     mono: string
     sans: string
+    terminal: string
   }
   keybinds: Record<string, string>
   permissions: {
@@ -50,13 +51,17 @@ export interface Settings {
 
 export const monoDefault = "System Mono"
 export const sansDefault = "System Sans"
+export const terminalDefault = "JetBrainsMono Nerd Font Mono"
 
 const monoFallback =
   'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
 const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
+const terminalFallback =
+  '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
 
 const monoBase = monoFallback
 const sansBase = sansFallback
+const terminalBase = terminalFallback
 
 function input(font: string | undefined) {
   return font ?? ""
@@ -89,6 +94,14 @@ export function sansFontFamily(font: string | undefined) {
   return stack(font, sansBase)
 }
 
+export function terminalInput(font: string | undefined) {
+  return input(font)
+}
+
+export function terminalFontFamily(font: string | undefined) {
+  return stack(font, terminalBase)
+}
+
 const defaultSettings: Settings = {
   general: {
     autoSave: true,
@@ -110,6 +123,7 @@ const defaultSettings: Settings = {
     fontSize: 14,
     mono: "",
     sans: "",
+    terminal: "",
   },
   keybinds: {},
   permissions: {
@@ -233,6 +247,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
         setUIFont(value: string) {
           setStore("appearance", "sans", value.trim() ? value : "")
         },
+        terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal),
+        setTerminalFont(value: string) {
+          setStore("appearance", "terminal", value.trim() ? value : "")
+        },
       },
       keybinds: {
         get: (action: string) => store.keybinds?.[action],

+ 3 - 1
packages/app/src/i18n/ar.ts

@@ -565,7 +565,9 @@ export const dict = {
   "settings.general.row.theme.title": "السمة",
   "settings.general.row.theme.description": "تخصيص سمة OpenCode.",
   "settings.general.row.font.title": "خط الكود",
-  "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية والطرفيات",
+  "settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "خط الواجهة",
   "settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
   "settings.general.row.followup.title": "سلوك المتابعة",

+ 3 - 1
packages/app/src/i18n/br.ts

@@ -572,7 +572,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
   "settings.general.row.font.title": "Fonte de código",
-  "settings.general.row.font.description": "Personalize a fonte usada em blocos de código e terminais",
+  "settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Fonte da interface",
   "settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
   "settings.general.row.followup.title": "Comportamento de acompanhamento",

+ 3 - 1
packages/app/src/i18n/bs.ts

@@ -637,7 +637,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
   "settings.general.row.font.title": "Font za kod",
-  "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda i terminalima",
+  "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI font",
   "settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
   "settings.general.row.followup.title": "Ponašanje nadovezivanja",

+ 3 - 1
packages/app/src/i18n/da.ts

@@ -632,7 +632,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
   "settings.general.row.font.title": "Kode-skrifttype",
-  "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke og terminaler",
+  "settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI-skrifttype",
   "settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
   "settings.general.row.followup.title": "Opfølgningsadfærd",

+ 3 - 1
packages/app/src/i18n/de.ts

@@ -582,7 +582,9 @@ export const dict = {
   "settings.general.row.theme.title": "Thema",
   "settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
   "settings.general.row.font.title": "Code-Schriftart",
-  "settings.general.row.font.description": "Die in Codeblöcken und Terminals verwendete Schriftart anpassen",
+  "settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI-Schriftart",
   "settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
   "settings.general.row.followup.title": "Verhalten bei Folgefragen",

+ 3 - 1
packages/app/src/i18n/en.ts

@@ -736,7 +736,9 @@ export const dict = {
   "settings.general.row.theme.title": "Theme",
   "settings.general.row.theme.description": "Customise how OpenCode is themed.",
   "settings.general.row.font.title": "Code Font",
-  "settings.general.row.font.description": "Customise the font used in code blocks and terminals",
+  "settings.general.row.font.description": "Customise the font used in code blocks",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI Font",
   "settings.general.row.uiFont.description": "Customise the font used throughout the interface",
   "settings.general.row.followup.title": "Follow-up behavior",

+ 3 - 1
packages/app/src/i18n/es.ts

@@ -640,7 +640,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
   "settings.general.row.font.title": "Fuente de código",
-  "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código y terminales",
+  "settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Fuente de la interfaz",
   "settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
   "settings.general.row.followup.title": "Comportamiento de seguimiento",

+ 3 - 1
packages/app/src/i18n/fr.ts

@@ -579,7 +579,9 @@ export const dict = {
   "settings.general.row.theme.title": "Thème",
   "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
   "settings.general.row.font.title": "Police de code",
-  "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code et les terminaux",
+  "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Police de l'interface",
   "settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
   "settings.general.row.followup.title": "Comportement de suivi",

+ 3 - 1
packages/app/src/i18n/ja.ts

@@ -569,7 +569,9 @@ export const dict = {
   "settings.general.row.theme.title": "テーマ",
   "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
   "settings.general.row.font.title": "コードフォント",
-  "settings.general.row.font.description": "コードブロックとターミナルで使用するフォントをカスタマイズします",
+  "settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UIフォント",
   "settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
   "settings.general.row.followup.title": "フォローアップの動作",

+ 3 - 1
packages/app/src/i18n/ko.ts

@@ -566,7 +566,9 @@ export const dict = {
   "settings.general.row.theme.title": "테마",
   "settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
   "settings.general.row.font.title": "코드 글꼴",
-  "settings.general.row.font.description": "코드 블록과 터미널에 사용되는 글꼴을 사용자 지정",
+  "settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI 글꼴",
   "settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
   "settings.general.row.followup.title": "후속 조치 동작",

+ 3 - 1
packages/app/src/i18n/no.ts

@@ -640,7 +640,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
   "settings.general.row.font.title": "Kodefont",
-  "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker og terminaler",
+  "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "UI-skrift",
   "settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
   "settings.general.row.followup.title": "Oppfølgingsadferd",

+ 3 - 1
packages/app/src/i18n/pl.ts

@@ -571,7 +571,9 @@ export const dict = {
   "settings.general.row.theme.title": "Motyw",
   "settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
   "settings.general.row.font.title": "Czcionka kodu",
-  "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu i terminalach",
+  "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Czcionka interfejsu",
   "settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
   "settings.general.row.followup.title": "Zachowanie kontynuacji",

+ 3 - 1
packages/app/src/i18n/ru.ts

@@ -637,7 +637,9 @@ export const dict = {
   "settings.general.row.theme.title": "Тема",
   "settings.general.row.theme.description": "Настройте оформление OpenCode.",
   "settings.general.row.font.title": "Шрифт кода",
-  "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода и терминалах",
+  "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Шрифт интерфейса",
   "settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
   "settings.general.row.followup.title": "Поведение уточняющих вопросов",

+ 3 - 1
packages/app/src/i18n/th.ts

@@ -631,7 +631,9 @@ export const dict = {
   "settings.general.row.theme.title": "ธีม",
   "settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
   "settings.general.row.font.title": "ฟอนต์โค้ด",
-  "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ดและเทอร์มินัล",
+  "settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "ฟอนต์ UI",
   "settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
   "settings.general.row.followup.title": "พฤติกรรมการติดตามผล",

+ 3 - 1
packages/app/src/i18n/tr.ts

@@ -644,7 +644,9 @@ export const dict = {
   "settings.general.row.theme.title": "Tema",
   "settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
   "settings.general.row.font.title": "Kod Yazı Tipi",
-  "settings.general.row.font.description": "Kod bloklarında ve terminallerde kullanılan yazı tipini özelleştirin",
+  "settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
   "settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
   "settings.general.row.followup.title": "Takip davranışı",

+ 3 - 1
packages/app/src/i18n/zh.ts

@@ -631,7 +631,9 @@ export const dict = {
   "settings.general.row.theme.title": "主题",
   "settings.general.row.theme.description": "自定义 OpenCode 的主题。",
   "settings.general.row.font.title": "代码字体",
-  "settings.general.row.font.description": "自定义代码块和终端使用的字体",
+  "settings.general.row.font.description": "自定义代码块使用的字体",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "界面字体",
   "settings.general.row.uiFont.description": "自定义整个界面使用的字体",
   "settings.general.row.followup.title": "跟进消息行为",

+ 3 - 1
packages/app/src/i18n/zht.ts

@@ -626,7 +626,9 @@ export const dict = {
   "settings.general.row.theme.title": "主題",
   "settings.general.row.theme.description": "自訂 OpenCode 的主題。",
   "settings.general.row.font.title": "程式碼字型",
-  "settings.general.row.font.description": "自訂程式碼區塊和終端機使用的字型",
+  "settings.general.row.font.description": "自訂程式碼區塊使用的字型",
+  "settings.general.row.terminalFont.title": "Terminal Font",
+  "settings.general.row.terminalFont.description": "Customise the font used in the terminal",
   "settings.general.row.uiFont.title": "介面字型",
   "settings.general.row.uiFont.description": "自訂整個介面使用的字型",
   "settings.general.row.followup.title": "後續追問行為",

+ 7 - 0
packages/app/src/index.css

@@ -1,5 +1,12 @@
 @import "@opencode-ai/ui/styles/tailwind";
 
+@font-face {
+  font-family: "JetBrainsMono Nerd Font Mono";
+  src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
+  font-weight: normal;
+  font-style: normal;
+}
+
 @layer components {
   @keyframes session-progress-whip {
     0% {

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

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.4.10",
+  "version": "1.14.18",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 8 - 1
packages/console/app/src/i18n/ar.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "المؤسسات",
   "nav.zen": "Zen",
   "nav.login": "تسجيل الدخول",
-  "nav.free": "مجانا",
+  "nav.free": "تحميل",
   "nav.home": "الرئيسية",
   "nav.openMenu": "فتح القائمة",
   "nav.getStartedFree": "ابدأ مجانا",
@@ -558,6 +558,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي لـ",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
 
+  "workspace.redeem.title": "استرداد قسيمة",
+  "workspace.redeem.subtitle": "استرد رمز القسيمة للحصول على رصيد أو مزايا.",
+  "workspace.redeem.placeholder": "أدخل رمز القسيمة",
+  "workspace.redeem.redeem": "استرداد",
+  "workspace.redeem.redeeming": "جارٍ الاسترداد...",
+  "workspace.redeem.success": "تم استرداد القسيمة بنجاح.",
+
   "workspace.reload.title": "إعادة الشحن التلقائي",
   "workspace.reload.disabled.before": "إعادة الشحن التلقائي",
   "workspace.reload.disabled.state": "معطّل",

+ 8 - 1
packages/console/app/src/i18n/br.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Entrar",
-  "nav.free": "Grátis",
+  "nav.free": "Download",
   "nav.home": "Início",
   "nav.openMenu": "Abrir menu",
   "nav.getStartedFree": "Começar grátis",
@@ -567,6 +567,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso atual para",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "é $",
 
+  "workspace.redeem.title": "Resgatar Cupom",
+  "workspace.redeem.subtitle": "Resgate um código de cupom para receber créditos ou vantagens.",
+  "workspace.redeem.placeholder": "Digite o código do cupom",
+  "workspace.redeem.redeem": "Resgatar",
+  "workspace.redeem.redeeming": "Resgatando...",
+  "workspace.redeem.success": "Cupom resgatado com sucesso.",
+
   "workspace.reload.title": "Recarga Automática",
   "workspace.reload.disabled.before": "A recarga automática está",
   "workspace.reload.disabled.state": "desativada",

+ 8 - 1
packages/console/app/src/i18n/da.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Log ind",
-  "nav.free": "Gratis",
+  "nav.free": "Download",
   "nav.home": "Hjem",
   "nav.openMenu": "Åbn menu",
   "nav.getStartedFree": "Kom i gang gratis",
@@ -563,6 +563,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
 
+  "workspace.redeem.title": "Indløs kupon",
+  "workspace.redeem.subtitle": "Indløs en kuponkode for at få kreditter eller fordele.",
+  "workspace.redeem.placeholder": "Indtast kuponkode",
+  "workspace.redeem.redeem": "Indløs",
+  "workspace.redeem.redeeming": "Indløser...",
+  "workspace.redeem.success": "Kuponen blev indløst.",
+
   "workspace.reload.title": "Automatisk genopfyldning",
   "workspace.reload.disabled.before": "Automatisk genopfyldning er",
   "workspace.reload.disabled.state": "deaktiveret",

+ 8 - 1
packages/console/app/src/i18n/de.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Anmelden",
-  "nav.free": "Kostenlos",
+  "nav.free": "Download",
   "nav.home": "Startseite",
   "nav.openMenu": "Menü öffnen",
   "nav.getStartedFree": "Kostenlos starten",
@@ -566,6 +566,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
 
+  "workspace.redeem.title": "Gutschein einlösen",
+  "workspace.redeem.subtitle": "Löse einen Gutscheincode ein, um Guthaben oder Vorteile zu erhalten.",
+  "workspace.redeem.placeholder": "Gutscheincode eingeben",
+  "workspace.redeem.redeem": "Einlösen",
+  "workspace.redeem.redeeming": "Wird eingelöst...",
+  "workspace.redeem.success": "Gutschein erfolgreich eingelöst.",
+
   "workspace.reload.title": "Auto-Reload",
   "workspace.reload.disabled.before": "Auto-Reload ist",
   "workspace.reload.disabled.state": "deaktiviert",

+ 8 - 1
packages/console/app/src/i18n/en.ts

@@ -8,7 +8,7 @@ export const dict = {
   "nav.zen": "Zen",
   "nav.go": "Go",
   "nav.login": "Login",
-  "nav.free": "Free",
+  "nav.free": "Download",
   "nav.home": "Home",
   "nav.openMenu": "Open menu",
   "nav.getStartedFree": "Get started for free",
@@ -559,6 +559,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "is $",
 
+  "workspace.redeem.title": "Redeem Coupon",
+  "workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.",
+  "workspace.redeem.placeholder": "Enter coupon code",
+  "workspace.redeem.redeem": "Redeem",
+  "workspace.redeem.redeeming": "Redeeming...",
+  "workspace.redeem.success": "Coupon redeemed successfully.",
+
   "workspace.reload.title": "Auto Reload",
   "workspace.reload.disabled.before": "Auto reload is",
   "workspace.reload.disabled.state": "disabled",

+ 8 - 1
packages/console/app/src/i18n/es.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Iniciar sesión",
-  "nav.free": "Gratis",
+  "nav.free": "Descargar",
   "nav.home": "Inicio",
   "nav.openMenu": "Abrir menú",
   "nav.getStartedFree": "Empezar gratis",
@@ -567,6 +567,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Uso actual para",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "es $",
 
+  "workspace.redeem.title": "Canjear cupón",
+  "workspace.redeem.subtitle": "Canjea un código de cupón para obtener crédito o beneficios.",
+  "workspace.redeem.placeholder": "Introduce el código del cupón",
+  "workspace.redeem.redeem": "Canjear",
+  "workspace.redeem.redeeming": "Canjeando...",
+  "workspace.redeem.success": "Cupón canjeado correctamente.",
+
   "workspace.reload.title": "Auto Recarga",
   "workspace.reload.disabled.before": "La auto recarga está",
   "workspace.reload.disabled.state": "deshabilitada",

+ 8 - 1
packages/console/app/src/i18n/fr.ts

@@ -12,7 +12,7 @@ export const dict = {
   "nav.enterprise": "Entreprise",
   "nav.zen": "Zen",
   "nav.login": "Se connecter",
-  "nav.free": "Gratuit",
+  "nav.free": "Télécharger",
   "nav.home": "Accueil",
   "nav.openMenu": "Ouvrir le menu",
   "nav.getStartedFree": "Commencer gratuitement",
@@ -569,6 +569,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "L'utilisation actuelle pour",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "est de",
 
+  "workspace.redeem.title": "Utiliser un coupon",
+  "workspace.redeem.subtitle": "Utilisez un code promo pour obtenir du crédit ou des avantages.",
+  "workspace.redeem.placeholder": "Saisissez le code promo",
+  "workspace.redeem.redeem": "Utiliser",
+  "workspace.redeem.redeeming": "Utilisation...",
+  "workspace.redeem.success": "Coupon utilisé avec succès.",
+
   "workspace.reload.title": "Rechargement automatique",
   "workspace.reload.disabled.before": "Le rechargement automatique est",
   "workspace.reload.disabled.state": "désactivé",

+ 8 - 1
packages/console/app/src/i18n/it.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Accedi",
-  "nav.free": "Gratis",
+  "nav.free": "Scarica",
   "nav.home": "Home",
   "nav.openMenu": "Apri menu",
   "nav.getStartedFree": "Inizia gratis",
@@ -565,6 +565,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
 
+  "workspace.redeem.title": "Riscatta Coupon",
+  "workspace.redeem.subtitle": "Riscatta un codice coupon per ottenere credito o vantaggi.",
+  "workspace.redeem.placeholder": "Inserisci il codice coupon",
+  "workspace.redeem.redeem": "Riscatta",
+  "workspace.redeem.redeeming": "Riscatto in corso...",
+  "workspace.redeem.success": "Coupon riscattato con successo.",
+
   "workspace.reload.title": "Ricarica Auto",
   "workspace.reload.disabled.before": "La ricarica auto è",
   "workspace.reload.disabled.state": "disabilitata",

+ 8 - 1
packages/console/app/src/i18n/ja.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "エンタープライズ",
   "nav.zen": "Zen",
   "nav.login": "ログイン",
-  "nav.free": "無料",
+  "nav.free": "ダウンロード",
   "nav.home": "ホーム",
   "nav.openMenu": "メニューを開く",
   "nav.getStartedFree": "無料ではじめる",
@@ -564,6 +564,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "現在の使用状況(",
   "workspace.monthlyLimit.currentUsage.beforeAmount": ")は $",
 
+  "workspace.redeem.title": "クーポンを利用",
+  "workspace.redeem.subtitle": "クーポンコードを利用して、クレジットや特典を受け取ります。",
+  "workspace.redeem.placeholder": "クーポンコードを入力",
+  "workspace.redeem.redeem": "利用する",
+  "workspace.redeem.redeeming": "利用中...",
+  "workspace.redeem.success": "クーポンを利用しました。",
+
   "workspace.reload.title": "自動チャージ",
   "workspace.reload.disabled.before": "自動チャージは",
   "workspace.reload.disabled.state": "無効",

+ 8 - 1
packages/console/app/src/i18n/ko.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "엔터프라이즈",
   "nav.zen": "Zen",
   "nav.login": "로그인",
-  "nav.free": "무료",
+  "nav.free": "다운로드",
   "nav.home": "홈",
   "nav.openMenu": "메뉴 열기",
   "nav.getStartedFree": "무료로 시작하기",
@@ -558,6 +558,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "현재",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "사용량: $",
 
+  "workspace.redeem.title": "쿠폰 사용",
+  "workspace.redeem.subtitle": "쿠폰 코드를 사용해 크레딧이나 혜택을 받으세요.",
+  "workspace.redeem.placeholder": "쿠폰 코드를 입력하세요",
+  "workspace.redeem.redeem": "사용",
+  "workspace.redeem.redeeming": "사용 중...",
+  "workspace.redeem.success": "쿠폰을 성공적으로 사용했습니다.",
+
   "workspace.reload.title": "자동 충전",
   "workspace.reload.disabled.before": "자동 충전이",
   "workspace.reload.disabled.state": "비활성화",

+ 8 - 1
packages/console/app/src/i18n/no.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Logg inn",
-  "nav.free": "Gratis",
+  "nav.free": "Last ned",
   "nav.home": "Hjem",
   "nav.openMenu": "Åpne meny",
   "nav.getStartedFree": "Kom i gang gratis",
@@ -564,6 +564,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Gjeldende forbruk for",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
 
+  "workspace.redeem.title": "Løs inn kupong",
+  "workspace.redeem.subtitle": "Løs inn en kupongkode for å få kreditt eller fordeler.",
+  "workspace.redeem.placeholder": "Skriv inn kupongkode",
+  "workspace.redeem.redeem": "Løs inn",
+  "workspace.redeem.redeeming": "Løser inn...",
+  "workspace.redeem.success": "Kupongen ble løst inn.",
+
   "workspace.reload.title": "Auto-påfyll",
   "workspace.reload.disabled.before": "Auto-påfyll er",
   "workspace.reload.disabled.state": "deaktivert",

+ 8 - 1
packages/console/app/src/i18n/pl.ts

@@ -10,7 +10,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Zaloguj się",
-  "nav.free": "Darmowe",
+  "nav.free": "Pobierz",
   "nav.home": "Strona główna",
   "nav.openMenu": "Otwórz menu",
   "nav.getStartedFree": "Zacznij za darmo",
@@ -565,6 +565,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Aktualne użycie za",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "wynosi $",
 
+  "workspace.redeem.title": "Zrealizuj kupon",
+  "workspace.redeem.subtitle": "Zrealizuj kod kuponu, aby otrzymać środki lub korzyści.",
+  "workspace.redeem.placeholder": "Wpisz kod kuponu",
+  "workspace.redeem.redeem": "Zrealizuj",
+  "workspace.redeem.redeeming": "Realizowanie...",
+  "workspace.redeem.success": "Kupon został zrealizowany.",
+
   "workspace.reload.title": "Automatyczne doładowanie",
   "workspace.reload.disabled.before": "Automatyczne doładowanie jest",
   "workspace.reload.disabled.state": "wyłączone",

+ 8 - 1
packages/console/app/src/i18n/ru.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Enterprise",
   "nav.zen": "Zen",
   "nav.login": "Войти",
-  "nav.free": "Бесплатно",
+  "nav.free": "Скачать",
   "nav.home": "Главная",
   "nav.openMenu": "Открыть меню",
   "nav.getStartedFree": "Начать бесплатно",
@@ -571,6 +571,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Текущее использование за",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "составляет $",
 
+  "workspace.redeem.title": "Активировать купон",
+  "workspace.redeem.subtitle": "Активируйте код купона, чтобы получить кредит или бонусы.",
+  "workspace.redeem.placeholder": "Введите код купона",
+  "workspace.redeem.redeem": "Активировать",
+  "workspace.redeem.redeeming": "Активация...",
+  "workspace.redeem.success": "Купон успешно активирован.",
+
   "workspace.reload.title": "Автопополнение",
   "workspace.reload.disabled.before": "Автопополнение",
   "workspace.reload.disabled.state": "отключено",

+ 8 - 1
packages/console/app/src/i18n/th.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "องค์กร",
   "nav.zen": "Zen",
   "nav.login": "เข้าสู่ระบบ",
-  "nav.free": "ฟรี",
+  "nav.free": "ดาวน์โหลด",
   "nav.home": "หน้าหลัก",
   "nav.openMenu": "เปิดเมนู",
   "nav.getStartedFree": "เริ่มต้นฟรี",
@@ -560,6 +560,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "การใช้งานปัจจุบันสำหรับ",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "คือ $",
 
+  "workspace.redeem.title": "แลกคูปอง",
+  "workspace.redeem.subtitle": "แลกรหัสคูปองเพื่อรับเครดิตหรือสิทธิพิเศษ",
+  "workspace.redeem.placeholder": "กรอกรหัสคูปอง",
+  "workspace.redeem.redeem": "แลก",
+  "workspace.redeem.redeeming": "กำลังแลก...",
+  "workspace.redeem.success": "แลกคูปองสำเร็จ",
+
   "workspace.reload.title": "โหลดซ้ำอัตโนมัติ",
   "workspace.reload.disabled.before": "การโหลดซ้ำอัตโนมัติ",
   "workspace.reload.disabled.state": "ปิดใช้งานอยู่",

+ 8 - 1
packages/console/app/src/i18n/tr.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "Kurumsal",
   "nav.zen": "Zen",
   "nav.login": "Giriş",
-  "nav.free": "Ücretsiz",
+  "nav.free": "İndir",
   "nav.home": "Ana sayfa",
   "nav.openMenu": "Menüyü aç",
   "nav.getStartedFree": "Ücretsiz başla",
@@ -567,6 +567,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "Şu anki kullanım",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "$",
 
+  "workspace.redeem.title": "Kupon Kullan",
+  "workspace.redeem.subtitle": "Kredi veya avantajlardan yararlanmak için bir kupon kodu kullanın.",
+  "workspace.redeem.placeholder": "Kupon kodunu girin",
+  "workspace.redeem.redeem": "Kullan",
+  "workspace.redeem.redeeming": "Kullanılıyor...",
+  "workspace.redeem.success": "Kupon başarıyla kullanıldı.",
+
   "workspace.reload.title": "Otomatik Yeniden Yükleme",
   "workspace.reload.disabled.before": "Otomatik yeniden yükleme:",
   "workspace.reload.disabled.state": "devre dışı",

+ 8 - 1
packages/console/app/src/i18n/zh.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "企业版",
   "nav.zen": "Zen",
   "nav.login": "登录",
-  "nav.free": "免费",
+  "nav.free": "下载",
   "nav.home": "首页",
   "nav.openMenu": "打开菜单",
   "nav.getStartedFree": "免费开始",
@@ -542,6 +542,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
 
+  "workspace.redeem.title": "兑换优惠券",
+  "workspace.redeem.subtitle": "兑换优惠码以领取充值额度或权益。",
+  "workspace.redeem.placeholder": "输入优惠码",
+  "workspace.redeem.redeem": "兑换",
+  "workspace.redeem.redeeming": "兑换中...",
+  "workspace.redeem.success": "优惠券兑换成功。",
+
   "workspace.reload.title": "自动充值",
   "workspace.reload.disabled.before": "自动充值已",
   "workspace.reload.disabled.state": "禁用",

+ 8 - 1
packages/console/app/src/i18n/zht.ts

@@ -11,7 +11,7 @@ export const dict = {
   "nav.enterprise": "企業",
   "nav.zen": "Zen",
   "nav.login": "登入",
-  "nav.free": "免費",
+  "nav.free": "下載",
   "nav.home": "首頁",
   "nav.openMenu": "開啟選單",
   "nav.getStartedFree": "免費開始使用",
@@ -542,6 +542,13 @@ export const dict = {
   "workspace.monthlyLimit.currentUsage.beforeMonth": "目前",
   "workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
 
+  "workspace.redeem.title": "兌換優惠券",
+  "workspace.redeem.subtitle": "兌換優惠碼以領取儲值額度或權益。",
+  "workspace.redeem.placeholder": "輸入優惠碼",
+  "workspace.redeem.redeem": "兌換",
+  "workspace.redeem.redeeming": "兌換中...",
+  "workspace.redeem.success": "優惠券兌換成功。",
+
   "workspace.reload.title": "自動儲值",
   "workspace.reload.disabled.before": "自動儲值已",
   "workspace.reload.disabled.state": "停用",

+ 69 - 2
packages/console/app/src/routes/api/enterprise.ts

@@ -1,5 +1,6 @@
 import type { APIEvent } from "@solidjs/start/server"
 import { AWS } from "@opencode-ai/console-core/aws.js"
+import { Resource } from "@opencode-ai/console-resource"
 import { i18n } from "~/i18n"
 import { localeFromRequest } from "~/lib/language"
 import { createLead } from "~/lib/salesforce"
@@ -14,6 +15,64 @@ interface EnterpriseFormData {
   message: string
 }
 
+const EMAIL_OCTOPUS_LIST_ID = "1b381e5e-39bd-11f1-ba4a-cdd4791f0c43"
+
+function splitFullName(fullName: string) {
+  const parts = fullName
+    .trim()
+    .split(/\s+/)
+    .filter((p) => p.length > 0)
+  if (parts.length === 0) return { firstName: "", lastName: "" }
+  if (parts.length === 1) return { firstName: parts[0], lastName: "" }
+  return { firstName: parts[0], lastName: parts.slice(1).join(" ") }
+}
+
+function getEmailOctopusApiKey() {
+  if (process.env.EMAILOCTOPUS_API_KEY) return process.env.EMAILOCTOPUS_API_KEY
+  try {
+    return Resource.EMAILOCTOPUS_API_KEY.value
+  } catch {
+    return
+  }
+}
+
+function subscribe(email: string, fullName: string) {
+  const apiKey = getEmailOctopusApiKey()
+  if (!apiKey) {
+    console.warn("Skipping EmailOctopus subscribe: missing API key")
+    return Promise.resolve(false)
+  }
+
+  const name = splitFullName(fullName)
+  const fields: Record<string, string> = {}
+  if (name.firstName) fields.FirstName = name.firstName
+  if (name.lastName) fields.LastName = name.lastName
+
+  const payload: { email_address: string; fields?: Record<string, string> } = { email_address: email }
+  if (Object.keys(fields).length) payload.fields = fields
+
+  return fetch(`https://api.emailoctopus.com/lists/${EMAIL_OCTOPUS_LIST_ID}/contacts`, {
+    method: "PUT",
+    headers: {
+      Authorization: `Bearer ${apiKey}`,
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify(payload),
+  }).then(
+    (res) => {
+      if (!res.ok) {
+        console.error("EmailOctopus subscribe failed:", res.status, res.statusText)
+        return false
+      }
+      return true
+    },
+    (err) => {
+      console.error("Failed to subscribe enterprise email:", err)
+      return false
+    },
+  )
+}
+
 export async function POST(event: APIEvent) {
   const dict = i18n(localeFromRequest(event.request))
   try {
@@ -41,7 +100,7 @@ ${body.role}<br>
 ${body.company ? `${body.company}<br>` : ""}${body.email}<br>
 ${body.phone ? `${body.phone}<br>` : ""}`.trim()
 
-    const [lead, mail] = await Promise.all([
+    const [lead, mail, octopus] = await Promise.all([
       createLead({
         name: body.name,
         role: body.role,
@@ -49,6 +108,9 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
         email: body.email,
         phone: body.phone,
         message: body.message,
+      }).catch((err) => {
+        console.error("Failed to create Salesforce lead:", err)
+        return false
       }),
       AWS.sendEmail({
         to: "contact@anoma.ly",
@@ -62,9 +124,14 @@ ${body.phone ? `${body.phone}<br>` : ""}`.trim()
           return false
         },
       ),
+      subscribe(body.email, body.name),
     ])
 
-    if (!lead && !mail) {
+    if (!lead && !mail && !octopus) {
+      if (import.meta.env.DEV) {
+        console.warn("Enterprise inquiry accepted in dev mode without integrations", { email: body.email })
+        return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
+      }
       console.error("Enterprise inquiry delivery failed", { email: body.email })
       return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
     }

+ 7 - 0
packages/console/app/src/routes/stripe/webhook.ts

@@ -9,6 +9,7 @@ import { Actor } from "@opencode-ai/console-core/actor.js"
 import { Resource } from "@opencode-ai/console-resource"
 import { LiteData } from "@opencode-ai/console-core/lite.js"
 import { BlackData } from "@opencode-ai/console-core/black.js"
+import { User } from "@opencode-ai/console-core/user.js"
 
 export async function POST(input: APIEvent) {
   const body = await Billing.stripe().webhooks.constructEventAsync(
@@ -109,6 +110,8 @@ export async function POST(input: APIEvent) {
       if (type === "lite") {
         const workspaceID = body.data.object.metadata?.workspaceID
         const userID = body.data.object.metadata?.userID
+        const userEmail = body.data.object.metadata?.userEmail
+        const coupon = body.data.object.metadata?.coupon
         const customerID = body.data.object.customer as string
         const invoiceID = body.data.object.latest_invoice as string
         const subscriptionID = body.data.object.id as string
@@ -156,6 +159,10 @@ export async function POST(input: APIEvent) {
               id: Identifier.create("lite"),
               userID: userID,
             })
+
+            if (userEmail && coupon === LiteData.firstMonth100Coupon) {
+              await Billing.redeemCoupon(userEmail, "GOFREEMONTH")
+            }
           })
         })
       }

+ 2 - 0
packages/console/app/src/routes/workspace/[id]/billing/index.tsx

@@ -3,6 +3,7 @@ import { BillingSection } from "./billing-section"
 import { ReloadSection } from "./reload-section"
 import { PaymentSection } from "./payment-section"
 import { BlackSection } from "./black-section"
+import { RedeemSection } from "./redeem-section"
 import { createMemo, Show } from "solid-js"
 import { createAsync, useParams } from "@solidjs/router"
 import { queryBillingInfo, querySessionInfo } from "../../common"
@@ -21,6 +22,7 @@ export default function () {
             <BlackSection />
           </Show>
           <BillingSection />
+          <RedeemSection />
           <Show when={billingInfo()?.customerID}>
             <ReloadSection />
             <MonthlyLimitSection />

+ 61 - 0
packages/console/app/src/routes/workspace/[id]/billing/redeem-section.module.css

@@ -0,0 +1,61 @@
+.root {
+  [data-slot="redeem-container"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-3);
+    min-width: 20rem;
+    width: fit-content;
+
+    @media (max-width: 30rem) {
+      width: 100%;
+    }
+  }
+
+  [data-slot="redeem-form"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-2);
+
+    [data-slot="input-row"] {
+      display: flex;
+      gap: var(--space-2);
+      align-items: stretch;
+
+      @media (max-width: 30rem) {
+        flex-direction: column;
+      }
+    }
+
+    input {
+      flex: 1;
+      padding: var(--space-2) var(--space-3);
+      border: 1px solid var(--color-border);
+      border-radius: var(--border-radius-sm);
+      background-color: var(--color-bg);
+      color: var(--color-text);
+      font-size: var(--font-size-sm);
+      font-family: var(--font-mono);
+
+      &:focus {
+        outline: none;
+        border-color: var(--color-accent);
+      }
+
+      &::placeholder {
+        color: var(--color-text-disabled);
+      }
+    }
+
+    [data-slot="form-error"] {
+      color: var(--color-danger);
+      font-size: var(--font-size-sm);
+      line-height: 1.4;
+    }
+
+    [data-slot="form-success"] {
+      color: var(--color-success, var(--color-accent));
+      font-size: var(--font-size-sm);
+      line-height: 1.4;
+    }
+  }
+}

+ 71 - 0
packages/console/app/src/routes/workspace/[id]/billing/redeem-section.tsx

@@ -0,0 +1,71 @@
+import { json, action, useParams, useSubmission } from "@solidjs/router"
+import { Show } from "solid-js"
+import { withActor } from "~/context/auth.withActor"
+import { Billing } from "@opencode-ai/console-core/billing.js"
+import { User } from "@opencode-ai/console-core/user.js"
+import { Actor } from "@opencode-ai/console-core/actor.js"
+import { CouponType } from "@opencode-ai/console-core/schema/billing.sql.js"
+import styles from "./redeem-section.module.css"
+import { queryBillingInfo } from "../../common"
+import { useI18n } from "~/context/i18n"
+import { formError, localizeError } from "~/lib/form-error"
+
+const redeem = action(async (form: FormData) => {
+  "use server"
+  const workspaceID = form.get("workspaceID") as string | null
+  if (!workspaceID) return { error: formError.workspaceRequired }
+  const code = (form.get("code") as string | null)?.trim().toUpperCase()
+  if (!code) return { error: "Coupon code is required." }
+  if (!(CouponType as readonly string[]).includes(code)) return { error: "Invalid coupon code." }
+
+  return json(
+    await withActor(async () => {
+      const actor = Actor.assert("user")
+      const email = await User.getAuthEmail(actor.properties.userID)
+      if (!email) return { error: "No email on account." }
+      return Billing.redeemCoupon(email, code as (typeof CouponType)[number])
+        .then(() => ({ error: undefined, data: true }))
+        .catch((e) => ({ error: e.message as string }))
+    }, workspaceID),
+    { revalidate: queryBillingInfo.key },
+  )
+}, "billing.redeemCoupon")
+
+export function RedeemSection() {
+  const params = useParams()
+  const i18n = useI18n()
+  const submission = useSubmission(redeem)
+
+  return (
+    <section class={styles.root}>
+      <div data-slot="section-title">
+        <h2>{i18n.t("workspace.redeem.title")}</h2>
+        <p>{i18n.t("workspace.redeem.subtitle")}</p>
+      </div>
+      <div data-slot="redeem-container">
+        <form action={redeem} method="post" data-slot="redeem-form">
+          <div data-slot="input-row">
+            <input
+              required
+              data-component="input"
+              name="code"
+              type="text"
+              autocomplete="off"
+              placeholder={i18n.t("workspace.redeem.placeholder")}
+            />
+            <button type="submit" data-color="primary" disabled={submission.pending}>
+              {submission.pending ? i18n.t("workspace.redeem.redeeming") : i18n.t("workspace.redeem.redeem")}
+            </button>
+          </div>
+          <Show when={submission.result && (submission.result as any).error}>
+            {(err: any) => <div data-slot="form-error">{localizeError(i18n.t, err())}</div>}
+          </Show>
+          <Show when={submission.result && !(submission.result as any).error && (submission.result as any).data}>
+            <div data-slot="form-success">{i18n.t("workspace.redeem.success")}</div>
+          </Show>
+          <input type="hidden" name="workspaceID" value={params.id} />
+        </form>
+      </div>
+    </section>
+  )
+}

+ 2 - 1
packages/console/app/src/routes/zen/util/handler.ts

@@ -762,7 +762,8 @@ export async function handler(
     const billing = authInfo.billing
     const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
     const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
-    if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
+    if (!billing.paymentMethodID && billing.balance <= 0)
+      throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
     if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
 
     const now = new Date()

+ 6 - 0
packages/console/core/migrations/20260418195905_shocking_marvel_zombies/migration.sql

@@ -0,0 +1,6 @@
+CREATE TABLE `coupon` (
+	`email` varchar(255),
+	`type` enum('BUILDATHON','GOFREEMONTH') NOT NULL,
+	`time_redeemed` timestamp(3),
+	CONSTRAINT PRIMARY KEY(`email`,`type`)
+);

+ 2619 - 0
packages/console/core/migrations/20260418195905_shocking_marvel_zombies/snapshot.json

@@ -0,0 +1,2619 @@
+{
+  "version": "6",
+  "dialect": "mysql",
+  "id": "18b4281c-1609-47d8-9d51-0b08e3925f2b",
+  "prevIds": ["93c492af-c95b-4213-9fc2-38c3dd10374d"],
+  "ddl": [
+    {
+      "name": "account",
+      "entityType": "tables"
+    },
+    {
+      "name": "auth",
+      "entityType": "tables"
+    },
+    {
+      "name": "benchmark",
+      "entityType": "tables"
+    },
+    {
+      "name": "billing",
+      "entityType": "tables"
+    },
+    {
+      "name": "coupon",
+      "entityType": "tables"
+    },
+    {
+      "name": "lite",
+      "entityType": "tables"
+    },
+    {
+      "name": "payment",
+      "entityType": "tables"
+    },
+    {
+      "name": "subscription",
+      "entityType": "tables"
+    },
+    {
+      "name": "usage",
+      "entityType": "tables"
+    },
+    {
+      "name": "ip_rate_limit",
+      "entityType": "tables"
+    },
+    {
+      "name": "ip",
+      "entityType": "tables"
+    },
+    {
+      "name": "key_rate_limit",
+      "entityType": "tables"
+    },
+    {
+      "name": "model_rate_limit",
+      "entityType": "tables"
+    },
+    {
+      "name": "key",
+      "entityType": "tables"
+    },
+    {
+      "name": "model",
+      "entityType": "tables"
+    },
+    {
+      "name": "provider",
+      "entityType": "tables"
+    },
+    {
+      "name": "user",
+      "entityType": "tables"
+    },
+    {
+      "name": "workspace",
+      "entityType": "tables"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "account"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "enum('email','github','google')",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "provider",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "subject",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "account_id",
+      "entityType": "columns",
+      "table": "auth"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "varchar(64)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "model",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "varchar(64)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "agent",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "mediumtext",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "result",
+      "entityType": "columns",
+      "table": "benchmark"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "customer_id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "payment_method_id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(32)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "payment_method_type",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(4)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "payment_method_last4",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "bigint",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "balance",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "monthly_limit",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "monthly_usage",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_monthly_usage_updated",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "boolean",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "reload",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "reload_trigger",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "reload_amount",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "reload_error",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_reload_error",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_reload_locked_till",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "json",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "subscription",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(28)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "subscription_id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "enum('20','100','200')",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "subscription_plan",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_subscription_booked",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_subscription_selected",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(28)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "lite_subscription_id",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "json",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "lite",
+      "entityType": "columns",
+      "table": "billing"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "email",
+      "entityType": "columns",
+      "table": "coupon"
+    },
+    {
+      "type": "enum('BUILDATHON','GOFREEMONTH')",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "type",
+      "entityType": "columns",
+      "table": "coupon"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_redeemed",
+      "entityType": "columns",
+      "table": "coupon"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "rolling_usage",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "weekly_usage",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "monthly_usage",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_rolling_updated",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_weekly_updated",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_monthly_updated",
+      "entityType": "columns",
+      "table": "lite"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "customer_id",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "invoice_id",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "payment_id",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "bigint",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "amount",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_refunded",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "json",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "enrichment",
+      "entityType": "columns",
+      "table": "payment"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "rolling_usage",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "fixed_usage",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_rolling_updated",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_fixed_updated",
+      "entityType": "columns",
+      "table": "subscription"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "model",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "provider",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "input_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "output_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "reasoning_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "cache_read_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "cache_write_5m_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "cache_write_1h_tokens",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "bigint",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "cost",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "key_id",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "session_id",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "json",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "enrichment",
+      "entityType": "columns",
+      "table": "usage"
+    },
+    {
+      "type": "varchar(45)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "ip",
+      "entityType": "columns",
+      "table": "ip_rate_limit"
+    },
+    {
+      "type": "varchar(10)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "interval",
+      "entityType": "columns",
+      "table": "ip_rate_limit"
+    },
+    {
+      "type": "int",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "count",
+      "entityType": "columns",
+      "table": "ip_rate_limit"
+    },
+    {
+      "type": "varchar(45)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "ip",
+      "entityType": "columns",
+      "table": "ip"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "ip"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "ip"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "ip"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "usage",
+      "entityType": "columns",
+      "table": "ip"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "key",
+      "entityType": "columns",
+      "table": "key_rate_limit"
+    },
+    {
+      "type": "varchar(40)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "interval",
+      "entityType": "columns",
+      "table": "key_rate_limit"
+    },
+    {
+      "type": "int",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "count",
+      "entityType": "columns",
+      "table": "key_rate_limit"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "key",
+      "entityType": "columns",
+      "table": "model_rate_limit"
+    },
+    {
+      "type": "varchar(40)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "interval",
+      "entityType": "columns",
+      "table": "model_rate_limit"
+    },
+    {
+      "type": "int",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "count",
+      "entityType": "columns",
+      "table": "model_rate_limit"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "key",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "user_id",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_used",
+      "entityType": "columns",
+      "table": "key"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "varchar(64)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "model",
+      "entityType": "columns",
+      "table": "model"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "varchar(64)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "provider",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "credentials",
+      "entityType": "columns",
+      "table": "provider"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "workspace_id",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "account_id",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "email",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_seen",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "color",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "enum('admin','member')",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "role",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "int",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "monthly_limit",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "bigint",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "monthly_usage",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_monthly_usage_updated",
+      "entityType": "columns",
+      "table": "user"
+    },
+    {
+      "type": "varchar(30)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "slug",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "varchar(255)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(now())",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": true,
+      "autoIncrement": false,
+      "default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "type": "timestamp(3)",
+      "notNull": false,
+      "autoIncrement": false,
+      "default": null,
+      "onUpdateNow": false,
+      "onUpdateNowFsp": null,
+      "charSet": null,
+      "collation": null,
+      "generated": null,
+      "name": "time_deleted",
+      "entityType": "columns",
+      "table": "workspace"
+    },
+    {
+      "columns": ["id"],
+      "name": "PRIMARY",
+      "table": "account",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["id"],
+      "name": "PRIMARY",
+      "table": "auth",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["id"],
+      "name": "PRIMARY",
+      "table": "benchmark",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "billing",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["email", "type"],
+      "name": "PRIMARY",
+      "table": "coupon",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "lite",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "payment",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "subscription",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "usage",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["ip", "interval"],
+      "name": "PRIMARY",
+      "table": "ip_rate_limit",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["ip"],
+      "name": "PRIMARY",
+      "table": "ip",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["key", "interval"],
+      "name": "PRIMARY",
+      "table": "key_rate_limit",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["key", "interval"],
+      "name": "PRIMARY",
+      "table": "model_rate_limit",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "key",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "model",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "provider",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["workspace_id", "id"],
+      "name": "PRIMARY",
+      "table": "user",
+      "entityType": "pks"
+    },
+    {
+      "columns": ["id"],
+      "name": "PRIMARY",
+      "table": "workspace",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        {
+          "value": "provider",
+          "isExpression": false
+        },
+        {
+          "value": "subject",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "provider",
+      "entityType": "indexes",
+      "table": "auth"
+    },
+    {
+      "columns": [
+        {
+          "value": "account_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "account_id",
+      "entityType": "indexes",
+      "table": "auth"
+    },
+    {
+      "columns": [
+        {
+          "value": "time_created",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "time_created",
+      "entityType": "indexes",
+      "table": "benchmark"
+    },
+    {
+      "columns": [
+        {
+          "value": "customer_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "global_customer_id",
+      "entityType": "indexes",
+      "table": "billing"
+    },
+    {
+      "columns": [
+        {
+          "value": "subscription_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "global_subscription_id",
+      "entityType": "indexes",
+      "table": "billing"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "user_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "workspace_user_id",
+      "entityType": "indexes",
+      "table": "lite"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "user_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "workspace_user_id",
+      "entityType": "indexes",
+      "table": "subscription"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "time_created",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "usage_time_created",
+      "entityType": "indexes",
+      "table": "usage"
+    },
+    {
+      "columns": [
+        {
+          "value": "key",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "global_key",
+      "entityType": "indexes",
+      "table": "key"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "model",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "model_workspace_model",
+      "entityType": "indexes",
+      "table": "model"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "provider",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "workspace_provider",
+      "entityType": "indexes",
+      "table": "provider"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "account_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "user_account_id",
+      "entityType": "indexes",
+      "table": "user"
+    },
+    {
+      "columns": [
+        {
+          "value": "workspace_id",
+          "isExpression": false
+        },
+        {
+          "value": "email",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "user_email",
+      "entityType": "indexes",
+      "table": "user"
+    },
+    {
+      "columns": [
+        {
+          "value": "account_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "global_account_id",
+      "entityType": "indexes",
+      "table": "user"
+    },
+    {
+      "columns": [
+        {
+          "value": "email",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "global_email",
+      "entityType": "indexes",
+      "table": "user"
+    },
+    {
+      "columns": [
+        {
+          "value": "slug",
+          "isExpression": false
+        }
+      ],
+      "isUnique": true,
+      "using": null,
+      "algorithm": null,
+      "lock": null,
+      "nameExplicit": true,
+      "name": "slug",
+      "entityType": "indexes",
+      "table": "workspace"
+    }
+  ],
+  "renames": []
+}

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

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

+ 24 - 0
packages/console/core/script/create-coupon.ts

@@ -0,0 +1,24 @@
+import { Database } from "../src/drizzle/index.js"
+import { CouponTable, CouponType } from "../src/schema/billing.sql.js"
+
+const email = process.argv[2]
+const type = process.argv[3] as (typeof CouponType)[number]
+
+if (!email || !type) {
+  console.error(`Usage: bun create-coupon.ts <email> <${CouponType.join("|")}>`)
+  process.exit(1)
+}
+
+if (!(CouponType as readonly string[]).includes(type)) {
+  console.error(`Error: type must be one of ${CouponType.join(", ")}`)
+  process.exit(1)
+}
+
+await Database.use((tx) =>
+  tx.insert(CouponTable).values({
+    email,
+    type,
+  }),
+)
+
+console.log(`Created ${type} coupon for ${email}`)

+ 49 - 5
packages/console/core/src/billing.ts

@@ -1,6 +1,14 @@
 import { Stripe } from "stripe"
-import { Database, eq, sql } from "./drizzle"
-import { BillingTable, LiteTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql"
+import { and, Database, eq, isNull, sql } from "./drizzle"
+import {
+  BillingTable,
+  CouponTable,
+  CouponType,
+  LiteTable,
+  PaymentTable,
+  SubscriptionTable,
+  UsageTable,
+} from "./schema/billing.sql"
 import { Actor } from "./actor"
 import { fn } from "./util/fn"
 import { z } from "zod"
@@ -147,6 +155,37 @@ export namespace Billing {
     return amountInMicroCents
   }
 
+  export const redeemCoupon = async (email: string, type: (typeof CouponType)[number]) => {
+    const coupon = await Database.use((tx) =>
+      tx
+        .select()
+        .from(CouponTable)
+        .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type)))
+        .then((rows) => rows[0]),
+    )
+    if (!coupon) throw new Error("Invalid coupon code")
+    if (coupon.timeRedeemed) throw new Error("Coupon already redeemed")
+
+    if (type === "BUILDATHON") await grantCredit(Actor.workspace(), 500)
+
+    await Database.use((tx) =>
+      tx
+        .update(CouponTable)
+        .set({ timeRedeemed: sql`now()` })
+        .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type))),
+    )
+  }
+
+  export const hasCoupon = async (email: string, type: (typeof CouponType)[number]) => {
+    return await Database.use((tx) =>
+      tx
+        .select()
+        .from(CouponTable)
+        .where(and(eq(CouponTable.email, email), eq(CouponTable.type, type), isNull(CouponTable.timeRedeemed)))
+        .then((rows) => rows.length > 0),
+    )
+  }
+
   export const setMonthlyLimit = fn(z.number(), async (input) => {
     return await Database.use((tx) =>
       tx
@@ -245,16 +284,19 @@ export namespace Billing {
       const user = Actor.assert("user")
       const { successUrl, cancelUrl, method } = input
 
-      const email = await User.getAuthEmail(user.properties.userID)
+      const email = (await User.getAuthEmail(user.properties.userID))!
       const billing = await Billing.get()
 
       if (billing.subscriptionID) throw new Error("Already subscribed to Black")
       if (billing.liteSubscriptionID) throw new Error("Already subscribed to Lite")
 
+      const coupon = (await Billing.hasCoupon(email, "GOFREEMONTH"))
+        ? LiteData.firstMonth100Coupon
+        : LiteData.firstMonth50Coupon
       const createSession = () =>
         Billing.stripe().checkout.sessions.create({
           mode: "subscription",
-          discounts: [{ coupon: LiteData.firstMonthCoupon(email!) }],
+          discounts: [{ coupon }],
           ...(billing.customerID
             ? {
                 customer: billing.customerID,
@@ -264,7 +306,7 @@ export namespace Billing {
                 },
               }
             : {
-                customer_email: email!,
+                customer_email: email,
               }),
           ...(() => {
             if (method === "alipay") {
@@ -312,6 +354,8 @@ export namespace Billing {
             metadata: {
               workspaceID: Actor.workspace(),
               userID: user.properties.userID,
+              userEmail: email,
+              coupon,
               type: "lite",
             },
           },

+ 2 - 6
packages/console/core/src/lite.ts

@@ -11,11 +11,7 @@ export namespace LiteData {
   export const productID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.product)
   export const priceID = fn(z.void(), () => Resource.ZEN_LITE_PRICE.price)
   export const priceInr = fn(z.void(), () => Resource.ZEN_LITE_PRICE.priceInr)
-  export const firstMonthCoupon = fn(z.string(), (email) => {
-    const invitees = Resource.ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES.value.split(",")
-    return invitees.includes(email)
-      ? Resource.ZEN_LITE_PRICE.firstMonth100Coupon
-      : Resource.ZEN_LITE_PRICE.firstMonth50Coupon
-  })
+  export const firstMonth100Coupon = Resource.ZEN_LITE_PRICE.firstMonth100Coupon
+  export const firstMonth50Coupon = Resource.ZEN_LITE_PRICE.firstMonth50Coupon
   export const planName = fn(z.void(), () => "lite")
 }

+ 23 - 1
packages/console/core/src/schema/billing.sql.ts

@@ -1,4 +1,15 @@
-import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
+import {
+  bigint,
+  boolean,
+  index,
+  int,
+  json,
+  mysqlEnum,
+  mysqlTable,
+  primaryKey,
+  uniqueIndex,
+  varchar,
+} from "drizzle-orm/mysql-core"
 import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
 import { workspaceIndexes } from "./workspace.sql"
 
@@ -121,3 +132,14 @@ export const UsageTable = mysqlTable(
   },
   (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
 )
+
+export const CouponType = ["BUILDATHON", "GOFREEMONTH"] as const
+export const CouponTable = mysqlTable(
+  "coupon",
+  {
+    email: varchar("email", { length: 255 }),
+    type: mysqlEnum("type", CouponType).notNull(),
+    timeRedeemed: utc("time_redeemed"),
+  },
+  (table) => [primaryKey({ columns: [table.email, table.type] })],
+)

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

@@ -142,10 +142,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
-    "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "ZEN_LITE_PRICE": {
       "firstMonth100Coupon": string
       "firstMonth50Coupon": string

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

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

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

@@ -142,10 +142,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
-    "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "ZEN_LITE_PRICE": {
       "firstMonth100Coupon": string
       "firstMonth50Coupon": string

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

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

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

@@ -142,10 +142,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
-    "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "ZEN_LITE_PRICE": {
       "firstMonth100Coupon": string
       "firstMonth50Coupon": string

+ 3 - 2
packages/desktop-electron/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/desktop-electron",
   "private": true,
-  "version": "1.4.10",
+  "version": "1.14.18",
   "type": "module",
   "license": "MIT",
   "homepage": "https://opencode.ai",
@@ -30,6 +30,7 @@
     "electron-store": "^10",
     "electron-updater": "^6",
     "electron-window-state": "^5.0.3",
+    "drizzle-orm": "catalog:",
     "marked": "^15"
   },
   "devDependencies": {
@@ -45,7 +46,7 @@
     "@types/node": "catalog:",
     "@typescript/native-preview": "catalog:",
     "@valibot/to-json-schema": "1.6.0",
-    "electron": "40.4.1",
+    "electron": "41.2.1",
     "electron-builder": "^26",
     "electron-vite": "^5",
     "solid-js": "catalog:",

+ 26 - 10
packages/desktop-electron/src/main/index.ts

@@ -28,8 +28,10 @@ const APP_IDS: Record<string, string> = {
   beta: "ai.opencode.desktop.beta",
   prod: "ai.opencode.desktop",
 }
+const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"
 app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev")
-app.setPath("userData", join(app.getPath("appData"), app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev"))
+app.setAppUserModelId(appId)
+app.setPath("userData", join(app.getPath("appData"), appId))
 const { autoUpdater } = pkg
 
 import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
@@ -41,6 +43,7 @@ import { parseMarkdown } from "./markdown"
 import { createMenu } from "./menu"
 import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
 import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
+import { drizzle } from "drizzle-orm/node-sqlite/driver"
 import type { Server } from "virtual:opencode-server"
 
 const initEmitter = new EventEmitter()
@@ -137,15 +140,6 @@ async function initialize() {
   const url = `http://${hostname}:${port}`
   const password = randomUUID()
 
-  logger.log("spawning sidecar", { url })
-  const { listener, health } = await spawnLocalServer(hostname, port, password)
-  server = listener
-  serverReady.resolve({
-    url,
-    username: "opencode",
-    password,
-  })
-
   const loadingTask = (async () => {
     logger.log("sidecar connection started", { url })
 
@@ -156,10 +150,32 @@ async function initialize() {
       if (progress.type === "Done") sqliteDone?.resolve()
     })
 
+    if (needsMigration) {
+      const { Database, JsonMigration } = await import("virtual:opencode-server")
+      await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
+        progress: (event: { current: number; total: number }) => {
+          const percent = Math.round(event.current / event.total) * 100
+          initEmitter.emit("sqlite", { type: "InProgress", value: percent })
+        },
+      })
+      initEmitter.emit("sqlite", { type: "Done" })
+
+      sqliteDone?.resolve()
+    }
+
     if (needsMigration) {
       await sqliteDone?.promise
     }
 
+    logger.log("spawning sidecar", { url })
+    const { listener, health } = await spawnLocalServer(hostname, port, password)
+    server = listener
+    serverReady.resolve({
+      url,
+      username: "opencode",
+      password,
+    })
+
     await Promise.race([
       health.wait,
       delay(30_000).then(() => {

+ 4 - 4
packages/desktop-electron/src/main/migrate.ts

@@ -4,7 +4,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"
 import { homedir } from "node:os"
 import { join } from "node:path"
 import { CHANNEL } from "./constants"
-import { getStore, store } from "./store"
+import { getStore } from "./store"
 
 const TAURI_MIGRATED_KEY = "tauriMigrated"
 
@@ -67,7 +67,7 @@ function migrateFile(datPath: string, filename: string) {
 }
 
 export function migrate() {
-  if (store.get(TAURI_MIGRATED_KEY)) {
+  if (getStore().get(TAURI_MIGRATED_KEY)) {
     log.log("tauri migration: already done, skipping")
     return
   }
@@ -77,7 +77,7 @@ export function migrate() {
 
   if (!existsSync(dir)) {
     log.log("tauri migration: no tauri data directory found, nothing to migrate")
-    store.set(TAURI_MIGRATED_KEY, true)
+    getStore().set(TAURI_MIGRATED_KEY, true)
     return
   }
 
@@ -87,5 +87,5 @@ export function migrate() {
   }
 
   log.log("tauri migration: complete")
-  store.set(TAURI_MIGRATED_KEY, true)
+  getStore().set(TAURI_MIGRATED_KEY, true)
 }

+ 8 - 8
packages/desktop-electron/src/main/server.ts

@@ -2,16 +2,16 @@ import { randomBytes } from "node:crypto"
 import { app } from "electron"
 import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
 import { getUserShell, loadShellEnv } from "./shell-env"
-import { store } from "./store"
+import { getStore } from "./store"
 
 const DEFAULT_RELAY_URL = "https://apn.dev.opencode.ai"
 const RELAY_SECRET_KEY = "relaySecret"
 
 function getOrCreateRelaySecret(): string {
-  const existing = store.get(RELAY_SECRET_KEY)
+  const existing = getStore().get(RELAY_SECRET_KEY)
   if (typeof existing === "string" && existing.length > 0) return existing
   const secret = randomBytes(18).toString("base64url")
-  store.set(RELAY_SECRET_KEY, secret)
+  getStore().set(RELAY_SECRET_KEY, secret)
   return secret
 }
 
@@ -20,26 +20,26 @@ export type WslConfig = { enabled: boolean }
 export type HealthCheck = { wait: Promise<void> }
 
 export function getDefaultServerUrl(): string | null {
-  const value = store.get(DEFAULT_SERVER_URL_KEY)
+  const value = getStore().get(DEFAULT_SERVER_URL_KEY)
   return typeof value === "string" ? value : null
 }
 
 export function setDefaultServerUrl(url: string | null) {
   if (url) {
-    store.set(DEFAULT_SERVER_URL_KEY, url)
+    getStore().set(DEFAULT_SERVER_URL_KEY, url)
     return
   }
 
-  store.delete(DEFAULT_SERVER_URL_KEY)
+  getStore().delete(DEFAULT_SERVER_URL_KEY)
 }
 
 export function getWslConfig(): WslConfig {
-  const value = store.get(WSL_ENABLED_KEY)
+  const value = getStore().get(WSL_ENABLED_KEY)
   return { enabled: typeof value === "boolean" ? value : false }
 }
 
 export function setWslConfig(config: WslConfig) {
-  store.set(WSL_ENABLED_KEY, config.enabled)
+  getStore().set(WSL_ENABLED_KEY, config.enabled)
 }
 
 export async function spawnLocalServer(hostname: string, port: number, password: string) {

+ 4 - 2
packages/desktop-electron/src/main/store.ts

@@ -4,6 +4,10 @@ import { SETTINGS_STORE } from "./constants"
 
 const cache = new Map<string, Store>()
 
+// We cannot instantiate the electron-store at module load time because
+// module import hoisting causes this to run before app.setPath("userData", ...)
+// in index.ts has executed, which would result in files being written to the default directory
+// (e.g. bad: %APPDATA%\@opencode-ai\desktop-electron\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings).
 export function getStore(name = SETTINGS_STORE) {
   const cached = cache.get(name)
   if (cached) return cached
@@ -11,5 +15,3 @@ export function getStore(name = SETTINGS_STORE) {
   cache.set(name, next)
   return next
 }
-
-export const store = getStore(SETTINGS_STORE)

+ 1 - 1
packages/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/desktop",
   "private": true,
-  "version": "1.4.10",
+  "version": "1.14.18",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.4.10",
+  "version": "1.14.18",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 0 - 4
packages/enterprise/sst-env.d.ts

@@ -142,10 +142,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
-    "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "ZEN_LITE_PRICE": {
       "firstMonth100Coupon": string
       "firstMonth50Coupon": string

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

@@ -1,7 +1,7 @@
 id = "opencode"
 name = "OpenCode"
 description = "The open source coding agent."
-version = "1.4.10"
+version = "1.14.18"
 schema_version = 1
 authors = ["Anomaly"]
 repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
 icon = "./icons/opencode.svg"
 
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-arm64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-arm64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-linux-x64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.18/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 1 - 1
packages/function/package.json

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

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

@@ -142,10 +142,6 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
-    "ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES": {
-      "type": "sst.sst.Secret"
-      "value": string
-    }
     "ZEN_LITE_PRICE": {
       "firstMonth100Coupon": string
       "firstMonth50Coupon": string

+ 2 - 3
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.4.10",
+  "version": "1.14.18",
   "name": "opencode",
   "type": "module",
   "license": "MIT",
@@ -80,7 +80,7 @@
     "@actions/github": "6.0.1",
     "@agentclientprotocol/sdk": "0.16.1",
     "@ai-sdk/alibaba": "1.0.17",
-    "@ai-sdk/amazon-bedrock": "4.0.95",
+    "@ai-sdk/amazon-bedrock": "4.0.96",
     "@ai-sdk/anthropic": "3.0.71",
     "@ai-sdk/azure": "3.0.49",
     "@ai-sdk/cerebras": "2.0.41",
@@ -163,7 +163,6 @@
     "partial-json": "0.1.7",
     "qrcode": "1.5.4",
     "remeda": "catalog:",
-    "ripgrep": "0.3.1",
     "semver": "^7.6.3",
     "solid-js": "catalog:",
     "strip-ansi": "7.1.2",

+ 1 - 9
packages/opencode/script/build.ts

@@ -187,7 +187,6 @@ for (const item of targets) {
   const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
   const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
   const workerPath = "./src/cli/cmd/tui/worker.ts"
-  const rgPath = "./src/file/ripgrep.worker.ts"
 
   // Use platform-specific bunfs root path based on target OS
   const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
@@ -212,19 +211,12 @@ for (const item of targets) {
       windows: {},
     },
     files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
-    entrypoints: [
-      "./src/index.ts",
-      parserWorker,
-      workerPath,
-      rgPath,
-      ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []),
-    ],
+    entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
     define: {
       OPENCODE_VERSION: `'${Script.version}'`,
       OPENCODE_MIGRATIONS: JSON.stringify(migrations),
       OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
       OPENCODE_WORKER_PATH: workerPath,
-      OPENCODE_RIPGREP_WORKER_PATH: rgPath,
       OPENCODE_CHANNEL: `'${Script.channel}'`,
       OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
     },

+ 23 - 8
packages/opencode/script/publish.ts

@@ -7,6 +7,22 @@ import { fileURLToPath } from "url"
 const dir = fileURLToPath(new URL("..", import.meta.url))
 process.chdir(dir)
 
+async function published(name: string, version: string) {
+  return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
+}
+
+async function publish(dir: string, name: string, version: string) {
+  // GitHub artifact downloads can drop the executable bit, and Docker uses the
+  // unpacked dist binaries directly rather than the published tarball.
+  if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
+  if (await published(name, version)) {
+    console.log(`already published ${name}@${version}`)
+    return
+  }
+  await $`bun pm pack`.cwd(dir)
+  await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
+}
+
 const binaries: Record<string, string> = {}
 for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
   const pkg = await Bun.file(`./dist/${filepath}`).json()
@@ -40,14 +56,10 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
 )
 
 const tasks = Object.entries(binaries).map(async ([name]) => {
-  if (process.platform !== "win32") {
-    await $`chmod -R 755 .`.cwd(`./dist/${name}`)
-  }
-  await $`bun pm pack`.cwd(`./dist/${name}`)
-  await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(`./dist/${name}`)
+  await publish(`./dist/${name}`, name, binaries[name])
 })
 await Promise.all(tasks)
-await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel}`
+await publish(`./dist/${pkg.name}`, `${pkg.name}-ai`, version)
 
 const image = "ghcr.io/anomalyco/opencode"
 const platforms = "linux/amd64,linux/arm64"
@@ -104,6 +116,7 @@ if (!Script.preview) {
         await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
         await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
         await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
+        if ((await $`cd ./dist/aur-${pkg} && git diff --cached --quiet`.nothrow()).exitCode === 0) break
         await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
         await $`cd ./dist/aur-${pkg} && git push`
         break
@@ -176,6 +189,8 @@ if (!Script.preview) {
   await $`git clone ${tap} ./dist/homebrew-tap`
   await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
   await $`cd ./dist/homebrew-tap && git add opencode.rb`
-  await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
-  await $`cd ./dist/homebrew-tap && git push`
+  if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode !== 0) {
+    await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
+    await $`cd ./dist/homebrew-tap && git push`
+  }
 }

+ 32 - 49
packages/opencode/specs/effect/facades.md

@@ -1,12 +1,13 @@
 # Facade removal checklist
 
-Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`.
+Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`.
 
-As of 2026-04-13, latest `origin/dev`:
+Current status on this branch:
 
-- `src/` still has 15 `makeRuntime(...)` call sites.
-- 13 of those are still in scope for facade removal.
-- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`.
+- `src/` has 5 `makeRuntime(...)` call sites total.
+- 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
+- 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`.
+- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
 
 Recent progress:
 
@@ -15,8 +16,9 @@ Recent progress:
 
 ## Priority hotspots
 
-- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades.
-- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion.
+- `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
+- `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
+- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`.
 
 ## Completed Batches
 
@@ -184,53 +186,34 @@ These were the recurring mistakes and useful corrections from the first two batc
 5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
 6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
 
-## Next batch
+## Remaining work
 
-Recommended next five, in order:
+Most of the original facade-removal backlog is already done. The practical remaining work is narrower now:
 
-1. `src/permission/index.ts`
-2. `src/agent/agent.ts`
-3. `src/session/summary.ts`
-4. `src/session/revert.ts`
-5. `src/mcp/auth.ts`
-
-Why this batch:
-
-- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
-- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
-- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
-
-After that batch, the expected follow-up is the main session cluster:
-
-1. `src/session/index.ts`
-2. `src/session/prompt.ts`
-3. `src/session/compaction.ts`
+1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
+2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
+3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist
 
 ## Checklist
 
-- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts`
-- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts`
-- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts`
-- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts`
-- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts`
-- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`
-- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts`
-- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts`
-- [x] `src/file/index.ts` (`File`) - facades removed and merged.
-- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged.
-- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged.
-- [x] `src/config/config.ts` (`Config`) - facades removed and merged.
-- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged.
-- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged.
-- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged.
-- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged.
-- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged.
-- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts`
-- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
-- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
-- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
-- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
-- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
+- [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service`
+- [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service`
+- [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed
+- [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed
+- [x] `src/permission/index.ts` (`Permission`) - service-local facades removed
+- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
+- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
+- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
+- [x] `src/file/index.ts` (`File`) - facades removed and merged
+- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
+- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
+- [x] `src/config/config.ts` (`Config`) - facades removed and merged
+- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged
+- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged
+- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged
+- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged
+- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged
+- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged
 
 ## Excluded `makeRuntime(...)` sites
 

+ 27 - 27
packages/opencode/specs/effect/http-api.md

@@ -76,7 +76,7 @@ Many route boundaries still use Zod-first validators. That does not block all ex
 
 ### Mixed handler styles
 
-Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first.
+Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first.
 
 ### Non-JSON routes
 
@@ -90,7 +90,7 @@ The current server composition, middleware, and docs flow are Hono-centered toda
 
 ### 1. Finish the prerequisites first
 
-- continue route-handler effectification in `server/instance/*.ts`
+- continue route-handler effectification in `server/routes/instance/*.ts`
 - continue schema migration toward Effect Schema-first DTOs and errors
 - keep removing service facades
 
@@ -98,9 +98,9 @@ The current server composition, middleware, and docs flow are Hono-centered toda
 
 Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
 
-- `server/instance/question.ts`
-- `server/instance/provider.ts`
-- `server/instance/permission.ts`
+- `server/routes/instance/question.ts`
+- `server/routes/instance/provider.ts`
+- `server/routes/instance/permission.ts`
 
 Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
 
@@ -155,9 +155,9 @@ This gives:
 
 As each route group is ported to `HttpApi`:
 
-1. change its `root` path from `/experimental/httpapi/<group>` to `/<group>`
-2. add `.all("/<group>", handler)` / `.all("/<group>/*", handler)` to the flag block in `instance/index.ts`
-3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
+1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts`
+2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
+3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes
 4. verify SDK output is unchanged
 
 Leave streaming-style endpoints on Hono until there is a clear reason to move them.
@@ -267,7 +267,7 @@ Use the same sequence for each route group.
 3. Apply the schema migration ordering above so those types are Effect Schema-first.
 4. Define the `HttpApi` contract separately from the handlers.
 5. Implement handlers by yielding the existing service from context.
-6. Mount the new surface in parallel under an experimental prefix.
+6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge.
 7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
 8. Add one end-to-end test and one OpenAPI-focused test.
 9. Compare ergonomics before migrating the next endpoint.
@@ -286,20 +286,20 @@ Placement rule:
 - keep `HttpApi` code under `src/server`, not `src/effect`
 - `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
 - place each `HttpApi` slice next to the HTTP boundary it serves
-- for instance-scoped routes, prefer `src/server/instance/httpapi/*`
-- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*`
+- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*`
+- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*`
 
 Suggested file layout for a repeatable spike:
 
-- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group
-- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups
-- `test/server/question-httpapi.test.ts` — end-to-end test against the real service
+- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group
+- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups
+- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch
 
 Suggested responsibilities:
 
 - `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers
-- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup)
-- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server
+- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup)
+- tests should verify the bridged routes through the normal server surface
 
 ## Example migration shape
 
@@ -319,33 +319,33 @@ Each route-group spike should follow the same shape.
 - keep handler bodies thin
 - keep transport mapping at the HTTP boundary only
 
-### 3. Standalone server
+### 3. Bridged server
 
-- the Effect HTTP server is self-contained in `httpapi/server.ts`
-- it is **not** mounted into the Hono app — no bridge, no `toWebHandler`
-- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover
-- each route group exposes its own OpenAPI doc endpoint
+- the Effect HTTP layer is composed in `httpapi/server.ts`
+- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)`
+- routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
+- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works
 
 ### 4. Verification
 
 - seed real state through the existing service
-- call the experimental endpoints
+- call the bridged endpoints with the flag enabled
 - assert that the service behavior is unchanged
 - assert that the generated OpenAPI contains the migrated paths and schemas
 
 ## Boundary composition
 
-The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server.
+The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server.
 
 ### Auth
 
-- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
+- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
 - each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served
-- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer
+- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice
 
 ### Instance and workspace lookup
 
-- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params
+- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params
 - this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware`
 - `HttpApi` handlers yield services from context and assume the correct instance has already been provided
 
@@ -360,7 +360,7 @@ The standalone Effect server owns its own middleware stack. It does not share mi
 
 The first slice is successful if:
 
-- the standalone Effect server starts and serves the endpoints independently of the Hono server
+- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled
 - the handlers reuse the existing Effect service
 - request decoding and response shapes are schema-defined from canonical Effect schemas
 - any remaining Zod boundary usage is derived from `.zod` or clearly temporary

+ 10 - 10
packages/opencode/specs/effect/instance-context.md

@@ -157,7 +157,7 @@ Direct legacy usage means any source file that still calls one of:
 - `Instance.reload(...)`
 - `Instance.dispose()` / `Instance.disposeAll()`
 
-Current total: `54` files in `packages/opencode/src`.
+Current total: `56` files in `packages/opencode/src`.
 
 ### Core bridge and plumbing
 
@@ -177,13 +177,13 @@ Migration rule:
 
 These are the current request-entry seams that still create or consume instance context through the legacy helper.
 
-- `src/server/instance/middleware.ts`
-- `src/server/instance/index.ts`
-- `src/server/instance/project.ts`
-- `src/server/instance/workspace.ts`
-- `src/server/instance/file.ts`
-- `src/server/instance/experimental.ts`
-- `src/server/instance/global.ts`
+- `src/server/routes/instance/middleware.ts`
+- `src/server/routes/instance/index.ts`
+- `src/server/routes/instance/project.ts`
+- `src/server/routes/control/workspace.ts`
+- `src/server/routes/instance/file.ts`
+- `src/server/routes/instance/experimental.ts`
+- `src/server/routes/global.ts`
 
 Migration rule:
 
@@ -239,7 +239,7 @@ Migration rule:
 These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
 
 - `src/agent/agent.ts`
-- `src/config/tui-migrate.ts`
+- `src/cli/cmd/tui/config/tui-migrate.ts`
 - `src/file/index.ts`
 - `src/file/watcher.ts`
 - `src/format/formatter.ts`
@@ -250,7 +250,7 @@ These modules are already the best near-term migration targets because they are
 - `src/project/vcs.ts`
 - `src/provider/provider.ts`
 - `src/pty/index.ts`
-- `src/session/index.ts`
+- `src/session/session.ts`
 - `src/session/instruction.ts`
 - `src/session/llm.ts`
 - `src/session/system.ts`

+ 6 - 8
packages/opencode/specs/effect/loose-ends.md

@@ -4,11 +4,11 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
 
 ## Config / TUI
 
-- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal.
+- [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration.
       Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
-- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
+- [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
       Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
-- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted.
+- [x] `env/index.ts` - already uses `InstanceState.make(...)`.
 
 ## ConfigPaths
 
@@ -21,14 +21,12 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
   - `readFile(...)`
   - `parseText(...)`
 - [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
-- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
-- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
+- [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
+- [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
 
 ## Instance cleanup
 
-- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated.
-- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone.
-- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal.
+- [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over.
 
 ## Notes
 

+ 30 - 40
packages/opencode/specs/effect/migration.md

@@ -19,53 +19,43 @@ See `instance-context.md` for the phased plan to remove the legacy ALS / promise
 
 ## Service shape
 
-Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions:
+Every service follows the same pattern: one module, flat top-level exports, traced Effect methods, and a self-reexport at the bottom when the file is the public module.
 
 ```ts
-export namespace Foo {
-  export interface Interface {
-    readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
-  }
-
-  export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
-
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      // For instance-scoped services:
-      const state = yield* InstanceState.make<State>(
-        Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
-      )
+export interface Interface {
+  readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
+}
 
-      const get = Effect.fn("Foo.get")(function* (id: FooID) {
-        const s = yield* InstanceState.get(state)
-        // ...
-      })
+export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
 
-      return Service.of({ get })
-    }),
-  )
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
+    )
 
-  // Optional: wire dependencies
-  export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
+    const get = Effect.fn("Foo.get")(function* (id: FooID) {
+      const s = yield* InstanceState.get(state)
+      // ...
+    })
 
-  // Per-service runtime (inside the namespace)
-  const { runPromise } = makeRuntime(Service, defaultLayer)
+    return Service.of({ get })
+  }),
+)
 
-  // Async facade functions
-  export async function get(id: FooID) {
-    return runPromise((svc) => svc.get(id))
-  }
-}
+export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
+
+export * as Foo from "."
 ```
 
 Rules:
 
-- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split
-- `runPromise` goes inside the namespace (not exported unless tests need it)
-- Facade functions are plain `async function` — no `fn()` wrappers
-- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing)
-- No `Layer.fresh` — InstanceState handles per-directory isolation
+- Keep the service surface in one module; prefer flat top-level exports over `export namespace Foo { ... }`
+- Use `Effect.fn("Foo.method")` for Effect methods
+- Use a self-reexport (`export * as Foo from "."` or `"./foo"`) for the public namespace projection
+- Avoid service-local `makeRuntime(...)` facades unless a file is still intentionally in the older migration phase
+- No `Layer.fresh` for normal per-directory isolation; use `InstanceState`
 
 ## Schema → Zod interop
 
@@ -266,7 +256,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
 
 ## Destroying the facades
 
-This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`.
+This phase is no longer broadly open. There are 5 `makeRuntime(...)` call sites under `src/`, and only a small subset are still ordinary facade-removal targets. The live checklist now lives in `facades.md`.
 
 These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
 
@@ -297,11 +287,11 @@ For each service, the migration is roughly:
 - `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
 - `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
 - `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
-- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed.
-- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
+- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/instance/session.ts` converted; facade removed.
+- `Account` — migrated 2026-04-11. Callers in `server/routes/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
 - `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
 - `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
-- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed.
+- `Question` — migrated 2026-04-11. Callers in `server/routes/instance/question.ts` and test converted; facade removed.
 - `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
 
 ## Route handler effectification

+ 16 - 18
packages/opencode/specs/effect/routes.md

@@ -39,28 +39,26 @@ This eliminates multiple `runPromise` round-trips and lets handlers compose natu
 
 ## Current route files
 
-Current instance route files live under `src/server/instance`, not `server/routes`.
+Current instance route files live under `src/server/routes/instance`.
 
-The main migration targets are:
+Files that are already mostly on the intended service-yielding shape:
 
-- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus
-- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions
-- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider
-- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style
-- [ ] `server/instance/pty.ts` — still calls Pty facades directly
-- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
+- [x] `server/routes/instance/question.ts` — handlers yield `Question.Service`
+- [x] `server/routes/instance/provider.ts` — handlers yield `Provider.Service`, `ProviderAuth.Service`, and `Config.Service`
+- [x] `server/routes/instance/permission.ts` — handlers yield `Permission.Service`
+- [x] `server/routes/instance/mcp.ts` — handlers mostly yield `MCP.Service`
+- [x] `server/routes/instance/pty.ts` — handlers yield `Pty.Service`
 
-Additional route files that still participate in the migration:
+Files still worth tracking here:
 
-- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format
-- [ ] `server/instance/file.ts` — Ripgrep, File, LSP
-- [ ] `server/instance/mcp.ts` — MCP facade-heavy
-- [ ] `server/instance/permission.ts` — Permission
-- [ ] `server/instance/workspace.ts` — Workspace
-- [ ] `server/instance/tui.ts` — Bus and Session
-- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
+- [ ] `server/routes/instance/session.ts` — still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has direct `Bus.publish(...)` / `Session.list(...)` usage
+- [ ] `server/routes/instance/index.ts` — mostly converted, but still has direct `Instance.dispose()` / `Instance.*` reads for `/instance/dispose` and `/path`
+- [ ] `server/routes/instance/file.ts` — most handlers yield services, but `/find` still passes `Instance.directory` directly into ripgrep and `/find/symbol` is still stubbed
+- [ ] `server/routes/instance/experimental.ts` — mixed state; many handlers are composed, but some still rely on `runRequest(...)` or direct `Instance.project` reads
+- [ ] `server/routes/instance/middleware.ts` — still enters the instance via `Instance.provide(...)`
+- [ ] `server/routes/global.ts` — still uses `Instance.disposeAll()` and remains partly outside the fully-composed style
 
 ## Notes
 
-- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style.
-- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly.
+- Route conversion is now less about facade removal and more about removing the remaining direct `Instance.*` reads, `Instance.provide(...)` boundaries, and small Promise-style bridges inside route files.
+- `jsonRequest(...)` / `runRequest(...)` already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files.

+ 254 - 28
packages/opencode/specs/effect/schema.md

@@ -1,12 +1,19 @@
 # Schema migration
 
-Practical reference for migrating data types in `packages/opencode` from Zod-first definitions to Effect Schema with Zod compatibility shims.
+Practical reference for migrating data types in `packages/opencode` from
+Zod-first definitions to Effect Schema with Zod compatibility shims.
 
 ## Goal
 
-Use Effect Schema as the source of truth for domain models, IDs, inputs, outputs, and typed errors.
+Use Effect Schema as the source of truth for domain models, IDs, inputs,
+outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
+compatibility boundaries by exposing a `.zod` static derived from the Effect
+schema via `@/util/effect-zod`.
 
-Keep Zod available at existing HTTP, tool, and compatibility boundaries by exposing a `.zod` field derived from the Effect schema.
+The long-term driver is `specs/effect/http-api.md` — once the HTTP server
+moves to `@effect/platform`, every Schema-first DTO can flow through
+`HttpApi` / `HttpRouter` without a zod translation layer, and the entire
+`effect-zod` walker plus every `.zod` static can be deleted.
 
 ## Preferred shapes
 
@@ -24,17 +31,14 @@ export class Info extends Schema.Class<Info>("Foo.Info")({
 }
 ```
 
-If the class cannot reference itself cleanly during initialization, use the existing two-step pattern:
+If the class cannot reference itself cleanly during initialization, use the
+two-step `withStatics` pattern:
 
 ```ts
-const _Info = Schema.Struct({
+export const Info = Schema.Struct({
   id: FooID,
   name: Schema.String,
-})
-
-export const Info = Object.assign(_Info, {
-  zod: zod(_Info),
-})
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
 ```
 
 ### Errors
@@ -49,27 +53,89 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Foo
 
 ### IDs and branded leaf types
 
-Keep branded/schema-backed IDs as Effect schemas and expose `static readonly zod` for compatibility when callers still expect Zod.
+Keep branded/schema-backed IDs as Effect schemas and expose
+`static readonly zod` for compatibility when callers still expect Zod.
+
+### Refinements
+
+Reuse named refinements instead of re-spelling `z.number().int().positive()`
+in every schema. The `effect-zod` walker translates the Effect versions into
+the corresponding zod methods, so JSON Schema output (`type: integer`,
+`exclusiveMinimum`, `pattern`, `format: uuid`, …) is preserved.
+
+```ts
+const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
+const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
+const HexColor = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))
+```
+
+See `test/util/effect-zod.test.ts` for the full set of translated checks.
 
 ## Compatibility rule
 
-During migration, route validators, tool parameters, and any existing Zod-based boundary should consume the derived `.zod` schema instead of maintaining a second hand-written Zod schema.
+During migration, route validators, tool parameters, and any existing
+Zod-based boundary should consume the derived `.zod` schema instead of
+maintaining a second hand-written Zod schema.
 
 The default should be:
 
 - Effect Schema owns the type
 - `.zod` exists only as a compatibility surface
-- new domain models should not start Zod-first unless there is a concrete boundary-specific need
+- new domain models should not start Zod-first unless there is a concrete
+  boundary-specific need
 
 ## When Zod can stay
 
 It is fine to keep a Zod-native schema temporarily when:
 
-- the type is only used at an HTTP or tool boundary
+- the type is only used at an HTTP or tool boundary and is not reused elsewhere
 - the validator depends on Zod-only transforms or behavior not yet covered by `zod()`
 - the migration would force unrelated churn across a large call graph
 
-When this happens, prefer leaving a short note or TODO rather than silently creating a parallel schema source of truth.
+When this happens, prefer leaving a short note or TODO rather than silently
+creating a parallel schema source of truth.
+
+## Escape hatches
+
+The walker in `@/util/effect-zod` exposes three explicit escape hatches for
+cases the pure-Schema path cannot express. Each one stays in the codebase
+only as long as its upstream or local dependency requires it — inline
+comments document when each can be deleted.
+
+### `ZodOverride` annotation
+
+Replaces the entire derivation with a hand-crafted zod schema. Used when:
+
+- the target carries external `$ref` metadata (e.g.
+  `config/model-id.ts` points at `https://models.dev/...`)
+- the target is a zod-only schema that cannot yet be expressed as Schema
+  (e.g. `ConfigAgent.Info`, `ConfigPermission.Info`, `Log.Level`)
+
+### `ZodPreprocess` annotation
+
+Wraps the derived zod schema with `z.preprocess(fn, inner)`. Used by
+`config/permission.ts` to inject `__originalKeys` before parsing, because
+`Schema.StructWithRest` canonicalises output (known fields first, catchall
+after) and destroys the user's original property order — which permission
+rule precedence depends on.
+
+Tracked upstream as `effect:core/wlh553`: "Schema: add preserveInputOrder
+(or pre-parse hook) for open structs." Once that lands, `ZodPreprocess` and
+the `__originalKeys` hack can both be deleted.
+
+### Local `DeepMutable<T>` in `config/config.ts`
+
+`Schema.Struct` produces `readonly` types. Some consumer code (notably the
+`Config` service) mutates `Info` objects directly, so a readonly-stripping
+utility is needed when casting the derived zod schema's output type.
+
+`Types.DeepMutable` from effect-smol would be a drop-in, but it widens
+`unknown` to `{}` in the fallback branch — a bug that affects any schema
+using `Schema.Record(String, Schema.Unknown)`.
+
+Tracked upstream as `effect:core/x228my`: "Types.DeepMutable widens unknown
+to `{}`." Once that lands, the local `DeepMutable` copy can be deleted and
+`Types.DeepMutable` used directly.
 
 ## Ordering
 
@@ -81,19 +147,179 @@ Migrate in this order:
 4. Service-local internal models
 5. Route and tool boundary validators that can switch to `.zod`
 
-This keeps shared types canonical first and makes boundary updates mostly mechanical.
-
-## Checklist
-
-- [ ] Shared `schema.ts` leaf models are Effect Schema-first
-- [ ] Exported `Info` / `Input` / `Output` types use `Schema.Class` where appropriate
-- [ ] Domain errors use `Schema.TaggedErrorClass`
-- [ ] Migrated types expose `.zod` for back compatibility
-- [ ] Route and tool validators consume derived `.zod` instead of duplicate Zod definitions
-- [ ] New domain models default to Effect Schema first
+This keeps shared types canonical first and makes boundary updates mostly
+mechanical.
+
+## Progress tracker
+
+### `src/config/` ✅ complete
+
+All of `packages/opencode/src/config/` has been migrated. Files that still
+import `z` do so only for local `ZodOverride` bridges or for `z.ZodType`
+type annotations — the `export const <Info|Spec>` values are all Effect
+Schema at source.
+
+- [x] skills, formatter, console-state, mcp, lsp, permission (leaves), model-id, command, plugin, provider
+- [x] server, layout
+- [x] keybinds
+- [x] permission#Info
+- [x] agent
+- [x] config.ts root
+
+### `src/*/schema.ts` leaf modules
+
+These are the highest-priority next targets. Each is a small, self-contained
+schema module with a clear domain.
+
+- [ ] `src/control-plane/schema.ts`
+- [ ] `src/permission/schema.ts`
+- [ ] `src/project/schema.ts`
+- [ ] `src/provider/schema.ts`
+- [ ] `src/pty/schema.ts`
+- [ ] `src/question/schema.ts`
+- [ ] `src/session/schema.ts`
+- [ ] `src/sync/schema.ts`
+- [ ] `src/tool/schema.ts`
+
+### Session domain
+
+Major cluster. Message + event types flow through the SSE API and every SDK
+output, so byte-identical SDK surface is critical.
+
+- [ ] `src/session/compaction.ts`
+- [ ] `src/session/message-v2.ts`
+- [ ] `src/session/message.ts`
+- [ ] `src/session/prompt.ts`
+- [ ] `src/session/revert.ts`
+- [ ] `src/session/session.ts`
+- [ ] `src/session/status.ts`
+- [ ] `src/session/summary.ts`
+- [ ] `src/session/todo.ts`
+
+### Provider domain
+
+- [ ] `src/provider/auth.ts`
+- [ ] `src/provider/models.ts`
+- [ ] `src/provider/provider.ts`
+
+### Tool schemas
+
+Each tool declares its parameters via a zod schema. Tools are consumed by
+both the in-process runtime and the AI SDK's tool-calling layer, so the
+emitted JSON Schema must stay byte-identical.
+
+- [ ] `src/tool/apply_patch.ts`
+- [ ] `src/tool/bash.ts`
+- [ ] `src/tool/codesearch.ts`
+- [ ] `src/tool/edit.ts`
+- [ ] `src/tool/glob.ts`
+- [ ] `src/tool/grep.ts`
+- [ ] `src/tool/invalid.ts`
+- [ ] `src/tool/lsp.ts`
+- [ ] `src/tool/multiedit.ts`
+- [ ] `src/tool/plan.ts`
+- [ ] `src/tool/question.ts`
+- [ ] `src/tool/read.ts`
+- [ ] `src/tool/registry.ts`
+- [ ] `src/tool/skill.ts`
+- [ ] `src/tool/task.ts`
+- [ ] `src/tool/todo.ts`
+- [ ] `src/tool/tool.ts`
+- [ ] `src/tool/webfetch.ts`
+- [ ] `src/tool/websearch.ts`
+- [ ] `src/tool/write.ts`
+
+### HTTP route boundaries
+
+Every file in `src/server/routes/` uses hono-openapi with zod validators for
+route inputs/outputs. Migrating these individually is the last step; most
+will switch to `.zod` derived from the Schema-migrated domain types above,
+which means touching them is largely mechanical once the domain side is
+done.
+
+- [ ] `src/server/error.ts`
+- [ ] `src/server/event.ts`
+- [ ] `src/server/projectors.ts`
+- [ ] `src/server/routes/control/index.ts`
+- [ ] `src/server/routes/control/workspace.ts`
+- [ ] `src/server/routes/global.ts`
+- [ ] `src/server/routes/instance/index.ts`
+- [ ] `src/server/routes/instance/config.ts`
+- [ ] `src/server/routes/instance/event.ts`
+- [ ] `src/server/routes/instance/experimental.ts`
+- [ ] `src/server/routes/instance/file.ts`
+- [ ] `src/server/routes/instance/mcp.ts`
+- [ ] `src/server/routes/instance/permission.ts`
+- [ ] `src/server/routes/instance/project.ts`
+- [ ] `src/server/routes/instance/provider.ts`
+- [ ] `src/server/routes/instance/pty.ts`
+- [ ] `src/server/routes/instance/question.ts`
+- [ ] `src/server/routes/instance/session.ts`
+- [ ] `src/server/routes/instance/sync.ts`
+- [ ] `src/server/routes/instance/tui.ts`
+
+The bigger prize for this group is the `@effect/platform` HTTP migration
+described in `specs/effect/http-api.md`. Once that lands, every one of
+these files changes shape entirely (`HttpApi.endpoint(...)` and friends),
+so the Schema-first domain types become a prerequisite rather than a
+sibling task.
+
+### Everything else
+
+Small / shared / control-plane / CLI. Mostly independent; can be done
+piecewise.
+
+- [ ] `src/acp/agent.ts`
+- [ ] `src/agent/agent.ts`
+- [ ] `src/bus/bus-event.ts`
+- [ ] `src/bus/index.ts`
+- [ ] `src/cli/cmd/tui/config/tui-migrate.ts`
+- [ ] `src/cli/cmd/tui/config/tui-schema.ts`
+- [ ] `src/cli/cmd/tui/config/tui.ts`
+- [ ] `src/cli/cmd/tui/event.ts`
+- [ ] `src/cli/ui.ts`
+- [ ] `src/command/index.ts`
+- [ ] `src/control-plane/adaptors/worktree.ts`
+- [ ] `src/control-plane/types.ts`
+- [ ] `src/control-plane/workspace.ts`
+- [ ] `src/file/index.ts`
+- [ ] `src/file/ripgrep.ts`
+- [ ] `src/file/watcher.ts`
+- [ ] `src/format/index.ts`
+- [ ] `src/id/id.ts`
+- [ ] `src/ide/index.ts`
+- [ ] `src/installation/index.ts`
+- [ ] `src/lsp/client.ts`
+- [ ] `src/lsp/lsp.ts`
+- [ ] `src/mcp/auth.ts`
+- [ ] `src/patch/index.ts`
+- [ ] `src/plugin/github-copilot/models.ts`
+- [ ] `src/project/project.ts`
+- [ ] `src/project/vcs.ts`
+- [ ] `src/pty/index.ts`
+- [ ] `src/skill/index.ts`
+- [ ] `src/snapshot/index.ts`
+- [ ] `src/storage/db.ts`
+- [ ] `src/storage/storage.ts`
+- [ ] `src/sync/index.ts`
+- [ ] `src/util/fn.ts`
+- [ ] `src/util/log.ts`
+- [ ] `src/util/update-schema.ts`
+- [ ] `src/worktree/index.ts`
+
+### Do-not-migrate
+
+- `src/util/effect-zod.ts` — the walker itself. Stays zod-importing forever
+  (it's what emits zod from Schema). Goes away only when the `.zod`
+  compatibility layer is no longer needed anywhere.
 
 ## Notes
 
-- Use `@/util/effect-zod` for all Schema -> Zod conversion.
-- Prefer one canonical schema definition. Avoid maintaining parallel Zod and Effect definitions for the same domain type.
-- Keep the migration incremental. Converting the domain model first is more valuable than converting every boundary in the same change.
+- Use `@/util/effect-zod` for all Schema → Zod conversion.
+- Prefer one canonical schema definition. Avoid maintaining parallel Zod and
+  Effect definitions for the same domain type.
+- Keep the migration incremental. Converting the domain model first is more
+  valuable than converting every boundary in the same change.
+- Every migrated file should leave the generated SDK output (`packages/sdk/
+openapi.json` and `packages/sdk/js/src/v2/gen/types.gen.ts`) byte-identical
+  unless the change is deliberately user-visible.

+ 19 - 17
packages/opencode/specs/effect/server-package.md

@@ -40,13 +40,13 @@ Everything still lives in `packages/opencode`.
 Important current facts:
 
 - there is no `packages/core` or `packages/cli` workspace yet
-- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet
+- there is no `packages/server` workspace yet on this branch
 - the main host server is still Hono-based in `src/server/server.ts`
 - current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
 - the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
-- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts`
-- that experimental slice is mounted under `/experimental/httpapi/question`
-- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts`
+- there are already bridged Effect `HttpApi` slices under `src/server/routes/instance/httpapi/*`
+- those slices are mounted into the Hono server behind `OPENCODE_EXPERIMENTAL_HTTPAPI`
+- the bridge currently covers `question`, `permission`, `provider`, partial `config`, and partial `project` routes
 
 This means the package split should start from an extraction path, not from greenfield package ownership.
 
@@ -209,17 +209,19 @@ Current host and route composition:
 
 - `src/server/server.ts`
 - `src/server/control/index.ts`
-- `src/server/instance/index.ts`
+- `src/server/routes/instance/index.ts`
 - `src/server/middleware.ts`
 - `src/server/adapter.bun.ts`
 - `src/server/adapter.node.ts`
 
-Current experimental `HttpApi` slice:
+Current bridged `HttpApi` slices:
 
-- `src/server/instance/httpapi/question.ts`
-- `src/server/instance/httpapi/index.ts`
-- `src/server/instance/experimental.ts`
-- `test/server/question-httpapi.test.ts`
+- `src/server/routes/instance/httpapi/question.ts`
+- `src/server/routes/instance/httpapi/permission.ts`
+- `src/server/routes/instance/httpapi/provider.ts`
+- `src/server/routes/instance/httpapi/config.ts`
+- `src/server/routes/instance/httpapi/project.ts`
+- `src/server/routes/instance/httpapi/server.ts`
 
 Current OpenAPI flow:
 
@@ -245,7 +247,7 @@ Keep in `packages/opencode` for now:
 
 - `src/server/server.ts`
 - `src/server/control/index.ts`
-- `src/server/instance/*.ts`
+- `src/server/routes/**/*.ts`
 - `src/server/middleware.ts`
 - `src/server/adapter.*.ts`
 - `src/effect/app-runtime.ts`
@@ -305,14 +307,13 @@ Bad early migration targets:
 
 ## First vertical slice
 
-The first slice for the package split is the existing experimental `question` group.
+The first slice for the package split is still the existing `question` `HttpApi` group.
 
 Why `question` first:
 
 - it already exists as an experimental `HttpApi` slice
 - it already follows the desired contract and implementation split in one file
 - it is already mounted through the current Hono host
-- it already has an end-to-end test
 - it is JSON-only
 - it has low blast radius
 
@@ -357,7 +358,7 @@ Done means:
 
 Scope:
 
-- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts`
+- extract the pure `HttpApi` contract from `src/server/routes/instance/httpapi/question.ts`
 - place it in `packages/server/src/definition/question.ts`
 - aggregate it in `packages/server/src/definition/api.ts`
 - generate OpenAPI in `packages/server/src/openapi.ts`
@@ -399,8 +400,9 @@ Scope:
 
 - replace local experimental question route wiring in `packages/opencode`
 - keep the same mount path:
-- `/experimental/httpapi/question`
-- `/experimental/httpapi/question/doc`
+- `/question`
+- `/question/:requestID/reply`
+- `/question/:requestID/reject`
 
 Rules:
 
@@ -569,7 +571,7 @@ For package-split PRs, validate the smallest useful thing.
 Typical validation for the first waves:
 
 - `bun typecheck` in the touched package directory or directories
-- the relevant route test, especially `test/server/question-httpapi.test.ts`
+- the relevant server / route coverage for the migrated slice
 - merged OpenAPI coverage if the PR touches spec generation
 
 Do not run tests from repo root.

+ 3 - 5
packages/opencode/specs/effect/tools.md

@@ -36,7 +36,7 @@ This keeps tool tests aligned with the production service graph and makes follow
 
 ## Exported tools
 
-These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path:
+These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
 
 - [x] `apply_patch.ts`
 - [x] `bash.ts`
@@ -45,7 +45,6 @@ These exported tool definitions already exist in `src/tool` and are on the curre
 - [x] `glob.ts`
 - [x] `grep.ts`
 - [x] `invalid.ts`
-- [x] `ls.ts`
 - [x] `lsp.ts`
 - [x] `multiedit.ts`
 - [x] `plan.ts`
@@ -60,7 +59,7 @@ These exported tool definitions already exist in `src/tool` and are on the curre
 
 Notes:
 
-- `batch.ts` is no longer a current tool file and should not be tracked here.
+- There is no current `ls.ts` tool file on this branch.
 - `truncate.ts` is an Effect service used by tools, not a tool definition itself.
 - `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
 
@@ -73,7 +72,7 @@ Current spot cleanups worth tracking:
 - [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
 - [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
 - [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
-- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts`
+- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
 - [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
 
 Notable items that are already effectively on the target path and do not need separate migration bullets right now:
@@ -83,7 +82,6 @@ Notable items that are already effectively on the target path and do not need se
 - `write.ts`
 - `codesearch.ts`
 - `websearch.ts`
-- `ls.ts`
 - `multiedit.ts`
 - `edit.ts`
 

+ 13 - 1
packages/opencode/src/cli/cmd/generate.ts

@@ -25,7 +25,19 @@ export const GenerateCommand = {
         ]
       }
     }
-    const json = JSON.stringify(specs, null, 2)
+    const raw = JSON.stringify(specs, null, 2)
+
+    // Format through prettier so output is byte-identical to committed file
+    // regardless of whether ./script/format.ts runs afterward.
+    const prettier = await import("prettier")
+    const babel = await import("prettier/plugins/babel")
+    const estree = await import("prettier/plugins/estree")
+    const format = prettier.format ?? prettier.default?.format
+    const json = await format(raw, {
+      parser: "json",
+      plugins: [babel.default ?? babel, estree.default ?? estree],
+      printWidth: 120,
+    })
 
     // Wait for stdout to finish writing before process.exit() is called
     await new Promise<void>((resolve, reject) => {

+ 0 - 1
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -467,7 +467,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         return store.status
       },
       get ready() {
-        return true
         if (process.env.OPENCODE_FAST_BOOT) return true
         return store.status !== "loading"
       },

+ 18 - 7
packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx

@@ -5,11 +5,11 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
 import { Locale } from "@/util"
 import { useSDK } from "@tui/context/sdk"
 import { useRoute } from "@tui/context/route"
-import { useDialog } from "../../ui/dialog"
+import { useDialog, type DialogContext } from "../../ui/dialog"
 import type { PromptInfo } from "@tui/component/prompt/history"
 import { strip } from "@tui/component/prompt/part"
 
-export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
+export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID?: string) => void }) {
   const sync = useSync()
   const dialog = useDialog()
   const sdk = useSDK()
@@ -19,9 +19,21 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
     dialog.setSize("large")
   })
 
-  const options = createMemo((): DialogSelectOption<string>[] => {
+  const options = createMemo((): DialogSelectOption<string | undefined>[] => {
     const messages = sync.data.message[props.sessionID] ?? []
-    const result = [] as DialogSelectOption<string>[]
+    const fullSession = {
+      title: "Full session",
+      value: undefined,
+      onSelect: async (dialog: DialogContext) => {
+        const forked = await sdk.client.session.fork({ sessionID: props.sessionID })
+        route.navigate({
+          sessionID: forked.data!.id,
+          type: "session",
+        })
+        dialog.clear()
+      },
+    } satisfies DialogSelectOption<string | undefined>
+    const result = [] as DialogSelectOption<string | undefined>[]
     for (const message of messages) {
       if (message.role !== "user") continue
       const part = (sync.data.part[message.id] ?? []).find(
@@ -57,9 +69,8 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
         },
       })
     }
-    result.reverse()
-    return result
+    return [fullSession, ...result.reverse()]
   })
 
-  return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork from message" options={options()} />
+  return <DialogSelect onMove={(option) => props.onMove(option.value)} title="Fork session" options={options()} />
 }

+ 2 - 1
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -451,7 +451,7 @@ export function Session() {
       },
     },
     {
-      title: "Fork from message",
+      title: "Fork session",
       value: "session.fork",
       keybind: "session_fork",
       category: "Session",
@@ -462,6 +462,7 @@ export function Session() {
         dialog.replace(() => (
           <DialogForkFromTimeline
             onMove={(messageID) => {
+              if (!messageID) return
               const child = scroll.getChildren().find((child) => {
                 return child.id === messageID
               })

+ 96 - 81
packages/opencode/src/config/agent.ts

@@ -1,10 +1,12 @@
 export * as ConfigAgent from "./agent"
 
-import { Log } from "../util"
+import { Schema } from "effect"
 import z from "zod"
+import { Bus } from "@/bus"
+import { zod, ZodOverride } from "@/util/effect-zod"
+import { Log } from "../util"
 import { NamedError } from "@opencode-ai/shared/util/error"
 import { Glob } from "@opencode-ai/shared/util/glob"
-import { Bus } from "@/bus"
 import { configEntryNameFromPath } from "./entry-name"
 import { InvalidError } from "./error"
 import * as ConfigMarkdown from "./markdown"
@@ -13,89 +15,102 @@ import { ConfigPermission } from "./permission"
 
 const log = Log.create({ service: "config" })
 
-export const Info = z
-  .object({
-    model: ConfigModelID.zod.optional(),
-    variant: z
-      .string()
-      .optional()
-      .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
-    temperature: z.number().optional(),
-    top_p: z.number().optional(),
-    prompt: z.string().optional(),
-    tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
-    disable: z.boolean().optional(),
-    description: z.string().optional().describe("Description of when to use the agent"),
-    mode: z.enum(["subagent", "primary", "all"]).optional(),
-    hidden: z
-      .boolean()
-      .optional()
-      .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
-    options: z.record(z.string(), z.any()).optional(),
-    color: z
-      .union([
-        z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
-        z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
-      ])
-      .optional()
-      .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
-    steps: z
-      .number()
-      .int()
-      .positive()
-      .optional()
-      .describe("Maximum number of agentic iterations before forcing text-only response"),
-    maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
-    permission: ConfigPermission.Info.optional(),
-  })
-  .catchall(z.any())
-  .transform((agent, _ctx) => {
-    const knownKeys = new Set([
-      "name",
-      "model",
-      "variant",
-      "prompt",
-      "description",
-      "temperature",
-      "top_p",
-      "mode",
-      "hidden",
-      "color",
-      "steps",
-      "maxSteps",
-      "options",
-      "permission",
-      "disable",
-      "tools",
-    ])
-
-    const options: Record<string, unknown> = { ...agent.options }
-    for (const [key, value] of Object.entries(agent)) {
-      if (!knownKeys.has(key)) options[key] = value
-    }
+const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
 
-    const permission: ConfigPermission.Info = {}
-    for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
-      const action = enabled ? "allow" : "deny"
-      if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
-        permission.edit = action
-        continue
-      }
-      permission[tool] = action
-    }
-    Object.assign(permission, agent.permission)
+const Color = Schema.Union([
+  Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
+  Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
+])
+
+// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
+// shape lives outside the Effect Schema type system), so the walker reaches it
+// via ZodOverride rather than a pure Schema reference.  This preserves the
+// `$ref: PermissionConfig` emitted in openapi.json.
+const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
+
+const AgentSchema = Schema.StructWithRest(
+  Schema.Struct({
+    model: Schema.optional(ConfigModelID),
+    variant: Schema.optional(Schema.String).annotate({
+      description: "Default model variant for this agent (applies only when using the agent's configured model).",
+    }),
+    temperature: Schema.optional(Schema.Number),
+    top_p: Schema.optional(Schema.Number),
+    prompt: Schema.optional(Schema.String),
+    tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
+      description: "@deprecated Use 'permission' field instead",
+    }),
+    disable: Schema.optional(Schema.Boolean),
+    description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
+    mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
+    hidden: Schema.optional(Schema.Boolean).annotate({
+      description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
+    }),
+    options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+    color: Schema.optional(Color).annotate({
+      description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
+    }),
+    steps: Schema.optional(PositiveInt).annotate({
+      description: "Maximum number of agentic iterations before forcing text-only response",
+    }),
+    maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
+    permission: Schema.optional(PermissionRef),
+  }),
+  [Schema.Record(Schema.String, Schema.Any)],
+)
 
-    const steps = agent.steps ?? agent.maxSteps
+const KNOWN_KEYS = new Set([
+  "name",
+  "model",
+  "variant",
+  "prompt",
+  "description",
+  "temperature",
+  "top_p",
+  "mode",
+  "hidden",
+  "color",
+  "steps",
+  "maxSteps",
+  "options",
+  "permission",
+  "disable",
+  "tools",
+])
 
-    return { ...agent, options, permission, steps } as typeof agent & {
-      options?: Record<string, unknown>
-      permission?: ConfigPermission.Info
-      steps?: number
+// Post-parse normalisation:
+//  - Promote any unknown-but-present keys into `options` so they survive the
+//    round-trip in a well-known field.
+//  - Translate the deprecated `tools: { name: boolean }` map into the new
+//    `permission` shape (write-adjacent tools collapse into `permission.edit`).
+//  - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
+const normalize = (agent: z.infer<typeof Info>) => {
+  const options: Record<string, unknown> = { ...agent.options }
+  for (const [key, value] of Object.entries(agent)) {
+    if (!KNOWN_KEYS.has(key)) options[key] = value
+  }
+
+  const permission: ConfigPermission.Info = {}
+  for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
+    const action = enabled ? "allow" : "deny"
+    if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
+      permission.edit = action
+      continue
     }
-  })
-  .meta({
-    ref: "AgentConfig",
-  })
+    permission[tool] = action
+  }
+  globalThis.Object.assign(permission, agent.permission)
+
+  return { ...agent, options, permission, steps: agent.steps ?? agent.maxSteps }
+}
+
+export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType<
+  Omit<z.infer<ReturnType<typeof zod<typeof AgentSchema>>>, "options" | "permission" | "steps"> & {
+    options?: Record<string, unknown>
+    permission?: ConfigPermission.Info
+    steps?: number
+  }
+>
 export type Info = z.infer<typeof Info>
 
 export async function load(dir: string) {

+ 185 - 166
packages/opencode/src/config/config.ts

@@ -21,22 +21,25 @@ import { isRecord } from "@/util/record"
 import type { ConsoleState } from "./console-state"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { InstanceState } from "@/effect"
-import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
+import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
 import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
 import { InstanceRef } from "@/effect/instance-ref"
+import { zod, ZodOverride } from "@/util/effect-zod"
 import { ConfigAgent } from "./agent"
+import { ConfigCommand } from "./command"
+import { ConfigFormatter } from "./formatter"
+import { ConfigLayout } from "./layout"
+import { ConfigLSP } from "./lsp"
+import { ConfigManaged } from "./managed"
 import { ConfigMCP } from "./mcp"
 import { ConfigModelID } from "./model-id"
-import { ConfigPlugin } from "./plugin"
-import { ConfigManaged } from "./managed"
-import { ConfigCommand } from "./command"
 import { ConfigParse } from "./parse"
+import { ConfigPaths } from "./paths"
 import { ConfigPermission } from "./permission"
+import { ConfigPlugin } from "./plugin"
 import { ConfigProvider } from "./provider"
+import { ConfigServer } from "./server"
 import { ConfigSkills } from "./skills"
-import { ConfigPaths } from "./paths"
-import { ConfigFormatter } from "./formatter"
-import { ConfigLSP } from "./lsp"
 import { ConfigVariable } from "./variable"
 import { Npm } from "@/npm"
 
@@ -73,170 +76,186 @@ async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(
   return config
 }
 
-export const Server = z
-  .object({
-    port: z.number().int().positive().optional().describe("Port to listen on"),
-    hostname: z.string().optional().describe("Hostname to listen on"),
-    mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
-    mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"),
-    cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
-  })
-  .strict()
-  .meta({
-    ref: "ServerConfig",
-  })
+export const Server = ConfigServer.Server.zod
+export const Layout = ConfigLayout.Layout.zod
+export type Layout = ConfigLayout.Layout
 
-export const Layout = z.enum(["auto", "stretch"]).meta({
-  ref: "LayoutConfig",
-})
-export type Layout = z.infer<typeof Layout>
-
-export const Info = z
-  .object({
-    $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
-    logLevel: Log.Level.optional().describe("Log level"),
-    server: Server.optional().describe("Server configuration for opencode serve and web commands"),
-    command: z
-      .record(z.string(), ConfigCommand.Info.zod)
-      .optional()
-      .describe("Command configuration, see https://opencode.ai/docs/commands"),
-    skills: ConfigSkills.Info.zod.optional().describe("Additional skill folder paths"),
-    watcher: z
-      .object({
-        ignore: z.array(z.string()).optional(),
-      })
-      .optional(),
-    snapshot: z
-      .boolean()
-      .optional()
-      .describe(
-        "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
-      ),
-    // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
-    plugin: ConfigPlugin.Spec.zod.array().optional(),
-    share: z
-      .enum(["manual", "auto", "disabled"])
-      .optional()
-      .describe(
-        "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
-      ),
-    autoshare: z
-      .boolean()
-      .optional()
-      .describe("@deprecated Use 'share' field instead. Share newly created sessions automatically"),
-    autoupdate: z
-      .union([z.boolean(), z.literal("notify")])
-      .optional()
-      .describe(
-        "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
-      ),
-    disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
-    enabled_providers: z
-      .array(z.string())
-      .optional()
-      .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
-    model: ConfigModelID.zod.describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
-    small_model: ConfigModelID.zod
-      .describe("Small model to use for tasks like title generation in the format of provider/model")
-      .optional(),
-    default_agent: z
-      .string()
-      .optional()
-      .describe(
-        "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
-      ),
-    username: z.string().optional().describe("Custom username to display in conversations instead of system username"),
-    mode: z
-      .object({
-        build: ConfigAgent.Info.optional(),
-        plan: ConfigAgent.Info.optional(),
-      })
-      .catchall(ConfigAgent.Info)
-      .optional()
-      .describe("@deprecated Use `agent` field instead."),
-    agent: z
-      .object({
+// Schemas that still live at the zod layer (have .transform / .preprocess /
+// .meta not expressible in current Effect Schema) get referenced via a
+// ZodOverride-annotated Schema.Any.  Walker sees the annotation and emits the
+// exact zod directly, preserving component $refs.
+const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
+const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
+const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
+
+const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
+const NonNegativeInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))
+
+const InfoSchema = Schema.Struct({
+  $schema: Schema.optional(Schema.String).annotate({
+    description: "JSON schema reference for configuration validation",
+  }),
+  logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
+  server: Schema.optional(ConfigServer.Server).annotate({
+    description: "Server configuration for opencode serve and web commands",
+  }),
+  command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
+    description: "Command configuration, see https://opencode.ai/docs/commands",
+  }),
+  skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
+  watcher: Schema.optional(
+    Schema.Struct({
+      ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+    }),
+  ),
+  snapshot: Schema.optional(Schema.Boolean).annotate({
+    description:
+      "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
+  }),
+  // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
+  plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
+  share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
+    description:
+      "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
+  }),
+  autoshare: Schema.optional(Schema.Boolean).annotate({
+    description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
+  }),
+  autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
+    description:
+      "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
+  }),
+  disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
+    description: "Disable providers that are loaded automatically",
+  }),
+  enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
+    description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
+  }),
+  model: Schema.optional(ConfigModelID).annotate({
+    description: "Model to use in the format of provider/model, eg anthropic/claude-2",
+  }),
+  small_model: Schema.optional(ConfigModelID).annotate({
+    description: "Small model to use for tasks like title generation in the format of provider/model",
+  }),
+  default_agent: Schema.optional(Schema.String).annotate({
+    description:
+      "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
+  }),
+  username: Schema.optional(Schema.String).annotate({
+    description: "Custom username to display in conversations instead of system username",
+  }),
+  mode: Schema.optional(
+    Schema.StructWithRest(
+      Schema.Struct({
+        build: Schema.optional(AgentRef),
+        plan: Schema.optional(AgentRef),
+      }),
+      [Schema.Record(Schema.String, AgentRef)],
+    ),
+  ).annotate({ description: "@deprecated Use `agent` field instead." }),
+  agent: Schema.optional(
+    Schema.StructWithRest(
+      Schema.Struct({
         // primary
-        plan: ConfigAgent.Info.optional(),
-        build: ConfigAgent.Info.optional(),
+        plan: Schema.optional(AgentRef),
+        build: Schema.optional(AgentRef),
         // subagent
-        general: ConfigAgent.Info.optional(),
-        explore: ConfigAgent.Info.optional(),
+        general: Schema.optional(AgentRef),
+        explore: Schema.optional(AgentRef),
         // specialized
-        title: ConfigAgent.Info.optional(),
-        summary: ConfigAgent.Info.optional(),
-        compaction: ConfigAgent.Info.optional(),
-      })
-      .catchall(ConfigAgent.Info)
-      .optional()
-      .describe("Agent configuration, see https://opencode.ai/docs/agents"),
-    provider: z
-      .record(z.string(), ConfigProvider.Info)
-      .optional()
-      .describe("Custom provider configurations and model overrides"),
-    mcp: z
-      .record(
-        z.string(),
-        z.union([
-          ConfigMCP.Info.zod,
-          z
-            .object({
-              enabled: z.boolean(),
-            })
-            .strict(),
-        ]),
-      )
-      .optional()
-      .describe("MCP (Model Context Protocol) server configurations"),
-    formatter: ConfigFormatter.Info.zod.optional(),
-    lsp: ConfigLSP.Info.zod.optional(),
-    instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
-    layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
-    permission: ConfigPermission.Info.optional(),
-    tools: z.record(z.string(), z.boolean()).optional(),
-    enterprise: z
-      .object({
-        url: z.string().optional().describe("Enterprise URL"),
-      })
-      .optional(),
-    compaction: z
-      .object({
-        auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
-        prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
-        reserved: z
-          .number()
-          .int()
-          .min(0)
-          .optional()
-          .describe("Token buffer for compaction. Leaves enough window to avoid overflow during compaction."),
-      })
-      .optional(),
-    experimental: z
-      .object({
-        disable_paste_summary: z.boolean().optional(),
-        batch_tool: z.boolean().optional().describe("Enable the batch tool"),
-        openTelemetry: z
-          .boolean()
-          .optional()
-          .describe("Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)"),
-        primary_tools: z
-          .array(z.string())
-          .optional()
-          .describe("Tools that should only be available to primary agents."),
-        continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
-        mcp_timeout: z
-          .number()
-          .int()
-          .positive()
-          .optional()
-          .describe("Timeout in milliseconds for model context protocol (MCP) requests"),
-      })
-      .optional(),
-  })
+        title: Schema.optional(AgentRef),
+        summary: Schema.optional(AgentRef),
+        compaction: Schema.optional(AgentRef),
+      }),
+      [Schema.Record(Schema.String, AgentRef)],
+    ),
+  ).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
+  provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({
+    description: "Custom provider configurations and model overrides",
+  }),
+  mcp: Schema.optional(
+    Schema.Record(
+      Schema.String,
+      Schema.Union([
+        ConfigMCP.Info,
+        // Matches the legacy `{ enabled: false }` form used to disable a server.
+        Schema.Any.annotate({ [ZodOverride]: z.object({ enabled: z.boolean() }).strict() }),
+      ]),
+    ),
+  ).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
+  formatter: Schema.optional(ConfigFormatter.Info),
+  lsp: Schema.optional(ConfigLSP.Info),
+  instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
+    description: "Additional instruction files or patterns to include",
+  }),
+  layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
+  permission: Schema.optional(PermissionRef),
+  tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
+  enterprise: Schema.optional(
+    Schema.Struct({
+      url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
+    }),
+  ),
+  compaction: Schema.optional(
+    Schema.Struct({
+      auto: Schema.optional(Schema.Boolean).annotate({
+        description: "Enable automatic compaction when context is full (default: true)",
+      }),
+      prune: Schema.optional(Schema.Boolean).annotate({
+        description: "Enable pruning of old tool outputs (default: true)",
+      }),
+      reserved: Schema.optional(NonNegativeInt).annotate({
+        description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
+      }),
+    }),
+  ),
+  experimental: Schema.optional(
+    Schema.Struct({
+      disable_paste_summary: Schema.optional(Schema.Boolean),
+      batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
+      openTelemetry: Schema.optional(Schema.Boolean).annotate({
+        description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
+      }),
+      primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
+        description: "Tools that should only be available to primary agents.",
+      }),
+      continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
+        description: "Continue the agent loop when a tool call is denied",
+      }),
+      mcp_timeout: Schema.optional(PositiveInt).annotate({
+        description: "Timeout in milliseconds for model context protocol (MCP) requests",
+      }),
+    }),
+  ),
+})
+
+// Schema.Struct produces readonly types by default, but the service code
+// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
+// readonly recursively so callers get the same mutable shape zod inferred.
+//
+// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
+// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
+// (since `keyof unknown = never`), which widens `Record<string, unknown>`
+// fields like `ConfigPlugin.Options`. The local version gates on
+// `extends object` so `unknown` passes through.
+//
+// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
+// shape (otherwise the general array branch widens it to an array).
+type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
+  ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
+  : T extends readonly (infer U)[]
+    ? DeepMutable<U>[]
+    : T extends object
+      ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
+      : T
+
+// The walker emits `z.object({...})` which is non-strict by default. Config
+// historically uses `.strict()` (additionalProperties: false in openapi.json),
+// so layer that on after derivation.  Re-apply the Config ref afterward
+// since `.strict()` strips the walker's meta annotation.
+export const Info = (zod(InfoSchema) as unknown as z.ZodObject<any>)
   .strict()
-  .meta({
-    ref: "Config",
-  })
+  .meta({ ref: "Config" }) as unknown as z.ZodType<DeepMutable<Schema.Schema.Type<typeof InfoSchema>>>
 
 export type Info = z.output<typeof Info> & {
   // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together

+ 124 - 163
packages/opencode/src/config/keybinds.ts

@@ -1,166 +1,127 @@
 export * as ConfigKeybinds from "./keybinds"
 
-import z from "zod"
+import { Effect, Schema } from "effect"
+import type z from "zod"
+import { zod } from "@/util/effect-zod"
 
-export const Keybinds = z
-  .object({
-    leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
-    app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
-    editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
-    theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
-    sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
-    scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
-    username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
-    status_view: z.string().optional().default("<leader>s").describe("View status"),
-    session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
-    session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
-    session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
-    session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
-    session_fork: z.string().optional().default("none").describe("Fork session from message"),
-    session_rename: z.string().optional().default("ctrl+r").describe("Rename session"),
-    session_delete: z.string().optional().default("ctrl+d").describe("Delete session"),
-    stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"),
-    model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"),
-    model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"),
-    session_share: z.string().optional().default("none").describe("Share current session"),
-    session_unshare: z.string().optional().default("none").describe("Unshare current session"),
-    session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
-    session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
-    messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"),
-    messages_page_down: z
-      .string()
-      .optional()
-      .default("pagedown,ctrl+alt+f")
-      .describe("Scroll messages down by one page"),
-    messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"),
-    messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"),
-    messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
-    messages_half_page_down: z.string().optional().default("ctrl+alt+d").describe("Scroll messages down by half page"),
-    messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
-    messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
-    messages_next: z.string().optional().default("none").describe("Navigate to next message"),
-    messages_previous: z.string().optional().default("none").describe("Navigate to previous message"),
-    messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"),
-    messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
-    messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
-    messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
-    messages_toggle_conceal: z
-      .string()
-      .optional()
-      .default("<leader>h")
-      .describe("Toggle code block concealment in messages"),
-    tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"),
-    model_list: z.string().optional().default("<leader>m").describe("List available models"),
-    model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
-    model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
-    model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
-    model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
-    command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
-    agent_list: z.string().optional().default("<leader>a").describe("List agents"),
-    agent_cycle: z.string().optional().default("tab").describe("Next agent"),
-    agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
-    variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
-    variant_list: z.string().optional().default("none").describe("List model variants"),
-    input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
-    input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
-    input_submit: z.string().optional().default("return").describe("Submit input"),
-    input_newline: z
-      .string()
-      .optional()
-      .default("shift+return,ctrl+return,alt+return,ctrl+j")
-      .describe("Insert newline in input"),
-    input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
-    input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
-    input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
-    input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
-    input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
-    input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
-    input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
-    input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
-    input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
-    input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
-    input_select_line_home: z.string().optional().default("ctrl+shift+a").describe("Select to start of line in input"),
-    input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
-    input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
-    input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
-    input_select_visual_line_home: z
-      .string()
-      .optional()
-      .default("alt+shift+a")
-      .describe("Select to start of visual line in input"),
-    input_select_visual_line_end: z
-      .string()
-      .optional()
-      .default("alt+shift+e")
-      .describe("Select to end of visual line in input"),
-    input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
-    input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
-    input_select_buffer_home: z
-      .string()
-      .optional()
-      .default("shift+home")
-      .describe("Select to start of buffer in input"),
-    input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
-    input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
-    input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
-    input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
-    input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
-    input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
-    input_undo: z
-      .string()
-      .optional()
-      // On Windows prepend ctrl+z since terminal_suspend releases the binding.
-      .default(process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z")
-      .describe("Undo in input"),
-    input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
-    input_word_forward: z
-      .string()
-      .optional()
-      .default("alt+f,alt+right,ctrl+right")
-      .describe("Move word forward in input"),
-    input_word_backward: z
-      .string()
-      .optional()
-      .default("alt+b,alt+left,ctrl+left")
-      .describe("Move word backward in input"),
-    input_select_word_forward: z
-      .string()
-      .optional()
-      .default("alt+shift+f,alt+shift+right")
-      .describe("Select word forward in input"),
-    input_select_word_backward: z
-      .string()
-      .optional()
-      .default("alt+shift+b,alt+shift+left")
-      .describe("Select word backward in input"),
-    input_delete_word_forward: z
-      .string()
-      .optional()
-      .default("alt+d,alt+delete,ctrl+delete")
-      .describe("Delete word forward in input"),
-    input_delete_word_backward: z
-      .string()
-      .optional()
-      .default("ctrl+w,ctrl+backspace,alt+backspace")
-      .describe("Delete word backward in input"),
-    history_previous: z.string().optional().default("up").describe("Previous history item"),
-    history_next: z.string().optional().default("down").describe("Next history item"),
-    session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"),
-    session_child_cycle: z.string().optional().default("right").describe("Go to next child session"),
-    session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"),
-    session_parent: z.string().optional().default("up").describe("Go to parent session"),
-    terminal_suspend: z
-      .string()
-      .optional()
-      .default("ctrl+z")
-      .transform((v) => (process.platform === "win32" ? "none" : v))
-      .describe("Suspend terminal"),
-    terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
-    tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
-    plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"),
-    display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
-  })
-  .strict()
-  .meta({
-    ref: "KeybindsConfig",
-  })
+// Every keybind field has the same shape: an optional string with a default
+// binding and a human description.  `keybind()` keeps the declaration list
+// below dense and readable.
+const keybind = (value: string, description: string) =>
+  Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(value))).annotate({ description })
+
+// Windows prepends ctrl+z to the undo binding because `terminal_suspend`
+// cannot consume ctrl+z on native Windows terminals (no POSIX suspend).
+const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z"
+
+const KeybindsSchema = Schema.Struct({
+  leader: keybind("ctrl+x", "Leader key for keybind combinations"),
+  app_exit: keybind("ctrl+c,ctrl+d,<leader>q", "Exit the application"),
+  editor_open: keybind("<leader>e", "Open external editor"),
+  theme_list: keybind("<leader>t", "List available themes"),
+  sidebar_toggle: keybind("<leader>b", "Toggle sidebar"),
+  scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
+  username_toggle: keybind("none", "Toggle username visibility"),
+  status_view: keybind("<leader>s", "View status"),
+  session_export: keybind("<leader>x", "Export session to editor"),
+  session_new: keybind("<leader>n", "Create a new session"),
+  session_list: keybind("<leader>l", "List all sessions"),
+  session_timeline: keybind("<leader>g", "Show session timeline"),
+  session_fork: keybind("none", "Fork session from message"),
+  session_rename: keybind("ctrl+r", "Rename session"),
+  session_delete: keybind("ctrl+d", "Delete session"),
+  stash_delete: keybind("ctrl+d", "Delete stash entry"),
+  model_provider_list: keybind("ctrl+a", "Open provider list from model dialog"),
+  model_favorite_toggle: keybind("ctrl+f", "Toggle model favorite status"),
+  session_share: keybind("none", "Share current session"),
+  session_unshare: keybind("none", "Unshare current session"),
+  session_interrupt: keybind("escape", "Interrupt current session"),
+  session_compact: keybind("<leader>c", "Compact the session"),
+  messages_page_up: keybind("pageup,ctrl+alt+b", "Scroll messages up by one page"),
+  messages_page_down: keybind("pagedown,ctrl+alt+f", "Scroll messages down by one page"),
+  messages_line_up: keybind("ctrl+alt+y", "Scroll messages up by one line"),
+  messages_line_down: keybind("ctrl+alt+e", "Scroll messages down by one line"),
+  messages_half_page_up: keybind("ctrl+alt+u", "Scroll messages up by half page"),
+  messages_half_page_down: keybind("ctrl+alt+d", "Scroll messages down by half page"),
+  messages_first: keybind("ctrl+g,home", "Navigate to first message"),
+  messages_last: keybind("ctrl+alt+g,end", "Navigate to last message"),
+  messages_next: keybind("none", "Navigate to next message"),
+  messages_previous: keybind("none", "Navigate to previous message"),
+  messages_last_user: keybind("none", "Navigate to last user message"),
+  messages_copy: keybind("<leader>y", "Copy message"),
+  messages_undo: keybind("<leader>u", "Undo message"),
+  messages_redo: keybind("<leader>r", "Redo message"),
+  messages_toggle_conceal: keybind("<leader>h", "Toggle code block concealment in messages"),
+  tool_details: keybind("none", "Toggle tool details visibility"),
+  model_list: keybind("<leader>m", "List available models"),
+  model_cycle_recent: keybind("f2", "Next recently used model"),
+  model_cycle_recent_reverse: keybind("shift+f2", "Previous recently used model"),
+  model_cycle_favorite: keybind("none", "Next favorite model"),
+  model_cycle_favorite_reverse: keybind("none", "Previous favorite model"),
+  command_list: keybind("ctrl+p", "List available commands"),
+  agent_list: keybind("<leader>a", "List agents"),
+  agent_cycle: keybind("tab", "Next agent"),
+  agent_cycle_reverse: keybind("shift+tab", "Previous agent"),
+  variant_cycle: keybind("ctrl+t", "Cycle model variants"),
+  variant_list: keybind("none", "List model variants"),
+  input_clear: keybind("ctrl+c", "Clear input field"),
+  input_paste: keybind("ctrl+v", "Paste from clipboard"),
+  input_submit: keybind("return", "Submit input"),
+  input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"),
+  input_move_left: keybind("left,ctrl+b", "Move cursor left in input"),
+  input_move_right: keybind("right,ctrl+f", "Move cursor right in input"),
+  input_move_up: keybind("up", "Move cursor up in input"),
+  input_move_down: keybind("down", "Move cursor down in input"),
+  input_select_left: keybind("shift+left", "Select left in input"),
+  input_select_right: keybind("shift+right", "Select right in input"),
+  input_select_up: keybind("shift+up", "Select up in input"),
+  input_select_down: keybind("shift+down", "Select down in input"),
+  input_line_home: keybind("ctrl+a", "Move to start of line in input"),
+  input_line_end: keybind("ctrl+e", "Move to end of line in input"),
+  input_select_line_home: keybind("ctrl+shift+a", "Select to start of line in input"),
+  input_select_line_end: keybind("ctrl+shift+e", "Select to end of line in input"),
+  input_visual_line_home: keybind("alt+a", "Move to start of visual line in input"),
+  input_visual_line_end: keybind("alt+e", "Move to end of visual line in input"),
+  input_select_visual_line_home: keybind("alt+shift+a", "Select to start of visual line in input"),
+  input_select_visual_line_end: keybind("alt+shift+e", "Select to end of visual line in input"),
+  input_buffer_home: keybind("home", "Move to start of buffer in input"),
+  input_buffer_end: keybind("end", "Move to end of buffer in input"),
+  input_select_buffer_home: keybind("shift+home", "Select to start of buffer in input"),
+  input_select_buffer_end: keybind("shift+end", "Select to end of buffer in input"),
+  input_delete_line: keybind("ctrl+shift+d", "Delete line in input"),
+  input_delete_to_line_end: keybind("ctrl+k", "Delete to end of line in input"),
+  input_delete_to_line_start: keybind("ctrl+u", "Delete to start of line in input"),
+  input_backspace: keybind("backspace,shift+backspace", "Backspace in input"),
+  input_delete: keybind("ctrl+d,delete,shift+delete", "Delete character in input"),
+  input_undo: keybind(inputUndoDefault, "Undo in input"),
+  input_redo: keybind("ctrl+.,super+shift+z", "Redo in input"),
+  input_word_forward: keybind("alt+f,alt+right,ctrl+right", "Move word forward in input"),
+  input_word_backward: keybind("alt+b,alt+left,ctrl+left", "Move word backward in input"),
+  input_select_word_forward: keybind("alt+shift+f,alt+shift+right", "Select word forward in input"),
+  input_select_word_backward: keybind("alt+shift+b,alt+shift+left", "Select word backward in input"),
+  input_delete_word_forward: keybind("alt+d,alt+delete,ctrl+delete", "Delete word forward in input"),
+  input_delete_word_backward: keybind("ctrl+w,ctrl+backspace,alt+backspace", "Delete word backward in input"),
+  history_previous: keybind("up", "Previous history item"),
+  history_next: keybind("down", "Next history item"),
+  session_child_first: keybind("<leader>down", "Go to first child session"),
+  session_child_cycle: keybind("right", "Go to next child session"),
+  session_child_cycle_reverse: keybind("left", "Go to previous child session"),
+  session_parent: keybind("up", "Go to parent session"),
+  // `terminal_suspend` was formerly `.default("ctrl+z").transform((v) => win32 ? "none" : v)`,
+  // but `tui.ts` already forces the binding to "none" on win32 before calling
+  // `Keybinds.parse(...)`, so the schema-level transform was redundant.
+  terminal_suspend: keybind("ctrl+z", "Suspend terminal"),
+  terminal_title_toggle: keybind("none", "Toggle terminal title"),
+  tips_toggle: keybind("<leader>h", "Toggle tips on home screen"),
+  plugin_manager: keybind("none", "Open plugin manager dialog"),
+  display_thinking: keybind("none", "Toggle thinking blocks visibility"),
+}).annotate({ identifier: "KeybindsConfig" })
+
+export type Keybinds = Schema.Schema.Type<typeof KeybindsSchema>
+
+// Consumers access `Keybinds.shape` and `Keybinds.shape.X.parse(undefined)`,
+// which requires the runtime type to be a ZodObject, not just ZodType.  Every
+// field is `string().optional().default(...)` at runtime, so widen to that.
+export const Keybinds = zod(KeybindsSchema) as unknown as z.ZodObject<
+  Record<keyof Keybinds, z.ZodDefault<z.ZodOptional<z.ZodString>>>
+>

+ 10 - 0
packages/opencode/src/config/layout.ts

@@ -0,0 +1,10 @@
+import { Schema } from "effect"
+import { zod } from "@/util/effect-zod"
+import { withStatics } from "@/util/schema"
+
+export const Layout = Schema.Literals(["auto", "stretch"])
+  .annotate({ identifier: "LayoutConfig" })
+  .pipe(withStatics((s) => ({ zod: zod(s) })))
+export type Layout = Schema.Schema.Type<typeof Layout>
+
+export * as ConfigLayout from "./layout"

+ 45 - 40
packages/opencode/src/config/permission.ts

@@ -1,16 +1,8 @@
 export * as ConfigPermission from "./permission"
 import { Schema } from "effect"
-import z from "zod"
-import { zod } from "@/util/effect-zod"
+import { zod, ZodPreprocess } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 
-const permissionPreprocess = (val: unknown) => {
-  if (typeof val === "object" && val !== null && !Array.isArray(val)) {
-    return { __originalKeys: globalThis.Object.keys(val), ...val }
-  }
-  return val
-}
-
 export const Action = Schema.Literals(["ask", "allow", "deny"])
   .annotate({ identifier: "PermissionActionConfig" })
   .pipe(withStatics((s) => ({ zod: zod(s) })))
@@ -26,6 +18,48 @@ export const Rule = Schema.Union([Action, Object])
   .pipe(withStatics((s) => ({ zod: zod(s) })))
 export type Rule = Schema.Schema.Type<typeof Rule>
 
+// Captures the user's original property insertion order before Schema.Struct
+// canonicalises the object.  See the `ZodPreprocess` comment in
+// `util/effect-zod.ts` for the full rationale — in short: rule precedence is
+// encoded in JSON key order (`evaluate.ts` uses `findLast`, so later keys win)
+// and `Schema.StructWithRest` would otherwise drop that order.
+const permissionPreprocess = (val: unknown) => {
+  if (typeof val === "object" && val !== null && !Array.isArray(val)) {
+    return { __originalKeys: globalThis.Object.keys(val), ...val }
+  }
+  return val
+}
+
+const ObjectShape = Schema.StructWithRest(
+  Schema.Struct({
+    __originalKeys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+    read: Schema.optional(Rule),
+    edit: Schema.optional(Rule),
+    glob: Schema.optional(Rule),
+    grep: Schema.optional(Rule),
+    list: Schema.optional(Rule),
+    bash: Schema.optional(Rule),
+    task: Schema.optional(Rule),
+    external_directory: Schema.optional(Rule),
+    todowrite: Schema.optional(Action),
+    question: Schema.optional(Action),
+    webfetch: Schema.optional(Action),
+    websearch: Schema.optional(Action),
+    codesearch: Schema.optional(Action),
+    lsp: Schema.optional(Rule),
+    doom_loop: Schema.optional(Action),
+    skill: Schema.optional(Rule),
+  }),
+  [Schema.Record(Schema.String, Rule)],
+)
+
+const InnerSchema = Schema.Union([ObjectShape, Action]).annotate({
+  [ZodPreprocess]: permissionPreprocess,
+})
+
+// Post-parse: drop the __originalKeys metadata and rebuild the rule map in the
+// user's original insertion order.  A plain string input (the Action branch of
+// the union) becomes `{ "*": action }`.
 const transform = (x: unknown): Record<string, Rule> => {
   if (typeof x === "string") return { "*": x as Action }
   const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
@@ -38,34 +72,5 @@ const transform = (x: unknown): Record<string, Rule> => {
   return result
 }
 
-export const Info = z
-  .preprocess(
-    permissionPreprocess,
-    z
-      .object({
-        __originalKeys: z.string().array().optional(),
-        read: Rule.zod.optional(),
-        edit: Rule.zod.optional(),
-        glob: Rule.zod.optional(),
-        grep: Rule.zod.optional(),
-        list: Rule.zod.optional(),
-        bash: Rule.zod.optional(),
-        task: Rule.zod.optional(),
-        external_directory: Rule.zod.optional(),
-        todowrite: Action.zod.optional(),
-        question: Action.zod.optional(),
-        webfetch: Action.zod.optional(),
-        websearch: Action.zod.optional(),
-        codesearch: Action.zod.optional(),
-        lsp: Rule.zod.optional(),
-        doom_loop: Action.zod.optional(),
-        skill: Rule.zod.optional(),
-      })
-      .catchall(Rule.zod)
-      .or(Action.zod),
-  )
-  .transform(transform)
-  .meta({
-    ref: "PermissionConfig",
-  })
-export type Info = z.infer<typeof Info>
+export const Info = zod(InnerSchema).transform(transform).meta({ ref: "PermissionConfig" })
+export type Info = Record<string, Rule>

+ 107 - 113
packages/opencode/src/config/provider.ts

@@ -1,120 +1,114 @@
-import z from "zod"
+import { Schema } from "effect"
+import { zod } from "@/util/effect-zod"
+import { withStatics } from "@/util/schema"
 
-export const Model = z
-  .object({
-    id: z.string(),
-    name: z.string(),
-    family: z.string().optional(),
-    release_date: z.string(),
-    attachment: z.boolean(),
-    reasoning: z.boolean(),
-    temperature: z.boolean(),
-    tool_call: z.boolean(),
-    interleaved: z
-      .union([
-        z.literal(true),
-        z
-          .object({
-            field: z.enum(["reasoning_content", "reasoning_details"]),
-          })
-          .strict(),
-      ])
-      .optional(),
-    cost: z
-      .object({
-        input: z.number(),
-        output: z.number(),
-        cache_read: z.number().optional(),
-        cache_write: z.number().optional(),
-        context_over_200k: z
-          .object({
-            input: z.number(),
-            output: z.number(),
-            cache_read: z.number().optional(),
-            cache_write: z.number().optional(),
-          })
-          .optional(),
-      })
-      .optional(),
-    limit: z.object({
-      context: z.number(),
-      input: z.number().optional(),
-      output: z.number(),
+const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
+
+export const Model = Schema.Struct({
+  id: Schema.optional(Schema.String),
+  name: Schema.optional(Schema.String),
+  family: Schema.optional(Schema.String),
+  release_date: Schema.optional(Schema.String),
+  attachment: Schema.optional(Schema.Boolean),
+  reasoning: Schema.optional(Schema.Boolean),
+  temperature: Schema.optional(Schema.Boolean),
+  tool_call: Schema.optional(Schema.Boolean),
+  interleaved: Schema.optional(
+    Schema.Union([
+      Schema.Literal(true),
+      Schema.Struct({
+        field: Schema.Literals(["reasoning_content", "reasoning_details"]),
+      }),
+    ]),
+  ),
+  cost: Schema.optional(
+    Schema.Struct({
+      input: Schema.Number,
+      output: Schema.Number,
+      cache_read: Schema.optional(Schema.Number),
+      cache_write: Schema.optional(Schema.Number),
+      context_over_200k: Schema.optional(
+        Schema.Struct({
+          input: Schema.Number,
+          output: Schema.Number,
+          cache_read: Schema.optional(Schema.Number),
+          cache_write: Schema.optional(Schema.Number),
+        }),
+      ),
+    }),
+  ),
+  limit: Schema.optional(
+    Schema.Struct({
+      context: Schema.Number,
+      input: Schema.optional(Schema.Number),
+      output: Schema.Number,
     }),
-    modalities: z
-      .object({
-        input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
-        output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
-      })
-      .optional(),
-    experimental: z.boolean().optional(),
-    status: z.enum(["alpha", "beta", "deprecated"]).optional(),
-    provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
-    options: z.record(z.string(), z.any()),
-    headers: z.record(z.string(), z.string()).optional(),
-    variants: z
-      .record(
-        z.string(),
-        z
-          .object({
-            disabled: z.boolean().optional().describe("Disable this variant for the model"),
-          })
-          .catchall(z.any()),
-      )
-      .optional()
-      .describe("Variant-specific configuration"),
-  })
-  .partial()
+  ),
+  modalities: Schema.optional(
+    Schema.Struct({
+      input: Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))),
+      output: Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))),
+    }),
+  ),
+  experimental: Schema.optional(Schema.Boolean),
+  status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
+  provider: Schema.optional(
+    Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
+  ),
+  options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+  variants: Schema.optional(
+    Schema.Record(
+      Schema.String,
+      Schema.StructWithRest(
+        Schema.Struct({
+          disabled: Schema.optional(Schema.Boolean).annotate({ description: "Disable this variant for the model" }),
+        }),
+        [Schema.Record(Schema.String, Schema.Any)],
+      ),
+    ).annotate({ description: "Variant-specific configuration" }),
+  ),
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
 
-export const Info = z
-  .object({
-    api: z.string().optional(),
-    name: z.string(),
-    env: z.array(z.string()),
-    id: z.string(),
-    npm: z.string().optional(),
-    whitelist: z.array(z.string()).optional(),
-    blacklist: z.array(z.string()).optional(),
-    options: z
-      .object({
-        apiKey: z.string().optional(),
-        baseURL: z.string().optional(),
-        enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
-        setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"),
-        timeout: z
-          .union([
-            z
-              .number()
-              .int()
-              .positive()
-              .describe(
-                "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
-              ),
-            z.literal(false).describe("Disable timeout for this provider entirely."),
-          ])
-          .optional()
-          .describe(
+export class Info extends Schema.Class<Info>("ProviderConfig")({
+  api: Schema.optional(Schema.String),
+  name: Schema.optional(Schema.String),
+  env: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+  id: Schema.optional(Schema.String),
+  npm: Schema.optional(Schema.String),
+  whitelist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+  blacklist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+  options: Schema.optional(
+    Schema.StructWithRest(
+      Schema.Struct({
+        apiKey: Schema.optional(Schema.String),
+        baseURL: Schema.optional(Schema.String),
+        enterpriseUrl: Schema.optional(Schema.String).annotate({
+          description: "GitHub Enterprise URL for copilot authentication",
+        }),
+        setCacheKey: Schema.optional(Schema.Boolean).annotate({
+          description: "Enable promptCacheKey for this provider (default false)",
+        }),
+        timeout: Schema.optional(
+          Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
+            description:
+              "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
+          }),
+        ).annotate({
+          description:
             "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
-          ),
-        chunkTimeout: z
-          .number()
-          .int()
-          .positive()
-          .optional()
-          .describe(
+        }),
+        chunkTimeout: Schema.optional(PositiveInt).annotate({
+          description:
             "Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
-          ),
-      })
-      .catchall(z.any())
-      .optional(),
-    models: z.record(z.string(), Model).optional(),
-  })
-  .partial()
-  .strict()
-  .meta({
-    ref: "ProviderConfig",
-  })
-
-export type Info = z.infer<typeof Info>
+        }),
+      }),
+      [Schema.Record(Schema.String, Schema.Any)],
+    ),
+  ),
+  models: Schema.optional(Schema.Record(Schema.String, Model)),
+}) {
+  static readonly zod = zod(this)
+}
 
 export * as ConfigProvider from "./provider"

+ 20 - 0
packages/opencode/src/config/server.ts

@@ -0,0 +1,20 @@
+import { Schema } from "effect"
+import { zod } from "@/util/effect-zod"
+
+export class Server extends Schema.Class<Server>("ServerConfig")({
+  port: Schema.optional(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))).annotate({
+    description: "Port to listen on",
+  }),
+  hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }),
+  mdns: Schema.optional(Schema.Boolean).annotate({ description: "Enable mDNS service discovery" }),
+  mdnsDomain: Schema.optional(Schema.String).annotate({
+    description: "Custom domain name for mDNS service (default: opencode.local)",
+  }),
+  cors: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
+    description: "Additional domains to allow for CORS",
+  }),
+}) {
+  static readonly zod = zod(this)
+}
+
+export * as ConfigServer from "./server"

+ 1 - 0
packages/opencode/src/control-plane/workspace.ts

@@ -117,6 +117,7 @@ export const create = fn(CreateInput, async (input) => {
     OPENCODE_EXPERIMENTAL_WORKSPACES: "true",
     OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS,
     OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+    OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
   }
   await adaptor.create(config, env)
 

+ 7 - 1
packages/opencode/src/effect/logger.ts

@@ -3,6 +3,8 @@ import { Log } from "@/util"
 
 type Fields = Record<string, unknown>
 
+const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
+
 export interface Handle {
   readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
   readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
@@ -12,7 +14,11 @@ export interface Handle {
 }
 
 const clean = (input?: Fields): Fields =>
-  Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
+  Object.fromEntries(
+    Object.entries(input ?? {})
+      .filter((entry) => entry[1] !== undefined && entry[1] !== null)
+      .map(([key, value]) => [normalizeKey(key), value]),
+  )
 
 const text = (input: unknown): string => {
   // oxlint-disable-next-line no-base-to-string

Някои файлове не бяха показани, защото твърде много файлове са промени