gloomberb 0.4.1 → 0.5.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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -325,7 +325,7 @@ export function getScreenFooterLeft(route: CommandBarRoute | null): string {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
export function getScreenFooterRight(route: CommandBarRoute | null): string {
|
|
328
|
-
if (!route) return "esc
|
|
328
|
+
if (!route) return "esc cancel";
|
|
329
329
|
|
|
330
330
|
if (route.kind === "workflow") return "esc back";
|
|
331
331
|
if (route.kind === "confirm") return "backspace/esc back";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PaneTemplateDef } from "../../types/plugin";
|
|
1
|
+
import type { CommandDef, PaneTemplateDef } from "../../types/plugin";
|
|
2
2
|
import type { Command } from "./command-registry";
|
|
3
3
|
import { getPaneTemplateDisplayLabel } from "./pane-template-display";
|
|
4
4
|
|
|
@@ -26,10 +26,16 @@ export interface PaneTemplateShortcutIntent extends ShortcutIntentBase {
|
|
|
26
26
|
template: PaneTemplateDef;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface PluginCommandShortcutIntent extends ShortcutIntentBase {
|
|
30
|
+
source: "plugin-command";
|
|
31
|
+
command: CommandDef;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export type ShortcutIntent =
|
|
30
35
|
| { kind: "none" }
|
|
31
36
|
| CommandShortcutIntent
|
|
32
|
-
| PaneTemplateShortcutIntent
|
|
37
|
+
| PaneTemplateShortcutIntent
|
|
38
|
+
| PluginCommandShortcutIntent;
|
|
33
39
|
|
|
34
40
|
interface ShortcutParseCandidate {
|
|
35
41
|
prefix: string;
|
|
@@ -37,8 +43,9 @@ interface ShortcutParseCandidate {
|
|
|
37
43
|
description: string;
|
|
38
44
|
argKind: RootShortcutArgKind | null;
|
|
39
45
|
argPlaceholder?: string;
|
|
40
|
-
source: "command" | "pane-template";
|
|
46
|
+
source: "command" | "pane-template" | "plugin-command";
|
|
41
47
|
command?: Command;
|
|
48
|
+
pluginCommand?: CommandDef;
|
|
42
49
|
template?: PaneTemplateDef;
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -71,6 +78,7 @@ function inferShortcutArg(argKind: RootShortcutArgKind | null, activeTicker: str
|
|
|
71
78
|
|
|
72
79
|
function buildShortcutCandidates(
|
|
73
80
|
commands: Command[],
|
|
81
|
+
pluginCommands: CommandDef[],
|
|
74
82
|
paneTemplates: PaneTemplateDef[],
|
|
75
83
|
): ShortcutParseCandidate[] {
|
|
76
84
|
return [
|
|
@@ -85,6 +93,17 @@ function buildShortcutCandidates(
|
|
|
85
93
|
source: "command" as const,
|
|
86
94
|
command,
|
|
87
95
|
})),
|
|
96
|
+
...pluginCommands
|
|
97
|
+
.filter((command) => command.shortcut?.trim().length)
|
|
98
|
+
.map((command) => ({
|
|
99
|
+
prefix: normalizeShortcutPrefix(command.shortcut!),
|
|
100
|
+
label: command.label,
|
|
101
|
+
description: command.description ?? "",
|
|
102
|
+
argKind: command.shortcutArg?.kind ?? (command.shortcutArg ? "text" : null),
|
|
103
|
+
argPlaceholder: command.shortcutArg?.placeholder,
|
|
104
|
+
source: "plugin-command" as const,
|
|
105
|
+
pluginCommand: command,
|
|
106
|
+
})),
|
|
88
107
|
...paneTemplates
|
|
89
108
|
.filter((template) => template.shortcut?.prefix)
|
|
90
109
|
.map((template) => ({
|
|
@@ -102,11 +121,13 @@ function buildShortcutCandidates(
|
|
|
102
121
|
export function parseRootShortcutIntent({
|
|
103
122
|
query,
|
|
104
123
|
commands,
|
|
124
|
+
pluginCommands = [],
|
|
105
125
|
paneTemplates,
|
|
106
126
|
activeTicker,
|
|
107
127
|
}: {
|
|
108
128
|
query: string;
|
|
109
129
|
commands: Command[];
|
|
130
|
+
pluginCommands?: CommandDef[];
|
|
110
131
|
paneTemplates: PaneTemplateDef[];
|
|
111
132
|
activeTicker: string | null;
|
|
112
133
|
}): ShortcutIntent {
|
|
@@ -114,7 +135,7 @@ export function parseRootShortcutIntent({
|
|
|
114
135
|
if (!trimmed) return { kind: "none" };
|
|
115
136
|
|
|
116
137
|
const upper = trimmed.toUpperCase();
|
|
117
|
-
const match = buildShortcutCandidates(commands, paneTemplates).find((candidate) => (
|
|
138
|
+
const match = buildShortcutCandidates(commands, pluginCommands, paneTemplates).find((candidate) => (
|
|
118
139
|
upper === candidate.prefix || upper.startsWith(`${candidate.prefix} `)
|
|
119
140
|
));
|
|
120
141
|
if (!match) return { kind: "none" };
|
|
@@ -151,6 +172,14 @@ export function parseRootShortcutIntent({
|
|
|
151
172
|
};
|
|
152
173
|
}
|
|
153
174
|
|
|
175
|
+
if (match.source === "plugin-command" && match.pluginCommand) {
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
source: "plugin-command",
|
|
179
|
+
command: match.pluginCommand,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
154
183
|
return {
|
|
155
184
|
...base,
|
|
156
185
|
source: "pane-template",
|
|
@@ -45,15 +45,15 @@ describe("command bar view model helpers", () => {
|
|
|
45
45
|
test("returns footer hints for plugin toggles", () => {
|
|
46
46
|
expect(getFooterHints("plugins", false)).toEqual({
|
|
47
47
|
left: "up/down move enter select space toggle",
|
|
48
|
-
right: "esc
|
|
48
|
+
right: "esc cancel",
|
|
49
49
|
});
|
|
50
50
|
expect(getFooterHints("layout", false)).toEqual({
|
|
51
51
|
left: "up/down move enter select",
|
|
52
|
-
right: "esc
|
|
52
|
+
right: "esc cancel",
|
|
53
53
|
});
|
|
54
54
|
expect(getFooterHints("new-pane", false)).toEqual({
|
|
55
55
|
left: "up/down move enter select",
|
|
56
|
-
right: "esc
|
|
56
|
+
right: "esc cancel",
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -111,16 +111,16 @@ export function getFooterHints(mode: CommandBarMode, isNarrow: boolean): Command
|
|
|
111
111
|
if (mode === "plugins") {
|
|
112
112
|
return {
|
|
113
113
|
left: isNarrow ? "space toggle" : `${moveAndSelect} space toggle`,
|
|
114
|
-
right: "esc
|
|
114
|
+
right: "esc cancel",
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
if (mode === "layout") {
|
|
118
|
-
return { left: moveAndSelect, right: "esc
|
|
118
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
119
119
|
}
|
|
120
120
|
if (mode === "new-pane") {
|
|
121
|
-
return { left: moveAndSelect, right: "esc
|
|
121
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
122
122
|
}
|
|
123
|
-
return { left: moveAndSelect, right: "esc
|
|
123
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?: string): { label: string; detail: string } {
|
|
@@ -152,6 +152,16 @@ export function resolvePreferredCollectionTarget(
|
|
|
152
152
|
return options.some((option) => option.id === activeCollectionId) ? activeCollectionId : null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function resolveSoleCollectionTarget(
|
|
156
|
+
state: AppState,
|
|
157
|
+
kind: CollectionKind,
|
|
158
|
+
action: CollectionMembershipAction,
|
|
159
|
+
ticker: TickerRecord | null,
|
|
160
|
+
): string | null {
|
|
161
|
+
const options = getCollectionTargetOptions(state, kind, action, ticker);
|
|
162
|
+
return options.length === 1 ? options[0]!.id : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
export async function resolveTickerInput(
|
|
156
166
|
rawInput: string | undefined,
|
|
157
167
|
activeTicker: string | null,
|
|
@@ -7,6 +7,7 @@ import { useAppState } from "../../state/app-context";
|
|
|
7
7
|
import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
8
8
|
import { useQuoteEntry, useResolvedEntryValue } from "../../market-data/hooks";
|
|
9
9
|
import { formatPercentRaw } from "../../utils/format";
|
|
10
|
+
import { formatMarketPrice } from "../../utils/market-format";
|
|
10
11
|
import { marketStateLabel, marketStateColor, getExtendedHoursInfo } from "../../utils/market-status";
|
|
11
12
|
import { VERSION } from "../../version";
|
|
12
13
|
|
|
@@ -103,7 +104,7 @@ export function Header() {
|
|
|
103
104
|
|
|
104
105
|
const spyColor = spyQuote ? priceColor(spyQuote.change) : colors.headerText;
|
|
105
106
|
const spyText = spyQuote
|
|
106
|
-
? `SPY ${spyQuote.price
|
|
107
|
+
? `SPY ${formatMarketPrice(spyQuote.price, { assetCategory: "ETF" })} ${formatPercentRaw(spyQuote.changePercent)}`
|
|
107
108
|
: "SPY —";
|
|
108
109
|
|
|
109
110
|
// Extended hours info
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
3
4
|
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
import { act, useReducer } from "react";
|
|
6
7
|
import { AppContext, appReducer, createInitialState } from "../../state/app-context";
|
|
8
|
+
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
9
|
+
import { setSharedDataProviderForTests } from "../../plugins/registry";
|
|
7
10
|
import { cloneLayout, createDefaultConfig } from "../../types/config";
|
|
8
11
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
9
12
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
10
13
|
import { portfolioListPlugin } from "../../plugins/builtin/portfolio-list";
|
|
11
14
|
import type { PaneProps } from "../../types/plugin";
|
|
15
|
+
import { StockChart } from "../chart/stock-chart";
|
|
12
16
|
import { StatusBar } from "./status-bar";
|
|
13
17
|
import { Header } from "./header";
|
|
14
18
|
import { buildNativeWindowState, finalizePaneDragRelease, resolveNativeDockDividers, Shell } from "./shell";
|
|
15
19
|
import type { DataProvider } from "../../types/data-provider";
|
|
16
|
-
import type { Quote } from "../../types/financials";
|
|
20
|
+
import type { PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
17
21
|
import type { TickerRecord } from "../../types/ticker";
|
|
18
22
|
|
|
19
23
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
@@ -25,6 +29,8 @@ afterEach(() => {
|
|
|
25
29
|
testSetup = undefined;
|
|
26
30
|
}
|
|
27
31
|
harnessState = null;
|
|
32
|
+
setSharedMarketDataCoordinator(null);
|
|
33
|
+
setSharedDataProviderForTests(undefined);
|
|
28
34
|
setSharedRegistryForTests(undefined);
|
|
29
35
|
});
|
|
30
36
|
|
|
@@ -134,31 +140,42 @@ function makeTicker(overrides: Partial<TickerRecord["metadata"]> = {}): TickerRe
|
|
|
134
140
|
};
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
function
|
|
143
|
+
function makePriceHistory(
|
|
144
|
+
length: number,
|
|
145
|
+
startDate = new Date(Date.UTC(2025, 0, 1)),
|
|
146
|
+
): PricePoint[] {
|
|
147
|
+
return Array.from({ length }, (_, index) => ({
|
|
148
|
+
date: new Date(startDate.getTime() + index * 24 * 3600_000),
|
|
149
|
+
open: 100 + index * 0.2,
|
|
150
|
+
high: 101 + index * 0.2,
|
|
151
|
+
low: 99 + index * 0.2,
|
|
152
|
+
close: 100.5 + index * 0.2,
|
|
153
|
+
volume: 1_000 + index * 10,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createChartShellDataProvider(historyBySymbol: Record<string, PricePoint[]>): DataProvider {
|
|
138
158
|
return {
|
|
139
|
-
id: "test",
|
|
140
|
-
name: "Test",
|
|
141
|
-
async
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
async getPriceHistory() {
|
|
160
|
-
return [];
|
|
161
|
-
},
|
|
159
|
+
id: "shell-test-provider",
|
|
160
|
+
name: "Shell Test Provider",
|
|
161
|
+
getTickerFinancials: async (symbol) => ({
|
|
162
|
+
annualStatements: [],
|
|
163
|
+
quarterlyStatements: [],
|
|
164
|
+
priceHistory: historyBySymbol[symbol] ?? [],
|
|
165
|
+
}),
|
|
166
|
+
getQuote: async (symbol) => ({
|
|
167
|
+
symbol,
|
|
168
|
+
price: historyBySymbol[symbol]?.[historyBySymbol[symbol]!.length - 1]?.close ?? 0,
|
|
169
|
+
currency: "USD",
|
|
170
|
+
change: 0,
|
|
171
|
+
changePercent: 0,
|
|
172
|
+
lastUpdated: Date.now(),
|
|
173
|
+
}),
|
|
174
|
+
getExchangeRate: async () => 1,
|
|
175
|
+
search: async () => [],
|
|
176
|
+
getNews: async () => [],
|
|
177
|
+
getArticleSummary: async () => null,
|
|
178
|
+
getPriceHistory: async (symbol) => historyBySymbol[symbol] ?? [],
|
|
162
179
|
};
|
|
163
180
|
}
|
|
164
181
|
|
|
@@ -256,7 +273,7 @@ function BrokerShellHarness({ pluginRegistry }: { pluginRegistry: PluginRegistry
|
|
|
256
273
|
return (
|
|
257
274
|
<AppContext value={{ state, dispatch }}>
|
|
258
275
|
<box flexDirection="column">
|
|
259
|
-
<Header
|
|
276
|
+
<Header />
|
|
260
277
|
<Shell pluginRegistry={pluginRegistry} />
|
|
261
278
|
<StatusBar />
|
|
262
279
|
</box>
|
|
@@ -264,6 +281,36 @@ function BrokerShellHarness({ pluginRegistry }: { pluginRegistry: PluginRegistry
|
|
|
264
281
|
);
|
|
265
282
|
}
|
|
266
283
|
|
|
284
|
+
function ChartShellHarness({
|
|
285
|
+
pluginRegistry,
|
|
286
|
+
config,
|
|
287
|
+
ticker,
|
|
288
|
+
financials,
|
|
289
|
+
focusedPaneId,
|
|
290
|
+
}: {
|
|
291
|
+
pluginRegistry: PluginRegistry;
|
|
292
|
+
config: ReturnType<typeof createDefaultConfig>;
|
|
293
|
+
ticker: TickerRecord;
|
|
294
|
+
financials: TickerFinancials;
|
|
295
|
+
focusedPaneId: string | null;
|
|
296
|
+
}) {
|
|
297
|
+
const initialState = createInitialState(config);
|
|
298
|
+
initialState.focusedPaneId = focusedPaneId;
|
|
299
|
+
initialState.tickers = new Map([[ticker.metadata.ticker, ticker]]);
|
|
300
|
+
initialState.financials = new Map([[ticker.metadata.ticker, financials]]);
|
|
301
|
+
|
|
302
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
303
|
+
harnessState = state;
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<AppContext value={{ state, dispatch }}>
|
|
307
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
308
|
+
<Shell pluginRegistry={pluginRegistry} />
|
|
309
|
+
</DialogProvider>
|
|
310
|
+
</AppContext>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
267
314
|
describe("Header", () => {
|
|
268
315
|
test("shows automatic self-update status for standalone binaries", async () => {
|
|
269
316
|
testSetup = await testRender(
|
|
@@ -524,13 +571,98 @@ describe("Shell", () => {
|
|
|
524
571
|
expect(testSetup.captureCharFrame()).toContain("Settings");
|
|
525
572
|
});
|
|
526
573
|
|
|
574
|
+
test("applies a chart resolution chip on the first click even when the chart pane is unfocused", async () => {
|
|
575
|
+
const symbol = "AAPL";
|
|
576
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-chart-test");
|
|
577
|
+
config.layout = cloneLayout(config.layout);
|
|
578
|
+
config.layouts = [{ name: "Default", layout: cloneLayout(config.layout) }];
|
|
579
|
+
config.layout.instances = config.layout.instances.map((instance) => (
|
|
580
|
+
instance.instanceId === "ticker-detail:main"
|
|
581
|
+
? {
|
|
582
|
+
...instance,
|
|
583
|
+
binding: { kind: "fixed" as const, symbol },
|
|
584
|
+
settings: {
|
|
585
|
+
...(instance.settings ?? {}),
|
|
586
|
+
chartResolution: "auto",
|
|
587
|
+
chartRangePreset: "5Y",
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
: instance
|
|
591
|
+
));
|
|
592
|
+
|
|
593
|
+
const ticker = makeTicker({ ticker: symbol, name: symbol });
|
|
594
|
+
const history = makePriceHistory(260);
|
|
595
|
+
const financials: TickerFinancials = {
|
|
596
|
+
annualStatements: [],
|
|
597
|
+
quarterlyStatements: [],
|
|
598
|
+
priceHistory: history,
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const provider = createChartShellDataProvider({ [symbol]: history });
|
|
602
|
+
setSharedDataProviderForTests(provider);
|
|
603
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
604
|
+
|
|
605
|
+
const pluginRegistry = createShellPluginRegistry({
|
|
606
|
+
tickerDetailComponent: (props) => (
|
|
607
|
+
<StockChart width={props.width} height={props.height} focused={props.focused} />
|
|
608
|
+
),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
testSetup = await testRender(
|
|
612
|
+
<ChartShellHarness
|
|
613
|
+
pluginRegistry={pluginRegistry}
|
|
614
|
+
config={config}
|
|
615
|
+
ticker={ticker}
|
|
616
|
+
financials={financials}
|
|
617
|
+
focusedPaneId="portfolio-list:main"
|
|
618
|
+
/>,
|
|
619
|
+
{ width: 120, height: 32 },
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
await testSetup.renderOnce();
|
|
623
|
+
await testSetup.renderOnce();
|
|
624
|
+
|
|
625
|
+
const initialLines = testSetup.captureCharFrame().split("\n");
|
|
626
|
+
const resolutionRow = initialLines.findIndex((line) => line.includes("AUTO 1M 5M 15M"));
|
|
627
|
+
const targetResolution = "1W";
|
|
628
|
+
expect(resolutionRow).toBeGreaterThanOrEqual(0);
|
|
629
|
+
const resolutionSpansBefore = testSetup.captureSpans().lines[resolutionRow]?.spans ?? [];
|
|
630
|
+
let resolutionCol = -1;
|
|
631
|
+
let spanColumn = 0;
|
|
632
|
+
for (const span of resolutionSpansBefore) {
|
|
633
|
+
const chipOffset = span.text.indexOf(targetResolution);
|
|
634
|
+
if (chipOffset >= 0) {
|
|
635
|
+
resolutionCol = spanColumn + chipOffset + Math.floor(targetResolution.length / 2);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
spanColumn += span.width;
|
|
639
|
+
}
|
|
640
|
+
expect(resolutionCol).toBeGreaterThanOrEqual(0);
|
|
641
|
+
expect(testSetup.captureCharFrame()).toContain("AAPL - AUTO");
|
|
642
|
+
|
|
643
|
+
await act(async () => {
|
|
644
|
+
await testSetup!.mockMouse.click(resolutionCol, resolutionRow);
|
|
645
|
+
await testSetup!.renderOnce();
|
|
646
|
+
});
|
|
647
|
+
await testSetup.renderOnce();
|
|
648
|
+
await testSetup.renderOnce();
|
|
649
|
+
|
|
650
|
+
expect(
|
|
651
|
+
harnessState?.config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main")?.settings?.chartResolution,
|
|
652
|
+
).toBe("1wk");
|
|
653
|
+
const resolutionSpans = testSetup.captureSpans().lines[resolutionRow]?.spans ?? [];
|
|
654
|
+
expect(
|
|
655
|
+
resolutionSpans.some((span) => span.text === targetResolution && (span.attributes & TextAttributes.BOLD) !== 0),
|
|
656
|
+
).toBe(true);
|
|
657
|
+
});
|
|
658
|
+
|
|
527
659
|
test("keeps the cash drawer and gridlock tip on distinct click rows in the full app layout", async () => {
|
|
528
660
|
const pluginRegistry = createBrokerPortfolioRegistry();
|
|
529
661
|
const layoutUpdates: unknown[] = [];
|
|
530
662
|
const toasts: string[] = [];
|
|
531
663
|
pluginRegistry.getLayoutFn = () => harnessState?.config.layout ?? { dockRoot: null, instances: [], floating: [] };
|
|
532
664
|
pluginRegistry.updateLayoutFn = (layout) => { layoutUpdates.push(layout); };
|
|
533
|
-
pluginRegistry.
|
|
665
|
+
pluginRegistry.notify = ({ body }: { body: string }) => { toasts.push(body); };
|
|
534
666
|
setSharedRegistryForTests(pluginRegistry);
|
|
535
667
|
|
|
536
668
|
testSetup = await testRender(
|
|
@@ -676,6 +808,33 @@ describe("Shell", () => {
|
|
|
676
808
|
expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
|
|
677
809
|
});
|
|
678
810
|
|
|
811
|
+
test("keeps a floating pane at the preview rect after a free drag release", () => {
|
|
812
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
|
|
813
|
+
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
814
|
+
if (!detailPane) throw new Error("missing detail pane");
|
|
815
|
+
const floatingOnlyLayout = {
|
|
816
|
+
dockRoot: null,
|
|
817
|
+
instances: [{ ...detailPane, binding: { kind: "fixed" as const, symbol: "AAPL" } }],
|
|
818
|
+
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8, zIndex: 75 }],
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const result = finalizePaneDragRelease(
|
|
822
|
+
floatingOnlyLayout,
|
|
823
|
+
"ticker-detail:main",
|
|
824
|
+
{ x: 14, y: 4, width: 30, height: 8 },
|
|
825
|
+
null,
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
expect(result.shouldShowGridlockTip).toBe(false);
|
|
829
|
+
expect(result.nextLayout.floating[0]).toEqual(expect.objectContaining({
|
|
830
|
+
instanceId: "ticker-detail:main",
|
|
831
|
+
x: 14,
|
|
832
|
+
y: 4,
|
|
833
|
+
width: 30,
|
|
834
|
+
height: 8,
|
|
835
|
+
}));
|
|
836
|
+
});
|
|
837
|
+
|
|
679
838
|
test("keeps the last floating pane body row visible above the border", async () => {
|
|
680
839
|
const config = createDefaultConfig("/tmp/gloomberb-shell-footer-test");
|
|
681
840
|
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveFloating,
|
|
20
20
|
simulateDrop,
|
|
21
21
|
swapPanes,
|
|
22
|
+
type DockDividerLayout,
|
|
22
23
|
type DockLeafLayout,
|
|
23
24
|
type DropTarget,
|
|
24
25
|
type FloatingRect,
|
|
@@ -778,8 +779,8 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
778
779
|
event.preventDefault();
|
|
779
780
|
return;
|
|
780
781
|
}
|
|
781
|
-
|
|
782
|
-
|
|
782
|
+
// Focus floating panes without consuming body clicks so child controls
|
|
783
|
+
// can act on the same initial mouse interaction.
|
|
783
784
|
return;
|
|
784
785
|
}
|
|
785
786
|
|
|
@@ -837,8 +838,8 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
837
838
|
event.preventDefault();
|
|
838
839
|
return;
|
|
839
840
|
}
|
|
840
|
-
|
|
841
|
-
|
|
841
|
+
// Focus docked panes without consuming body clicks so embedded widgets
|
|
842
|
+
// receive the first click instead of requiring a second one.
|
|
842
843
|
return;
|
|
843
844
|
}
|
|
844
845
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { testRender } from "@opentui/react/test-utils";
|
|
3
3
|
import { AppContext, createInitialState } from "../../state/app-context";
|
|
4
|
-
import { cloneLayout, createDefaultConfig } from "../../types/config";
|
|
4
|
+
import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types/config";
|
|
5
|
+
import type { AppNotificationRequest } from "../../types/plugin";
|
|
5
6
|
import { StatusBar } from "./status-bar";
|
|
6
7
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
7
8
|
|
|
@@ -89,13 +90,13 @@ describe("StatusBar", () => {
|
|
|
89
90
|
|
|
90
91
|
const actions: Array<{ type: string }> = [];
|
|
91
92
|
let updatedLayout = null as ReturnType<typeof cloneLayout> | null;
|
|
92
|
-
const
|
|
93
|
+
const notifications: AppNotificationRequest[] = [];
|
|
93
94
|
|
|
94
95
|
setSharedRegistryForTests({
|
|
95
96
|
getLayoutFn: () => state.config.layout,
|
|
96
97
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
97
|
-
updateLayoutFn: (layout) => { updatedLayout = layout; },
|
|
98
|
-
|
|
98
|
+
updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
|
|
99
|
+
notify: (notification: AppNotificationRequest) => { notifications.push(notification); },
|
|
99
100
|
Slot: () => null,
|
|
100
101
|
} as any);
|
|
101
102
|
|
|
@@ -119,8 +120,16 @@ describe("StatusBar", () => {
|
|
|
119
120
|
await testSetup.renderOnce();
|
|
120
121
|
|
|
121
122
|
expect(updatedLayout?.floating).toHaveLength(0);
|
|
122
|
-
expect(
|
|
123
|
+
expect(notifications).toHaveLength(1);
|
|
124
|
+
expect(notifications[0]).toMatchObject({
|
|
125
|
+
body: "Retiled all panes",
|
|
126
|
+
type: "success",
|
|
127
|
+
action: { label: "Revert" },
|
|
128
|
+
});
|
|
123
129
|
expect(actions).toContainEqual({ type: "DISMISS_GRIDLOCK_TIP" });
|
|
130
|
+
|
|
131
|
+
notifications[0]!.action!.onClick();
|
|
132
|
+
expect(actions).toContainEqual({ type: "UNDO_LAYOUT" });
|
|
124
133
|
});
|
|
125
134
|
|
|
126
135
|
test("auto-dismisses the gridlock tip after its timeout", async () => {
|
|
@@ -136,7 +145,7 @@ describe("StatusBar", () => {
|
|
|
136
145
|
const originalSetTimeout = globalThis.setTimeout;
|
|
137
146
|
const originalClearTimeout = globalThis.clearTimeout;
|
|
138
147
|
|
|
139
|
-
globalThis.setTimeout = ((callback:
|
|
148
|
+
globalThis.setTimeout = ((callback: Parameters<typeof setTimeout>[0], delay?: number) => {
|
|
140
149
|
timers.push({ callback: typeof callback === "function" ? callback : null, delay });
|
|
141
150
|
return 1 as unknown as ReturnType<typeof setTimeout>;
|
|
142
151
|
}) as typeof setTimeout;
|
|
@@ -146,7 +155,7 @@ describe("StatusBar", () => {
|
|
|
146
155
|
getLayoutFn: () => state.config.layout,
|
|
147
156
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
148
157
|
updateLayoutFn: () => {},
|
|
149
|
-
|
|
158
|
+
notify: () => {},
|
|
150
159
|
Slot: () => null,
|
|
151
160
|
} as any);
|
|
152
161
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../utils/market-status";
|
|
11
11
|
import { getSharedRegistry } from "../../plugins/registry";
|
|
12
12
|
import { gridlockAllPanes } from "../../plugins/pane-manager";
|
|
13
|
+
import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
|
|
13
14
|
|
|
14
15
|
const GRIDLOCK_TIP_DURATION_MS = 60_000;
|
|
15
16
|
|
|
@@ -60,7 +61,9 @@ export function StatusBar() {
|
|
|
60
61
|
if (!registry) return;
|
|
61
62
|
const { width, height } = registry.getTermSizeFn();
|
|
62
63
|
registry.updateLayoutFn(gridlockAllPanes(registry.getLayoutFn(), { x: 0, y: 0, width, height }));
|
|
63
|
-
registry.
|
|
64
|
+
notifyGridlockComplete(registry.notify.bind(registry), () => {
|
|
65
|
+
dispatch({ type: "UNDO_LAYOUT" });
|
|
66
|
+
});
|
|
64
67
|
dispatch({ type: "DISMISS_GRIDLOCK_TIP" });
|
|
65
68
|
};
|
|
66
69
|
|