gloomberb 0.9.2 → 0.9.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 (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -24,6 +24,7 @@ import {
24
24
  attachPredictionMarketsPersistence,
25
25
  } from "./services/fetch";
26
26
  import { normalizeKalshiMarket } from "./services/kalshi/adapter";
27
+ import type { PredictionDetailTab } from "./types";
27
28
 
28
29
  export const TEST_PANE_ID = "prediction-markets:main";
29
30
  const originalFetch = globalThis.fetch;
@@ -408,7 +409,13 @@ export function ChartHarness({
408
409
  );
409
410
  }
410
411
 
411
- export function GroupedDetailHarness() {
412
+ export function GroupedDetailHarness({
413
+ detailTab = "overview",
414
+ loading = false,
415
+ }: {
416
+ detailTab?: PredictionDetailTab;
417
+ loading?: boolean;
418
+ } = {}) {
412
419
  const [state, dispatch] = useReducer(
413
420
  appReducer,
414
421
  (() => {
@@ -475,29 +482,40 @@ export function GroupedDetailHarness() {
475
482
  <AppContext value={{ state, dispatch }}>
476
483
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
477
484
  <PredictionMarketDetailPane
478
- detail={{
479
- summary,
480
- siblings: selectedRow!.markets.map((market) => ({
481
- key: market.key,
482
- marketId: market.marketId,
483
- label: market.marketLabel,
484
- yesPrice: market.yesPrice,
485
- volume24h: market.volume24h,
486
- })),
487
- rules: [],
488
- history: [],
489
- book: {
490
- yesBids: [],
491
- yesAsks: [],
492
- noBids: [],
493
- noAsks: [],
494
- lastTradePrice: null,
495
- },
496
- trades: [],
497
- }}
485
+ detail={
486
+ loading
487
+ ? null
488
+ : {
489
+ summary,
490
+ siblings: selectedRow!.markets.map((market) => ({
491
+ key: market.key,
492
+ marketId: market.marketId,
493
+ label: market.marketLabel,
494
+ yesPrice: market.yesPrice,
495
+ volume24h: market.volume24h,
496
+ })),
497
+ rules: [],
498
+ history: [],
499
+ book: {
500
+ yesBids: [{ price: 0.47, size: 120 }],
501
+ yesAsks: [{ price: 0.49, size: 95 }],
502
+ noBids: [{ price: 0.51, size: 80 }],
503
+ noAsks: [{ price: 0.53, size: 105 }],
504
+ lastTradePrice: null,
505
+ },
506
+ trades: [{
507
+ id: "trade-1",
508
+ timestamp: Date.parse("2026-04-01T12:34:56Z"),
509
+ side: "buy",
510
+ outcome: "yes",
511
+ price: 0.48,
512
+ size: 50,
513
+ }],
514
+ }
515
+ }
498
516
  detailError={null}
499
- detailLoadCount={0}
500
- detailTab="overview"
517
+ detailLoadCount={loading ? 1 : 0}
518
+ detailTab={detailTab}
501
519
  detailWidth={58}
502
520
  focused
503
521
  height={24}
@@ -531,6 +549,7 @@ export async function emitKeypress(
531
549
  event: {
532
550
  name?: string;
533
551
  sequence?: string;
552
+ targetEditable?: boolean;
534
553
  shift?: boolean;
535
554
  },
536
555
  ) {
@@ -30,6 +30,15 @@ describe("applicationMenuCommand", () => {
30
30
  })).toEqual({ type: "open-devtools" });
31
31
  });
32
32
 
33
+ test("parses the native quit command", () => {
34
+ expect(applicationMenuCommand({
35
+ data: {
36
+ action: ELECTROBUN_APPLICATION_MENU_ACTION,
37
+ data: { type: "quit" },
38
+ },
39
+ })).toEqual({ type: "quit" });
40
+ });
41
+
33
42
  test("ignores unknown command types", () => {
34
43
  expect(applicationMenuCommand({
35
44
  data: {
@@ -45,6 +45,7 @@ function normalizeApplicationMenuCommand(value: unknown): ElectrobunApplicationM
45
45
  case "layout-redo":
46
46
  case "layout-gridlock":
47
47
  case "open-devtools":
48
+ case "quit":
48
49
  return { type: command.type };
49
50
  default:
50
51
  return null;
@@ -9,4 +9,13 @@ describe("desktop application menu", () => {
9
9
  test("keeps the native application menu on macOS", () => {
10
10
  expect(buildDesktopApplicationMenu("darwin")).toEqual(buildApplicationMenu());
11
11
  });
12
+
13
+ test("routes macOS quit through an explicit command item", () => {
14
+ const appMenu = buildApplicationMenu()[0]?.submenu;
15
+ expect(appMenu?.at(-1)).toMatchObject({
16
+ label: "Quit Gloomberb",
17
+ accelerator: "CmdOrCtrl+Q",
18
+ data: { type: "quit" },
19
+ });
20
+ });
12
21
  });
@@ -6,7 +6,8 @@ const GITHUB_ISSUE_URL = "https://github.com/vincelwt/gloomberb/issues/new/choos
6
6
 
7
7
  export type ElectrobunApplicationMenuCommand =
8
8
  | DesktopApplicationMenuCommand
9
- | { type: "open-devtools" };
9
+ | { type: "open-devtools" }
10
+ | { type: "quit" };
10
11
 
11
12
  function commandItem(
12
13
  label: string,
@@ -38,7 +39,7 @@ export function buildApplicationMenu(): ApplicationMenuItemConfig[] {
38
39
  { role: "hideOthers" },
39
40
  { role: "showAll" },
40
41
  { type: "divider" },
41
- { role: "quit" },
42
+ commandItem("Quit Gloomberb", { type: "quit" }, { accelerator: "CmdOrCtrl+Q" }),
42
43
  ],
43
44
  },
44
45
  {
@@ -281,6 +281,15 @@ function closeAllDetachedWindows(): void {
281
281
  detachedWindowManager.closeAll();
282
282
  }
283
283
 
284
+ function quitDesktopApp(): void {
285
+ closeAllDetachedWindows();
286
+ teardownServices();
287
+ const window = mainWindow;
288
+ mainWindow = null;
289
+ window?.close();
290
+ Utils.quit();
291
+ }
292
+
284
293
  function controlWindowForRpcKey(windowKey: string | undefined, action: DesktopWindowControlAction): boolean {
285
294
  const targetWindow = windowKey === MAIN_WINDOW_RPC_KEY
286
295
  ? mainWindow
@@ -446,6 +455,10 @@ ApplicationMenu.on("application-menu-clicked", (event: unknown) => {
446
455
  openMainWindowDevTools();
447
456
  return;
448
457
  }
458
+ if (command.type === "quit") {
459
+ quitDesktopApp();
460
+ return;
461
+ }
449
462
  if (!isWindowRpcReady(MAIN_WINDOW_RPC_KEY)) return;
450
463
  getWindowRpc(MAIN_WINDOW_RPC_KEY)?.send["application-menu.select"]({ command });
451
464
  });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { detachedRpcKey, focusWindowForRpcKey, MAIN_WINDOW_RPC_KEY, type FocusableElectrobunWindow } from "./focus";
3
+
4
+ describe("Electrobun window focus routing", () => {
5
+ test("uses activate for the main window", () => {
6
+ const calls: string[] = [];
7
+ const mainWindow: FocusableElectrobunWindow = {
8
+ activate: () => calls.push("activate"),
9
+ focus: () => calls.push("focus"),
10
+ };
11
+
12
+ expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
13
+ expect(calls).toEqual(["activate"]);
14
+ });
15
+
16
+ test("uses activate for detached windows", () => {
17
+ const calls: string[] = [];
18
+ const detachedWindows = new Map<string, FocusableElectrobunWindow>([
19
+ ["pane:1", { activate: () => calls.push("detached") }],
20
+ ]);
21
+
22
+ expect(focusWindowForRpcKey(detachedRpcKey("pane:1"), null, detachedWindows)).toBe(true);
23
+ expect(calls).toEqual(["detached"]);
24
+ });
25
+
26
+ test("falls back to focus for legacy window objects", () => {
27
+ const calls: string[] = [];
28
+ const mainWindow: FocusableElectrobunWindow = {
29
+ focus: () => calls.push("focus"),
30
+ };
31
+
32
+ expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
33
+ expect(calls).toEqual(["focus"]);
34
+ });
35
+ });
@@ -3,6 +3,7 @@ export const MAIN_WINDOW_RPC_KEY = "main";
3
3
  const DETACHED_WINDOW_RPC_PREFIX = "detached:";
4
4
 
5
5
  export interface FocusableElectrobunWindow {
6
+ activate?: () => void;
6
7
  focus?: () => void;
7
8
  }
8
9
 
@@ -31,6 +32,10 @@ export function focusWindowForRpcKey(
31
32
  detachedWindows: ReadonlyMap<string, FocusableElectrobunWindow>,
32
33
  ): boolean {
33
34
  const window = resolveWindowForRpcKey(rpcKey, mainWindow, detachedWindows);
35
+ if (window?.activate) {
36
+ window.activate();
37
+ return true;
38
+ }
34
39
  if (!window?.focus) return false;
35
40
  window.focus();
36
41
  return true;
@@ -7,7 +7,7 @@ import type { AppServices } from "../../../core/app-services";
7
7
  import type { AppConfig } from "../../../types/config";
8
8
  import { newsProvider } from "../../../capabilities";
9
9
  import { debugLog } from "../../../utils/debug-log";
10
- import { measurePerf } from "../../../utils/perf-marks";
10
+ import { measurePerf, measurePerfAsync } from "../../../utils/perf-marks";
11
11
  import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
12
12
  import { createRemoteAssetDataClient } from "./remote/asset-data-client";
13
13
  import { RemotePersistence } from "./remote/persistence";
@@ -47,10 +47,11 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
47
47
  }));
48
48
 
49
49
  const plugins = getRendererBuiltinPlugins();
50
+ const pluginReadyPromises: Promise<void>[] = [];
50
51
  for (const plugin of plugins) {
51
- measurePerf("startup.services.register-plugin", () => {
52
- void pluginRegistry.register(plugin);
53
- }, { pluginId: plugin.id });
52
+ pluginReadyPromises.push(measurePerfAsync("startup.services.register-plugin", () => (
53
+ pluginRegistry.register(plugin)
54
+ ), { pluginId: plugin.id }));
54
55
  }
55
56
  measurePerf("startup.services.news-start", () => {
56
57
  newsService.start();
@@ -65,6 +66,7 @@ export function createAppServices({ config }: { config: AppConfig }): AppService
65
66
  marketData,
66
67
  pluginRegistry,
67
68
  newsService,
69
+ ready: Promise.all(pluginReadyPromises).then(() => {}),
68
70
  destroy() {
69
71
  setSharedMarketDataCoordinator(null);
70
72
  setSharedNewsService(null);
@@ -20,7 +20,7 @@ import { resolvePaneBodyFrame } from "../../../components/layout/pane/sizing";
20
20
  import { getPaneDisplayTitle } from "../../../components/layout/pane/title";
21
21
  import type { AppConfig } from "../../../types/config";
22
22
  import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
23
- import type { TickerFinancials } from "../../../types/financials";
23
+ import type { OptionsChain, TickerFinancials } from "../../../types/financials";
24
24
  import type { TickerRecord } from "../../../types/ticker";
25
25
  import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
26
26
  import type { PaneDef } from "../../../types/plugin";
@@ -32,6 +32,7 @@ interface CliPaneShotPayload {
32
32
  heightCells: number;
33
33
  tickers: TickerRecord[];
34
34
  financials: Array<[string, TickerFinancials]>;
35
+ optionsChains: Array<[string, OptionsChain]>;
35
36
  paneState: Record<string, PaneRuntimeState>;
36
37
  }
37
38
 
@@ -56,6 +57,7 @@ const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
56
57
 
57
58
  let pendingShotWork = 0;
58
59
  let didInstallShotFetchTracker = false;
60
+ let shotDataProvider: DataProvider | null = null;
59
61
 
60
62
  const rendererHost: RendererHost = {
61
63
  requestExit() {},
@@ -152,6 +154,7 @@ function waitForShotReadiness(): () => void {
152
154
 
153
155
  function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
154
156
  const financials = new Map(payload.financials.map(([symbol, data]) => [normalizeSymbol(symbol), data]));
157
+ const optionsChains = new Map((payload.optionsChains ?? []).map(([symbol, data]) => [normalizeSymbol(symbol), data]));
155
158
 
156
159
  const getFinancials = (symbol: string) => {
157
160
  const data = financials.get(normalizeSymbol(symbol));
@@ -187,6 +190,13 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
187
190
  getExchangeRate() {
188
191
  return resolveShotWork(1);
189
192
  },
193
+ getOptionsChain(ticker) {
194
+ return trackShotWork(Promise.resolve().then(() => {
195
+ const chain = optionsChains.get(normalizeSymbol(ticker));
196
+ if (!chain) throw new Error(`No screenshot options data available for ${ticker}.`);
197
+ return chain;
198
+ }));
199
+ },
190
200
  search(query) {
191
201
  const normalized = normalizeSymbol(query);
192
202
  return resolveShotWork(payload.tickers
@@ -217,6 +227,7 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
217
227
 
218
228
  function installShotMarketData(payload: CliPaneShotPayload): void {
219
229
  const provider = createShotDataProvider(payload);
230
+ shotDataProvider = provider;
220
231
  const coordinator = new MarketDataCoordinator(provider);
221
232
  coordinator.primeCachedFinancials(payload.tickers.flatMap((ticker) => {
222
233
  const financials = payload.financials.find(([symbol]) => normalizeSymbol(symbol) === normalizeSymbol(ticker.metadata.ticker))?.[1];
@@ -236,7 +247,7 @@ function createRuntime(payload: CliPaneShotPayload): PluginRuntimeAccess {
236
247
  return (payload.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? null;
237
248
  }
238
249
  return {
239
- getMarketData: () => null,
250
+ getMarketData: () => shotDataProvider,
240
251
  getCapability: () => null,
241
252
  getBrokerAdapter: () => null,
242
253
  connectBrokerInstance: async () => {},
@@ -59,6 +59,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
59
59
  isSelected,
60
60
  onSelect,
61
61
  onActivate,
62
+ onTableMouseDown,
62
63
  onRowMouseDown,
63
64
  onRowContextMenu,
64
65
  rowContextMenuSurface = false,
@@ -71,6 +72,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
71
72
  emptyStateHint,
72
73
  virtualize = true,
73
74
  overscan = 3,
75
+ columnGap = 1,
76
+ horizontalPadding = 1,
77
+ fillAvailableWidth = true,
74
78
  showHorizontalScrollbar = true,
75
79
  scrollToIndex,
76
80
  scrollToIndexAlign = "nearest",
@@ -85,10 +89,13 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
85
89
  const bodyVertical = useScrollbarState(true);
86
90
  const [viewportWidth, setViewportWidth] = useState(0);
87
91
  const [scrollbarActive, markScrollbarActive] = useScrollbarActivity();
88
- const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
92
+ const tableWidth = useMemo(
93
+ () => getTableWidth(columns, columnGap, horizontalPadding),
94
+ [columnGap, columns, horizontalPadding],
95
+ );
89
96
  const gridTemplateColumns = useMemo(
90
- () => buildTableGridTemplateColumns(columns),
91
- [columns],
97
+ () => buildTableGridTemplateColumns(columns, fillAvailableWidth),
98
+ [columns, fillAvailableWidth],
92
99
  );
93
100
  const selectRow = useCallback((item: T, index: number) => {
94
101
  onSelect(item, index);
@@ -136,8 +143,9 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
136
143
  : WEB_CELL_HEIGHT + items.length * WEB_CELL_HEIGHT;
137
144
  const bodyAfterHeight = bodyAfter ? WEB_CELL_HEIGHT * 6 : 0;
138
145
  const horizontalScrollEnabled = showHorizontalScrollbar
139
- && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth);
146
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, viewportWidth, columnGap);
140
147
  const scrollContentWidth = horizontalScrollEnabled
148
+ || !fillAvailableWidth
141
149
  ? Math.max(1, tableWidth * WEB_CELL_WIDTH)
142
150
  : "100%";
143
151
  const measureViewportWidth = useCallback(() => {
@@ -247,6 +255,7 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
247
255
  style={bodyScrollerStyle}
248
256
  onMouseDown={() => {
249
257
  focusPane();
258
+ onTableMouseDown?.({});
250
259
  }}
251
260
  onMouseLeave={() => {
252
261
  if (hoveredIdx !== null) setHoveredIdx(null);
@@ -270,7 +279,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
270
279
  >
271
280
  <WebDataTableHeader
272
281
  columns={columns}
282
+ columnGap={columnGap}
283
+ horizontalPadding={horizontalPadding}
273
284
  focusPane={focusPane}
285
+ onTableMouseDown={onTableMouseDown}
274
286
  gridTemplateColumns={gridTemplateColumns}
275
287
  onHeaderClick={onHeaderClick}
276
288
  sortColumnId={sortColumnId}
@@ -314,7 +326,10 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
314
326
  item={item}
315
327
  itemKey={itemKey}
316
328
  columns={columns}
329
+ columnGap={columnGap}
330
+ horizontalPadding={horizontalPadding}
317
331
  focusPane={focusPane}
332
+ onTableMouseDown={onTableMouseDown}
318
333
  gridTemplateColumns={gridTemplateColumns}
319
334
  onActivateRow={onActivate ? activateRow : undefined}
320
335
  onRowContextMenu={onRowContextMenu}
@@ -44,16 +44,30 @@ function contentJustifyForAlign(align: string | undefined): CSSProperties["justi
44
44
  return "flex-start";
45
45
  }
46
46
 
47
+ function columnGapCss(columnGap: number): CSSProperties["columnGap"] {
48
+ return columnGap === 0 ? 0 : `calc(${columnGap} * var(--cell-w))`;
49
+ }
50
+
51
+ function inlinePaddingPx(horizontalPadding: number): number {
52
+ return TABLE_INLINE_PADDING_PX * horizontalPadding;
53
+ }
54
+
47
55
  export function WebDataTableHeader<C extends DataTableColumn>({
48
56
  columns,
57
+ columnGap,
58
+ horizontalPadding,
49
59
  focusPane,
60
+ onTableMouseDown,
50
61
  gridTemplateColumns,
51
62
  onHeaderClick,
52
63
  sortColumnId,
53
64
  sortDirection,
54
65
  }: {
55
66
  columns: C[];
67
+ columnGap: number;
68
+ horizontalPadding: number;
56
69
  focusPane: () => void;
70
+ onTableMouseDown?: (event: any) => void;
57
71
  gridTemplateColumns: string;
58
72
  onHeaderClick: (columnId: string) => void;
59
73
  sortColumnId: string | null;
@@ -69,13 +83,13 @@ export function WebDataTableHeader<C extends DataTableColumn>({
69
83
  zIndex: 2,
70
84
  display: "grid",
71
85
  gridTemplateColumns,
72
- columnGap: "var(--cell-w)",
86
+ columnGap: columnGapCss(columnGap),
73
87
  alignItems: "center",
74
88
  width: "100%",
75
89
  minWidth: 0,
76
90
  height: WEB_CELL_HEIGHT,
77
- paddingLeft: TABLE_INLINE_PADDING_PX,
78
- paddingRight: TABLE_INLINE_PADDING_PX,
91
+ paddingLeft: inlinePaddingPx(horizontalPadding),
92
+ paddingRight: inlinePaddingPx(horizontalPadding),
79
93
  boxSizing: "border-box",
80
94
  backgroundColor: CSS_PANEL,
81
95
  }}
@@ -99,6 +113,7 @@ export function WebDataTableHeader<C extends DataTableColumn>({
99
113
  }}
100
114
  onMouseDown={(event) => {
101
115
  focusPane();
116
+ onTableMouseDown?.(event);
102
117
  event.preventDefault();
103
118
  onHeaderClick(column.id);
104
119
  }}
@@ -128,7 +143,10 @@ function WebDataTableRowInner<
128
143
  C extends DataTableColumn,
129
144
  >({
130
145
  columns,
146
+ columnGap,
147
+ horizontalPadding,
131
148
  focusPane,
149
+ onTableMouseDown,
132
150
  onActivateRow,
133
151
  onRowContextMenu,
134
152
  onRowMouseDown,
@@ -148,7 +166,10 @@ function WebDataTableRowInner<
148
166
  setHoveredIdx,
149
167
  }: {
150
168
  columns: C[];
169
+ columnGap: number;
170
+ horizontalPadding: number;
151
171
  focusPane: () => void;
172
+ onTableMouseDown?: (event: any) => void;
152
173
  onActivateRow?: (item: T, index: number) => void;
153
174
  onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
154
175
  onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
@@ -177,13 +198,13 @@ function WebDataTableRowInner<
177
198
  transform: `translateY(${rowStart}px)`,
178
199
  display: "grid",
179
200
  gridTemplateColumns,
180
- columnGap: "var(--cell-w)",
201
+ columnGap: columnGapCss(columnGap),
181
202
  alignItems: "center",
182
203
  width: "100%",
183
204
  minWidth: 0,
184
205
  height: rowSize,
185
- paddingLeft: TABLE_INLINE_PADDING_PX,
186
- paddingRight: TABLE_INLINE_PADDING_PX,
206
+ paddingLeft: inlinePaddingPx(horizontalPadding),
207
+ paddingRight: inlinePaddingPx(horizontalPadding),
187
208
  boxSizing: "border-box",
188
209
  lineHeight: "var(--cell-h)",
189
210
  };
@@ -199,6 +220,7 @@ function WebDataTableRowInner<
199
220
  }}
200
221
  onMouseDown={(event) => {
201
222
  focusPane();
223
+ onTableMouseDown?.(event);
202
224
  event.preventDefault();
203
225
  }}
204
226
  >
@@ -243,6 +265,7 @@ function WebDataTableRowInner<
243
265
  }}
244
266
  onMouseDown={(event) => {
245
267
  focusPane();
268
+ onTableMouseDown?.(event);
246
269
  if (onRowMouseDown?.(item, index, eventWithCellCoordinates(event)) === true) {
247
270
  return;
248
271
  }
@@ -274,6 +297,7 @@ function WebDataTableRowInner<
274
297
  }}
275
298
  onMouseDown={(event) => {
276
299
  focusPane();
300
+ onTableMouseDown?.(event);
277
301
  if (cell.onMouseDown) {
278
302
  cell.onMouseDown(eventWithCellCoordinates(event));
279
303
  return;
@@ -84,6 +84,7 @@ export function WebTextField({
84
84
  inputRef,
85
85
  onChange,
86
86
  onSubmit,
87
+ onBlur,
87
88
  hint,
88
89
  type = "text",
89
90
  variant = "default",
@@ -155,7 +156,12 @@ export function WebTextField({
155
156
  }}
156
157
  onInput={onChange}
157
158
  onChange={onChange}
158
- onSubmit={() => onSubmit?.(value ?? resolvedInputRef.current?.editBuffer.getText() ?? "")}
159
+ onSubmit={(nextValue?: string) => onSubmit?.(
160
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
161
+ )}
162
+ onBlur={(nextValue?: string) => onBlur?.(
163
+ typeof nextValue === "string" ? nextValue : resolvedInputRef.current?.editBuffer.getText() ?? value ?? "",
164
+ )}
159
165
  />
160
166
  </Box>
161
167
  {hint && (
@@ -347,14 +353,14 @@ export function WebPageStackView({
347
353
 
348
354
  if (!detailOpen) {
349
355
  return (
350
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
356
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
351
357
  {rootContent}
352
358
  </Box>
353
359
  );
354
360
  }
355
361
 
356
362
  return (
357
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
363
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
358
364
  <Box
359
365
  flexDirection="row"
360
366
  alignItems="flex-start"
@@ -370,7 +376,7 @@ export function WebPageStackView({
370
376
  flexDirection="row"
371
377
  alignItems="center"
372
378
  backgroundColor={panelFill()}
373
- onMouseDown={(event) => {
379
+ onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
374
380
  event.preventDefault?.();
375
381
  event.stopPropagation?.();
376
382
  onBack();
@@ -413,7 +419,7 @@ export function WebPageStackView({
413
419
  </Text>
414
420
  ) : null}
415
421
  </Box>
416
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
422
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
417
423
  {detailContent}
418
424
  </Box>
419
425
  </Box>
@@ -0,0 +1,50 @@
1
+ import { renderAsciiText, type AsciiFontName } from "../../../../ui/ascii-font";
2
+
3
+ const WEB_GLOOMBERB_WORDMARK = [
4
+ " ____ _ _ _ ",
5
+ " / ___| | ___ ___ _ __ ___ | |__ ___ _ __| |__ ",
6
+ "| | _| |/ _ \\ / _ \\| '_ ` _ \\| '_ \\ / _ \\ '__| '_ \\ ",
7
+ "| |_| | | (_) | (_) | | | | | | |_) | __/ | | |_) |",
8
+ " \\____|_|\\___/ \\___/|_| |_| |_|_.__/ \\___|_| |_.__/ ",
9
+ ];
10
+
11
+ type WebWordmarkVariant = "legacy" | "compat" | null;
12
+
13
+ function currentDesktopPlatform(): string {
14
+ const navigatorLike = (globalThis as {
15
+ navigator?: {
16
+ platform?: string;
17
+ userAgent?: string;
18
+ userAgentData?: { platform?: string };
19
+ };
20
+ }).navigator;
21
+ return [
22
+ navigatorLike?.userAgentData?.platform,
23
+ navigatorLike?.platform,
24
+ navigatorLike?.userAgent,
25
+ ].filter(Boolean).join(" ");
26
+ }
27
+
28
+ function isMacDesktopPlatform(desktopPlatform?: string): boolean {
29
+ const raw = desktopPlatform?.trim() || currentDesktopPlatform();
30
+ return /(darwin|mac|iphone|ipad|ipod)/i.test(raw);
31
+ }
32
+
33
+ export function webAsciiTextWordmarkVariant(
34
+ text: string,
35
+ font: AsciiFontName = "tiny",
36
+ desktopPlatform?: string,
37
+ ): WebWordmarkVariant {
38
+ if (font !== "wordmark" || text.trim().toLowerCase() !== "gloomberb") return null;
39
+ return isMacDesktopPlatform(desktopPlatform) ? "legacy" : "compat";
40
+ }
41
+
42
+ export function webAsciiTextLines(
43
+ text: string,
44
+ font: AsciiFontName = "tiny",
45
+ desktopPlatform?: string,
46
+ ): string[] {
47
+ return webAsciiTextWordmarkVariant(text, font, desktopPlatform) === "compat"
48
+ ? WEB_GLOOMBERB_WORDMARK
49
+ : renderAsciiText(text, font);
50
+ }