@slkiser/opencode-quota 4.10.6 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/README.md +42 -31
  2. package/dist/bin/opencode-quota.d.ts +6 -0
  3. package/dist/bin/opencode-quota.d.ts.map +1 -1
  4. package/dist/bin/opencode-quota.js +17 -0
  5. package/dist/bin/opencode-quota.js.map +1 -1
  6. package/dist/data/modelsdev-pricing.min.json +83 -2
  7. package/dist/index.d.ts +1 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -13
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/accounting-format.js +1 -1
  12. package/dist/lib/accounting-format.js.map +1 -1
  13. package/dist/lib/alibaba-auth.d.ts +5 -5
  14. package/dist/lib/alibaba-auth.d.ts.map +1 -1
  15. package/dist/lib/alibaba-auth.js +15 -8
  16. package/dist/lib/alibaba-auth.js.map +1 -1
  17. package/dist/lib/alibaba-token-plan.d.ts +7 -4
  18. package/dist/lib/alibaba-token-plan.d.ts.map +1 -1
  19. package/dist/lib/alibaba-token-plan.js +32 -10
  20. package/dist/lib/alibaba-token-plan.js.map +1 -1
  21. package/dist/lib/anthropic-auth.d.ts +4 -1
  22. package/dist/lib/anthropic-auth.d.ts.map +1 -1
  23. package/dist/lib/anthropic-auth.js +5 -1
  24. package/dist/lib/anthropic-auth.js.map +1 -1
  25. package/dist/lib/anthropic.d.ts +7 -1
  26. package/dist/lib/anthropic.d.ts.map +1 -1
  27. package/dist/lib/anthropic.js +61 -7
  28. package/dist/lib/anthropic.js.map +1 -1
  29. package/dist/lib/api-key-resolver.d.ts +27 -16
  30. package/dist/lib/api-key-resolver.d.ts.map +1 -1
  31. package/dist/lib/api-key-resolver.js +46 -19
  32. package/dist/lib/api-key-resolver.js.map +1 -1
  33. package/dist/lib/chutes-config.d.ts +2 -2
  34. package/dist/lib/chutes-config.d.ts.map +1 -1
  35. package/dist/lib/chutes-config.js +4 -4
  36. package/dist/lib/chutes-config.js.map +1 -1
  37. package/dist/lib/cli-reports.d.ts +30 -0
  38. package/dist/lib/cli-reports.d.ts.map +1 -0
  39. package/dist/lib/cli-reports.js +172 -0
  40. package/dist/lib/cli-reports.js.map +1 -0
  41. package/dist/lib/cli-show.d.ts +14 -1
  42. package/dist/lib/cli-show.d.ts.map +1 -1
  43. package/dist/lib/cli-show.js +40 -138
  44. package/dist/lib/cli-show.js.map +1 -1
  45. package/dist/lib/cli-status.d.ts.map +1 -1
  46. package/dist/lib/cli-status.js +9 -36
  47. package/dist/lib/cli-status.js.map +1 -1
  48. package/dist/lib/config-file-utils.d.ts +10 -13
  49. package/dist/lib/config-file-utils.d.ts.map +1 -1
  50. package/dist/lib/config-file-utils.js +28 -30
  51. package/dist/lib/config-file-utils.js.map +1 -1
  52. package/dist/lib/config.d.ts +2 -2
  53. package/dist/lib/config.d.ts.map +1 -1
  54. package/dist/lib/config.js +6 -17
  55. package/dist/lib/config.js.map +1 -1
  56. package/dist/lib/copilot.d.ts +4 -1
  57. package/dist/lib/copilot.d.ts.map +1 -1
  58. package/dist/lib/copilot.js +17 -19
  59. package/dist/lib/copilot.js.map +1 -1
  60. package/dist/lib/cursor-detection.d.ts +1 -1
  61. package/dist/lib/cursor-detection.d.ts.map +1 -1
  62. package/dist/lib/cursor-detection.js +54 -51
  63. package/dist/lib/cursor-detection.js.map +1 -1
  64. package/dist/lib/cursor-pricing.d.ts +1 -1
  65. package/dist/lib/cursor-pricing.d.ts.map +1 -1
  66. package/dist/lib/cursor-pricing.js +85 -0
  67. package/dist/lib/cursor-pricing.js.map +1 -1
  68. package/dist/lib/deepseek-auth.d.ts +2 -2
  69. package/dist/lib/deepseek-auth.d.ts.map +1 -1
  70. package/dist/lib/deepseek-auth.js +4 -4
  71. package/dist/lib/deepseek-auth.js.map +1 -1
  72. package/dist/lib/display-sanitize.js +1 -1
  73. package/dist/lib/display-sanitize.js.map +1 -1
  74. package/dist/lib/entries.d.ts +7 -2
  75. package/dist/lib/entries.d.ts.map +1 -1
  76. package/dist/lib/format-utils.d.ts +1 -1
  77. package/dist/lib/format-utils.js +1 -1
  78. package/dist/lib/glm-coding-plan.d.ts +1 -0
  79. package/dist/lib/glm-coding-plan.d.ts.map +1 -1
  80. package/dist/lib/glm-coding-plan.js +3 -1
  81. package/dist/lib/glm-coding-plan.js.map +1 -1
  82. package/dist/lib/google-agy.d.ts +6 -12
  83. package/dist/lib/google-agy.d.ts.map +1 -1
  84. package/dist/lib/google-agy.js +26 -24
  85. package/dist/lib/google-agy.js.map +1 -1
  86. package/dist/lib/google-companion-credentials.d.ts.map +1 -1
  87. package/dist/lib/google-companion-credentials.js +76 -26
  88. package/dist/lib/google-companion-credentials.js.map +1 -1
  89. package/dist/lib/google-gemini-cli.d.ts +7 -12
  90. package/dist/lib/google-gemini-cli.d.ts.map +1 -1
  91. package/dist/lib/google-gemini-cli.js +42 -31
  92. package/dist/lib/google-gemini-cli.js.map +1 -1
  93. package/dist/lib/init-installer.d.ts +3 -1
  94. package/dist/lib/init-installer.d.ts.map +1 -1
  95. package/dist/lib/init-installer.js +69 -172
  96. package/dist/lib/init-installer.js.map +1 -1
  97. package/dist/lib/kilo-config.d.ts +2 -2
  98. package/dist/lib/kilo-config.d.ts.map +1 -1
  99. package/dist/lib/kilo-config.js +6 -5
  100. package/dist/lib/kilo-config.js.map +1 -1
  101. package/dist/lib/kimi-auth.d.ts +2 -2
  102. package/dist/lib/kimi-auth.d.ts.map +1 -1
  103. package/dist/lib/kimi-auth.js +4 -4
  104. package/dist/lib/kimi-auth.js.map +1 -1
  105. package/dist/lib/maintainer-announcements.d.ts.map +1 -1
  106. package/dist/lib/maintainer-announcements.js +5 -5
  107. package/dist/lib/maintainer-announcements.js.map +1 -1
  108. package/dist/lib/mimo-config.d.ts.map +1 -1
  109. package/dist/lib/mimo-config.js +2 -3
  110. package/dist/lib/mimo-config.js.map +1 -1
  111. package/dist/lib/minimax-auth.d.ts +6 -6
  112. package/dist/lib/minimax-auth.d.ts.map +1 -1
  113. package/dist/lib/minimax-auth.js +18 -17
  114. package/dist/lib/minimax-auth.js.map +1 -1
  115. package/dist/lib/nanogpt-config.d.ts +2 -2
  116. package/dist/lib/nanogpt-config.d.ts.map +1 -1
  117. package/dist/lib/nanogpt-config.js +4 -4
  118. package/dist/lib/nanogpt-config.js.map +1 -1
  119. package/dist/lib/ollama-cloud-config.d.ts +2 -2
  120. package/dist/lib/ollama-cloud-config.d.ts.map +1 -1
  121. package/dist/lib/ollama-cloud-config.js +6 -5
  122. package/dist/lib/ollama-cloud-config.js.map +1 -1
  123. package/dist/lib/openai.d.ts +7 -2
  124. package/dist/lib/openai.d.ts.map +1 -1
  125. package/dist/lib/openai.js +22 -8
  126. package/dist/lib/openai.js.map +1 -1
  127. package/dist/lib/opencode-auth-sqlite.d.ts +17 -0
  128. package/dist/lib/opencode-auth-sqlite.d.ts.map +1 -0
  129. package/dist/lib/opencode-auth-sqlite.js +105 -0
  130. package/dist/lib/opencode-auth-sqlite.js.map +1 -0
  131. package/dist/lib/opencode-auth.d.ts +127 -14
  132. package/dist/lib/opencode-auth.d.ts.map +1 -1
  133. package/dist/lib/opencode-auth.js +373 -46
  134. package/dist/lib/opencode-auth.js.map +1 -1
  135. package/dist/lib/opencode-config-providers.d.ts.map +1 -1
  136. package/dist/lib/opencode-config-providers.js +17 -18
  137. package/dist/lib/opencode-config-providers.js.map +1 -1
  138. package/dist/lib/opencode-console-auth.d.ts +47 -0
  139. package/dist/lib/opencode-console-auth.d.ts.map +1 -0
  140. package/dist/lib/opencode-console-auth.js +63 -0
  141. package/dist/lib/opencode-console-auth.js.map +1 -0
  142. package/dist/lib/opencode-db-path.d.ts +9 -0
  143. package/dist/lib/opencode-db-path.d.ts.map +1 -0
  144. package/dist/lib/opencode-db-path.js +16 -0
  145. package/dist/lib/opencode-db-path.js.map +1 -0
  146. package/dist/lib/opencode-go-auth.d.ts +4 -2
  147. package/dist/lib/opencode-go-auth.d.ts.map +1 -1
  148. package/dist/lib/opencode-go-auth.js +9 -5
  149. package/dist/lib/opencode-go-auth.js.map +1 -1
  150. package/dist/lib/opencode-go.d.ts +12 -0
  151. package/dist/lib/opencode-go.d.ts.map +1 -1
  152. package/dist/lib/opencode-go.js +113 -0
  153. package/dist/lib/opencode-go.js.map +1 -1
  154. package/dist/lib/opencode-runtime-paths.d.ts +6 -12
  155. package/dist/lib/opencode-runtime-paths.d.ts.map +1 -1
  156. package/dist/lib/opencode-runtime-paths.js +12 -82
  157. package/dist/lib/opencode-runtime-paths.js.map +1 -1
  158. package/dist/lib/opencode-sqlite.d.ts +4 -0
  159. package/dist/lib/opencode-sqlite.d.ts.map +1 -1
  160. package/dist/lib/opencode-sqlite.js +17 -52
  161. package/dist/lib/opencode-sqlite.js.map +1 -1
  162. package/dist/lib/opencode-storage.d.ts +4 -8
  163. package/dist/lib/opencode-storage.d.ts.map +1 -1
  164. package/dist/lib/opencode-storage.js +66 -90
  165. package/dist/lib/opencode-storage.js.map +1 -1
  166. package/dist/lib/opencode-version.d.ts +6 -0
  167. package/dist/lib/opencode-version.d.ts.map +1 -0
  168. package/dist/lib/opencode-version.js +26 -0
  169. package/dist/lib/opencode-version.js.map +1 -0
  170. package/dist/lib/opencode-zen.d.ts +10 -4
  171. package/dist/lib/opencode-zen.d.ts.map +1 -1
  172. package/dist/lib/opencode-zen.js +11 -11
  173. package/dist/lib/opencode-zen.js.map +1 -1
  174. package/dist/lib/provider-registration.d.ts +3 -4
  175. package/dist/lib/provider-registration.d.ts.map +1 -1
  176. package/dist/lib/provider-registration.js +3 -4
  177. package/dist/lib/provider-registration.js.map +1 -1
  178. package/dist/lib/quota-dialog-command-specs.d.ts +59 -0
  179. package/dist/lib/quota-dialog-command-specs.d.ts.map +1 -0
  180. package/dist/lib/quota-dialog-command-specs.js +145 -0
  181. package/dist/lib/quota-dialog-command-specs.js.map +1 -0
  182. package/dist/lib/quota-dialog-commands.d.ts +1 -25
  183. package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
  184. package/dist/lib/quota-dialog-commands.js +13 -132
  185. package/dist/lib/quota-dialog-commands.js.map +1 -1
  186. package/dist/lib/quota-export.d.ts.map +1 -1
  187. package/dist/lib/quota-export.js +1 -0
  188. package/dist/lib/quota-export.js.map +1 -1
  189. package/dist/lib/quota-providers-remote.d.ts +2 -2
  190. package/dist/lib/quota-providers-remote.d.ts.map +1 -1
  191. package/dist/lib/quota-providers-remote.js +6 -4
  192. package/dist/lib/quota-providers-remote.js.map +1 -1
  193. package/dist/lib/quota-render-data.d.ts +3 -0
  194. package/dist/lib/quota-render-data.d.ts.map +1 -1
  195. package/dist/lib/quota-render-data.js +2 -0
  196. package/dist/lib/quota-render-data.js.map +1 -1
  197. package/dist/lib/quota-report-message.d.ts +21 -0
  198. package/dist/lib/quota-report-message.d.ts.map +1 -0
  199. package/dist/lib/quota-report-message.js +40 -0
  200. package/dist/lib/quota-report-message.js.map +1 -0
  201. package/dist/lib/quota-runtime-context.d.ts +1 -0
  202. package/dist/lib/quota-runtime-context.d.ts.map +1 -1
  203. package/dist/lib/quota-runtime-context.js +1 -0
  204. package/dist/lib/quota-runtime-context.js.map +1 -1
  205. package/dist/lib/quota-state-codec.js +5 -5
  206. package/dist/lib/quota-state-codec.js.map +1 -1
  207. package/dist/lib/quota-stats.js +2 -1
  208. package/dist/lib/quota-stats.js.map +1 -1
  209. package/dist/lib/quota-status.d.ts +2 -10
  210. package/dist/lib/quota-status.d.ts.map +1 -1
  211. package/dist/lib/quota-status.js +33 -42
  212. package/dist/lib/quota-status.js.map +1 -1
  213. package/dist/lib/quota-surface-data.d.ts +26 -0
  214. package/dist/lib/quota-surface-data.d.ts.map +1 -0
  215. package/dist/lib/quota-surface-data.js +299 -0
  216. package/dist/lib/quota-surface-data.js.map +1 -0
  217. package/dist/lib/scoped-update-migration.d.ts +4 -4
  218. package/dist/lib/scoped-update-migration.d.ts.map +1 -1
  219. package/dist/lib/scoped-update-migration.js +11 -18
  220. package/dist/lib/scoped-update-migration.js.map +1 -1
  221. package/dist/lib/scoped-update.d.ts +5 -1
  222. package/dist/lib/scoped-update.d.ts.map +1 -1
  223. package/dist/lib/scoped-update.js +73 -51
  224. package/dist/lib/scoped-update.js.map +1 -1
  225. package/dist/lib/synthetic-config.d.ts +2 -2
  226. package/dist/lib/synthetic-config.d.ts.map +1 -1
  227. package/dist/lib/synthetic-config.js +4 -4
  228. package/dist/lib/synthetic-config.js.map +1 -1
  229. package/dist/lib/toast-format-grouped.js +1 -1
  230. package/dist/lib/toast-format-grouped.js.map +1 -1
  231. package/dist/lib/tui-panel-state.d.ts +0 -49
  232. package/dist/lib/tui-panel-state.d.ts.map +1 -1
  233. package/dist/lib/tui-panel-state.js +1 -41
  234. package/dist/lib/tui-panel-state.js.map +1 -1
  235. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  236. package/dist/lib/tui-sidebar-format.js +8 -1
  237. package/dist/lib/tui-sidebar-format.js.map +1 -1
  238. package/dist/lib/types.d.ts +26 -7
  239. package/dist/lib/types.d.ts.map +1 -1
  240. package/dist/lib/types.js +1 -2
  241. package/dist/lib/types.js.map +1 -1
  242. package/dist/lib/xai.d.ts +5 -1
  243. package/dist/lib/xai.d.ts.map +1 -1
  244. package/dist/lib/xai.js +13 -4
  245. package/dist/lib/xai.js.map +1 -1
  246. package/dist/lib/zai-auth.d.ts +2 -2
  247. package/dist/lib/zai-auth.d.ts.map +1 -1
  248. package/dist/lib/zai-auth.js +4 -4
  249. package/dist/lib/zai-auth.js.map +1 -1
  250. package/dist/lib/zai.d.ts +1 -0
  251. package/dist/lib/zai.d.ts.map +1 -1
  252. package/dist/lib/zai.js.map +1 -1
  253. package/dist/lib/zhipu-auth.d.ts +2 -2
  254. package/dist/lib/zhipu-auth.d.ts.map +1 -1
  255. package/dist/lib/zhipu-auth.js +4 -4
  256. package/dist/lib/zhipu-auth.js.map +1 -1
  257. package/dist/lib/zhipu.d.ts +1 -0
  258. package/dist/lib/zhipu.d.ts.map +1 -1
  259. package/dist/lib/zhipu.js.map +1 -1
  260. package/dist/plugin.d.ts +6 -9
  261. package/dist/plugin.d.ts.map +1 -1
  262. package/dist/plugin.js +213 -259
  263. package/dist/plugin.js.map +1 -1
  264. package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
  265. package/dist/providers/alibaba-coding-plan.js +1 -1
  266. package/dist/providers/alibaba-coding-plan.js.map +1 -1
  267. package/dist/providers/alibaba-token-plan.d.ts.map +1 -1
  268. package/dist/providers/alibaba-token-plan.js +1 -0
  269. package/dist/providers/alibaba-token-plan.js.map +1 -1
  270. package/dist/providers/anthropic.d.ts.map +1 -1
  271. package/dist/providers/anthropic.js +83 -1
  272. package/dist/providers/anthropic.js.map +1 -1
  273. package/dist/providers/cache-policies.d.ts +2 -2
  274. package/dist/providers/cache-policies.d.ts.map +1 -1
  275. package/dist/providers/cache-policies.js +10 -9
  276. package/dist/providers/cache-policies.js.map +1 -1
  277. package/dist/providers/copilot.d.ts.map +1 -1
  278. package/dist/providers/copilot.js +60 -9
  279. package/dist/providers/copilot.js.map +1 -1
  280. package/dist/providers/deepseek.js +1 -1
  281. package/dist/providers/deepseek.js.map +1 -1
  282. package/dist/providers/glm-coding-plan-provider.d.ts +13 -1
  283. package/dist/providers/glm-coding-plan-provider.d.ts.map +1 -1
  284. package/dist/providers/glm-coding-plan-provider.js +53 -0
  285. package/dist/providers/glm-coding-plan-provider.js.map +1 -1
  286. package/dist/providers/google-agy.d.ts.map +1 -1
  287. package/dist/providers/google-agy.js +56 -7
  288. package/dist/providers/google-agy.js.map +1 -1
  289. package/dist/providers/google-gemini-cli.d.ts.map +1 -1
  290. package/dist/providers/google-gemini-cli.js +72 -7
  291. package/dist/providers/google-gemini-cli.js.map +1 -1
  292. package/dist/providers/kilo.js +1 -1
  293. package/dist/providers/kilo.js.map +1 -1
  294. package/dist/providers/kimi-code.d.ts.map +1 -1
  295. package/dist/providers/kimi-code.js +60 -1
  296. package/dist/providers/kimi-code.js.map +1 -1
  297. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  298. package/dist/providers/minimax-coding-plan.js +52 -1
  299. package/dist/providers/minimax-coding-plan.js.map +1 -1
  300. package/dist/providers/nanogpt.js +1 -1
  301. package/dist/providers/nanogpt.js.map +1 -1
  302. package/dist/providers/ollama-cloud.js +1 -1
  303. package/dist/providers/ollama-cloud.js.map +1 -1
  304. package/dist/providers/openai.d.ts.map +1 -1
  305. package/dist/providers/openai.js +59 -29
  306. package/dist/providers/openai.js.map +1 -1
  307. package/dist/providers/opencode-go.d.ts.map +1 -1
  308. package/dist/providers/opencode-go.js +207 -55
  309. package/dist/providers/opencode-go.js.map +1 -1
  310. package/dist/providers/opencode-zen.d.ts.map +1 -1
  311. package/dist/providers/opencode-zen.js +37 -51
  312. package/dist/providers/opencode-zen.js.map +1 -1
  313. package/dist/providers/openrouter.js +1 -1
  314. package/dist/providers/openrouter.js.map +1 -1
  315. package/dist/providers/quota-providers.js +9 -9
  316. package/dist/providers/quota-providers.js.map +1 -1
  317. package/dist/providers/result-helpers.d.ts +2 -2
  318. package/dist/providers/result-helpers.d.ts.map +1 -1
  319. package/dist/providers/result-helpers.js +2 -2
  320. package/dist/providers/result-helpers.js.map +1 -1
  321. package/dist/providers/xai.d.ts.map +1 -1
  322. package/dist/providers/xai.js +49 -1
  323. package/dist/providers/xai.js.map +1 -1
  324. package/dist/providers/zai.d.ts.map +1 -1
  325. package/dist/providers/zai.js +3 -1
  326. package/dist/providers/zai.js.map +1 -1
  327. package/dist/providers/zhipu.d.ts.map +1 -1
  328. package/dist/providers/zhipu.js +3 -1
  329. package/dist/providers/zhipu.js.map +1 -1
  330. package/dist/rpc.d.ts +72 -0
  331. package/dist/rpc.d.ts.map +1 -0
  332. package/dist/rpc.js +77 -0
  333. package/dist/rpc.js.map +1 -0
  334. package/dist/tui-v2.d.ts +5 -0
  335. package/dist/tui-v2.d.ts.map +1 -0
  336. package/dist/tui-v2.jsx +498 -0
  337. package/dist/tui-v2.jsx.map +1 -0
  338. package/dist/tui.d.ts +4 -6
  339. package/dist/tui.d.ts.map +1 -1
  340. package/dist/tui.js +542 -955
  341. package/package.json +7 -19
  342. package/dist/lib/cache.d.ts +0 -39
  343. package/dist/lib/cache.d.ts.map +0 -1
  344. package/dist/lib/cache.js +0 -124
  345. package/dist/lib/cache.js.map +0 -1
  346. package/dist/lib/command-handled.d.ts +0 -17
  347. package/dist/lib/command-handled.d.ts.map +0 -1
  348. package/dist/lib/command-handled.js +0 -36
  349. package/dist/lib/command-handled.js.map +0 -1
  350. package/dist/lib/command-surfaces.d.ts +0 -10
  351. package/dist/lib/command-surfaces.d.ts.map +0 -1
  352. package/dist/lib/command-surfaces.js +0 -9
  353. package/dist/lib/command-surfaces.js.map +0 -1
  354. package/dist/lib/opencode-zen-config.d.ts +0 -33
  355. package/dist/lib/opencode-zen-config.d.ts.map +0 -1
  356. package/dist/lib/opencode-zen-config.js +0 -158
  357. package/dist/lib/opencode-zen-config.js.map +0 -1
  358. package/dist/lib/path-pick.d.ts +0 -2
  359. package/dist/lib/path-pick.d.ts.map +0 -1
  360. package/dist/lib/path-pick.js +0 -15
  361. package/dist/lib/path-pick.js.map +0 -1
  362. package/dist/lib/quota-toast-runtime.d.ts +0 -60
  363. package/dist/lib/quota-toast-runtime.d.ts.map +0 -1
  364. package/dist/lib/quota-toast-runtime.js +0 -766
  365. package/dist/lib/quota-toast-runtime.js.map +0 -1
  366. package/dist/lib/tui-config-diagnostics.d.ts +0 -17
  367. package/dist/lib/tui-config-diagnostics.d.ts.map +0 -1
  368. package/dist/lib/tui-config-diagnostics.js +0 -72
  369. package/dist/lib/tui-config-diagnostics.js.map +0 -1
  370. package/dist/lib/tui-line-style.d.ts +0 -3
  371. package/dist/lib/tui-line-style.d.ts.map +0 -1
  372. package/dist/lib/tui-line-style.js +0 -7
  373. package/dist/lib/tui-line-style.js.map +0 -1
  374. package/dist/lib/tui-native-provider-quota.d.ts +0 -7
  375. package/dist/lib/tui-native-provider-quota.d.ts.map +0 -1
  376. package/dist/lib/tui-native-provider-quota.js +0 -25
  377. package/dist/lib/tui-native-provider-quota.js.map +0 -1
  378. package/dist/lib/tui-refresh-lifecycle.d.ts +0 -17
  379. package/dist/lib/tui-refresh-lifecycle.d.ts.map +0 -1
  380. package/dist/lib/tui-refresh-lifecycle.js +0 -82
  381. package/dist/lib/tui-refresh-lifecycle.js.map +0 -1
  382. package/dist/lib/tui-runtime.d.ts +0 -84
  383. package/dist/lib/tui-runtime.d.ts.map +0 -1
  384. package/dist/lib/tui-runtime.js +0 -527
  385. package/dist/lib/tui-runtime.js.map +0 -1
  386. package/dist/lib/tui-sidebar-content.d.ts +0 -32
  387. package/dist/lib/tui-sidebar-content.d.ts.map +0 -1
  388. package/dist/lib/tui-sidebar-content.js +0 -52
  389. package/dist/lib/tui-sidebar-content.js.map +0 -1
  390. package/dist/tui.tsx +0 -1030
package/dist/tui.js CHANGED
@@ -1,1035 +1,622 @@
1
+ import { memo as _$memo } from "@opentui/solid";
2
+ import { effect as _$effect } from "@opentui/solid";
3
+ import { use as _$use } from "@opentui/solid";
1
4
  import { createTextNode as _$createTextNode } from "@opentui/solid";
2
5
  import { insertNode as _$insertNode } from "@opentui/solid";
3
- import { effect as _$effect } from "@opentui/solid";
4
- import { memo as _$memo } from "@opentui/solid";
5
- import { insert as _$insert } from "@opentui/solid";
6
6
  import { createComponent as _$createComponent } from "@opentui/solid";
7
+ import { insert as _$insert } from "@opentui/solid";
7
8
  import { setProp as _$setProp } from "@opentui/solid";
8
9
  import { createElement as _$createElement } from "@opentui/solid";
9
10
  /** @jsxImportSource @opentui/solid */
10
11
 
11
- import { createEffect, createSignal, Index, onCleanup, Show } from "solid-js";
12
- import { formatQuotaModeHeading, formatResetCountdown, isResetTimeDecimals, resolveDisplayedPercent } from "./lib/format-utils.js";
13
- import { buildQuotaDialogCommandOutput, QUOTA_DIALOG_COMMANDS } from "./lib/quota-dialog-commands.js";
14
- import { disposeQuotaTelemetryOwner } from "./lib/quota-telemetry.js";
15
- import { getSidebarBodyLineColor } from "./lib/tui-line-style.js";
16
- import { getCompactStatusText, getHomeBottomAnnouncementText, getSidebarPanelLines, getSidebarPanelLinesExpanded, shouldRenderCompactStatus, shouldRenderHomeBottom, shouldRenderSidebarPanel } from "./lib/tui-panel-state.js";
17
- import { formatPromptBarPercentMeta, PROMPT_BAR_WIDTH, resolvePromptBarLabel } from "./lib/tui-prompt-bar-format.js";
18
- import { createTuiRefreshLifecycle } from "./lib/tui-refresh-lifecycle.js";
19
- import { createTuiQuotaClient, getTuiRuntimeRootHints, getTuiSessionModelMeta, loadTuiHomeBottomStatus, loadTuiSessionQuotaSurfaces, normalizeTuiSessionID, resolveTuiSurfaceRegistration, writeTuiQuotaExportIfEnabled } from "./lib/tui-runtime.js";
20
- import { buildSidebarContentRows } from "./lib/tui-sidebar-content.js";
21
- const id = "@slkiser/opencode-quota";
22
- // Place Quota near the top so variable-height built-in sections
23
- // (MCP/LSP/Todo/Files) do not push it below the visible fold.
24
- const SIDEBAR_ORDER = 150;
25
- const COMPACT_ORDER = 90;
12
+ import { Plugin } from "@opencode/plugin/tui";
13
+ import { RGBA, TextAttributes } from "@opentui/core";
14
+ import { useTerminalDimensions } from "@opentui/solid";
15
+ import { createSignal, onCleanup, Show } from "solid-js";
16
+ import { loadConfig } from "./lib/config.js";
17
+ import { resolveOpenCodeLocationRoots } from "./lib/config-file-utils.js";
18
+ import { sanitizeDisplayText } from "./lib/display-sanitize.js";
19
+ import { parseQuotaSlashCommand, QUOTA_DIALOG_COMMANDS } from "./lib/quota-dialog-command-specs.js";
20
+ import { readQuotaReport, readQuotaReportMetadata } from "./lib/quota-report-message.js";
21
+ import { QuotaRpc } from "./rpc.js";
22
+ const terminalForeground = RGBA.defaultForeground();
26
23
  const REFRESH_INTERVAL_MS = 60_000;
27
- const EVENT_REFRESH_DELAYS_MS = [150, 600];
28
- const MOUNT_RECOVERY_DELAYS_MS = [500, 1_500, 4_000];
29
- const FALLBACK_SURFACE_REGISTRATION = {
30
- commandDisplay: "inline",
31
- sidebar: {
32
- enabled: true
33
- },
34
- compact: {
35
- enabled: false,
36
- homeBottom: false,
37
- sessionPrompt: false,
38
- hasNativeProviderQuota: false,
39
- suppressedByNativeProviderQuota: false
40
- },
41
- promptBar: {
42
- enabled: false
43
- },
44
- announcements: {
45
- homeBottom: false
46
- },
47
- homeBottom: false
48
- };
49
- function createTuiInitialLoadCoordinator(seed) {
50
- let sessionAvailable = true;
51
- let homeAvailable = true;
52
- return {
53
- takeInitialSession() {
54
- if (!sessionAvailable) return undefined;
55
- sessionAvailable = false;
56
- return seed;
57
- },
58
- takeInitialHome() {
59
- if (!homeAvailable) return undefined;
60
- homeAvailable = false;
61
- return seed;
62
- }
63
- };
24
+ const RPC_TIMEOUT_MS = 60_000;
25
+ function getSessionID(event) {
26
+ const sessionID = event.data?.sessionID;
27
+ return typeof sessionID === "string" && sessionID ? sessionID : undefined;
64
28
  }
65
- function createTuiRegistrationGate() {
66
- const [current, setCurrent] = createSignal({
67
- status: "pending"
68
- });
29
+ function quotaClient(context) {
69
30
  return {
70
- current,
71
- activate(registration, initialLoads) {
72
- if (current().status !== "pending") return;
73
- setCurrent({
74
- status: "active",
75
- registration,
76
- initialLoads
77
- });
78
- },
79
- dispose() {
80
- if (current().status === "disposed") return;
81
- setCurrent({
82
- status: "disposed"
83
- });
31
+ config: {
32
+ get: async () => ({
33
+ data: {}
34
+ }),
35
+ providers: async () => ({
36
+ data: {
37
+ providers: context.data.location?.provider.list(context.location ?? context.data.location.default()) ?? []
38
+ }
39
+ })
84
40
  }
85
41
  };
86
42
  }
87
- const sessionResources = new WeakMap();
88
- const homeResources = new WeakMap();
89
- function getSessionResourceMap(api) {
90
- const existing = sessionResources.get(api);
91
- if (existing) return existing;
92
- const next = new Map();
93
- sessionResources.set(api, next);
94
- return next;
43
+
44
+ /** Same project roots as the server plugin: the location's Git worktree, else the location directory. */
45
+ function quotaRoots(context) {
46
+ return resolveOpenCodeLocationRoots(context.location?.directory ?? process.cwd());
95
47
  }
96
- function createSessionQuotaResource(api, sessionID, initialLoads) {
97
- const [sidebar, setSidebar] = createSignal({
98
- status: "loading",
99
- lines: []
100
- });
101
- const [compact, setCompact] = createSignal({
102
- status: "loading"
103
- });
104
- const [promptBar, setPromptBar] = createSignal({
105
- status: "loading"
106
- });
107
- let loadOrdinal = 0;
108
- const lifecycle = createTuiRefreshLifecycle({
109
- load: () => {
110
- const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialSession() : undefined;
111
- loadOrdinal += 1;
112
- return loadTuiSessionQuotaSurfaces({
113
- api,
114
- sessionID,
115
- ...(initialRuntimeSeed ? {
116
- initialRuntimeSeed
117
- } : {})
118
- });
119
- },
120
- apply: next => {
121
- setSidebar(next.sidebar);
122
- setCompact(next.compact);
123
- setPromptBar(next.promptBar ?? {
124
- status: "loading"
125
- });
126
- },
127
- intervalMs: REFRESH_INTERVAL_MS,
128
- eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS,
129
- // TUI/session state can hydrate asynchronously after mount or session switch,
130
- // so retry a few times to recover from empty first-load reads.
131
- recoveryDelaysMs: MOUNT_RECOVERY_DELAYS_MS,
132
- subscribe: scheduleRefresh => [api.event.on("session.updated", event => {
133
- if (event.properties?.info?.id === sessionID) {
134
- scheduleRefresh();
135
- }
136
- }), api.event.on("message.updated", event => {
137
- if (event.properties?.info?.sessionID === sessionID) {
138
- scheduleRefresh();
139
- }
140
- }), api.event.on("message.removed", event => {
141
- if (event.properties?.sessionID === sessionID) {
142
- scheduleRefresh();
143
- }
144
- }), api.event.on("tui.session.select", event => {
145
- if (event.properties?.sessionID === sessionID) {
146
- scheduleRefresh();
147
- }
148
- })],
149
- onDispose: () => {
150
- getSessionResourceMap(api).delete(sessionID);
151
- }
152
- });
153
- const resource = {
154
- sessionID,
155
- sidebar,
156
- compact,
157
- promptBar,
158
- retain: () => {
159
- lifecycle.retain();
160
- return resource;
161
- },
162
- release: lifecycle.release
48
+
49
+ /**
50
+ * The server plugin computes every quota surface; the TUI asks it over the quota RPC. The
51
+ * RPC client is made per call, never during setup. Each call names the TUI's location, so
52
+ * the server uses that folder's settings, and gives up after a minute.
53
+ */
54
+ function quotaRpc(context) {
55
+ return context.client.rpc(QuotaRpc);
56
+ }
57
+ function quotaRpcOptions(context) {
58
+ return {
59
+ location: context.location ?? context.data.location.default(),
60
+ signal: AbortSignal.timeout(RPC_TIMEOUT_MS)
163
61
  };
164
- return resource;
165
62
  }
166
- function acquireSessionQuotaResource(api, sessionID, initialLoads) {
167
- const resources = getSessionResourceMap(api);
168
- const existing = resources.get(sessionID);
169
- if (existing) return existing.retain();
170
- const next = createSessionQuotaResource(api, sessionID, initialLoads).retain();
171
- resources.set(sessionID, next);
172
- return next;
63
+ async function getQuotaMessage(context, sessionID, surface) {
64
+ const output = await quotaRpc(context).surface({
65
+ surface,
66
+ sessionID
67
+ }, quotaRpcOptions(context));
68
+ return output.quota ?? undefined;
69
+ }
70
+
71
+ /** RPC calls reject with a plain `{ type, message }` object, not an Error. */
72
+ function rpcErrorMessage(error) {
73
+ if (error instanceof Error) return error.message;
74
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
75
+ return error.message;
76
+ }
77
+ return String(error);
173
78
  }
174
- function createHomeBottomResource(api, compactHomeBottomEnabled, initialLoads) {
175
- const [bottom, setBottom] = createSignal({
176
- status: "loading",
177
- compact: compactHomeBottomEnabled ? {
178
- status: "loading"
179
- } : {
180
- status: "disabled"
79
+
80
+ /**
81
+ * One view's refresh loop, as in v4 (tui-refresh-lifecycle.ts): a refresh that
82
+ * arrives while a load runs is coalesced into one follow-up load, and a result is
83
+ * dropped when the view was disposed or a newer load started.
84
+ */
85
+ function createViewRefresh(load, apply) {
86
+ let disposed = false;
87
+ let loadVersion = 0;
88
+ let inFlight = false;
89
+ let queued = false;
90
+ const refresh = () => {
91
+ if (disposed) return;
92
+ if (inFlight) {
93
+ queued = true;
94
+ return;
181
95
  }
182
- });
183
- let loadOrdinal = 0;
184
- const lifecycle = createTuiRefreshLifecycle({
185
- load: () => {
186
- const initialRuntimeSeed = loadOrdinal === 0 ? initialLoads?.takeInitialHome() : undefined;
187
- loadOrdinal += 1;
188
- return loadTuiHomeBottomStatus({
189
- api,
190
- ...(initialRuntimeSeed ? {
191
- initialRuntimeSeed
192
- } : {})
193
- });
194
- },
195
- apply: setBottom,
196
- afterApply: () => {
197
- // Fire-and-forget: write export file if enabled. A failed write must
198
- // never affect TUI rendering, so log a warning and continue.
199
- void writeTuiQuotaExportIfEnabled({
200
- api
201
- }).catch(err => {
202
- console.warn(`[opencode-quota] quota export write failed: ${String(err)}`);
203
- });
204
- },
205
- intervalMs: REFRESH_INTERVAL_MS,
206
- eventRefreshDelaysMs: EVENT_REFRESH_DELAYS_MS,
207
- subscribe: scheduleRefresh => [api.event.on("session.updated", scheduleRefresh), api.event.on("message.updated", scheduleRefresh), api.event.on("message.removed", scheduleRefresh), api.event.on("tui.session.select", scheduleRefresh)],
208
- onDispose: () => {
209
- homeResources.delete(api);
96
+ inFlight = true;
97
+ const currentVersion = ++loadVersion;
98
+ void load().then(value => {
99
+ if (disposed || currentVersion !== loadVersion) return;
100
+ apply(value);
101
+ }).catch(reportFailure).finally(() => {
102
+ if (disposed) return;
103
+ inFlight = false;
104
+ if (queued) {
105
+ queued = false;
106
+ refresh();
107
+ }
108
+ });
109
+ };
110
+ return {
111
+ refresh,
112
+ dispose: () => {
113
+ disposed = true;
210
114
  }
211
- });
212
- const resource = {
213
- bottom,
214
- retain: () => {
215
- lifecycle.retain();
216
- return resource;
217
- },
218
- release: lifecycle.release
219
115
  };
220
- return resource;
221
116
  }
222
- function acquireHomeBottomResource(api, compactHomeBottomEnabled, initialLoads) {
223
- const existing = homeResources.get(api);
224
- if (existing) return existing.retain();
225
- const next = createHomeBottomResource(api, compactHomeBottomEnabled, initialLoads).retain();
226
- homeResources.set(api, next);
227
- return next;
228
- }
229
- function useSessionQuotaResource(api, sessionID, initialLoads) {
230
- let current = acquireSessionQuotaResource(api, sessionID(), initialLoads);
231
- const [resource, setResource] = createSignal(current);
232
- createEffect(() => {
233
- const nextSessionID = sessionID();
234
- if (current.sessionID === nextSessionID) return;
235
- const previous = current;
236
- current = acquireSessionQuotaResource(api, nextSessionID, initialLoads);
237
- setResource(current);
238
- previous.release();
117
+ function QuotaFooter(props) {
118
+ const [lines, setLines] = createSignal([]);
119
+ const view = createViewRefresh(async () => {
120
+ const output = await quotaRpc(props.context).footer({
121
+ surface: props.surface,
122
+ sessionID: props.sessionID
123
+ }, quotaRpcOptions(props.context));
124
+ return output.lines;
125
+ }, next => {
126
+ setLines(next);
127
+ if (props.surface !== "home") return;
128
+ // Fire-and-forget: the server writes the export file if enabled. A failed write
129
+ // must never affect rendering, so log a warning and continue.
130
+ void quotaRpc(props.context).writeExport({}, quotaRpcOptions(props.context)).catch(error => {
131
+ console.warn(`[opencode-quota] quota export write failed: ${rpcErrorMessage(error)}`);
132
+ });
133
+ });
134
+ view.refresh();
135
+ const interval = setInterval(view.refresh, REFRESH_INTERVAL_MS);
136
+ const stop = props.context.data.on("session.step.ended", event => {
137
+ if (props.surface === "home" || getSessionID(event) === props.sessionID) view.refresh();
239
138
  });
240
139
  onCleanup(() => {
241
- current.release();
140
+ view.dispose();
141
+ clearInterval(interval);
142
+ stop();
242
143
  });
243
- return resource;
244
- }
245
- function SidebarContentView(props) {
246
- const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
247
- const panel = () => resource().sidebar();
248
- const lines = () => getSidebarPanelLines(panel());
249
- const hasDetailLines = () => Boolean(panel().linesExpanded?.length);
250
- const [collapsed, setCollapsed] = createSignal(props.api.kv?.get("quota-sidebar-collapsed", true) ?? true);
251
- const toggleCollapsed = () => {
252
- if (!hasDetailLines()) return;
253
- const next = !collapsed();
254
- setCollapsed(next);
255
- props.api.kv?.set("quota-sidebar-collapsed", next);
256
- };
257
- const displayLines = () => {
258
- if (!hasDetailLines()) return lines();
259
- return collapsed() ? lines() : getSidebarPanelLinesExpanded(panel());
260
- };
261
- const contentRows = () => {
262
- const heading = panel().headerPercentMode ? formatQuotaModeHeading(panel().headerPercentMode) : "Quota";
263
- return buildSidebarContentRows({
264
- collapsed: collapsed(),
265
- heading,
266
- hasDetailLines: hasDetailLines(),
267
- providerCount: panel().providerCount ?? 0,
268
- lines: displayLines()
269
- });
270
- };
271
144
  return _$createComponent(Show, {
272
145
  get when() {
273
- return shouldRenderSidebarPanel(panel());
146
+ return lines().length;
274
147
  },
275
148
  get children() {
276
149
  var _el$ = _$createElement("box");
277
- _$setProp(_el$, "gap", 0);
278
- _$setProp(_el$, "width", "100%");
279
- _$insert(_el$, _$createComponent(Index, {
280
- get each() {
281
- return contentRows();
282
- },
283
- children: row => (() => {
284
- var _el$2 = _$createElement("text");
285
- _$setProp(_el$2, "wrapMode", "none");
286
- _$setProp(_el$2, "width", "100%");
287
- _$insert(_el$2, (() => {
288
- var _c$ = _$memo(() => row().kind === "header");
289
- return () => _c$() ? row().segments.map(segment => segment.style === "muted" ? (() => {
290
- var _el$3 = _$createElement("span");
291
- _$insert(_el$3, () => segment.text);
292
- _$effect(_$p => _$setProp(_el$3, "fg", props.api.theme.current.textMuted, _$p));
293
- return _el$3;
294
- })() : (() => {
295
- var _el$4 = _$createElement("b");
296
- _$insert(_el$4, () => segment.text);
297
- return _el$4;
298
- })()) : row().text || " ";
299
- })());
300
- _$effect(_p$ => {
301
- var _v$ = row().kind === "header" ? props.api.theme.current.text : getSidebarBodyLineColor(row().text, props.api.theme.current),
302
- _v$2 = row().kind === "header" ? toggleCollapsed : undefined;
303
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
304
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "onMouseDown", _v$2, _p$.t));
305
- return _p$;
306
- }, {
307
- e: undefined,
308
- t: undefined
309
- });
310
- return _el$2;
311
- })()
312
- }));
150
+ _$setProp(_el$, "flexDirection", "column");
151
+ _$insert(_el$, () => lines().map(line => (() => {
152
+ var _el$2 = _$createElement("text");
153
+ _$setProp(_el$2, "fg", terminalForeground);
154
+ _$insert(_el$2, line);
155
+ return _el$2;
156
+ })()));
313
157
  return _el$;
314
158
  }
315
159
  });
316
160
  }
317
- function CompactStatusLine(props) {
318
- const text = () => {
319
- const panel = props.panel();
320
- if (!shouldRenderCompactStatus(panel)) return "";
321
- return getCompactStatusText(panel);
322
- };
323
- const line = () => (() => {
324
- var _el$5 = _$createElement("box"),
325
- _el$6 = _$createElement("text");
326
- _$insertNode(_el$5, _el$6);
327
- _$setProp(_el$5, "flexDirection", "row");
328
- _$setProp(_el$6, "wrapMode", "none");
329
- _$insert(_el$6, text);
161
+ function reportFailure(error) {
162
+ // OpenCode answers rpc.unavailable when no server plugin registered the quota RPC here.
163
+ const hint = typeof error === "object" && error !== null && "type" in error && error.type === "rpc.unavailable" ? " (OpenCode Quota's server plugin is not loaded for this folder)" : "";
164
+ console.warn(`[opencode-quota] failed to load quota: ${rpcErrorMessage(error)}${hint}`);
165
+ }
166
+
167
+ /**
168
+ * Shows command output like OpenCode's alert dialog, but inside a scrollbox so long
169
+ * reports (/quota_status, /tokens_*) stay reachable. The mouse wheel scrolls the box;
170
+ * arrows, PageUp/PageDown, and Home/End scroll it from the keyboard. Esc is handled by
171
+ * the host dialog; Enter and the ok/esc labels close it.
172
+ */
173
+ function QuotaOutputDialog(props) {
174
+ const theme = () => props.context.theme.surface("dialog");
175
+ const dimensions = useTerminalDimensions();
176
+ // The host dialog starts a quarter of the way down the terminal. The remaining
177
+ // 8 rows cover the title, ok button, paddings, gaps, and one spare row.
178
+ const maxHeight = () => Math.max(1, Math.floor(dimensions().height * 0.75) - 8);
179
+ let scroll;
180
+ const close = () => props.context.ui.dialog.clear();
181
+ props.context.keymap.layer(() => ({
182
+ mode: "modal",
183
+ commands: [{
184
+ bind: "return",
185
+ title: "Close",
186
+ group: "Dialog",
187
+ run: close
188
+ }, {
189
+ bind: "up",
190
+ title: "Scroll up",
191
+ group: "Dialog",
192
+ run: () => scroll?.scrollBy(-1)
193
+ }, {
194
+ bind: "down",
195
+ title: "Scroll down",
196
+ group: "Dialog",
197
+ run: () => scroll?.scrollBy(1)
198
+ }, {
199
+ bind: "pageup",
200
+ title: "Scroll up one page",
201
+ group: "Dialog",
202
+ run: () => scroll?.scrollBy(-maxHeight())
203
+ }, {
204
+ bind: "pagedown",
205
+ title: "Scroll down one page",
206
+ group: "Dialog",
207
+ run: () => scroll?.scrollBy(maxHeight())
208
+ }, {
209
+ bind: "home",
210
+ title: "Scroll to top",
211
+ group: "Dialog",
212
+ run: () => scroll?.scrollTo(0)
213
+ }, {
214
+ bind: "end",
215
+ title: "Scroll to bottom",
216
+ group: "Dialog",
217
+ run: () => scroll?.scrollTo(scroll.scrollHeight)
218
+ }]
219
+ }));
220
+ return (() => {
221
+ var _el$3 = _$createElement("box"),
222
+ _el$4 = _$createElement("box"),
223
+ _el$5 = _$createElement("text"),
224
+ _el$6 = _$createElement("text"),
225
+ _el$8 = _$createElement("box"),
226
+ _el$9 = _$createElement("scrollbox"),
227
+ _el$0 = _$createElement("text"),
228
+ _el$1 = _$createElement("box"),
229
+ _el$10 = _$createElement("box"),
230
+ _el$11 = _$createElement("text");
231
+ _$insertNode(_el$3, _el$4);
232
+ _$insertNode(_el$3, _el$8);
233
+ _$insertNode(_el$3, _el$1);
234
+ _$setProp(_el$3, "paddingLeft", 2);
235
+ _$setProp(_el$3, "paddingRight", 2);
236
+ _$setProp(_el$3, "gap", 1);
237
+ _$insertNode(_el$4, _el$5);
238
+ _$insertNode(_el$4, _el$6);
239
+ _$setProp(_el$4, "flexDirection", "row");
240
+ _$setProp(_el$4, "justifyContent", "space-between");
241
+ _$insert(_el$5, () => props.title);
242
+ _$insertNode(_el$6, _$createTextNode(`esc`));
243
+ _$setProp(_el$6, "onMouseUp", close);
244
+ _$insertNode(_el$8, _el$9);
245
+ _$setProp(_el$8, "paddingBottom", 1);
246
+ _$insertNode(_el$9, _el$0);
247
+ _$use(element => {
248
+ scroll = element;
249
+ }, _el$9);
250
+ _$insert(_el$0, () => props.message);
251
+ _$insertNode(_el$1, _el$10);
252
+ _$setProp(_el$1, "flexDirection", "row");
253
+ _$setProp(_el$1, "justifyContent", "flex-end");
254
+ _$setProp(_el$1, "paddingBottom", 1);
255
+ _$insertNode(_el$10, _el$11);
256
+ _$setProp(_el$10, "paddingLeft", 3);
257
+ _$setProp(_el$10, "paddingRight", 3);
258
+ _$setProp(_el$10, "onMouseUp", close);
259
+ _$insertNode(_el$11, _$createTextNode(`ok`));
330
260
  _$effect(_p$ => {
331
- var _v$3 = props.justifyContent,
332
- _v$4 = props.api.theme.current.textMuted;
333
- _v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "justifyContent", _v$3, _p$.e));
334
- _v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
261
+ var _v$ = TextAttributes.BOLD,
262
+ _v$2 = theme().text.base,
263
+ _v$3 = theme().text.muted,
264
+ _v$4 = maxHeight(),
265
+ _v$5 = theme().text.muted,
266
+ _v$6 = theme().background.action.primary.focused,
267
+ _v$7 = theme().text.action.primary.focused;
268
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$5, "attributes", _v$, _p$.e));
269
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "fg", _v$2, _p$.t));
270
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$6, "fg", _v$3, _p$.a));
271
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$9, "maxHeight", _v$4, _p$.o));
272
+ _v$5 !== _p$.i && (_p$.i = _$setProp(_el$0, "fg", _v$5, _p$.i));
273
+ _v$6 !== _p$.n && (_p$.n = _$setProp(_el$10, "backgroundColor", _v$6, _p$.n));
274
+ _v$7 !== _p$.s && (_p$.s = _$setProp(_el$11, "fg", _v$7, _p$.s));
335
275
  return _p$;
336
276
  }, {
337
277
  e: undefined,
338
- t: undefined
278
+ t: undefined,
279
+ a: undefined,
280
+ o: undefined,
281
+ i: undefined,
282
+ n: undefined,
283
+ s: undefined
339
284
  });
340
- return _el$5;
285
+ return _el$3;
341
286
  })();
342
- return _$createComponent(Show, {
343
- get when() {
344
- return text();
345
- },
346
- get children() {
347
- return _$createComponent(Show, {
348
- get when() {
349
- return props.blankLineBefore;
350
- },
351
- get fallback() {
352
- return line();
353
- },
354
- get children() {
355
- var _el$7 = _$createElement("box"),
356
- _el$8 = _$createElement("text");
357
- _$insertNode(_el$7, _el$8);
358
- _$setProp(_el$7, "gap", 0);
359
- _$insertNode(_el$8, _$createTextNode(` `));
360
- _$insert(_el$7, line, null);
361
- return _el$7;
362
- }
363
- });
364
- }
365
- });
366
287
  }
367
- function SessionPromptWithCompactStatus(props) {
368
- const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
369
- const panel = () => resource().compact();
370
- return (() => {
371
- var _el$0 = _$createElement("box");
372
- _$setProp(_el$0, "gap", 0);
373
- _$insert(_el$0, _$createComponent(props.api.ui.Prompt, {
374
- get sessionID() {
375
- return props.sessionID;
376
- },
377
- get visible() {
378
- return props.visible;
379
- },
380
- get disabled() {
381
- return props.disabled;
382
- },
383
- get onSubmit() {
384
- return props.onSubmit;
288
+ function showQuotaOutputDialog(context, output) {
289
+ return new Promise(resolve => {
290
+ context.ui.dialog.show(() => _$createComponent(QuotaOutputDialog, {
291
+ context: context,
292
+ get title() {
293
+ return output.title;
385
294
  },
386
- ref(r$) {
387
- var _ref$ = props.promptRef;
388
- typeof _ref$ === "function" ? _ref$(r$) : props.promptRef = r$;
389
- }
390
- }), null);
391
- _$insert(_el$0, _$createComponent(CompactStatusLine, {
392
- get api() {
393
- return props.api;
394
- },
395
- panel: panel,
396
- justifyContent: "flex-end"
397
- }), null);
398
- return _el$0;
399
- })();
400
- }
401
- function shouldRenderPromptBar(bar) {
402
- return bar.status === "ready" && Boolean(bar.entry);
403
- }
404
- function useSessionRunning(api, sessionID) {
405
- const [running, setRunning] = createSignal(false);
406
- createEffect(() => {
407
- const id = sessionID();
408
- if (!id) {
409
- setRunning(false);
410
- return;
411
- }
412
- const update = () => {
413
- try {
414
- const sessionState = api.state.session;
415
- const status = sessionState.status?.(id);
416
- setRunning(status?.type === "busy" || status?.type === "retry");
417
- } catch {
418
- setRunning(false);
419
- }
420
- };
421
- update();
422
- const disposers = [api.event.on("session.status", event => {
423
- if (event.properties?.sessionID === id) {
424
- update();
425
- }
426
- }), api.event.on("session.updated", event => {
427
- if (event.properties?.info?.id === id) {
428
- update();
429
- }
430
- })];
431
- onCleanup(() => {
432
- for (const dispose of disposers) {
433
- if (typeof dispose === "function") {
434
- dispose();
435
- }
295
+ get message() {
296
+ return output.message;
436
297
  }
298
+ }), resolve);
299
+ context.ui.dialog.set({
300
+ size: output.dialogSize
437
301
  });
438
302
  });
439
- return running;
440
303
  }
441
- function buildPromptBarParts(params) {
442
- const bar = params.bar();
443
- if (!shouldRenderPromptBar(bar)) return undefined;
444
- const entry = bar.entry;
445
- if (!entry) return undefined;
446
- const hasPercent = Number.isFinite(entry.percentRemaining);
447
- if (entry.semanticSegment && !hasPercent) {
448
- const reset = entry.resetTimeIso ? formatResetCountdown(entry.resetTimeIso, isResetTimeDecimals(bar.resetTimeDecimals) ? {
449
- compactRounded: true,
450
- decimals: bar.resetTimeDecimals
451
- } : {
452
- spaced: bar.resetTimeSpaced
453
- }) : "";
454
- return {
455
- label: resolvePromptBarLabel(entry),
456
- barText: "",
457
- meta: reset
458
- };
304
+
305
+ /**
306
+ * Runs a quota command from the command palette or the prompt. It opens the dialog and
307
+ * posts no message. `typed` holds the arguments typed after the command name in the prompt.
308
+ */
309
+ async function runQuotaCommand(context, command, sessionID, typed) {
310
+ const spec = QUOTA_DIALOG_COMMANDS.find(item => item.id === command);
311
+ let argumentsText = typed?.argumentsText;
312
+ // Only /tokens_between needs arguments; the palette asks for them.
313
+ if (command === "tokens_between" && !typed) {
314
+ const value = await context.ui.dialog.prompt({
315
+ title: spec.title,
316
+ placeholder: "YYYY-MM-DD YYYY-MM-DD"
317
+ });
318
+ if (value === undefined) return;
319
+ argumentsText = value.trim() || undefined;
459
320
  }
460
- const p = Math.min(100, resolveDisplayedPercent(entry.percentRemaining ?? 0, bar.percentDisplayMode ?? "remaining"));
461
- const filled = Math.round(p / 100 * PROMPT_BAR_WIDTH);
462
- const empty = PROMPT_BAR_WIDTH - filled;
463
- let barText = "█".repeat(filled) + "░".repeat(empty);
464
- if (params.running() && filled > 0) {
465
- const cells = Array(filled).fill("▓");
466
- const center = params.phase() % filled;
467
- const gradient = ["▒", "▓", "█", "▓", "▒"];
468
- for (let offset = -2; offset <= 2; offset++) {
469
- const position = (center + offset + filled) % filled;
470
- cells[position] = gradient[offset + 2];
471
- }
472
- barText = cells.join("") + "░".repeat(empty);
321
+ try {
322
+ const result = await quotaRpc(context).command({
323
+ command,
324
+ arguments: argumentsText,
325
+ sessionID
326
+ }, quotaRpcOptions(context));
327
+ if (result.state === "noop") return;
328
+ await showQuotaOutputDialog(context, {
329
+ title: result.title,
330
+ message: result.output,
331
+ dialogSize: result.dialogSize
332
+ });
333
+ } catch (error) {
334
+ context.ui.toast.show({
335
+ variant: "error",
336
+ title: "OpenCode Quota",
337
+ message: sanitizeDisplayText(rpcErrorMessage(error))
338
+ });
473
339
  }
474
- return {
475
- label: resolvePromptBarLabel(entry),
476
- barText,
477
- meta: formatPromptBarPercentMeta({
478
- percentRemaining: entry.percentRemaining ?? 0,
479
- percentDisplayMode: bar.percentDisplayMode,
480
- resetTimeIso: entry.resetTimeIso,
481
- resetTimeDecimals: bar.resetTimeDecimals,
482
- resetTimeSpaced: bar.resetTimeSpaced,
483
- runway: entry.runway
484
- })
485
- };
486
340
  }
487
- function PromptQuotaHint(props) {
488
- const parts = () => buildPromptBarParts(props);
489
- const barColor = () => props.api.theme.current.textMuted;
490
- const label = () => parts()?.label ?? "";
491
- const bar = () => parts()?.barText ?? "";
492
- const meta = () => parts()?.meta ?? "";
493
- return _$createComponent(Show, {
494
- get when() {
495
- return parts();
496
- },
497
- get children() {
498
- var _el$1 = _$createElement("box"),
499
- _el$10 = _$createElement("text"),
500
- _el$11 = _$createElement("text"),
501
- _el$12 = _$createElement("text");
502
- _$insertNode(_el$1, _el$10);
503
- _$insertNode(_el$1, _el$11);
504
- _$insertNode(_el$1, _el$12);
505
- _$setProp(_el$1, "flexDirection", "row");
506
- _$setProp(_el$1, "justifyContent", "flex-end");
507
- _$setProp(_el$1, "gap", 1);
508
- _$setProp(_el$10, "wrapMode", "none");
509
- _$insert(_el$10, label);
510
- _$setProp(_el$11, "wrapMode", "none");
511
- _$insert(_el$11, bar);
512
- _$setProp(_el$12, "wrapMode", "none");
513
- _$insert(_el$12, meta);
514
- _$effect(_p$ => {
515
- var _v$5 = props.api.theme.current.textMuted,
516
- _v$6 = barColor(),
517
- _v$7 = props.api.theme.current.textMuted;
518
- _v$5 !== _p$.e && (_p$.e = _$setProp(_el$10, "fg", _v$5, _p$.e));
519
- _v$6 !== _p$.t && (_p$.t = _$setProp(_el$11, "fg", _v$6, _p$.t));
520
- _v$7 !== _p$.a && (_p$.a = _$setProp(_el$12, "fg", _v$7, _p$.a));
521
- return _p$;
522
- }, {
523
- e: undefined,
524
- t: undefined,
525
- a: undefined
526
- });
527
- return _el$1;
528
- }
341
+
342
+ /**
343
+ * Quota slash commands that reach the server (from Web or Desktop, queued in the TUI, or
344
+ * typed in "inline" mode) post the report into the chat as a pending inbox item. When a
345
+ * report arrives for the session on screen, tuiCommandDisplay decides what the TUI shows: "dialog" opens the report in the dialog and cancels the pending
346
+ * item, so no chat message remains; "inline" leaves the report in the chat.
347
+ * A command sent from Home creates its session, and OpenCode opens that session before the
348
+ * server runs the command, so its report also arrives for the session on screen.
349
+ * The TUI cannot tell which client typed the command, so in "dialog" mode it also removes a
350
+ * report that Web or Desktop requested for the same session while the TUI shows it.
351
+ */
352
+ async function showPostedQuotaReport(context, event) {
353
+ const sessionID = getSessionID(event);
354
+ if (!sessionID || sessionID !== getRouteSessionID(context)) return;
355
+ const inboxID = event.data?.inboxID;
356
+ const item = event.data?.item;
357
+ if (typeof inboxID !== "string") return;
358
+ if (item?.type !== "user" || typeof item.payload?.text !== "string") return;
359
+ const report = readQuotaReportMetadata(item.payload.metadata);
360
+ if (!report) return;
361
+ const spec = QUOTA_DIALOG_COMMANDS.find(candidate => candidate.id === report.command);
362
+ if (!spec) return;
363
+ const config = await loadConfig(quotaClient(context), undefined, {
364
+ configRootDir: quotaRoots(context).configRoot
529
365
  });
530
- }
531
- function SessionQuotaPromptBar(props) {
532
- const resource = useSessionQuotaResource(props.api, () => props.sessionID, props.initialLoads);
533
- const promptBar = () => resource().promptBar();
534
- const running = useSessionRunning(props.api, () => props.sessionID);
535
- const [phase, setPhase] = createSignal(0);
536
- createEffect(() => {
537
- if (!running() || !shouldRenderPromptBar(promptBar())) {
538
- setPhase(0);
539
- return;
540
- }
541
- const interval = setInterval(() => setPhase(p => p + 1), 160);
542
- onCleanup(() => clearInterval(interval));
366
+ if (config.tuiCommandDisplay !== "dialog") return;
367
+ // Cancelling deletes the pending item before it becomes a stored message, the same
368
+ // request the TUI sends when the user deletes a pending prompt.
369
+ void context.client.session.inbox.cancel({
370
+ sessionID,
371
+ inboxID
372
+ }).catch(error => {
373
+ const message = error instanceof Error ? error.message : String(error);
374
+ console.warn(`[opencode-quota] failed to remove the quota report from the chat: ${message}`);
543
375
  });
544
- return (() => {
545
- var _el$13 = _$createElement("box");
546
- _$setProp(_el$13, "gap", 0);
547
- _$insert(_el$13, _$createComponent(props.api.ui.Prompt, {
548
- get sessionID() {
549
- return props.sessionID;
550
- },
551
- get visible() {
552
- return props.visible;
553
- },
554
- get disabled() {
555
- return props.disabled;
556
- },
557
- get onSubmit() {
558
- return props.onSubmit;
559
- },
560
- ref(r$) {
561
- var _ref$2 = props.promptRef;
562
- typeof _ref$2 === "function" ? _ref$2(r$) : props.promptRef = r$;
376
+ void showQuotaOutputDialog(context, {
377
+ title: report.title,
378
+ message: readQuotaReport(item.payload.text),
379
+ dialogSize: spec.dialogSize
380
+ });
381
+ }
382
+ function getRouteSessionID(context) {
383
+ const route = context.ui.router.current();
384
+ return route.type === "session" ? route.sessionID : undefined;
385
+ }
386
+ function registerQuotaCommands(context) {
387
+ context.keymap.layer(() => ({
388
+ mode: "global",
389
+ commands: QUOTA_DIALOG_COMMANDS.map(spec => ({
390
+ id: `quota.${spec.id}`,
391
+ title: spec.title,
392
+ group: "OpenCode Quota",
393
+ // No slash entry: the server plugin registers the "/" commands, so each is listed once.
394
+ palette: true,
395
+ run: () => runQuotaCommand(context, spec.id, getRouteSessionID(context))
396
+ }))
397
+ }));
398
+ }
399
+
400
+ /**
401
+ * Runs typed quota slash commands in the TUI. On Enter, before OpenCode submits the prompt,
402
+ * a prompt that holds exactly a quota command is cleared and the command opens its dialog.
403
+ * So the report never shows in the chat, and on Home no session is created for it.
404
+ * Everything else, and every command in "inline" mode, goes on to OpenCode's submit, and
405
+ * the server command posts the report as before.
406
+ * The layer has no mode, so it is off while the "/" list or a dialog is open. tuiCommandDisplay
407
+ * must be known when Enter is pressed, so it is read at start and again after each typed
408
+ * quota command. Until the first read finishes, typed commands follow the default, "dialog".
409
+ */
410
+ function registerTypedQuotaCommands(context) {
411
+ let commandDisplay;
412
+ const readCommandDisplay = () => {
413
+ void loadConfig(quotaClient(context), undefined, {
414
+ configRootDir: quotaRoots(context).configRoot
415
+ }).then(config => {
416
+ commandDisplay = config.tuiCommandDisplay;
417
+ }).catch(reportFailure);
418
+ };
419
+ readCommandDisplay();
420
+ context.keymap.layer(() => ({
421
+ // Above OpenCode's textarea layer, which submits the prompt on Enter.
422
+ priority: 1,
423
+ commands: [{
424
+ bind: "return",
425
+ // Synchronous on purpose: a returned promise counts as handled.
426
+ run: () => {
427
+ const editor = context.renderer.currentFocusedEditor;
428
+ const typed = editor ? parseQuotaSlashCommand(editor.plainText) : undefined;
429
+ if (!editor || !typed) return false;
430
+ const display = commandDisplay;
431
+ readCommandDisplay();
432
+ if (display === "inline") return false;
433
+ editor.clear();
434
+ void runQuotaCommand(context, typed.command, getRouteSessionID(context), typed);
563
435
  }
564
- }), null);
565
- _$insert(_el$13, _$createComponent(PromptQuotaHint, {
566
- get api() {
567
- return props.api;
568
- },
569
- bar: promptBar,
570
- running: running,
571
- phase: phase
572
- }), null);
573
- return _el$13;
574
- })();
436
+ }]
437
+ }));
575
438
  }
576
- function HomeBottomView(props) {
577
- const resource = acquireHomeBottomResource(props.api, props.compactHomeBottomEnabled, props.initialLoads);
578
- onCleanup(() => resource.release());
579
- const announcement = () => getHomeBottomAnnouncementText(resource.bottom());
580
- const compact = () => resource.bottom().compact;
581
- const visible = () => shouldRenderHomeBottom(resource.bottom());
439
+ function SidebarQuotaView(props) {
440
+ const [open, setOpen] = createSignal(true);
441
+ const [quota, setQuota] = createSignal(undefined);
442
+ const lines = () => quota()?.message.split("\n") ?? [];
443
+ const expandable = () => lines().length > 2;
444
+ const view = createViewRefresh(() => getQuotaMessage(props.context, props.sessionID, "sidebar"), setQuota);
445
+ view.refresh();
446
+ const interval = setInterval(view.refresh, REFRESH_INTERVAL_MS);
447
+ const unsubscribe = props.context.data.on("session.step.ended", event => {
448
+ if (getSessionID(event) === props.sessionID) view.refresh();
449
+ });
450
+ onCleanup(() => {
451
+ view.dispose();
452
+ clearInterval(interval);
453
+ unsubscribe();
454
+ });
582
455
  return (() => {
583
- var _el$14 = _$createElement("box");
584
- _$setProp(_el$14, "gap", 0);
456
+ var _el$13 = _$createElement("box"),
457
+ _el$14 = _$createElement("box"),
458
+ _el$16 = _$createElement("text"),
459
+ _el$17 = _$createElement("b");
460
+ _$insertNode(_el$13, _el$14);
461
+ _$setProp(_el$13, "flexDirection", "column");
462
+ _$insertNode(_el$14, _el$16);
463
+ _$setProp(_el$14, "flexDirection", "row");
464
+ _$setProp(_el$14, "gap", 1);
465
+ _$setProp(_el$14, "onMouseDown", () => expandable() && setOpen(value => !value));
585
466
  _$insert(_el$14, _$createComponent(Show, {
586
467
  get when() {
587
- return visible();
468
+ return expandable();
588
469
  },
589
470
  get children() {
590
471
  var _el$15 = _$createElement("text");
591
- _$insertNode(_el$15, _$createTextNode(` `));
472
+ _$setProp(_el$15, "fg", terminalForeground);
473
+ _$insert(_el$15, () => open() ? "▼" : "▶");
592
474
  return _el$15;
593
475
  }
594
- }), null);
595
- _$insert(_el$14, _$createComponent(Show, {
476
+ }), _el$16);
477
+ _$insertNode(_el$16, _el$17);
478
+ _$setProp(_el$16, "fg", terminalForeground);
479
+ _$insertNode(_el$17, _$createTextNode(`Quota`));
480
+ _$insert(_el$16, _$createComponent(Show, {
596
481
  get when() {
597
- return _$memo(() => !!visible())() && announcement();
482
+ return _$memo(() => !!(expandable() && !open()))() && quota()?.activeProviderCount;
598
483
  },
599
- get children() {
600
- var _el$17 = _$createElement("box"),
601
- _el$18 = _$createElement("text");
602
- _$insertNode(_el$17, _el$18);
603
- _$setProp(_el$17, "flexDirection", "row");
604
- _$setProp(_el$17, "justifyContent", "center");
605
- _$setProp(_el$18, "wrapMode", "none");
606
- _$insert(_el$18, announcement);
607
- _$effect(_$p => _$setProp(_el$18, "fg", props.api.theme.current.textMuted, _$p));
608
- return _el$17;
609
- }
484
+ children: count => ` (${count()} active)`
610
485
  }), null);
611
- _$insert(_el$14, _$createComponent(Show, {
486
+ _$insert(_el$13, _$createComponent(Show, {
612
487
  get when() {
613
- return visible();
488
+ return quota();
489
+ },
490
+ get fallback() {
491
+ return (() => {
492
+ var _el$19 = _$createElement("text");
493
+ _$insertNode(_el$19, _$createTextNode(`No quota data available`));
494
+ _$setProp(_el$19, "fg", terminalForeground);
495
+ return _el$19;
496
+ })();
614
497
  },
615
498
  get children() {
616
- return _$createComponent(CompactStatusLine, {
617
- get api() {
618
- return props.api;
499
+ return _$createComponent(Show, {
500
+ get when() {
501
+ return !expandable() || open();
619
502
  },
620
- panel: compact,
621
- justifyContent: "center"
503
+ get children() {
504
+ return lines().map(line => (() => {
505
+ var _el$21 = _$createElement("text");
506
+ _$setProp(_el$21, "fg", terminalForeground);
507
+ _$setProp(_el$21, "wrapMode", "none");
508
+ _$insert(_el$21, line || " ");
509
+ return _el$21;
510
+ })());
511
+ }
622
512
  });
623
513
  }
624
514
  }), null);
625
- return _el$14;
626
- })();
627
- }
628
- function getActiveTuiSessionID(api) {
629
- if (api.route.current.name !== "session") return undefined;
630
- return normalizeTuiSessionID(api.route.current.params?.sessionID);
631
- }
632
- function getTuiCommandArguments(input) {
633
- if (!input || typeof input !== "object") return undefined;
634
- const record = input;
635
- for (const key of ["arguments", "args", "query"]) {
636
- const value = record[key];
637
- if (typeof value === "string" && value.trim()) return value.trim();
638
- }
639
- return undefined;
640
- }
641
- function CommandLoadingDialog(props) {
642
- return (() => {
643
- var _el$19 = _$createElement("box"),
644
- _el$20 = _$createElement("text"),
645
- _el$21 = _$createElement("b"),
646
- _el$22 = _$createElement("text");
647
- _$insertNode(_el$19, _el$20);
648
- _$insertNode(_el$19, _el$22);
649
- _$setProp(_el$19, "gap", 1);
650
- _$setProp(_el$19, "paddingLeft", 2);
651
- _$setProp(_el$19, "paddingRight", 2);
652
- _$setProp(_el$19, "paddingBottom", 1);
653
- _$insertNode(_el$20, _el$21);
654
- _$insert(_el$21, () => props.title);
655
- _$insertNode(_el$22, _$createTextNode(`Loading deterministic local output…`));
656
- _$effect(_p$ => {
657
- var _v$8 = props.api.theme.current.text,
658
- _v$9 = props.api.theme.current.textMuted;
659
- _v$8 !== _p$.e && (_p$.e = _$setProp(_el$20, "fg", _v$8, _p$.e));
660
- _v$9 !== _p$.t && (_p$.t = _$setProp(_el$22, "fg", _v$9, _p$.t));
661
- return _p$;
662
- }, {
663
- e: undefined,
664
- t: undefined
665
- });
666
- return _el$19;
667
- })();
668
- }
669
- function CommandOutputDialog(props) {
670
- const lines = () => props.output.split("\n");
671
- const bodyHeight = () => Math.min(28, Math.max(6, lines().length));
672
- return (() => {
673
- var _el$24 = _$createElement("box"),
674
- _el$25 = _$createElement("text"),
675
- _el$26 = _$createElement("b"),
676
- _el$27 = _$createElement("scrollbox"),
677
- _el$28 = _$createElement("box"),
678
- _el$29 = _$createElement("text");
679
- _$insertNode(_el$24, _el$25);
680
- _$insertNode(_el$24, _el$27);
681
- _$insertNode(_el$24, _el$29);
682
- _$setProp(_el$24, "gap", 1);
683
- _$setProp(_el$24, "width", "100%");
684
- _$setProp(_el$24, "flexGrow", 1);
685
- _$setProp(_el$24, "paddingLeft", 2);
686
- _$setProp(_el$24, "paddingRight", 2);
687
- _$setProp(_el$24, "paddingBottom", 1);
688
- _$insertNode(_el$25, _el$26);
689
- _$insert(_el$26, () => props.title);
690
- _$insertNode(_el$27, _el$28);
691
- _$setProp(_el$27, "width", "100%");
692
- _$setProp(_el$27, "flexGrow", 1);
693
- _$setProp(_el$27, "maxHeight", 28);
694
- _$setProp(_el$28, "gap", 0);
695
- _$setProp(_el$28, "width", "100%");
696
- _$setProp(_el$28, "minWidth", 0);
697
- _$insert(_el$28, () => lines().map(line => (() => {
698
- var _el$31 = _$createElement("text");
699
- _$setProp(_el$31, "wrapMode", "word");
700
- _$setProp(_el$31, "width", "100%");
701
- _$insert(_el$31, line || " ");
702
- _$effect(_$p => _$setProp(_el$31, "fg", props.api.theme.current.text, _$p));
703
- return _el$31;
704
- })()));
705
- _$insertNode(_el$29, _$createTextNode(`esc closes`));
706
- _$effect(_p$ => {
707
- var _v$0 = props.api.theme.current.text,
708
- _v$1 = bodyHeight(),
709
- _v$10 = props.api.theme.current.textMuted;
710
- _v$0 !== _p$.e && (_p$.e = _$setProp(_el$25, "fg", _v$0, _p$.e));
711
- _v$1 !== _p$.t && (_p$.t = _$setProp(_el$27, "minHeight", _v$1, _p$.t));
712
- _v$10 !== _p$.a && (_p$.a = _$setProp(_el$29, "fg", _v$10, _p$.a));
713
- return _p$;
714
- }, {
715
- e: undefined,
716
- t: undefined,
717
- a: undefined
718
- });
719
- return _el$24;
720
- })();
721
- }
722
- function CommandErrorDialog(props) {
723
- const message = props.error instanceof Error ? props.error.message : String(props.error);
724
- return (() => {
725
- var _el$32 = _$createElement("box"),
726
- _el$33 = _$createElement("text"),
727
- _el$34 = _$createElement("b"),
728
- _el$35 = _$createElement("text"),
729
- _el$37 = _$createElement("text"),
730
- _el$38 = _$createElement("text");
731
- _$insertNode(_el$32, _el$33);
732
- _$insertNode(_el$32, _el$35);
733
- _$insertNode(_el$32, _el$37);
734
- _$insertNode(_el$32, _el$38);
735
- _$setProp(_el$32, "gap", 1);
736
- _$setProp(_el$32, "paddingLeft", 2);
737
- _$setProp(_el$32, "paddingRight", 2);
738
- _$setProp(_el$32, "paddingBottom", 1);
739
- _$insertNode(_el$33, _el$34);
740
- _$insert(_el$34, () => props.title);
741
- _$insertNode(_el$35, _$createTextNode(`OpenCode Quota command failed.`));
742
- _$setProp(_el$37, "wrapMode", "none");
743
- _$insert(_el$37, message || "Unknown error");
744
- _$insertNode(_el$38, _$createTextNode(`esc closes`));
745
- _$effect(_p$ => {
746
- var _v$11 = props.api.theme.current.text,
747
- _v$12 = props.api.theme.current.text,
748
- _v$13 = props.api.theme.current.textMuted,
749
- _v$14 = props.api.theme.current.textMuted;
750
- _v$11 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$11, _p$.e));
751
- _v$12 !== _p$.t && (_p$.t = _$setProp(_el$35, "fg", _v$12, _p$.t));
752
- _v$13 !== _p$.a && (_p$.a = _$setProp(_el$37, "fg", _v$13, _p$.a));
753
- _v$14 !== _p$.o && (_p$.o = _$setProp(_el$38, "fg", _v$14, _p$.o));
754
- return _p$;
755
- }, {
756
- e: undefined,
757
- t: undefined,
758
- a: undefined,
759
- o: undefined
760
- });
761
- return _el$32;
515
+ return _el$13;
762
516
  })();
763
517
  }
764
- function getCommandPromptCopy(spec) {
765
- switch (spec.id) {
766
- case "tokens_between":
767
- return {
768
- title: "OpenCode Quota Token Range",
769
- placeholder: "YYYY-MM-DD YYYY-MM-DD",
770
- description: "Enter start and end dates, for example: 2026-01-01 2026-01-15"
771
- };
772
- default:
773
- return {
774
- title: spec.title,
775
- placeholder: "Optional arguments",
776
- description: "Leave blank to run with no arguments."
777
- };
778
- }
779
- }
780
- function replaceDialog(api, size, render) {
781
- api.ui.dialog.replace(render);
782
- // OpenCode dialog.replace() resets size to medium.
783
- api.ui.dialog.setSize(size);
784
- }
785
- async function runQuotaDialogCommandAsync(api, command, commandDisplay, rawInput, state) {
786
- const spec = QUOTA_DIALOG_COMMANDS.find(item => item.id === command);
787
- const argumentsText = getTuiCommandArguments(rawInput);
788
- const sessionID = getActiveTuiSessionID(api);
789
- if (spec.acceptsArguments && rawInput === undefined) {
790
- const prompt = getCommandPromptCopy(spec);
791
- replaceDialog(api, "medium", () => _$createComponent(api.ui.DialogPrompt, {
792
- get title() {
793
- return prompt.title;
794
- },
795
- get placeholder() {
796
- return prompt.placeholder;
797
- },
798
- description: () => (() => {
799
- var _el$40 = _$createElement("text");
800
- _$setProp(_el$40, "wrapMode", "word");
801
- _$insert(_el$40, () => prompt.description);
802
- _$effect(_$p => _$setProp(_el$40, "fg", api.theme.current.textMuted, _$p));
803
- return _el$40;
804
- })(),
805
- onCancel: () => api.ui.dialog.clear(),
806
- onConfirm: value => {
807
- void runQuotaDialogCommandAsync(api, command, commandDisplay, {
808
- arguments: value.trim()
809
- }, state);
810
- }
811
- }));
812
- return;
813
- }
814
- const destination = commandDisplay === "inline" && sessionID ? {
815
- type: "inline",
816
- sessionID
817
- } : {
818
- type: "dialog"
819
- };
820
- if (destination.type === "dialog") {
821
- replaceDialog(api, spec.dialogSize, () => _$createComponent(CommandLoadingDialog, {
822
- api: api,
823
- get title() {
824
- return spec.title;
825
- }
826
- }));
827
- }
828
- try {
829
- const result = await buildQuotaDialogCommandOutput({
830
- command,
831
- arguments: argumentsText,
832
- client: createTuiQuotaClient(api),
833
- roots: getTuiRuntimeRootHints(api),
834
- sessionID,
835
- resolveSessionMeta: id => getTuiSessionModelMeta(api, id),
836
- lastSessionTokenError: state?.lastSessionTokenError,
837
- setLastSessionTokenError: state ? error => {
838
- state.lastSessionTokenError = error;
839
- } : undefined,
840
- log: async (message, extra) => {
841
- await api.client.app.log({
842
- body: {
843
- service: "quota-toast",
844
- level: "debug",
845
- message,
846
- extra
518
+ const plugin = Plugin.define({
519
+ id: "@slkiser/opencode-quota",
520
+ setup(context) {
521
+ const api = context;
522
+ let disposeEvents;
523
+ const questionToolCalls = new Set();
524
+ const disposeApp = api.ui.slot({
525
+ append: "app",
526
+ render: () => {
527
+ // OpenCode mounts this slot again when it reconnects to its server, which can also
528
+ // happen right after it starts. Leaving a mount drops its keymap layers and event
529
+ // listeners, so each mount replaces the listeners and registers everything again.
530
+ disposeEvents?.();
531
+ registerQuotaCommands(api);
532
+ registerTypedQuotaCommands(api);
533
+ const trigger = (event, reason) => {
534
+ const sessionID = getSessionID(event);
535
+ if (!sessionID) return;
536
+ // As in v4, subagent (child) sessions never show quota toasts.
537
+ if (api.data.session.get(sessionID)?.parentID) return;
538
+ void getQuotaMessage(api, sessionID, reason).then(quota => {
539
+ if (!quota) return;
540
+ api.ui.toast.show({
541
+ variant: "info",
542
+ title: "OpenCode Quota",
543
+ message: quota.message,
544
+ duration: quota.duration
545
+ });
546
+ if (quota.resetNotification) {
547
+ api.ui.toast.show({
548
+ variant: "success",
549
+ title: "Quota available",
550
+ message: sanitizeDisplayText(quota.resetNotification),
551
+ duration: quota.duration
552
+ });
553
+ }
554
+ }).catch(reportFailure);
555
+ };
556
+ const onExecutionSucceeded = api.data.on("session.execution.succeeded", event => trigger(event, "idle"));
557
+ const onCompacted = api.data.on("session.compaction.ended", event => trigger(event, "compacted"));
558
+ const onQuestionStarted = api.data.on("session.tool.input.started", event => {
559
+ const id = event.data?.id;
560
+ if (event.data?.name === "question" && typeof id === "string") {
561
+ questionToolCalls.add(id);
847
562
  }
848
563
  });
849
- }
850
- });
851
- if (result.state === "noop") {
852
- if (destination.type === "dialog") api.ui.dialog.clear();
853
- return;
854
- }
855
- if (destination.type === "inline") {
856
- await api.client.session.prompt({
857
- sessionID: destination.sessionID,
858
- noReply: true,
859
- parts: [{
860
- type: "text",
861
- text: result.output,
862
- ignored: true
863
- }]
864
- });
865
- return;
866
- }
867
- replaceDialog(api, result.dialogSize, () => _$createComponent(CommandOutputDialog, {
868
- api: api,
869
- get title() {
870
- return result.title;
871
- },
872
- get output() {
873
- return result.output;
874
- }
875
- }));
876
- } catch (error) {
877
- replaceDialog(api, "large", () => _$createComponent(CommandErrorDialog, {
878
- api: api,
879
- get title() {
880
- return spec.title;
881
- },
882
- error: error
883
- }));
884
- api.ui.toast({
885
- variant: "error",
886
- message: "OpenCode Quota command failed"
887
- });
888
- }
889
- }
890
- function registerQuotaDialogCommands(api, gate) {
891
- const commandState = {};
892
- const dispose = api.keymap.registerLayer({
893
- commands: QUOTA_DIALOG_COMMANDS.map(spec => ({
894
- namespace: "palette",
895
- name: `opencode-quota.${spec.id}`,
896
- title: spec.title,
897
- desc: spec.description,
898
- category: "OpenCode Quota",
899
- slashName: spec.slashName,
900
- run(input) {
901
- const state = gate.current();
902
- if (state.status !== "active") return;
903
- void runQuotaDialogCommandAsync(api, spec.id, state.registration.commandDisplay, input, commandState);
904
- }
905
- })),
906
- bindings: []
907
- });
908
- api.lifecycle.onDispose(dispose);
909
- }
910
- function registerStableTuiSlots(api, current) {
911
- api.slots.register({
912
- order: SIDEBAR_ORDER,
913
- slots: {
914
- sidebar_content(_ctx, props) {
915
- const state = current();
916
- if (state.status !== "active" || !state.registration.sidebar.enabled) return null;
917
- return _$createComponent(SidebarContentView, {
918
- api: api,
919
- get sessionID() {
920
- return props.session_id;
921
- },
922
- get initialLoads() {
923
- return state.initialLoads;
924
- }
564
+ const onQuestionSucceeded = api.data.on("session.tool.success", event => {
565
+ const id = event.data?.id;
566
+ if (typeof id === "string" && questionToolCalls.delete(id)) trigger(event, "question");
925
567
  });
926
- }
927
- }
928
- });
929
- api.slots.register({
930
- order: COMPACT_ORDER,
931
- slots: {
932
- session_prompt(_ctx, props) {
933
- const state = current();
934
- if (state.status !== "active") return null;
935
- if (state.registration.promptBar.enabled) {
936
- return _$createComponent(SessionQuotaPromptBar, {
937
- api: api,
938
- get sessionID() {
939
- return props.session_id;
940
- },
941
- get initialLoads() {
942
- return state.initialLoads;
943
- },
944
- get visible() {
945
- return props.visible;
946
- },
947
- get disabled() {
948
- return props.disabled;
949
- },
950
- get onSubmit() {
951
- return props.on_submit;
952
- },
953
- get promptRef() {
954
- return props.ref;
955
- }
956
- });
957
- }
958
- if (!state.registration.compact.sessionPrompt) return null;
959
- return _$createComponent(SessionPromptWithCompactStatus, {
960
- api: api,
961
- get sessionID() {
962
- return props.session_id;
963
- },
964
- get initialLoads() {
965
- return state.initialLoads;
966
- },
967
- get visible() {
968
- return props.visible;
969
- },
970
- get disabled() {
971
- return props.disabled;
972
- },
973
- get onSubmit() {
974
- return props.on_submit;
975
- },
976
- get promptRef() {
977
- return props.ref;
978
- }
568
+ const onQuestionFailed = api.data.on("session.tool.failed", event => {
569
+ const id = event.data?.id;
570
+ if (typeof id === "string") questionToolCalls.delete(id);
979
571
  });
980
- },
981
- home_bottom() {
982
- const state = current();
983
- if (state.status !== "active" || !state.registration.homeBottom) return null;
984
- return _$createComponent(HomeBottomView, {
985
- api: api,
986
- get compactHomeBottomEnabled() {
987
- return state.registration.compact.homeBottom;
988
- },
989
- get initialLoads() {
990
- return state.initialLoads;
991
- }
572
+ const onInboxEnqueued = api.data.on("session.inbox.enqueued", event => {
573
+ void showPostedQuotaReport(api, event).catch(reportFailure);
992
574
  });
575
+ disposeEvents = () => {
576
+ onExecutionSucceeded();
577
+ onCompacted();
578
+ onQuestionStarted();
579
+ onQuestionSucceeded();
580
+ onQuestionFailed();
581
+ onInboxEnqueued();
582
+ questionToolCalls.clear();
583
+ };
584
+ return null;
993
585
  }
994
- }
995
- });
996
- }
997
- async function initializeTuiRegistration(api, gate) {
998
- let initialRuntimeSeed;
999
- let surfaceRegistration;
1000
- try {
1001
- surfaceRegistration = resolveTuiSurfaceRegistration(api, {
1002
- captureInitialRuntime(seed) {
1003
- initialRuntimeSeed = seed;
1004
- }
1005
- }).then(registration => ({
1006
- registration,
1007
- initialRuntimeSeed
1008
- })).catch(() => ({
1009
- registration: FALLBACK_SURFACE_REGISTRATION
1010
- }));
1011
- } catch {
1012
- surfaceRegistration = Promise.resolve({
1013
- registration: FALLBACK_SURFACE_REGISTRATION
1014
586
  });
587
+ const disposeSidebar = api.ui.slot({
588
+ append: "sidebar.content",
589
+ render: props => _$createComponent(SidebarQuotaView, {
590
+ context: api,
591
+ get sessionID() {
592
+ return props.sessionID;
593
+ }
594
+ })
595
+ });
596
+ const disposePrompt = api.ui.slot({
597
+ append: "prompt.footer",
598
+ render: props => _$createComponent(QuotaFooter, {
599
+ context: api,
600
+ get sessionID() {
601
+ return props.sessionID;
602
+ },
603
+ surface: "prompt"
604
+ })
605
+ });
606
+ const disposeHome = api.ui.slot({
607
+ append: "home.footer.status",
608
+ render: () => _$createComponent(QuotaFooter, {
609
+ context: api,
610
+ surface: "home"
611
+ })
612
+ });
613
+ return () => {
614
+ disposeEvents?.();
615
+ disposeApp();
616
+ disposeSidebar();
617
+ disposePrompt();
618
+ disposeHome();
619
+ };
1015
620
  }
1016
- registerQuotaDialogCommands(api, gate);
1017
- void surfaceRegistration.then(({
1018
- registration,
1019
- initialRuntimeSeed
1020
- }) => gate.activate(registration, initialRuntimeSeed ? createTuiInitialLoadCoordinator(initialRuntimeSeed) : undefined));
1021
- registerStableTuiSlots(api, gate.current);
1022
- }
1023
- const tui = async api => {
1024
- const registrationGate = createTuiRegistrationGate();
1025
- api.lifecycle.onDispose(() => {
1026
- registrationGate.dispose();
1027
- disposeQuotaTelemetryOwner(createTuiQuotaClient(api));
1028
- });
1029
- void initializeTuiRegistration(api, registrationGate).catch(() => {});
1030
- };
1031
- const pluginModule = {
1032
- id,
1033
- tui
1034
- };
1035
- export default pluginModule;
621
+ });
622
+ export default plugin;