gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,47 @@
1
+ import { useEffect } from "react";
2
+ import type { CloudSearchHit } from "../../../api-client";
3
+
4
+ type FocusListener = (hit: CloudSearchHit) => void;
5
+
6
+ const pendingByPaneId = new Map<string, CloudSearchHit>();
7
+ const listenersByPaneId = new Map<string, Set<FocusListener>>();
8
+
9
+ /**
10
+ * Hands a hit to the pane that is about to show it. Opening a document from the
11
+ * command bar creates the pane and names the document in one gesture, but the
12
+ * two cannot be one call: pane creation is async and the hit is far too big to
13
+ * push through pane settings, which are persisted and shared. So the request is
14
+ * parked here until the pane mounts, and delivered directly if it already has.
15
+ */
16
+ export function requestDocumentFocus(paneId: string, hit: CloudSearchHit): void {
17
+ const listeners = listenersByPaneId.get(paneId);
18
+ if (listeners && listeners.size > 0) {
19
+ for (const listener of listeners) listener(hit);
20
+ return;
21
+ }
22
+ pendingByPaneId.set(paneId, hit);
23
+ }
24
+
25
+ export function useDocumentFocusRequest(paneId: string, onFocus: FocusListener): void {
26
+ useEffect(() => {
27
+ const listeners = listenersByPaneId.get(paneId) ?? new Set<FocusListener>();
28
+ listeners.add(onFocus);
29
+ listenersByPaneId.set(paneId, listeners);
30
+
31
+ const pending = pendingByPaneId.get(paneId);
32
+ if (pending) {
33
+ pendingByPaneId.delete(paneId);
34
+ onFocus(pending);
35
+ }
36
+
37
+ return () => {
38
+ listeners.delete(onFocus);
39
+ if (listeners.size === 0) listenersByPaneId.delete(paneId);
40
+ };
41
+ }, [onFocus, paneId]);
42
+ }
43
+
44
+ export function resetDocumentFocusRequests(): void {
45
+ pendingByPaneId.clear();
46
+ listenersByPaneId.clear();
47
+ }
@@ -0,0 +1,85 @@
1
+ import type { GloomPlugin, PaneTemplateCreateOptions } from "../../../types/plugin";
2
+ import { createDocumentSearchProvider, createNewsSearchProvider } from "./command-bar-search";
3
+ import { attachResearchSearchPersistence, resetResearchSearchPersistence } from "./data";
4
+ import { resetDocumentFocusRequests } from "./focus-handoff";
5
+ import {
6
+ researchSearchInstanceId,
7
+ RESEARCH_SEARCH_PANE_ID,
8
+ RESEARCH_SEARCH_TEMPLATE_ID,
9
+ } from "./model";
10
+ import { ResearchSearchPane } from "./pane";
11
+
12
+ const description =
13
+ "Full-text search across earnings call transcripts, news wires, and SEC filings, with saved searches and keyword alerts.";
14
+
15
+ function queryFromOptions(options?: PaneTemplateCreateOptions): string {
16
+ return (options?.arg ?? options?.values?.query ?? "").trim();
17
+ }
18
+
19
+ export const researchSearchPlugin: GloomPlugin = {
20
+ id: "research-search",
21
+ name: "Research Search",
22
+ version: "1.0.0",
23
+ description,
24
+ toggleable: true,
25
+
26
+ setup(ctx) {
27
+ attachResearchSearchPersistence(ctx.persistence);
28
+ ctx.registerCommandBarSearchProvider(createNewsSearchProvider(ctx));
29
+ ctx.registerCommandBarSearchProvider(createDocumentSearchProvider(ctx));
30
+ },
31
+
32
+ dispose() {
33
+ resetResearchSearchPersistence();
34
+ resetDocumentFocusRequests();
35
+ },
36
+
37
+ panes: [
38
+ {
39
+ id: RESEARCH_SEARCH_PANE_ID,
40
+ name: "Research Search",
41
+ icon: "RS",
42
+ component: ResearchSearchPane,
43
+ defaultPosition: "right",
44
+ defaultMode: "floating",
45
+ defaultFloatingSize: { width: 118, height: 32 },
46
+ tableExport: true,
47
+ },
48
+ ],
49
+
50
+ paneTemplates: [
51
+ {
52
+ id: RESEARCH_SEARCH_TEMPLATE_ID,
53
+ paneId: RESEARCH_SEARCH_PANE_ID,
54
+ label: "Research Search",
55
+ description,
56
+ keywords: [
57
+ "search",
58
+ "documents",
59
+ "full text",
60
+ "transcripts",
61
+ "calls",
62
+ "news",
63
+ "filings",
64
+ "sec",
65
+ "alerts",
66
+ "saved search",
67
+ ],
68
+ shortcut: {
69
+ prefix: "SRCH",
70
+ argPlaceholder: "query",
71
+ argKind: "text",
72
+ argOptional: true,
73
+ },
74
+ createInstance(_context, options) {
75
+ const query = queryFromOptions(options);
76
+ return {
77
+ instanceId: researchSearchInstanceId(query),
78
+ title: query ? `Search ${query}` : "Research Search",
79
+ placement: "floating",
80
+ settings: { query },
81
+ };
82
+ },
83
+ },
84
+ ],
85
+ };
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CloudSearchHit } from "../../../api-client";
3
+ import { appendUniqueHits } from "./model";
4
+
5
+ function hit(overrides: Partial<CloudSearchHit>): CloudSearchHit {
6
+ return {
7
+ id: "hit",
8
+ docType: "filing",
9
+ sourceId: "0000320193-26-000001",
10
+ chunkIndex: 0,
11
+ ticker: "AAPL",
12
+ publishedAt: "2026-05-02T21:00:00.000Z",
13
+ title: "Apple Inc. 10-Q",
14
+ url: "https://example.com/filing",
15
+ snippet: "gross margin",
16
+ score: 1,
17
+ metadata: {},
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ describe("appendUniqueHits", () => {
23
+ test("drops a document that arrives again under a different chunk", () => {
24
+ const first = [hit({ id: "chunk-4", chunkIndex: 4 })];
25
+ const second = [
26
+ hit({ id: "chunk-9", chunkIndex: 9 }),
27
+ hit({ id: "other", sourceId: "0000320193-26-000002" }),
28
+ ];
29
+
30
+ expect(appendUniqueHits(first, second).map((entry) => entry.id))
31
+ .toEqual(["chunk-4", "other"]);
32
+ });
33
+
34
+ test("keeps documents of different types that share a source id", () => {
35
+ const first = [hit({ id: "news", docType: "news", sourceId: "shared" })];
36
+ const second = [hit({ id: "filing", docType: "filing", sourceId: "shared" })];
37
+
38
+ expect(appendUniqueHits(first, second)).toHaveLength(2);
39
+ });
40
+ });
@@ -0,0 +1,291 @@
1
+ import type {
2
+ CloudSavedSearch,
3
+ CloudSavedSearchFilters,
4
+ CloudSearchChunkMetadata,
5
+ CloudSearchDocType,
6
+ CloudSearchHit,
7
+ CloudSearchSort,
8
+ } from "../../../api-client";
9
+ import type { CloudSearchParams } from "../../../api-client/paths";
10
+ import type { DataTableColumn } from "../../../components";
11
+
12
+ export const RESEARCH_SEARCH_PANE_ID = "research-search";
13
+ export const RESEARCH_SEARCH_TEMPLATE_ID = "research-search-pane";
14
+ const RESEARCH_SEARCH_PAGE_SIZE = 40;
15
+
16
+ /** One pane per query, so two searches can sit side by side. */
17
+ export function researchSearchInstanceId(query: string): string {
18
+ const trimmed = query.trim();
19
+ if (!trimmed) return `${RESEARCH_SEARCH_PANE_ID}:main`;
20
+ return `${RESEARCH_SEARCH_PANE_ID}:${encodeURIComponent(trimmed).replace(/%/g, "~")}`;
21
+ }
22
+
23
+ export type SearchRangeKey = "all" | "7d" | "30d" | "1y" | "custom";
24
+
25
+ export interface SearchFilters {
26
+ tickers: string[];
27
+ docTypes: CloudSearchDocType[];
28
+ range: SearchRangeKey;
29
+ /** Absolute bounds; presets recompute `from` per request so the window stays relative. */
30
+ from?: string;
31
+ to?: string;
32
+ sort: CloudSearchSort;
33
+ }
34
+
35
+ export const DEFAULT_FILTERS: SearchFilters = {
36
+ tickers: [],
37
+ docTypes: [],
38
+ range: "all",
39
+ sort: "relevance",
40
+ };
41
+
42
+ /** An empty selection means every type, so there is no explicit "all" option. */
43
+ export const DOC_TYPE_OPTIONS: Array<{ value: CloudSearchDocType; label: string }> = [
44
+ { value: "transcript", label: "Calls" },
45
+ { value: "news", label: "News" },
46
+ { value: "filing", label: "Filings" },
47
+ ];
48
+
49
+ export const RANGE_OPTIONS: Array<{ value: SearchRangeKey; label: string }> = [
50
+ { value: "all", label: "Any time" },
51
+ { value: "7d", label: "7D" },
52
+ { value: "30d", label: "30D" },
53
+ { value: "1y", label: "1Y" },
54
+ ];
55
+
56
+ export const SORT_OPTIONS: Array<{ value: CloudSearchSort; label: string }> = [
57
+ { value: "relevance", label: "Relevance" },
58
+ { value: "newest", label: "Newest" },
59
+ { value: "oldest", label: "Oldest" },
60
+ ];
61
+
62
+ const RANGE_DAYS: Record<Exclude<SearchRangeKey, "all" | "custom">, number> = {
63
+ "7d": 7,
64
+ "30d": 30,
65
+ "1y": 365,
66
+ };
67
+
68
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
69
+
70
+ export function parseTickerFilter(value: string): string[] {
71
+ return [...new Set(
72
+ value
73
+ .split(/[\s,]+/)
74
+ .map((entry) => entry.trim().toUpperCase())
75
+ .filter((entry) => entry.length > 0),
76
+ )];
77
+ }
78
+
79
+ /** Resolves the preset window at request time; a custom range keeps its stored bounds. */
80
+ function resolveRangeBounds(
81
+ filters: SearchFilters,
82
+ now = Date.now(),
83
+ ): { from?: string; to?: string } {
84
+ if (filters.range === "custom") return { from: filters.from, to: filters.to };
85
+ if (filters.range === "all") return {};
86
+ const days = RANGE_DAYS[filters.range];
87
+ return { from: new Date(now - days * MS_PER_DAY).toISOString() };
88
+ }
89
+
90
+ export function buildSearchParams(
91
+ query: string,
92
+ filters: SearchFilters,
93
+ options: { offset?: number; limit?: number; now?: number } = {},
94
+ ): CloudSearchParams {
95
+ const { from, to } = resolveRangeBounds(filters, options.now);
96
+ return {
97
+ query,
98
+ ...(filters.tickers.length > 0 ? { tickers: filters.tickers } : {}),
99
+ ...(filters.docTypes.length > 0 ? { docTypes: filters.docTypes } : {}),
100
+ ...(from ? { from } : {}),
101
+ ...(to ? { to } : {}),
102
+ sort: filters.sort,
103
+ limit: options.limit ?? RESEARCH_SEARCH_PAGE_SIZE,
104
+ ...(options.offset ? { offset: options.offset } : {}),
105
+ // A long press release matches on a dozen chunks, and repeating it a dozen
106
+ // times buries every other document. One row per document, best chunk wins.
107
+ distinct: true,
108
+ };
109
+ }
110
+
111
+ export function filtersToSaved(filters: SearchFilters, now = Date.now()): CloudSavedSearchFilters {
112
+ const { from, to } = resolveRangeBounds(filters, now);
113
+ return {
114
+ ...(filters.tickers.length > 0 ? { tickers: filters.tickers } : {}),
115
+ ...(filters.docTypes.length > 0 ? { docTypes: filters.docTypes } : {}),
116
+ ...(from ? { from } : {}),
117
+ ...(to ? { to } : {}),
118
+ };
119
+ }
120
+
121
+ export function filtersFromSaved(saved: CloudSavedSearch): SearchFilters {
122
+ const filters = saved.filters ?? {};
123
+ return {
124
+ tickers: filters.tickers ?? [],
125
+ docTypes: filters.docTypes ?? [],
126
+ range: filters.from || filters.to ? "custom" : "all",
127
+ from: filters.from,
128
+ to: filters.to,
129
+ // Sort is a viewing preference, not part of what the server matches.
130
+ sort: "relevance",
131
+ };
132
+ }
133
+
134
+ export function describeFilters(filters: CloudSavedSearchFilters | undefined): string {
135
+ const parts = [
136
+ filters?.tickers?.length ? filters.tickers.join(" ") : null,
137
+ filters?.docTypes?.length ? filters.docTypes.map(docTypeLabel).join("/") : null,
138
+ filters?.from ? `since ${formatHitDate(filters.from)}` : null,
139
+ filters?.to ? `to ${formatHitDate(filters.to)}` : null,
140
+ ].filter((part): part is string => !!part);
141
+ return parts.join(" · ");
142
+ }
143
+
144
+ export function docTypeLabel(docType: CloudSearchDocType | string): string {
145
+ switch (docType) {
146
+ case "transcript":
147
+ return "Call";
148
+ case "news":
149
+ return "News";
150
+ case "filing":
151
+ return "Filing";
152
+ default:
153
+ return String(docType);
154
+ }
155
+ }
156
+
157
+ /** Compact table label: filings show their form, which is more useful than "Filing". */
158
+ export function hitTypeLabel(hit: CloudSearchHit): string {
159
+ if (hit.docType === "filing") return hit.metadata?.form?.trim() || "FILING";
160
+ return hit.docType === "transcript" ? "CALL" : "NEWS";
161
+ }
162
+
163
+ export function formatHitDate(value: string | null | undefined): string {
164
+ if (!value) return "\u2014";
165
+ const date = new Date(value);
166
+ if (Number.isNaN(date.getTime())) return "\u2014";
167
+ return date.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "2-digit" });
168
+ }
169
+
170
+ const MS_PER_HOUR = 60 * 60 * 1000;
171
+
172
+ /**
173
+ * Date for a column too narrow for a full one: the age while it is fresh
174
+ * ("3h"), the day for the rest of this year ("Aug 30"), the month and year
175
+ * beyond that ("Aug 2025"). Empty rather than a dash when unknown, so a row
176
+ * without a date shows nothing rather than a placeholder.
177
+ */
178
+ export function formatHitDateShort(value: string | null | undefined, now = Date.now()): string {
179
+ if (!value) return "";
180
+ const date = new Date(value);
181
+ const time = date.getTime();
182
+ if (Number.isNaN(time)) return "";
183
+ const age = now - time;
184
+ if (age >= 0 && age < MS_PER_HOUR) return `${Math.max(1, Math.floor(age / 60_000))}m`;
185
+ if (age >= 0 && age < 24 * MS_PER_HOUR) return `${Math.floor(age / MS_PER_HOUR)}h`;
186
+ if (date.getFullYear() === new Date(now).getFullYear()) {
187
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
188
+ }
189
+ return date.toLocaleDateString("en-US", { month: "short", year: "numeric" });
190
+ }
191
+
192
+ /**
193
+ * Who or what a hit's text came from, shorter than `chunkAttribution` because
194
+ * the command bar sets it before the ticker on one line: the wire for news,
195
+ * "speaker, role" for a call, the section for a filing (its form is the badge).
196
+ */
197
+ export function hitLeadIn(hit: Pick<CloudSearchHit, "docType" | "metadata">): string {
198
+ const metadata = hit.metadata;
199
+ if (!metadata) return "";
200
+ if (hit.docType === "transcript") {
201
+ return [metadata.speaker, metadata.role]
202
+ .map((part) => part?.trim())
203
+ .filter((part): part is string => !!part)
204
+ .join(", ");
205
+ }
206
+ if (hit.docType === "filing") return metadata.section?.trim() || "";
207
+ return metadata.source?.trim() || "";
208
+ }
209
+
210
+ /** Speaker, wire source, or filing section — whatever identifies a chunk's origin. */
211
+ export function chunkAttribution(
212
+ docType: CloudSearchDocType,
213
+ metadata: CloudSearchChunkMetadata | undefined,
214
+ ): string {
215
+ if (!metadata) return "";
216
+ if (docType === "transcript") {
217
+ const speaker = [metadata.speaker, metadata.role, metadata.company]
218
+ .filter((part) => !!part?.trim())
219
+ .join(" · ");
220
+ return metadata.isQa && speaker ? `${speaker} · Q&A` : speaker;
221
+ }
222
+ if (docType === "filing") return metadata.section?.trim() || "";
223
+ return metadata.source?.trim() || "";
224
+ }
225
+
226
+ /**
227
+ * Columns a wrapped paragraph gets in the document view. The scroll box keeps
228
+ * one column of text inset on the left, and its vertical scrollbar sits in the
229
+ * last column of the pane, so those are the only two the body cannot use.
230
+ */
231
+ export function documentBodyWidth(width: number): number {
232
+ return Math.max(20, Math.floor(width) - 2);
233
+ }
234
+
235
+ type SearchColumnId = "ticker" | "type" | "date" | "title" | "match";
236
+
237
+ export interface SearchColumn extends DataTableColumn {
238
+ id: SearchColumnId;
239
+ }
240
+
241
+ export function buildResultColumns(width: number): SearchColumn[] {
242
+ // Wide enough for a five-letter symbol plus the badge's own padding.
243
+ const tickerWidth = 8;
244
+ const typeWidth = 7;
245
+ const dateWidth = 10;
246
+ const fixed = tickerWidth + typeWidth + dateWidth + 8;
247
+ const remaining = Math.max(24, width - fixed);
248
+ // The snippet is what makes a hit judgeable, so it takes the larger share.
249
+ const titleWidth = Math.max(14, Math.min(38, Math.round(remaining * 0.4)));
250
+ return [
251
+ { id: "ticker", label: "TICKER", width: tickerWidth, align: "left" },
252
+ { id: "type", label: "TYPE", width: typeWidth, align: "left" },
253
+ { id: "date", label: "DATE", width: dateWidth, align: "left" },
254
+ { id: "title", label: "TITLE", width: titleWidth, align: "left" },
255
+ { id: "match", label: "MATCH", width: Math.max(10, remaining - titleWidth), align: "left" },
256
+ ];
257
+ }
258
+
259
+ /**
260
+ * Identity of the thing a row stands for. Distinct search returns one row per
261
+ * source document but picks whichever chunk scored best, so the same document
262
+ * can arrive on two pages under two different chunk ids.
263
+ */
264
+ function hitDocumentKey(hit: CloudSearchHit): string {
265
+ return `${hit.docType}:${hit.sourceId}`;
266
+ }
267
+
268
+ /** Paged responses can overlap when new documents land between requests. */
269
+ export function appendUniqueHits(current: CloudSearchHit[], next: CloudSearchHit[]): CloudSearchHit[] {
270
+ if (next.length === 0) return current;
271
+ const seen = new Set(current.map(hitDocumentKey));
272
+ const merged = [...current];
273
+ for (const hit of next) {
274
+ const key = hitDocumentKey(hit);
275
+ if (seen.has(key)) continue;
276
+ seen.add(key);
277
+ merged.push(hit);
278
+ }
279
+ return merged;
280
+ }
281
+
282
+ /** Chunks behind a collapsed row, so hiding the rest stays visible. */
283
+ export function hitMatchCountLabel(hit: CloudSearchHit): string {
284
+ const count = hit.matchCount ?? 0;
285
+ return count > 1 ? `${count}\u00d7 ` : "";
286
+ }
287
+
288
+ export function savedSearchName(query: string, filters: SearchFilters): string {
289
+ const scope = filters.tickers.length > 0 ? ` (${filters.tickers.join(" ")})` : "";
290
+ return `${query.trim()}${scope}`.slice(0, 120);
291
+ }