gloomberb 0.8.1 → 0.9.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 (196) hide show
  1. package/README.md +106 -54
  2. package/package.json +14 -13
  3. package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
  4. package/src/api-client/chat.ts +8 -0
  5. package/src/api-client/index.test.ts +30 -0
  6. package/src/api-client/index.ts +4 -0
  7. package/src/api-client/normalizers.ts +1 -0
  8. package/src/api-client/types.ts +1 -0
  9. package/src/app/global-shortcuts.test.tsx +54 -4
  10. package/src/app/global-shortcuts.ts +11 -5
  11. package/src/capabilities/factories.ts +4 -0
  12. package/src/components/chart/comparison/data.test.ts +54 -0
  13. package/src/components/chart/comparison/data.ts +47 -24
  14. package/src/components/chart/core/projection.ts +51 -27
  15. package/src/components/command-bar/action-runtime.ts +1 -0
  16. package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
  17. package/src/components/command-bar/layout-items/current-layout.ts +33 -0
  18. package/src/components/command-bar/list/view.tsx +3 -1
  19. package/src/components/command-bar/pane-templates/items.ts +4 -1
  20. package/src/components/command-bar/panel/body.tsx +1 -0
  21. package/src/components/command-bar/routes/root/results.ts +0 -30
  22. package/src/components/command-bar/routes/root/runtime.ts +2 -20
  23. package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
  24. package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
  25. package/src/components/command-bar/routing/launch-request.ts +14 -7
  26. package/src/components/command-bar/routing/list-state.ts +1 -3
  27. package/src/components/command-bar/surface/index.test.tsx +76 -4
  28. package/src/components/command-bar/surface/index.tsx +0 -2
  29. package/src/components/command-bar/surface/pane.test.tsx +1 -0
  30. package/src/components/command-bar/surface/test-harness.tsx +1 -0
  31. package/src/components/command-bar/view-model.ts +3 -3
  32. package/src/components/data-table/stack-view.test.tsx +14 -11
  33. package/src/components/data-table/stack-view.tsx +1 -2
  34. package/src/components/data-table/view.test.tsx +24 -18
  35. package/src/components/data-table/view.tsx +313 -83
  36. package/src/components/feed-data-table/stack-view.tsx +6 -8
  37. package/src/components/layout/pane/footer/index.test.tsx +6 -0
  38. package/src/components/layout/pane/footer/index.tsx +14 -2
  39. package/src/components/layout/pane/title.ts +5 -1
  40. package/src/components/layout/shell/index.test.tsx +120 -2
  41. package/src/components/layout/shell/index.tsx +2 -0
  42. package/src/components/layout/shell/menu.ts +1 -7
  43. package/src/components/layout/shell/pane/actions.ts +8 -0
  44. package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
  45. package/src/components/layout/shell/shortcuts.ts +14 -6
  46. package/src/components/layout/shell/window-mode/index.ts +8 -3
  47. package/src/components/layout/window-edit/presentation.ts +4 -4
  48. package/src/components/onboarding/onboarding-steps.tsx +3 -2
  49. package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
  50. package/src/components/pane-settings-dialog/tui.tsx +1 -1
  51. package/src/components/ticker/list-table-view.tsx +17 -25
  52. package/src/components/ui/choice-dialog.tsx +12 -2
  53. package/src/components/ui/multi-select/dialog.tsx +17 -5
  54. package/src/components/ui/remote-image.tsx +1 -14
  55. package/src/components/ui/shortcut-hint.tsx +14 -2
  56. package/src/components/ui/table-layout.test.ts +2 -2
  57. package/src/components/ui/table-layout.ts +8 -2
  58. package/src/components/ui/tabs.tsx +115 -86
  59. package/src/components/ui/ui.test.tsx +83 -4
  60. package/src/core/state/app/types.ts +12 -4
  61. package/src/market-data/coordinator/auxiliary.ts +29 -1
  62. package/src/market-data/coordinator/index.ts +17 -1
  63. package/src/market-data/hooks.tsx +17 -1
  64. package/src/market-data/selectors.ts +4 -0
  65. package/src/plugins/builtin/account-management/form-components.tsx +0 -4
  66. package/src/plugins/builtin/account-management/model.test.ts +54 -0
  67. package/src/plugins/builtin/account-management/model.ts +13 -3
  68. package/src/plugins/builtin/account-management/pane.tsx +5 -11
  69. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  70. package/src/plugins/builtin/alerts/pane.tsx +5 -3
  71. package/src/plugins/builtin/analytics/index.tsx +0 -3
  72. package/src/plugins/builtin/analytics/view.tsx +6 -5
  73. package/src/plugins/builtin/broker-manager/index.tsx +6 -6
  74. package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
  75. package/src/plugins/builtin/changelog/index.tsx +40 -27
  76. package/src/plugins/builtin/changelog/model.test.ts +77 -0
  77. package/src/plugins/builtin/changelog/model.ts +98 -0
  78. package/src/plugins/builtin/chat/channel-labels.ts +53 -0
  79. package/src/plugins/builtin/chat/channels.test.ts +28 -0
  80. package/src/plugins/builtin/chat/channels.ts +36 -19
  81. package/src/plugins/builtin/chat/chat.test.tsx +264 -5
  82. package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
  83. package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
  84. package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
  85. package/src/plugins/builtin/chat/content/composer.tsx +23 -2
  86. package/src/plugins/builtin/chat/content/index.tsx +66 -4
  87. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
  88. package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
  89. package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
  90. package/src/plugins/builtin/chat/content/types.ts +1 -0
  91. package/src/plugins/builtin/chat/controller/index.ts +41 -0
  92. package/src/plugins/builtin/chat/controller/messages.ts +18 -0
  93. package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
  94. package/src/plugins/builtin/chat/controller/utils.ts +6 -3
  95. package/src/plugins/builtin/chat/controller/view.ts +1 -1
  96. package/src/plugins/builtin/chat/controller.test.ts +189 -0
  97. package/src/plugins/builtin/chat/edit-window.ts +24 -0
  98. package/src/plugins/builtin/chat/layout.ts +94 -31
  99. package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
  100. package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
  101. package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
  102. package/src/plugins/builtin/chat/message/render-state.ts +5 -1
  103. package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
  104. package/src/plugins/builtin/chat/message/types.ts +2 -0
  105. package/src/plugins/builtin/chat/pane.tsx +35 -7
  106. package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
  107. package/src/plugins/builtin/chat/status-widget.tsx +29 -11
  108. package/src/plugins/builtin/chat/test-harness.tsx +2 -0
  109. package/src/plugins/builtin/cloud/plugin.tsx +3 -0
  110. package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
  111. package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
  112. package/src/plugins/builtin/earnings/index.tsx +8 -7
  113. package/src/plugins/builtin/econ/index.tsx +6 -8
  114. package/src/plugins/builtin/help/components.tsx +1 -1
  115. package/src/plugins/builtin/help/index.test.tsx +105 -30
  116. package/src/plugins/builtin/help/index.tsx +326 -164
  117. package/src/plugins/builtin/help/shortcut-model.ts +5 -6
  118. package/src/plugins/builtin/holders/pane.tsx +7 -6
  119. package/src/plugins/builtin/market-movers/index.tsx +6 -3
  120. package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
  121. package/src/plugins/builtin/options/view.tsx +16 -8
  122. package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
  123. package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
  124. package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
  125. package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
  126. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
  127. package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
  128. package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
  129. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
  130. package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
  131. package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
  132. package/src/plugins/builtin/research/index.test.ts +30 -1
  133. package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
  134. package/src/plugins/builtin/sec/filing-documents.ts +66 -0
  135. package/src/plugins/builtin/sec/index.tsx +132 -20
  136. package/src/plugins/builtin/sectors/index.tsx +13 -23
  137. package/src/plugins/builtin/shared/ticker-request.ts +1 -0
  138. package/src/plugins/builtin/substack/api/auth.ts +185 -0
  139. package/src/plugins/builtin/substack/api/cache.ts +78 -0
  140. package/src/plugins/builtin/substack/api/loaders.ts +146 -0
  141. package/src/plugins/builtin/substack/api/store.ts +186 -0
  142. package/src/plugins/builtin/substack/api/types.ts +40 -0
  143. package/src/plugins/builtin/substack/api.test.ts +298 -0
  144. package/src/plugins/builtin/substack/article-detail.tsx +359 -0
  145. package/src/plugins/builtin/substack/article-stack.tsx +129 -0
  146. package/src/plugins/builtin/substack/content.ts +478 -0
  147. package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
  148. package/src/plugins/builtin/substack/index.tsx +49 -0
  149. package/src/plugins/builtin/substack/login-view.tsx +126 -0
  150. package/src/plugins/builtin/substack/model.test.ts +155 -0
  151. package/src/plugins/builtin/substack/normalize.ts +343 -0
  152. package/src/plugins/builtin/substack/pane-footer.ts +71 -0
  153. package/src/plugins/builtin/substack/pane-state.ts +170 -0
  154. package/src/plugins/builtin/substack/pane.tsx +500 -0
  155. package/src/plugins/builtin/substack/table.ts +111 -0
  156. package/src/plugins/builtin/substack/types.ts +71 -0
  157. package/src/plugins/builtin/substack/utils.ts +131 -0
  158. package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
  161. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
  162. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
  163. package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
  164. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  165. package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
  166. package/src/plugins/builtin/world-indices/index.tsx +9 -6
  167. package/src/plugins/catalog-ui.ts +2 -0
  168. package/src/plugins/pane-manager/layout-state.ts +11 -0
  169. package/src/plugins/pane-manager.ts +1 -1
  170. package/src/plugins/prediction-markets/pane.tsx +10 -9
  171. package/src/plugins/registry/index.ts +3 -0
  172. package/src/plugins/runtime/context.tsx +1 -0
  173. package/src/plugins/runtime/index.tsx +4 -0
  174. package/src/react/use-throttled-commit-value.ts +116 -0
  175. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
  176. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
  177. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  178. package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
  179. package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
  180. package/src/renderers/electrobun/view/host/input.tsx +23 -2
  181. package/src/renderers/electrobun/view/http-fetch.ts +10 -3
  182. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  183. package/src/sources/gloomberb-cloud/index.ts +4 -0
  184. package/src/sources/provider-router/cache.ts +1 -0
  185. package/src/sources/provider-router/document-routes.ts +45 -0
  186. package/src/sources/provider-router/index.ts +4 -0
  187. package/src/sources/sec-edgar/content.ts +9 -3
  188. package/src/sources/sec-edgar.test.ts +108 -1
  189. package/src/sources/sec-edgar.ts +105 -1
  190. package/src/sources/yahoo-finance.ts +4 -0
  191. package/src/state/app/context/index.test.tsx +16 -0
  192. package/src/test-support/plugin-runtime.ts +1 -0
  193. package/src/tickers/search/index.ts +1 -0
  194. package/src/types/data-provider.ts +11 -0
  195. package/src/ui/host.tsx +1 -0
  196. package/src/version.ts +1 -1
@@ -1,6 +1,5 @@
1
1
  import { Box, ImageSurface, Text } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
- import { ExternalLinkText } from "./external-link";
4
3
  import { normalizedHttpUrl } from "../../utils/url";
5
4
 
6
5
  export interface RemoteImageProps {
@@ -11,13 +10,6 @@ export interface RemoteImageProps {
11
10
  label?: string;
12
11
  }
13
12
 
14
- function truncate(value: string, width: number): string {
15
- if (width <= 0) return "";
16
- if (value.length <= width) return value;
17
- if (width <= 3) return value.slice(0, width);
18
- return `${value.slice(0, width - 3)}...`;
19
- }
20
-
21
13
  export function RemoteImage({
22
14
  src,
23
15
  alt = "Image",
@@ -37,13 +29,8 @@ export function RemoteImage({
37
29
  height={Math.max(4, height)}
38
30
  objectFit="contain"
39
31
  >
40
- <Box flexDirection="column" gap={1}>
32
+ <Box flexDirection="column">
41
33
  <Text fg={colors.textDim}>{label}</Text>
42
- <ExternalLinkText
43
- url={imageUrl}
44
- label={truncate(imageUrl, resolvedWidth)}
45
- color={colors.textBright}
46
- />
47
34
  </Box>
48
35
  </ImageSurface>
49
36
  );
@@ -1,5 +1,6 @@
1
1
  import { StyledText, Text, TextAttributes } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
+ import { useRef } from "react";
3
4
 
4
5
  type ShortcutHintMouseEvent = {
5
6
  stopPropagation?: () => void;
@@ -36,6 +37,17 @@ export function ShortcutHint({
36
37
  const keyColor = disabled ? colors.textMuted : colors.textBright;
37
38
  const labelColor = disabled ? colors.textMuted : colors.textDim;
38
39
  const keyText = `[${hotkey}]`;
40
+ const triggerMouseDownRef = useRef(false);
41
+ const startPress = (event?: ShortcutHintMouseEvent) => {
42
+ triggerMouseDownRef.current = true;
43
+ stopMouseEvent(event);
44
+ };
45
+ const finishPress = (event?: ShortcutHintMouseEvent) => {
46
+ const startedOnTrigger = triggerMouseDownRef.current;
47
+ triggerMouseDownRef.current = false;
48
+ if (startedOnTrigger) onPress?.(event);
49
+ else stopMouseEvent(event);
50
+ };
39
51
 
40
52
  return (
41
53
  <Text
@@ -47,8 +59,8 @@ export function ShortcutHint({
47
59
  ])}
48
60
  fg={disabled ? colors.textMuted : colors.textDim}
49
61
  attributes={interactive ? TextAttributes.BOLD : 0}
50
- onMouseDown={interactive ? stopMouseEvent : undefined}
51
- onMouseUp={interactive ? onPress : undefined}
62
+ onMouseDown={interactive ? startPress : undefined}
63
+ onMouseUp={interactive ? finishPress : undefined}
52
64
  {...(dataGloomRole ? { "data-gloom-role": dataGloomRole } : {})}
53
65
  />
54
66
  );
@@ -27,7 +27,7 @@ describe("table layout", () => {
27
27
  ]);
28
28
 
29
29
  expect(template).toBe(
30
- "minmax(4ch, 4ch) minmax(14ch, 40fr) minmax(8ch, 24ch) minmax(5ch, 5ch)",
30
+ "minmax(calc(4 * var(--cell-w)), calc(4 * var(--cell-w))) minmax(calc(14 * var(--cell-w)), 40fr) minmax(calc(8 * var(--cell-w)), calc(24 * var(--cell-w))) minmax(calc(5 * var(--cell-w)), calc(5 * var(--cell-w)))",
31
31
  );
32
32
  });
33
33
 
@@ -37,6 +37,6 @@ describe("table layout", () => {
37
37
  { width: 8, align: "right" },
38
38
  ]);
39
39
 
40
- expect(template).toBe("minmax(8ch, 12fr) minmax(8ch, 8fr)");
40
+ expect(template).toBe("minmax(calc(8 * var(--cell-w)), 12fr) minmax(calc(8 * var(--cell-w)), 8fr)");
41
41
  });
42
42
  });
@@ -8,6 +8,7 @@ export interface TableWidthColumn {
8
8
  }
9
9
 
10
10
  const TRAILING_COLUMN_GUTTER_WIDTH = 1;
11
+ const WEB_CELL_UNIT = "var(--cell-w)";
11
12
 
12
13
  export function getTableWidth(columns: readonly TableWidthColumn[]): number {
13
14
  return columns.reduce((sum, column) => sum + column.width + 1, 2);
@@ -57,15 +58,20 @@ function columnFlexWeight(column: TableWidthColumn): number {
57
58
  return flexGrow > 0 ? baseWeight * Math.max(1, flexGrow) : baseWeight;
58
59
  }
59
60
 
61
+ function cellWidthCss(width: number): string {
62
+ return `calc(${width} * ${WEB_CELL_UNIT})`;
63
+ }
64
+
60
65
  export function buildTableGridTemplateColumns(columns: readonly TableWidthColumn[]): string {
61
66
  const hasFlexColumn = columns.some((column) => (column.flexGrow ?? 0) > 0);
62
67
  return columns
63
68
  .map((column) => {
64
69
  const width = normalizedColumnWidth(column);
70
+ const minWidth = cellWidthCss(columnMinCh(column));
65
71
  if (!hasFlexColumn || (column.flexGrow ?? 0) > 0) {
66
- return `minmax(${columnMinCh(column)}ch, ${columnFlexWeight(column)}fr)`;
72
+ return `minmax(${minWidth}, ${columnFlexWeight(column)}fr)`;
67
73
  }
68
- return `minmax(${columnMinCh(column)}ch, ${width}ch)`;
74
+ return `minmax(${minWidth}, ${cellWidthCss(width)})`;
69
75
  })
70
76
  .join(" ");
71
77
  }
@@ -30,6 +30,7 @@ export interface TabsProps {
30
30
  onAdd?: () => void;
31
31
  focused?: boolean;
32
32
  keyboardNavigation?: boolean;
33
+ scrollable?: boolean;
33
34
  }
34
35
 
35
36
  const WHEEL_DELTA_PER_CELL = 8;
@@ -45,9 +46,16 @@ export function Tabs({
45
46
  onAdd,
46
47
  focused = false,
47
48
  keyboardNavigation = true,
49
+ scrollable = true,
48
50
  }: TabsProps) {
49
51
  const ui = useUiHost();
50
52
  const NativeTabs = ui.Tabs;
53
+ const navigationValueRef = useRef<string | null>(activeValue);
54
+ const renderedActiveValueRef = useRef<string | null>(activeValue);
55
+ if (renderedActiveValueRef.current !== activeValue) {
56
+ renderedActiveValueRef.current = activeValue;
57
+ navigationValueRef.current = activeValue;
58
+ }
51
59
  const palette = {
52
60
  activeFg: variant === "bare" ? colors.textBright : colors.text,
53
61
  inactiveFg: colors.textDim,
@@ -62,18 +70,26 @@ export function Tabs({
62
70
  closeFg: colors.textMuted,
63
71
  addFg: colors.textMuted,
64
72
  };
73
+ const handleSelect = useCallback((value: string) => {
74
+ navigationValueRef.current = value;
75
+ onSelect(value);
76
+ }, [onSelect]);
77
+
65
78
  const selectAdjacentTab = useCallback((direction: -1 | 1) => {
66
79
  const enabledTabs = tabs.filter((tab) => !tab.disabled);
67
80
  if (enabledTabs.length === 0) return;
68
81
 
69
- const activeIndex = enabledTabs.findIndex((tab) => tab.value === activeValue);
82
+ const navigationValue = enabledTabs.some((tab) => tab.value === navigationValueRef.current)
83
+ ? navigationValueRef.current
84
+ : activeValue;
85
+ const activeIndex = enabledTabs.findIndex((tab) => tab.value === navigationValue);
70
86
  const nextIndex = activeIndex >= 0
71
87
  ? Math.max(0, Math.min(activeIndex + direction, enabledTabs.length - 1))
72
88
  : direction > 0 ? 0 : enabledTabs.length - 1;
73
89
  const nextTab = enabledTabs[nextIndex];
74
- if (!nextTab || nextTab.value === activeValue) return;
75
- onSelect(nextTab.value);
76
- }, [activeValue, onSelect, tabs]);
90
+ if (!nextTab || nextTab.value === navigationValue) return;
91
+ handleSelect(nextTab.value);
92
+ }, [activeValue, handleSelect, tabs]);
77
93
 
78
94
  useShortcut((event) => {
79
95
  if (event.ctrl || event.meta || event.alt || event.targetEditable) return;
@@ -96,7 +112,7 @@ export function Tabs({
96
112
  <NativeTabs
97
113
  tabs={tabs}
98
114
  activeValue={activeValue}
99
- onSelect={onSelect}
115
+ onSelect={handleSelect}
100
116
  compact={compact}
101
117
  variant={variant}
102
118
  closeMode={closeMode}
@@ -111,12 +127,13 @@ export function Tabs({
111
127
  <OpenTuiTabs
112
128
  tabs={tabs}
113
129
  activeValue={activeValue}
114
- onSelect={onSelect}
130
+ onSelect={handleSelect}
115
131
  compact={compact}
116
132
  variant={variant}
117
133
  closeMode={closeMode}
118
134
  addLabel={addLabel}
119
135
  onAdd={onAdd}
136
+ scrollable={scrollable}
120
137
  palette={palette}
121
138
  />
122
139
  );
@@ -140,6 +157,7 @@ function OpenTuiTabs({
140
157
  closeMode = "always",
141
158
  addLabel = "+",
142
159
  onAdd,
160
+ scrollable = true,
143
161
  palette,
144
162
  }: TabsProps & {
145
163
  palette: {
@@ -213,6 +231,96 @@ function OpenTuiTabs({
213
231
  scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
214
232
  };
215
233
 
234
+ const tabRow = (
235
+ <Box flexDirection="row" width={totalWidth} height={1}>
236
+ {tabs.map((tab, index) => {
237
+ const active = tab.value === activeValue;
238
+ const hovered = hoveredValue === tab.value && !tab.disabled;
239
+ const tabWidth = tabWidths[index] ?? tab.label.length + 2;
240
+ const showClose = !!tab.onClose && (closeMode === "always" || active);
241
+ const attributes = (active ? TextAttributes.BOLD : 0)
242
+ | (variant === "underline" && !compact && (active || hovered) ? TextAttributes.UNDERLINE : 0);
243
+ const startHover = tab.disabled
244
+ ? undefined
245
+ : () => {
246
+ setHoveredValue((current) => (current === tab.value ? current : tab.value));
247
+ };
248
+ const endHover = tab.disabled
249
+ ? undefined
250
+ : () => {
251
+ setHoveredValue((current) => (current === tab.value ? null : current));
252
+ };
253
+ const selectTab = tab.disabled
254
+ ? undefined
255
+ : (event: TabPointerEvent) => {
256
+ if (event.button === 2 && tab.onContextMenu) {
257
+ event.preventDefault?.();
258
+ event.stopPropagation?.();
259
+ tab.onContextMenu(tab.value, event);
260
+ return;
261
+ }
262
+ event.preventDefault();
263
+ event.stopPropagation?.();
264
+ onSelect(tab.value);
265
+ };
266
+
267
+ return (
268
+ <Box
269
+ key={tab.value}
270
+ width={tabWidth}
271
+ height={1}
272
+ flexDirection="row"
273
+ backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
274
+ onMouseOver={startHover}
275
+ onMouseMove={startHover}
276
+ onMouseOut={endHover}
277
+ onMouseDown={selectTab}
278
+ onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
279
+ >
280
+ <Text
281
+ fg={tab.disabled ? palette.disabledFg : active && variant === "pill" ? palette.activePillFg : active ? palette.activeFg : hovered ? palette.hoverFg : palette.inactiveFg}
282
+ attributes={attributes}
283
+ onMouseDown={selectTab}
284
+ >
285
+ {` ${tab.label} `}
286
+ </Text>
287
+ {showClose && (
288
+ <Text
289
+ fg={active && variant === "pill" ? palette.activePillFg : palette.closeFg}
290
+ onMouseDown={(event: any) => {
291
+ event.preventDefault?.();
292
+ event.stopPropagation?.();
293
+ tab.onClose?.(tab.value);
294
+ }}
295
+ >
296
+ {"x "}
297
+ </Text>
298
+ )}
299
+ </Box>
300
+ );
301
+ })}
302
+ {onAdd && (
303
+ <Box
304
+ width={addWidth}
305
+ height={1}
306
+ backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
307
+ onMouseMove={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
308
+ onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
309
+ onMouseDown={(event: TabPointerEvent) => {
310
+ event.preventDefault();
311
+ onAdd();
312
+ }}
313
+ >
314
+ <Text fg={hoveredValue === "__add__" ? palette.hoverFg : palette.addFg}>
315
+ {` ${addLabel} `}
316
+ </Text>
317
+ </Box>
318
+ )}
319
+ </Box>
320
+ );
321
+
322
+ if (!scrollable) return tabRow;
323
+
216
324
  return (
217
325
  <ScrollBox
218
326
  ref={scrollRef}
@@ -223,86 +331,7 @@ function OpenTuiTabs({
223
331
  horizontalScrollbarOptions={{ visible: false }}
224
332
  onMouseScroll={handleMouseScroll}
225
333
  >
226
- <Box flexDirection="row" width={totalWidth} height={1}>
227
- {tabs.map((tab, index) => {
228
- const active = tab.value === activeValue;
229
- const hovered = hoveredValue === tab.value && !tab.disabled;
230
- const tabWidth = tabWidths[index] ?? tab.label.length + 2;
231
- const showClose = !!tab.onClose && (closeMode === "always" || active);
232
- const attributes = (active ? TextAttributes.BOLD : 0)
233
- | (variant === "underline" && !compact && (active || hovered) ? TextAttributes.UNDERLINE : 0);
234
- const startHover = tab.disabled
235
- ? undefined
236
- : () => {
237
- setHoveredValue((current) => (current === tab.value ? current : tab.value));
238
- };
239
- const endHover = tab.disabled
240
- ? undefined
241
- : () => {
242
- setHoveredValue((current) => (current === tab.value ? null : current));
243
- };
244
-
245
- return (
246
- <Box
247
- key={tab.value}
248
- width={tabWidth}
249
- height={1}
250
- flexDirection="row"
251
- backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
252
- onMouseOver={startHover}
253
- onMouseMove={startHover}
254
- onMouseOut={endHover}
255
- onMouseDown={tab.disabled ? undefined : (event: TabPointerEvent) => {
256
- if (event.button === 2 && tab.onContextMenu) {
257
- event.preventDefault?.();
258
- event.stopPropagation?.();
259
- tab.onContextMenu(tab.value, event);
260
- return;
261
- }
262
- event.preventDefault();
263
- onSelect(tab.value);
264
- }}
265
- onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
266
- >
267
- <Text
268
- fg={tab.disabled ? palette.disabledFg : active && variant === "pill" ? palette.activePillFg : active ? palette.activeFg : hovered ? palette.hoverFg : palette.inactiveFg}
269
- attributes={attributes}
270
- >
271
- {` ${tab.label} `}
272
- </Text>
273
- {showClose && (
274
- <Text
275
- fg={active && variant === "pill" ? palette.activePillFg : palette.closeFg}
276
- onMouseDown={(event: any) => {
277
- event.preventDefault?.();
278
- event.stopPropagation?.();
279
- tab.onClose?.(tab.value);
280
- }}
281
- >
282
- {"x "}
283
- </Text>
284
- )}
285
- </Box>
286
- );
287
- })}
288
- {onAdd && (
289
- <Box
290
- width={addWidth}
291
- height={1}
292
- backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
293
- onMouseMove={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
294
- onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
295
- onMouseDown={(event: TabPointerEvent) => {
296
- event.preventDefault();
297
- onAdd();
298
- }}
299
- >
300
- <Text fg={hoveredValue === "__add__" ? palette.hoverFg : palette.addFg}>
301
- {` ${addLabel} `}
302
- </Text>
303
- </Box>
304
- )}
305
- </Box>
334
+ {tabRow}
306
335
  </ScrollBox>
307
336
  );
308
337
  }
@@ -151,6 +151,7 @@ function DataTableHorizontalScrollHarness({
151
151
  <DataTableView
152
152
  rootWidth={containerWidth}
153
153
  rootHeight={containerHeight}
154
+ selection={{ kind: "none" }}
154
155
  columns={columns}
155
156
  items={items}
156
157
  sortColumnId={null}
@@ -159,8 +160,6 @@ function DataTableHorizontalScrollHarness({
159
160
  headerScrollRef={headerScrollRef}
160
161
  scrollRef={scrollRef}
161
162
  getItemKey={(row) => row.id}
162
- isSelected={() => false}
163
- onSelect={() => {}}
164
163
  renderCell={(row) => ({ text: row.name })}
165
164
  emptyStateTitle="No rows."
166
165
  showHorizontalScrollbar={showHorizontalScrollbar}
@@ -228,7 +227,7 @@ function MultiSelectDialogButtonHarness() {
228
227
  );
229
228
  }
230
229
 
231
- function ChoiceDialogHarness() {
230
+ function ChoiceDialogHarness({ selectedChoiceId }: { selectedChoiceId?: string } = {}) {
232
231
  return (
233
232
  <ChoiceDialog
234
233
  title="Choose Account"
@@ -241,6 +240,7 @@ function ChoiceDialogHarness() {
241
240
  { id: "beta", label: "Beta", description: "Beta account" },
242
241
  { id: "gamma", label: "Gamma", description: "Gamma account" },
243
242
  ]}
243
+ selectedChoiceId={selectedChoiceId}
244
244
  />
245
245
  );
246
246
  }
@@ -318,6 +318,7 @@ describe("shared UI kit", () => {
318
318
 
319
319
  test("moves focused tabs with arrow keys and leaves Tab for pane focus", async () => {
320
320
  let lastSelected = "overview";
321
+ const selectedValues: string[] = [];
321
322
 
322
323
  function KeyboardTabsHarness() {
323
324
  const [activeValue, setActiveValue] = useState("overview");
@@ -331,6 +332,7 @@ describe("shared UI kit", () => {
331
332
  activeValue={activeValue}
332
333
  onSelect={(value) => {
333
334
  lastSelected = value;
335
+ selectedValues.push(value);
334
336
  setActiveValue(value);
335
337
  }}
336
338
  focused
@@ -343,17 +345,73 @@ describe("shared UI kit", () => {
343
345
  await act(async () => {
344
346
  await testSetup!.renderOnce();
345
347
  testSetup!.mockInput.pressArrow("right");
348
+ testSetup!.mockInput.pressArrow("right");
346
349
  await testSetup!.renderOnce();
347
350
  });
348
351
 
349
- expect(lastSelected).toBe("news");
352
+ expect(selectedValues).toEqual(["news", "chart"]);
353
+ expect(lastSelected).toBe("chart");
350
354
 
351
355
  await act(async () => {
352
356
  testSetup!.mockInput.pressTab();
353
357
  await testSetup!.renderOnce();
354
358
  });
355
359
 
360
+ expect(lastSelected).toBe("chart");
361
+ });
362
+
363
+ test("selects tabs by clicking their text labels", async () => {
364
+ let lastSelected = "overview";
365
+ const selectedValues: string[] = [];
366
+
367
+ function PointerTabsHarness() {
368
+ const [activeValue, setActiveValue] = useState("overview");
369
+ return (
370
+ <Tabs
371
+ tabs={[
372
+ { label: "Overview", value: "overview" },
373
+ { label: "News", value: "news" },
374
+ { label: "Chart", value: "chart" },
375
+ ]}
376
+ activeValue={activeValue}
377
+ onSelect={(value) => {
378
+ lastSelected = value;
379
+ selectedValues.push(value);
380
+ setActiveValue(value);
381
+ }}
382
+ scrollable={false}
383
+ />
384
+ );
385
+ }
386
+
387
+ testSetup = await testRender(<PointerTabsHarness />, { width: 40, height: 4 });
388
+
389
+ await act(async () => {
390
+ await testSetup!.renderOnce();
391
+ });
392
+
393
+ let frame = testSetup.captureCharFrame();
394
+ const newsCol = frame.split("\n")[0]!.indexOf("News");
395
+ expect(newsCol).toBeGreaterThanOrEqual(0);
396
+
397
+ await act(async () => {
398
+ await testSetup!.mockMouse.click(newsCol + 1, 0);
399
+ await testSetup!.renderOnce();
400
+ });
401
+
356
402
  expect(lastSelected).toBe("news");
403
+
404
+ frame = testSetup.captureCharFrame();
405
+ const chartCol = frame.split("\n")[0]!.indexOf("Chart");
406
+ expect(chartCol).toBeGreaterThanOrEqual(0);
407
+
408
+ await act(async () => {
409
+ await testSetup!.mockMouse.click(chartCol + 1, 0);
410
+ await testSetup!.renderOnce();
411
+ });
412
+
413
+ expect(selectedValues).toEqual(["news", "chart"]);
414
+ expect(lastSelected).toBe("chart");
357
415
  });
358
416
 
359
417
  test("renders tab actions for editable tab sets", async () => {
@@ -407,6 +465,15 @@ describe("shared UI kit", () => {
407
465
  expect(button).toBeDefined();
408
466
  expect(button!.width).toBe(" IND: SMA ".length);
409
467
 
468
+ await act(async () => {
469
+ await testSetup!.mockMouse.release(button!.x + 1, button!.y);
470
+ await Promise.resolve();
471
+ await testSetup!.renderOnce();
472
+ });
473
+
474
+ frame = testSetup.captureCharFrame();
475
+ expect(frame).not.toContain("Chart Indicators");
476
+
410
477
  await act(async () => {
411
478
  await testSetup!.mockMouse.click(button!.x + 1, button!.y);
412
479
  await Promise.resolve();
@@ -470,6 +537,18 @@ describe("shared UI kit", () => {
470
537
  expect(resolvedChoice).toBe("gamma");
471
538
  });
472
539
 
540
+ test("preselects the current choice in choice dialogs", async () => {
541
+ testSetup = await testRender(<ChoiceDialogHarness selectedChoiceId="beta" />, { width: 44, height: 10 });
542
+
543
+ await act(async () => {
544
+ await testSetup!.renderOnce();
545
+ });
546
+
547
+ const frame = testSetup.captureCharFrame();
548
+ expect(frame).toContain("Beta account");
549
+ expect(frame).not.toContain("Alpha account");
550
+ });
551
+
473
552
  test("cancels choice dialogs with escape", async () => {
474
553
  testSetup = await testRender(<ChoiceDialogHarness />, { width: 44, height: 10 });
475
554
 
@@ -26,10 +26,18 @@ export interface CollectionSortPreference {
26
26
  direction: SortDirection;
27
27
  }
28
28
 
29
- interface CommandBarLaunchRequest {
29
+ interface CommandBarLaunchRequestBase {
30
+ sequence: number;
31
+ }
32
+
33
+ interface CommandBarPluginLaunchRequest extends CommandBarLaunchRequestBase {
30
34
  kind: "plugin-command";
31
35
  commandId: string;
32
- sequence: number;
36
+ }
37
+
38
+ interface CommandBarTickerSearchLaunchRequest extends CommandBarLaunchRequestBase {
39
+ kind: "ticker-search";
40
+ query?: string;
33
41
  }
34
42
 
35
43
  export interface AppState {
@@ -44,7 +52,7 @@ export interface AppState {
44
52
  recentTickers: string[];
45
53
  commandBarOpen: boolean;
46
54
  commandBarQuery: string;
47
- commandBarLaunchRequest: CommandBarLaunchRequest | null;
55
+ commandBarLaunchRequest: CommandBarPluginLaunchRequest | CommandBarTickerSearchLaunchRequest | null;
48
56
  themePreview: string | null;
49
57
  refreshing: Set<string>;
50
58
  initialized: boolean;
@@ -74,7 +82,7 @@ export type AppAction =
74
82
  type: "SET_COMMAND_BAR";
75
83
  open: boolean;
76
84
  query?: string;
77
- launch?: { kind: "plugin-command"; commandId: string } | null;
85
+ launch?: { kind: "plugin-command"; commandId: string } | { kind: "ticker-search"; query?: string } | null;
78
86
  }
79
87
  | { type: "SET_COMMAND_BAR_QUERY"; query: string }
80
88
  | { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
@@ -1,4 +1,4 @@
1
- import type { DataProvider, SecFilingItem } from "../../types/data-provider";
1
+ import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
2
  import type { NewsArticle } from "../../news/types";
3
3
  import type { OptionsChain } from "../../types/financials";
4
4
  import type { NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
@@ -26,6 +26,7 @@ import {
26
26
  buildNewsKey,
27
27
  buildOptionsKey,
28
28
  buildSecContentKey,
29
+ buildSecDocumentsKey,
29
30
  buildSecFilingsKey,
30
31
  toMarketDataContext,
31
32
  } from "../selectors";
@@ -170,6 +171,33 @@ export function loadSecFilingContentEntry(options: {
170
171
  });
171
172
  }
172
173
 
174
+ export function loadSecFilingDocumentsEntry(options: {
175
+ dataProvider: DataProvider;
176
+ filing: SecFilingItem;
177
+ store: QueryStore<SecFilingDocument[]>;
178
+ runSingleFlight: RunSingleFlight;
179
+ }): Promise<QueryEntry<SecFilingDocument[]>> {
180
+ const { dataProvider, filing, store, runSingleFlight } = options;
181
+ const key = buildSecDocumentsKey(filing.accessionNumber);
182
+ return loadAuxiliaryEntry({
183
+ dataProvider,
184
+ key,
185
+ store,
186
+ ttlMs: SEC_CONTENT_CACHE_TTL_MS,
187
+ runSingleFlight,
188
+ load: async (startedAt) => {
189
+ if (!dataProvider.getSecFilingDocuments) {
190
+ const attempt = createAttempt(dataProvider.id, startedAt, "unsupported", "UNSUPPORTED_RANGE", "SEC filing documents are not available");
191
+ return store.update(key, (current) => errorEntry(current, attempt));
192
+ }
193
+ const data = await dataProvider.getSecFilingDocuments(filing);
194
+ const status = data.length > 0 ? "success" : "empty";
195
+ const attempts = [createAttempt(dataProvider.id, startedAt, status, data.length > 0 ? undefined : "NO_DATA")];
196
+ return store.update(key, (current) => readyEntry(current, data.length > 0 ? data : null, dataProvider.id, attempts, { keepLastGoodOnEmpty: true }));
197
+ },
198
+ });
199
+ }
200
+
173
201
  export function loadArticleSummaryEntry(options: {
174
202
  dataProvider: DataProvider;
175
203
  url: string;
@@ -1,4 +1,4 @@
1
- import type { DataProvider, SecFilingItem } from "../../types/data-provider";
1
+ import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
2
  import type { NewsArticle } from "../../news/types";
3
3
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
4
4
  import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
@@ -13,6 +13,7 @@ import {
13
13
  buildOptionsKey,
14
14
  buildQuoteKey,
15
15
  buildSecContentKey,
16
+ buildSecDocumentsKey,
16
17
  buildSecFilingsKey,
17
18
  buildSnapshotKey,
18
19
  resolveEntryData,
@@ -41,6 +42,7 @@ import {
41
42
  loadNewsEntry,
42
43
  loadOptionsEntry,
43
44
  loadSecFilingContentEntry,
45
+ loadSecFilingDocumentsEntry,
44
46
  loadSecFilingsEntry,
45
47
  } from "./auxiliary";
46
48
  import {
@@ -72,6 +74,7 @@ export class MarketDataCoordinator {
72
74
  private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.events.bump(key));
73
75
  private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.events.bump(key));
74
76
  private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.events.bump(key));
77
+ private readonly secDocumentsStore = new QueryStore<SecFilingDocument[]>((key) => this.events.bump(key));
75
78
  private readonly secContentStore = new QueryStore<string | null>((key) => this.events.bump(key));
76
79
  private readonly articleSummaryStore = new QueryStore<string | null>((key) => this.events.bump(key));
77
80
  private readonly fxStore = new QueryStore<number>((key) => this.events.bump(key));
@@ -135,6 +138,10 @@ export class MarketDataCoordinator {
135
138
  return this.secContentStore.get(buildSecContentKey(accessionNumber));
136
139
  }
137
140
 
141
+ getSecDocumentsEntry(accessionNumber: string): QueryEntry<SecFilingDocument[]> {
142
+ return this.secDocumentsStore.get(buildSecDocumentsKey(accessionNumber));
143
+ }
144
+
138
145
  getArticleSummaryEntry(url: string): QueryEntry<string | null> {
139
146
  return this.articleSummaryStore.get(buildArticleSummaryKey(url));
140
147
  }
@@ -336,6 +343,15 @@ export class MarketDataCoordinator {
336
343
  });
337
344
  }
338
345
 
346
+ async loadSecFilingDocuments(filing: SecFilingItem): Promise<QueryEntry<SecFilingDocument[]>> {
347
+ return loadSecFilingDocumentsEntry({
348
+ dataProvider: this.dataProvider,
349
+ filing,
350
+ store: this.secDocumentsStore,
351
+ runSingleFlight: (key, task) => this.runSingleFlight(key, task),
352
+ });
353
+ }
354
+
339
355
  async loadArticleSummary(url: string): Promise<QueryEntry<string | null>> {
340
356
  return loadArticleSummaryEntry({
341
357
  dataProvider: this.dataProvider,