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
@@ -61,10 +61,11 @@ function createShellPluginRegistry(options?: {
61
61
  } as unknown as PluginRegistry;
62
62
  }
63
63
 
64
- async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean }) {
64
+ async function emitKeypress(event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; alt?: boolean }) {
65
65
  await act(async () => {
66
66
  const keyEvent = {
67
67
  ctrl: false,
68
+ alt: false,
68
69
  meta: false,
69
70
  option: false,
70
71
  shift: false,
@@ -312,6 +313,7 @@ describe("Shell", () => {
312
313
  const frame = testSetup.captureCharFrame();
313
314
  expect(frame).toContain("Settings");
314
315
  expect(frame).toContain("Ctrl+,");
316
+ expect(frame).not.toContain("Layout Actions");
315
317
  expect(frame).not.toContain("CmdOrCtrl");
316
318
  });
317
319
 
@@ -400,24 +402,92 @@ describe("Shell", () => {
400
402
  });
401
403
  await testSetup.renderOnce();
402
404
 
403
- expect(testSetup.captureCharFrame()).toContain("Dock Pane");
405
+ const frame = testSetup.captureCharFrame();
406
+ expect(frame).toContain("Dock Pane");
407
+ expect(frame).not.toContain("Layout Actions");
404
408
  });
405
409
 
406
410
  test("resolves pane management shortcuts", () => {
407
411
  const base = { ctrl: false, meta: true, super: true, shift: true, alt: false };
408
412
  expect(resolvePaneManagementShortcut({ ...base, name: ",", key: ",", shift: false })).toBe("settings");
409
413
  expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", ctrl: true, meta: false, super: false, shift: false })).toBe("close");
414
+ expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", shift: false, alt: true })).toBe("close-all-floating");
415
+ expect(resolvePaneManagementShortcut({ ...base, name: "W", key: "W", shift: false })).toBeNull();
410
416
  expect(resolvePaneManagementShortcut({ ...base, name: "D", key: "D" })).toBe("toggle-floating");
411
417
  expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out");
412
418
  expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot");
413
419
  expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-actions");
414
420
  expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all");
415
421
  expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode");
422
+ expect(resolvePaneManagementShortcut({ ...base, name: "r", key: "r" })).toBe("window-resize-mode");
416
423
  expect(resolvePaneManagementShortcut({ ...base, name: "n", key: "n" })).toBeNull();
417
424
  expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", alt: true })).toBeNull();
418
425
  expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull();
419
426
  });
420
427
 
428
+ test("exits window mode on Enter without changes", async () => {
429
+ const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
430
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
431
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
432
+ const dockedLayout = {
433
+ dockRoot: {
434
+ kind: "split" as const,
435
+ axis: "horizontal" as const,
436
+ ratio: 0.5,
437
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
438
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
439
+ },
440
+ instances: [{ ...mainPane }, { ...detailPane }],
441
+ floating: [],
442
+ detached: [],
443
+ };
444
+ const { actions } = await renderShellForWindowModeTest(
445
+ createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
446
+ );
447
+
448
+ await emitKeypress({ name: "m", ctrl: true, shift: true });
449
+ expect(testSetup.captureCharFrame()).toContain("WINDOW MOVE");
450
+
451
+ await emitKeypress({ name: "enter" });
452
+ await act(async () => {
453
+ await testSetup!.renderOnce();
454
+ });
455
+ expect(testSetup.captureCharFrame()).not.toContain("WINDOW MOVE");
456
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
457
+ });
458
+
459
+ test("starts resize mode directly from the resize shortcut", async () => {
460
+ const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-resize-shortcut-test");
461
+ const mainPane = requireLayoutInstance(config, "portfolio-list:main");
462
+ const detailPane = requireLayoutInstance(config, "ticker-detail:main");
463
+ const dockedLayout = {
464
+ dockRoot: {
465
+ kind: "split" as const,
466
+ axis: "horizontal" as const,
467
+ ratio: 0.5,
468
+ first: { kind: "pane" as const, instanceId: "portfolio-list:main" },
469
+ second: { kind: "pane" as const, instanceId: "ticker-detail:main" },
470
+ },
471
+ instances: [{ ...mainPane }, { ...detailPane }],
472
+ floating: [],
473
+ detached: [],
474
+ };
475
+ const { actions } = await renderShellForWindowModeTest(
476
+ createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"),
477
+ );
478
+
479
+ await emitKeypress({ name: "r", ctrl: true, shift: true });
480
+ expect(testSetup.captureCharFrame()).toContain("WINDOW RESIZE");
481
+
482
+ await emitKeypress({ name: "enter" });
483
+ await act(async () => {
484
+ await testSetup!.renderOnce();
485
+ });
486
+ const frame = testSetup.captureCharFrame();
487
+ expect(frame).not.toContain("WINDOW RESIZE");
488
+ expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
489
+ });
490
+
421
491
  test("moves a floating pane in window mode and commits once", async () => {
422
492
  const config = createDefaultConfig("/tmp/gloomberb-shell-window-mode-test");
423
493
  const floatingLayout = cloneLayout(config.layout);
@@ -785,4 +855,52 @@ describe("Shell", () => {
785
855
  expect(updateLayout?.layout.floating).toEqual([]);
786
856
  });
787
857
 
858
+ test("closes all floating panes with Ctrl+Alt+W", async () => {
859
+ const config = createDefaultConfig("/tmp/gloomberb-shell-test");
860
+ const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
861
+ const firstDetailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
862
+ if (!mainPane || !firstDetailPane) throw new Error("missing default panes");
863
+ const secondDetailPane = {
864
+ ...firstDetailPane,
865
+ instanceId: "ticker-detail:secondary",
866
+ };
867
+
868
+ const mixedLayout = {
869
+ dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
870
+ instances: [{ ...mainPane }, { ...firstDetailPane }, secondDetailPane],
871
+ floating: [
872
+ { instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 },
873
+ { instanceId: "ticker-detail:secondary", x: 8, y: 3, width: 30, height: 8 },
874
+ ],
875
+ detached: [],
876
+ };
877
+ const state = {
878
+ ...createInitialState({
879
+ ...config,
880
+ layout: cloneLayout(mixedLayout),
881
+ layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
882
+ }),
883
+ focusedPaneId: "portfolio-list:main",
884
+ };
885
+ const actions: Array<any> = [];
886
+
887
+ testSetup = await testRender(
888
+ <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
889
+ <TestDialogProvider>
890
+ <Shell pluginRegistry={createShellPluginRegistry()} />
891
+ </TestDialogProvider>
892
+ </AppContext>,
893
+ { width: 40, height: 12 },
894
+ );
895
+
896
+ await testSetup.renderOnce();
897
+ await emitKeypress({ name: "w", ctrl: true, alt: true });
898
+
899
+ const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
900
+ expect(actions).toContainEqual({ type: "PUSH_LAYOUT_HISTORY" });
901
+ expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
902
+ expect(updateLayout?.layout.floating).toEqual([]);
903
+ expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
904
+ });
905
+
788
906
  });
@@ -186,6 +186,7 @@ export function Shell({
186
186
  });
187
187
 
188
188
  const {
189
+ closeAllFloatingPanes,
189
190
  closeFocusedPane,
190
191
  copyFocusedPaneScreenshot,
191
192
  copyPaneScreenshot,
@@ -213,6 +214,7 @@ export function Shell({
213
214
 
214
215
  useShellPaneManagementShortcuts({
215
216
  cancelActiveDrag,
217
+ closeAllFloatingPanes,
216
218
  closeFocusedPane,
217
219
  copyFocusedPaneScreenshot,
218
220
  focusedPaneId,
@@ -101,15 +101,9 @@ export function menuForPane(
101
101
  {
102
102
  id: "window-resize-mode",
103
103
  label: "Resize Window...",
104
- accelerator: "WIN resize",
104
+ accelerator: PANE_MANAGEMENT_ACCELERATORS.windowResizeMode,
105
105
  onSelect: () => pluginRegistry.openWindowMode(pane.instance.instanceId, "resize"),
106
106
  },
107
- {
108
- id: "layout-actions",
109
- label: "Layout Actions...",
110
- accelerator: PANE_MANAGEMENT_ACCELERATORS.layoutActions,
111
- onSelect: () => pluginRegistry.openCommandBar("LAY "),
112
- },
113
107
  );
114
108
 
115
109
  return baseActions;
@@ -5,6 +5,7 @@ import {
5
5
  floatPane,
6
6
  gridlockAllPanes,
7
7
  isPaneInLayout,
8
+ removeFloatingPanes,
8
9
  removePane,
9
10
  type ResolvedPane,
10
11
  } from "../../../../plugins/pane-manager";
@@ -73,6 +74,12 @@ export function useShellPaneActions({
73
74
  return true;
74
75
  }, [focusedPaneId, persistLayout, visibleLayout]);
75
76
 
77
+ const closeAllFloatingPanes = useCallback(() => {
78
+ if (visibleLayout.floating.length === 0) return false;
79
+ persistLayout(removeFloatingPanes(visibleLayout));
80
+ return true;
81
+ }, [persistLayout, visibleLayout]);
82
+
76
83
  const copyFocusedPaneScreenshot = useCallback(() => {
77
84
  if (!focusedPaneId || !nativePaneChrome || !rendererHost.copyPngImage) return false;
78
85
  if (!paneMap.has(focusedPaneId)) return false;
@@ -115,6 +122,7 @@ export function useShellPaneActions({
115
122
  }, [persistLayout, visibleLayout]);
116
123
 
117
124
  return {
125
+ closeAllFloatingPanes,
118
126
  closeFocusedPane,
119
127
  copyFocusedPaneScreenshot,
120
128
  copyPaneScreenshot,
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef } from "react";
2
2
  import { useShortcut } from "../../../../react/input";
3
+ import type { WindowEditMode } from "../../../../plugins/registry";
3
4
  import {
4
5
  createDoubleEscapeCloseState,
5
6
  recordDoubleEscapeClose,
@@ -12,6 +13,7 @@ import {
12
13
 
13
14
  interface ShellPaneManagementShortcutOptions {
14
15
  cancelActiveDrag(): void;
16
+ closeAllFloatingPanes(): boolean;
15
17
  closeFocusedPane(): boolean;
16
18
  copyFocusedPaneScreenshot(): boolean;
17
19
  focusedPaneId: string | null;
@@ -22,12 +24,13 @@ interface ShellPaneManagementShortcutOptions {
22
24
  openLayoutMenu(): void;
23
25
  overlayOpen: boolean;
24
26
  popOutFocusedPane(): boolean;
25
- startWindowMode(): void;
27
+ startWindowMode(paneId?: string, mode?: WindowEditMode): void;
26
28
  toggleFocusedPaneFloating(): boolean;
27
29
  }
28
30
 
29
31
  export function useShellPaneManagementShortcuts({
30
32
  cancelActiveDrag,
33
+ closeAllFloatingPanes,
31
34
  closeFocusedPane,
32
35
  copyFocusedPaneScreenshot,
33
36
  focusedPaneId,
@@ -82,6 +85,9 @@ export function useShellPaneManagementShortcuts({
82
85
  case "close":
83
86
  handled = closeFocusedPane();
84
87
  break;
88
+ case "close-all-floating":
89
+ handled = closeAllFloatingPanes();
90
+ break;
85
91
  case "settings":
86
92
  handled = openFocusedPaneSettings();
87
93
  break;
@@ -102,7 +108,11 @@ export function useShellPaneManagementShortcuts({
102
108
  handled = gridlockVisiblePanes();
103
109
  break;
104
110
  case "window-mode":
105
- startWindowMode();
111
+ startWindowMode(undefined, "move");
112
+ handled = true;
113
+ break;
114
+ case "window-resize-mode":
115
+ startWindowMode(undefined, "resize");
106
116
  handled = true;
107
117
  break;
108
118
  }
@@ -6,9 +6,11 @@ export const PANE_MANAGEMENT_ACCELERATORS = {
6
6
  popOut: "CmdOrCtrl+Shift+O",
7
7
  copyScreenshot: "CmdOrCtrl+Shift+C",
8
8
  close: "CmdOrCtrl+W",
9
+ closeAllFloating: "CmdOrCtrl+Alt+W",
9
10
  layoutActions: "CmdOrCtrl+Shift+L",
10
11
  gridlockAll: "CmdOrCtrl+Shift+G",
11
12
  windowMode: "CmdOrCtrl+Shift+M",
13
+ windowResizeMode: "CmdOrCtrl+Shift+R",
12
14
  } as const;
13
15
 
14
16
  export type PaneManagementShortcut =
@@ -17,24 +19,30 @@ export type PaneManagementShortcut =
17
19
  | "pop-out"
18
20
  | "copy-screenshot"
19
21
  | "close"
22
+ | "close-all-floating"
20
23
  | "layout-actions"
21
24
  | "gridlock-all"
22
- | "window-mode";
25
+ | "window-mode"
26
+ | "window-resize-mode";
23
27
 
24
28
  export function resolvePaneManagementShortcut(
25
29
  event: Pick<KeyEventLike, "name" | "key" | "ctrl" | "meta" | "super" | "shift" | "alt">,
26
30
  ): PaneManagementShortcut | null {
27
31
  if (!event.ctrl && !event.meta && !event.super) return null;
28
- const name = (event.name ?? event.key ?? "").toLowerCase();
29
- if (name === "w") return "close";
30
- if (!event.shift && name === ",") return "settings";
31
- if (!event.shift || event.alt) return null;
32
+ const rawName = event.name ?? event.key ?? "";
33
+ const name = rawName.toLowerCase();
34
+ const shifted = event.shift || rawName !== name;
35
+ if (!shifted && event.alt && name === "w") return "close-all-floating";
36
+ if (!shifted && name === "w") return "close";
37
+ if (!shifted && name === ",") return "settings";
38
+ if (!shifted || event.alt) return null;
32
39
  if (name === "c") return "copy-screenshot";
33
40
  if (name === "d") return "toggle-floating";
34
41
  if (name === "o") return "pop-out";
35
42
  if (name === "l") return "layout-actions";
36
43
  if (name === "g") return "gridlock-all";
37
44
  if (name === "m") return "window-mode";
45
+ if (name === "r") return "window-resize-mode";
38
46
  return null;
39
47
  }
40
48
 
@@ -42,6 +50,6 @@ export function inputCaptureAllowsPaneManagementShortcut(
42
50
  shortcut: PaneManagementShortcut,
43
51
  event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
44
52
  ): boolean {
45
- if (shortcut !== "close") return false;
53
+ if (shortcut !== "close" && shortcut !== "close-all-floating") return false;
46
54
  return event.meta || event.super || event.targetEditable !== true;
47
55
  }
@@ -128,9 +128,13 @@ export function useShellWindowMode({
128
128
  if (!windowMode) return;
129
129
  const committedLayout = resolveWindowEditCommitLayout(windowMode, bounds, dockGeometryOptions);
130
130
  const hasPendingCommit = windowEditHasPendingCommit(windowMode, bounds, dockGeometryOptions);
131
- if (hasPendingCommit) {
132
- persistLayout(committedLayout);
131
+ if (!hasPendingCommit) {
132
+ focusPane(windowMode.paneId);
133
+ setWindowMode(null);
134
+ return;
133
135
  }
136
+
137
+ persistLayout(committedLayout);
134
138
  focusPane(windowMode.paneId);
135
139
  setWindowMode({
136
140
  paneId: windowMode.paneId,
@@ -138,7 +142,7 @@ export function useShellWindowMode({
138
142
  mode: "move",
139
143
  focus: normalizeWindowEditFocus({ kind: "move" }, committedLayout, windowMode.paneId, "move", bounds, dockGeometryOptions),
140
144
  dirty: false,
141
- notice: hasPendingCommit ? "Committed" : "No changes",
145
+ notice: "Committed",
142
146
  });
143
147
  }, [bounds, dockGeometryOptions, focusPane, persistLayout, windowMode]);
144
148
 
@@ -166,6 +170,7 @@ export function useShellWindowMode({
166
170
 
167
171
  useShortcut((event) => {
168
172
  if (!windowMode) return;
173
+ if (event.ctrl || event.meta || event.super || event.alt) return;
169
174
  const name = (event.name ?? event.key ?? "").toLowerCase();
170
175
  let handled = true;
171
176
 
@@ -73,14 +73,14 @@ export function windowEditStatusLine(
73
73
  export function windowEditHelpText(state: WindowEditState): string {
74
74
  if (state.mode === "move") {
75
75
  if (state.focus.kind === "dock-move") {
76
- return "arrows/hjkl choose side m retarget Tab/w window d float r resize Enter commit Esc exit/cancel";
76
+ return "arrows/hjkl choose side m retarget Tab/w window d float r resize Enter commit/exit Esc exit/cancel";
77
77
  }
78
- return "Tab/w window d dock/float r resize arrows/hjkl target/move Shift fast Enter commit Esc exit/cancel";
78
+ return "Tab/w window d dock/float r resize arrows/hjkl target/move Shift fast Enter commit/exit Esc exit/cancel";
79
79
  }
80
80
  if (state.focus.kind === "floating-resize") {
81
- return "Tab corner w window m move arrows/hjkl resize Shift fast Enter commit Esc exit/cancel";
81
+ return "Tab corner w window m move arrows/hjkl resize Shift fast Enter commit/exit Esc exit/cancel";
82
82
  }
83
- return "arrows/hjkl move divider Tab divider w window m move Shift fast Enter commit Esc exit/cancel";
83
+ return "arrows/hjkl move divider Tab divider w window m move Shift fast Enter commit/exit Esc exit/cancel";
84
84
  }
85
85
 
86
86
  export function resolveWindowEditDockMovePreview(
@@ -101,7 +101,8 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
101
101
  const shortcutDisplayMode = getShortcutDisplayMode(uiHost.kind);
102
102
  const platformShortcut = (keys: string | readonly string[]) => formatPrimaryShortcut(keys, shortcutPlatform, shortcutDisplayMode);
103
103
  const keyboardShortcuts = [
104
- { key: "Ctrl+P / `", desc: "Open the command bar" },
104
+ { key: "Ctrl+P", desc: "Open command mode" },
105
+ { key: "`", desc: "Open ticker search" },
105
106
  { key: "Tab", desc: "Switch between panels" },
106
107
  { key: platformShortcut("W"), desc: "Close the focused pane" },
107
108
  { key: platformShortcut(["Shift", "D"]), desc: "Dock or float the focused pane" },
@@ -155,7 +156,7 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
155
156
  </Box>
156
157
  <Box height={1} />
157
158
  <Box height={1}>
158
- <Text fg={colors.textDim}>{"Type these in the command bar ("}<Span fg={colors.text}><Strong>{"Ctrl+P"}</Strong></Span>{" or "}<Span fg={colors.text}><Strong>{"`"}</Strong></Span>{"):"}</Text>
159
+ <Text fg={colors.textDim}>{"Type these in the command bar ("}<Span fg={colors.text}><Strong>{"Ctrl+P"}</Strong></Span>{"):"}</Text>
159
160
  </Box>
160
161
  <Box height={1} />
161
162
 
@@ -61,7 +61,6 @@ export function TuiSelectFieldDialog(props: SelectFieldDialogProps) {
61
61
  return (
62
62
  <DialogFrame
63
63
  title={field.label}
64
- footer="Use ↑↓ to choose · enter to apply · esc cancel"
65
64
  >
66
65
  <ListView
67
66
  items={field.options.map((option) => ({
@@ -158,7 +157,6 @@ function TuiTextFieldDialog(props: TextFieldDialogProps) {
158
157
  return (
159
158
  <DialogFrame
160
159
  title={field.label}
161
- footer="Enter to apply · esc cancel"
162
160
  >
163
161
  <Box flexDirection="column" gap={1}>
164
162
  <TextField
@@ -29,7 +29,7 @@ export function TuiPaneSettingsDialogBody({
29
29
  onActivate: (field: PaneSettingField | undefined) => void;
30
30
  }) {
31
31
  return (
32
- <DialogFrame title={title} footer="Use ↑↓ to choose · enter to edit · esc cancel">
32
+ <DialogFrame title={title}>
33
33
  <ListView
34
34
  items={fields.map((field) => ({
35
35
  id: field.key,
@@ -3,7 +3,6 @@ import {
3
3
  useEffect,
4
4
  useMemo,
5
5
  useRef,
6
- useState,
7
6
  type ReactNode,
8
7
  type RefObject,
9
8
  } from "react";
@@ -61,8 +60,6 @@ export interface TickerListTableViewProps {
61
60
  setHoveredIdx?: (index: number | null) => void;
62
61
  keyboardNavigation?: boolean;
63
62
  onRootKeyDown?: (event: DataTableKeyEvent) => boolean | void;
64
- onSelectIndex?: (index: number, ticker: TickerRecord) => void;
65
- onActivateIndex?: (index: number, ticker: TickerRecord) => void;
66
63
  onVisibleRangeChange?: (range: TickerListVisibleRange) => void;
67
64
  visibleRangeBuffer?: number;
68
65
  resetScrollKey?: unknown;
@@ -84,12 +81,18 @@ const FLASHABLE_QUOTE_COLUMN_IDS = new Set([
84
81
  "bid",
85
82
  "ask",
86
83
  "spread",
84
+ "spread_pct",
87
85
  "ext_hours",
88
86
  "market_cap",
87
+ "volume",
88
+ "dollar_volume",
89
+ "range_52w",
89
90
  "mkt_value",
91
+ "weight",
90
92
  "day_pnl",
91
93
  "pnl",
92
94
  "pnl_pct",
95
+ "mark_delta",
93
96
  ]);
94
97
 
95
98
  const EMPTY_FLASH_SYMBOLS = new Map<string, QuoteFlashDirection>();
@@ -120,8 +123,6 @@ export function TickerListTableView({
120
123
  setHoveredIdx,
121
124
  keyboardNavigation = true,
122
125
  onRootKeyDown,
123
- onSelectIndex,
124
- onActivateIndex,
125
126
  onVisibleRangeChange,
126
127
  visibleRangeBuffer = 0,
127
128
  resetScrollKey,
@@ -153,8 +154,6 @@ export function TickerListTableView({
153
154
  () => tickers.findIndex((ticker) => ticker.metadata.ticker === cursorSymbol),
154
155
  [cursorSymbol, tickers],
155
156
  );
156
- const [scrollToIndex, setScrollToIndex] = useState<number | null>(null);
157
- const [scrollToIndexVersion, setScrollToIndexVersion] = useState(0);
158
157
  const lastScrollCursorSymbolRef = useRef<string | null | undefined>(undefined);
159
158
 
160
159
  const emitVisibleRange = useCallback(() => {
@@ -177,16 +176,12 @@ export function TickerListTableView({
177
176
  useEffect(() => {
178
177
  const shouldScrollToCursor = lastScrollCursorSymbolRef.current !== cursorSymbol;
179
178
  lastScrollCursorSymbolRef.current = cursorSymbol;
180
- if (shouldScrollToCursor && selectedIndex >= 0) {
181
- setScrollToIndex(selectedIndex);
182
- setScrollToIndexVersion((current) => current + 1);
183
- }
184
- queueMicrotask(emitVisibleRange);
179
+ if (shouldScrollToCursor && selectedIndex >= 0) queueMicrotask(emitVisibleRange);
185
180
  }, [cursorSymbol, emitVisibleRange, selectedIndex]);
186
181
 
187
182
  useEffect(() => {
188
183
  queueMicrotask(emitVisibleRange);
189
- }, [emitVisibleRange, scrollToIndexVersion]);
184
+ }, [emitVisibleRange]);
190
185
 
191
186
  const renderCell = useCallback((
192
187
  ticker: TickerRecord,
@@ -236,18 +231,16 @@ export function TickerListTableView({
236
231
  }, [financialsMap, renderer, showContextMenu]);
237
232
 
238
233
  const handleRowMouseDown = useCallback((ticker: TickerRecord, _index: number, event: TableMouseEvent) => {
239
- setCursorSymbol(ticker.metadata.ticker);
240
234
  if (event.button !== 2) return false;
241
235
  if (nativeContextMenu !== true) {
242
236
  showTickerContextMenu(ticker, event);
243
237
  }
244
238
  return true;
245
- }, [nativeContextMenu, setCursorSymbol, showTickerContextMenu]);
239
+ }, [nativeContextMenu, showTickerContextMenu]);
246
240
 
247
241
  const handleRowContextMenu = useCallback((ticker: TickerRecord, _index: number, event: TableMouseEvent) => {
248
- setCursorSymbol(ticker.metadata.ticker);
249
242
  showTickerContextMenu(ticker, event);
250
- }, [setCursorSymbol, showTickerContextMenu]);
243
+ }, [showTickerContextMenu]);
251
244
 
252
245
  return (
253
246
  <DataTableView<TickerRecord, ColumnConfig>
@@ -258,9 +251,13 @@ export function TickerListTableView({
258
251
  sortDirection={sortDirection}
259
252
  onHeaderClick={onHeaderClick ?? (() => {})}
260
253
  getItemKey={(ticker) => ticker.metadata.ticker}
261
- isSelected={(ticker) => ticker.metadata.ticker === cursorSymbol}
262
- onSelect={(ticker) => {
263
- setCursorSymbol(ticker.metadata.ticker);
254
+ selection={{
255
+ kind: "id",
256
+ selectedId: cursorSymbol,
257
+ getId: (ticker) => ticker.metadata.ticker,
258
+ onChange: (symbol) => {
259
+ setCursorSymbol(symbol);
260
+ },
264
261
  }}
265
262
  onActivate={(ticker) => {
266
263
  onRowActivate?.(ticker);
@@ -273,9 +270,6 @@ export function TickerListTableView({
273
270
  emptyStateHint={emptyHint}
274
271
  virtualize={virtualize}
275
272
  overscan={overscan}
276
- selectedIndex={selectedIndex}
277
- onSelectIndex={onSelectIndex}
278
- onActivateIndex={onActivateIndex}
279
273
  rootBefore={rootBefore}
280
274
  rootAfter={rootAfter}
281
275
  rootWidth={rootWidth}
@@ -290,8 +284,6 @@ export function TickerListTableView({
290
284
  keyboardNavigation={keyboardNavigation}
291
285
  onRootKeyDown={onRootKeyDown}
292
286
  resetScrollKey={resetScrollKey}
293
- scrollToIndex={scrollToIndex}
294
- scrollToIndexVersion={scrollToIndexVersion}
295
287
  />
296
288
  );
297
289
  }
@@ -17,6 +17,7 @@ export interface ChoiceDialogChoice {
17
17
  export interface ChoiceDialogProps extends PromptContext<string> {
18
18
  title: string;
19
19
  choices: ChoiceDialogChoice[];
20
+ selectedChoiceId?: string;
20
21
  footer?: string;
21
22
  bgColor?: string;
22
23
  }
@@ -30,14 +31,23 @@ function getChoiceDescription(choice: ChoiceDialogChoice | undefined): string {
30
31
  return choice?.description ?? "";
31
32
  }
32
33
 
34
+ function getInitialChoiceIndex(choices: ChoiceDialogChoice[], selectedChoiceId: string | undefined): number {
35
+ if (!selectedChoiceId) return 0;
36
+ const selectedIndex = choices.findIndex((choice) => choice.id === selectedChoiceId);
37
+ return selectedIndex >= 0 ? selectedIndex : 0;
38
+ }
39
+
33
40
  export function ChoiceDialog({
34
41
  resolve,
35
42
  title,
36
43
  choices,
37
- footer = "↑↓ choose · Enter/click select · Esc cancel",
44
+ selectedChoiceId,
45
+ footer,
38
46
  bgColor = colors.bg,
39
47
  }: ChoiceDialogProps) {
40
- const [index, setIndex] = useState(() => clampChoiceIndex(0, choices.length));
48
+ const [index, setIndex] = useState(() =>
49
+ clampChoiceIndex(getInitialChoiceIndex(choices, selectedChoiceId), choices.length)
50
+ );
41
51
  const selectedIndex = clampChoiceIndex(index, choices.length);
42
52
  const selectedChoice = selectedIndex >= 0 ? choices[selectedIndex] : undefined;
43
53
  const items = useMemo<ListViewItem[]>(() => choices.map((choice) => ({
@@ -2,7 +2,7 @@ import { Box, Text, useUiHost } from "../../../ui";
2
2
  import { TextAttributes } from "../../../ui";
3
3
  import { useShortcut } from "../../../react/input";
4
4
  import { type AlertContext, useDialog, useDialogKeyboard } from "../../../ui/dialog";
5
- import { type ReactNode, useEffect, useMemo, useState } from "react";
5
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { colors } from "../../../theme/colors";
7
7
  import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
8
8
  import { isPlainKey } from "../../../utils/keyboard";
@@ -253,6 +253,7 @@ export function MultiSelectDialogButton({
253
253
  }: MultiSelectDialogButtonProps) {
254
254
  const isDesktopWeb = useUiHost().kind === "desktop-web";
255
255
  const dialog = useDialog();
256
+ const triggerMouseDownRef = useRef(false);
256
257
  const summary = summarizeMultiSelectValues({ options, selectedValues, emptyLabel });
257
258
  const buttonLabel = `${label}: ${summary}`;
258
259
  const buttonText = ` ${buttonLabel} `;
@@ -311,6 +312,17 @@ export function MultiSelectDialogButton({
311
312
  );
312
313
  }
313
314
 
315
+ const startTriggerPress = (event?: DialogTriggerEvent) => {
316
+ triggerMouseDownRef.current = true;
317
+ stopMouseEvent(event);
318
+ };
319
+ const finishTriggerPress = (event?: DialogTriggerEvent) => {
320
+ const startedOnTrigger = triggerMouseDownRef.current;
321
+ triggerMouseDownRef.current = false;
322
+ if (startedOnTrigger) openDialog(event);
323
+ else stopMouseEvent(event);
324
+ };
325
+
314
326
  return (
315
327
  <Box
316
328
  id={idPrefix ? `${idPrefix}:button` : undefined}
@@ -318,14 +330,14 @@ export function MultiSelectDialogButton({
318
330
  width={buttonText.length}
319
331
  flexDirection="row"
320
332
  backgroundColor={disabled ? colors.panel : colors.selected}
321
- onMouseDown={stopMouseEvent}
322
- onMouseUp={openDialog}
333
+ onMouseDown={startTriggerPress}
334
+ onMouseUp={finishTriggerPress}
323
335
  >
324
336
  <Text
325
337
  fg={disabled ? colors.textMuted : colors.selectedText}
326
338
  attributes={TextAttributes.BOLD}
327
- onMouseDown={stopMouseEvent}
328
- onMouseUp={openDialog}
339
+ onMouseDown={startTriggerPress}
340
+ onMouseUp={finishTriggerPress}
329
341
  >
330
342
  {buttonText}
331
343
  </Text>