gloomberb 0.6.1 → 0.7.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 (139) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +48 -27
  14. package/src/components/chart/static-chart-surface.tsx +28 -4
  15. package/src/components/chart/stock-chart.test.ts +0 -16
  16. package/src/components/chart/stock-chart.tsx +38 -21
  17. package/src/components/chart/time-axis-label.tsx +43 -0
  18. package/src/components/command-bar/command-bar.test.tsx +71 -8
  19. package/src/components/command-bar/command-bar.tsx +388 -713
  20. package/src/components/command-bar/confirm-body.tsx +51 -0
  21. package/src/components/command-bar/list-model.ts +77 -0
  22. package/src/components/command-bar/view-model.ts +0 -19
  23. package/src/components/command-bar/workflow-ops.ts +4 -0
  24. package/src/components/data-table-stack-view.test.tsx +39 -4
  25. package/src/components/data-table-stack-view.tsx +11 -104
  26. package/src/components/data-table-view.tsx +43 -79
  27. package/src/components/feed-data-table-stack-view.tsx +10 -17
  28. package/src/components/feed-stack-controller.ts +39 -0
  29. package/src/components/index.ts +5 -1
  30. package/src/components/layout/floating-pane.tsx +30 -2
  31. package/src/components/layout/header.tsx +10 -7
  32. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  33. package/src/components/layout/pane-cycle-order.ts +32 -0
  34. package/src/components/layout/pane-footer.tsx +11 -4
  35. package/src/components/layout/pane-header.tsx +5 -5
  36. package/src/components/layout/shell.test.tsx +66 -22
  37. package/src/components/layout/shell.tsx +13 -6
  38. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  39. package/src/components/pane-settings-dialog.tsx +12 -4
  40. package/src/components/pane-template-wizard.test.tsx +3 -4
  41. package/src/components/speedometer-gauge.test.tsx +45 -0
  42. package/src/components/speedometer-gauge.tsx +558 -0
  43. package/src/components/table-view-shared.tsx +158 -0
  44. package/src/components/ticker-list-table-view.tsx +42 -72
  45. package/src/components/ui/confirm-dialog.tsx +66 -0
  46. package/src/components/ui/data-table.tsx +18 -10
  47. package/src/components/ui/index.ts +0 -1
  48. package/src/components/ui/page-stack-view.tsx +39 -10
  49. package/src/components/ui/table-layout.test.ts +16 -0
  50. package/src/components/ui/table-layout.ts +10 -3
  51. package/src/components/ui/ui.test.tsx +100 -20
  52. package/src/core/state/app-state.ts +6 -3
  53. package/src/market-data/coordinator.test.ts +1 -1
  54. package/src/market-data/coordinator.ts +0 -1
  55. package/src/news/aggregator.ts +0 -3
  56. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  57. package/src/plugins/builtin/ai/providers.ts +0 -17
  58. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  59. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  60. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  61. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  62. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  63. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  64. package/src/plugins/builtin/chat-controller.ts +75 -6
  65. package/src/plugins/builtin/chat.test.tsx +6 -6
  66. package/src/plugins/builtin/chat.tsx +29 -20
  67. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  68. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  69. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  70. package/src/plugins/builtin/correlation/index.tsx +1 -6
  71. package/src/plugins/builtin/debug.tsx +1 -3
  72. package/src/plugins/builtin/econ/index.tsx +1 -2
  73. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  74. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  75. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  76. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  77. package/src/plugins/builtin/help.test.tsx +2 -2
  78. package/src/plugins/builtin/help.tsx +2 -2
  79. package/src/plugins/builtin/holders/index.tsx +10 -15
  80. package/src/plugins/builtin/insider/index.tsx +22 -89
  81. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  82. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  83. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  84. package/src/plugins/builtin/news.tsx +4 -16
  85. package/src/plugins/builtin/notes.tsx +18 -55
  86. package/src/plugins/builtin/options.test.tsx +58 -2
  87. package/src/plugins/builtin/options.tsx +108 -49
  88. package/src/plugins/builtin/plugin-groups.ts +2 -0
  89. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  90. package/src/plugins/builtin/research/index.tsx +5 -21
  91. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  92. package/src/plugins/builtin/sec.tsx +13 -66
  93. package/src/plugins/builtin/sectors/index.tsx +1 -1
  94. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  95. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  96. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  97. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  98. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  99. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  100. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  101. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  102. package/src/plugins/ibkr/gateway-service.ts +1 -1
  103. package/src/plugins/ibkr/index.test.tsx +0 -2
  104. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  105. package/src/plugins/pane-manager.ts +0 -13
  106. package/src/plugins/plugin-runtime.test.tsx +6 -6
  107. package/src/plugins/plugin-runtime.tsx +13 -8
  108. package/src/plugins/prediction-markets/cli.ts +0 -1
  109. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  110. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  111. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  112. package/src/plugins/registry.ts +10 -39
  113. package/src/react/input.ts +1 -0
  114. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  115. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  116. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  117. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  118. package/src/renderers/electrobun/view/key-event.ts +1 -1
  119. package/src/renderers/electrobun/view/styles.css +331 -0
  120. package/src/renderers/opentui/chart-surface.tsx +179 -0
  121. package/src/renderers/opentui/start.tsx +48 -30
  122. package/src/renderers/opentui/test-utils.tsx +17 -0
  123. package/src/renderers/opentui/ui-host.tsx +5 -2
  124. package/src/sources/provider-router.ts +0 -4
  125. package/src/state/app-context.test.ts +45 -0
  126. package/src/state/use-inline-tickers.test.tsx +73 -0
  127. package/src/state/use-inline-tickers.ts +7 -1
  128. package/src/test-support/plugin-runtime.ts +3 -2
  129. package/src/theme/colors.ts +12 -3
  130. package/src/theme/themes.test.ts +23 -1
  131. package/src/theme/themes.ts +115 -0
  132. package/src/types/broker.ts +1 -1
  133. package/src/types/data-provider.ts +0 -1
  134. package/src/types/plugin.ts +0 -2
  135. package/src/ui/host.tsx +1 -0
  136. package/src/utils/back-navigation.ts +24 -1
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  139. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,10 +1,11 @@
1
1
  import { Box, Text } from "../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { TextAttributes } from "../../ui";
4
- import type { DetailTabProps, GloomPlugin, PaneProps } from "../../types/plugin";
4
+ import type { GloomPlugin } from "../../types/plugin";
5
5
  import type { OptionContract, OptionsChain } from "../../types/financials";
6
6
  import { usePaneTicker } from "../../state/app-context";
7
- import { blendHex, colors } from "../../theme/colors";
7
+ import { blendHex, colors, hoverBg } from "../../theme/colors";
8
+ import { blendForContrast, contrastRatio, higherContrast } from "../../theme/color-utils";
8
9
  import { formatCompact, formatNumber } from "../../utils/format";
9
10
  import { formatMarketPrice } from "../../utils/market-format";
10
11
  import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
@@ -12,7 +13,7 @@ import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks"
12
13
  import { DataTableView, Spinner, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../components";
13
14
  import { createTickerSurfacePaneTemplate } from "./ticker-surface";
14
15
 
15
- type OptionColumnId =
16
+ export type OptionColumnId =
16
17
  | "callLast"
17
18
  | "callBid"
18
19
  | "callAsk"
@@ -36,31 +37,40 @@ interface OptionTableRow {
36
37
  isPositionStrike: boolean;
37
38
  }
38
39
 
39
- type OptionsViewProps = Pick<DetailTabProps, "width" | "height" | "focused"> & {
40
- onCapture?: DetailTabProps["onCapture"];
40
+ type OptionsViewProps = {
41
+ width: number;
42
+ height: number;
43
+ focused: boolean;
44
+ onCapture?: (capturing: boolean) => void;
41
45
  };
42
46
 
43
- const OPTION_CALL_COLOR = "#5ed69a";
44
- const OPTION_PUT_COLOR = "#ff9c7a";
45
- const OPTION_PRICE_COLOR = "#dfc05b";
46
- const OPTION_ACTIVITY_COLOR = "#35a7d6";
47
- const OPTION_IV_COLOR = "#8bd878";
48
- const OPTION_STRIKE_COLOR = "#8fb7ff";
49
-
50
- const OPTION_COLUMNS: OptionColumn[] = [
51
- { id: "callOpenInterest", label: "C OI", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
52
- { id: "callVolume", label: "C VOL", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
53
- { id: "callLast", label: "C LAST", width: 7, align: "right", headerColor: OPTION_CALL_COLOR },
54
- { id: "callIv", label: "C IV", width: 6, align: "right", headerColor: OPTION_IV_COLOR },
55
- { id: "callBid", label: "C BID", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
56
- { id: "callAsk", label: "C ASK", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
57
- { id: "strike", label: "STRIKE", width: 9, align: "right", headerColor: OPTION_STRIKE_COLOR },
58
- { id: "putBid", label: "P BID", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
59
- { id: "putAsk", label: "P ASK", width: 7, align: "right", headerColor: OPTION_PRICE_COLOR },
60
- { id: "putIv", label: "P IV", width: 6, align: "right", headerColor: OPTION_IV_COLOR },
61
- { id: "putLast", label: "P LAST", width: 7, align: "right", headerColor: OPTION_PUT_COLOR },
62
- { id: "putVolume", label: "P VOL", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
63
- { id: "putOpenInterest", label: "P OI", width: 6, align: "right", headerColor: OPTION_ACTIVITY_COLOR },
47
+ type OptionColorRole = "call" | "put" | "price" | "activity" | "iv" | "strike";
48
+
49
+ const OPTION_TEXT_MIN_CONTRAST = 4.5;
50
+
51
+ const OPTION_BASE_COLORS: Record<OptionColorRole, string> = {
52
+ call: "#5ed69a",
53
+ put: "#ff9c7a",
54
+ price: "#dfc05b",
55
+ activity: "#35a7d6",
56
+ iv: "#8bd878",
57
+ strike: "#8fb7ff",
58
+ };
59
+
60
+ const OPTION_COLUMNS: Array<Omit<OptionColumn, "headerColor">> = [
61
+ { id: "callOpenInterest", label: "C OI", width: 6, align: "right" },
62
+ { id: "callVolume", label: "C VOL", width: 6, align: "right" },
63
+ { id: "callLast", label: "C LAST", width: 7, align: "right" },
64
+ { id: "callIv", label: "C IV", width: 6, align: "right" },
65
+ { id: "callBid", label: "C BID", width: 7, align: "right" },
66
+ { id: "callAsk", label: "C ASK", width: 7, align: "right" },
67
+ { id: "strike", label: "STRIKE", width: 9, align: "right" },
68
+ { id: "putBid", label: "P BID", width: 7, align: "right" },
69
+ { id: "putAsk", label: "P ASK", width: 7, align: "right" },
70
+ { id: "putIv", label: "P IV", width: 6, align: "right" },
71
+ { id: "putLast", label: "P LAST", width: 7, align: "right" },
72
+ { id: "putVolume", label: "P VOL", width: 6, align: "right" },
73
+ { id: "putOpenInterest", label: "P OI", width: 6, align: "right" },
64
74
  ];
65
75
 
66
76
  export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
@@ -161,6 +171,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
161
171
  const selectedStrike = strikes[strikeIdx];
162
172
  const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
163
173
  const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
174
+ const optionColumns = OPTION_COLUMNS.map((column) => ({
175
+ ...column,
176
+ headerColor: optionColumnColor(column.id, colors.panel),
177
+ }));
164
178
 
165
179
  usePaneFooter("options", () => {
166
180
  const info = [
@@ -303,7 +317,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
303
317
  focused={focused}
304
318
  selectedIndex={strikeIdx}
305
319
  onRootKeyDown={handleTableKeyDown}
306
- columns={OPTION_COLUMNS}
320
+ columns={optionColumns}
307
321
  items={rows}
308
322
  sortColumnId={null}
309
323
  sortDirection="asc"
@@ -327,14 +341,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
327
341
  );
328
342
  }
329
343
 
330
- export function OptionsTab(props: DetailTabProps) {
331
- return <OptionsView {...props} />;
332
- }
333
-
334
- export function OptionsPane({ focused, width, height }: PaneProps) {
335
- return <OptionsView focused={focused} width={width} height={height} />;
336
- }
337
-
338
344
  function buildStrikeList(chain: OptionsChain): number[] {
339
345
  const set = new Set<number>();
340
346
  for (const c of chain.calls) set.add(c.strike);
@@ -378,12 +384,60 @@ function optionContractForColumn(row: OptionTableRow, columnId: OptionColumnId):
378
384
  return columnId.startsWith("call") ? row.call : row.put;
379
385
  }
380
386
 
381
- function optionColumnColor(columnId: OptionColumnId): string {
382
- if (columnId === "strike") return OPTION_STRIKE_COLOR;
383
- if (columnId.endsWith("Iv")) return OPTION_IV_COLOR;
384
- if (columnId.endsWith("Volume") || columnId.endsWith("OpenInterest")) return OPTION_ACTIVITY_COLOR;
385
- if (columnId.endsWith("Bid") || columnId.endsWith("Ask")) return OPTION_PRICE_COLOR;
386
- return columnId.startsWith("call") ? OPTION_CALL_COLOR : OPTION_PUT_COLOR;
387
+ export function optionColumnColor(columnId: OptionColumnId, surface = colors.bg): string {
388
+ return optionRoleColor(optionColumnRole(columnId), surface);
389
+ }
390
+
391
+ function optionColumnRole(columnId: OptionColumnId): OptionColorRole {
392
+ if (columnId === "strike") return "strike";
393
+ if (columnId.endsWith("Iv")) return "iv";
394
+ if (columnId.endsWith("Volume") || columnId.endsWith("OpenInterest")) return "activity";
395
+ if (columnId.endsWith("Bid") || columnId.endsWith("Ask")) return "price";
396
+ return columnId.startsWith("call") ? "call" : "put";
397
+ }
398
+
399
+ function optionRoleThemeColor(role: OptionColorRole): string {
400
+ switch (role) {
401
+ case "call":
402
+ case "iv":
403
+ return colors.positive;
404
+ case "put":
405
+ return colors.negative;
406
+ case "price":
407
+ return colors.warning;
408
+ case "activity":
409
+ case "strike":
410
+ return colors.borderFocused;
411
+ }
412
+ }
413
+
414
+ function mostReadableColor(surface: string, candidates: readonly string[]): string {
415
+ return candidates.reduce((best, candidate) =>
416
+ contrastRatio(candidate, surface) > contrastRatio(best, surface) ? candidate : best,
417
+ );
418
+ }
419
+
420
+ export function optionRoleColor(role: OptionColorRole, surface: string): string {
421
+ const preferred = OPTION_BASE_COLORS[role];
422
+ const themeColor = optionRoleThemeColor(role);
423
+ const fallback = mostReadableColor(surface, [
424
+ preferred,
425
+ themeColor,
426
+ colors.text,
427
+ colors.textBright,
428
+ higherContrast("#ffffff", "#000000", surface),
429
+ ]);
430
+ return blendForContrast(preferred, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
431
+ }
432
+
433
+ export function optionMutedColor(surface: string): string {
434
+ const fallback = mostReadableColor(surface, [
435
+ colors.textDim,
436
+ colors.text,
437
+ colors.textBright,
438
+ higherContrast("#ffffff", "#000000", surface),
439
+ ]);
440
+ return blendForContrast(colors.textDim, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
387
441
  }
388
442
 
389
443
  function optionMoneynessBackground(
@@ -443,23 +497,28 @@ function renderOptionCell(
443
497
  rowState: { selected: boolean; hovered: boolean },
444
498
  ): DataTableCell {
445
499
  const selectedColor = rowState.selected ? colors.selectedText : undefined;
500
+ const rowSurface = rowState.selected ? colors.selected : rowState.hovered ? hoverBg() : colors.bg;
446
501
 
447
502
  if (column.id === "strike") {
503
+ const backgroundColor = rowState.selected || rowState.hovered
504
+ ? undefined
505
+ : blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1);
506
+ const surface = backgroundColor ?? rowSurface;
448
507
  return {
449
508
  text: formatStrikeLabel(row.strike),
450
- color: selectedColor ?? OPTION_STRIKE_COLOR,
451
- backgroundColor: rowState.selected || rowState.hovered
452
- ? undefined
453
- : blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1),
509
+ color: selectedColor ?? optionRoleColor("strike", surface),
510
+ backgroundColor,
454
511
  attributes: rowState.selected || row.isPositionStrike ? TextAttributes.BOLD : TextAttributes.NONE,
455
512
  };
456
513
  }
457
514
 
458
515
  const contract = optionContractForColumn(row, column.id);
516
+ const backgroundColor = optionMoneynessBackground(row, contract, column.id, rowState);
517
+ const surface = backgroundColor ?? rowSurface;
459
518
  return {
460
519
  text: formatOptionContractCell(contract, column),
461
- color: selectedColor ?? (contract ? optionColumnColor(column.id) : colors.textDim),
462
- backgroundColor: optionMoneynessBackground(row, contract, column.id, rowState),
520
+ color: selectedColor ?? (contract ? optionRoleColor(optionColumnRole(column.id), surface) : optionMutedColor(surface)),
521
+ backgroundColor,
463
522
  };
464
523
  }
465
524
 
@@ -475,7 +534,7 @@ export const optionsPlugin: GloomPlugin = {
475
534
  id: "options",
476
535
  name: "Options",
477
536
  icon: "O",
478
- component: OptionsPane,
537
+ component: OptionsView,
479
538
  defaultPosition: "right",
480
539
  defaultMode: "floating",
481
540
  defaultFloatingSize: { width: 112, height: 28 },
@@ -498,7 +557,7 @@ export const optionsPlugin: GloomPlugin = {
498
557
  id: "options",
499
558
  name: "Options",
500
559
  order: 35, // after Chart (30), before News (40)
501
- component: OptionsTab,
560
+ component: OptionsView,
502
561
  isVisible: ({ hasOptionsChain }) => hasOptionsChain,
503
562
  });
504
563
  },
@@ -4,6 +4,7 @@ import { correlationPlugin } from "./correlation";
4
4
  import { earningsPlugin } from "./earnings";
5
5
  import { registerEconCalendarFeature } from "./econ";
6
6
  import { fxMatrixPlugin } from "./fx-matrix";
7
+ import { fearGreedPlugin } from "./fear-greed";
7
8
  import { holdersPlugin } from "./holders";
8
9
  import { insiderPlugin } from "./insider";
9
10
  import { marketMoversPlugin } from "./market-movers";
@@ -74,6 +75,7 @@ export const marketOverviewPlugin = createPluginGroup({
74
75
  correlationPlugin,
75
76
  worldIndicesPlugin,
76
77
  marketMoversPlugin,
78
+ fearGreedPlugin,
77
79
  sectorsPlugin,
78
80
  fxMatrixPlugin,
79
81
  ],
@@ -16,7 +16,7 @@ import {
16
16
  renderTable,
17
17
  } from "../../../../utils/cli-output";
18
18
  import { exchangeShortName } from "../../../../utils/market-status";
19
- import { formatCompact, formatCurrency } from "../../../../utils/format";
19
+ import { formatCompact } from "../../../../utils/format";
20
20
  import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity } from "../../../../utils/market-format";
21
21
  import type { AppConfig } from "../../../../types/config";
22
22
  import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin";
@@ -8,7 +8,7 @@ import {
8
8
  type DataTableKeyEvent,
9
9
  } from "../../../components";
10
10
  import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
11
- import type { DetailTabProps, GloomPlugin, PaneProps } from "../../../types/plugin";
11
+ import type { GloomPlugin, PaneProps } from "../../../types/plugin";
12
12
  import { usePaneInstance, usePaneTicker } from "../../../state/app-context";
13
13
  import { blendHex, colors, priceColor } from "../../../theme/colors";
14
14
  import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
@@ -609,22 +609,6 @@ function RelativeValuationPane({ focused, width, height }: PaneProps) {
609
609
  );
610
610
  }
611
611
 
612
- function ResearchPane({ focused, width, height }: PaneProps) {
613
- return <AnalystResearchView focused={focused} width={width} height={height} />;
614
- }
615
-
616
- function EventsPane({ focused, width, height }: PaneProps) {
617
- return <CorporateActionsView focused={focused} width={width} height={height} />;
618
- }
619
-
620
- function AnalystTab({ focused, width, height }: DetailTabProps) {
621
- return <AnalystResearchView focused={focused} width={width} height={height} />;
622
- }
623
-
624
- function EventsTab({ focused, width, height }: DetailTabProps) {
625
- return <CorporateActionsView focused={focused} width={width} height={height} />;
626
- }
627
-
628
612
  export const researchPlugin: GloomPlugin = {
629
613
  id: "research",
630
614
  name: "Research",
@@ -637,14 +621,14 @@ export const researchPlugin: GloomPlugin = {
637
621
  id: "analyst-research",
638
622
  name: "Analyst",
639
623
  order: 32,
640
- component: AnalystTab,
624
+ component: AnalystResearchView,
641
625
  isVisible: ({ ticker }) => !!ticker,
642
626
  });
643
627
  ctx.registerDetailTab({
644
628
  id: "corporate-actions",
645
629
  name: "Events",
646
630
  order: 34,
647
- component: EventsTab,
631
+ component: CorporateActionsView,
648
632
  isVisible: ({ ticker }) => !!ticker,
649
633
  });
650
634
  },
@@ -654,7 +638,7 @@ export const researchPlugin: GloomPlugin = {
654
638
  id: "analyst-research",
655
639
  name: "Analyst Research",
656
640
  icon: "A",
657
- component: ResearchPane,
641
+ component: AnalystResearchView,
658
642
  defaultPosition: "right",
659
643
  defaultMode: "floating",
660
644
  defaultFloatingSize: { width: 90, height: 28 },
@@ -663,7 +647,7 @@ export const researchPlugin: GloomPlugin = {
663
647
  id: "corporate-actions",
664
648
  name: "Corporate Actions",
665
649
  icon: "E",
666
- component: EventsPane,
650
+ component: CorporateActionsView,
667
651
  defaultPosition: "right",
668
652
  defaultMode: "floating",
669
653
  defaultFloatingSize: { width: 82, height: 24 },
@@ -0,0 +1,84 @@
1
+ import { Box, Text } from "../../ui";
2
+ import { colors } from "../../theme/colors";
3
+ import { formatCompact, formatCurrency } from "../../utils/format";
4
+ import { transactionTypeLabel, type InsiderTransaction } from "./insider/insider-data";
5
+
6
+ const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
7
+
8
+ export function formatFilingShortDate(value: Date | string | number): string {
9
+ const date = value instanceof Date ? value : new Date(value);
10
+ if (isNaN(date.getTime())) return "—";
11
+ return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
12
+ }
13
+
14
+ export function formatFilingMetaDate(value: Date): string {
15
+ return value.toLocaleString("en-US", {
16
+ month: "short",
17
+ day: "numeric",
18
+ year: "numeric",
19
+ });
20
+ }
21
+
22
+ export function formatFilingFormLabel(form: string, fallback = "FORM 4"): string {
23
+ const value = form.trim();
24
+ return value ? `FORM ${value}` : fallback;
25
+ }
26
+
27
+ export function renderFilingNotice(message: string, width: number) {
28
+ const lines = wrapNoticeLines(message, width - 4);
29
+ return (
30
+ <Box flexDirection="column" paddingX={1} paddingY={1}>
31
+ {lines.map((line, index) => (
32
+ <Box key={index} height={1}>
33
+ <Text fg={colors.textDim}>{line}</Text>
34
+ </Box>
35
+ ))}
36
+ </Box>
37
+ );
38
+ }
39
+
40
+ export function buildInsiderTransactionTitle(transaction: InsiderTransaction): string {
41
+ const type = transactionTypeLabel(transaction.transactionType);
42
+ const price = transaction.pricePerShare != null
43
+ ? ` @ ${formatCurrency(transaction.pricePerShare)}`
44
+ : "";
45
+ const value = transaction.totalValue != null
46
+ ? ` | ${formatCurrency(transaction.totalValue)}`
47
+ : "";
48
+ return `${type} ${formatCompact(transaction.shares)} shares${price}${value}`;
49
+ }
50
+
51
+ export function buildInsiderTransactionDetailBody(transaction: InsiderTransaction): string {
52
+ const lines = [
53
+ `Transaction: ${transactionTypeLabel(transaction.transactionType)}`,
54
+ `Date: ${formatFilingShortDate(transaction.filingDate)}`,
55
+ `Shares: ${formatCompact(transaction.shares)}`,
56
+ `Price/Share: ${transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}`,
57
+ `Total Value: ${transaction.totalValue != null ? formatCurrency(transaction.totalValue) : "—"}`,
58
+ `Shares Owned After: ${transaction.sharesOwned != null ? formatCompact(transaction.sharesOwned) : "—"}`,
59
+ ];
60
+ return lines.join("\n");
61
+ }
62
+
63
+ function wrapNoticeLines(text: string, width: number): string[] {
64
+ const maxWidth = Math.max(width, 12);
65
+ const words = text.trim().split(/\s+/);
66
+ const lines: string[] = [];
67
+ let current = "";
68
+
69
+ for (const word of words) {
70
+ if (!current) {
71
+ current = word;
72
+ continue;
73
+ }
74
+ if (current.length + 1 + word.length <= maxWidth) {
75
+ current = `${current} ${word}`;
76
+ } else {
77
+ lines.push(current);
78
+ current = word;
79
+ }
80
+ }
81
+
82
+ if (current) lines.push(current);
83
+ return lines;
84
+ }
@@ -1,6 +1,6 @@
1
- import { Box, Text } from "../../ui";
1
+ import { Text } from "../../ui";
2
2
  import { useEffect, useMemo, useRef, useState } from "react";
3
- import type { DetailTabProps, GloomPlugin, PaneProps } from "../../types/plugin";
3
+ import type { GloomPlugin } from "../../types/plugin";
4
4
  import type { SecFilingItem } from "../../types/data-provider";
5
5
  import { useResolvedEntryValue, useSecFilingContent, useSecFilingsQuery } from "../../market-data/hooks";
6
6
  import { instrumentFromTicker } from "../../market-data/request-types";
@@ -13,6 +13,10 @@ import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
13
13
  import { parseForm4Xml, transactionTypeLabel } from "./insider/insider-data";
14
14
  import { formatCompact, formatCurrency } from "../../utils/format";
15
15
  import { createTickerSurfacePaneTemplate } from "./ticker-surface";
16
+ import {
17
+ formatFilingMetaDate,
18
+ renderFilingNotice,
19
+ } from "./sec-filing-display";
16
20
 
17
21
  const SEC_FILING_LIMIT = 50;
18
22
  const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
@@ -60,48 +64,8 @@ function getFilingDisplayTitle(filing: SecFilingItem): string {
60
64
  return description ? `${formLabel} | ${description}` : formLabel;
61
65
  }
62
66
 
63
- function wrapMessageLines(text: string, width: number): string[] {
64
- const maxWidth = Math.max(width, 12);
65
- const words = text.trim().split(/\s+/);
66
- const lines: string[] = [];
67
- let current = "";
68
-
69
- for (const word of words) {
70
- if (!current) {
71
- current = word;
72
- continue;
73
- }
74
- if ((current.length + 1 + word.length) <= maxWidth) {
75
- current = `${current} ${word}`;
76
- } else {
77
- lines.push(current);
78
- current = word;
79
- }
80
- }
81
-
82
- if (current) lines.push(current);
83
- return lines;
84
- }
85
-
86
- function renderNotice(message: string, width: number) {
87
- const lines = wrapMessageLines(message, width - 4);
88
- return (
89
- <Box flexDirection="column" paddingX={1} paddingY={1}>
90
- {lines.map((line, index) => (
91
- <Box key={index} height={1}>
92
- <Text fg={colors.textDim}>{line}</Text>
93
- </Box>
94
- ))}
95
- </Box>
96
- );
97
- }
98
-
99
67
  function formatFiledAt(filing: SecFilingItem): string {
100
- return filing.filingDate.toLocaleString("en-US", {
101
- month: "short",
102
- day: "numeric",
103
- year: "numeric",
104
- });
68
+ return formatFilingMetaDate(filing.filingDate);
105
69
  }
106
70
 
107
71
  function buildDetailBody(filing: SecFilingItem): string {
@@ -142,15 +106,6 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
142
106
  return lines.join("\n");
143
107
  }
144
108
 
145
- function getFormCategory(form: string): "annual" | "quarterly" | "current" | "ownership" | "other" {
146
- const f = form.trim().toUpperCase();
147
- if (f === "10-K" || f === "10-K/A" || f === "20-F") return "annual";
148
- if (f === "10-Q" || f === "10-Q/A") return "quarterly";
149
- if (f.startsWith("8-K")) return "current";
150
- if (OWNERSHIP_FORMS.has(f) || f === "SC 13G" || f === "SC 13D" || f === "SC 13G/A" || f === "SC 13D/A") return "ownership";
151
- return "other";
152
- }
153
-
154
109
  function getFormDescription(form: string): string {
155
110
  const f = form.trim().toUpperCase();
156
111
  switch (f) {
@@ -226,7 +181,7 @@ function toFeedItems(
226
181
  });
227
182
  }
228
183
 
229
- export function SecView({ width, height, focused }: Pick<DetailTabProps, "width" | "height" | "focused">) {
184
+ function SecView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
230
185
  const { ticker } = usePaneTicker();
231
186
  const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
232
187
  const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>(selectionKey, 0);
@@ -323,10 +278,10 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
323
278
  });
324
279
 
325
280
  if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view SEC filings.</Text>;
326
- if (!eligibleTicker) return renderNotice("SEC filings are only shown for US equities.", width);
281
+ if (!eligibleTicker) return renderFilingNotice("SEC filings are only shown for US equities.", width);
327
282
  if (loading && filings.length === 0) return <Spinner label="Loading SEC filings..." />;
328
- if (error) return renderNotice(`Error: ${error}`, width);
329
- if (filings.length === 0) return renderNotice(`No recent SEC filings for ${ticker.metadata.ticker}.`, width);
283
+ if (error) return renderFilingNotice(`Error: ${error}`, width);
284
+ if (filings.length === 0) return renderFilingNotice(`No recent SEC filings for ${ticker.metadata.ticker}.`, width);
330
285
 
331
286
  return (
332
287
  <FeedDataTableStackView
@@ -344,14 +299,6 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
344
299
  );
345
300
  }
346
301
 
347
- function SecTab(props: DetailTabProps) {
348
- return <SecView {...props} />;
349
- }
350
-
351
- function SecPane({ focused, width, height }: PaneProps) {
352
- return <SecView focused={focused} width={width} height={height} />;
353
- }
354
-
355
302
  export const secPlugin: GloomPlugin = {
356
303
  id: "sec",
357
304
  name: "SEC",
@@ -364,7 +311,7 @@ export const secPlugin: GloomPlugin = {
364
311
  id: "sec",
365
312
  name: "SEC",
366
313
  icon: "S",
367
- component: SecPane,
314
+ component: SecView,
368
315
  defaultPosition: "right",
369
316
  defaultMode: "floating",
370
317
  defaultFloatingSize: { width: 100, height: 32 },
@@ -388,7 +335,7 @@ export const secPlugin: GloomPlugin = {
388
335
  id: "sec",
389
336
  name: "SEC",
390
337
  order: 45,
391
- component: SecTab,
338
+ component: SecView,
392
339
  isVisible: ({ ticker }) => isUsEquityTicker(ticker),
393
340
  });
394
341
  },
@@ -109,7 +109,7 @@ function nextSortPreference(current: SectorSortPreference, columnId: string): Se
109
109
  return DEFAULT_SORT_PREFERENCE;
110
110
  }
111
111
 
112
- export function SectorPerformancePane({ focused, width, height }: PaneProps) {
112
+ function SectorPerformancePane({ focused, width, height }: PaneProps) {
113
113
  const dataProvider = useAssetData();
114
114
  const { navigateTicker } = usePluginTickerActions();
115
115
  const [rows, setRows] = useState<SectorRow[]>(
@@ -90,7 +90,6 @@ export function ChartTab({
90
90
  interactive={interactive}
91
91
  onActivate={onActivate}
92
92
  axisMode={axisMode}
93
- symbol={symbol}
94
93
  ticker={ticker}
95
94
  financials={financials}
96
95
  indicatorConfig={hasStoredIndicatorSelection ? selectedIndicatorConfig : undefined}
@@ -1,7 +1,6 @@
1
1
  import type { GloomPlugin } from "../../../types/plugin";
2
2
  import { normalizeTickerInput } from "../../../utils/ticker-search";
3
3
  import { TickerDetailPane } from "./pane";
4
- import { FinancialsTab } from "./financials-tab";
5
4
  import { QuoteMonitorPane } from "./quote-monitor";
6
5
  import {
7
6
  buildQuoteMonitorSettingsDef,
@@ -560,7 +560,6 @@ export function OverviewTab({
560
560
  height={10}
561
561
  focused={false}
562
562
  compact
563
- symbol={symbol}
564
563
  ticker={ticker}
565
564
  financials={financials}
566
565
  />
@@ -1,7 +1,6 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { createTestRenderer } from "@opentui/core/testing";
3
- import { createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
4
- import { DialogProvider } from "@opentui-ui/dialog/react";
3
+ import { TestDialogProvider, createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
5
4
  import { act, useReducer, type ReactElement } from "react";
6
5
  import {
7
6
  AppContext,
@@ -111,7 +110,7 @@ function DetailHarness({
111
110
  harnessDispatch = dispatch;
112
111
 
113
112
  return (
114
- <DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
113
+ <TestDialogProvider>
115
114
  <AppContext value={{ state, dispatch }}>
116
115
  <PaneInstanceProvider paneId={TEST_PANE_ID}>
117
116
  <PluginRenderProvider pluginId={tickerDetailPlugin.id} runtime={runtime}>
@@ -125,7 +124,7 @@ function DetailHarness({
125
124
  </PluginRenderProvider>
126
125
  </PaneInstanceProvider>
127
126
  </AppContext>
128
- </DialogProvider>
127
+ </TestDialogProvider>
129
128
  );
130
129
  }
131
130
 
@@ -148,7 +148,7 @@ function nextSortPreference(
148
148
  return DEFAULT_SORT_PREFERENCE;
149
149
  }
150
150
 
151
- export function WorldIndicesPane({ focused, width, height }: PaneProps) {
151
+ function WorldIndicesPane({ focused, width, height }: PaneProps) {
152
152
  const dataProvider = useAssetData();
153
153
  const { pinTicker } = usePluginTickerActions();
154
154
  const [quotes, setQuotes] = useState<QuoteMap>(new Map());
@@ -30,7 +30,7 @@ function spreadBp(points: YieldPoint[]): number | null {
30
30
  return Math.round((y10 - y2) * 100);
31
31
  }
32
32
 
33
- export function YieldCurvePane({ focused, width, height }: PaneProps) {
33
+ function YieldCurvePane({ focused, width, height }: PaneProps) {
34
34
  const [points, setPoints] = useState<YieldPoint[]>([]);
35
35
  const [loading, setLoading] = useState(false);
36
36
  const [error, setError] = useState<string | null>(null);
@@ -1,8 +1,6 @@
1
1
  import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
2
- import type { BrokerInstanceConfig } from "../../types/config";
3
2
  import {
4
3
  buildIbkrConfigFromValues,
5
- getGatewayConfig,
6
4
  IBKR_CONFIG_FIELDS,
7
5
  isFlexConfigured,
8
6
  isGatewayConfigured,
@@ -82,7 +80,7 @@ export const ibkrBroker: BrokerAdapter = {
82
80
  return [{
83
81
  id: "ibkr-console",
84
82
  label: "IBKR Console",
85
- widgetId: "ibkr-trading",
83
+ paneId: "ibkr-trading",
86
84
  disabled: normalized.connectionMode !== "gateway",
87
85
  disabledReason: "IBKR Console is available for Gateway / TWS profiles.",
88
86
  }];