gloomberb 0.16.2 → 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 (527) 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/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -2,10 +2,22 @@ import type { DataProvider } from "../../../types/data-provider";
2
2
  import type { Quote } from "../../../types/financials";
3
3
  import type { IndexEntry } from "./indices";
4
4
  import { errorMessage } from "../../../utils/errors";
5
+ import { isNoProviderMessage } from "../../../sources/provider-errors";
6
+
7
+ /** What a row says in place of figures the feed does not carry. */
8
+ export const NOT_IN_FEED = "not available from the feed";
9
+
10
+ /** Whether the feed has no quote for a symbol, as opposed to a quote that failed to load. */
11
+ export function isFeedGap(error: string | null | undefined): boolean {
12
+ return !!error && isNoProviderMessage(error);
13
+ }
5
14
 
6
15
  export interface WorldIndexQuoteResult {
7
16
  quotes: Map<string, Quote | null>;
17
+ /** Failures to load: a timeout, an outage. A symbol the feed simply lacks is in `gaps`. */
8
18
  errors: string[];
19
+ /** Symbols the feed has no quote for. They are a data gap, so their rows say so rather than carry an error. */
20
+ gaps: Set<string>;
9
21
  }
10
22
 
11
23
  export async function loadWorldIndexQuotes(
@@ -14,6 +26,12 @@ export async function loadWorldIndexQuotes(
14
26
  ): Promise<WorldIndexQuoteResult> {
15
27
  const quotes = new Map<string, Quote | null>();
16
28
  const errors: string[] = [];
29
+ const gaps = new Set<string>();
30
+ const fail = (symbol: string, error: unknown) => {
31
+ const message = errorMessage(error);
32
+ if (isFeedGap(message)) gaps.add(symbol);
33
+ else errors.push(`${symbol}: ${message}`);
34
+ };
17
35
  if (provider.getQuotesBatch) {
18
36
  const results = await provider.getQuotesBatch(
19
37
  entries.map((entry) => ({ symbol: entry.symbol, exchange: "" })),
@@ -22,10 +40,10 @@ export async function loadWorldIndexQuotes(
22
40
  for (const entry of entries) {
23
41
  const result = bySymbol.get(entry.symbol);
24
42
  quotes.set(entry.symbol, result?.quote ?? null);
25
- if (result?.error) errors.push(`${entry.symbol}: ${errorMessage(result.error)}`);
43
+ if (result?.error) fail(entry.symbol, result.error);
26
44
  else if (!result?.quote) errors.push(`${entry.symbol}: quote unavailable`);
27
45
  }
28
- return { quotes, errors };
46
+ return { quotes, errors, gaps };
29
47
  }
30
48
 
31
49
  await Promise.all(entries.map(async (entry) => {
@@ -33,8 +51,8 @@ export async function loadWorldIndexQuotes(
33
51
  quotes.set(entry.symbol, await provider.getQuote(entry.symbol, ""));
34
52
  } catch (error) {
35
53
  quotes.set(entry.symbol, null);
36
- errors.push(`${entry.symbol}: ${errorMessage(error)}`);
54
+ fail(entry.symbol, error);
37
55
  }
38
56
  }));
39
- return { quotes, errors };
57
+ return { quotes, errors, gaps };
40
58
  }
@@ -5,9 +5,9 @@ import type {
5
5
  HeadlessPaneLoadArgs,
6
6
  } from "../../../types/plugin";
7
7
  import { formatNumber, formatPercentRaw } from "../../../utils/format";
8
- import { marketStateLabel } from "../../../market-data/market/status";
8
+ import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, marketStateLabel, type ExtendedSession } from "../../../market-data/market/status";
9
9
  import type { MarketState } from "../../../types/financials";
10
- import { loadWorldIndexQuotes, type WorldIndexQuoteResult } from "./client";
10
+ import { loadWorldIndexQuotes, NOT_IN_FEED, type WorldIndexQuoteResult } from "./client";
11
11
  import {
12
12
  getIndicesByRegion,
13
13
  REGION_LABELS,
@@ -17,14 +17,17 @@ import {
17
17
  } from "./indices";
18
18
  import { quoteFreshnessFields } from "../shared/report-freshness";
19
19
 
20
+ /** A row for an index the feed does not carry has no figures to print; its last column says why. */
21
+ const isGap = (row: Record<string, unknown>) => row.unavailable === true;
22
+
20
23
  const COLUMNS = [
21
- { key: "shortName", header: "Index" },
24
+ { key: "shortName", header: "Index", shrink: false },
22
25
  { key: "name", header: "Name" },
23
26
  {
24
27
  key: "price",
25
28
  header: "Last",
26
29
  align: "right" as const,
27
- format: (value: unknown) => value == null
30
+ format: (value: unknown, row: Record<string, unknown>) => isGap(row) ? "" : value == null
28
31
  ? "-"
29
32
  : formatNumber(Number(value), 2),
30
33
  },
@@ -32,58 +35,82 @@ const COLUMNS = [
32
35
  key: "change",
33
36
  header: "Change",
34
37
  align: "right" as const,
35
- format: (value: unknown) => value == null ? "-" : `${Number(value) >= 0 ? "+" : ""}${formatNumber(Number(value), 2)}`,
38
+ format: (value: unknown, row: Record<string, unknown>) => isGap(row) ? "" : value == null ? "-" : `${Number(value) >= 0 ? "+" : ""}${formatNumber(Number(value), 2)}`,
36
39
  },
37
40
  {
38
41
  key: "changePercent",
39
42
  header: "Change %",
40
43
  align: "right" as const,
41
- format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)),
44
+ format: (value: unknown, row: Record<string, unknown>) => isGap(row) ? "" : value == null ? "-" : formatPercentRaw(Number(value)),
42
45
  },
43
46
  {
44
47
  key: "marketState",
45
48
  header: "Session",
46
- format: (value: unknown) => typeof value === "string" && value ? marketStateLabel(value as MarketState) ?? value : "-",
49
+ shrink: false,
50
+ format: (value: unknown, row: Record<string, unknown>) => isGap(row) ? "" : typeof value === "string" && value ? marketStateLabel(value as MarketState) ?? value : "-",
47
51
  },
48
52
  {
49
53
  key: "lastUpdated",
50
54
  header: "Updated",
51
- format: (value: unknown) => {
55
+ shrink: false,
56
+ format: (value: unknown, row: Record<string, unknown>) => {
52
57
  const time = value == null ? Number.NaN : Number(value);
53
- return Number.isFinite(time) ? `${new Date(time).toISOString().slice(0, 16).replace("T", " ")} UTC` : "-";
58
+ return Number.isFinite(time) ? `${new Date(time).toISOString().slice(0, 16).replace("T", " ")} UTC` : isGap(row) ? NOT_IN_FEED : "-";
54
59
  },
55
60
  },
56
61
  ];
57
62
 
63
+ /** A pre-market or after-hours print and its move from the regular close, for a row that has one. */
64
+ function extendedColumn(session: ExtendedSession) {
65
+ return {
66
+ key: "extendedPrice",
67
+ header: EXTENDED_SESSION_LABELS[session],
68
+ align: "right" as const,
69
+ format: (value: unknown, row: Record<string, unknown>) => row.extendedSession === session && typeof value === "number"
70
+ ? `${formatNumber(value, 2)} ${formatPercentRaw(typeof row.extendedChangePercent === "number" ? row.extendedChangePercent : undefined)}`
71
+ : isGap(row) ? "" : "-",
72
+ };
73
+ }
74
+
75
+ /** The table's columns, with an extended column after Change % only where a row has such a print. */
76
+ function sectionColumns(rows: readonly Record<string, unknown>[]) {
77
+ const sessions = (["PRE", "POST"] as const).filter((session) => rows.some((row) => row.extendedSession === session));
78
+ return sessions.length === 0 ? COLUMNS : [...COLUMNS.slice(0, 5), ...sessions.map(extendedColumn), ...COLUMNS.slice(5)];
79
+ }
80
+
58
81
  function projectWorldIndicesHeadless(
59
82
  entries: readonly IndexEntry[],
60
83
  loaded: WorldIndexQuoteResult,
61
84
  ): HeadlessBundleResult {
62
85
  const grouped = getIndicesByRegion(entries);
86
+ const isGapEntry = (entry: IndexEntry) => loaded.gaps.has(entry.symbol) && !loaded.quotes.get(entry.symbol);
63
87
  return {
64
88
  sections: REGION_ORDER.flatMap((region) => {
65
89
  const regionEntries = grouped.get(region) ?? [];
66
90
  if (regionEntries.length === 0) return [];
67
- return [{
68
- title: REGION_LABELS[region],
69
- columns: COLUMNS,
70
- rows: regionEntries.map((entry) => {
71
- const quote = loaded.quotes.get(entry.symbol);
72
- return {
73
- ...quoteFreshnessFields(quote),
74
- ...entry,
75
- price: quote?.price ?? null,
76
- unit: "index points",
77
- currency: quote?.currency ?? null,
78
- change: quote?.change ?? null,
79
- changePercent: quote?.changePercent ?? null,
80
- marketState: quote?.marketState ?? null,
81
- lastUpdated: quote?.lastUpdated ?? null,
82
- };
83
- }),
84
- }];
91
+ const rows = regionEntries.map((entry) => {
92
+ const quote = loaded.quotes.get(entry.symbol);
93
+ // Last and Change are the regular session, as `ticker` reads them.
94
+ const { price, change, changePercent, ...extended } = getQuoteSessionFields(quote);
95
+ return {
96
+ ...quoteFreshnessFields(quote),
97
+ ...entry,
98
+ price,
99
+ unit: "index points",
100
+ currency: quote?.currency ?? null,
101
+ change,
102
+ changePercent,
103
+ ...extended,
104
+ marketState: quote?.marketState ?? null,
105
+ lastUpdated: quote?.lastUpdated ?? null,
106
+ ...(isGapEntry(entry) ? { unavailable: true } : {}),
107
+ };
108
+ });
109
+ return [{ title: REGION_LABELS[region], columns: sectionColumns(rows), rows }];
85
110
  }),
86
111
  errors: loaded.errors,
112
+ // A symbol the feed lacks is a gap in the data, not a failure: its row says so, and the report is not whole.
113
+ ...(entries.some(isGapEntry) ? { unavailableSymbols: entries.filter(isGapEntry).map((entry) => entry.symbol) } : {}),
87
114
  metadata: {
88
115
  requested: entries.length,
89
116
  available: [...loaded.quotes.values()].filter(Boolean).length,
@@ -1,6 +1,6 @@
1
1
  import type { MarketState } from "../../../types/financials";
2
2
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
3
- import type { BoardQuoteMap } from "../shared/use-quote-board";
3
+ import { boardHeadlineQuote, type BoardQuoteMap } from "../shared/use-quote-board";
4
4
  import { REGION_ORDER, type IndexEntry } from "./indices";
5
5
 
6
6
  export type WorldIndexTableRow =
@@ -40,7 +40,9 @@ function getSortValue(
40
40
  entry: IndexEntry,
41
41
  quotes: BoardQuoteMap,
42
42
  ): string | number | null {
43
- const quote = quotes.get(entry.symbol)?.quote;
43
+ const sent = quotes.get(entry.symbol)?.quote;
44
+ // Sorted by the figures the rows show.
45
+ const quote = sent ? boardHeadlineQuote(sent) : sent;
44
46
 
45
47
  switch (columnId) {
46
48
  case "status":
@@ -3,7 +3,8 @@ import { colors } from "../../../theme/colors";
3
3
  import type { Quote } from "../../../types/financials";
4
4
  import { TextAttributes } from "../../../ui";
5
5
  import { formatNumber } from "../../../utils/format";
6
- import { renderQuoteBoardCell, type BoardQuoteMap } from "../shared/use-quote-board";
6
+ import { renderQuoteBoardCell, type BoardQuoteMap, type BoardQuoteState } from "../shared/use-quote-board";
7
+ import { isFeedGap, NOT_IN_FEED } from "./client";
7
8
  import type {
8
9
  WorldIndexColumnId,
9
10
  WorldIndexTableRow,
@@ -66,6 +67,11 @@ const formatIndexPrice = (quote: Quote) => formatNumber(quote.price, 2);
66
67
  const formatIndexChange = (quote: Quote) =>
67
68
  `${quote.change >= 0 ? "+" : "-"}${formatNumber(Math.abs(quote.change), 2)}`;
68
69
 
70
+ /** An index the feed has no quote for: not loading, and no figures to show. */
71
+ function isUnavailableInFeed(state: BoardQuoteState | undefined): boolean {
72
+ return !!state && !state.quote && !state.loading && isFeedGap(state.error);
73
+ }
74
+
69
75
  export function renderWorldIndexCell(
70
76
  row: WorldIndexTableRow,
71
77
  column: WorldIndexColumn,
@@ -75,6 +81,8 @@ export function renderWorldIndexCell(
75
81
  if (row.type === "header") return { text: "" };
76
82
 
77
83
  const { entry } = row;
84
+ // The figures are blank and the name cell says why, ahead of the name so a narrow pane clips the name, not the reason.
85
+ const unavailable = isUnavailableInFeed(quotes.get(entry.symbol));
78
86
  switch (column.id) {
79
87
  case "symbol":
80
88
  return {
@@ -83,8 +91,11 @@ export function renderWorldIndexCell(
83
91
  attributes: TextAttributes.BOLD,
84
92
  };
85
93
  case "name":
86
- return { text: entry.name };
94
+ return unavailable
95
+ ? { text: `${NOT_IN_FEED} · ${entry.name}`, color: colors.textDim }
96
+ : { text: entry.name };
87
97
  default:
98
+ if (unavailable) return { text: "" };
88
99
  return renderQuoteBoardCell(column.id, quotes.get(entry.symbol), {
89
100
  sessionText: options?.sessionText,
90
101
  formatPrice: formatIndexPrice,
@@ -24,6 +24,8 @@ export interface BasemapPalette {
24
24
  land: string;
25
25
  coast: string;
26
26
  border: string;
27
+ /** Country names. */
28
+ label: string;
27
29
  }
28
30
 
29
31
  interface BasemapLayerProps {
@@ -138,9 +138,13 @@ export function tilesInView(tiles: readonly DetailTile[], view: GeoBox, margin =
138
138
  return tiles.filter(({ box }) => box[0] < east && box[2] > west && box[1] < north && box[3] > south);
139
139
  }
140
140
 
141
+ /** A country name at its label point, with the web-map zoom it first shows at and its rank. */
142
+ export type CountryLabel = readonly [name: string, longitude: number, latitude: number, minZoom: number, rank: number];
143
+
141
144
  /** What `natural-earth/index.chunk.ts` exports. */
142
145
  interface BasemapIndex {
143
146
  loadWorld: () => Promise<BasemapChunk>;
147
+ loadLabels: () => Promise<{ COUNTRY_LABELS: readonly CountryLabel[] }>;
144
148
  DETAIL_TILES: readonly DetailTile[];
145
149
  }
146
150
 
@@ -175,6 +179,20 @@ export function loadWorldBasemap(): Promise<BasemapPaths> {
175
179
  return worldPaths;
176
180
  }
177
181
 
182
+ let countryLabels: Promise<readonly CountryLabel[]> | null = null;
183
+
184
+ /** Country names, most prominent first; a lazy chunk like the land. */
185
+ export function loadCountryLabels(): Promise<readonly CountryLabel[]> {
186
+ countryLabels ??= loadIndex()
187
+ .then((index) => index.loadLabels())
188
+ .then((chunk) => chunk.COUNTRY_LABELS)
189
+ .catch((error: unknown) => {
190
+ countryLabels = null;
191
+ throw error;
192
+ });
193
+ return countryLabels;
194
+ }
195
+
178
196
  export function loadDetailIndex(): Promise<readonly DetailTile[]> {
179
197
  return loadIndex().then((index) => index.DETAIL_TILES);
180
198
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Which country names the desktop and web map write, and where. Names come
3
+ * from Natural Earth's label points, most prominent first, each with the
4
+ * web-map zoom it first shows at. A name shows once the map is that close (a
5
+ * little before), sits wholly inside the map, and keeps clear of the names
6
+ * already placed and of the selected or hovered marker. Same view, same names.
7
+ */
8
+ import type { CountryLabel } from "./basemap";
9
+
10
+ export interface PlacedCountryLabel {
11
+ name: string;
12
+ longitude: number;
13
+ latitude: number;
14
+ }
15
+
16
+ /** A marker the names keep clear of, in pixels from the map's top left. */
17
+ export interface LabelObstacle {
18
+ x: number;
19
+ y: number;
20
+ radius: number;
21
+ }
22
+
23
+ export const COUNTRY_LABEL_FONT_PX = 10;
24
+ /** The map's font is monospaced: every character advances this much of the font size. */
25
+ const CHARACTER_EM = 0.6;
26
+ /** Room kept around a name, so two never touch. */
27
+ const GAP_PX = 6;
28
+ /** Names show at Natural Earth's own zoom; this moves the threshold earlier (positive) or later. */
29
+ const ZOOM_LEAD = 0;
30
+ /** The largest countries are named on any world view, room permitting. */
31
+ const WORLD_VIEW_MIN_ZOOM = 1.7;
32
+ /** Never more names than this on one view. */
33
+ const MAX_LABELS = 60;
34
+
35
+ /** The web-map zoom (256 px tiles for 360 degrees) of a map drawn at this many pixels per degree. */
36
+ export function webMapZoom(pxPerDegree: number): number {
37
+ return Math.log2((Math.max(pxPerDegree, 1e-9) * 360) / 256);
38
+ }
39
+
40
+ export function layoutCountryLabels(
41
+ labels: readonly CountryLabel[],
42
+ options: {
43
+ pxPerDegree: number;
44
+ /** Where a longitude and latitude fall, in pixels from the map's top left. */
45
+ toPixels: (longitude: number, latitude: number) => { x: number; y: number };
46
+ widthPx: number;
47
+ heightPx: number;
48
+ obstacles?: readonly LabelObstacle[];
49
+ fontPx?: number;
50
+ },
51
+ ): PlacedCountryLabel[] {
52
+ const { toPixels, widthPx, heightPx, obstacles = [] } = options;
53
+ const fontPx = options.fontPx ?? COUNTRY_LABEL_FONT_PX;
54
+ if (!(widthPx > 0 && heightPx > 0 && options.pxPerDegree > 0)) return [];
55
+ const threshold = Math.max(webMapZoom(options.pxPerDegree) + ZOOM_LEAD, WORLD_VIEW_MIN_ZOOM);
56
+ const taken: [number, number, number, number][] = [];
57
+ const placed: PlacedCountryLabel[] = [];
58
+ for (const [name, longitude, latitude, minZoom] of labels) {
59
+ if (placed.length >= MAX_LABELS) break;
60
+ if (minZoom > threshold) continue;
61
+ const { x, y } = toPixels(longitude, latitude);
62
+ const halfWidth = (name.length * fontPx * CHARACTER_EM) / 2 + GAP_PX / 2;
63
+ const halfHeight = fontPx / 2 + GAP_PX / 4;
64
+ const box: [number, number, number, number] = [x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight];
65
+ if (box[0] < 0 || box[1] < 0 || box[2] > widthPx || box[3] > heightPx) continue;
66
+ if (taken.some((other) => box[0] < other[2] && box[2] > other[0] && box[1] < other[3] && box[3] > other[1])) continue;
67
+ if (obstacles.some((marker) => circleMeetsBox(marker, box))) continue;
68
+ taken.push(box);
69
+ placed.push({ name, longitude, latitude });
70
+ }
71
+ return placed;
72
+ }
73
+
74
+ function circleMeetsBox(circle: LabelObstacle, box: readonly [number, number, number, number]): boolean {
75
+ const nearestX = Math.max(box[0], Math.min(circle.x, box[2]));
76
+ const nearestY = Math.max(box[1], Math.min(circle.y, box[3]));
77
+ return Math.hypot(circle.x - nearestX, circle.y - nearestY) < circle.radius;
78
+ }
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../../api-client/geo";
14
14
  import { chartSeriesProvider, type ChartSeriesCapability, type ChartSeriesCatalogItem } from "../../../capabilities";
15
15
  import { subtractTimeRange } from "../../../time-series/date-window";
16
- import { SERIES_COLORS } from "../../../time-series/resolve";
16
+ import { SERIES_COLORS } from "../../../theme/series-colors";
17
17
  import type { ChartViewportSpec, ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
18
18
  import { GEO_SERIES_CAPABILITY_ID } from "../chart-composer/series-expression";
19
19
  import { cloudGeoRequest } from "./client";
@@ -357,7 +357,8 @@ export function GeoSvgLayers({ overlay, matrix, project, view, unitPx, mapWidthP
357
357
  const selectedLayer = overlay.selected ? overlay.layers.find((layer) => layer.id === overlay.selected!.layerId) ?? null : null;
358
358
  const selectedFeature = selectedLayer?.features.find((feature) => feature.id === overlay.selected!.id) ?? null;
359
359
  const pxPerDegree = matrix.a * unitPx;
360
- const iconScale = { [ICON_SCALE_VAR]: 1 / Math.max(pxPerDegree, Number.EPSILON) } as CSSProperties;
360
+ // A map with no scale draws nothing; never hand the icons a counter-scale of 1/epsilon.
361
+ const iconScale = { [ICON_SCALE_VAR]: pxPerDegree > 0 && Number.isFinite(pxPerDegree) ? 1 / pxPerDegree : 0 } as CSSProperties;
361
362
  const trailPath = overlay.trail && overlay.trail.length > 1 ? degreePath([overlay.trail], false) : null;
362
363
  const points = new Map(overlay.layers.filter((layer) => layer.geometry === "point").map((layer) => {
363
364
  const landmark = layer.features.length > 0 && MAP_SYMBOLS[featureSymbol(layer.id, layer.features[0]!).id].landmark === true;
@@ -1,5 +1,5 @@
1
1
  import { peekGeoCatalog } from "../../../api-client/geo";
2
- import { SERIES_COLORS } from "../../../time-series/resolve";
2
+ import { SERIES_COLORS } from "../../../theme/series-colors";
3
3
  import { CHART_COMPOSER_PANE_ID } from "../../../types/config";
4
4
  import type { PaneTemplateDef } from "../../../types/plugin";
5
5
  import { chartHeadless } from "../chart-composer/headless";
@@ -49,7 +49,7 @@ export const worldVenueMapModule: PluginModule = {
49
49
  panes: [
50
50
  {
51
51
  id: WORLD_VENUE_MAP_PANE_ID,
52
- name: "World Venue Map",
52
+ name: "World Map",
53
53
  icon: "M",
54
54
  component: WorldVenueMapPane,
55
55
  defaultPosition: "right",
@@ -63,9 +63,9 @@ export const worldVenueMapModule: PluginModule = {
63
63
  {
64
64
  id: "world-venue-map-pane",
65
65
  paneId: WORLD_VENUE_MAP_PANE_ID,
66
- label: "World Venue Map",
67
- description: "Trading venues with live status and local time; ships, ports, energy and airports as layers on the same map.",
68
- keywords: ["world", "map", "venue", "venues", "exchange", "mic", "market hours", "open markets", "ships", "vessels", "ports", "chokepoints", "pipelines", "airports", "energy", "layers"],
66
+ label: "World Map",
67
+ description: "A world map with trading venues and their hours; ships, ports, energy and airports as layers.",
68
+ keywords: ["world", "map", "globe", "countries", "venue", "venues", "exchange", "mic", "market hours", "open markets", "ships", "vessels", "ports", "chokepoints", "pipelines", "airports", "energy", "layers"],
69
69
  shortcut: {
70
70
  prefix: "MAP",
71
71
  argPlaceholder: "layers",
@@ -78,7 +78,7 @@ export const worldVenueMapModule: PluginModule = {
78
78
  const preset = parseMapPreset(options?.arg ?? options?.values?.layers);
79
79
  if (!preset) return { placement: "floating" };
80
80
  return {
81
- title: `Map · ${preset.layers.map(groupTitle).join(", ")}`,
81
+ title: preset.layers.length ? `Map · ${preset.layers.map(groupTitle).join(", ")}` : "Map · Venues",
82
82
  placement: "floating",
83
83
  settings: { layers: preset.layers, venues: preset.venues },
84
84
  };
@@ -49,10 +49,13 @@ import type { GeoLayerState } from "./feed";
49
49
  import { geoLayerColor, type GeoMapLayer, type GeoMapOverlay, type GeoMapSelection } from "./geo-draw";
50
50
  import { WORLD_VENUE_MAP_PANE_ID } from "./ids";
51
51
  import {
52
+ entityOpenZoom,
53
+ entityTableBbox,
52
54
  featureAnchor,
53
55
  formatGeoValue,
54
56
  formatTickerLinks,
55
57
  isChangeColumn,
58
+ LAYERS_SETTING_KEY,
56
59
  readVenuesSetting,
57
60
  sameGeoView,
58
61
  tickerLinkKey,
@@ -71,6 +74,8 @@ interface LayeredMapViewProps extends PaneProps {
71
74
  layers: GeoLayerInfo[];
72
75
  /** The layer ids or groups the pane was asked for. */
73
76
  tokens: readonly string[];
77
+ /** A plain MAP: venues show unless the pane turned them off. */
78
+ plain?: boolean;
74
79
  }
75
80
 
76
81
  interface OpenEntity {
@@ -80,15 +85,9 @@ interface OpenEntity {
80
85
 
81
86
  type SortState = { key: string | null; dir: "asc" | "desc" };
82
87
 
83
- /**
84
- * Zoom an opened entity is shown at: 16 times the world (the server's zoom 4,
85
- * where dense point layers stop clustering) for a ship or a port, wider for a
86
- * pipeline or a field.
87
- */
88
- function openZoom(layer: GeoLayerInfo): number {
89
- return layer.geometry === "point" ? 16 : 4;
90
- }
91
88
  const NO_SORT: SortState = { key: null, dir: "asc" };
89
+ /** How long the map rests before a table that follows it asks again. */
90
+ const TABLE_VIEW_SETTLE_MS = 600;
92
91
  /** Layers go closer than venues: 64 times the world is the server's zoom 6, where flights and single ports show. */
93
92
  const LAYERED_MAX_ZOOM = 64;
94
93
 
@@ -165,7 +164,7 @@ const SelectedEntityHeader = memo(function SelectedEntityHeader({
165
164
  );
166
165
  });
167
166
 
168
- export function LayeredMapView({ focused, width, height, layers, tokens }: LayeredMapViewProps) {
167
+ export function LayeredMapView({ focused, width, height, layers, tokens, plain = false }: LayeredMapViewProps) {
169
168
  const colors = useThemeColors();
170
169
  const dark = relativeLuminance(colors.text) > relativeLuminance(colors.bg);
171
170
  const { nativePaneChrome } = useUiCapabilities();
@@ -174,7 +173,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
174
173
  const { hasProAccess } = usePlanAccess();
175
174
  const upgrade = useCloudUpgradeAction("map-layers");
176
175
  const [venuesSetting] = usePaneSettingValue<boolean | undefined>(VENUES_SETTING_KEY, undefined);
177
- const venuesOn = readVenuesSetting({ [VENUES_SETTING_KEY]: venuesSetting }, layers.length);
176
+ const venuesOn = readVenuesSetting(plain ? { [VENUES_SETTING_KEY]: venuesSetting } : { [VENUES_SETTING_KEY]: venuesSetting, [LAYERS_SETTING_KEY]: tokens }, layers.length);
178
177
  const { data: venueData } = useAsyncResource(venuesOn ? loadWorldVenues : null);
179
178
  const [now, setNow] = useState(Date.now());
180
179
  useEffect(() => {
@@ -214,16 +213,28 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
214
213
  const [selectedMic, setSelectedMic] = useState<string | null>(null);
215
214
  const [headerSubject, setHeaderSubject] = useState<"entity" | "venue">("entity");
216
215
  const [focus, setFocus] = useState<WorldMapFocus | null>(null);
216
+ // Kept per pane, so docking, undocking, popping out or reloading reopens the map where it was.
217
+ const [savedViewport, setSavedViewport] = usePluginPaneState<unknown>("map:viewport", null);
217
218
  const { active: searchFocused, focus: focusSearch, searchProps } = useQueryBarSearch();
218
219
  const scrollRef = useRef<ScrollBoxRenderable>(null);
219
220
  const detailScrollRef = useRef<ScrollBoxRenderable>(null);
220
221
 
221
222
  const tableLocked = !canAccess(tableLayer) || tableState?.phase === "locked";
222
223
  const tableReadable = tableLayer.status !== "unavailable" && tableState?.phase !== "unavailable" && !tableLocked;
224
+ // The table follows the map once the view rests, not every frame of a pan.
225
+ const [tableView, setTableView] = useState<GeoView>(view);
226
+ useEffect(() => {
227
+ if (sameGeoView(tableView, view)) return;
228
+ const timer = setTimeout(() => setTableView(view), TABLE_VIEW_SETTLE_MS);
229
+ return () => clearTimeout(timer);
230
+ }, [tableView, view]);
231
+ const tableBbox = entityTableBbox(tableLayer, tableView);
232
+ const tableBboxKey = tableBbox?.join(",") ?? "";
223
233
  const loadPage = useCallback(async ({ offset, signal }: PageRequest) => {
224
234
  const page = await loadEntityPage(tableLayer.id, {
225
235
  q: query,
226
236
  ...(sort.key ? { sort: sort.key, dir: sort.dir } : {}),
237
+ ...(tableBbox ? { bbox: tableBbox } : {}),
227
238
  offset,
228
239
  }, signal);
229
240
  const rows = page.rows ?? [];
@@ -234,7 +245,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
234
245
  hasMore: rows.length >= ENTITY_PAGE_SIZE && (total === null || offset + rows.length < total),
235
246
  nextOffset: offset + rows.length,
236
247
  };
237
- }, [query, sort.dir, sort.key, tableLayer.id]);
248
+ }, [query, sort.dir, sort.key, tableBboxKey, tableLayer.id]);
238
249
  const pages = usePagedRows(tableReadable ? loadPage : null, { getId: (row) => row.id });
239
250
  const rows = pages.rows;
240
251
  useWorldMapEvidence(tokens, states, rows.length, pages.loading);
@@ -301,7 +312,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
301
312
  const lats = trail.map(([, lat]) => lat);
302
313
  const span = Math.max(Math.max(...lons) - Math.min(...lons), (Math.max(...lats) - Math.min(...lats)) * 2, 0.05);
303
314
  const last = trail.at(-1)!;
304
- setFocus({ key: trailKey, longitude: last[0], latitude: last[1], zoom: Math.min(LAYERED_MAX_ZOOM, Math.max(openZoom(tableLayer), 360 / (span * 3))) });
315
+ setFocus({ key: trailKey, longitude: last[0], latitude: last[1], zoom: Math.min(LAYERED_MAX_ZOOM, Math.max(entityOpenZoom(tableLayer), 360 / (span * 3))) });
305
316
  }, [trailKey]);
306
317
  const overlay = useMemo<GeoMapOverlay>(() => ({ layers: mapLayers, selected: selection, trail }), [mapLayers, selection, trail]);
307
318
 
@@ -472,7 +483,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
472
483
  setHeaderSubject("entity");
473
484
  selectRow(row.id);
474
485
  setOpen({ layer: tableLayer.id, id: row.id });
475
- setFocus({ key: `open:${tableLayer.id}:${row.id}`, longitude: row.lon, latitude: row.lat, zoom: openZoom(tableLayer) });
486
+ setFocus({ key: `open:${tableLayer.id}:${row.id}`, longitude: row.lon, latitude: row.lat, zoom: entityOpenZoom(tableLayer) });
476
487
  }}
477
488
  detailOpen={detailOpen}
478
489
  onBack={() => setOpen(null)}
@@ -497,7 +508,7 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
497
508
  );
498
509
 
499
510
  const map = (
500
- <Box flexDirection="column" width={mapWidth} height={mapSectionHeight} overflow="hidden">
511
+ <Box key="map" flexDirection="column" width={mapWidth} height={mapSectionHeight} overflow="hidden">
501
512
  {headerSubject === "venue" && selectedVenue && venueData ? (
502
513
  <SelectedVenueHeader venue={selectedVenue} checkedAt={venueData.checkedAt} now={now} width={mapWidth} />
503
514
  ) : (
@@ -517,6 +528,8 @@ export function LayeredMapView({ focused, width, height, layers, tokens }: Layer
517
528
  onSelectGeo={onSelectGeo}
518
529
  onViewChange={onViewChange}
519
530
  focus={focus}
531
+ savedViewport={savedViewport}
532
+ onViewportSettled={setSavedViewport}
520
533
  />
521
534
  </Box>
522
535
  );