gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -1,4 +1,5 @@
1
- import { cloneLayout, createBlankLayout, type SavedLayout } from "../../../types/config";
1
+ import { cloneLayout, createBlankLayout, getPlacedPaneInstanceIds, type SavedLayout } from "../../../types/config";
2
+ import { movePaneBack, movePaneToOwnLayout, paneMoveBackTarget } from "../../../layout/pane-layout-move";
2
3
  import {
3
4
  clonePaneStateMap,
4
5
  cloneSavedLayout,
@@ -6,11 +7,12 @@ import {
6
7
  moveHistoryIndex,
7
8
  movedIndex,
8
9
  removeHistoryIndex,
10
+ resolveTickerForPane,
9
11
  setHistoryForIndex,
10
12
  syncConfigActiveLayoutState,
11
13
  withFocusedPane,
12
14
  } from "./layout";
13
- import type { AppAction, AppState } from "./types";
15
+ import type { AppAction, AppState, PaneRuntimeState } from "./types";
14
16
 
15
17
  const MAX_LAYOUT_HISTORY = 50;
16
18
 
@@ -293,7 +295,8 @@ export function reduceLayoutAction(state: AppState, action: AppAction): AppState
293
295
  state.paneState,
294
296
  state.focusedPaneId,
295
297
  );
296
- const source = currentConfig.layouts[action.index]!;
298
+ const { id: _id, ...source } = currentConfig.layouts[action.index]!;
299
+ // A copy is a layout of its own: sharing the id would send Move Back to either.
297
300
  const duplicate: SavedLayout = {
298
301
  ...cloneSavedLayout(source),
299
302
  name: `${source.name} Copy`,
@@ -313,6 +316,89 @@ export function reduceLayoutAction(state: AppState, action: AppAction): AppState
313
316
  });
314
317
  }
315
318
 
319
+ case "MOVE_PANE_TO_NEW_LAYOUT": {
320
+ const resolveSymbol = (instanceId: string) => resolveTickerForPane(state, instanceId);
321
+ const currentConfig = syncConfigActiveLayoutState(state.config, state.paneState, state.focusedPaneId);
322
+ const sourceIndex = currentConfig.activeLayoutIndex;
323
+ const sourceEntry = currentConfig.layouts[sourceIndex]!;
324
+ const sourceLayoutId = sourceEntry.id ?? action.sourceLayoutId;
325
+ const move = movePaneToOwnLayout(state.config.layout, action.paneId, sourceLayoutId, resolveSymbol);
326
+ if (!move) return state;
327
+
328
+ const { [action.paneId]: savedPaneState, ...sourcePaneState } = sourceEntry.paneState ?? {};
329
+ // The live object, so the pane's state keeps its identity across the move.
330
+ const livePaneState = (state.paneState[action.paneId] ?? savedPaneState) as PaneRuntimeState | undefined;
331
+ const movedPaneState: Record<string, PaneRuntimeState> = livePaneState ? { [action.paneId]: livePaneState } : {};
332
+ const previous = state.previousFocusedPaneId;
333
+ const sourceFocus = previous && getPlacedPaneInstanceIds(move.source).includes(previous) ? previous : null;
334
+ const layouts: SavedLayout[] = currentConfig.layouts.map((entry, index) => (
335
+ index === sourceIndex
336
+ ? { ...entry, id: sourceLayoutId, layout: move.source, paneState: sourcePaneState, focusedPaneId: sourceFocus }
337
+ : entry
338
+ ));
339
+ layouts.push({
340
+ name: availableLayoutName(action.name, layouts),
341
+ layout: move.target,
342
+ paneState: movedPaneState,
343
+ focusedPaneId: action.paneId,
344
+ });
345
+ return withFocusedPane({
346
+ ...state,
347
+ layoutHistory: setHistoryForIndex(state.layoutHistory, layouts.length - 1, { past: [], future: [] }),
348
+ }, {
349
+ ...currentConfig,
350
+ layout: cloneLayout(move.target),
351
+ layouts,
352
+ activeLayoutIndex: layouts.length - 1,
353
+ }, {
354
+ paneState: movedPaneState,
355
+ focusedPaneId: action.paneId,
356
+ });
357
+ }
358
+
359
+ case "MOVE_PANE_BACK": {
360
+ const target = paneMoveBackTarget(state.config, action.paneId);
361
+ if (!target) return state;
362
+ const resolveSymbol = (instanceId: string) => resolveTickerForPane(state, instanceId);
363
+ const currentConfig = syncConfigActiveLayoutState(state.config, state.paneState, state.focusedPaneId);
364
+ const activeIndex = currentConfig.activeLayoutIndex;
365
+ const activeEntry = currentConfig.layouts[activeIndex]!;
366
+ const targetEntry = currentConfig.layouts[target.index]!;
367
+ const move = movePaneBack(state.config.layout, targetEntry.layout, action.paneId, target.original, resolveSymbol);
368
+ if (!move) return state;
369
+
370
+ const { [action.paneId]: savedPaneState, ...activePaneState } = activeEntry.paneState ?? {};
371
+ const livePaneState = (state.paneState[action.paneId] ?? savedPaneState) as PaneRuntimeState | undefined;
372
+ const { [action.paneId]: _stale, ...targetPaneState } = targetEntry.paneState ?? {};
373
+ if (livePaneState) targetPaneState[action.paneId] = livePaneState;
374
+ let layouts: SavedLayout[] = currentConfig.layouts.map((entry, index) => {
375
+ if (index === target.index) return { ...entry, layout: move.target, paneState: targetPaneState, focusedPaneId: action.paneId };
376
+ if (index === activeIndex) return { ...entry, layout: move.source, paneState: activePaneState, focusedPaneId: null };
377
+ return entry;
378
+ });
379
+ // A layout the pane leaves empty was only there for it, unless a team layout is linked to it.
380
+ let layoutHistory = state.layoutHistory;
381
+ let nextIndex = target.index;
382
+ if (getPlacedPaneInstanceIds(move.source).length === 0 && !activeEntry.origin) {
383
+ layouts = layouts.filter((_, index) => index !== activeIndex);
384
+ layoutHistory = removeHistoryIndex(layoutHistory, activeIndex);
385
+ if (activeIndex < target.index) nextIndex -= 1;
386
+ }
387
+ const next = layouts[nextIndex]!;
388
+ return withFocusedPane({ ...state, layoutHistory }, {
389
+ ...currentConfig,
390
+ layout: cloneLayout(next.layout),
391
+ layouts,
392
+ activeLayoutIndex: nextIndex,
393
+ }, {
394
+ paneState: {
395
+ ...clonePaneStateMap(targetPaneState),
396
+ ...(livePaneState ? { [action.paneId]: livePaneState } : {}),
397
+ },
398
+ focusedPaneId: action.paneId,
399
+ });
400
+ }
401
+
316
402
  default:
317
403
  return undefined;
318
404
  }
@@ -73,6 +73,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
73
73
  case "SET_KEYBINDINGS":
74
74
  return { ...state, config: updateKeybindingsConfig(state.config, action.keybindings) };
75
75
 
76
+ case "SET_STAR_PROMPT":
77
+ return { ...state, config: { ...state.config, starPrompt: action.starPrompt } };
78
+
76
79
  case "SET_TICKERS":
77
80
  return { ...state, tickers: action.tickers };
78
81
 
@@ -214,11 +217,11 @@ export function appReducer(state: AppState, action: AppAction): AppState {
214
217
  case "SET_UPDATE_NOTICE":
215
218
  return { ...state, updateNotice: action.notice };
216
219
 
217
- case "TOGGLE_PLUGIN": {
218
- const disabledPlugins = state.config.disabledPlugins.includes(action.pluginId)
219
- ? state.config.disabledPlugins.filter((pluginId) => pluginId !== action.pluginId)
220
- : [...state.config.disabledPlugins, action.pluginId];
221
- return { ...state, config: { ...state.config, disabledPlugins } };
220
+ case "SET_DISABLED_PLUGINS": {
221
+ const config = { ...state.config, disabledPlugins: action.disabledPlugins };
222
+ return Object.prototype.hasOwnProperty.call(action, "focusedPaneId")
223
+ ? withFocusedPane(state, config, { focusedPaneId: action.focusedPaneId ?? null })
224
+ : { ...state, config };
222
225
  }
223
226
 
224
227
  case "SET_INPUT_CAPTURED":
@@ -1,5 +1,5 @@
1
1
  import type { BrokerAccount } from "../../../types/trading";
2
- import type { AppConfig, KeybindingsConfig, LayoutConfig, LayoutOrigin, OnboardingProgress } from "../../../types/config";
2
+ import type { AppConfig, KeybindingsConfig, LayoutConfig, LayoutOrigin, OnboardingProgress, StarPromptConfig } from "../../../types/config";
3
3
  import type { DesktopSharedStateSnapshot } from "../../../types/desktop-window";
4
4
  import type { Quote, TickerFinancials } from "../../../types/financials";
5
5
  import type { TickerRecord } from "../../../types/ticker";
@@ -99,6 +99,8 @@ export interface AppState {
99
99
  export type AppAction =
100
100
  | { type: "SET_CONFIG"; config: AppConfig }
101
101
  | { type: "SET_KEYBINDINGS"; keybindings: KeybindingsConfig | undefined }
102
+ /** Only the star prompt's own record, so a background write never touches layouts or a theme preview. */
103
+ | { type: "SET_STAR_PROMPT"; starPrompt: StarPromptConfig | undefined }
102
104
  | {
103
105
  type: "SET_ONBOARDING_STATE";
104
106
  complete: boolean;
@@ -137,7 +139,8 @@ export type AppAction =
137
139
  | { type: "SET_UPDATE_PROGRESS"; progress: UpdateProgress | null }
138
140
  | { type: "SET_UPDATE_CHECK_IN_PROGRESS"; checking: boolean }
139
141
  | { type: "SET_UPDATE_NOTICE"; notice: string | null }
140
- | { type: "TOGGLE_PLUGIN"; pluginId: string }
142
+ /** Plugins switched on or off; `focusedPaneId` moves focus off a pane that just went hidden. */
143
+ | { type: "SET_DISABLED_PLUGINS"; disabledPlugins: string[]; focusedPaneId?: string | null }
141
144
  | { type: "SET_INPUT_CAPTURED"; captured: boolean }
142
145
  /** @deprecated Only writes the unused `AppState.exchangeRates`. Use `useFxRatesMap`. */
143
146
  | { type: "SET_EXCHANGE_RATE"; currency: string; rate: number }
@@ -172,6 +175,14 @@ export type AppAction =
172
175
  | { type: "DELETE_LAYOUT"; index: number }
173
176
  | { type: "RENAME_LAYOUT"; index: number; name: string }
174
177
  | { type: "DUPLICATE_LAYOUT"; index: number }
178
+ /**
179
+ * Moves a pane out of the active layout into a new layout of its own, named
180
+ * `name` (made unique), which becomes active. `sourceLayoutId` names the
181
+ * layout it left when that one has no id yet, so Move Back can find it.
182
+ */
183
+ | { type: "MOVE_PANE_TO_NEW_LAYOUT"; paneId: string; name: string; sourceLayoutId: string }
184
+ /** Returns a pane Move to New Layout moved to the layout it came from, which becomes active. */
185
+ | { type: "MOVE_PANE_BACK"; paneId: string }
175
186
  | { type: "FOCUS_PANE"; paneId: string }
176
187
  | { type: "FOCUS_NEXT"; paneOrder: string[] }
177
188
  | { type: "FOCUS_PREV"; paneOrder: string[] }
@@ -1,9 +1,12 @@
1
1
  import type {
2
2
  DetachedPlacementMemory,
3
+ DockedPlacementMemory,
3
4
  FloatingPlacementMemory,
4
5
  LayoutConfig,
5
6
  PaneBinding,
6
7
  PaneInstanceConfig,
8
+ PaneMovedFrom,
9
+ PaneMovedLink,
7
10
  PanePlacementMemory,
8
11
  } from "../../types/config";
9
12
  import type { BrokerContractRef, TickerListingRef } from "../../types/instrument";
@@ -93,30 +96,59 @@ function sanitizeDetachedPlacementMemory(value: unknown): DetachedPlacementMemor
93
96
  return { x, y, width, height };
94
97
  }
95
98
 
99
+ function sanitizeDockedPlacementMemory(raw: unknown): DockedPlacementMemory | undefined {
100
+ if (!raw || typeof raw !== "object") return undefined;
101
+ const rawPath = (raw as { path?: unknown }).path;
102
+ const path = Array.isArray(rawPath)
103
+ ? rawPath.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
104
+ : undefined;
105
+ const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
106
+ ? (raw as { anchorInstanceId: string }).anchorInstanceId
107
+ : undefined;
108
+ const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
109
+ ? (raw as { position: "left" | "right" | "above" | "below" }).position
110
+ : undefined;
111
+ if (!path && !anchorInstanceId && !position) return undefined;
112
+ return {
113
+ path,
114
+ anchorInstanceId,
115
+ position,
116
+ };
117
+ }
118
+
119
+ function sanitizePaneMovedFrom(value: unknown): PaneMovedFrom | undefined {
120
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
121
+ const raw = value as Record<string, unknown>;
122
+ if (typeof raw.layoutId !== "string" || !raw.layoutId) return undefined;
123
+ const docked = sanitizeDockedPlacementMemory(raw.docked);
124
+ const rawRatio = (raw.docked as { ratio?: unknown } | undefined)?.ratio;
125
+ const ratio = typeof rawRatio === "number" && rawRatio > 0 && rawRatio < 1 ? rawRatio : undefined;
126
+ const floating = docked ? undefined : sanitizeFloatingPlacementMemory(raw.floating);
127
+ const links = Array.isArray(raw.links)
128
+ ? raw.links.flatMap((entry): PaneMovedLink[] => {
129
+ if (!entry || typeof entry !== "object") return [];
130
+ const link = entry as Record<string, unknown>;
131
+ if (typeof link.instanceId !== "string" || typeof link.sourceInstanceId !== "string" || typeof link.symbol !== "string") return [];
132
+ return [{
133
+ instanceId: link.instanceId,
134
+ sourceInstanceId: link.sourceInstanceId,
135
+ symbol: link.symbol,
136
+ ...(typeof link.title === "string" ? { title: link.title } : {}),
137
+ }];
138
+ })
139
+ : [];
140
+ return {
141
+ layoutId: raw.layoutId,
142
+ ...(docked ? { docked: ratio === undefined ? docked : { ...docked, ratio } } : {}),
143
+ ...(floating ? { floating } : {}),
144
+ ...(links.length > 0 ? { links } : {}),
145
+ };
146
+ }
147
+
96
148
  function sanitizePlacementMemory(value: unknown): PanePlacementMemory | undefined {
97
149
  if (!value || typeof value !== "object") return undefined;
98
150
 
99
- const docked = (() => {
100
- const raw = (value as PanePlacementMemory).docked;
101
- if (!raw || typeof raw !== "object") return undefined;
102
- const rawPath = (raw as { path?: unknown }).path;
103
- const path = Array.isArray(rawPath)
104
- ? rawPath.filter((segment): segment is 0 | 1 => segment === 0 || segment === 1)
105
- : undefined;
106
- const anchorInstanceId = typeof (raw as { anchorInstanceId?: unknown }).anchorInstanceId === "string"
107
- ? (raw as { anchorInstanceId: string }).anchorInstanceId
108
- : undefined;
109
- const position = ["left", "right", "above", "below"].includes(String((raw as { position?: unknown }).position))
110
- ? (raw as { position: "left" | "right" | "above" | "below" }).position
111
- : undefined;
112
- if (!path && !anchorInstanceId && !position) return undefined;
113
- return {
114
- path,
115
- anchorInstanceId,
116
- position,
117
- };
118
- })();
119
-
151
+ const docked = sanitizeDockedPlacementMemory((value as PanePlacementMemory).docked);
120
152
  const floating = sanitizeFloatingPlacementMemory((value as PanePlacementMemory).floating);
121
153
  const detached = sanitizeDetachedPlacementMemory((value as PanePlacementMemory).detached);
122
154
  if (!docked && !floating && !detached) return undefined;
@@ -176,6 +208,7 @@ function sanitizePaneInstances(
176
208
  seen.add(instanceId);
177
209
  const binding = sanitizePaneBinding(entry.binding);
178
210
  const settings = sanitizePaneSettings(entry.settings);
211
+ const movedFrom = sanitizePaneMovedFrom(entry.movedFrom);
179
212
  return {
180
213
  instanceId,
181
214
  paneId,
@@ -189,6 +222,7 @@ function sanitizePaneInstances(
189
222
  settings: migrateChartPaneSettings(originalPaneId, binding, settings, chartMigration),
190
223
  placementMemory: sanitizePlacementMemory(entry.placementMemory),
191
224
  locked: entry.locked === true ? true : undefined,
225
+ ...(movedFrom ? { movedFrom } : {}),
192
226
  };
193
227
  });
194
228
  return instances;
@@ -0,0 +1,149 @@
1
+ import {
2
+ removePaneInstances,
3
+ type DockLayoutNode,
4
+ type LayoutConfig,
5
+ type PaneInstanceConfig,
6
+ } from "../../../types/config";
7
+ import { isRecord } from "../../../utils/guards";
8
+
9
+ const SHORT_VOLUME_PANE_ID = "short-volume";
10
+ const SHORT_INTEREST_PANE_ID = "short-interest";
11
+ const SOVR_PANE_ID = "sovr-board";
12
+ const CDX_PANE_ID = "cdx-board";
13
+
14
+ interface FoldedLayout {
15
+ layout: LayoutConfig;
16
+ idMap: Map<string, string>;
17
+ removed: Set<string>;
18
+ }
19
+
20
+ function asLayout(value: unknown): LayoutConfig | null {
21
+ if (!isRecord(value) || !Array.isArray(value.instances)) return null;
22
+ return {
23
+ dockRoot: (value.dockRoot ?? null) as DockLayoutNode | null,
24
+ instances: value.instances as PaneInstanceConfig[],
25
+ floating: Array.isArray(value.floating) ? value.floating as LayoutConfig["floating"] : [],
26
+ detached: Array.isArray(value.detached) ? value.detached as LayoutConfig["detached"] : [],
27
+ };
28
+ }
29
+
30
+ function interestInstanceId(instanceId: string): string {
31
+ if (instanceId === SHORT_VOLUME_PANE_ID || instanceId.startsWith(`${SHORT_VOLUME_PANE_ID}:`)) {
32
+ return `${SHORT_INTEREST_PANE_ID}${instanceId.slice(SHORT_VOLUME_PANE_ID.length)}`;
33
+ }
34
+ return instanceId;
35
+ }
36
+
37
+ function mapNode(node: DockLayoutNode | null, idMap: Map<string, string>): DockLayoutNode | null {
38
+ if (!node) return null;
39
+ if (node.kind === "pane") return { ...node, instanceId: idMap.get(node.instanceId) ?? node.instanceId };
40
+ return { ...node, first: mapNode(node.first, idMap)!, second: mapNode(node.second, idMap)! };
41
+ }
42
+
43
+ /** Daily short volume becomes the short interest pane's volume tab, and sovereign CDS the index board's Sovereign tab. */
44
+ export function foldPaneLayout(layout: LayoutConfig): FoldedLayout {
45
+ const removed = new Set<string>();
46
+ const idMap = new Map<string, string>();
47
+ const claimed = new Set(layout.instances.map((instance) => instance.instanceId));
48
+ let hasIndexBoard = layout.instances.some((instance) => instance.paneId === CDX_PANE_ID);
49
+
50
+ const instances = layout.instances.flatMap((instance) => {
51
+ if (instance.paneId === SOVR_PANE_ID) {
52
+ if (hasIndexBoard) {
53
+ removed.add(instance.instanceId);
54
+ return [];
55
+ }
56
+ hasIndexBoard = true;
57
+ return [{
58
+ ...instance,
59
+ paneId: CDX_PANE_ID,
60
+ params: { ...(instance.params ?? {}), tab: "sovereign" },
61
+ }];
62
+ }
63
+ if (instance.paneId !== SHORT_VOLUME_PANE_ID) return [instance];
64
+ const targetId = interestInstanceId(instance.instanceId);
65
+ const taken = targetId !== instance.instanceId && claimed.has(targetId);
66
+ if (taken) {
67
+ removed.add(instance.instanceId);
68
+ return [];
69
+ }
70
+ if (targetId !== instance.instanceId) {
71
+ claimed.add(targetId);
72
+ idMap.set(instance.instanceId, targetId);
73
+ }
74
+ return [{
75
+ ...instance,
76
+ instanceId: targetId,
77
+ paneId: SHORT_INTEREST_PANE_ID,
78
+ params: { ...(instance.params ?? {}), tab: "volume" },
79
+ }];
80
+ });
81
+
82
+ const mapped: LayoutConfig = {
83
+ dockRoot: mapNode(layout.dockRoot, idMap),
84
+ instances: instances.map((instance) => {
85
+ const binding = instance.binding?.kind === "follow"
86
+ ? { ...instance.binding, sourceInstanceId: idMap.get(instance.binding.sourceInstanceId) ?? instance.binding.sourceInstanceId }
87
+ : instance.binding;
88
+ const anchor = instance.placementMemory?.docked?.anchorInstanceId;
89
+ const placementMemory = anchor
90
+ ? {
91
+ ...instance.placementMemory,
92
+ docked: { ...instance.placementMemory?.docked, anchorInstanceId: idMap.get(anchor) ?? anchor },
93
+ }
94
+ : instance.placementMemory;
95
+ return { ...instance, binding, placementMemory };
96
+ }),
97
+ floating: layout.floating.map((entry) => ({ ...entry, instanceId: idMap.get(entry.instanceId) ?? entry.instanceId })),
98
+ detached: (layout.detached ?? []).map((entry) => ({ ...entry, instanceId: idMap.get(entry.instanceId) ?? entry.instanceId })),
99
+ };
100
+ return { layout: removePaneInstances(mapped, removed), idMap, removed };
101
+ }
102
+
103
+ function foldPaneState(
104
+ value: unknown,
105
+ idMap: Map<string, string>,
106
+ removed: Set<string>,
107
+ ): unknown {
108
+ if (!isRecord(value)) return value;
109
+ return Object.fromEntries(
110
+ Object.entries(value)
111
+ .filter(([instanceId]) => !removed.has(instanceId))
112
+ .map(([instanceId, entry]) => [idMap.get(instanceId) ?? instanceId, entry]),
113
+ );
114
+ }
115
+
116
+ function foldFocus(value: unknown, idMap: Map<string, string>, removed: Set<string>): unknown {
117
+ if (typeof value !== "string") return value;
118
+ if (removed.has(value)) return null;
119
+ return idMap.get(value) ?? value;
120
+ }
121
+
122
+ function foldLayoutField(value: unknown): FoldedLayout | null {
123
+ const layout = asLayout(value);
124
+ if (!layout) return null;
125
+ return foldPaneLayout(layout);
126
+ }
127
+
128
+ /** Rewrite the open layout and every saved tab. */
129
+ export function foldSavedPanes(saved: Record<string, unknown>): Record<string, unknown> {
130
+ const open = foldLayoutField(saved.layout);
131
+ const layouts = Array.isArray(saved.layouts)
132
+ ? saved.layouts.map((entry) => {
133
+ if (!isRecord(entry)) return entry;
134
+ const folded = foldLayoutField(entry.layout);
135
+ if (!folded) return entry;
136
+ return {
137
+ ...entry,
138
+ layout: folded.layout,
139
+ paneState: foldPaneState(entry.paneState, folded.idMap, folded.removed),
140
+ focusedPaneId: foldFocus(entry.focusedPaneId, folded.idMap, folded.removed),
141
+ };
142
+ })
143
+ : saved.layouts;
144
+ return {
145
+ ...saved,
146
+ ...(open ? { layout: open.layout } : {}),
147
+ layouts,
148
+ };
149
+ }
@@ -17,6 +17,7 @@ import {
17
17
  type LegacyChartMigrationContext,
18
18
  } from "../chart-settings";
19
19
  import { sanitizeLayout } from "../layout";
20
+ import { foldSavedPanes } from "./fold-panes";
20
21
  import { sanitizeSavedPaneState } from "./pane-state";
21
22
  import { isRecord } from "../../../utils/guards";
22
23
 
@@ -26,6 +27,7 @@ const ONBOARDING_BACKFILL_CONFIG_VERSION = 21;
26
27
  const UNREACHABLE_PANE_CLEANUP_CONFIG_VERSION = 22;
27
28
  const MARKET_OVERVIEW_ABSORBED_CONFIG_VERSION = 23;
28
29
  const IPO_CALENDAR_ABSORBED_CONFIG_VERSION = 24;
30
+ const FOLDED_PANES_CONFIG_VERSION = 25;
29
31
 
30
32
  /** What a migration may ask of the machine the config is loaded on. */
31
33
  export interface ConfigMigrationHost {
@@ -80,6 +82,11 @@ const CONFIG_MIGRATIONS: readonly ConfigMigration[] = [
80
82
  toVersion: IPO_CALENDAR_ABSORBED_CONFIG_VERSION,
81
83
  migrate: keepAbsorbedPluginsOff("macro", ["ipo-calendar"]),
82
84
  },
85
+ {
86
+ name: "fold-short-volume-and-sovereign-cds",
87
+ toVersion: FOLDED_PANES_CONFIG_VERSION,
88
+ migrate: foldSavedPanes,
89
+ },
83
90
  ];
84
91
 
85
92
  export function migrateSavedConfig(
@@ -102,14 +102,16 @@ async function loadConfigState(dataDir: string): Promise<{ config: AppConfig; ne
102
102
  }
103
103
  }
104
104
 
105
- export async function saveConfig(config: AppConfig): Promise<void> {
106
- const configPath = configFileFor(config.dataDir);
107
- await mkdir(dirname(configPath), { recursive: true });
105
+ /** The save in flight or queued last for each config file; it never rejects. */
106
+ const saveQueues = new Map<string, Promise<void>>();
107
+ let tempFileCounter = 0;
108
108
 
109
- const persisted = normalizeConfigForSave(config);
110
- const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
109
+ async function writeConfigFile(configPath: string, contents: string): Promise<void> {
110
+ await mkdir(dirname(configPath), { recursive: true });
111
+ // Unique per call: a clock tick is not, and two saves can land in one.
112
+ const tempPath = `${configPath}.${process.pid}.${Date.now()}.${tempFileCounter++}.tmp`;
111
113
  try {
112
- await writeFile(tempPath, JSON.stringify(persisted, null, 2), "utf-8");
114
+ await writeFile(tempPath, contents, "utf-8");
113
115
  await rename(tempPath, configPath);
114
116
  } catch (error) {
115
117
  await rm(tempPath, { force: true }).catch(() => {});
@@ -117,6 +119,25 @@ export async function saveConfig(config: AppConfig): Promise<void> {
117
119
  }
118
120
  }
119
121
 
122
+ /**
123
+ * Saves to one file run one after another in call order, so the last call is
124
+ * the one on disk and none is lost. A failed save rejects only its own caller
125
+ * and the next one still runs.
126
+ */
127
+ export async function saveConfig(config: AppConfig): Promise<void> {
128
+ const configPath = configFileFor(config.dataDir);
129
+ // Written as it was when called, however the caller's object changes while
130
+ // earlier saves are still running.
131
+ const contents = JSON.stringify(normalizeConfigForSave(config), null, 2);
132
+ const result = (saveQueues.get(configPath) ?? Promise.resolve()).then(() => writeConfigFile(configPath, contents));
133
+ const settled = result.catch(() => {});
134
+ saveQueues.set(configPath, settled);
135
+ void settled.then(() => {
136
+ if (saveQueues.get(configPath) === settled) saveQueues.delete(configPath);
137
+ });
138
+ return result;
139
+ }
140
+
120
141
  export async function initDataDir(dataDir: string): Promise<AppConfig> {
121
142
  configLog.info(`Initializing data directory: ${dataDir}`);
122
143
  await mkdir(dataDir, { recursive: true });
@@ -21,8 +21,30 @@ import { isLanguagePreference } from "../../../i18n/languages";
21
21
  import { clampFontSize } from "../../../theme/font-scale";
22
22
  import { isLayoutConfig, sanitizeLayout } from "../layout";
23
23
  import { migrateSavedConfig, type ConfigMigrationHost } from "./migrations";
24
+ import { encodeBuiltinDisabledPluginIds, expandBuiltinPluginGroups } from "../../../plugins/ownership";
24
25
  import { sanitizeSavedPaneState } from "./pane-state";
25
26
  import { isRecord } from "../../../utils/guards";
27
+ import { debugLog } from "../../../utils/debug-log";
28
+ import { DEFAULT_THEME, themes } from "../../../theme/themes";
29
+ import { sanitizeStarPrompt } from "../../../app/star-prompt/model";
30
+
31
+ const configLog = debugLog.createLogger("config");
32
+ const reportedUnknownThemes = new Set<string>();
33
+
34
+ /**
35
+ * A theme id this build does not know is kept, not replaced: a newer build on
36
+ * another device may have saved it, and writing the default back would undo
37
+ * that choice everywhere through sync. The app draws the default meanwhile,
38
+ * and says so once.
39
+ */
40
+ function sanitizeTheme(value: unknown, fallback: string): string {
41
+ if (typeof value !== "string") return fallback;
42
+ if (!themes[value] && !reportedUnknownThemes.has(value)) {
43
+ reportedUnknownThemes.add(value);
44
+ configLog.warn(`Unknown theme "${value}" in config; showing ${DEFAULT_THEME}. gloomberb config themes lists the ids.`);
45
+ }
46
+ return value;
47
+ }
26
48
 
27
49
  export function normalizeLoadedConfig(
28
50
  saved: Record<string, unknown>,
@@ -42,7 +64,10 @@ export function normalizeLoadedConfig(
42
64
  : entry
43
65
  ));
44
66
 
45
- const disabledPlugins = sanitizeUniqueStringList(candidate.disabledPlugins ?? defaults.disabledPlugins);
67
+ // After the migrations, which read retired group ids as they were saved.
68
+ const disabledPlugins = expandBuiltinPluginGroups(
69
+ sanitizeUniqueStringList(candidate.disabledPlugins ?? defaults.disabledPlugins),
70
+ );
46
71
  const onboardingProgress = sanitizeOnboardingProgress(candidate.onboardingProgress);
47
72
  const onboardingComplete = onboardingProgress
48
73
  ? false
@@ -64,9 +89,10 @@ export function normalizeLoadedConfig(
64
89
  disabledPlugins,
65
90
  seededPlugins: sanitizeUniqueStringList(candidate.seededPlugins),
66
91
  ...(candidate.portfolioCurrenciesAdopted === true ? { portfolioCurrenciesAdopted: true } : {}),
92
+ ...(candidate.presentationMode === true ? { presentationMode: true } : {}),
67
93
  disabledSources: sanitizeUniqueStringList(candidate.disabledSources ?? defaults.disabledSources),
68
94
  pluginConfig: sanitizePluginConfig(candidate.pluginConfig),
69
- theme: typeof candidate.theme === "string" ? candidate.theme : defaults.theme,
95
+ theme: sanitizeTheme(candidate.theme, defaults.theme),
70
96
  chartPreferences: sanitizeChartPreferences(candidate.chartPreferences, defaults.chartPreferences),
71
97
  valueFlashingEnabled: typeof candidate.valueFlashingEnabled === "boolean" ? candidate.valueFlashingEnabled : defaults.valueFlashingEnabled,
72
98
  fontSize: sanitizeFontSize(candidate.fontSize, defaults.fontSize),
@@ -78,6 +104,7 @@ export function normalizeLoadedConfig(
78
104
  lastLaunchedVersion: typeof candidate.lastLaunchedVersion === "string" ? candidate.lastLaunchedVersion : undefined,
79
105
  ...withKeybindings(sanitizeKeybindings(candidate.keybindings)),
80
106
  ...withTelemetry(sanitizeTelemetry(candidate.telemetry)),
107
+ ...withStarPrompt(sanitizeStarPrompt(candidate.starPrompt)),
81
108
  };
82
109
 
83
110
  const needsSave =
@@ -122,7 +149,8 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
122
149
  layouts,
123
150
  activeLayoutIndex,
124
151
  brokerInstances: sanitizeBrokerInstances(config.brokerInstances),
125
- disabledPlugins: sanitizeUniqueStringList(config.disabledPlugins),
152
+ // Older apps read the retired group ids, so a group that is all off is saved as one.
153
+ disabledPlugins: encodeBuiltinDisabledPluginIds(sanitizeUniqueStringList(config.disabledPlugins)),
126
154
  seededPlugins: sanitizeUniqueStringList(config.seededPlugins),
127
155
  disabledSources: sanitizeUniqueStringList(config.disabledSources),
128
156
  pluginConfig: sanitizePluginConfig(config.pluginConfig),
@@ -138,6 +166,8 @@ export function normalizeConfigForSave(config: AppConfig): AppConfig {
138
166
  Object.assign(persisted, withKeybindings(sanitizeKeybindings(config.keybindings)));
139
167
  delete persisted.telemetry;
140
168
  Object.assign(persisted, withTelemetry(sanitizeTelemetry(config.telemetry)));
169
+ delete persisted.starPrompt;
170
+ Object.assign(persisted, withStarPrompt(sanitizeStarPrompt(config.starPrompt)));
141
171
 
142
172
  return persisted;
143
173
  }
@@ -150,6 +180,10 @@ function withTelemetry(telemetry: TelemetryConfig | undefined): Pick<AppConfig,
150
180
  return telemetry ? { telemetry } : {};
151
181
  }
152
182
 
183
+ function withStarPrompt(starPrompt: AppConfig["starPrompt"]): Pick<AppConfig, "starPrompt"> {
184
+ return starPrompt ? { starPrompt } : {};
185
+ }
186
+
153
187
  /** Only the switches that are set survive; an empty object is the same as none. */
154
188
  function sanitizeTelemetry(value: unknown): TelemetryConfig | undefined {
155
189
  if (!isRecord(value)) return undefined;