gloomberb 0.9.9 → 0.10.0

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 (519) hide show
  1. package/README.md +41 -3
  2. package/README.zh-CN.md +294 -0
  3. package/package.json +10 -4
  4. package/patches/@opentui%2Fcore@0.3.2.patch +26 -69
  5. package/src/api-client/auth.ts +2 -0
  6. package/src/api-client/index.test.ts +4 -0
  7. package/src/api-client/types.ts +3 -0
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -0
  9. package/src/app/pane-template-dialog-wizard.test.tsx +36 -0
  10. package/src/app/pane-template-dialog-wizard.tsx +10 -3
  11. package/src/app/runtime/desktop-deeplink.test.ts +15 -0
  12. package/src/app/runtime/desktop-deeplink.ts +21 -6
  13. package/src/app.tsx +40 -33
  14. package/src/capabilities/service-capabilities.ts +7 -2
  15. package/src/cli/commands/automation.test.ts +202 -0
  16. package/src/cli/commands/automation.ts +153 -36
  17. package/src/cli/commands/market.ts +1 -14
  18. package/src/cli/commands/system.ts +4 -1
  19. package/src/cli/context.ts +1 -1
  20. package/src/cli/desktop-pane-shot.ts +65 -4
  21. package/src/cli/entry.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +532 -0
  23. package/src/cli/pane-functions/catalog.ts +39 -4
  24. package/src/cli/pane-functions/data.test.ts +17 -0
  25. package/src/cli/pane-functions/data.ts +55 -13
  26. package/src/cli/pane-functions/discovery.ts +36 -9
  27. package/src/cli/pane-functions/index.test.ts +104 -1
  28. package/src/cli/pane-functions/index.ts +64 -5
  29. package/src/cli/pane-functions/options.ts +21 -6
  30. package/src/cli/pane-functions/report.ts +735 -72
  31. package/src/cli/pane-functions/resolver.ts +70 -6
  32. package/src/cli/pane-functions/screenshot.test.ts +431 -0
  33. package/src/cli/pane-functions/screenshot.ts +952 -8
  34. package/src/components/chart/composite/column-layout.ts +215 -0
  35. package/src/components/chart/composite/composite-chart.test.tsx +630 -0
  36. package/src/components/chart/composite/composite-chart.tsx +1148 -0
  37. package/src/components/chart/composite/format.test.ts +137 -0
  38. package/src/components/chart/composite/format.ts +135 -0
  39. package/src/components/chart/composite/index.ts +38 -0
  40. package/src/components/chart/composite/interactions.test.ts +109 -0
  41. package/src/components/chart/composite/interactions.ts +247 -0
  42. package/src/components/chart/composite/price-series.test.ts +39 -0
  43. package/src/components/chart/composite/price-series.ts +78 -0
  44. package/src/components/chart/composite/rasterizer.ts +359 -0
  45. package/src/components/chart/composite/renderers.test.ts +610 -0
  46. package/src/components/chart/composite/scene.test.ts +464 -0
  47. package/src/components/chart/composite/scene.ts +553 -0
  48. package/src/components/chart/composite/text-renderer.ts +285 -0
  49. package/src/components/chart/composite/time-scale.test.ts +88 -0
  50. package/src/components/chart/composite/time-scale.ts +217 -0
  51. package/src/components/chart/composite/types.ts +141 -0
  52. package/src/components/chart/core/data.ts +81 -5
  53. package/src/components/chart/core/index.test.ts +128 -43
  54. package/src/components/chart/core/resolution.ts +0 -6
  55. package/src/components/chart/native/chart-rasterizer.ts +1 -3
  56. package/src/components/chart/native/kitty/index.test.ts +99 -120
  57. package/src/components/chart/native/kitty/manager.ts +3 -0
  58. package/src/components/chart/native/kitty/protocol.ts +4 -1
  59. package/src/components/chart/native/raster/crosshair.ts +79 -46
  60. package/src/components/chart/native/raster/types.ts +2 -11
  61. package/src/components/chart/native/surface/manager.ts +48 -3
  62. package/src/components/chart/price-axis-labels.tsx +18 -4
  63. package/src/components/command-bar/commands/direct/index.ts +25 -0
  64. package/src/components/command-bar/commands/registry.ts +9 -0
  65. package/src/components/command-bar/helpers.test.ts +4 -1
  66. package/src/components/command-bar/list/view.tsx +18 -6
  67. package/src/components/command-bar/multi-select-picker.tsx +6 -5
  68. package/src/components/command-bar/pane-settings.test.ts +141 -0
  69. package/src/components/command-bar/pane-settings.ts +61 -1
  70. package/src/components/command-bar/panel/body.tsx +7 -6
  71. package/src/components/command-bar/routes/root/command-items.ts +4 -4
  72. package/src/components/command-bar/routes/root/results.ts +10 -1
  73. package/src/components/command-bar/routes/root/selection.test.ts +54 -0
  74. package/src/components/command-bar/routes/root/selection.ts +13 -0
  75. package/src/components/command-bar/routing/actions.ts +18 -2
  76. package/src/components/command-bar/routing/list-state.ts +2 -1
  77. package/src/components/command-bar/surface/index.test.tsx +36 -4
  78. package/src/components/command-bar/surface/pane.test.tsx +93 -0
  79. package/src/components/command-bar/theme-picker.tsx +2 -1
  80. package/src/components/command-bar/view-model.ts +19 -20
  81. package/src/components/command-bar/workflow/body.tsx +2 -1
  82. package/src/components/command-bar/workflow/field-row.tsx +3 -2
  83. package/src/components/command-bar/workflow/fields.ts +4 -2
  84. package/src/components/command-bar/workflow/ops.test.ts +161 -0
  85. package/src/components/command-bar/workflow/ops.ts +20 -50
  86. package/src/components/command-bar/workflow/route-actions.test.ts +34 -0
  87. package/src/components/command-bar/workflow/route-actions.ts +4 -0
  88. package/src/components/command-bar/workflow/types.ts +1 -0
  89. package/src/components/feed-data-table/stack-view.tsx +6 -5
  90. package/src/components/index.ts +13 -1
  91. package/src/components/layout/detached-pane-shell.tsx +2 -1
  92. package/src/components/layout/floating-pane.tsx +3 -0
  93. package/src/components/layout/header.tsx +15 -8
  94. package/src/components/layout/pane/footer/index.test.tsx +22 -3
  95. package/src/components/layout/pane/footer/index.tsx +3 -1
  96. package/src/components/layout/pane/footer/model.ts +9 -2
  97. package/src/components/layout/pane/header.tsx +4 -6
  98. package/src/components/layout/pane/index.tsx +3 -0
  99. package/src/components/layout/pane/sidebar.test.tsx +100 -0
  100. package/src/components/layout/pane/sidebar.tsx +239 -0
  101. package/src/components/layout/pane/title.ts +4 -3
  102. package/src/components/layout/shell/action-menu-overlay.tsx +5 -3
  103. package/src/components/layout/shell/drag/runtime.ts +2 -0
  104. package/src/components/layout/shell/menu.test.ts +19 -0
  105. package/src/components/layout/shell/menu.ts +8 -5
  106. package/src/components/layout/shell/native/pointer-runtime.ts +4 -1
  107. package/src/components/layout/shell/pane/layers.tsx +2 -2
  108. package/src/components/layout/status-bar.tsx +8 -7
  109. package/src/components/layout/window-controls.tsx +4 -3
  110. package/src/components/metric-treemap/index.tsx +3 -2
  111. package/src/components/onboarding/onboarding-steps.tsx +30 -29
  112. package/src/components/onboarding/onboarding-wizard.tsx +5 -4
  113. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +15 -12
  114. package/src/components/onboarding/portfolio-step/broker-setup-panel.tsx +17 -16
  115. package/src/components/onboarding/portfolio-step/broker-sync-panel.tsx +6 -5
  116. package/src/components/onboarding/portfolio-step/choose-panel.tsx +5 -4
  117. package/src/components/pane-settings-dialog/desktop.tsx +25 -3
  118. package/src/components/pane-settings-dialog/field-dialogs.tsx +5 -4
  119. package/src/components/pane-settings-dialog/tui.tsx +11 -9
  120. package/src/components/pane-settings-dialog/value.ts +8 -5
  121. package/src/components/pane-settings-dialog.test.tsx +235 -0
  122. package/src/components/pane-settings-dialog.tsx +24 -0
  123. package/src/components/pane-template-wizard.tsx +6 -2
  124. package/src/components/ticker/list-table-view.tsx +3 -2
  125. package/src/components/ui/button.tsx +3 -1
  126. package/src/components/ui/choice-dialog.tsx +6 -1
  127. package/src/components/ui/confirm-dialog.tsx +3 -2
  128. package/src/components/ui/fields.tsx +10 -1
  129. package/src/components/ui/frame.tsx +4 -1
  130. package/src/components/ui/index.ts +5 -0
  131. package/src/components/ui/inline-quick-add.tsx +104 -0
  132. package/src/components/ui/list-view.tsx +5 -4
  133. package/src/components/ui/loading.tsx +2 -1
  134. package/src/components/ui/multi-select/dialog.tsx +188 -44
  135. package/src/components/ui/popover.tsx +10 -0
  136. package/src/components/ui/shortcut-hint.tsx +2 -0
  137. package/src/components/ui/status.tsx +4 -3
  138. package/src/components/ui/tabs.tsx +19 -8
  139. package/src/components/ui/toggle.tsx +44 -3
  140. package/src/components/ui/ui.test.tsx +67 -5
  141. package/src/core/app-services.ts +4 -6
  142. package/src/core/state/session-persistence.ts +3 -2
  143. package/src/data/config/chart-settings.ts +712 -0
  144. package/src/data/config/layout.ts +24 -5
  145. package/src/data/config/store/index.test.ts +377 -14
  146. package/src/data/config/store/normalize.ts +79 -65
  147. package/src/data/fred-series.ts +137 -0
  148. package/src/i18n/index.test.ts +114 -0
  149. package/src/i18n/index.ts +147 -0
  150. package/src/i18n/ja.ts +1013 -0
  151. package/src/i18n/ko.ts +1011 -0
  152. package/src/i18n/languages.test.ts +31 -0
  153. package/src/i18n/languages.ts +72 -0
  154. package/src/i18n/zh-cn.ts +1013 -0
  155. package/src/i18n/zh-tw.ts +1013 -0
  156. package/src/plugins/builtin/account-management/footer.ts +3 -2
  157. package/src/plugins/builtin/account-management/form-components.tsx +3 -2
  158. package/src/plugins/builtin/account-management/keyboard.ts +6 -0
  159. package/src/plugins/builtin/account-management/model.ts +25 -21
  160. package/src/plugins/builtin/account-management/navigation.test.ts +27 -0
  161. package/src/plugins/builtin/account-management/navigation.ts +24 -0
  162. package/src/plugins/builtin/account-management/pane.tsx +120 -85
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +10 -9
  164. package/src/plugins/builtin/ai/agent-history.ts +139 -0
  165. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +133 -51
  166. package/src/plugins/builtin/ai/ask-ai.test.tsx +83 -22
  167. package/src/plugins/builtin/ai/index.test.ts +302 -0
  168. package/src/plugins/builtin/ai/index.tsx +374 -40
  169. package/src/plugins/builtin/ai/pane-settings.test.ts +155 -0
  170. package/src/plugins/builtin/ai/pane-settings.ts +319 -0
  171. package/src/plugins/builtin/ai/pi/credential-store.test.ts +80 -0
  172. package/src/plugins/builtin/ai/pi/credential-store.ts +138 -0
  173. package/src/plugins/builtin/ai/pi/file-store.ts +138 -0
  174. package/src/plugins/builtin/ai/pi/host.test.ts +500 -0
  175. package/src/plugins/builtin/ai/pi/host.ts +564 -0
  176. package/src/plugins/builtin/ai/pi/index.ts +5 -0
  177. package/src/plugins/builtin/ai/pi/models-store.test.ts +44 -0
  178. package/src/plugins/builtin/ai/pi/models-store.ts +141 -0
  179. package/src/plugins/builtin/ai/pi/providers.ts +68 -0
  180. package/src/plugins/builtin/ai/pi/runtime.test.ts +240 -0
  181. package/src/plugins/builtin/ai/pi/runtime.ts +635 -0
  182. package/src/plugins/builtin/ai/providers.test.ts +68 -0
  183. package/src/plugins/builtin/ai/providers.ts +163 -38
  184. package/src/plugins/builtin/ai/runner-selection.test.ts +174 -0
  185. package/src/plugins/builtin/ai/runner-selection.ts +193 -0
  186. package/src/plugins/builtin/ai/runner-selector.tsx +153 -0
  187. package/src/plugins/builtin/ai/runner.test.ts +198 -0
  188. package/src/plugins/builtin/ai/runner.ts +276 -73
  189. package/src/plugins/builtin/ai/screener/contract.test.ts +26 -73
  190. package/src/plugins/builtin/ai/screener/contract.ts +14 -52
  191. package/src/plugins/builtin/ai/screener/editor-runtime.ts +55 -5
  192. package/src/plugins/builtin/ai/screener/editor.tsx +42 -36
  193. package/src/plugins/builtin/ai/screener/footer.ts +82 -67
  194. package/src/plugins/builtin/ai/screener/keyboard.ts +27 -11
  195. package/src/plugins/builtin/ai/screener/model.test.ts +24 -0
  196. package/src/plugins/builtin/ai/screener/model.ts +9 -3
  197. package/src/plugins/builtin/ai/screener/pane.test.tsx +142 -55
  198. package/src/plugins/builtin/ai/screener/pane.tsx +95 -97
  199. package/src/plugins/builtin/ai/screener/results-view.tsx +8 -41
  200. package/src/plugins/builtin/ai/screener/runner.ts +41 -25
  201. package/src/plugins/builtin/ai/screener/tabs-bar.tsx +2 -2
  202. package/src/plugins/builtin/ai/use-runtime-providers.ts +28 -0
  203. package/src/plugins/builtin/ai/workspace/model.test.ts +227 -0
  204. package/src/plugins/builtin/ai/workspace/model.ts +308 -0
  205. package/src/plugins/builtin/ai/workspace/pane.test.tsx +197 -0
  206. package/src/plugins/builtin/ai/workspace/pane.tsx +896 -0
  207. package/src/plugins/builtin/analytics/index.test.tsx +3 -3
  208. package/src/plugins/builtin/analytics/index.tsx +3 -8
  209. package/src/plugins/builtin/broker-manager/detail.tsx +25 -24
  210. package/src/plugins/builtin/broker-manager/index.test.tsx +1 -1
  211. package/src/plugins/builtin/broker-manager/index.tsx +9 -11
  212. package/src/plugins/builtin/broker-manager/model.ts +11 -10
  213. package/src/plugins/builtin/broker-manager/pane-actions.tsx +22 -21
  214. package/src/plugins/builtin/broker-manager/table.ts +9 -8
  215. package/src/plugins/builtin/changelog/index.tsx +3 -8
  216. package/src/plugins/builtin/chart-composer/chart-spec.ts +89 -0
  217. package/src/plugins/builtin/chart-composer/cli-options.ts +98 -0
  218. package/src/plugins/builtin/chart-composer/date-window-dialog.tsx +120 -0
  219. package/src/plugins/builtin/chart-composer/editor.test.tsx +332 -0
  220. package/src/plugins/builtin/chart-composer/editor.tsx +1107 -0
  221. package/src/plugins/builtin/chart-composer/index.tsx +228 -0
  222. package/src/plugins/builtin/chart-composer/pane.tsx +645 -0
  223. package/src/plugins/builtin/chart-composer/presets.test.ts +561 -0
  224. package/src/plugins/builtin/chart-composer/presets.ts +759 -0
  225. package/src/plugins/builtin/chart-composer/quick-add.test.tsx +245 -0
  226. package/src/plugins/builtin/chart-composer/quick-add.tsx +409 -0
  227. package/src/plugins/builtin/chart-composer/semantic.ts +86 -0
  228. package/src/plugins/builtin/chart-composer/series-catalog.test.ts +65 -0
  229. package/src/plugins/builtin/chart-composer/series-catalog.ts +295 -0
  230. package/src/plugins/builtin/chart-composer/settings.test.ts +163 -0
  231. package/src/plugins/builtin/chart-composer/settings.ts +397 -0
  232. package/src/plugins/builtin/chart-composer/shortcuts.test.ts +44 -0
  233. package/src/plugins/builtin/chart-composer/shortcuts.ts +35 -0
  234. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +103 -0
  235. package/src/plugins/builtin/chat/channel-labels.ts +5 -4
  236. package/src/plugins/builtin/chat/channels.ts +12 -11
  237. package/src/plugins/builtin/chat/chat.test.tsx +32 -0
  238. package/src/plugins/builtin/chat/content/composer-runtime.ts +6 -1
  239. package/src/plugins/builtin/chat/content/composer.tsx +9 -8
  240. package/src/plugins/builtin/chat/content/index.tsx +36 -1
  241. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +8 -7
  242. package/src/plugins/builtin/chat/content/transcript.tsx +9 -3
  243. package/src/plugins/builtin/chat/controller/utils.ts +10 -9
  244. package/src/plugins/builtin/chat/message/desktop.tsx +17 -10
  245. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +122 -3
  246. package/src/plugins/builtin/chat/message/profile-popover.tsx +38 -1
  247. package/src/plugins/builtin/chat/message/terminal.tsx +19 -10
  248. package/src/plugins/builtin/chat/pane-state.test.ts +31 -0
  249. package/src/plugins/builtin/chat/pane-state.ts +30 -0
  250. package/src/plugins/builtin/chat/pane.tsx +27 -4
  251. package/src/plugins/builtin/chat/profile-popover.ts +62 -5
  252. package/src/plugins/builtin/chat/sidebar.test.tsx +4 -7
  253. package/src/plugins/builtin/chat/sidebar.tsx +107 -168
  254. package/src/plugins/builtin/chat/test-harness.tsx +6 -0
  255. package/src/plugins/builtin/cloud/auth-actions.tsx +5 -4
  256. package/src/plugins/builtin/cloud/plugin.tsx +150 -127
  257. package/src/plugins/builtin/composite-plugins.ts +71 -0
  258. package/src/plugins/builtin/correlation/index.test.tsx +4 -4
  259. package/src/plugins/builtin/correlation/index.tsx +3 -8
  260. package/src/plugins/builtin/earnings/index.tsx +37 -21
  261. package/src/plugins/builtin/earnings/model.test.ts +52 -0
  262. package/src/plugins/builtin/earnings/model.ts +32 -0
  263. package/src/plugins/builtin/econ/detail-view.tsx +17 -4
  264. package/src/plugins/builtin/econ/fred-cache.test.ts +57 -11
  265. package/src/plugins/builtin/econ/index.tsx +16 -20
  266. package/src/plugins/builtin/fear-greed/index.tsx +2 -8
  267. package/src/plugins/builtin/fx-matrix/index.tsx +3 -8
  268. package/src/plugins/builtin/help/components.tsx +5 -4
  269. package/src/plugins/builtin/help/index.tsx +14 -17
  270. package/src/plugins/builtin/holders/index.tsx +2 -8
  271. package/src/plugins/builtin/insider/index.tsx +2 -7
  272. package/src/plugins/builtin/kelly-sizer/index.test.tsx +2 -2
  273. package/src/plugins/builtin/kelly-sizer/index.tsx +3 -7
  274. package/src/plugins/builtin/kelly-sizer/test-support.tsx +3 -3
  275. package/src/plugins/builtin/layout-manager/index.tsx +3 -7
  276. package/src/plugins/builtin/market-heatmap/index.tsx +3 -8
  277. package/src/plugins/builtin/market-movers/index.tsx +3 -8
  278. package/src/plugins/builtin/news/index.tsx +12 -16
  279. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +3 -3
  280. package/src/plugins/builtin/news/wire/index.tsx +83 -73
  281. package/src/plugins/builtin/options/index.tsx +2 -8
  282. package/src/plugins/builtin/plugin-module.test.ts +89 -0
  283. package/src/plugins/builtin/plugin-module.ts +116 -0
  284. package/src/plugins/builtin/portfolio-list/header.tsx +4 -3
  285. package/src/plugins/builtin/portfolio-list/index.test.ts +17 -0
  286. package/src/plugins/builtin/portfolio-list/index.tsx +3 -5
  287. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +3 -3
  288. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +53 -53
  289. package/src/plugins/builtin/portfolio-list/settings.ts +5 -3
  290. package/src/plugins/builtin/portfolio-list/summary/index.tsx +4 -3
  291. package/src/plugins/builtin/research/index.tsx +2 -8
  292. package/src/plugins/builtin/sec/index.tsx +2 -8
  293. package/src/plugins/builtin/sectors/index.tsx +3 -8
  294. package/src/plugins/builtin/thirteenf/index.tsx +3 -8
  295. package/src/plugins/builtin/thirteenf/pane.test.tsx +1 -1
  296. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +49 -23
  297. package/src/plugins/builtin/ticker-detail/index.test.tsx +15 -136
  298. package/src/plugins/builtin/ticker-detail/index.tsx +2 -93
  299. package/src/plugins/builtin/ticker-detail/overview/components.tsx +6 -5
  300. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +41 -17
  301. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -3
  302. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +1 -1
  303. package/src/plugins/builtin/ticker-detail/settings.ts +3 -50
  304. package/src/plugins/builtin/ticker-research-backend-plugin.ts +17 -0
  305. package/src/plugins/builtin/ticker-research-plugin.ts +27 -0
  306. package/src/plugins/builtin/tv/channels.ts +27 -0
  307. package/src/plugins/builtin/tv/index.tsx +40 -0
  308. package/src/plugins/builtin/tv/pane.tsx +286 -0
  309. package/src/plugins/builtin/tv/youtube-stream.ts +100 -0
  310. package/src/plugins/builtin/world-indices/index.tsx +3 -8
  311. package/src/plugins/builtin/yield-curve/index.tsx +8 -8
  312. package/src/plugins/catalog-backend.test.ts +21 -0
  313. package/src/plugins/catalog-backend.ts +42 -0
  314. package/src/plugins/catalog-ui.ts +6 -14
  315. package/src/plugins/ibkr/instance-selection.test.ts +0 -1
  316. package/src/plugins/ownership.ts +143 -0
  317. package/src/plugins/prediction-markets/backend-plugin.ts +19 -0
  318. package/src/plugins/prediction-markets/chart.test.tsx +7 -5
  319. package/src/plugins/prediction-markets/chart.tsx +25 -7
  320. package/src/plugins/registry/index.test.ts +51 -0
  321. package/src/plugins/registry/index.ts +8 -0
  322. package/src/plugins/registry/pane-settings.ts +1 -1
  323. package/src/plugins/runtime/index.test.tsx +2 -2
  324. package/src/remote/app-host.tsx +16 -3
  325. package/src/remote/controller.test.ts +102 -1
  326. package/src/remote/controller.ts +122 -0
  327. package/src/remote/types.ts +11 -0
  328. package/src/renderers/electrobun/bun/core-capabilities.ts +123 -27
  329. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +5 -1
  330. package/src/renderers/electrobun/bun/desktop/initialization.ts +5 -1
  331. package/src/renderers/electrobun/bun/desktop/media.ts +15 -0
  332. package/src/renderers/electrobun/bun/desktop/window-style.ts +4 -1
  333. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +32 -1
  334. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +10 -0
  335. package/src/renderers/electrobun/bun/index.ts +5 -0
  336. package/src/renderers/electrobun/bun/tui-entry.ts +1 -6
  337. package/src/renderers/electrobun/view/ai-host.ts +124 -19
  338. package/src/renderers/electrobun/view/backend-rpc.ts +4 -0
  339. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +151 -39
  340. package/src/renderers/electrobun/view/desktop/popover.tsx +166 -0
  341. package/src/renderers/electrobun/view/dialog-host.test.ts +25 -0
  342. package/src/renderers/electrobun/view/dialog-host.tsx +75 -12
  343. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  344. package/src/renderers/electrobun/view/host/chart-surface.tsx +10 -9
  345. package/src/renderers/electrobun/view/host/input.tsx +9 -19
  346. package/src/renderers/electrobun/view/host/media-surface.tsx +170 -0
  347. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  348. package/src/renderers/electrobun/view/host/style.ts +1 -1
  349. package/src/renderers/electrobun/view/host/tabs.tsx +4 -3
  350. package/src/renderers/electrobun/view/main.tsx +10 -3
  351. package/src/renderers/electrobun/view/styles.css +17 -0
  352. package/src/renderers/electrobun/view/ui-host.tsx +7 -0
  353. package/src/renderers/opentui/chart-surface.test.tsx +52 -6
  354. package/src/renderers/opentui/chart-surface.tsx +95 -11
  355. package/src/renderers/opentui/dialog-host.tsx +36 -5
  356. package/src/renderers/opentui/host.tsx +39 -0
  357. package/src/renderers/opentui/start.tsx +30 -0
  358. package/src/renderers/opentui/ui-host.tsx +2 -0
  359. package/src/sources/provider-router/index.test.ts +43 -0
  360. package/src/sources/provider-router/index.ts +4 -0
  361. package/src/sources/provider-router/supplemental.ts +46 -0
  362. package/src/sources/sec-edgar.test.ts +203 -2
  363. package/src/sources/sec-edgar.ts +70 -15
  364. package/src/state/app/context/index.tsx +13 -1
  365. package/src/state/app/context/selector.test.tsx +62 -1
  366. package/src/state/session-persistence.test.ts +13 -2
  367. package/src/sync/controller.test.ts +190 -3
  368. package/src/sync/controller.ts +76 -45
  369. package/src/sync/core-contributors.test.ts +121 -0
  370. package/src/sync/core-contributors.ts +104 -27
  371. package/src/sync/react.ts +6 -8
  372. package/src/sync/types.ts +3 -0
  373. package/src/time-series/alignment.ts +158 -0
  374. package/src/time-series/economic.ts +49 -0
  375. package/src/time-series/field-catalog.ts +306 -0
  376. package/src/time-series/fundamentals.test.ts +505 -0
  377. package/src/time-series/fundamentals.ts +842 -0
  378. package/src/time-series/hooks.ts +45 -0
  379. package/src/time-series/index.ts +14 -0
  380. package/src/time-series/live-quotes.test.ts +219 -0
  381. package/src/time-series/live-quotes.ts +203 -0
  382. package/src/time-series/market-resolution.ts +53 -0
  383. package/src/time-series/market.ts +125 -0
  384. package/src/time-series/reporting.ts +98 -0
  385. package/src/time-series/resolve.test.ts +1281 -0
  386. package/src/time-series/resolve.ts +783 -0
  387. package/src/time-series/spec-studies.test.ts +400 -0
  388. package/src/time-series/spec.ts +487 -0
  389. package/src/time-series/studies.ts +563 -0
  390. package/src/time-series/transforms.test.ts +105 -0
  391. package/src/time-series/transforms.ts +152 -0
  392. package/src/time-series/types.ts +162 -0
  393. package/src/time-series/use-chart-resolution.ts +166 -0
  394. package/src/types/config.ts +9 -11
  395. package/src/types/desktop-window.ts +1 -0
  396. package/src/types/financials.ts +4 -0
  397. package/src/types/media.ts +17 -0
  398. package/src/types/plugin.ts +32 -1
  399. package/src/ui/context-menu.tsx +15 -3
  400. package/src/ui/dialog.tsx +14 -2
  401. package/src/ui/host.tsx +40 -1
  402. package/src/ui/index.tsx +9 -0
  403. package/src/utils/exchanges.test.ts +13 -1
  404. package/src/utils/exchanges.ts +10 -0
  405. package/src/utils/financial-statements.test.ts +138 -0
  406. package/src/utils/financial-statements.ts +115 -8
  407. package/src/utils/format.test.ts +11 -1
  408. package/src/utils/format.ts +7 -0
  409. package/src/version.ts +1 -1
  410. package/src/components/chart/bar-chart-renderer.test.ts +0 -233
  411. package/src/components/chart/bar-chart-renderer.ts +0 -445
  412. package/src/components/chart/comparison/data.test.ts +0 -159
  413. package/src/components/chart/comparison/data.ts +0 -302
  414. package/src/components/chart/comparison/renderer.test.ts +0 -187
  415. package/src/components/chart/comparison/renderer.ts +0 -282
  416. package/src/components/chart/comparison/scene.ts +0 -178
  417. package/src/components/chart/comparison-stock-chart/axis-width.ts +0 -99
  418. package/src/components/chart/comparison-stock-chart/canvas.ts +0 -148
  419. package/src/components/chart/comparison-stock-chart/controls.ts +0 -252
  420. package/src/components/chart/comparison-stock-chart/helpers.ts +0 -101
  421. package/src/components/chart/comparison-stock-chart/interactions.ts +0 -208
  422. package/src/components/chart/comparison-stock-chart/layout-metrics.ts +0 -62
  423. package/src/components/chart/comparison-stock-chart/layout.tsx +0 -155
  424. package/src/components/chart/comparison-stock-chart/legend.tsx +0 -74
  425. package/src/components/chart/comparison-stock-chart/native-surfaces.ts +0 -89
  426. package/src/components/chart/comparison-stock-chart/render-data.ts +0 -227
  427. package/src/components/chart/comparison-stock-chart/render-output.ts +0 -140
  428. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +0 -128
  429. package/src/components/chart/comparison-stock-chart/sources.ts +0 -51
  430. package/src/components/chart/comparison-stock-chart/surface-runtime.ts +0 -159
  431. package/src/components/chart/comparison-stock-chart/toolbar.tsx +0 -117
  432. package/src/components/chart/comparison-stock-chart/types.ts +0 -22
  433. package/src/components/chart/comparison-stock-chart/view.tsx +0 -572
  434. package/src/components/chart/comparison-stock-chart/viewport-runtime.ts +0 -239
  435. package/src/components/chart/comparison-stock-chart.tsx +0 -24
  436. package/src/components/chart/core/axis-measure.ts +0 -33
  437. package/src/components/chart/core/controller.test.ts +0 -157
  438. package/src/components/chart/core/controller.ts +0 -349
  439. package/src/components/chart/core/indicator-selector.tsx +0 -75
  440. package/src/components/chart/core/keyboard.ts +0 -24
  441. package/src/components/chart/core/pane-settings.test.ts +0 -52
  442. package/src/components/chart/core/pane-settings.ts +0 -141
  443. package/src/components/chart/core/scroll.test.ts +0 -64
  444. package/src/components/chart/core/scroll.ts +0 -82
  445. package/src/components/chart/core/window-viewport.test.ts +0 -69
  446. package/src/components/chart/core/window-viewport.ts +0 -162
  447. package/src/components/chart/display-cursor.ts +0 -153
  448. package/src/components/chart/indicators/bands.test.ts +0 -36
  449. package/src/components/chart/indicators/bands.ts +0 -29
  450. package/src/components/chart/indicators/moving-averages.test.ts +0 -39
  451. package/src/components/chart/indicators/moving-averages.ts +0 -42
  452. package/src/components/chart/indicators/options.test.ts +0 -32
  453. package/src/components/chart/indicators/options.ts +0 -107
  454. package/src/components/chart/indicators/oscillators.test.ts +0 -58
  455. package/src/components/chart/indicators/oscillators.ts +0 -92
  456. package/src/components/chart/native/canvas-bitmaps.ts +0 -140
  457. package/src/components/chart/native/raster/comparison-chart.ts +0 -162
  458. package/src/components/chart/native/surface/hooks.ts +0 -144
  459. package/src/components/chart/native/surface/rendering.ts +0 -211
  460. package/src/components/chart/static/bar-chart-surface.test.tsx +0 -101
  461. package/src/components/chart/static/bar-chart-surface.tsx +0 -292
  462. package/src/components/chart/stock-chart/auto.ts +0 -216
  463. package/src/components/chart/stock-chart/axis-width.ts +0 -146
  464. package/src/components/chart/stock-chart/bitmaps.ts +0 -22
  465. package/src/components/chart/stock-chart/canvas.ts +0 -202
  466. package/src/components/chart/stock-chart/controls.tsx +0 -149
  467. package/src/components/chart/stock-chart/cursor.ts +0 -60
  468. package/src/components/chart/stock-chart/data-runtime.ts +0 -128
  469. package/src/components/chart/stock-chart/footer.test.ts +0 -65
  470. package/src/components/chart/stock-chart/footer.ts +0 -303
  471. package/src/components/chart/stock-chart/geometry.ts +0 -160
  472. package/src/components/chart/stock-chart/index.test.ts +0 -532
  473. package/src/components/chart/stock-chart/index.tsx +0 -29
  474. package/src/components/chart/stock-chart/indicators.ts +0 -226
  475. package/src/components/chart/stock-chart/interaction-runtime.ts +0 -44
  476. package/src/components/chart/stock-chart/interactions.ts +0 -300
  477. package/src/components/chart/stock-chart/keyboard.ts +0 -373
  478. package/src/components/chart/stock-chart/layout-metrics.ts +0 -62
  479. package/src/components/chart/stock-chart/layout.tsx +0 -177
  480. package/src/components/chart/stock-chart/native-surfaces.ts +0 -121
  481. package/src/components/chart/stock-chart/presentation.ts +0 -67
  482. package/src/components/chart/stock-chart/renderer-switch.test.tsx +0 -263
  483. package/src/components/chart/stock-chart/rendering/bounds.ts +0 -116
  484. package/src/components/chart/stock-chart/rendering/candidates.ts +0 -145
  485. package/src/components/chart/stock-chart/rendering/data.ts +0 -370
  486. package/src/components/chart/stock-chart/rendering/invalidation.ts +0 -68
  487. package/src/components/chart/stock-chart/rendering/output.ts +0 -304
  488. package/src/components/chart/stock-chart/rendering/planning.ts +0 -122
  489. package/src/components/chart/stock-chart/rendering/projection.test.tsx +0 -98
  490. package/src/components/chart/stock-chart/rendering/projection.ts +0 -112
  491. package/src/components/chart/stock-chart/rendering/state.test.ts +0 -70
  492. package/src/components/chart/stock-chart/rendering/state.ts +0 -283
  493. package/src/components/chart/stock-chart/requests.ts +0 -337
  494. package/src/components/chart/stock-chart/resolution-support.ts +0 -96
  495. package/src/components/chart/stock-chart/resolved/runtime.ts +0 -289
  496. package/src/components/chart/stock-chart/resolved.tsx +0 -359
  497. package/src/components/chart/stock-chart/settings.ts +0 -81
  498. package/src/components/chart/stock-chart/surface-runtime.ts +0 -62
  499. package/src/components/chart/stock-chart/types.ts +0 -25
  500. package/src/components/chart/stock-chart/view.tsx +0 -155
  501. package/src/components/chart/stock-chart/viewport/effects.ts +0 -293
  502. package/src/components/chart/stock-chart/viewport/index.ts +0 -144
  503. package/src/components/chart/stock-chart/viewport/runtime.ts +0 -375
  504. package/src/components/chart/time-axis-label.tsx +0 -175
  505. package/src/plugins/builtin/ai/gloomberb-cli.ts +0 -71
  506. package/src/plugins/builtin/ai/screener/action-bar.tsx +0 -58
  507. package/src/plugins/builtin/comparison-chart/index.test.tsx +0 -848
  508. package/src/plugins/builtin/comparison-chart/index.tsx +0 -194
  509. package/src/plugins/builtin/econ/fred-cache.ts +0 -106
  510. package/src/plugins/builtin/plugin-groups.ts +0 -130
  511. package/src/plugins/builtin/ticker-detail/chart-pane.tsx +0 -91
  512. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -100
  513. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +0 -296
  514. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/data.ts +0 -62
  515. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +0 -608
  516. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +0 -65
  517. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +0 -58
  518. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +0 -138
  519. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +0 -220
@@ -0,0 +1,319 @@
1
+ import type {
2
+ PaneSettingActionContext,
3
+ PaneSettingField,
4
+ PaneSettingOption,
5
+ PaneSettingsDef,
6
+ } from "../../../types/plugin";
7
+ import { migrateLegacyAiProviderId } from "./providers";
8
+
9
+ export const AI_DEFAULT_PROVIDER_SETTING_KEY = "defaultProviderId";
10
+ export const AI_DEFAULT_MODEL_SETTING_KEY = "defaultModelId";
11
+ export const AI_PANE_PROVIDER_SETTING_KEY = "providerId";
12
+ export const AI_PANE_MODEL_SETTING_KEY = "modelId";
13
+ export const AI_INHERIT_SETTING_VALUE = "";
14
+
15
+ export interface AiSharedDefaults {
16
+ providerId: string;
17
+ modelId: string | null;
18
+ }
19
+
20
+ export interface ResolveAiPaneSelectionOptions {
21
+ settings: Record<string, unknown> | undefined;
22
+ savedProviderId: string | null | undefined;
23
+ savedModelId: string | null | undefined;
24
+ defaultProviderId: string;
25
+ defaultModelId: string | null | undefined;
26
+ providerKey?: string;
27
+ modelKey?: string;
28
+ }
29
+
30
+ export interface AiSettingsModel {
31
+ id: string;
32
+ label: string;
33
+ providerId: string;
34
+ description?: string;
35
+ }
36
+
37
+ export interface AiSettingsProvider {
38
+ id: string;
39
+ label: string;
40
+ description?: string;
41
+ }
42
+
43
+ export interface AiAccountSettingRow {
44
+ providerId: string;
45
+ providerLabel: string;
46
+ description: string;
47
+ actionLabel: string;
48
+ disabled?: boolean;
49
+ action: (context: PaneSettingActionContext) => void | Promise<void>;
50
+ }
51
+
52
+ export interface AiManageAccountsSettingRow {
53
+ description?: string;
54
+ actionLabel?: string;
55
+ action: (context: PaneSettingActionContext) => void | Promise<void>;
56
+ }
57
+
58
+ export interface AiPaneOverrideSettings {
59
+ providerId: string | null | undefined;
60
+ modelId: string | null | undefined;
61
+ providerKey?: string;
62
+ modelKey?: string;
63
+ }
64
+
65
+ export interface BuildAiPaneSettingsOptions {
66
+ title: string;
67
+ providers: AiSettingsProvider[];
68
+ models: AiSettingsModel[];
69
+ defaultProviderId: string | null | undefined;
70
+ defaultModelId: string | null | undefined;
71
+ paneOverride?: AiPaneOverrideSettings;
72
+ accountRows?: AiAccountSettingRow[];
73
+ manageAccounts?: AiManageAccountsSettingRow;
74
+ additional?: PaneSettingsDef;
75
+ }
76
+
77
+ function normalizeId(value: string | null | undefined): string {
78
+ return value?.trim() ?? "";
79
+ }
80
+
81
+ function normalizeProviderId(value: string | null | undefined): string {
82
+ const providerId = normalizeId(value);
83
+ return providerId ? migrateLegacyAiProviderId(providerId) : "";
84
+ }
85
+
86
+ function settingValue(
87
+ settings: Record<string, unknown> | undefined,
88
+ key: string,
89
+ ): { configured: boolean; value: string } {
90
+ if (!settings || !Object.prototype.hasOwnProperty.call(settings, key)) {
91
+ return { configured: false, value: "" };
92
+ }
93
+ const value = settings[key];
94
+ return {
95
+ configured: true,
96
+ value: typeof value === "string" ? value.trim() : "",
97
+ };
98
+ }
99
+
100
+ export function resolveAiSharedDefaults(
101
+ pluginConfig: Record<string, unknown> | undefined,
102
+ fallbackProviderId: string,
103
+ ): AiSharedDefaults {
104
+ const providerId = typeof pluginConfig?.[AI_DEFAULT_PROVIDER_SETTING_KEY] === "string"
105
+ ? normalizeProviderId(pluginConfig[AI_DEFAULT_PROVIDER_SETTING_KEY] as string)
106
+ : "";
107
+ const modelId = typeof pluginConfig?.[AI_DEFAULT_MODEL_SETTING_KEY] === "string"
108
+ ? normalizeId(pluginConfig[AI_DEFAULT_MODEL_SETTING_KEY] as string)
109
+ : "";
110
+ return {
111
+ providerId: providerId || normalizeProviderId(fallbackProviderId),
112
+ modelId: modelId || null,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Resolves a pane's effective runner without rewriting the saved thread or tab.
118
+ * Missing settings preserve legacy saved selections. An explicitly blank value
119
+ * means "use the shared default", which lets old panes opt in intentionally.
120
+ */
121
+ export function resolveAiPaneSelection({
122
+ settings,
123
+ savedProviderId,
124
+ savedModelId,
125
+ defaultProviderId,
126
+ defaultModelId,
127
+ providerKey = AI_PANE_PROVIDER_SETTING_KEY,
128
+ modelKey = AI_PANE_MODEL_SETTING_KEY,
129
+ }: ResolveAiPaneSelectionOptions): AiSharedDefaults {
130
+ const providerSetting = settingValue(settings, providerKey);
131
+ const modelSetting = settingValue(settings, modelKey);
132
+ const configuredProvider = normalizeProviderId(providerSetting.value);
133
+ const savedProvider = normalizeProviderId(savedProviderId);
134
+ const sharedProvider = normalizeProviderId(defaultProviderId);
135
+ const providerId = providerSetting.configured
136
+ ? configuredProvider || sharedProvider
137
+ : savedProvider || sharedProvider;
138
+
139
+ let modelId: string | null;
140
+ if (modelSetting.configured) {
141
+ modelId = modelSetting.value
142
+ || (providerId === sharedProvider ? normalizeId(defaultModelId) : "")
143
+ || null;
144
+ } else if (providerSetting.configured && configuredProvider !== savedProvider) {
145
+ modelId = (providerId === sharedProvider ? normalizeId(defaultModelId) : "") || null;
146
+ } else {
147
+ modelId = normalizeId(savedModelId) || null;
148
+ }
149
+
150
+ return { providerId, modelId };
151
+ }
152
+
153
+ function appendCurrentOption(
154
+ options: PaneSettingOption[],
155
+ currentValue: string,
156
+ label: string,
157
+ ): PaneSettingOption[] {
158
+ if (!currentValue || options.some((option) => option.value === currentValue)) return options;
159
+ return [
160
+ ...options,
161
+ {
162
+ value: currentValue,
163
+ label,
164
+ description: "Currently selected, but not present in the latest provider catalog.",
165
+ },
166
+ ];
167
+ }
168
+
169
+ function uniqueOptions(options: PaneSettingOption[]): PaneSettingOption[] {
170
+ const seen = new Set<string>();
171
+ return options.filter((option) => {
172
+ if (seen.has(option.value)) return false;
173
+ seen.add(option.value);
174
+ return true;
175
+ });
176
+ }
177
+
178
+ function providerOptions(
179
+ providers: AiSettingsProvider[],
180
+ currentValue: string,
181
+ ): PaneSettingOption[] {
182
+ const options = uniqueOptions(providers
183
+ .map((provider) => ({
184
+ value: normalizeId(provider.id),
185
+ label: provider.label,
186
+ description: provider.description,
187
+ }))
188
+ .filter((provider) => provider.value));
189
+ return appendCurrentOption(options, currentValue, `${currentValue} (current)`);
190
+ }
191
+
192
+ function modelOptions(
193
+ models: AiSettingsModel[],
194
+ providerId: string,
195
+ currentValue: string,
196
+ automaticLabel: string,
197
+ ): PaneSettingOption[] {
198
+ const options = uniqueOptions([
199
+ {
200
+ value: AI_INHERIT_SETTING_VALUE,
201
+ label: automaticLabel,
202
+ },
203
+ ...models
204
+ .filter((model) => normalizeId(model.providerId) === providerId)
205
+ .map((model) => ({
206
+ value: normalizeId(model.id),
207
+ label: model.label,
208
+ description: model.description,
209
+ }))
210
+ .filter((model) => model.value),
211
+ ]);
212
+ return appendCurrentOption(options, currentValue, `${currentValue} (current)`);
213
+ }
214
+
215
+ function getProviderLabel(providers: AiSettingsProvider[], providerId: string): string {
216
+ return providers.find((provider) => normalizeId(provider.id) === providerId)?.label ?? providerId;
217
+ }
218
+
219
+ export function buildAiPaneSettingsDef(options: BuildAiPaneSettingsOptions): PaneSettingsDef {
220
+ const defaultProviderId = normalizeProviderId(options.defaultProviderId)
221
+ || normalizeProviderId(options.providers[0]?.id);
222
+ const defaultModelId = normalizeId(options.defaultModelId);
223
+ const paneProviderId = normalizeProviderId(options.paneOverride?.providerId);
224
+ const paneModelId = normalizeId(options.paneOverride?.modelId);
225
+ const effectiveProviderId = paneProviderId || defaultProviderId;
226
+ const providerLabel = getProviderLabel(options.providers, defaultProviderId) || "configured provider";
227
+ const paneProviderKey = options.paneOverride?.providerKey ?? AI_PANE_PROVIDER_SETTING_KEY;
228
+ const paneModelKey = options.paneOverride?.modelKey ?? AI_PANE_MODEL_SETTING_KEY;
229
+
230
+ const fields: PaneSettingField[] = [
231
+ {
232
+ key: AI_DEFAULT_PROVIDER_SETTING_KEY,
233
+ label: "Default provider",
234
+ description: "Used by new AI panes and conversations.",
235
+ type: "select",
236
+ storage: "plugin",
237
+ clearOnChange: [AI_DEFAULT_MODEL_SETTING_KEY],
238
+ options: providerOptions(options.providers, defaultProviderId),
239
+ },
240
+ {
241
+ key: AI_DEFAULT_MODEL_SETTING_KEY,
242
+ label: "Default model",
243
+ description: "Used when a pane does not have its own model override.",
244
+ type: "select",
245
+ storage: "plugin",
246
+ options: modelOptions(options.models, defaultProviderId, defaultModelId, "Auto (recommended)"),
247
+ },
248
+ ];
249
+
250
+ if (options.paneOverride) {
251
+ fields.push(
252
+ {
253
+ key: paneProviderKey,
254
+ label: "Provider for this pane",
255
+ description: "Overrides the shared default for this pane only.",
256
+ type: "select",
257
+ clearOnChange: [paneModelKey],
258
+ options: [
259
+ {
260
+ value: AI_INHERIT_SETTING_VALUE,
261
+ label: `Use default · ${providerLabel}`,
262
+ },
263
+ ...providerOptions(options.providers, paneProviderId),
264
+ ],
265
+ },
266
+ {
267
+ key: paneModelKey,
268
+ label: "Model for this pane",
269
+ description: "Overrides the shared default for this pane only.",
270
+ type: "select",
271
+ options: modelOptions(options.models, effectiveProviderId, paneModelId, "Use default"),
272
+ },
273
+ );
274
+ }
275
+
276
+ for (const account of options.accountRows ?? []) {
277
+ fields.push({
278
+ key: `account:${account.providerId}`,
279
+ label: `${account.providerLabel} account`,
280
+ description: account.description,
281
+ type: "action",
282
+ actionId: `ai:account:${account.providerId}`,
283
+ actionLabel: account.actionLabel,
284
+ disabled: account.disabled,
285
+ action: account.action,
286
+ });
287
+ }
288
+
289
+ if (options.manageAccounts) {
290
+ fields.push({
291
+ key: "manageAiAccounts",
292
+ label: "AI accounts",
293
+ description: options.manageAccounts.description ?? "Connect, reconnect, or disconnect AI providers.",
294
+ type: "action",
295
+ actionId: "ai:manage-accounts",
296
+ actionLabel: options.manageAccounts.actionLabel ?? "Manage",
297
+ action: options.manageAccounts.action,
298
+ });
299
+ }
300
+
301
+ return {
302
+ title: options.title,
303
+ values: {
304
+ [AI_DEFAULT_PROVIDER_SETTING_KEY]: defaultProviderId,
305
+ [AI_DEFAULT_MODEL_SETTING_KEY]: defaultModelId,
306
+ ...(options.paneOverride
307
+ ? {
308
+ [paneProviderKey]: paneProviderId,
309
+ [paneModelKey]: paneModelId,
310
+ }
311
+ : {}),
312
+ ...(options.additional?.values ?? {}),
313
+ },
314
+ fields: [
315
+ ...fields,
316
+ ...(options.additional?.fields ?? []),
317
+ ],
318
+ };
319
+ }
@@ -0,0 +1,80 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { PiFileCredentialStore, resolvePiCredentialPath } from "./credential-store";
6
+
7
+ const tempDirs: string[] = [];
8
+
9
+ async function tempDataDir(): Promise<string> {
10
+ const path = await mkdtemp(join(tmpdir(), "gloomberb-pi-credentials-"));
11
+ tempDirs.push(path);
12
+ return path;
13
+ }
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(tempDirs.splice(0).map((path) => rm(path, { recursive: true, force: true })));
17
+ });
18
+
19
+ describe("PiFileCredentialStore", () => {
20
+ test("persists credentials outside AppConfig and never returns mutable storage references", async () => {
21
+ const dataDir = await tempDataDir();
22
+ const store = new PiFileCredentialStore(dataDir);
23
+ await store.modify("anthropic", async () => ({
24
+ type: "oauth",
25
+ access: "access-one",
26
+ refresh: "refresh-one",
27
+ expires: 123,
28
+ accountId: "account-one",
29
+ }));
30
+
31
+ const first = await store.read("anthropic");
32
+ expect(first?.type).toBe("oauth");
33
+ if (first?.type === "oauth") first.access = "mutated-only-in-memory";
34
+
35
+ const reloaded = await new PiFileCredentialStore(dataDir).read("anthropic");
36
+ expect(reloaded).toMatchObject({
37
+ type: "oauth",
38
+ access: "access-one",
39
+ refresh: "refresh-one",
40
+ expires: 123,
41
+ accountId: "account-one",
42
+ });
43
+ expect(resolvePiCredentialPath(dataDir)).toBe(join(dataDir, "ai", "credentials.json"));
44
+
45
+ if (process.platform !== "win32") {
46
+ expect((await stat(resolvePiCredentialPath(dataDir))).mode & 0o777).toBe(0o600);
47
+ expect((await stat(dirname(resolvePiCredentialPath(dataDir)))).mode & 0o777).toBe(0o700);
48
+ }
49
+ });
50
+
51
+ test("serializes updates made through separate store instances", async () => {
52
+ const dataDir = await tempDataDir();
53
+ const first = new PiFileCredentialStore(dataDir);
54
+ const second = new PiFileCredentialStore(dataDir);
55
+
56
+ await Promise.all([
57
+ first.modify("anthropic", async () => ({ type: "api_key", key: "anthropic-key" })),
58
+ second.modify("openai", async () => ({ type: "api_key", key: "openai-key" })),
59
+ ]);
60
+
61
+ expect(await first.list()).toEqual([
62
+ { providerId: "anthropic", type: "api_key" },
63
+ { providerId: "openai", type: "api_key" },
64
+ ]);
65
+ });
66
+
67
+ test("fails closed instead of overwriting a malformed credential file", async () => {
68
+ const dataDir = await tempDataDir();
69
+ const path = resolvePiCredentialPath(dataDir);
70
+ await mkdir(dirname(path), { recursive: true });
71
+ await writeFile(path, JSON.stringify({ version: 1, credentials: { anthropic: { type: "oauth" } } }), "utf8");
72
+
73
+ const store = new PiFileCredentialStore(dataDir);
74
+ await expect(store.read("anthropic")).rejects.toThrow("stored AI credential");
75
+ await expect(store.modify("openai", async () => ({ type: "api_key", key: "key" }))).rejects.toThrow(
76
+ "stored AI credential",
77
+ );
78
+ expect(await readFile(path, "utf8")).toContain('"type":"oauth"');
79
+ });
80
+ });
@@ -0,0 +1,138 @@
1
+ import type {
2
+ ApiKeyCredential,
3
+ Credential,
4
+ CredentialInfo,
5
+ CredentialStore,
6
+ OAuthCredential,
7
+ } from "@earendil-works/pi-ai";
8
+ import { join } from "node:path";
9
+ import { LockedJsonFile } from "./file-store";
10
+
11
+ const CREDENTIAL_FILE_VERSION = 1;
12
+ const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
13
+
14
+ interface CredentialFile {
15
+ version: typeof CREDENTIAL_FILE_VERSION;
16
+ credentials: Record<string, Credential>;
17
+ }
18
+
19
+ function assertProviderId(providerId: string): void {
20
+ if (!PROVIDER_ID_PATTERN.test(providerId)) {
21
+ throw new Error(`Invalid AI provider id: ${providerId}`);
22
+ }
23
+ }
24
+
25
+ function isStringRecord(value: unknown): value is Record<string, string> {
26
+ return !!value
27
+ && typeof value === "object"
28
+ && !Array.isArray(value)
29
+ && Object.values(value).every((entry) => typeof entry === "string");
30
+ }
31
+
32
+ function isApiKeyCredential(value: unknown): value is ApiKeyCredential {
33
+ if (!value || typeof value !== "object" || (value as { type?: unknown }).type !== "api_key") return false;
34
+ const credential = value as { key?: unknown; env?: unknown };
35
+ return (credential.key === undefined || typeof credential.key === "string")
36
+ && (credential.env === undefined || isStringRecord(credential.env));
37
+ }
38
+
39
+ function isOAuthCredential(value: unknown): value is OAuthCredential {
40
+ if (!value || typeof value !== "object" || (value as { type?: unknown }).type !== "oauth") return false;
41
+ const credential = value as { access?: unknown; refresh?: unknown; expires?: unknown };
42
+ return typeof credential.access === "string"
43
+ && typeof credential.refresh === "string"
44
+ && typeof credential.expires === "number"
45
+ && Number.isFinite(credential.expires);
46
+ }
47
+
48
+ function isCredential(value: unknown): value is Credential {
49
+ return isApiKeyCredential(value) || isOAuthCredential(value);
50
+ }
51
+
52
+ function cloneCredential<T extends Credential | undefined>(credential: T): T {
53
+ return credential === undefined ? credential : structuredClone(credential);
54
+ }
55
+
56
+ function validateCredentialFile(value: unknown): CredentialFile {
57
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
58
+ throw new Error("The AI credential file is not an object.");
59
+ }
60
+ const candidate = value as { version?: unknown; credentials?: unknown };
61
+ if (candidate.version !== CREDENTIAL_FILE_VERSION) {
62
+ throw new Error(`Unsupported AI credential file version: ${String(candidate.version)}`);
63
+ }
64
+ if (!candidate.credentials || typeof candidate.credentials !== "object" || Array.isArray(candidate.credentials)) {
65
+ throw new Error("The AI credential file does not contain a credentials object.");
66
+ }
67
+
68
+ const credentials: Record<string, Credential> = Object.create(null) as Record<string, Credential>;
69
+ for (const [providerId, credential] of Object.entries(candidate.credentials)) {
70
+ assertProviderId(providerId);
71
+ if (!isCredential(credential)) {
72
+ throw new Error(`The stored AI credential for ${providerId} is invalid.`);
73
+ }
74
+ credentials[providerId] = cloneCredential(credential);
75
+ }
76
+ return { version: CREDENTIAL_FILE_VERSION, credentials };
77
+ }
78
+
79
+ function createCredentialFile(): CredentialFile {
80
+ return {
81
+ version: CREDENTIAL_FILE_VERSION,
82
+ credentials: Object.create(null) as Record<string, Credential>,
83
+ };
84
+ }
85
+
86
+ export function resolvePiCredentialPath(dataDir: string): string {
87
+ return join(dataDir, "ai", "credentials.json");
88
+ }
89
+
90
+ /** Persistent backend credential store for Pi. Secrets never enter AppConfig. */
91
+ export class PiFileCredentialStore implements CredentialStore {
92
+ readonly path: string;
93
+ private readonly file: LockedJsonFile<CredentialFile>;
94
+
95
+ constructor(dataDir: string) {
96
+ this.path = resolvePiCredentialPath(dataDir);
97
+ this.file = new LockedJsonFile(this.path, {
98
+ create: createCredentialFile,
99
+ validate: validateCredentialFile,
100
+ });
101
+ }
102
+
103
+ async read(providerId: string): Promise<Credential | undefined> {
104
+ assertProviderId(providerId);
105
+ const file = await this.file.read();
106
+ return cloneCredential(file.credentials[providerId]);
107
+ }
108
+
109
+ async list(): Promise<readonly CredentialInfo[]> {
110
+ const file = await this.file.read();
111
+ return Object.entries(file.credentials)
112
+ .map(([providerId, credential]) => ({ providerId, type: credential.type }))
113
+ .sort((left, right) => left.providerId.localeCompare(right.providerId));
114
+ }
115
+
116
+ async modify(
117
+ providerId: string,
118
+ update: (current: Credential | undefined) => Promise<Credential | undefined>,
119
+ ): Promise<Credential | undefined> {
120
+ assertProviderId(providerId);
121
+ return this.file.update(async (file) => {
122
+ const current = cloneCredential(file.credentials[providerId]);
123
+ const next = await update(current);
124
+ if (next !== undefined) {
125
+ if (!isCredential(next)) throw new Error(`Refusing to store an invalid AI credential for ${providerId}.`);
126
+ file.credentials[providerId] = cloneCredential(next);
127
+ }
128
+ return cloneCredential(file.credentials[providerId]);
129
+ });
130
+ }
131
+
132
+ async delete(providerId: string): Promise<void> {
133
+ assertProviderId(providerId);
134
+ await this.file.update((file) => {
135
+ delete file.credentials[providerId];
136
+ });
137
+ }
138
+ }
@@ -0,0 +1,138 @@
1
+ import { chmod, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+
5
+ const DIRECTORY_MODE = 0o700;
6
+ const FILE_MODE = 0o600;
7
+
8
+ export interface LockedJsonFileOptions<T> {
9
+ create: () => T;
10
+ validate: (value: unknown) => T;
11
+ lockTimeoutMs?: number;
12
+ staleLockMs?: number;
13
+ }
14
+
15
+ function errorCode(error: unknown): string | null {
16
+ return error && typeof error === "object" && "code" in error
17
+ ? String((error as { code?: unknown }).code)
18
+ : null;
19
+ }
20
+
21
+ function wait(delayMs: number): Promise<void> {
22
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
23
+ }
24
+
25
+ /**
26
+ * Small backend-only JSON store with atomic replacement and a cross-process
27
+ * lock. The lock is important because the TUI and desktop backend may share
28
+ * one Gloomberb data directory and Pi refreshes rotating OAuth tokens inside
29
+ * CredentialStore.modify().
30
+ */
31
+ export class LockedJsonFile<T> {
32
+ readonly path: string;
33
+
34
+ private readonly options: Required<Pick<LockedJsonFileOptions<T>, "lockTimeoutMs" | "staleLockMs">>
35
+ & Omit<LockedJsonFileOptions<T>, "lockTimeoutMs" | "staleLockMs">;
36
+ private writeQueue: Promise<void> = Promise.resolve();
37
+
38
+ constructor(path: string, options: LockedJsonFileOptions<T>) {
39
+ this.path = path;
40
+ this.options = {
41
+ ...options,
42
+ lockTimeoutMs: options.lockTimeoutMs ?? 45_000,
43
+ staleLockMs: options.staleLockMs ?? 10 * 60_000,
44
+ };
45
+ }
46
+
47
+ async read(): Promise<T> {
48
+ let raw: string;
49
+ try {
50
+ raw = await readFile(this.path, "utf8");
51
+ } catch (error) {
52
+ if (errorCode(error) === "ENOENT") return this.options.create();
53
+ throw error;
54
+ }
55
+
56
+ let parsed: unknown;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ } catch (error) {
60
+ throw new Error(`Could not parse ${this.path}`, { cause: error });
61
+ }
62
+ return this.options.validate(parsed);
63
+ }
64
+
65
+ update<R>(mutate: (current: T) => R | Promise<R>): Promise<R> {
66
+ const queued = this.writeQueue.catch(() => {}).then(async () => {
67
+ const release = await this.acquireLock();
68
+ try {
69
+ const current = await this.read();
70
+ const result = await mutate(current);
71
+ await this.write(current);
72
+ return result;
73
+ } finally {
74
+ await release();
75
+ }
76
+ });
77
+ this.writeQueue = queued.then(() => {}, () => {});
78
+ return queued;
79
+ }
80
+
81
+ private async write(value: T): Promise<void> {
82
+ await mkdir(dirname(this.path), { recursive: true, mode: DIRECTORY_MODE });
83
+ await chmod(dirname(this.path), DIRECTORY_MODE).catch(() => {});
84
+
85
+ const temporaryPath = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
86
+ const handle = await open(temporaryPath, "wx", FILE_MODE);
87
+ try {
88
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
89
+ await handle.sync();
90
+ } finally {
91
+ await handle.close();
92
+ }
93
+
94
+ try {
95
+ await rename(temporaryPath, this.path);
96
+ await chmod(this.path, FILE_MODE).catch(() => {});
97
+ } catch (error) {
98
+ await rm(temporaryPath, { force: true }).catch(() => {});
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ private async acquireLock(): Promise<() => Promise<void>> {
104
+ const lockPath = `${this.path}.lock`;
105
+ const token = `${process.pid}:${randomUUID()}`;
106
+ const deadline = Date.now() + this.options.lockTimeoutMs;
107
+ await mkdir(dirname(this.path), { recursive: true, mode: DIRECTORY_MODE });
108
+ await chmod(dirname(this.path), DIRECTORY_MODE).catch(() => {});
109
+
110
+ while (true) {
111
+ try {
112
+ const handle = await open(lockPath, "wx", FILE_MODE);
113
+ try {
114
+ await handle.writeFile(token, "utf8");
115
+ await handle.sync();
116
+ } finally {
117
+ await handle.close();
118
+ }
119
+ return async () => {
120
+ const owner = await readFile(lockPath, "utf8").catch(() => null);
121
+ if (owner === token) await rm(lockPath, { force: true }).catch(() => {});
122
+ };
123
+ } catch (error) {
124
+ if (errorCode(error) !== "EEXIST") throw error;
125
+ }
126
+
127
+ const lockStat = await stat(lockPath).catch(() => null);
128
+ if (lockStat && Date.now() - lockStat.mtimeMs >= this.options.staleLockMs) {
129
+ await rm(lockPath, { force: true }).catch(() => {});
130
+ continue;
131
+ }
132
+ if (Date.now() >= deadline) {
133
+ throw new Error(`Timed out waiting for the AI store lock at ${lockPath}`);
134
+ }
135
+ await wait(25);
136
+ }
137
+ }
138
+ }