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
|
@@ -44,26 +44,38 @@ function createRegistry(shortcutExecute?: () => void): PluginRegistry {
|
|
|
44
44
|
|
|
45
45
|
function ShortcutHarness({
|
|
46
46
|
dispatch,
|
|
47
|
+
focusedTickerSymbol = null,
|
|
47
48
|
pluginRegistry,
|
|
49
|
+
refreshTicker = () => {},
|
|
48
50
|
state,
|
|
49
51
|
}: {
|
|
50
52
|
dispatch: (action: AppAction) => void;
|
|
53
|
+
focusedTickerSymbol?: string | null;
|
|
51
54
|
pluginRegistry: PluginRegistry;
|
|
55
|
+
refreshTicker?: (symbol: string, exchange?: string, tickerOverride?: any, priority?: number) => void;
|
|
52
56
|
state: AppState;
|
|
53
57
|
}) {
|
|
54
58
|
useAppGlobalShortcuts({
|
|
55
59
|
dispatch,
|
|
56
|
-
focusedTickerSymbol
|
|
60
|
+
focusedTickerSymbol,
|
|
57
61
|
isDetachedWindow: false,
|
|
58
62
|
pluginRegistry,
|
|
59
|
-
refreshTicker
|
|
63
|
+
refreshTicker,
|
|
60
64
|
startUpdate: () => {},
|
|
61
65
|
state,
|
|
62
66
|
});
|
|
63
67
|
return <text>ready</text>;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
async function renderHarness(
|
|
70
|
+
async function renderHarness(
|
|
71
|
+
state: AppState,
|
|
72
|
+
registry: PluginRegistry,
|
|
73
|
+
dispatch: (action: AppAction) => void,
|
|
74
|
+
options: {
|
|
75
|
+
focusedTickerSymbol?: string | null;
|
|
76
|
+
refreshTicker?: (symbol: string, exchange?: string, tickerOverride?: any, priority?: number) => void;
|
|
77
|
+
} = {},
|
|
78
|
+
) {
|
|
67
79
|
testSetup = await createTestRenderer({ width: 40, height: 8 });
|
|
68
80
|
root = createRoot(testSetup.renderer);
|
|
69
81
|
act(() => {
|
|
@@ -71,14 +83,16 @@ async function renderHarness(state: AppState, registry: PluginRegistry, dispatch
|
|
|
71
83
|
<TestDialogProvider>
|
|
72
84
|
<ShortcutHarness
|
|
73
85
|
dispatch={dispatch}
|
|
86
|
+
focusedTickerSymbol={options.focusedTickerSymbol}
|
|
74
87
|
pluginRegistry={registry}
|
|
88
|
+
refreshTicker={options.refreshTicker}
|
|
75
89
|
state={state}
|
|
76
90
|
/>
|
|
77
91
|
</TestDialogProvider>,
|
|
78
92
|
);
|
|
79
93
|
});
|
|
80
94
|
await act(async () => {
|
|
81
|
-
await testSetup
|
|
95
|
+
await testSetup!.renderOnce();
|
|
82
96
|
});
|
|
83
97
|
}
|
|
84
98
|
|
|
@@ -121,6 +135,23 @@ describe("useAppGlobalShortcuts", () => {
|
|
|
121
135
|
expect(event.propagationStopped).toBe(true);
|
|
122
136
|
});
|
|
123
137
|
|
|
138
|
+
test("opens ticker search with backtick", async () => {
|
|
139
|
+
const actions: AppAction[] = [];
|
|
140
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-global-shortcuts"));
|
|
141
|
+
await renderHarness(state, createRegistry(), (action) => actions.push(action));
|
|
142
|
+
|
|
143
|
+
const event = await emitKeypress({ name: "`" });
|
|
144
|
+
|
|
145
|
+
expect(actions).toEqual([{
|
|
146
|
+
type: "SET_COMMAND_BAR",
|
|
147
|
+
open: true,
|
|
148
|
+
query: "",
|
|
149
|
+
launch: { kind: "ticker-search", query: "" },
|
|
150
|
+
}]);
|
|
151
|
+
expect(event.defaultPrevented).toBe(true);
|
|
152
|
+
expect(event.propagationStopped).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
124
155
|
test("does not run plain plugin shortcuts while input is captured", async () => {
|
|
125
156
|
let executed = 0;
|
|
126
157
|
const actions: AppAction[] = [];
|
|
@@ -163,4 +194,23 @@ describe("useAppGlobalShortcuts", () => {
|
|
|
163
194
|
expect(event.defaultPrevented).toBe(true);
|
|
164
195
|
expect(event.propagationStopped).toBe(true);
|
|
165
196
|
});
|
|
197
|
+
|
|
198
|
+
test("does not treat modified Shift-R as force refresh", async () => {
|
|
199
|
+
const refreshes: Array<{ symbol: string; priority?: number }> = [];
|
|
200
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-global-shortcuts-resize"));
|
|
201
|
+
state.tickers.set("AAPL", {
|
|
202
|
+
metadata: { ticker: "AAPL", exchange: "NASDAQ" },
|
|
203
|
+
} as any);
|
|
204
|
+
await renderHarness(state, createRegistry(), () => {}, {
|
|
205
|
+
refreshTicker: (symbol, _exchange, _ticker, priority) => {
|
|
206
|
+
refreshes.push({ symbol, priority });
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const event = await emitKeypress({ name: "r", ctrl: true, shift: true });
|
|
211
|
+
|
|
212
|
+
expect(refreshes).toEqual([]);
|
|
213
|
+
expect(event.defaultPrevented).toBe(false);
|
|
214
|
+
expect(event.propagationStopped).toBe(false);
|
|
215
|
+
});
|
|
166
216
|
});
|
|
@@ -62,7 +62,12 @@ export function useAppGlobalShortcuts({
|
|
|
62
62
|
if (!isDetachedWindow && event.name === "`" && !state.commandBarOpen) {
|
|
63
63
|
event.preventDefault();
|
|
64
64
|
event.stopPropagation();
|
|
65
|
-
dispatch({
|
|
65
|
+
dispatch({
|
|
66
|
+
type: "SET_COMMAND_BAR",
|
|
67
|
+
open: true,
|
|
68
|
+
query: "",
|
|
69
|
+
launch: { kind: "ticker-search", query: "" },
|
|
70
|
+
});
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
73
|
if (!isDetachedWindow && /^[1-9]$/.test(event.name ?? "") && event.ctrl && (state.config.layouts ?? []).length > 1) {
|
|
@@ -96,18 +101,19 @@ export function useAppGlobalShortcuts({
|
|
|
96
101
|
|
|
97
102
|
if (state.inputCaptured) return;
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
const hasShortcutModifier = event.ctrl || event.meta || event.super || event.alt;
|
|
105
|
+
if (!hasShortcutModifier && !isDetachedWindow && event.name === "q") {
|
|
100
106
|
rendererHost.requestExit();
|
|
101
|
-
} else if (event.name === "r") {
|
|
107
|
+
} else if (!hasShortcutModifier && event.name === "r") {
|
|
102
108
|
if (focusedTickerSymbol) {
|
|
103
109
|
const ticker = state.tickers.get(focusedTickerSymbol);
|
|
104
110
|
if (ticker) refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 0);
|
|
105
111
|
}
|
|
106
|
-
} else if (event.name === "R" || (event.name === "r" && event.shift)) {
|
|
112
|
+
} else if (!hasShortcutModifier && (event.name === "R" || (event.name === "r" && event.shift))) {
|
|
107
113
|
for (const ticker of state.tickers.values()) {
|
|
108
114
|
refreshTicker(ticker.metadata.ticker, ticker.metadata.exchange, ticker, 1);
|
|
109
115
|
}
|
|
110
|
-
} else if (event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
|
|
116
|
+
} else if (!hasShortcutModifier && event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
|
|
111
117
|
startUpdate(state.updateAvailable);
|
|
112
118
|
} else {
|
|
113
119
|
const disabledPlugins = new Set(state.config.disabledPlugins || []);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createInitialState, type AppAction, type AppState } from "../../../state/app/context";
|
|
3
|
+
import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../../types/config";
|
|
4
|
+
import type { PluginRegistry } from "../../../plugins/registry";
|
|
5
|
+
import type { CommandBarRoute } from "../workflow/types";
|
|
6
|
+
import type { LayoutItemsContext } from "./types";
|
|
7
|
+
import { buildCurrentLayoutItems } from "./current-layout";
|
|
8
|
+
|
|
9
|
+
type InlineConfirmOptions = Parameters<LayoutItemsContext["openInlineConfirm"]>[0];
|
|
10
|
+
|
|
11
|
+
function createFloatingLayoutFixture(): { layout: LayoutConfig; state: AppState } {
|
|
12
|
+
const config = createDefaultConfig("/tmp/gloomberb-layout-actions-test");
|
|
13
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
14
|
+
const firstDetailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
15
|
+
if (!mainPane || !firstDetailPane) throw new Error("missing default panes");
|
|
16
|
+
|
|
17
|
+
const secondDetailPane = {
|
|
18
|
+
...firstDetailPane,
|
|
19
|
+
instanceId: "ticker-detail:secondary",
|
|
20
|
+
};
|
|
21
|
+
const layout: LayoutConfig = {
|
|
22
|
+
dockRoot: { kind: "pane", instanceId: "portfolio-list:main" },
|
|
23
|
+
instances: [{ ...mainPane }, { ...firstDetailPane }, secondDetailPane],
|
|
24
|
+
floating: [
|
|
25
|
+
{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 },
|
|
26
|
+
{ instanceId: "ticker-detail:secondary", x: 8, y: 3, width: 30, height: 8 },
|
|
27
|
+
],
|
|
28
|
+
detached: [],
|
|
29
|
+
};
|
|
30
|
+
const state = createInitialState({
|
|
31
|
+
...config,
|
|
32
|
+
layout: cloneLayout(layout),
|
|
33
|
+
layouts: [{ name: "Default", layout: cloneLayout(layout) }],
|
|
34
|
+
});
|
|
35
|
+
return { layout, state };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createLayoutItemsContext(
|
|
39
|
+
options: {
|
|
40
|
+
confirmDangerousActions?: boolean;
|
|
41
|
+
layouts: LayoutConfig[];
|
|
42
|
+
confirmations: InlineConfirmOptions[];
|
|
43
|
+
},
|
|
44
|
+
): LayoutItemsContext {
|
|
45
|
+
const { layout, state } = createFloatingLayoutFixture();
|
|
46
|
+
return {
|
|
47
|
+
closeAll: () => {},
|
|
48
|
+
currentLayout: layout,
|
|
49
|
+
dispatch: (_action: AppAction) => {},
|
|
50
|
+
duplicatePane: () => {},
|
|
51
|
+
focusedPaneId: state.focusedPaneId,
|
|
52
|
+
notifyGridlockRevert: () => {},
|
|
53
|
+
openBuiltInWorkflow: () => {},
|
|
54
|
+
openInlineConfirm: (confirmOptions) => {
|
|
55
|
+
options.confirmations.push(confirmOptions);
|
|
56
|
+
},
|
|
57
|
+
persistLayoutChange: (nextLayout) => {
|
|
58
|
+
options.layouts.push(nextLayout);
|
|
59
|
+
},
|
|
60
|
+
pluginRegistry: {
|
|
61
|
+
getTermSizeFn: () => ({ width: 80, height: 24 }),
|
|
62
|
+
} as PluginRegistry,
|
|
63
|
+
pushRoute: (_route: CommandBarRoute) => {},
|
|
64
|
+
state,
|
|
65
|
+
...(options.confirmDangerousActions === undefined ? {} : { confirmDangerousActions: options.confirmDangerousActions }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("buildCurrentLayoutItems", () => {
|
|
70
|
+
test("closes all floating panes from the current layout", () => {
|
|
71
|
+
const layouts: LayoutConfig[] = [];
|
|
72
|
+
const confirmations: InlineConfirmOptions[] = [];
|
|
73
|
+
const items = buildCurrentLayoutItems(createLayoutItemsContext({ layouts, confirmations }));
|
|
74
|
+
|
|
75
|
+
const item = items.find((entry) => entry.id === "layout-close-all-floating");
|
|
76
|
+
expect(item).toMatchObject({
|
|
77
|
+
label: "Close All Floating Panes",
|
|
78
|
+
disabled: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
item?.action();
|
|
82
|
+
|
|
83
|
+
expect(confirmations).toEqual([]);
|
|
84
|
+
expect(layouts).toHaveLength(1);
|
|
85
|
+
expect(layouts[0]?.floating).toEqual([]);
|
|
86
|
+
expect(layouts[0]?.instances.map((instance) => instance.instanceId)).toEqual(["portfolio-list:main"]);
|
|
87
|
+
expect(layouts[0]?.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("confirms before closing all floating panes when dangerous actions are guarded", () => {
|
|
91
|
+
const layouts: LayoutConfig[] = [];
|
|
92
|
+
const confirmations: InlineConfirmOptions[] = [];
|
|
93
|
+
const items = buildCurrentLayoutItems(createLayoutItemsContext({
|
|
94
|
+
confirmDangerousActions: true,
|
|
95
|
+
layouts,
|
|
96
|
+
confirmations,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
items.find((entry) => entry.id === "layout-close-all-floating")?.action();
|
|
100
|
+
|
|
101
|
+
expect(layouts).toEqual([]);
|
|
102
|
+
expect(confirmations).toHaveLength(1);
|
|
103
|
+
expect(confirmations[0]).toMatchObject({
|
|
104
|
+
confirmId: "layout-close-all-floating",
|
|
105
|
+
title: "Close All Floating Panes",
|
|
106
|
+
confirmLabel: "Close Floating Panes",
|
|
107
|
+
tone: "danger",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
confirmations[0]?.onConfirm();
|
|
111
|
+
|
|
112
|
+
expect(layouts).toHaveLength(1);
|
|
113
|
+
expect(layouts[0]?.floating).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
gridlockAllPanes,
|
|
3
|
+
removeFloatingPanes,
|
|
3
4
|
} from "../../../plugins/pane-manager";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_LAYOUT,
|
|
@@ -21,6 +22,8 @@ export function buildCurrentLayoutItems({
|
|
|
21
22
|
state,
|
|
22
23
|
}: LayoutItemsContext): ResultItem[] {
|
|
23
24
|
const layoutHistory = state.layoutHistory[state.config.activeLayoutIndex];
|
|
25
|
+
const floatingPaneCount = currentLayout.floating.length;
|
|
26
|
+
const floatingPaneLabel = floatingPaneCount === 1 ? "floating pane" : "floating panes";
|
|
24
27
|
|
|
25
28
|
return [
|
|
26
29
|
{
|
|
@@ -89,6 +92,36 @@ export function buildCurrentLayoutItems({
|
|
|
89
92
|
closeAll({ revertThemePreview: false });
|
|
90
93
|
},
|
|
91
94
|
},
|
|
95
|
+
{
|
|
96
|
+
id: "layout-close-all-floating",
|
|
97
|
+
label: "Close All Floating Panes",
|
|
98
|
+
detail: floatingPaneCount > 0
|
|
99
|
+
? `Remove ${floatingPaneCount} ${floatingPaneLabel} from the current layout`
|
|
100
|
+
: "No floating panes in the current layout",
|
|
101
|
+
category: "Current Layout",
|
|
102
|
+
kind: "action",
|
|
103
|
+
disabled: floatingPaneCount === 0,
|
|
104
|
+
action: confirmDangerousActions
|
|
105
|
+
? () => {
|
|
106
|
+
if (floatingPaneCount === 0) return;
|
|
107
|
+
openInlineConfirm({
|
|
108
|
+
confirmId: "layout-close-all-floating",
|
|
109
|
+
title: "Close All Floating Panes",
|
|
110
|
+
body: [`Close ${floatingPaneCount} ${floatingPaneLabel}?`],
|
|
111
|
+
confirmLabel: "Close Floating Panes",
|
|
112
|
+
cancelLabel: "Back",
|
|
113
|
+
tone: "danger",
|
|
114
|
+
onConfirm: () => {
|
|
115
|
+
persistLayoutChange(removeFloatingPanes(currentLayout));
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
: () => {
|
|
120
|
+
if (floatingPaneCount === 0) return;
|
|
121
|
+
persistLayoutChange(removeFloatingPanes(currentLayout));
|
|
122
|
+
closeAll({ revertThemePreview: false });
|
|
123
|
+
},
|
|
124
|
+
},
|
|
92
125
|
{
|
|
93
126
|
id: "layout-rename",
|
|
94
127
|
label: "Rename Layout",
|
|
@@ -19,6 +19,7 @@ export type CommandBarListScrollEvent = {
|
|
|
19
19
|
|
|
20
20
|
interface CommandBarListHeaderProps {
|
|
21
21
|
kind: ListScreenState["kind"];
|
|
22
|
+
title: string;
|
|
22
23
|
query: string;
|
|
23
24
|
queryDisplayWidth: number;
|
|
24
25
|
nativePaneChrome: boolean;
|
|
@@ -36,6 +37,7 @@ interface CommandBarListHeaderProps {
|
|
|
36
37
|
|
|
37
38
|
export const CommandBarListHeader = memo(function CommandBarListHeader({
|
|
38
39
|
kind,
|
|
40
|
+
title,
|
|
39
41
|
query,
|
|
40
42
|
queryDisplayWidth,
|
|
41
43
|
nativePaneChrome,
|
|
@@ -66,7 +68,7 @@ export const CommandBarListHeader = memo(function CommandBarListHeader({
|
|
|
66
68
|
value={query}
|
|
67
69
|
onInput={onQueryChange}
|
|
68
70
|
onChange={onQueryChange}
|
|
69
|
-
placeholder={kind === "root" ? "Search" : "Filter"}
|
|
71
|
+
placeholder={kind === "root" ? "Search commands" : title === "Security Description" ? "Search tickers" : "Filter"}
|
|
70
72
|
focused
|
|
71
73
|
width={nativePaneChrome ? "100%" : queryDisplayWidth}
|
|
72
74
|
backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
|
|
@@ -141,7 +141,10 @@ export function buildPaneTemplateItem(options: {
|
|
|
141
141
|
const arg = options.createOptions?.arg;
|
|
142
142
|
|
|
143
143
|
const action = () => {
|
|
144
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
options.template.shortcut
|
|
146
|
+
&& (options.shortcutExecution || getPaneTemplateArgKind(options.template) === "ticker")
|
|
147
|
+
) {
|
|
145
148
|
options.runPaneTemplateShortcut(options.template, arg);
|
|
146
149
|
return;
|
|
147
150
|
}
|
|
@@ -130,6 +130,7 @@ export function CommandBarPanelBody({
|
|
|
130
130
|
{(visibleListState || currentRoute?.kind === "picker") && visibleListState && (
|
|
131
131
|
<CommandBarListHeader
|
|
132
132
|
kind={visibleListState.kind}
|
|
133
|
+
title={visibleListState.title}
|
|
133
134
|
query={visibleListState.query}
|
|
134
135
|
queryDisplayWidth={queryDisplayWidth}
|
|
135
136
|
nativePaneChrome={nativePaneChrome}
|
|
@@ -6,12 +6,10 @@ import type {
|
|
|
6
6
|
} from "../../../../types/plugin";
|
|
7
7
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
8
8
|
import { fuzzyFilter } from "../../../../utils/fuzzy-search";
|
|
9
|
-
import type { TickerSearchCandidate } from "../../../../tickers/search";
|
|
10
9
|
import { matchPrefix, type Command } from "../../commands/registry";
|
|
11
10
|
import { isCollectionCommand } from "../../helpers";
|
|
12
11
|
import type { ResultItem } from "../../list/model";
|
|
13
12
|
import type { parseRootShortcutIntent } from "./shortcuts";
|
|
14
|
-
import { createQuickLookTickerCandidates } from "../ticker-search/results";
|
|
15
13
|
import type { CommandBarRoute } from "../../workflow/types";
|
|
16
14
|
import { createRootCommandItemBuilder } from "./command-items";
|
|
17
15
|
import { buildRootShortcutItem } from "./shortcut-items";
|
|
@@ -55,7 +53,6 @@ export interface RootResultModelOptions {
|
|
|
55
53
|
getAvailablePaneShortcutTemplates: (query: string) => PaneTemplateDef[];
|
|
56
54
|
hasPaneSettings: (paneId: string) => boolean;
|
|
57
55
|
localTickerSearchResultItems: (query?: string, options?: { category?: string; limit?: number }) => ResultItem[];
|
|
58
|
-
mapTickerSearchCandidateToResultItem: (candidate: TickerSearchCandidate) => ResultItem;
|
|
59
56
|
nonShortcutPaneTemplateItems: (filterQuery?: string) => ResultItem[];
|
|
60
57
|
openModeRoute: (screen: "ticker-search" | "plugins" | "layout", initialQuery?: string) => void;
|
|
61
58
|
paneShortcutItems: (options?: PaneShortcutItemsOptions) => ResultItem[];
|
|
@@ -86,7 +83,6 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
|
|
|
86
83
|
getAvailablePaneShortcutTemplates,
|
|
87
84
|
hasPaneSettings,
|
|
88
85
|
localTickerSearchResultItems,
|
|
89
|
-
mapTickerSearchCandidateToResultItem,
|
|
90
86
|
nonShortcutPaneTemplateItems,
|
|
91
87
|
openModeRoute,
|
|
92
88
|
paneShortcutItems,
|
|
@@ -180,30 +176,6 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
|
|
|
180
176
|
const item = commandToItem(match.command);
|
|
181
177
|
if (item) items.push(item);
|
|
182
178
|
} else if (!rootQuery) {
|
|
183
|
-
const maxDefaultTickers = 5;
|
|
184
|
-
const recentSymbols = state.recentTickers.slice(0, maxDefaultTickers);
|
|
185
|
-
const recentTickers = recentSymbols
|
|
186
|
-
.map((symbol) => state.tickers.get(symbol))
|
|
187
|
-
.filter((ticker): ticker is NonNullable<typeof ticker> => (
|
|
188
|
-
ticker != null && createQuickLookTickerCandidates([ticker]).length > 0
|
|
189
|
-
));
|
|
190
|
-
if (recentTickers.length < maxDefaultTickers) {
|
|
191
|
-
const seen = new Set(recentSymbols);
|
|
192
|
-
for (const ticker of state.tickers.values()) {
|
|
193
|
-
if (recentTickers.length >= maxDefaultTickers) break;
|
|
194
|
-
if (createQuickLookTickerCandidates([ticker]).length === 0) continue;
|
|
195
|
-
if (!seen.has(ticker.metadata.ticker)) recentTickers.push(ticker);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
items.push(...recentTickers.flatMap((ticker) => {
|
|
199
|
-
const candidate = createQuickLookTickerCandidates([ticker])[0];
|
|
200
|
-
return candidate
|
|
201
|
-
? [{
|
|
202
|
-
...mapTickerSearchCandidateToResultItem(candidate),
|
|
203
|
-
category: "Tickers",
|
|
204
|
-
}]
|
|
205
|
-
: [];
|
|
206
|
-
}));
|
|
207
179
|
items.push(...paneShortcutItems());
|
|
208
180
|
for (const command of availableCommands) {
|
|
209
181
|
const item = commandToItem(command);
|
|
@@ -212,12 +184,10 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
|
|
|
212
184
|
items.push(...tickerActionItems());
|
|
213
185
|
items.push(...pluginCommandItems());
|
|
214
186
|
} else {
|
|
215
|
-
const tickerItems = localTickerSearchResultItems(undefined, { category: "Tickers" });
|
|
216
187
|
const commandItems = availableCommands
|
|
217
188
|
.map((command) => commandToItem(command))
|
|
218
189
|
.filter((item): item is ResultItem => item !== null);
|
|
219
190
|
const allItems = [
|
|
220
|
-
...tickerItems,
|
|
221
191
|
...commandItems,
|
|
222
192
|
...buildLayoutItems("", { confirmDangerousActions: true }),
|
|
223
193
|
...buildPaneSettingItems(state.focusedPaneId, rootQuery),
|
|
@@ -8,10 +8,6 @@ import type { TickerSearchCandidate } from "../../../../tickers/search";
|
|
|
8
8
|
import { matchPrefix, type Command } from "../../commands/registry";
|
|
9
9
|
import type { ResultItem } from "../../list/model";
|
|
10
10
|
import type { CommandBarRoute } from "../../workflow/types";
|
|
11
|
-
import {
|
|
12
|
-
isLikelyPlainTickerSearch,
|
|
13
|
-
normalizeCommandTickerSearchText,
|
|
14
|
-
} from "../ticker-search/results";
|
|
15
11
|
import { useTickerSearchRouteResults } from "../ticker-search/route";
|
|
16
12
|
import { buildRootResultModel, type RootResultModel } from "./results";
|
|
17
13
|
import { useRootProviderSearch } from "./provider-search";
|
|
@@ -47,7 +43,6 @@ interface UseCommandBarRootRuntimeOptions {
|
|
|
47
43
|
getTickers(): AppState["tickers"];
|
|
48
44
|
hasPaneSettings(paneId: string): boolean;
|
|
49
45
|
localTickerSearchResultItems(query?: string, options?: { category?: string; limit?: number }): ResultItem[];
|
|
50
|
-
mapTickerSearchCandidateToResultItem(candidate: TickerSearchCandidate): ResultItem;
|
|
51
46
|
nativeListScrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
52
47
|
nonShortcutPaneTemplateItems(filterQuery?: string): ResultItem[];
|
|
53
48
|
openModeRoute(screen: "ticker-search" | "plugins" | "layout", initialQuery?: string): void;
|
|
@@ -101,7 +96,6 @@ export function useCommandBarRootRuntime({
|
|
|
101
96
|
getTickers,
|
|
102
97
|
hasPaneSettings,
|
|
103
98
|
localTickerSearchResultItems,
|
|
104
|
-
mapTickerSearchCandidateToResultItem,
|
|
105
99
|
nativeListScrollRef,
|
|
106
100
|
nonShortcutPaneTemplateItems,
|
|
107
101
|
openModeRoute,
|
|
@@ -178,7 +172,6 @@ export function useCommandBarRootRuntime({
|
|
|
178
172
|
getAvailablePaneShortcutTemplates,
|
|
179
173
|
hasPaneSettings,
|
|
180
174
|
localTickerSearchResultItems,
|
|
181
|
-
mapTickerSearchCandidateToResultItem,
|
|
182
175
|
nonShortcutPaneTemplateItems,
|
|
183
176
|
openModeRoute,
|
|
184
177
|
paneShortcutItems,
|
|
@@ -206,7 +199,6 @@ export function useCommandBarRootRuntime({
|
|
|
206
199
|
getAvailablePaneShortcutTemplates,
|
|
207
200
|
hasPaneSettings,
|
|
208
201
|
localTickerSearchResultItems,
|
|
209
|
-
mapTickerSearchCandidateToResultItem,
|
|
210
202
|
nonShortcutPaneTemplateItems,
|
|
211
203
|
openModeRoute,
|
|
212
204
|
paneShortcutItems,
|
|
@@ -220,20 +212,10 @@ export function useCommandBarRootRuntime({
|
|
|
220
212
|
tickerActionItems,
|
|
221
213
|
]);
|
|
222
214
|
|
|
223
|
-
const rootPlainTickerSearchArg = useMemo(() => {
|
|
224
|
-
if (activeMatch || rootShortcutIntent.kind !== "none" || !isLikelyPlainTickerSearch(rootQuery)) return null;
|
|
225
|
-
const normalizedQuery = normalizeCommandTickerSearchText(rootQuery);
|
|
226
|
-
const hasExactNonTickerResult = rootResultModel.items.some((item) => (
|
|
227
|
-
item.kind !== "ticker"
|
|
228
|
-
&& item.kind !== "search"
|
|
229
|
-
&& normalizeCommandTickerSearchText(item.label) === normalizedQuery
|
|
230
|
-
));
|
|
231
|
-
return hasExactNonTickerResult ? null : rootQuery.trim();
|
|
232
|
-
}, [activeMatch, rootQuery, rootResultModel.items, rootShortcutIntent.kind]);
|
|
233
215
|
const rootSecurityDescriptionArg = activeMatch?.command.id === "security-description" && activeMatch.arg.length >= 1
|
|
234
216
|
? activeMatch.arg
|
|
235
217
|
: null;
|
|
236
|
-
const rootTickerSearchArg = rootSecurityDescriptionArg
|
|
218
|
+
const rootTickerSearchArg = rootSecurityDescriptionArg;
|
|
237
219
|
|
|
238
220
|
useEffect(() => {
|
|
239
221
|
if (currentRoute) return;
|
|
@@ -272,7 +254,7 @@ export function useCommandBarRootRuntime({
|
|
|
272
254
|
localTickerSearchResultItems,
|
|
273
255
|
portfolios: state.config.portfolios,
|
|
274
256
|
readTickerSearchCache,
|
|
275
|
-
rootPlainTickerSearchArg,
|
|
257
|
+
rootPlainTickerSearchArg: null,
|
|
276
258
|
rootResultItems: rootResultModel.items,
|
|
277
259
|
rootTickerSearchArg,
|
|
278
260
|
tickers: state.tickers,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
rankTickerSearchItems,
|
|
7
7
|
upsertTickerFromSearchResult,
|
|
8
8
|
type TickerSearchCandidate,
|
|
9
|
+
type TickerSearchInstrumentClass,
|
|
9
10
|
} from "../../../../tickers/search";
|
|
10
11
|
import type { ResultItem } from "../../list/model";
|
|
11
12
|
import {
|
|
@@ -45,12 +46,15 @@ export function useCommandBarTickerSearchActions({
|
|
|
45
46
|
}, [tickerRepository, dispatch, pluginRegistry.events, focusTicker, closeAll]);
|
|
46
47
|
|
|
47
48
|
const mapTickerSearchCandidateToResultItem = useCallback((candidate: TickerSearchCandidate): ResultItem => {
|
|
49
|
+
const detail = formatTickerSearchDetail(candidate);
|
|
50
|
+
const right = formatTickerSearchRight(candidate);
|
|
51
|
+
|
|
48
52
|
if (candidate.kind === "ticker" && candidate.ticker) {
|
|
49
53
|
return {
|
|
50
54
|
id: candidate.id,
|
|
51
55
|
label: candidate.label,
|
|
52
|
-
detail
|
|
53
|
-
right
|
|
56
|
+
detail,
|
|
57
|
+
right,
|
|
54
58
|
category: candidate.category,
|
|
55
59
|
kind: "ticker",
|
|
56
60
|
secondaryAction: () => {
|
|
@@ -67,8 +71,8 @@ export function useCommandBarTickerSearchActions({
|
|
|
67
71
|
return {
|
|
68
72
|
id: candidate.id,
|
|
69
73
|
label: candidate.label,
|
|
70
|
-
detail
|
|
71
|
-
right
|
|
74
|
+
detail,
|
|
75
|
+
right,
|
|
72
76
|
category: candidate.category,
|
|
73
77
|
kind: "search",
|
|
74
78
|
secondaryAction: () => openTickerResearch(candidate.result!, { forceNewPane: true }),
|
|
@@ -140,3 +144,37 @@ export function useCommandBarTickerSearchActions({
|
|
|
140
144
|
writeTickerSearchCache,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
147
|
+
|
|
148
|
+
function formatTickerSearchRight(candidate: TickerSearchCandidate): string | undefined {
|
|
149
|
+
const assetClass = formatInstrumentClass(candidate.instrumentClass);
|
|
150
|
+
const exchange = candidate.exchangeLabel || candidate.primaryExchangeLabel || candidate.right || "";
|
|
151
|
+
if (!exchange) return assetClass;
|
|
152
|
+
if (assetClass === "Other") return exchange;
|
|
153
|
+
return `${assetClass} ${exchange}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatTickerSearchDetail(candidate: TickerSearchCandidate): string {
|
|
157
|
+
const metadata = [
|
|
158
|
+
formatInstrumentClass(candidate.instrumentClass),
|
|
159
|
+
candidate.exchangeLabel,
|
|
160
|
+
candidate.primaryExchangeLabel && candidate.primaryExchangeLabel !== candidate.exchangeLabel
|
|
161
|
+
? candidate.primaryExchangeLabel
|
|
162
|
+
: null,
|
|
163
|
+
].filter((value): value is string => !!value);
|
|
164
|
+
return metadata.length > 0
|
|
165
|
+
? `${candidate.detail} | ${metadata.join(" | ")}`
|
|
166
|
+
: candidate.detail;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatInstrumentClass(instrumentClass: TickerSearchInstrumentClass): string {
|
|
170
|
+
switch (instrumentClass) {
|
|
171
|
+
case "equity":
|
|
172
|
+
return "Equity";
|
|
173
|
+
case "fund":
|
|
174
|
+
return "Fund";
|
|
175
|
+
case "derivative":
|
|
176
|
+
return "Derivative";
|
|
177
|
+
case "other":
|
|
178
|
+
return "Other";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -65,7 +65,10 @@ export function useTickerSearchRouteResults(options: {
|
|
|
65
65
|
|
|
66
66
|
const searchQuery = routeQuery.trim();
|
|
67
67
|
if (!searchQuery) {
|
|
68
|
-
|
|
68
|
+
searchRequestIdRef.current += 1;
|
|
69
|
+
setTickerSearchPending(false);
|
|
70
|
+
setTickerSearchResults(localTickerSearchResultItems(undefined, { limit: 8 }));
|
|
71
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import type { PluginRegistry } from "../../../plugins/registry";
|
|
3
|
+
import type { AppState } from "../../../state/app/context";
|
|
3
4
|
import type { CommandDef } from "../../../types/plugin";
|
|
4
5
|
|
|
5
|
-
interface CommandBarPluginLaunchRequest {
|
|
6
|
-
kind: "plugin-command";
|
|
7
|
-
commandId: string;
|
|
8
|
-
sequence: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
interface UseCommandBarLaunchRequestOptions {
|
|
12
7
|
activeTickerSymbol: string | null;
|
|
13
|
-
commandBarLaunchRequest:
|
|
8
|
+
commandBarLaunchRequest: AppState["commandBarLaunchRequest"];
|
|
14
9
|
commandBarOpen: boolean;
|
|
10
|
+
openModeRoute: (
|
|
11
|
+
screen: "ticker-search" | "plugins" | "layout",
|
|
12
|
+
initialQuery?: string,
|
|
13
|
+
payload?: Record<string, unknown>,
|
|
14
|
+
) => void;
|
|
15
15
|
openPluginCommandWorkflow: (
|
|
16
16
|
command: CommandDef,
|
|
17
17
|
options?: { values?: Record<string, string> },
|
|
@@ -23,6 +23,7 @@ export function useCommandBarLaunchRequest({
|
|
|
23
23
|
activeTickerSymbol,
|
|
24
24
|
commandBarLaunchRequest,
|
|
25
25
|
commandBarOpen,
|
|
26
|
+
openModeRoute,
|
|
26
27
|
openPluginCommandWorkflow,
|
|
27
28
|
pluginRegistry,
|
|
28
29
|
}: UseCommandBarLaunchRequestOptions) {
|
|
@@ -38,6 +39,11 @@ export function useCommandBarLaunchRequest({
|
|
|
38
39
|
if (processedLaunchSequenceRef.current === launch.sequence) return;
|
|
39
40
|
processedLaunchSequenceRef.current = launch.sequence;
|
|
40
41
|
|
|
42
|
+
if (launch.kind === "ticker-search") {
|
|
43
|
+
openModeRoute("ticker-search", launch.query ?? "");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
const command = pluginRegistry.commands.get(launch.commandId);
|
|
42
48
|
if (!command?.wizard || command.wizard.length === 0) return;
|
|
43
49
|
let values: Record<string, string> | undefined;
|
|
@@ -53,6 +59,7 @@ export function useCommandBarLaunchRequest({
|
|
|
53
59
|
activeTickerSymbol,
|
|
54
60
|
commandBarLaunchRequest,
|
|
55
61
|
commandBarOpen,
|
|
62
|
+
openModeRoute,
|
|
56
63
|
openPluginCommandWorkflow,
|
|
57
64
|
pluginRegistry,
|
|
58
65
|
]);
|
|
@@ -125,9 +125,7 @@ function buildRouteListState(options: BuildRouteListStateOptions): ListScreenSta
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
case "ticker-search": {
|
|
128
|
-
const results =
|
|
129
|
-
? tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload))
|
|
130
|
-
: [];
|
|
128
|
+
const results = tickerSearchResults.map((item) => adaptTickerSearchRouteResult(item, currentRoute.payload));
|
|
131
129
|
const emptyState = getEmptyState("search", currentRoute.query, currentRoute.query);
|
|
132
130
|
return {
|
|
133
131
|
kind: "mode",
|