gloomberb 0.8.2 → 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 (85) hide show
  1. package/README.md +9 -7
  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/app/global-shortcuts.test.tsx +54 -4
  5. package/src/app/global-shortcuts.ts +11 -5
  6. package/src/components/command-bar/action-runtime.ts +1 -0
  7. package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
  8. package/src/components/command-bar/layout-items/current-layout.ts +33 -0
  9. package/src/components/command-bar/list/view.tsx +3 -1
  10. package/src/components/command-bar/pane-templates/items.ts +4 -1
  11. package/src/components/command-bar/panel/body.tsx +1 -0
  12. package/src/components/command-bar/routes/root/results.ts +0 -30
  13. package/src/components/command-bar/routes/root/runtime.ts +2 -20
  14. package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
  15. package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
  16. package/src/components/command-bar/routing/launch-request.ts +14 -7
  17. package/src/components/command-bar/routing/list-state.ts +1 -3
  18. package/src/components/command-bar/surface/index.test.tsx +76 -4
  19. package/src/components/command-bar/surface/index.tsx +0 -2
  20. package/src/components/command-bar/surface/pane.test.tsx +1 -0
  21. package/src/components/command-bar/surface/test-harness.tsx +1 -0
  22. package/src/components/command-bar/view-model.ts +3 -3
  23. package/src/components/layout/shell/index.test.tsx +120 -2
  24. package/src/components/layout/shell/index.tsx +2 -0
  25. package/src/components/layout/shell/menu.ts +1 -7
  26. package/src/components/layout/shell/pane/actions.ts +8 -0
  27. package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
  28. package/src/components/layout/shell/shortcuts.ts +14 -6
  29. package/src/components/layout/shell/window-mode/index.ts +8 -3
  30. package/src/components/layout/window-edit/presentation.ts +4 -4
  31. package/src/components/onboarding/onboarding-steps.tsx +3 -2
  32. package/src/components/ticker/list-table-view.tsx +6 -0
  33. package/src/components/ui/remote-image.tsx +1 -14
  34. package/src/components/ui/tabs.tsx +95 -80
  35. package/src/components/ui/ui.test.tsx +54 -0
  36. package/src/core/state/app/types.ts +12 -4
  37. package/src/plugins/builtin/chat/content/channel-navigation.ts +44 -11
  38. package/src/plugins/builtin/chat/content/index.tsx +5 -4
  39. package/src/plugins/builtin/chat/content/shortcuts.ts +7 -0
  40. package/src/plugins/builtin/chat/sidebar.test.tsx +71 -0
  41. package/src/plugins/builtin/help/components.tsx +1 -1
  42. package/src/plugins/builtin/help/index.test.tsx +105 -30
  43. package/src/plugins/builtin/help/index.tsx +324 -168
  44. package/src/plugins/builtin/help/shortcut-model.ts +5 -6
  45. package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
  46. package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
  47. package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
  48. package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
  49. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
  50. package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
  51. package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
  52. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
  53. package/src/plugins/builtin/substack/api/auth.ts +185 -0
  54. package/src/plugins/builtin/substack/api/cache.ts +78 -0
  55. package/src/plugins/builtin/substack/api/loaders.ts +146 -0
  56. package/src/plugins/builtin/substack/api/store.ts +186 -0
  57. package/src/plugins/builtin/substack/api/types.ts +40 -0
  58. package/src/plugins/builtin/substack/api.test.ts +298 -0
  59. package/src/plugins/builtin/substack/article-detail.tsx +359 -0
  60. package/src/plugins/builtin/substack/article-stack.tsx +129 -0
  61. package/src/plugins/builtin/substack/content.ts +478 -0
  62. package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
  63. package/src/plugins/builtin/substack/index.tsx +49 -0
  64. package/src/plugins/builtin/substack/login-view.tsx +126 -0
  65. package/src/plugins/builtin/substack/model.test.ts +155 -0
  66. package/src/plugins/builtin/substack/normalize.ts +343 -0
  67. package/src/plugins/builtin/substack/pane-footer.ts +71 -0
  68. package/src/plugins/builtin/substack/pane-state.ts +170 -0
  69. package/src/plugins/builtin/substack/pane.tsx +500 -0
  70. package/src/plugins/builtin/substack/table.ts +111 -0
  71. package/src/plugins/builtin/substack/types.ts +71 -0
  72. package/src/plugins/builtin/substack/utils.ts +131 -0
  73. package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
  74. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  75. package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
  76. package/src/plugins/catalog-ui.ts +2 -0
  77. package/src/plugins/pane-manager/layout-state.ts +11 -0
  78. package/src/plugins/pane-manager.ts +1 -1
  79. package/src/react/use-throttled-commit-value.ts +116 -0
  80. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
  81. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
  82. package/src/renderers/electrobun/view/http-fetch.ts +10 -3
  83. package/src/state/app/context/index.test.tsx +16 -0
  84. package/src/tickers/search/index.ts +1 -0
  85. package/src/version.ts +1 -1
@@ -81,13 +81,35 @@ describe("CommandBar", () => {
81
81
 
82
82
  const frame = testSetup.captureCharFrame();
83
83
  expect(frame).toContain("Commands");
84
- expect(frame).toContain("Tickers");
85
- expect(frame).toContain("AAPL");
84
+ expect(frame).not.toContain("Tickers");
86
85
  expect(frame).toContain("Panes");
87
86
  expect(frame).toContain("Portfolio");
88
87
  expect(frame).toContain("Help");
89
88
  });
90
89
 
90
+ test("keeps generic command filtering separate from ticker search", async () => {
91
+ const searchQueries: string[] = [];
92
+ testSetup = await testRender(<CommandBarHarness
93
+ query="MSFT"
94
+ dataProvider={makeDataProvider(async (query) => {
95
+ searchQueries.push(query);
96
+ return [];
97
+ })}
98
+ />, {
99
+ width: 80,
100
+ height: 24,
101
+ });
102
+
103
+ await testSetup.renderOnce();
104
+ await Bun.sleep(260);
105
+ await testSetup.renderOnce();
106
+
107
+ const frame = testSetup.captureCharFrame();
108
+ expect(frame).not.toContain("Tickers");
109
+ expect(frame).not.toContain("Microsoft Corp.");
110
+ expect(searchQueries).toEqual([]);
111
+ });
112
+
91
113
  test("runs check for updates from the command bar", async () => {
92
114
  const calls: number[] = [];
93
115
 
@@ -416,6 +438,56 @@ describe("CommandBar", () => {
416
438
  expect(frame).toContain("AMD");
417
439
  });
418
440
 
441
+ test("opens ticker search from a launch request with saved ticker metadata", async () => {
442
+ testSetup = await testRender(<CommandBarHarness
443
+ query=""
444
+ extraTickers={[makeTicker("BRK.B", "Berkshire Hathaway Inc.", {
445
+ exchange: "NYSE",
446
+ assetCategory: "STK",
447
+ })]}
448
+ configureState={(state) => ({
449
+ ...state,
450
+ commandBarLaunchRequest: {
451
+ kind: "ticker-search",
452
+ query: "",
453
+ sequence: 1,
454
+ },
455
+ })}
456
+ />, {
457
+ width: 100,
458
+ height: 24,
459
+ });
460
+
461
+ const frame = await waitForFrameToContain("Security Description");
462
+ expectSingleBackControl(frame);
463
+ expect(frame).toContain("BRK.B");
464
+ expect(frame).toContain("Equity NYSE");
465
+ });
466
+
467
+ test("opens ticker search when activating the Ticker Research pane item without a ticker", async () => {
468
+ testSetup = await testRender(<CommandBarHarness query="ticker research" />, {
469
+ width: 100,
470
+ height: 24,
471
+ });
472
+
473
+ await testSetup.renderOnce();
474
+ const rootFrame = testSetup.captureCharFrame();
475
+ const tickerResearchRow = rootFrame
476
+ .split("\n")
477
+ .find((line) => line.includes("Ticker Research"));
478
+ expect(tickerResearchRow).toMatch(/\bT\s*$/);
479
+
480
+ await act(async () => {
481
+ testSetup!.mockInput.pressEnter();
482
+ await testSetup!.renderOnce();
483
+ });
484
+
485
+ const frame = testSetup.captureCharFrame();
486
+ expectSingleBackControl(frame);
487
+ expect(frame).toContain("Security Description");
488
+ expect(frame).toContain("Search tickers");
489
+ });
490
+
419
491
  test("keeps typed prefixes in the root query until a result is activated", async () => {
420
492
  testSetup = await testRender(<CommandBarHarness query="DES " />, {
421
493
  width: 80,
@@ -763,8 +835,8 @@ describe("CommandBar", () => {
763
835
  const rows = frame.split("\n");
764
836
  const savedHeadings = frame.split("\n").filter((line) => line.trim() === "Saved");
765
837
  const otherListingsHeadings = frame.split("\n").filter((line) => line.trim() === "Other Listings");
766
- const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL") && line.includes("NASDAQ"));
767
- const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP") && line.includes("NASDAQ"));
838
+ const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL"));
839
+ const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP"));
768
840
  expect(savedHeadings).toHaveLength(1);
769
841
  expect(otherListingsHeadings).toHaveLength(1);
770
842
  expect(aaplRow).toBeGreaterThanOrEqual(0);
@@ -110,7 +110,6 @@ export function CommandBar({
110
110
  getWorkflowFieldStringValue,
111
111
  getWorkflowInputRef,
112
112
  localTickerSearchResultItems,
113
- mapTickerSearchCandidateToResultItem,
114
113
  moveWorkflowFocus,
115
114
  nonShortcutPaneTemplateItems,
116
115
  openInlineConfirm,
@@ -193,7 +192,6 @@ export function CommandBar({
193
192
  getTickers: getTickerSearchTickers,
194
193
  hasPaneSettings,
195
194
  localTickerSearchResultItems,
196
- mapTickerSearchCandidateToResultItem,
197
195
  nativeListScrollRef,
198
196
  nonShortcutPaneTemplateItems,
199
197
  openModeRoute,
@@ -158,6 +158,7 @@ describe("CommandBar pane and layout routes", () => {
158
158
  expect(frame).toContain("Float Pane");
159
159
  expect(frame).toContain("Undo Layout Change");
160
160
  expect(frame).toContain("Current Layout");
161
+ expect(frame).toContain("Close All Floating Panes");
161
162
  });
162
163
 
163
164
  test("runs layout actions directly from root search", async () => {
@@ -174,6 +174,7 @@ function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () =>
174
174
  paneId: TICKER_RESEARCH_PANE_ID,
175
175
  label: "Ticker Research",
176
176
  description: "Open another research pane",
177
+ shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
177
178
  }],
178
179
  ["new-chat-pane", {
179
180
  id: "new-chat-pane",
@@ -40,7 +40,7 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
40
40
  const match = matchPrefix(query, commandList);
41
41
 
42
42
  if (!query.trim()) {
43
- return { kind: "default", badge: "BROWSE", hint: "Type a command, ticker, or prefix" };
43
+ return { kind: "default", badge: "BROWSE", hint: "Type a command or prefix" };
44
44
  }
45
45
 
46
46
  if (!match) {
@@ -100,9 +100,9 @@ export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?:
100
100
  return { label: "No layout actions match", detail: query.trim() || "Focused-pane and layout actions will appear here" };
101
101
  default:
102
102
  if (query.trim()) {
103
- return { label: `No matches for "${query.trim()}"`, detail: "Try a ticker, command name, or prefix" };
103
+ return { label: `No matches for "${query.trim()}"`, detail: "Try a command name or prefix. Use T for ticker search" };
104
104
  }
105
- return { label: "No results yet", detail: "Recent tickers and suggested commands will appear here" };
105
+ return { label: "No results yet", detail: "Suggested commands will appear here" };
106
106
  }
107
107
  }
108
108
 
@@ -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
 
@@ -81,12 +81,18 @@ const FLASHABLE_QUOTE_COLUMN_IDS = new Set([
81
81
  "bid",
82
82
  "ask",
83
83
  "spread",
84
+ "spread_pct",
84
85
  "ext_hours",
85
86
  "market_cap",
87
+ "volume",
88
+ "dollar_volume",
89
+ "range_52w",
86
90
  "mkt_value",
91
+ "weight",
87
92
  "day_pnl",
88
93
  "pnl",
89
94
  "pnl_pct",
95
+ "mark_delta",
90
96
  ]);
91
97
 
92
98
  const EMPTY_FLASH_SYMBOLS = new Map<string, QuoteFlashDirection>();
@@ -1,6 +1,5 @@
1
1
  import { Box, ImageSurface, Text } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
- import { ExternalLinkText } from "./external-link";
4
3
  import { normalizedHttpUrl } from "../../utils/url";
5
4
 
6
5
  export interface RemoteImageProps {
@@ -11,13 +10,6 @@ export interface RemoteImageProps {
11
10
  label?: string;
12
11
  }
13
12
 
14
- function truncate(value: string, width: number): string {
15
- if (width <= 0) return "";
16
- if (value.length <= width) return value;
17
- if (width <= 3) return value.slice(0, width);
18
- return `${value.slice(0, width - 3)}...`;
19
- }
20
-
21
13
  export function RemoteImage({
22
14
  src,
23
15
  alt = "Image",
@@ -37,13 +29,8 @@ export function RemoteImage({
37
29
  height={Math.max(4, height)}
38
30
  objectFit="contain"
39
31
  >
40
- <Box flexDirection="column" gap={1}>
32
+ <Box flexDirection="column">
41
33
  <Text fg={colors.textDim}>{label}</Text>
42
- <ExternalLinkText
43
- url={imageUrl}
44
- label={truncate(imageUrl, resolvedWidth)}
45
- color={colors.textBright}
46
- />
47
34
  </Box>
48
35
  </ImageSurface>
49
36
  );