gloomberb 0.3.7 → 0.4.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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, type RefObject } from "react";
|
|
1
|
+
import { memo, useRef, type RefObject } from "react";
|
|
2
2
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
3
|
import { EmptyState } from "./ui";
|
|
4
4
|
import { colors, hoverBg } from "../theme/colors";
|
|
@@ -13,6 +13,11 @@ export interface TickerTableCell {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type QuoteFlashDirection = "up" | "down" | "flat";
|
|
16
|
+
type ResolveTickerTableCell = (
|
|
17
|
+
column: ColumnConfig,
|
|
18
|
+
ticker: TickerRecord,
|
|
19
|
+
financials: TickerFinancials | undefined,
|
|
20
|
+
) => TickerTableCell;
|
|
16
21
|
|
|
17
22
|
const FLASHABLE_QUOTE_COLUMN_IDS = new Set([
|
|
18
23
|
"price",
|
|
@@ -39,6 +44,153 @@ function resolveQuoteFlashColor(direction: QuoteFlashDirection, fallbackColor: s
|
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
interface TickerListTableProps {
|
|
48
|
+
columns: ColumnConfig[];
|
|
49
|
+
tickers: TickerRecord[];
|
|
50
|
+
cursorSymbol: string | null;
|
|
51
|
+
hoveredIdx: number | null;
|
|
52
|
+
setHoveredIdx: (index: number | null) => void;
|
|
53
|
+
setCursorSymbol: (symbol: string) => void;
|
|
54
|
+
resolveCell: ResolveTickerTableCell;
|
|
55
|
+
financialsMap: Map<string, TickerFinancials>;
|
|
56
|
+
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
57
|
+
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
58
|
+
syncHeaderScroll?: () => void;
|
|
59
|
+
onBodyScrollActivity?: () => void;
|
|
60
|
+
flashSymbols?: Map<string, QuoteFlashDirection>;
|
|
61
|
+
sortColumnId?: string | null;
|
|
62
|
+
sortDirection?: "asc" | "desc";
|
|
63
|
+
onHeaderClick?: (columnId: string) => void;
|
|
64
|
+
onRowActivate?: (ticker: TickerRecord) => void;
|
|
65
|
+
emptyTitle?: string;
|
|
66
|
+
emptyHint?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const TickerListHeader = memo(function TickerListHeader({
|
|
70
|
+
columns,
|
|
71
|
+
headerScrollRef,
|
|
72
|
+
sortColumnId,
|
|
73
|
+
sortDirection,
|
|
74
|
+
onHeaderClick,
|
|
75
|
+
}: {
|
|
76
|
+
columns: ColumnConfig[];
|
|
77
|
+
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
78
|
+
sortColumnId?: string | null;
|
|
79
|
+
sortDirection?: "asc" | "desc";
|
|
80
|
+
onHeaderClick?: (columnId: string) => void;
|
|
81
|
+
}) {
|
|
82
|
+
return (
|
|
83
|
+
<scrollbox
|
|
84
|
+
ref={headerScrollRef}
|
|
85
|
+
height={1}
|
|
86
|
+
scrollX
|
|
87
|
+
focusable={false}
|
|
88
|
+
>
|
|
89
|
+
<box flexDirection="row" height={1} paddingX={1}>
|
|
90
|
+
{columns.map((column) => {
|
|
91
|
+
const isSorted = sortColumnId === column.id;
|
|
92
|
+
const indicator = isSorted ? (sortDirection === "asc" ? " \u25B2" : " \u25BC") : "";
|
|
93
|
+
const labelText = column.label + indicator;
|
|
94
|
+
return (
|
|
95
|
+
<box
|
|
96
|
+
key={column.id}
|
|
97
|
+
width={column.width + 1}
|
|
98
|
+
onMouseDown={onHeaderClick ? (event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
onHeaderClick(column.id);
|
|
101
|
+
} : undefined}
|
|
102
|
+
>
|
|
103
|
+
<text attributes={TextAttributes.BOLD} fg={isSorted ? colors.text : colors.textDim}>
|
|
104
|
+
{padTo(labelText, column.width, column.align)}
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</box>
|
|
110
|
+
</scrollbox>
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const TickerListRow = memo(function TickerListRow({
|
|
115
|
+
columns,
|
|
116
|
+
ticker,
|
|
117
|
+
index,
|
|
118
|
+
isSelected,
|
|
119
|
+
isHovered,
|
|
120
|
+
financials,
|
|
121
|
+
flashDirection,
|
|
122
|
+
setHoveredIdx,
|
|
123
|
+
setCursorSymbol,
|
|
124
|
+
resolveCell,
|
|
125
|
+
onRowActivate,
|
|
126
|
+
}: {
|
|
127
|
+
columns: ColumnConfig[];
|
|
128
|
+
ticker: TickerRecord;
|
|
129
|
+
index: number;
|
|
130
|
+
isSelected: boolean;
|
|
131
|
+
isHovered: boolean;
|
|
132
|
+
financials: TickerFinancials | undefined;
|
|
133
|
+
flashDirection: QuoteFlashDirection | undefined;
|
|
134
|
+
setHoveredIdx: (index: number | null) => void;
|
|
135
|
+
setCursorSymbol: (symbol: string) => void;
|
|
136
|
+
resolveCell: ResolveTickerTableCell;
|
|
137
|
+
onRowActivate?: (ticker: TickerRecord) => void;
|
|
138
|
+
}) {
|
|
139
|
+
const lastActivatedAtRef = useRef<number | null>(null);
|
|
140
|
+
const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<box
|
|
144
|
+
flexDirection="row"
|
|
145
|
+
height={1}
|
|
146
|
+
paddingX={1}
|
|
147
|
+
backgroundColor={rowBg}
|
|
148
|
+
onMouseMove={() => setHoveredIdx(index)}
|
|
149
|
+
onMouseDown={(event) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
setCursorSymbol(ticker.metadata.ticker);
|
|
152
|
+
if (!onRowActivate) return;
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
if (lastActivatedAtRef.current != null && now - lastActivatedAtRef.current <= 350) {
|
|
155
|
+
lastActivatedAtRef.current = null;
|
|
156
|
+
onRowActivate(ticker);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
lastActivatedAtRef.current = now;
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{columns.map((column) => {
|
|
163
|
+
const { text, color } = resolveCell(column, ticker, financials);
|
|
164
|
+
const baseFg = color || (isSelected ? colors.selectedText : colors.text);
|
|
165
|
+
const shouldFlash = flashDirection != null && FLASHABLE_QUOTE_COLUMN_IDS.has(column.id);
|
|
166
|
+
const cellFg = shouldFlash
|
|
167
|
+
? resolveQuoteFlashColor(flashDirection, baseFg)
|
|
168
|
+
: baseFg;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<box key={column.id} width={column.width + 1}>
|
|
172
|
+
<text fg={cellFg}>
|
|
173
|
+
{padTo(text, column.width, column.align)}
|
|
174
|
+
</text>
|
|
175
|
+
</box>
|
|
176
|
+
);
|
|
177
|
+
})}
|
|
178
|
+
</box>
|
|
179
|
+
);
|
|
180
|
+
}, (previous, next) => (
|
|
181
|
+
previous.columns === next.columns
|
|
182
|
+
&& previous.ticker === next.ticker
|
|
183
|
+
&& previous.index === next.index
|
|
184
|
+
&& previous.isSelected === next.isSelected
|
|
185
|
+
&& previous.isHovered === next.isHovered
|
|
186
|
+
&& previous.financials === next.financials
|
|
187
|
+
&& previous.flashDirection === next.flashDirection
|
|
188
|
+
&& previous.setHoveredIdx === next.setHoveredIdx
|
|
189
|
+
&& previous.setCursorSymbol === next.setCursorSymbol
|
|
190
|
+
&& previous.resolveCell === next.resolveCell
|
|
191
|
+
&& previous.onRowActivate === next.onRowActivate
|
|
192
|
+
));
|
|
193
|
+
|
|
42
194
|
export function TickerListTable({
|
|
43
195
|
columns,
|
|
44
196
|
tickers,
|
|
@@ -59,60 +211,18 @@ export function TickerListTable({
|
|
|
59
211
|
onRowActivate,
|
|
60
212
|
emptyTitle = "No tickers.",
|
|
61
213
|
emptyHint = "Press Ctrl+P to add one.",
|
|
62
|
-
}: {
|
|
63
|
-
columns: ColumnConfig[];
|
|
64
|
-
tickers: TickerRecord[];
|
|
65
|
-
cursorSymbol: string | null;
|
|
66
|
-
hoveredIdx: number | null;
|
|
67
|
-
setHoveredIdx: (index: number | null) => void;
|
|
68
|
-
setCursorSymbol: (symbol: string) => void;
|
|
69
|
-
resolveCell: (column: ColumnConfig, ticker: TickerRecord, financials: TickerFinancials | undefined) => TickerTableCell;
|
|
70
|
-
financialsMap: Map<string, TickerFinancials>;
|
|
71
|
-
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
72
|
-
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
73
|
-
syncHeaderScroll?: () => void;
|
|
74
|
-
onBodyScrollActivity?: () => void;
|
|
75
|
-
flashSymbols?: Map<string, QuoteFlashDirection>;
|
|
76
|
-
sortColumnId?: string | null;
|
|
77
|
-
sortDirection?: "asc" | "desc";
|
|
78
|
-
onHeaderClick?: (columnId: string) => void;
|
|
79
|
-
onRowActivate?: (ticker: TickerRecord) => void;
|
|
80
|
-
emptyTitle?: string;
|
|
81
|
-
emptyHint?: string;
|
|
82
|
-
}) {
|
|
214
|
+
}: TickerListTableProps) {
|
|
83
215
|
const safeFlashSymbols = flashSymbols ?? new Map<string, QuoteFlashDirection>();
|
|
84
|
-
const lastActivatedRef = useRef<{ symbol: string; at: number } | null>(null);
|
|
85
216
|
|
|
86
217
|
return (
|
|
87
218
|
<>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{columns.map((column) => {
|
|
96
|
-
const isSorted = sortColumnId === column.id;
|
|
97
|
-
const indicator = isSorted ? (sortDirection === "asc" ? " \u25B2" : " \u25BC") : "";
|
|
98
|
-
const labelText = column.label + indicator;
|
|
99
|
-
return (
|
|
100
|
-
<box
|
|
101
|
-
key={column.id}
|
|
102
|
-
width={column.width + 1}
|
|
103
|
-
onMouseDown={onHeaderClick ? (event) => {
|
|
104
|
-
event.preventDefault();
|
|
105
|
-
onHeaderClick(column.id);
|
|
106
|
-
} : undefined}
|
|
107
|
-
>
|
|
108
|
-
<text attributes={TextAttributes.BOLD} fg={isSorted ? colors.text : colors.textDim}>
|
|
109
|
-
{padTo(labelText, column.width, column.align)}
|
|
110
|
-
</text>
|
|
111
|
-
</box>
|
|
112
|
-
);
|
|
113
|
-
})}
|
|
114
|
-
</box>
|
|
115
|
-
</scrollbox>
|
|
219
|
+
<TickerListHeader
|
|
220
|
+
columns={columns}
|
|
221
|
+
headerScrollRef={headerScrollRef}
|
|
222
|
+
sortColumnId={sortColumnId}
|
|
223
|
+
sortDirection={sortDirection}
|
|
224
|
+
onHeaderClick={onHeaderClick}
|
|
225
|
+
/>
|
|
116
226
|
|
|
117
227
|
<scrollbox
|
|
118
228
|
ref={scrollRef}
|
|
@@ -131,54 +241,21 @@ export function TickerListTable({
|
|
|
131
241
|
</box>
|
|
132
242
|
) : (
|
|
133
243
|
tickers.map((ticker, index) => {
|
|
134
|
-
const isSelected = ticker.metadata.ticker === cursorSymbol;
|
|
135
|
-
const isHovered = index === hoveredIdx && !isSelected;
|
|
136
|
-
const financials = financialsMap.get(ticker.metadata.ticker);
|
|
137
|
-
const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
|
|
138
|
-
const flashDirection = safeFlashSymbols.get(ticker.metadata.ticker);
|
|
139
|
-
|
|
140
244
|
return (
|
|
141
|
-
<
|
|
245
|
+
<TickerListRow
|
|
142
246
|
key={ticker.metadata.ticker}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
lastActivatedRef.current = null;
|
|
156
|
-
onRowActivate(ticker);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
lastActivatedRef.current = {
|
|
160
|
-
symbol: ticker.metadata.ticker,
|
|
161
|
-
at: now,
|
|
162
|
-
};
|
|
163
|
-
}}
|
|
164
|
-
>
|
|
165
|
-
{columns.map((column) => {
|
|
166
|
-
const { text, color } = resolveCell(column, ticker, financials);
|
|
167
|
-
const baseFg = color || (isSelected ? colors.selectedText : colors.text);
|
|
168
|
-
const shouldFlash = flashDirection != null && FLASHABLE_QUOTE_COLUMN_IDS.has(column.id);
|
|
169
|
-
const cellFg = shouldFlash
|
|
170
|
-
? resolveQuoteFlashColor(flashDirection, baseFg)
|
|
171
|
-
: baseFg;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<box key={column.id} width={column.width + 1}>
|
|
175
|
-
<text fg={cellFg}>
|
|
176
|
-
{padTo(text, column.width, column.align)}
|
|
177
|
-
</text>
|
|
178
|
-
</box>
|
|
179
|
-
);
|
|
180
|
-
})}
|
|
181
|
-
</box>
|
|
247
|
+
columns={columns}
|
|
248
|
+
ticker={ticker}
|
|
249
|
+
index={index}
|
|
250
|
+
isSelected={ticker.metadata.ticker === cursorSymbol}
|
|
251
|
+
isHovered={index === hoveredIdx && ticker.metadata.ticker !== cursorSymbol}
|
|
252
|
+
financials={financialsMap.get(ticker.metadata.ticker)}
|
|
253
|
+
flashDirection={safeFlashSymbols.get(ticker.metadata.ticker)}
|
|
254
|
+
setHoveredIdx={setHoveredIdx}
|
|
255
|
+
setCursorSymbol={setCursorSymbol}
|
|
256
|
+
resolveCell={resolveCell}
|
|
257
|
+
onRowActivate={onRowActivate}
|
|
258
|
+
/>
|
|
182
259
|
);
|
|
183
260
|
})
|
|
184
261
|
)}
|
|
@@ -3,6 +3,7 @@ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
|
3
3
|
import { colors, hoverBg } from "../../theme/colors";
|
|
4
4
|
import type { ColumnConfig } from "../../types/config";
|
|
5
5
|
import { padTo } from "../../utils/format";
|
|
6
|
+
import { useDoubleClickActivation } from "../use-double-click-activation";
|
|
6
7
|
import { EmptyState } from "./status";
|
|
7
8
|
|
|
8
9
|
export type DataTableColumn = Pick<
|
|
@@ -34,6 +35,7 @@ export interface DataTableProps<
|
|
|
34
35
|
getItemKey: (item: T, index: number) => string;
|
|
35
36
|
isSelected: (item: T, index: number) => boolean;
|
|
36
37
|
onSelect: (item: T, index: number) => void;
|
|
38
|
+
onActivate?: (item: T, index: number) => void;
|
|
37
39
|
renderCell: (
|
|
38
40
|
item: T,
|
|
39
41
|
column: C,
|
|
@@ -46,6 +48,11 @@ export interface DataTableProps<
|
|
|
46
48
|
overscan?: number;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
interface DataTableRowPointerTarget<T> {
|
|
52
|
+
item: T;
|
|
53
|
+
index: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
50
57
|
columns,
|
|
51
58
|
items,
|
|
@@ -61,6 +68,7 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
61
68
|
getItemKey,
|
|
62
69
|
isSelected,
|
|
63
70
|
onSelect,
|
|
71
|
+
onActivate,
|
|
64
72
|
renderCell,
|
|
65
73
|
emptyStateTitle,
|
|
66
74
|
emptyStateHint,
|
|
@@ -80,6 +88,17 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
80
88
|
() => items.slice(startIndex, endIndex),
|
|
81
89
|
[endIndex, items, scrollVersion, startIndex],
|
|
82
90
|
);
|
|
91
|
+
const handleRowMouseDown =
|
|
92
|
+
useDoubleClickActivation<DataTableRowPointerTarget<T>>({
|
|
93
|
+
onSelect: ({ item, index }) => {
|
|
94
|
+
onSelect(item, index);
|
|
95
|
+
},
|
|
96
|
+
onActivate: onActivate
|
|
97
|
+
? ({ item, index }) => {
|
|
98
|
+
onActivate(item, index);
|
|
99
|
+
}
|
|
100
|
+
: undefined,
|
|
101
|
+
});
|
|
83
102
|
|
|
84
103
|
const handleBodyScrollActivity = () => {
|
|
85
104
|
if (virtualize) {
|
|
@@ -163,7 +182,10 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
163
182
|
onMouseMove={() => setHoveredIdx(index)}
|
|
164
183
|
onMouseDown={(event) => {
|
|
165
184
|
event.preventDefault();
|
|
166
|
-
|
|
185
|
+
handleRowMouseDown(getItemKey(item, index), {
|
|
186
|
+
item,
|
|
187
|
+
index,
|
|
188
|
+
});
|
|
167
189
|
}}
|
|
168
190
|
>
|
|
169
191
|
{columns.map((column) => {
|
|
@@ -182,7 +204,10 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
182
204
|
}
|
|
183
205
|
event.preventDefault();
|
|
184
206
|
event.stopPropagation?.();
|
|
185
|
-
|
|
207
|
+
handleRowMouseDown(getItemKey(item, index), {
|
|
208
|
+
item,
|
|
209
|
+
index,
|
|
210
|
+
});
|
|
186
211
|
}}
|
|
187
212
|
>
|
|
188
213
|
<text
|
|
@@ -43,6 +43,7 @@ export { Section, FieldRow, DialogFrame } from "./frame";
|
|
|
43
43
|
export type { SectionProps, FieldRowProps, DialogFrameProps } from "./frame";
|
|
44
44
|
|
|
45
45
|
export { ExternalLink, openUrl } from "./external-link";
|
|
46
|
+
export { PageStackView } from "./page-stack-view";
|
|
46
47
|
|
|
47
48
|
export { Spinner, ProgressBar, SkeletonRow, LoadingBlock } from "./loading";
|
|
48
49
|
export type {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { colors } from "../../theme/colors";
|
|
4
|
+
import { isBackNavigationKey } from "../../utils/back-navigation";
|
|
5
|
+
|
|
6
|
+
interface PageStackViewProps {
|
|
7
|
+
focused: boolean;
|
|
8
|
+
detailOpen: boolean;
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
rootContent: ReactNode;
|
|
11
|
+
detailContent: ReactNode;
|
|
12
|
+
backLabel?: string;
|
|
13
|
+
backHint?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PageStackView({
|
|
17
|
+
focused,
|
|
18
|
+
detailOpen,
|
|
19
|
+
onBack,
|
|
20
|
+
rootContent,
|
|
21
|
+
detailContent,
|
|
22
|
+
backLabel = "Back",
|
|
23
|
+
backHint,
|
|
24
|
+
}: PageStackViewProps) {
|
|
25
|
+
useKeyboard((event) => {
|
|
26
|
+
if (!focused || !detailOpen || !isBackNavigationKey(event)) return;
|
|
27
|
+
event.stopPropagation?.();
|
|
28
|
+
event.preventDefault?.();
|
|
29
|
+
onBack();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!detailOpen) {
|
|
33
|
+
return (
|
|
34
|
+
<box flexDirection="column" flexGrow={1}>
|
|
35
|
+
{rootContent}
|
|
36
|
+
</box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<box flexDirection="column" flexGrow={1}>
|
|
42
|
+
<box height={1} flexDirection="row">
|
|
43
|
+
<box
|
|
44
|
+
onMouseDown={(event) => {
|
|
45
|
+
event.preventDefault?.();
|
|
46
|
+
event.stopPropagation?.();
|
|
47
|
+
onBack();
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<text fg={colors.textBright}>{`<- ${backLabel}`}</text>
|
|
51
|
+
</box>
|
|
52
|
+
<box flexGrow={1} />
|
|
53
|
+
{backHint ? <text fg={colors.textMuted}>{backHint}</text> : null}
|
|
54
|
+
</box>
|
|
55
|
+
<box flexDirection="column" flexGrow={1}>
|
|
56
|
+
{detailContent}
|
|
57
|
+
</box>
|
|
58
|
+
</box>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface LastClickState {
|
|
4
|
+
targetKey: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useDoubleClickActivation<T>({
|
|
8
|
+
onSelect,
|
|
9
|
+
onActivate,
|
|
10
|
+
}: {
|
|
11
|
+
onSelect?: (value: T) => void;
|
|
12
|
+
onActivate?: (value: T) => void;
|
|
13
|
+
}) {
|
|
14
|
+
const lastClickRef = useRef<LastClickState | null>(null);
|
|
15
|
+
|
|
16
|
+
return useCallback((targetKey: string, value: T) => {
|
|
17
|
+
const lastClick = lastClickRef.current;
|
|
18
|
+
|
|
19
|
+
onSelect?.(value);
|
|
20
|
+
|
|
21
|
+
if (lastClick && lastClick.targetKey === targetKey) {
|
|
22
|
+
lastClickRef.current = null;
|
|
23
|
+
onActivate?.(value);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lastClickRef.current = {
|
|
28
|
+
targetKey,
|
|
29
|
+
};
|
|
30
|
+
}, [onActivate, onSelect]);
|
|
31
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -4,10 +4,9 @@ import { App } from "./app";
|
|
|
4
4
|
import { getDataDir, initDataDir } from "./data/config-store";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { existsSync, mkdirSync } from "fs";
|
|
7
|
-
import {
|
|
7
|
+
import { dispatchCli } from "./cli/index";
|
|
8
8
|
import { loadExternalPlugins } from "./plugins/loader";
|
|
9
9
|
import { debugLog } from "./utils/debug-log";
|
|
10
|
-
import { parsePredictionLaunchArgs } from "./plugins/prediction-markets/launch";
|
|
11
10
|
|
|
12
11
|
async function main() {
|
|
13
12
|
// Intercept console.log/warn/error to capture in debug log
|
|
@@ -18,10 +17,14 @@ async function main() {
|
|
|
18
17
|
|
|
19
18
|
// Handle CLI subcommands (install, remove, update, plugins)
|
|
20
19
|
const cliArgs = process.argv.slice(2);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const externalPlugins = await loadExternalPlugins();
|
|
21
|
+
let cliLaunchRequest = null;
|
|
22
|
+
if (cliArgs.length > 0) {
|
|
23
|
+
const dispatchResult = await dispatchCli(cliArgs, { externalPlugins });
|
|
24
|
+
if (dispatchResult.kind === "handled") return;
|
|
25
|
+
if (dispatchResult.kind === "launch-ui") {
|
|
26
|
+
cliLaunchRequest = dispatchResult.request;
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
// Determine data directory
|
|
@@ -40,9 +43,6 @@ async function main() {
|
|
|
40
43
|
// Load or create config
|
|
41
44
|
const config = await initDataDir(dataDir);
|
|
42
45
|
|
|
43
|
-
// Load external plugins from ~/.gloomberb/plugins/
|
|
44
|
-
const externalPlugins = await loadExternalPlugins();
|
|
45
|
-
|
|
46
46
|
// Create renderer
|
|
47
47
|
const renderer = await createCliRenderer({
|
|
48
48
|
exitOnCtrlC: true,
|
|
@@ -56,7 +56,7 @@ async function main() {
|
|
|
56
56
|
config={config}
|
|
57
57
|
renderer={renderer}
|
|
58
58
|
externalPlugins={externalPlugins}
|
|
59
|
-
|
|
59
|
+
cliLaunchRequest={cliLaunchRequest}
|
|
60
60
|
/>
|
|
61
61
|
);
|
|
62
62
|
}
|
|
@@ -80,8 +80,8 @@ describe("MarketDataCoordinator", () => {
|
|
|
80
80
|
const coordinator = new MarketDataCoordinator(provider);
|
|
81
81
|
const request = {
|
|
82
82
|
instrument: { symbol: "AAPL", exchange: "NASDAQ" },
|
|
83
|
-
|
|
84
|
-
granularity: "
|
|
83
|
+
bufferRange: "1Y" as const,
|
|
84
|
+
granularity: "range" as const,
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
const first = await coordinator.loadChart(request);
|
|
@@ -103,8 +103,8 @@ describe("MarketDataCoordinator", () => {
|
|
|
103
103
|
const coordinator = new MarketDataCoordinator(provider);
|
|
104
104
|
const request = {
|
|
105
105
|
instrument: { symbol: "AAPL", exchange: "NASDAQ" },
|
|
106
|
-
|
|
107
|
-
granularity: "
|
|
106
|
+
bufferRange: "1Y" as const,
|
|
107
|
+
granularity: "range" as const,
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
const entry = await coordinator.loadChart(request);
|
|
@@ -142,6 +142,30 @@ describe("MarketDataCoordinator", () => {
|
|
|
142
142
|
expect(coordinator.getQuoteEntry(instrument).data?.price).toBe(412.5);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it("routes manual-resolution chart requests through the resolution-aware provider path", async () => {
|
|
146
|
+
let requested: { range: string; resolution: string } | null = null;
|
|
147
|
+
const provider = createProvider({
|
|
148
|
+
getPriceHistoryForResolution: async (_symbol, _exchange, range, resolution) => {
|
|
149
|
+
requested = { range, resolution };
|
|
150
|
+
return [
|
|
151
|
+
{ date: new Date("2024-01-01"), close: 100 },
|
|
152
|
+
{ date: new Date("2024-01-02"), close: 101 },
|
|
153
|
+
];
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
157
|
+
|
|
158
|
+
const entry = await coordinator.loadChart({
|
|
159
|
+
instrument: { symbol: "AAPL", exchange: "NASDAQ" },
|
|
160
|
+
bufferRange: "1Y",
|
|
161
|
+
granularity: "resolution",
|
|
162
|
+
resolution: "1d",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(requested).toEqual({ range: "1Y", resolution: "1d" });
|
|
166
|
+
expect(entry.data?.length).toBe(2);
|
|
167
|
+
});
|
|
168
|
+
|
|
145
169
|
it("projects live quote updates into premarket display fields when the stream lacks explicit ext-hours fields", async () => {
|
|
146
170
|
let streamed: ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null = null;
|
|
147
171
|
const provider = createProvider({
|
|
@@ -192,6 +216,7 @@ describe("MarketDataCoordinator", () => {
|
|
|
192
216
|
change: 2.5,
|
|
193
217
|
changePercent: 2.48,
|
|
194
218
|
lastUpdated: Date.now(),
|
|
219
|
+
dataSource: "live",
|
|
195
220
|
},
|
|
196
221
|
);
|
|
197
222
|
|
|
@@ -287,6 +312,69 @@ describe("MarketDataCoordinator", () => {
|
|
|
287
312
|
expect(coordinator.getTickerFinancialsSync(instrument)?.quote?.price).toBe(0.245);
|
|
288
313
|
});
|
|
289
314
|
|
|
315
|
+
it("keeps the fresh snapshot quote when a stale cloud stream update arrives later", async () => {
|
|
316
|
+
const fixedNow = Date.parse("2026-04-08T10:30:00Z");
|
|
317
|
+
const realDateNow = Date.now;
|
|
318
|
+
Date.now = () => fixedNow;
|
|
319
|
+
try {
|
|
320
|
+
let streamed: ((target: QuoteSubscriptionTarget, quote: Quote) => void) | null = null;
|
|
321
|
+
const provider = createProvider({
|
|
322
|
+
getTickerFinancials: async () => ({
|
|
323
|
+
quote: {
|
|
324
|
+
symbol: "HY9H",
|
|
325
|
+
providerId: "yahoo",
|
|
326
|
+
dataSource: "yahoo",
|
|
327
|
+
price: 598,
|
|
328
|
+
currency: "EUR",
|
|
329
|
+
change: 6,
|
|
330
|
+
changePercent: 1.01,
|
|
331
|
+
lastUpdated: Date.parse("2026-04-08T10:25:00Z"),
|
|
332
|
+
listingExchangeName: "FWB2",
|
|
333
|
+
marketState: "REGULAR",
|
|
334
|
+
sessionConfidence: "derived",
|
|
335
|
+
},
|
|
336
|
+
annualStatements: [],
|
|
337
|
+
quarterlyStatements: [],
|
|
338
|
+
priceHistory: [],
|
|
339
|
+
}),
|
|
340
|
+
subscribeQuotes: (_targets, onQuote) => {
|
|
341
|
+
streamed = onQuote as typeof streamed;
|
|
342
|
+
return () => {};
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
const coordinator = new MarketDataCoordinator(provider);
|
|
346
|
+
const instrument = { symbol: "HY9H", exchange: "FWB2" };
|
|
347
|
+
|
|
348
|
+
await coordinator.loadSnapshot(instrument);
|
|
349
|
+
coordinator.subscribeQuotes([{ instrument }]);
|
|
350
|
+
const onStreamed = streamed;
|
|
351
|
+
if (!onStreamed) throw new Error("expected streaming callback");
|
|
352
|
+
onStreamed(
|
|
353
|
+
{ symbol: "HY9H", exchange: "FWB2" },
|
|
354
|
+
{
|
|
355
|
+
symbol: "HY9H",
|
|
356
|
+
providerId: "gloomberb-cloud",
|
|
357
|
+
dataSource: "delayed",
|
|
358
|
+
price: 528,
|
|
359
|
+
currency: "EUR",
|
|
360
|
+
change: 4,
|
|
361
|
+
changePercent: 0.76,
|
|
362
|
+
lastUpdated: Date.parse("2026-04-07T17:55:00Z"),
|
|
363
|
+
listingExchangeName: "FWB2",
|
|
364
|
+
marketState: "REGULAR",
|
|
365
|
+
sessionConfidence: "explicit",
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(coordinator.getQuoteEntry(instrument).data).toBeNull();
|
|
370
|
+
expect(coordinator.getQuoteEntry(instrument).lastGoodData?.price).toBe(598);
|
|
371
|
+
expect(coordinator.getTickerFinancialsSync(instrument)?.quote?.price).toBe(598);
|
|
372
|
+
expect(coordinator.getTickerFinancialsSync(instrument)?.quote?.providerId).toBe("yahoo");
|
|
373
|
+
} finally {
|
|
374
|
+
Date.now = realDateNow;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
290
378
|
it("hydrates ticker financials synchronously from primed cached data", () => {
|
|
291
379
|
const coordinator = new MarketDataCoordinator(createProvider());
|
|
292
380
|
const instrument = { symbol: "AAPL", exchange: "NASDAQ" };
|