gloomberb 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +10 -9
  2. package/README.zh-CN.md +6 -6
  3. package/package.json +1 -1
  4. package/src/api-client/data.ts +7 -0
  5. package/src/api-client/index.test.ts +47 -0
  6. package/src/api-client/index.ts +61 -0
  7. package/src/api-client/paths.ts +16 -0
  8. package/src/api-client/types.ts +35 -0
  9. package/src/app/pane-runtime/index.tsx +4 -1
  10. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  11. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  12. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  13. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  14. package/src/app/runtime/desktop-deeplink.ts +66 -2
  15. package/src/app/runtime/desktop-menu.ts +3 -0
  16. package/src/app/runtime/plugin-bindings.ts +30 -28
  17. package/src/app.tsx +9 -2
  18. package/src/cli/commands/automation.test.ts +9 -0
  19. package/src/cli/commands/automation.ts +22 -73
  20. package/src/cli/index.ts +0 -2
  21. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  22. package/src/components/chart/composite/composite-chart.tsx +12 -9
  23. package/src/components/chart/composite/scene.ts +0 -21
  24. package/src/components/chart/composite/tools.ts +49 -0
  25. package/src/components/command-bar/commands/direct/index.ts +4 -0
  26. package/src/components/command-bar/commands/registry.ts +8 -1
  27. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  28. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  29. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  30. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  31. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  32. package/src/components/command-bar/surface/index.test.tsx +6 -6
  33. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  34. package/src/components/command-bar/view-model.test.ts +2 -1
  35. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  36. package/src/components/command-bar/workflow/ops.ts +1 -1
  37. package/src/components/layout/detached-pane-shell.tsx +74 -10
  38. package/src/components/layout/pane/title.ts +31 -3
  39. package/src/components/layout/shell/active-drag.ts +4 -11
  40. package/src/components/layout/shell/drag/index.ts +4 -20
  41. package/src/components/layout/shell/drag/runtime.ts +3 -4
  42. package/src/components/layout/shell/index.test.tsx +17 -3
  43. package/src/components/layout/shell/index.tsx +86 -30
  44. package/src/components/layout/shell/menu.test.ts +37 -1
  45. package/src/components/layout/shell/menu.ts +14 -0
  46. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  47. package/src/components/layout/shell/pane/actions.ts +0 -5
  48. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  49. package/src/components/layout/shell/shortcuts.ts +6 -3
  50. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  51. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  52. package/src/components/layout/status-bar.test.tsx +62 -61
  53. package/src/components/layout/status-bar.tsx +53 -75
  54. package/src/core/state/app/layout-reducer.ts +36 -0
  55. package/src/core/state/app/layout.ts +10 -5
  56. package/src/core/state/app/state.ts +0 -13
  57. package/src/core/state/app/types.ts +6 -4
  58. package/src/data/config/chart-settings.ts +1 -1
  59. package/src/data/config/layout.ts +2 -0
  60. package/src/data/config/store/index.test.ts +27 -6
  61. package/src/data/config/store/normalize.ts +3 -5
  62. package/src/data/plugin-state-store.ts +11 -0
  63. package/src/data/sqlite/cache.test.ts +10 -0
  64. package/src/i18n/es.ts +3 -4
  65. package/src/i18n/ja.ts +3 -4
  66. package/src/i18n/ko.ts +3 -4
  67. package/src/i18n/zh-cn.ts +3 -4
  68. package/src/i18n/zh-tw.ts +3 -4
  69. package/src/layout-marketplace/api.test.ts +54 -0
  70. package/src/layout-marketplace/api.ts +65 -0
  71. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  72. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  73. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  74. package/src/layout-marketplace/gallery.test.tsx +84 -0
  75. package/src/layout-marketplace/gallery.tsx +323 -0
  76. package/src/layout-marketplace/geometry.test.ts +47 -0
  77. package/src/layout-marketplace/geometry.ts +80 -0
  78. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  79. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  80. package/src/layout-marketplace/model.test.ts +118 -0
  81. package/src/layout-marketplace/model.ts +171 -0
  82. package/src/layout-marketplace/name-dialog.tsx +60 -0
  83. package/src/layout-marketplace/pane-imagery.ts +85 -0
  84. package/src/layout-marketplace/pane.tsx +20 -0
  85. package/src/layout-marketplace/payload.test.ts +246 -0
  86. package/src/layout-marketplace/payload.ts +624 -0
  87. package/src/layout-marketplace/use-marketplace.ts +63 -0
  88. package/src/market-data/hooks.tsx +0 -41
  89. package/src/plugins/broker-sync/broker-sync.test.ts +29 -0
  90. package/src/plugins/broker-sync/normalize.ts +9 -2
  91. package/src/plugins/builtin/ai/index.tsx +18 -2
  92. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  93. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  94. package/src/plugins/builtin/alerts/alert-engine.test.ts +16 -1
  95. package/src/plugins/builtin/alerts/alert-engine.ts +4 -0
  96. package/src/plugins/builtin/alerts/index.tsx +4 -0
  97. package/src/plugins/builtin/alerts/pane.tsx +2 -8
  98. package/src/plugins/builtin/analytics/index.tsx +3 -0
  99. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  100. package/src/plugins/builtin/cds/client.test.ts +83 -0
  101. package/src/plugins/builtin/cds/client.ts +67 -0
  102. package/src/plugins/builtin/cds/index.tsx +41 -0
  103. package/src/plugins/builtin/cds/model.test.ts +232 -0
  104. package/src/plugins/builtin/cds/model.ts +401 -0
  105. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  106. package/src/plugins/builtin/cds/pane.tsx +358 -0
  107. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  108. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  109. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  110. package/src/plugins/builtin/chart-composer/pane.tsx +81 -14
  111. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  112. package/src/plugins/builtin/chat/controller/index.ts +8 -1
  113. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  114. package/src/plugins/builtin/chat/controller/session-runtime.ts +14 -2
  115. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  116. package/src/plugins/builtin/chat/controller.test.ts +64 -14
  117. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  118. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  119. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  120. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  121. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  122. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  123. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  124. package/src/plugins/builtin/cloud/auth-commands.ts +0 -4
  125. package/src/plugins/builtin/cloud/auth-model.ts +4 -1
  126. package/src/plugins/builtin/cloud/plugin.tsx +11 -1
  127. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  128. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  129. package/src/plugins/builtin/cloud-tweets/registration.ts +44 -5
  130. package/src/plugins/builtin/composite-plugins.ts +3 -1
  131. package/src/plugins/builtin/connections/index.ts +3 -0
  132. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  133. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  134. package/src/plugins/builtin/help/index.tsx +2 -2
  135. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  136. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  137. package/src/plugins/builtin/news/index.tsx +1 -0
  138. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  139. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  140. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  141. package/src/plugins/builtin/options/index.tsx +1 -0
  142. package/src/plugins/builtin/options/view.test.tsx +25 -3
  143. package/src/plugins/builtin/options/view.tsx +1 -0
  144. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +2 -0
  145. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  146. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  147. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  148. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  149. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  150. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  151. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  152. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  153. package/src/plugins/builtin/portfolio-list/summary/index.tsx +21 -16
  154. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  155. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +33 -7
  156. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +109 -12
  157. package/src/plugins/builtin/research/index.tsx +4 -0
  158. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  159. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  160. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  161. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  162. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  163. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  164. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  165. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  166. package/src/plugins/catalog-browser.test.ts +10 -0
  167. package/src/plugins/catalog-browser.ts +3 -1
  168. package/src/plugins/gridlock-notification.ts +2 -1
  169. package/src/plugins/pane-manager/gridlock.ts +87 -3
  170. package/src/plugins/pane-manager.test.ts +30 -0
  171. package/src/plugins/pane-manager.ts +6 -1
  172. package/src/plugins/prediction-markets/chart.tsx +2 -2
  173. package/src/plugins/prediction-markets/controller/index.ts +5 -1
  174. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  175. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  176. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  177. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  178. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  179. package/src/plugins/prediction-markets/index.tsx +8 -4
  180. package/src/plugins/prediction-markets/navigation.ts +0 -1
  181. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  182. package/src/plugins/prediction-markets/pane.tsx +4 -0
  183. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  184. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  185. package/src/plugins/registry/index.ts +2 -0
  186. package/src/plugins/ticker-navigation.test.ts +24 -0
  187. package/src/plugins/ticker-navigation.ts +40 -0
  188. package/src/remote/schema.ts +1 -1
  189. package/src/renderers/browser/cloud-transport.test.ts +36 -0
  190. package/src/renderers/browser/cloud-transport.ts +19 -0
  191. package/src/renderers/browser/config-host.ts +0 -4
  192. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  193. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  194. package/src/renderers/browser/main.tsx +3 -0
  195. package/src/renderers/cloudflare/worker.test.ts +8 -5
  196. package/src/renderers/cloudflare/worker.ts +6 -5
  197. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  198. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  199. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  201. package/src/renderers/electrobun/view/host/native.ts +0 -5
  202. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  203. package/src/renderers/electrobun/view/styles.css +107 -7
  204. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  205. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  206. package/src/renderers/electrobun/view/ui-host.tsx +4 -1
  207. package/src/renderers/share/layout-view.test.tsx +58 -0
  208. package/src/renderers/share/layout-view.tsx +133 -0
  209. package/src/renderers/share/main.tsx +54 -6
  210. package/src/renderers/share/styles.css +35 -0
  211. package/src/renderers/share/view.test.tsx +78 -0
  212. package/src/renderers/share/view.tsx +71 -0
  213. package/src/shares/api.test.ts +52 -3
  214. package/src/shares/api.ts +39 -15
  215. package/src/shares/location.test.ts +19 -0
  216. package/src/shares/location.ts +11 -0
  217. package/src/shares/pane.test.ts +110 -0
  218. package/src/shares/pane.ts +54 -0
  219. package/src/shares/payload.ts +74 -1
  220. package/src/state/app/bootstrap.ts +6 -7
  221. package/src/state/app/context/index.test.ts +41 -1
  222. package/src/state/app/context/index.test.tsx +34 -16
  223. package/src/state/selectors-ui.ts +0 -8
  224. package/src/sync/controller.test.ts +57 -1
  225. package/src/sync/controller.ts +22 -11
  226. package/src/sync/react.ts +13 -0
  227. package/src/theme/font-scale.ts +0 -11
  228. package/src/types/config.ts +35 -5
  229. package/src/types/desktop-menu.ts +1 -0
  230. package/src/types/plugin.ts +36 -0
  231. package/src/ui/context-menu.tsx +1 -1
  232. package/src/ui/toast.tsx +2 -0
  233. package/src/utils/back-navigation.ts +0 -4
package/src/cli/index.ts CHANGED
@@ -23,7 +23,6 @@ import { createSystemCliCommands } from "./commands/system";
23
23
  import {
24
24
  aiCliCommand,
25
25
  brokerCliCommand,
26
- cloudCliCommands,
27
26
  ibkrCliCommand,
28
27
  rssCliCommand,
29
28
  } from "./commands/automation";
@@ -198,7 +197,6 @@ function createCoreCliCommands(
198
197
  ibkrCliCommand,
199
198
  aiCliCommand,
200
199
  rssCliCommand,
201
- ...cloudCliCommands,
202
200
  ];
203
201
  return commands;
204
202
  }
@@ -31,6 +31,9 @@ import {
31
31
  resolveCompositeMinimumSpanMs,
32
32
  zoomCompositeViewport,
33
33
  } from "./interactions";
34
+ import { getThemeColors, syncTheme } from "../../../theme/colors";
35
+ import { ThemeProvider } from "../../../theme/theme-context";
36
+ import { DEFAULT_THEME } from "../../../theme/themes";
34
37
 
35
38
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
36
39
  let chartShortcut: ((event: KeyEventLike) => void) | null = null;
@@ -131,12 +134,12 @@ function keyEvent(name: string, shift = false): KeyEventLike {
131
134
  }
132
135
 
133
136
  afterEach(async () => {
134
- if (!testSetup) return;
135
- await act(async () => testSetup!.renderer.destroy());
137
+ if (testSetup) await act(async () => testSetup!.renderer.destroy());
136
138
  testSetup = undefined;
137
139
  chartShortcut = null;
138
140
  capturedSurfaceProps = null;
139
141
  capturedSurfaceNode = null;
142
+ syncTheme(DEFAULT_THEME);
140
143
  });
141
144
 
142
145
  function point(date: string, value: number): TimeSeriesPoint {
@@ -204,6 +207,46 @@ function twoSessionCandles(): ResolvedSeries {
204
207
  }
205
208
 
206
209
  describe("CompositeChart", () => {
210
+ test("repaints cached chart rasters when the theme changes", async () => {
211
+ let setThemeId: ((themeId: string) => void) | null = null;
212
+ const chartSeries = [series("price", "main", "left", "USD", [100, 101, 102])];
213
+
214
+ function ThemedChart() {
215
+ const [themeId, setTheme] = useState(DEFAULT_THEME);
216
+ setThemeId = setTheme;
217
+ return (
218
+ <ThemeProvider themeId={themeId}>
219
+ <CaptureChartSurfaceProvider>
220
+ <CompositeChart
221
+ width={60}
222
+ height={12}
223
+ series={chartSeries}
224
+ panels={[{ id: "main" }]}
225
+ />
226
+ </CaptureChartSurfaceProvider>
227
+ </ThemeProvider>
228
+ );
229
+ }
230
+
231
+ testSetup = await testRender(<ThemedChart />, { width: 62, height: 14 });
232
+ await act(async () => {
233
+ await testSetup!.renderOnce();
234
+ await testSetup!.renderOnce();
235
+ });
236
+
237
+ const nextThemeId = "github-light";
238
+ await act(async () => {
239
+ setThemeId?.(nextThemeId);
240
+ await testSetup!.renderOnce();
241
+ await testSetup!.renderOnce();
242
+ });
243
+
244
+ const bitmap = capturedSurfaceProps!.bitmaps[0] as { pixels: Uint8Array };
245
+ const expectedBackground = getThemeColors(nextThemeId).bg;
246
+ const expectedRgb = [1, 3, 5].map((offset) => parseInt(expectedBackground.slice(offset, offset + 2), 16));
247
+ expect(Array.from(bitmap.pixels.slice(0, 3))).toEqual(expectedRgb);
248
+ });
249
+
207
250
  test("shows the regular-session move beside the latest intraday value", async () => {
208
251
  testSetup = await testRender(
209
252
  <CompositeChart
@@ -14,6 +14,7 @@ import {
14
14
  import { useShortcut } from "../../../react/input";
15
15
  import { useOptionalPaneInstanceId, usePaneSettingValue } from "../../../state/app/context";
16
16
  import { colors as themeColors, hoverBg } from "../../../theme/colors";
17
+ import { useThemeColors } from "../../../theme/theme-context";
17
18
  import { formatPercentRaw } from "../../../utils/format";
18
19
  import { isPlainKey } from "../../../utils/keyboard";
19
20
  import { truncateWithEllipsis } from "../../../utils/text-wrap";
@@ -62,6 +63,7 @@ import { renderCompositePanelBitmap } from "./rasterizer";
62
63
  import {
63
64
  buildChartToolVectors,
64
65
  CHART_DRAWING_COLORS,
66
+ CHART_DRAWINGS_SETTING_KEY,
65
67
  countMeasureBars,
66
68
  drawChartToolOverlay,
67
69
  resolveChartToolKind,
@@ -70,6 +72,7 @@ import {
70
72
  hitTestDrawings,
71
73
  isDrawingTool,
72
74
  nextDrawingColor,
75
+ parseChartDrawings,
73
76
  resolveDrawingFromDrag,
74
77
  resolveZoomBoxRange,
75
78
  shiftDrawing,
@@ -1371,7 +1374,6 @@ function CompositeLegend({
1371
1374
  );
1372
1375
  }
1373
1376
 
1374
- const CHART_DRAWINGS_SETTING_KEY = "chartDrawings";
1375
1377
  const NO_DRAWINGS: readonly ChartDrawing[] = [];
1376
1378
  /** Coalesces a drag into one write instead of one per pointer move. */
1377
1379
  const DRAWING_PERSIST_DELAY_MS = 400;
@@ -1397,7 +1399,7 @@ function ChartDrawingStore({
1397
1399
  );
1398
1400
  const restoredRef = useRef<readonly ChartDrawing[] | null>(null);
1399
1401
  if (restoredRef.current === null) {
1400
- restoredRef.current = Array.isArray(stored) ? stored : NO_DRAWINGS;
1402
+ restoredRef.current = parseChartDrawings(stored);
1401
1403
  }
1402
1404
 
1403
1405
  useEffect(() => {
@@ -1445,6 +1447,7 @@ export function CompositeChart({
1445
1447
  onToggleSeries,
1446
1448
  isSeriesToggleable,
1447
1449
  }: CompositeChartProps) {
1450
+ const activeThemeColors = useThemeColors();
1448
1451
  const { cellWidthPx = 8, pixelRatio = 1 } = useUiCapabilities();
1449
1452
  const isDesktopWeb = useUiHost().kind === "desktop-web";
1450
1453
  const showTextFallback = useShowChartTextFallback();
@@ -1692,13 +1695,13 @@ export function CompositeChart({
1692
1695
  }).join("|");
1693
1696
  const plotHeight = Math.max(panelCount, totalHeight - legendRows - timeAxisRows);
1694
1697
  const resolvedColors = useMemo<CompositeChartColors>(() => ({
1695
- background: colors?.background ?? themeColors.bg,
1696
- grid: colors?.grid ?? themeColors.border,
1697
- crosshair: colors?.crosshair ?? themeColors.borderFocused,
1698
- text: colors?.text ?? themeColors.text,
1699
- textDim: colors?.textDim ?? themeColors.textDim,
1700
- negative: colors?.negative ?? themeColors.negative,
1701
- }), [colors]);
1698
+ background: colors?.background ?? activeThemeColors.bg,
1699
+ grid: colors?.grid ?? activeThemeColors.border,
1700
+ crosshair: colors?.crosshair ?? activeThemeColors.borderFocused,
1701
+ text: colors?.text ?? activeThemeColors.text,
1702
+ textDim: colors?.textDim ?? activeThemeColors.textDim,
1703
+ negative: colors?.negative ?? activeThemeColors.negative,
1704
+ }), [activeThemeColors, colors]);
1702
1705
  const projectedScene = useMemo(() => {
1703
1706
  // lastTickKey busts this memo when a live tick mutates series identity in place.
1704
1707
  void lastTickKey;
@@ -17,7 +17,6 @@ import type {
17
17
  import {
18
18
  buildCompositeTimeScale,
19
19
  projectCompositeTimestamp,
20
- unprojectCompositeTimestamp,
21
20
  } from "./time-scale";
22
21
  import type { CompositeTimeScale } from "./types";
23
22
 
@@ -523,26 +522,6 @@ export function resolveCompositeCursorDate(scene: CompositeChartScene, localX: n
523
522
  return scene.dates[nearestIndex] ?? null;
524
523
  }
525
524
 
526
- export function resolveCompositeTimeAxisDate(
527
- scene: CompositeChartScene,
528
- ratio: number,
529
- ): Date {
530
- const safeRatio = Math.max(0, Math.min(1, ratio));
531
- if (scene.timeScale.kind === "market" && scene.dates.length > 0) {
532
- let nearestIndex = 0;
533
- let nearestDistance = Number.POSITIVE_INFINITY;
534
- scene.dateRatios.forEach((candidate, index) => {
535
- const distance = Math.abs(candidate - safeRatio);
536
- if (distance < nearestDistance) {
537
- nearestIndex = index;
538
- nearestDistance = distance;
539
- }
540
- });
541
- return scene.dates[nearestIndex] ?? new Date(scene.startTime);
542
- }
543
- return new Date(unprojectCompositeTimestamp(scene.timeScale, safeRatio));
544
- }
545
-
546
525
  export function resolveAdjacentCompositeCursorDate(
547
526
  scene: CompositeChartScene,
548
527
  current: Date | null,
@@ -27,6 +27,55 @@ export interface ChartDrawing {
27
27
  color: string;
28
28
  }
29
29
 
30
+ export const CHART_DRAWINGS_SETTING_KEY = "chartDrawings";
31
+ const MAX_SHARED_CHART_DRAWINGS = 100;
32
+ const MAX_SHARED_DRAWING_POINTS = 5_000;
33
+
34
+ export function parseChartDrawings(value: unknown): ChartDrawing[] {
35
+ if (!Array.isArray(value) || value.length > MAX_SHARED_CHART_DRAWINGS) return [];
36
+ let pointCount = 0;
37
+ const drawings: ChartDrawing[] = [];
38
+ for (const entry of value) {
39
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
40
+ const drawing = entry as Record<string, unknown>;
41
+ if (!Object.keys(drawing).every((key) => ["id", "panelId", "points", "color"].includes(key))) return [];
42
+ if (
43
+ typeof drawing.id !== "string"
44
+ || drawing.id.length === 0
45
+ || drawing.id.length > 160
46
+ || typeof drawing.panelId !== "string"
47
+ || drawing.panelId.length === 0
48
+ || drawing.panelId.length > 120
49
+ || typeof drawing.color !== "string"
50
+ || !/^#[0-9a-f]{6}$/i.test(drawing.color)
51
+ || !Array.isArray(drawing.points)
52
+ || drawing.points.length < 2
53
+ ) return [];
54
+ pointCount += drawing.points.length;
55
+ if (pointCount > MAX_SHARED_DRAWING_POINTS) return [];
56
+ const points: ChartDrawingPoint[] = [];
57
+ for (const point of drawing.points) {
58
+ if (!point || typeof point !== "object" || Array.isArray(point)) return [];
59
+ const raw = point as Record<string, unknown>;
60
+ if (
61
+ !Object.keys(raw).every((key) => key === "time" || key === "value")
62
+ || typeof raw.time !== "number"
63
+ || !Number.isFinite(raw.time)
64
+ || typeof raw.value !== "number"
65
+ || !Number.isFinite(raw.value)
66
+ ) return [];
67
+ points.push({ time: raw.time, value: raw.value });
68
+ }
69
+ drawings.push({
70
+ id: drawing.id,
71
+ panelId: drawing.panelId,
72
+ points,
73
+ color: drawing.color,
74
+ });
75
+ }
76
+ return drawings;
77
+ }
78
+
30
79
  export interface ChartDrawingHit {
31
80
  drawing: ChartDrawing;
32
81
  /** Grabbed endpoint, or null when the whole shape was grabbed. */
@@ -87,6 +87,10 @@ export function runDirectCommandAction(options: {
87
87
  closeAll({ revertThemePreview: false });
88
88
  pluginRegistry.showPane("help");
89
89
  return;
90
+ case "layout-marketplace":
91
+ closeAll({ revertThemePreview: false });
92
+ pluginRegistry.showPane("layout-marketplace");
93
+ return;
90
94
  case "pane-settings":
91
95
  if (state.focusedPaneId) openPaneSettingsRoute(state.focusedPaneId);
92
96
  return;
@@ -138,8 +138,15 @@ export const commands: Command[] = [
138
138
  category: "Config",
139
139
  },
140
140
  {
141
- id: "layout",
141
+ id: "layout-marketplace",
142
142
  prefix: "LAY",
143
+ label: "Layouts",
144
+ description: "Open the layout browser to switch, publish, or add layouts",
145
+ category: "Config",
146
+ },
147
+ {
148
+ id: "layout",
149
+ prefix: "LMA",
143
150
  label: "Layout Actions",
144
151
  description: "Organize panes and saved layouts",
145
152
  hasArg: true,
@@ -79,7 +79,7 @@ export function buildCurrentLayoutItems({
79
79
  },
80
80
  {
81
81
  id: "layout-gridlock",
82
- label: "Gridlock All Windows",
82
+ label: "Tidy Windows",
83
83
  detail: currentLayout.floating.length > 0
84
84
  ? "Infer a tiled layout from the current window positions"
85
85
  : "Retile all panes from their current arrangement",
@@ -112,6 +112,10 @@ export function useCommandBarPaneTemplateActions({
112
112
  const trimmedArg = rawArg?.trim() || "";
113
113
  const argKind = template.shortcut?.argKind ?? template.shortcut?.argPlaceholder;
114
114
  if (argKind === "ticker") {
115
+ if (!trimmedArg && template.shortcut?.argOptional) {
116
+ await openPaneTemplateDirect(template);
117
+ return;
118
+ }
115
119
  const resolvedTicker = await resolveTickerInput(
116
120
  trimmedArg || undefined,
117
121
  activeTickerSymbol,
@@ -34,6 +34,9 @@ export function buildRootShortcutFeedback({
34
34
  if (rootShortcutIntent.source === "pane-template") {
35
35
  const argKind = getPaneTemplateArgKind(rootShortcutIntent.template);
36
36
  if (argKind === "ticker") {
37
+ if (rootShortcutIntent.template.shortcut?.argOptional && !rootShortcutIntent.argText) {
38
+ return `Shortcut: ${rootShortcutIntent.label}`;
39
+ }
37
40
  const symbol = normalizeTickerInput(activeTickerSymbol, rootShortcutIntent.argText);
38
41
  if (symbol) {
39
42
  return rootShortcutIntent.kind === "inferred-complete"
@@ -46,6 +46,13 @@ const paneTemplates: PaneTemplateDef[] = [
46
46
  description: "Search provider instruments",
47
47
  shortcut: { prefix: "SRCH", argPlaceholder: "query", argKind: "text" },
48
48
  },
49
+ {
50
+ id: "cds-pane",
51
+ paneId: "cds",
52
+ label: "Single-Name CDS",
53
+ description: "CDS shortcut",
54
+ shortcut: { prefix: "CDS", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
55
+ },
49
56
  ];
50
57
 
51
58
  const pluginCommands: CommandDef[] = [{
@@ -135,6 +142,18 @@ describe("ticker data root shortcuts", () => {
135
142
  }
136
143
  });
137
144
 
145
+ test("optional ticker shortcuts do not infer the active ticker", () => {
146
+ const bare = parse("CDS", "MSFT");
147
+ expect(bare.kind).toBe("partial");
148
+ if (bare.kind === "none") throw new Error("Expected shortcut intent");
149
+ expect(bare.completionQuery).toBeNull();
150
+
151
+ const scoped = parse("CDS ORCL", "MSFT");
152
+ expect(scoped.kind).toBe("complete");
153
+ if (scoped.kind === "none") throw new Error("Expected shortcut intent");
154
+ expect(scoped.argText).toBe("ORCL");
155
+ });
156
+
138
157
  test("EM accepts optional text tickers without active ticker inference", () => {
139
158
  const bare = parse("EM", "AAPL");
140
159
  expect(bare.kind).toBe("partial");
@@ -145,7 +145,9 @@ export function parseRootShortcutIntent({
145
145
  if (!match) return { kind: "none" };
146
146
 
147
147
  const argText = trimmed.slice(match.prefix.length).trim();
148
- const inferredArg = inferShortcutArg(match.argKind, activeTicker);
148
+ const inferredArg = match.source === "pane-template" && match.template?.shortcut?.argOptional
149
+ ? null
150
+ : inferShortcutArg(match.argKind, activeTicker);
149
151
  const completionQuery = inferredArg ? `${match.prefix} ${inferredArg}` : null;
150
152
 
151
153
  let kind: Exclude<ShortcutIntentKind, "none">;
@@ -179,17 +179,17 @@ describe("CommandBar", () => {
179
179
  expect(testSetup.captureCharFrame()).not.toContain("GitHub Light");
180
180
  });
181
181
 
182
- test("runs plugin command shortcuts from the root query", async () => {
182
+ test("finds the tidy windows command by its snap alias", async () => {
183
183
  const calls: string[] = [];
184
184
 
185
185
  testSetup = await testRender(<CommandBarHarness
186
- query="GL"
186
+ query="snap"
187
187
  configurePluginRegistry={(pluginRegistry) => {
188
188
  (pluginRegistry.commands as Map<string, any>).set("gridlock-all", {
189
189
  id: "gridlock-all",
190
- label: "Gridlock All Windows",
191
- description: "Arrange all visible panes into a tiled grid",
192
- keywords: ["grid", "gridlock", "tile", "arrange", "windows", "layout"],
190
+ label: "Tidy Windows",
191
+ description: "Arrange every window into one tiled layout",
192
+ keywords: ["tidy", "snap", "grid", "gridlock", "tile", "arrange", "organize", "organise", "cleanup", "dock", "floating", "windows", "layout"],
193
193
  shortcut: "GL",
194
194
  category: "config",
195
195
  execute: async () => {
@@ -215,7 +215,7 @@ describe("CommandBar", () => {
215
215
  await testSetup.renderOnce();
216
216
 
217
217
  const frame = testSetup.captureCharFrame();
218
- expect(frame).toContain("Gridlock All Windows");
218
+ expect(frame).toContain("Tidy Windows");
219
219
  expect(frame).toContain("GL");
220
220
 
221
221
  await act(async () => {
@@ -142,6 +142,23 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
142
142
  });
143
143
  }
144
144
 
145
+ function registerOptionalTickerPane(pluginRegistry: MutablePaneRegistry): void {
146
+ mutableRegistryMap(pluginRegistry.panes).set("optional-ticker", {
147
+ id: "optional-ticker",
148
+ name: "Optional Ticker",
149
+ component: () => null,
150
+ defaultPosition: "right",
151
+ defaultMode: "floating",
152
+ });
153
+ mutableRegistryMap(pluginRegistry.paneTemplates).set("optional-ticker-pane", {
154
+ id: "optional-ticker-pane",
155
+ paneId: "optional-ticker",
156
+ label: "Optional Ticker",
157
+ description: "Open market-wide or for one ticker.",
158
+ shortcut: { prefix: "OT", argPlaceholder: "ticker", argKind: "ticker", argOptional: true },
159
+ });
160
+ }
161
+
145
162
  function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
146
163
  mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
147
164
  id: "prediction-markets",
@@ -204,7 +221,7 @@ describe("CommandBar pane and layout routes", () => {
204
221
 
205
222
  test("renders layout mode with focused pane actions", async () => {
206
223
  testSetup = await testRender(<CommandBarHarness
207
- query="LAY "
224
+ query="LMA "
208
225
  configureConfig={layoutModeConfig}
209
226
  configureState={layoutModeState}
210
227
  />, {
@@ -248,9 +265,29 @@ describe("CommandBar pane and layout routes", () => {
248
265
  expect(actions.some((action) => action.type === "UNDO_LAYOUT")).toBe(true);
249
266
  });
250
267
 
268
+ test("opens Layouts as a normal pane from LAY", async () => {
269
+ const opened: string[] = [];
270
+ testSetup = await testRender(
271
+ <CommandBarHarness
272
+ query="LAY"
273
+ live
274
+ configurePluginRegistry={(pluginRegistry) => {
275
+ pluginRegistry.showPane = (paneId) => opened.push(paneId);
276
+ }}
277
+ />,
278
+ { width: 90, height: 18 },
279
+ );
280
+ await testSetup.renderOnce();
281
+ expect(testSetup.captureCharFrame()).toContain("Layouts");
282
+
283
+ await clickFrameText("Layouts");
284
+
285
+ expect(opened).toEqual(["layout-marketplace"]);
286
+ });
287
+
251
288
  test("renders filtered saved layouts with textual previews", async () => {
252
289
  testSetup = await testRender(<CommandBarHarness
253
- query="LAY Research"
290
+ query="LMA Research"
254
291
  configureConfig={layoutModeConfig}
255
292
  configureState={layoutModeState}
256
293
  />, {
@@ -353,6 +390,33 @@ describe("CommandBar pane and layout routes", () => {
353
390
  expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
354
391
  });
355
392
 
393
+ test("keeps an optional ticker shortcut market-wide when a ticker is active", async () => {
394
+ const created: CreatedPaneCall[] = [];
395
+
396
+ testSetup = await testRender(<CommandBarHarness
397
+ query="OT"
398
+ selectedTicker="AAPL"
399
+ live
400
+ configurePluginRegistry={(pluginRegistry) => {
401
+ registerOptionalTickerPane(pluginRegistry);
402
+ recordPaneCreations(pluginRegistry, created);
403
+ }}
404
+ />, {
405
+ width: 100,
406
+ height: 18,
407
+ });
408
+
409
+ await testSetup.renderOnce();
410
+
411
+ await act(async () => {
412
+ testSetup!.mockInput.pressEnter();
413
+ await Bun.sleep(0);
414
+ await testSetup!.renderOnce();
415
+ });
416
+
417
+ expect(created).toEqual([{ templateId: "optional-ticker-pane", options: undefined }]);
418
+ });
419
+
356
420
  for (const scenario of [
357
421
  {
358
422
  query: "prediction markets",
@@ -14,7 +14,8 @@ describe("command bar view model helpers", () => {
14
14
  expect(resolveCommandBarMode("T NVDA")).toMatchObject({ kind: "search", badge: "T" });
15
15
  expect(resolveCommandBarMode("TH ")).toMatchObject({ kind: "themes", badge: "THEMES" });
16
16
  expect(resolveCommandBarMode("PL notes")).toMatchObject({ kind: "plugins", badge: "PLUGINS" });
17
- expect(resolveCommandBarMode("LAY ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
17
+ expect(resolveCommandBarMode("LAY")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
18
+ expect(resolveCommandBarMode("LMA ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
18
19
  expect(resolveCommandBarMode("NP ")).toMatchObject({ kind: "default", badge: "FILTER" });
19
20
  expect(resolveCommandBarMode("PS")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
20
21
  expect(resolveCommandBarMode("AW")).toMatchObject({ kind: "direct-command", badge: "COMMAND" });
@@ -3,6 +3,7 @@ import { cloneLayout, createDefaultConfig, findPaneInstance, type LayoutConfig }
3
3
  import { createInitialState } from "../../../state/app/context";
4
4
  import { createTestDataProvider } from "../../../test-support/data-provider";
5
5
  import { applyPaneSettingFieldValue, createPaneTemplateOrThrow } from "./ops";
6
+ import type { TickerRecord } from "../../../types/ticker";
6
7
 
7
8
  function makeDataProvider() {
8
9
  return createTestDataProvider({ id: "test" });
@@ -68,6 +69,59 @@ describe("createPaneTemplateOrThrow", () => {
68
69
  expect(placeCalls).toHaveLength(0);
69
70
  });
70
71
 
72
+ test("uses an explicit shared symbol instead of the recipient's active ticker", async () => {
73
+ const config = createDefaultConfig("/tmp/gloomberb-workflow-ops-test");
74
+ const state = createInitialState(config);
75
+ const msft: TickerRecord = {
76
+ metadata: {
77
+ ticker: "MSFT",
78
+ exchange: "NASDAQ",
79
+ currency: "USD",
80
+ name: "Microsoft",
81
+ portfolios: [],
82
+ watchlists: [],
83
+ positions: [],
84
+ broker_contracts: [],
85
+ custom: {},
86
+ tags: [],
87
+ },
88
+ };
89
+ state.tickers.set("MSFT", msft);
90
+ let createdSymbol: string | null | undefined;
91
+
92
+ await createPaneTemplateOrThrow("ticker-pane", { symbol: "MSFT" }, {
93
+ dataProvider: makeDataProvider() as any,
94
+ tickerRepository: makeTickerRepository() as any,
95
+ dispatch: () => {},
96
+ getState: () => state,
97
+ pluginRegistry: {
98
+ paneTemplates: new Map([["ticker-pane", {
99
+ id: "ticker-pane",
100
+ paneId: "ticker-view",
101
+ label: "Ticker View",
102
+ description: "Ticker View",
103
+ shortcut: { prefix: "TV", argPlaceholder: "ticker", argKind: "ticker" },
104
+ createInstance: (_context: unknown, options: { symbol?: string } | undefined) => {
105
+ createdSymbol = options?.symbol;
106
+ return { binding: { kind: "fixed", symbol: options?.symbol ?? "" } };
107
+ },
108
+ }]]),
109
+ panes: new Map([["ticker-view", {
110
+ id: "ticker-view",
111
+ name: "Ticker View",
112
+ component: () => null,
113
+ defaultPosition: "right",
114
+ }]]),
115
+ getPaneTemplatePluginId: () => undefined,
116
+ events: { emit: () => {} },
117
+ } as any,
118
+ buildPaneInstance: () => ({ instanceId: "ticker-view:1", paneId: "ticker-view" }) as any,
119
+ placePaneInstance: () => {},
120
+ });
121
+
122
+ expect(createdSymbol).toBe("MSFT");
123
+ });
124
+
71
125
  test("passes pane template instance ids through to pane creation", async () => {
72
126
  const config = createDefaultConfig("/tmp/gloomberb-workflow-ops-test");
73
127
  const state = createInitialState(config);
@@ -152,7 +152,7 @@ async function resolvePaneTemplateOptions(
152
152
  let resolvedOptions = options;
153
153
  if (template.shortcut?.argPlaceholder === "ticker") {
154
154
  const resolvedTicker = await resolveTickerInputOrThrow(
155
- resolvedOptions?.arg,
155
+ resolvedOptions?.symbol ?? resolvedOptions?.arg,
156
156
  baseContext.activeTicker,
157
157
  baseContext.activeCollectionId,
158
158
  deps,