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
@@ -62,7 +62,12 @@ export function useAppGlobalShortcuts({
62
62
  if (!isDetachedWindow && event.name === "`" && !state.commandBarOpen) {
63
63
  event.preventDefault();
64
64
  event.stopPropagation();
65
- dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
65
+ dispatch({
66
+ type: "SET_COMMAND_BAR",
67
+ open: true,
68
+ query: "",
69
+ launch: { kind: "ticker-search", query: "" },
70
+ });
66
71
  return;
67
72
  }
68
73
  if (!isDetachedWindow && /^[1-9]$/.test(event.name ?? "") && event.ctrl && (state.config.layouts ?? []).length > 1) {
@@ -96,18 +101,19 @@ export function useAppGlobalShortcuts({
96
101
 
97
102
  if (state.inputCaptured) return;
98
103
 
99
- if (!isDetachedWindow && event.name === "q") {
104
+ const hasShortcutModifier = event.ctrl || event.meta || event.super || event.alt;
105
+ if (!hasShortcutModifier && !isDetachedWindow && event.name === "q") {
100
106
  rendererHost.requestExit();
101
- } else if (event.name === "r") {
107
+ } else if (!hasShortcutModifier && event.name === "r") {
102
108
  if (focusedTickerSymbol) {
103
109
  const ticker = state.tickers.get(focusedTickerSymbol);
104
110
  if (ticker) refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 0);
105
111
  }
106
- } else if (event.name === "R" || (event.name === "r" && event.shift)) {
112
+ } else if (!hasShortcutModifier && (event.name === "R" || (event.name === "r" && event.shift))) {
107
113
  for (const ticker of state.tickers.values()) {
108
114
  refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 1);
109
115
  }
110
- } else if (event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
116
+ } else if (!hasShortcutModifier && event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
111
117
  startUpdate(state.updateAvailable);
112
118
  } else {
113
119
  const disabledPlugins = new Set(state.config.disabledPlugins || []);
@@ -52,6 +52,10 @@ export function assetDataProvider(provider: AssetDataProvider): AssetDataCapabil
52
52
  if (!provider.getEarningsCalendar) throw new Error(`${provider.name} does not provide earnings calendar.`);
53
53
  return provider.getEarningsCalendar(input.symbols ?? [], input.context);
54
54
  }, "query"),
55
+ getSecFilingDocuments: op((input: any) => {
56
+ if (!provider.getSecFilingDocuments) throw new Error(`${provider.name} does not provide SEC filing documents.`);
57
+ return provider.getSecFilingDocuments(input.filing);
58
+ }, "query"),
55
59
  getSecFilingContent: op((input: any) => {
56
60
  if (!provider.getSecFilingContent) throw new Error(`${provider.name} does not provide SEC filing content.`);
57
61
  return provider.getSecFilingContent(input.filing);
@@ -57,6 +57,60 @@ describe("projectComparisonChartData", () => {
57
57
  expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, null, 20, 22]);
58
58
  });
59
59
 
60
+ test("carries the latest series value across shared timestamps from other markets", () => {
61
+ const projection = projectComparisonChartData([
62
+ makeSeries("DAX", "EUR", [
63
+ ["2024-01-02T08:00:00.000Z", 100],
64
+ ["2024-01-02T08:15:00.000Z", 101],
65
+ ["2024-01-03T08:00:00.000Z", 102],
66
+ ]),
67
+ makeSeries("AAPL", "USD", [
68
+ ["2024-01-02T14:30:00.000Z", 200],
69
+ ["2024-01-02T14:45:00.000Z", 201],
70
+ ]),
71
+ ], 8, {
72
+ panOffset: 0,
73
+ zoomLevel: 1,
74
+ renderMode: "line",
75
+ }, "price");
76
+
77
+ expect(projection.dates.map((date) => date.toISOString())).toEqual([
78
+ "2024-01-02T08:00:00.000Z",
79
+ "2024-01-02T08:15:00.000Z",
80
+ "2024-01-02T14:30:00.000Z",
81
+ "2024-01-02T14:45:00.000Z",
82
+ "2024-01-03T08:00:00.000Z",
83
+ ]);
84
+ expect(projection.series[0]?.points.map((point) => point.rawValue)).toEqual([100, 101, 101, 101, 102]);
85
+ expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, null, 200, 201, 201]);
86
+ });
87
+
88
+ test("uses the same alternating close bucket selection as stock chart projection", () => {
89
+ const projection = projectComparisonChartData([
90
+ makeSeries("DAX", "EUR", [
91
+ ["2024-01-02T08:00:00.000Z", 100],
92
+ ["2024-01-02T08:15:00.000Z", 105],
93
+ ["2024-01-02T14:30:00.000Z", 101],
94
+ ["2024-01-02T14:45:00.000Z", 99],
95
+ ]),
96
+ makeSeries("AAPL", "USD", [
97
+ ["2024-01-02T14:30:00.000Z", 200],
98
+ ["2024-01-02T14:45:00.000Z", 201],
99
+ ]),
100
+ ], 2, {
101
+ panOffset: 0,
102
+ zoomLevel: 1,
103
+ renderMode: "line",
104
+ }, "price");
105
+
106
+ expect(projection.dates.map((date) => date.toISOString())).toEqual([
107
+ "2024-01-02T08:15:00.000Z",
108
+ "2024-01-02T14:45:00.000Z",
109
+ ]);
110
+ expect(projection.series[0]?.points.map((point) => point.rawValue)).toEqual([105, 99]);
111
+ expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, 200]);
112
+ });
113
+
60
114
  test("normalizes percent mode from each series first visible point", () => {
61
115
  const projection = projectComparisonChartData([
62
116
  makeSeries("AAPL", "USD", [
@@ -7,6 +7,11 @@ import type {
7
7
  } from "../core/types";
8
8
  import { buildVisibleDateWindowFromRange } from "../core/date-window";
9
9
  import { getVisiblePointCount } from "../core/viewport";
10
+ import {
11
+ buildIndexProjectionBuckets,
12
+ selectRepresentativeCloseValue,
13
+ type IndexProjectionBucket,
14
+ } from "../core/projection";
10
15
 
11
16
  export interface ComparisonProjectedPoint {
12
17
  date: Date;
@@ -38,6 +43,14 @@ interface ComparisonAxisModeResolution {
38
43
  warning: string | null;
39
44
  }
40
45
 
46
+ type ComparisonWindowViewState =
47
+ Pick<ComparisonChartViewState, "panOffset" | "zoomLevel">
48
+ & Partial<Pick<ComparisonChartViewState, "dateWindow">>;
49
+
50
+ type ComparisonProjectionViewState =
51
+ ComparisonWindowViewState
52
+ & Pick<ComparisonChartViewState, "renderMode">;
53
+
41
54
  export interface ComparisonChartProjection {
42
55
  dates: Date[];
43
56
  series: ComparisonProjectedSeries[];
@@ -92,7 +105,7 @@ export function getMaxComparisonPanOffset(
92
105
 
93
106
  export function getVisibleComparisonWindow(
94
107
  series: ComparisonChartSeries[],
95
- viewState: Pick<ComparisonChartViewState, "dateWindow" | "panOffset" | "zoomLevel">,
108
+ viewState: ComparisonWindowViewState,
96
109
  ): ComparisonVisibleWindow {
97
110
  const dates = getUniqueSortedDates(series);
98
111
 
@@ -138,10 +151,7 @@ function transformRawValue(rawValue: number | null, axisMode: ChartAxisMode, bas
138
151
  }
139
152
 
140
153
  function resolveRepresentativeValue(values: number[], index: number): number {
141
- if (values.length === 1) return values[0]!;
142
- return index % 2 === 0
143
- ? Math.max(...values)
144
- : Math.min(...values);
154
+ return selectRepresentativeCloseValue(values, index, (value) => value);
145
155
  }
146
156
 
147
157
  function buildSeriesMap(points: PricePoint[]): Map<number, number> {
@@ -155,6 +165,28 @@ function buildSeriesMap(points: PricePoint[]): Map<number, number> {
155
165
  return map;
156
166
  }
157
167
 
168
+ function buildFilledRawValuesByDate(
169
+ rawByDate: ReadonlyMap<number, number>,
170
+ dates: readonly Date[],
171
+ ): Array<number | null> {
172
+ let latestRawValue: number | null = null;
173
+ return dates.map((date) => {
174
+ const timestamp = date.getTime();
175
+ if (rawByDate.has(timestamp)) {
176
+ latestRawValue = rawByDate.get(timestamp)!;
177
+ return latestRawValue;
178
+ }
179
+ return latestRawValue;
180
+ });
181
+ }
182
+
183
+ function getBucketDate(
184
+ dates: readonly Date[],
185
+ bucket: IndexProjectionBucket,
186
+ ): Date {
187
+ return dates[Math.max(bucket.end - 1, 0)] ?? dates[dates.length - 1] ?? new Date(0);
188
+ }
189
+
158
190
  function resolveComparisonAxisMode(
159
191
  requestedAxisMode: ChartAxisMode,
160
192
  series: ComparisonChartSeries[],
@@ -194,7 +226,7 @@ function resolveComparisonAxisMode(
194
226
  export function projectComparisonChartData(
195
227
  series: ComparisonChartSeries[],
196
228
  chartWidth: number,
197
- viewState: Pick<ComparisonChartViewState, "dateWindow" | "panOffset" | "zoomLevel" | "renderMode">,
229
+ viewState: ComparisonProjectionViewState,
198
230
  requestedAxisMode: ChartAxisMode,
199
231
  ): ComparisonChartProjection {
200
232
  const normalizedSeries = series.map((entry) => ({ ...entry, points: normalizeSeriesPoints(entry.points) }));
@@ -221,31 +253,26 @@ export function projectComparisonChartData(
221
253
  warning: axisResolution.warning,
222
254
  };
223
255
  }
224
- const bucketCount = Math.min(Math.max(chartWidth, 1), Math.max(window.dates.length, 1));
225
- const bucketSize = window.dates.length > 0 ? window.dates.length / bucketCount : 1;
256
+ const buckets = buildIndexProjectionBuckets(window.dates.length, chartWidth);
226
257
 
227
258
  const projectedSeries = normalizedSeries.map((entry) => {
228
259
  const rawByDate = buildSeriesMap(entry.points);
229
- const visibleRawValues = window.dates
230
- .map((date) => rawByDate.get(date.getTime()) ?? null)
260
+ const filledRawValues = buildFilledRawValuesByDate(rawByDate, window.dates);
261
+ const visibleRawValues = filledRawValues
231
262
  .filter((value): value is number => value !== null);
232
263
  const baseValue = visibleRawValues[0] ?? null;
233
264
  const latestRawValue = visibleRawValues[visibleRawValues.length - 1] ?? null;
234
265
 
235
- const points = Array.from({ length: bucketCount }, (_, index) => {
236
- const start = Math.floor(index * bucketSize);
237
- const end = Math.min(window.dates.length, Math.max(Math.floor((index + 1) * bucketSize), start + 1));
238
- const bucketDates = window.dates.slice(start, end);
239
- const date = bucketDates[bucketDates.length - 1] ?? window.dates[window.dates.length - 1] ?? new Date(0);
240
- const bucketRawValues = bucketDates
241
- .map((bucketDate) => rawByDate.get(bucketDate.getTime()) ?? null)
266
+ const points = buckets.map((bucket) => {
267
+ const bucketRawValues = filledRawValues
268
+ .slice(bucket.start, bucket.end)
242
269
  .filter((value): value is number => value !== null);
243
270
  const rawValue = bucketRawValues.length > 0
244
- ? resolveRepresentativeValue(bucketRawValues, index)
271
+ ? resolveRepresentativeValue(bucketRawValues, bucket.index)
245
272
  : null;
246
273
 
247
274
  return {
248
- date,
275
+ date: getBucketDate(window.dates, bucket),
249
276
  rawValue,
250
277
  value: transformRawValue(rawValue, axisResolution.effectiveAxisMode, baseValue),
251
278
  };
@@ -264,11 +291,7 @@ export function projectComparisonChartData(
264
291
  });
265
292
 
266
293
  return {
267
- dates: Array.from({ length: bucketCount }, (_, index) => {
268
- const start = Math.floor(index * bucketSize);
269
- const end = Math.min(window.dates.length, Math.max(Math.floor((index + 1) * bucketSize), start + 1));
270
- return window.dates[Math.max(end - 1, 0)] ?? new Date(0);
271
- }),
294
+ dates: buckets.map((bucket) => getBucketDate(window.dates, bucket)),
272
295
  series: projectedSeries,
273
296
  requestedMode,
274
297
  effectiveMode: requestedMode,
@@ -24,6 +24,12 @@ export interface ProjectChartDataOptions {
24
24
  sourceIndexOffset?: number;
25
25
  }
26
26
 
27
+ export interface IndexProjectionBucket {
28
+ index: number;
29
+ start: number;
30
+ end: number;
31
+ }
32
+
27
33
  interface StableOhlcProjectionOptionsInput {
28
34
  pointCount: number;
29
35
  sourceIndexOffset: number;
@@ -131,6 +137,44 @@ function aggregateOhlcBucket(bucket: readonly ProjectedChartPoint[]): ProjectedC
131
137
  };
132
138
  }
133
139
 
140
+ export function buildIndexProjectionBuckets(
141
+ itemCount: number,
142
+ targetWidth: number,
143
+ ): IndexProjectionBucket[] {
144
+ const normalizedItemCount = Math.max(Math.floor(itemCount), 0);
145
+ if (normalizedItemCount === 0) return [];
146
+
147
+ const bucketCount = Math.min(normalizedItemCount, Math.max(Math.floor(targetWidth), 1));
148
+ const bucketSize = normalizedItemCount / bucketCount;
149
+
150
+ return Array.from({ length: bucketCount }, (_, index) => {
151
+ const start = Math.floor(index * bucketSize);
152
+ const end = Math.min(
153
+ normalizedItemCount,
154
+ Math.max(Math.floor((index + 1) * bucketSize), start + 1),
155
+ );
156
+ return { index, start, end };
157
+ });
158
+ }
159
+
160
+ export function selectRepresentativeCloseValue<T>(
161
+ bucket: readonly T[],
162
+ index: number,
163
+ getClose: (item: T) => number,
164
+ ): T {
165
+ let best = bucket[0] as T;
166
+ if (index % 2 === 0) {
167
+ for (const item of bucket) {
168
+ if (getClose(item) > getClose(best)) best = item;
169
+ }
170
+ } else {
171
+ for (const item of bucket) {
172
+ if (getClose(item) < getClose(best)) best = item;
173
+ }
174
+ }
175
+ return best;
176
+ }
177
+
134
178
  function bucketOhlcBySourceIndex(
135
179
  normalizedPoints: readonly ProjectedChartPoint[],
136
180
  bucketSize: number,
@@ -202,33 +246,13 @@ function projectCloseSeries(
202
246
  targetWidth: number,
203
247
  ): ProjectedChartPoint[] {
204
248
  if (points.length === 0) return [];
205
- if (points.length <= targetWidth) return points.map(normalizePoint);
206
-
207
- const result: ProjectedChartPoint[] = [];
208
- const bucketSize = points.length / targetWidth;
209
-
210
- for (let i = 0; i < targetWidth; i++) {
211
- const start = Math.floor(i * bucketSize);
212
- const end = Math.min(points.length, Math.max(Math.floor((i + 1) * bucketSize), start + 1));
213
- const bucket = points.slice(start, end);
214
-
215
- if (bucket.length === 0) continue;
216
-
217
- // Alternate bucket extremes to preserve line/area shape after downsampling.
218
- let best = bucket[0]!;
219
- if (i % 2 === 0) {
220
- for (const point of bucket) {
221
- if (point.close > best.close) best = point;
222
- }
223
- } else {
224
- for (const point of bucket) {
225
- if (point.close < best.close) best = point;
226
- }
227
- }
228
- result.push(normalizePoint(best));
229
- }
230
-
231
- return result;
249
+ return buildIndexProjectionBuckets(points.length, targetWidth).map((bucket) => (
250
+ normalizePoint(selectRepresentativeCloseValue(
251
+ points.slice(bucket.start, bucket.end),
252
+ bucket.index,
253
+ (point) => point.close,
254
+ ))
255
+ ));
232
256
  }
233
257
 
234
258
  /**
@@ -229,6 +229,7 @@ export function useCommandBarActionRuntime({
229
229
  activeTickerSymbol,
230
230
  commandBarLaunchRequest: state.commandBarLaunchRequest,
231
231
  commandBarOpen: state.commandBarOpen,
232
+ openModeRoute,
232
233
  openPluginCommandWorkflow,
233
234
  pluginRegistry,
234
235
  });
@@ -0,0 +1,115 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createInitialState, type AppAction, type AppState } from "../../../state/app/context";
3
+ import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../../types/config";
4
+ import type { PluginRegistry } from "../../../plugins/registry";
5
+ import type { CommandBarRoute } from "../workflow/types";
6
+ import type { LayoutItemsContext } from "./types";
7
+ import { buildCurrentLayoutItems } from "./current-layout";
8
+
9
+ type InlineConfirmOptions = Parameters<LayoutItemsContext["openInlineConfirm"]>[0];
10
+
11
+ function createFloatingLayoutFixture(): { layout: LayoutConfig; state: AppState } {
12
+ const config = createDefaultConfig("/tmp/gloomberb-layout-actions-test");
13
+ const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
14
+ const firstDetailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
15
+ if (!mainPane || !firstDetailPane) throw new Error("missing default panes");
16
+
17
+ const secondDetailPane = {
18
+ ...firstDetailPane,
19
+ instanceId: "ticker-detail:secondary",
20
+ };
21
+ const layout: LayoutConfig = {
22
+ dockRoot: { kind: "pane", instanceId: "portfolio-list:main" },
23
+ instances: [{ ...mainPane }, { ...firstDetailPane }, secondDetailPane],
24
+ floating: [
25
+ { instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 },
26
+ { instanceId: "ticker-detail:secondary", x: 8, y: 3, width: 30, height: 8 },
27
+ ],
28
+ detached: [],
29
+ };
30
+ const state = createInitialState({
31
+ ...config,
32
+ layout: cloneLayout(layout),
33
+ layouts: [{ name: "Default", layout: cloneLayout(layout) }],
34
+ });
35
+ return { layout, state };
36
+ }
37
+
38
+ function createLayoutItemsContext(
39
+ options: {
40
+ confirmDangerousActions?: boolean;
41
+ layouts: LayoutConfig[];
42
+ confirmations: InlineConfirmOptions[];
43
+ },
44
+ ): LayoutItemsContext {
45
+ const { layout, state } = createFloatingLayoutFixture();
46
+ return {
47
+ closeAll: () => {},
48
+ currentLayout: layout,
49
+ dispatch: (_action: AppAction) => {},
50
+ duplicatePane: () => {},
51
+ focusedPaneId: state.focusedPaneId,
52
+ notifyGridlockRevert: () => {},
53
+ openBuiltInWorkflow: () => {},
54
+ openInlineConfirm: (confirmOptions) => {
55
+ options.confirmations.push(confirmOptions);
56
+ },
57
+ persistLayoutChange: (nextLayout) => {
58
+ options.layouts.push(nextLayout);
59
+ },
60
+ pluginRegistry: {
61
+ getTermSizeFn: () => ({ width: 80, height: 24 }),
62
+ } as PluginRegistry,
63
+ pushRoute: (_route: CommandBarRoute) => {},
64
+ state,
65
+ ...(options.confirmDangerousActions === undefined ? {} : { confirmDangerousActions: options.confirmDangerousActions }),
66
+ };
67
+ }
68
+
69
+ describe("buildCurrentLayoutItems", () => {
70
+ test("closes all floating panes from the current layout", () => {
71
+ const layouts: LayoutConfig[] = [];
72
+ const confirmations: InlineConfirmOptions[] = [];
73
+ const items = buildCurrentLayoutItems(createLayoutItemsContext({ layouts, confirmations }));
74
+
75
+ const item = items.find((entry) => entry.id === "layout-close-all-floating");
76
+ expect(item).toMatchObject({
77
+ label: "Close All Floating Panes",
78
+ disabled: false,
79
+ });
80
+
81
+ item?.action();
82
+
83
+ expect(confirmations).toEqual([]);
84
+ expect(layouts).toHaveLength(1);
85
+ expect(layouts[0]?.floating).toEqual([]);
86
+ expect(layouts[0]?.instances.map((instance) => instance.instanceId)).toEqual(["portfolio-list:main"]);
87
+ expect(layouts[0]?.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
88
+ });
89
+
90
+ test("confirms before closing all floating panes when dangerous actions are guarded", () => {
91
+ const layouts: LayoutConfig[] = [];
92
+ const confirmations: InlineConfirmOptions[] = [];
93
+ const items = buildCurrentLayoutItems(createLayoutItemsContext({
94
+ confirmDangerousActions: true,
95
+ layouts,
96
+ confirmations,
97
+ }));
98
+
99
+ items.find((entry) => entry.id === "layout-close-all-floating")?.action();
100
+
101
+ expect(layouts).toEqual([]);
102
+ expect(confirmations).toHaveLength(1);
103
+ expect(confirmations[0]).toMatchObject({
104
+ confirmId: "layout-close-all-floating",
105
+ title: "Close All Floating Panes",
106
+ confirmLabel: "Close Floating Panes",
107
+ tone: "danger",
108
+ });
109
+
110
+ confirmations[0]?.onConfirm();
111
+
112
+ expect(layouts).toHaveLength(1);
113
+ expect(layouts[0]?.floating).toEqual([]);
114
+ });
115
+ });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  gridlockAllPanes,
3
+ removeFloatingPanes,
3
4
  } from "../../../plugins/pane-manager";
4
5
  import {
5
6
  DEFAULT_LAYOUT,
@@ -21,6 +22,8 @@ export function buildCurrentLayoutItems({
21
22
  state,
22
23
  }: LayoutItemsContext): ResultItem[] {
23
24
  const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
25
+ const floatingPaneCount = currentLayout.floating.length;
26
+ const floatingPaneLabel = floatingPaneCount === 1 ? "floating pane" : "floating panes";
24
27
 
25
28
  return [
26
29
  {
@@ -89,6 +92,36 @@ export function buildCurrentLayoutItems({
89
92
  closeAll({ revertThemePreview: false });
90
93
  },
91
94
  },
95
+ {
96
+ id: "layout-close-all-floating",
97
+ label: "Close All Floating Panes",
98
+ detail: floatingPaneCount > 0
99
+ ? `Remove ${floatingPaneCount} ${floatingPaneLabel} from the current layout`
100
+ : "No floating panes in the current layout",
101
+ category: "Current Layout",
102
+ kind: "action",
103
+ disabled: floatingPaneCount === 0,
104
+ action: confirmDangerousActions
105
+ ? () => {
106
+ if (floatingPaneCount === 0) return;
107
+ openInlineConfirm({
108
+ confirmId: "layout-close-all-floating",
109
+ title: "Close All Floating Panes",
110
+ body: [`Close ${floatingPaneCount} ${floatingPaneLabel}?`],
111
+ confirmLabel: "Close Floating Panes",
112
+ cancelLabel: "Back",
113
+ tone: "danger",
114
+ onConfirm: () => {
115
+ persistLayoutChange(removeFloatingPanes(currentLayout));
116
+ },
117
+ });
118
+ }
119
+ : () => {
120
+ if (floatingPaneCount === 0) return;
121
+ persistLayoutChange(removeFloatingPanes(currentLayout));
122
+ closeAll({ revertThemePreview: false });
123
+ },
124
+ },
92
125
  {
93
126
  id: "layout-rename",
94
127
  label: "Rename Layout",
@@ -19,6 +19,7 @@ export type CommandBarListScrollEvent = {
19
19
 
20
20
  interface CommandBarListHeaderProps {
21
21
  kind: ListScreenState["kind"];
22
+ title: string;
22
23
  query: string;
23
24
  queryDisplayWidth: number;
24
25
  nativePaneChrome: boolean;
@@ -36,6 +37,7 @@ interface CommandBarListHeaderProps {
36
37
 
37
38
  export const CommandBarListHeader = memo(function CommandBarListHeader({
38
39
  kind,
40
+ title,
39
41
  query,
40
42
  queryDisplayWidth,
41
43
  nativePaneChrome,
@@ -66,7 +68,7 @@ export const CommandBarListHeader = memo(function CommandBarListHeader({
66
68
  value={query}
67
69
  onInput={onQueryChange}
68
70
  onChange={onQueryChange}
69
- placeholder={kind === "root" ? "Search" : "Filter"}
71
+ placeholder={kind === "root" ? "Search commands" : title === "Security Description" ? "Search tickers" : "Filter"}
70
72
  focused
71
73
  width={nativePaneChrome ? "100%" : queryDisplayWidth}
72
74
  backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
@@ -141,7 +141,10 @@ export function buildPaneTemplateItem(options: {
141
141
  const arg = options.createOptions?.arg;
142
142
 
143
143
  const action = () => {
144
- if (options.shortcutExecution && options.template.shortcut) {
144
+ if (
145
+ options.template.shortcut
146
+ && (options.shortcutExecution || getPaneTemplateArgKind(options.template) === "ticker")
147
+ ) {
145
148
  options.runPaneTemplateShortcut(options.template, arg);
146
149
  return;
147
150
  }
@@ -130,6 +130,7 @@ export function CommandBarPanelBody({
130
130
  {(visibleListState || currentRoute?.kind === "picker") && visibleListState && (
131
131
  <CommandBarListHeader
132
132
  kind={visibleListState.kind}
133
+ title={visibleListState.title}
133
134
  query={visibleListState.query}
134
135
  queryDisplayWidth={queryDisplayWidth}
135
136
  nativePaneChrome={nativePaneChrome}
@@ -6,12 +6,10 @@ import type {
6
6
  } from "../../../../types/plugin";
7
7
  import type { TickerRecord } from "../../../../types/ticker";
8
8
  import { fuzzyFilter } from "../../../../utils/fuzzy-search";
9
- import type { TickerSearchCandidate } from "../../../../tickers/search";
10
9
  import { matchPrefix, type Command } from "../../commands/registry";
11
10
  import { isCollectionCommand } from "../../helpers";
12
11
  import type { ResultItem } from "../../list/model";
13
12
  import type { parseRootShortcutIntent } from "./shortcuts";
14
- import { createQuickLookTickerCandidates } from "../ticker-search/results";
15
13
  import type { CommandBarRoute } from "../../workflow/types";
16
14
  import { createRootCommandItemBuilder } from "./command-items";
17
15
  import { buildRootShortcutItem } from "./shortcut-items";
@@ -55,7 +53,6 @@ export interface RootResultModelOptions {
55
53
  getAvailablePaneShortcutTemplates: (query: string) => PaneTemplateDef[];
56
54
  hasPaneSettings: (paneId: string) => boolean;
57
55
  localTickerSearchResultItems: (query?: string, options?: { category?: string; limit?: number }) => ResultItem[];
58
- mapTickerSearchCandidateToResultItem: (candidate: TickerSearchCandidate) => ResultItem;
59
56
  nonShortcutPaneTemplateItems: (filterQuery?: string) => ResultItem[];
60
57
  openModeRoute: (screen: "ticker-search" | "plugins" | "layout", initialQuery?: string) => void;
61
58
  paneShortcutItems: (options?: PaneShortcutItemsOptions) => ResultItem[];
@@ -86,7 +83,6 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
86
83
  getAvailablePaneShortcutTemplates,
87
84
  hasPaneSettings,
88
85
  localTickerSearchResultItems,
89
- mapTickerSearchCandidateToResultItem,
90
86
  nonShortcutPaneTemplateItems,
91
87
  openModeRoute,
92
88
  paneShortcutItems,
@@ -180,30 +176,6 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
180
176
  const item = commandToItem(match.command);
181
177
  if (item) items.push(item);
182
178
  } else if (!rootQuery) {
183
- const maxDefaultTickers = 5;
184
- const recentSymbols = state.recentTickers.slice(0, maxDefaultTickers);
185
- const recentTickers = recentSymbols
186
- .map((symbol) => state.tickers.get(symbol))
187
- .filter((ticker): ticker is NonNullable<typeof ticker> => (
188
- ticker != null && createQuickLookTickerCandidates([ticker]).length > 0
189
- ));
190
- if (recentTickers.length < maxDefaultTickers) {
191
- const seen = new Set(recentSymbols);
192
- for (const ticker of state.tickers.values()) {
193
- if (recentTickers.length >= maxDefaultTickers) break;
194
- if (createQuickLookTickerCandidates([ticker]).length === 0) continue;
195
- if (!seen.has(ticker.metadata.ticker)) recentTickers.push(ticker);
196
- }
197
- }
198
- items.push(...recentTickers.flatMap((ticker) => {
199
- const candidate = createQuickLookTickerCandidates([ticker])[0];
200
- return candidate
201
- ? [{
202
- ...mapTickerSearchCandidateToResultItem(candidate),
203
- category: "Tickers",
204
- }]
205
- : [];
206
- }));
207
179
  items.push(...paneShortcutItems());
208
180
  for (const command of availableCommands) {
209
181
  const item = commandToItem(command);
@@ -212,12 +184,10 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
212
184
  items.push(...tickerActionItems());
213
185
  items.push(...pluginCommandItems());
214
186
  } else {
215
- const tickerItems = localTickerSearchResultItems(undefined, { category: "Tickers" });
216
187
  const commandItems = availableCommands
217
188
  .map((command) => commandToItem(command))
218
189
  .filter((item): item is ResultItem => item !== null);
219
190
  const allItems = [
220
- ...tickerItems,
221
191
  ...commandItems,
222
192
  ...buildLayoutItems("", { confirmDangerousActions: true }),
223
193
  ...buildPaneSettingItems(state.focusedPaneId, rootQuery),