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.
- package/README.md +9 -7
- package/package.json +14 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/src/app/global-shortcuts.test.tsx +54 -4
- package/src/app/global-shortcuts.ts +11 -5
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/ticker/list-table-view.tsx +6 -0
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/tabs.tsx +95 -80
- package/src/components/ui/ui.test.tsx +54 -0
- package/src/core/state/app/types.ts +12 -4
- package/src/plugins/builtin/chat/content/channel-navigation.ts +44 -11
- package/src/plugins/builtin/chat/content/index.tsx +5 -4
- package/src/plugins/builtin/chat/content/shortcuts.ts +7 -0
- package/src/plugins/builtin/chat/sidebar.test.tsx +71 -0
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +324 -168
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/tickers/search/index.ts +1 -0
- 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")
|
|
767
|
-
const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP")
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
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
|
|
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>{"
|
|
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"
|
|
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
|
);
|