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
|
@@ -19,7 +19,7 @@ interface ChatContentProps {
|
|
|
19
19
|
close?: () => void;
|
|
20
20
|
controller?: Pick<
|
|
21
21
|
ChatController,
|
|
22
|
-
"getSnapshot" | "refreshSession" | "send" | "setDraft" | "setReplyToId" | "subscribe"
|
|
22
|
+
"attachView" | "getSnapshot" | "refreshMessages" | "refreshSession" | "send" | "setDraft" | "setReplyToId" | "subscribe"
|
|
23
23
|
>;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -34,16 +34,27 @@ function estimateWrappedLineCount(text: string, width: number) {
|
|
|
34
34
|
), 0);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const MESSAGE_GROUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
38
|
+
|
|
39
|
+
function isGroupedWithPrevious(messages: ChatMessage[], index: number) {
|
|
40
|
+
if (index === 0) return false;
|
|
41
|
+
const prev = messages[index - 1]!;
|
|
42
|
+
const curr = messages[index]!;
|
|
43
|
+
if (prev.user.id !== curr.user.id) return false;
|
|
44
|
+
return new Date(curr.createdAt).getTime() - new Date(prev.createdAt).getTime() < MESSAGE_GROUP_THRESHOLD_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function estimateMessageHeight(message: ChatMessage, width: number, grouped = false) {
|
|
38
48
|
const contentLineWidth = Math.max(width - 4, 1);
|
|
39
49
|
const normalizedContent = message.content.replace(/\$[A-Z][A-Z0-9.-]{0,9}/g, (match) => ` ${match.slice(1)} +0% `);
|
|
40
|
-
|
|
50
|
+
const headerHeight = grouped ? 0 : 1;
|
|
51
|
+
return headerHeight + (message.replyTo ? 1 : 0) + estimateWrappedLineCount(normalizedContent, contentLineWidth);
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
function getMessageTopOffset(messages: ChatMessage[], index: number, width: number) {
|
|
44
55
|
let offset = 0;
|
|
45
56
|
for (let i = 0; i < index; i += 1) {
|
|
46
|
-
offset += estimateMessageHeight(messages[i]!, width);
|
|
57
|
+
offset += estimateMessageHeight(messages[i]!, width, isGroupedWithPrevious(messages, i));
|
|
47
58
|
}
|
|
48
59
|
return offset;
|
|
49
60
|
}
|
|
@@ -53,6 +64,42 @@ function scrollToBottom(scrollBox: ScrollBoxRenderable | null) {
|
|
|
53
64
|
scrollBox.scrollTo(Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height));
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
function openAuthCommand(query: string, event?: { preventDefault?: () => void; stopPropagation?: () => void }) {
|
|
68
|
+
event?.preventDefault?.();
|
|
69
|
+
event?.stopPropagation?.();
|
|
70
|
+
getSharedRegistry()?.openCommandBarFn(query);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function InlineAuthActions({ showSignup = true }: { showSignup?: boolean }) {
|
|
74
|
+
const [hoveredAction, setHoveredAction] = useState<"login" | "signup" | null>(null);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<box flexDirection="row">
|
|
78
|
+
<box
|
|
79
|
+
backgroundColor={hoveredAction === "login" ? hoverBg() : undefined}
|
|
80
|
+
onMouseMove={() => setHoveredAction("login")}
|
|
81
|
+
onMouseOut={() => setHoveredAction((current) => (current === "login" ? null : current))}
|
|
82
|
+
onMouseDown={(event: any) => openAuthCommand("Login", event)}
|
|
83
|
+
>
|
|
84
|
+
<text fg={hoveredAction === "login" ? colors.text : colors.textDim}> Login </text>
|
|
85
|
+
</box>
|
|
86
|
+
{showSignup && (
|
|
87
|
+
<>
|
|
88
|
+
<text fg={colors.textDim}>/</text>
|
|
89
|
+
<box
|
|
90
|
+
backgroundColor={hoveredAction === "signup" ? hoverBg() : undefined}
|
|
91
|
+
onMouseMove={() => setHoveredAction("signup")}
|
|
92
|
+
onMouseOut={() => setHoveredAction((current) => (current === "signup" ? null : current))}
|
|
93
|
+
onMouseDown={(event: any) => openAuthCommand("Sign Up", event)}
|
|
94
|
+
>
|
|
95
|
+
<text fg={hoveredAction === "signup" ? colors.text : colors.textDim}> Sign Up </text>
|
|
96
|
+
</box>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</box>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
56
103
|
export function ChatContent({
|
|
57
104
|
width,
|
|
58
105
|
height,
|
|
@@ -77,6 +124,11 @@ export function ChatContent({
|
|
|
77
124
|
));
|
|
78
125
|
const inputRef = useRef<InputRenderable>(null);
|
|
79
126
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
127
|
+
const canSend = !!user?.emailVerified;
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
return controller.attachView();
|
|
131
|
+
}, [controller]);
|
|
80
132
|
|
|
81
133
|
useEffect(() => {
|
|
82
134
|
const unsubscribe = controller.subscribe((snapshot) => {
|
|
@@ -91,6 +143,7 @@ export function ChatContent({
|
|
|
91
143
|
});
|
|
92
144
|
|
|
93
145
|
void controller.refreshSession().catch(() => {});
|
|
146
|
+
void controller.refreshMessages().catch(() => {});
|
|
94
147
|
return unsubscribe;
|
|
95
148
|
}, [controller]);
|
|
96
149
|
|
|
@@ -121,11 +174,16 @@ export function ChatContent({
|
|
|
121
174
|
const content = inputValueRef.current.trim();
|
|
122
175
|
if (!content) return;
|
|
123
176
|
controller.send(content, replyToRef.current?.id);
|
|
124
|
-
setReplyTo(null);
|
|
125
177
|
setSelectedIdx(-1);
|
|
126
178
|
setFollowMessages(true);
|
|
127
179
|
}, [controller]);
|
|
128
180
|
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!canSend && inputFocused) {
|
|
183
|
+
blurInput();
|
|
184
|
+
}
|
|
185
|
+
}, [blurInput, canSend, inputFocused]);
|
|
186
|
+
|
|
129
187
|
useEffect(() => {
|
|
130
188
|
if (!focused && inputFocused) {
|
|
131
189
|
blurInput();
|
|
@@ -138,6 +196,12 @@ export function ChatContent({
|
|
|
138
196
|
}
|
|
139
197
|
}, [focused, inputFocused]);
|
|
140
198
|
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (canSend || !replyTo) return;
|
|
201
|
+
setReplyTo(null);
|
|
202
|
+
controller.setReplyToId(null);
|
|
203
|
+
}, [canSend, controller, replyTo]);
|
|
204
|
+
|
|
141
205
|
useKeyboard((event) => {
|
|
142
206
|
if (!focused) return;
|
|
143
207
|
|
|
@@ -161,7 +225,7 @@ export function ChatContent({
|
|
|
161
225
|
return;
|
|
162
226
|
}
|
|
163
227
|
|
|
164
|
-
if (event.name === "return" || event.name === "i") {
|
|
228
|
+
if ((event.name === "return" || event.name === "i") && canSend) {
|
|
165
229
|
focusInput();
|
|
166
230
|
return;
|
|
167
231
|
}
|
|
@@ -187,7 +251,7 @@ export function ChatContent({
|
|
|
187
251
|
return;
|
|
188
252
|
}
|
|
189
253
|
|
|
190
|
-
if (event.name === "r" && selectedIdx >= 0 && selectedIdx < messages.length) {
|
|
254
|
+
if (canSend && event.name === "r" && selectedIdx >= 0 && selectedIdx < messages.length) {
|
|
191
255
|
const nextReplyTo = messages[selectedIdx] ?? null;
|
|
192
256
|
setReplyTo(nextReplyTo);
|
|
193
257
|
controller.setReplyToId(nextReplyTo?.id ?? null);
|
|
@@ -207,7 +271,7 @@ export function ChatContent({
|
|
|
207
271
|
queueMicrotask(() => scrollToBottom(scrollRef.current));
|
|
208
272
|
return;
|
|
209
273
|
}
|
|
210
|
-
}, [blurInput, close, controller, focusInput, focused, inputFocused, messages, replyTo, selectedIdx]);
|
|
274
|
+
}, [blurInput, canSend, close, controller, focusInput, focused, inputFocused, messages, replyTo, selectedIdx]);
|
|
211
275
|
|
|
212
276
|
useEffect(() => {
|
|
213
277
|
if (selectedIdx < messages.length) return;
|
|
@@ -220,7 +284,7 @@ export function ChatContent({
|
|
|
220
284
|
if (followMessages) return;
|
|
221
285
|
if (selectedIdx < 0 || selectedIdx >= messages.length) return;
|
|
222
286
|
const top = getMessageTopOffset(messages, selectedIdx, contentWidth);
|
|
223
|
-
const rowHeight = estimateMessageHeight(messages[selectedIdx]!, contentWidth);
|
|
287
|
+
const rowHeight = estimateMessageHeight(messages[selectedIdx]!, contentWidth, isGroupedWithPrevious(messages, selectedIdx));
|
|
224
288
|
const viewportHeight = Math.max(sb.viewport.height, 1);
|
|
225
289
|
if (top < sb.scrollTop) {
|
|
226
290
|
sb.scrollTo(top);
|
|
@@ -229,13 +293,13 @@ export function ChatContent({
|
|
|
229
293
|
}
|
|
230
294
|
}, [contentWidth, followMessages, messages, selectedIdx]);
|
|
231
295
|
|
|
232
|
-
const replyBarHeight = replyTo ? 1 : 0;
|
|
233
|
-
const inputAreaHeight = 1 + replyBarHeight;
|
|
296
|
+
const replyBarHeight = canSend && replyTo ? 1 : 0;
|
|
297
|
+
const inputAreaHeight = canSend ? 1 + replyBarHeight : 2;
|
|
234
298
|
const headerHeight = 1;
|
|
235
299
|
const separatorHeight = 1;
|
|
236
300
|
const messageAreaHeight = Math.max(1, height - headerHeight - separatorHeight - inputAreaHeight - 1);
|
|
237
301
|
|
|
238
|
-
if (loading) {
|
|
302
|
+
if (loading && messages.length === 0) {
|
|
239
303
|
return (
|
|
240
304
|
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
241
305
|
<text fg={colors.textDim}>Loading...</text>
|
|
@@ -243,39 +307,6 @@ export function ChatContent({
|
|
|
243
307
|
);
|
|
244
308
|
}
|
|
245
309
|
|
|
246
|
-
if (!user && !hasSavedSession) {
|
|
247
|
-
return (
|
|
248
|
-
<box flexGrow={1} alignItems="center" justifyContent="center" flexDirection="column">
|
|
249
|
-
<text fg={colors.textDim}>Not logged in.</text>
|
|
250
|
-
<text fg={colors.textDim}> </text>
|
|
251
|
-
<text fg={colors.text}>Press Ctrl+P and search "Login" or "Sign Up"</text>
|
|
252
|
-
<text fg={colors.textDim}>to start chatting.</text>
|
|
253
|
-
</box>
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (!user && hasSavedSession) {
|
|
258
|
-
return (
|
|
259
|
-
<box flexGrow={1} alignItems="center" justifyContent="center" flexDirection="column">
|
|
260
|
-
<text fg={colors.positive}>Saved login found.</text>
|
|
261
|
-
<text fg={colors.textDim}> </text>
|
|
262
|
-
<text fg={colors.text}>Reconnect to refresh your Gloomberb Cloud session.</text>
|
|
263
|
-
<text fg={colors.textDim}>Your cached transcript is still available locally.</text>
|
|
264
|
-
</box>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!user.emailVerified) {
|
|
269
|
-
return (
|
|
270
|
-
<box flexGrow={1} alignItems="center" justifyContent="center" flexDirection="column">
|
|
271
|
-
<text fg={colors.positive}>Check your email to unlock Gloomberb Cloud.</text>
|
|
272
|
-
<text fg={colors.textDim}> </text>
|
|
273
|
-
<text fg={colors.text}>Near real-time data and chat are free, but require email verification.</text>
|
|
274
|
-
<text fg={colors.textDim}>Use Ctrl+P and search "Resend Verification Email" if you need another link.</text>
|
|
275
|
-
</box>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
310
|
return (
|
|
280
311
|
<box flexDirection="column" width={width} height={height}>
|
|
281
312
|
<box height={1} width={contentWidth} flexDirection="row">
|
|
@@ -305,6 +336,13 @@ export function ChatContent({
|
|
|
305
336
|
const isSelected = index === selectedIdx;
|
|
306
337
|
const isHovered = index === hoveredIdx && !isSelected;
|
|
307
338
|
const bgColor = isSelected ? colors.selected : isHovered ? hoverBg() : undefined;
|
|
339
|
+
const grouped = isGroupedWithPrevious(messages, index);
|
|
340
|
+
const isSending = msg.clientStatus === "sending";
|
|
341
|
+
const hasFailed = msg.clientStatus === "failed";
|
|
342
|
+
const headerStatus = isSending ? "sending..." : hasFailed ? "failed" : formatTimeAgo(msg.createdAt);
|
|
343
|
+
const headerStatusColor = isSending ? colors.textDim : hasFailed ? colors.negative : colors.textMuted;
|
|
344
|
+
const authorAttributes = (isSending ? TextAttributes.DIM : 0) | TextAttributes.BOLD;
|
|
345
|
+
const bodyColor = hasFailed ? colors.negative : isSending ? colors.textDim : colors.text;
|
|
308
346
|
|
|
309
347
|
return (
|
|
310
348
|
<box
|
|
@@ -329,18 +367,20 @@ export function ChatContent({
|
|
|
329
367
|
</text>
|
|
330
368
|
</box>
|
|
331
369
|
)}
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
370
|
+
{!grouped && (
|
|
371
|
+
<box flexDirection="row" height={1} paddingLeft={1}>
|
|
372
|
+
<text fg={colors.positive} attributes={authorAttributes}>
|
|
373
|
+
{msg.user.username ?? "anon"}
|
|
374
|
+
</text>
|
|
375
|
+
<text fg={headerStatusColor}> ({headerStatus})</text>
|
|
376
|
+
</box>
|
|
377
|
+
)}
|
|
338
378
|
<box paddingLeft={3}>
|
|
339
379
|
<TickerBadgeText
|
|
340
380
|
text={msg.content}
|
|
341
381
|
lineWidth={contentWidth - 4}
|
|
342
382
|
catalog={catalog}
|
|
343
|
-
textColor={
|
|
383
|
+
textColor={bodyColor}
|
|
344
384
|
openTicker={openTicker}
|
|
345
385
|
/>
|
|
346
386
|
</box>
|
|
@@ -353,35 +393,58 @@ export function ChatContent({
|
|
|
353
393
|
<text fg={colors.border}>{"-".repeat(contentWidth)}</text>
|
|
354
394
|
</box>
|
|
355
395
|
|
|
356
|
-
{
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
396
|
+
{canSend ? (
|
|
397
|
+
<>
|
|
398
|
+
{replyTo && (
|
|
399
|
+
<box height={1} width={contentWidth} flexDirection="row">
|
|
400
|
+
<text fg={colors.textMuted}> replying to </text>
|
|
401
|
+
<text fg={colors.positive}>{replyTo.user.username}</text>
|
|
402
|
+
<box flexGrow={1} />
|
|
403
|
+
<text fg={colors.textDim}>[Esc cancel]</text>
|
|
404
|
+
</box>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
<box height={1} width={contentWidth} flexDirection="row" onMouseDown={focusInput}>
|
|
408
|
+
<text fg={colors.textDim}> {">"} </text>
|
|
409
|
+
<input
|
|
410
|
+
ref={inputRef}
|
|
411
|
+
value={inputValue}
|
|
412
|
+
focused={inputFocused && focused}
|
|
413
|
+
placeholder="Type a message..."
|
|
414
|
+
placeholderColor={colors.textMuted}
|
|
415
|
+
textColor={colors.text}
|
|
416
|
+
backgroundColor={colors.bg}
|
|
417
|
+
flexGrow={1}
|
|
418
|
+
onInput={updateDraft}
|
|
419
|
+
onChange={updateDraft}
|
|
420
|
+
onSubmit={() => {
|
|
421
|
+
if (inputValueRef.current.trim()) {
|
|
422
|
+
sendMessage();
|
|
423
|
+
}
|
|
424
|
+
}}
|
|
425
|
+
/>
|
|
426
|
+
</box>
|
|
427
|
+
</>
|
|
428
|
+
) : (
|
|
429
|
+
<box width={contentWidth} height={2} flexDirection="column">
|
|
430
|
+
{!user && !hasSavedSession ? (
|
|
431
|
+
<>
|
|
432
|
+
<text fg={colors.textDim}>Read-only chat. Log in or sign up to send.</text>
|
|
433
|
+
<InlineAuthActions />
|
|
434
|
+
</>
|
|
435
|
+
) : !user ? (
|
|
436
|
+
<>
|
|
437
|
+
<text fg={colors.positive}>Saved login found. Log in again to send.</text>
|
|
438
|
+
<InlineAuthActions showSignup={false} />
|
|
439
|
+
</>
|
|
440
|
+
) : (
|
|
441
|
+
<>
|
|
442
|
+
<text fg={colors.positive}>Verify your email to send messages.</text>
|
|
443
|
+
<text fg={colors.textDim}>Ctrl+P: Resend Verification Email</text>
|
|
444
|
+
</>
|
|
445
|
+
)}
|
|
362
446
|
</box>
|
|
363
447
|
)}
|
|
364
|
-
|
|
365
|
-
<box height={1} width={contentWidth} flexDirection="row" onMouseDown={focusInput}>
|
|
366
|
-
<text fg={colors.textDim}> {">"} </text>
|
|
367
|
-
<input
|
|
368
|
-
ref={inputRef}
|
|
369
|
-
value={inputValue}
|
|
370
|
-
focused={inputFocused && focused}
|
|
371
|
-
placeholder="Type a message..."
|
|
372
|
-
placeholderColor={colors.textMuted}
|
|
373
|
-
textColor={colors.text}
|
|
374
|
-
backgroundColor={colors.bg}
|
|
375
|
-
flexGrow={1}
|
|
376
|
-
onInput={updateDraft}
|
|
377
|
-
onChange={updateDraft}
|
|
378
|
-
onSubmit={() => {
|
|
379
|
-
if (inputValueRef.current.trim()) {
|
|
380
|
-
sendMessage();
|
|
381
|
-
}
|
|
382
|
-
}}
|
|
383
|
-
/>
|
|
384
|
-
</box>
|
|
385
448
|
</box>
|
|
386
449
|
);
|
|
387
450
|
}
|
|
@@ -398,17 +461,24 @@ export function ChatPane({ focused, width, height, close }: PaneProps) {
|
|
|
398
461
|
}
|
|
399
462
|
|
|
400
463
|
export function ChatStatusWidget({ controller = chatController }: ChatStatusWidgetProps) {
|
|
401
|
-
const registry = getSharedRegistry();
|
|
402
464
|
const { state } = useAppState();
|
|
403
465
|
const initialSnapshot = controller.getSnapshot();
|
|
404
|
-
const [hoveredAction, setHoveredAction] = useState<"login" | "signup" | null>(null);
|
|
405
466
|
const [username, setUsername] = useState<string | null>(initialSnapshot.user?.username ?? null);
|
|
406
467
|
const [hasSavedSession, setHasSavedSession] = useState(initialSnapshot.hasSavedSession);
|
|
468
|
+
const [unreadMentionCount, setUnreadMentionCount] = useState(initialSnapshot.unreadMentionCount);
|
|
469
|
+
const [hovered, setHovered] = useState(false);
|
|
470
|
+
|
|
471
|
+
const openChat = (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
472
|
+
event?.preventDefault?.();
|
|
473
|
+
event?.stopPropagation?.();
|
|
474
|
+
getSharedRegistry()?.showWidget("chat");
|
|
475
|
+
};
|
|
407
476
|
|
|
408
477
|
useEffect(() => {
|
|
409
478
|
const unsubscribe = controller.subscribe((snapshot) => {
|
|
410
479
|
setUsername(snapshot.user?.username ?? null);
|
|
411
480
|
setHasSavedSession(snapshot.hasSavedSession);
|
|
481
|
+
setUnreadMentionCount(snapshot.unreadMentionCount);
|
|
412
482
|
});
|
|
413
483
|
void controller.refreshSession().catch(() => {});
|
|
414
484
|
return unsubscribe;
|
|
@@ -416,47 +486,34 @@ export function ChatStatusWidget({ controller = chatController }: ChatStatusWidg
|
|
|
416
486
|
|
|
417
487
|
if (state.config.disabledPlugins.includes("gloomberb-cloud")) return null;
|
|
418
488
|
|
|
419
|
-
const openAuthCommand = (query: string, event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
420
|
-
event?.preventDefault?.();
|
|
421
|
-
event?.stopPropagation?.();
|
|
422
|
-
registry?.openCommandBarFn(query);
|
|
423
|
-
};
|
|
424
|
-
|
|
425
489
|
return (
|
|
426
490
|
<box flexDirection="row" paddingRight={1}>
|
|
427
491
|
{!username && !hasSavedSession ? (
|
|
428
492
|
<>
|
|
429
493
|
<text fg={colors.textDim}>☁ </text>
|
|
430
|
-
<
|
|
431
|
-
backgroundColor={hoveredAction === "login" ? hoverBg() : undefined}
|
|
432
|
-
onMouseMove={() => setHoveredAction("login")}
|
|
433
|
-
onMouseOut={() => setHoveredAction((current) => (current === "login" ? null : current))}
|
|
434
|
-
onMouseDown={(event: any) => openAuthCommand("Login", event)}
|
|
435
|
-
>
|
|
436
|
-
<text fg={hoveredAction === "login" ? colors.text : colors.textDim}> Login </text>
|
|
437
|
-
</box>
|
|
438
|
-
<text fg={colors.textDim}>/</text>
|
|
439
|
-
<box
|
|
440
|
-
backgroundColor={hoveredAction === "signup" ? hoverBg() : undefined}
|
|
441
|
-
onMouseMove={() => setHoveredAction("signup")}
|
|
442
|
-
onMouseOut={() => setHoveredAction((current) => (current === "signup" ? null : current))}
|
|
443
|
-
onMouseDown={(event: any) => openAuthCommand("Sign Up", event)}
|
|
444
|
-
>
|
|
445
|
-
<text fg={hoveredAction === "signup" ? colors.text : colors.textDim}> Sign Up </text>
|
|
446
|
-
</box>
|
|
494
|
+
<InlineAuthActions />
|
|
447
495
|
</>
|
|
448
496
|
) : (
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
497
|
+
<box
|
|
498
|
+
flexDirection="row"
|
|
499
|
+
backgroundColor={hovered ? hoverBg() : undefined}
|
|
500
|
+
onMouseMove={() => setHovered(true)}
|
|
501
|
+
onMouseOut={() => setHovered(false)}
|
|
502
|
+
onMouseDown={openChat}
|
|
503
|
+
>
|
|
504
|
+
<text fg={unreadMentionCount > 0 ? colors.text : colors.textDim}>
|
|
505
|
+
<span fg={colors.positive}>@</span>
|
|
506
|
+
{username ? (
|
|
507
|
+
<>
|
|
508
|
+
{" "}
|
|
509
|
+
<span fg={colors.positive}>{username}</span>
|
|
510
|
+
</>
|
|
511
|
+
) : null}
|
|
512
|
+
</text>
|
|
513
|
+
{unreadMentionCount > 0 ? (
|
|
514
|
+
<text fg={colors.positive} attributes={TextAttributes.BOLD}>{` [${unreadMentionCount}]`}</text>
|
|
515
|
+
) : null}
|
|
516
|
+
</box>
|
|
460
517
|
)}
|
|
461
518
|
</box>
|
|
462
519
|
);
|
|
@@ -488,6 +545,7 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
488
545
|
|
|
489
546
|
setup(ctx) {
|
|
490
547
|
chatController.attachPersistence(ctx.persistence, ctx.resume);
|
|
548
|
+
chatController.setToastNotifier(ctx.showToast);
|
|
491
549
|
|
|
492
550
|
ctx.registerPane({
|
|
493
551
|
id: "chat",
|
|
@@ -609,7 +667,8 @@ export const gloomberbCloudPlugin: GloomPlugin = {
|
|
|
609
667
|
return;
|
|
610
668
|
}
|
|
611
669
|
await apiClient.signOut();
|
|
612
|
-
chatController.
|
|
670
|
+
await chatController.refreshSession();
|
|
671
|
+
await chatController.refreshMessages();
|
|
613
672
|
ctx.showToast("Logged out.", { type: "info" });
|
|
614
673
|
},
|
|
615
674
|
hidden: () => !apiClient.getSessionToken(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act } from "react";
|
|
3
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
4
5
|
import {
|
|
5
6
|
AppContext,
|
|
6
7
|
createInitialState,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
const TEST_PANE_ID = "comparison-chart:test";
|
|
24
25
|
|
|
25
26
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
27
|
+
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
26
28
|
|
|
27
29
|
function makeTicker(symbol: string, currency: string): TickerRecord {
|
|
28
30
|
return {
|
|
@@ -76,12 +78,19 @@ function createProvider(historyBySymbol: Record<string, number[]>, currencyBySym
|
|
|
76
78
|
search: async () => [],
|
|
77
79
|
getNews: async () => [],
|
|
78
80
|
getArticleSummary: async () => null,
|
|
81
|
+
getChartResolutionCapabilities: async () => ["5m", "15m", "1h", "1d", "1wk", "1mo"],
|
|
79
82
|
getPriceHistory: async (symbol) => (
|
|
80
83
|
historyBySymbol[symbol] ?? []
|
|
81
84
|
).map((close, index) => ({
|
|
82
85
|
date: new Date(2024, 0, index + 2),
|
|
83
86
|
close,
|
|
84
87
|
})),
|
|
88
|
+
getPriceHistoryForResolution: async (symbol) => (
|
|
89
|
+
historyBySymbol[symbol] ?? []
|
|
90
|
+
).map((close, index) => ({
|
|
91
|
+
date: new Date(2024, 0, index + 2),
|
|
92
|
+
close,
|
|
93
|
+
})),
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -154,6 +163,8 @@ afterEach(() => {
|
|
|
154
163
|
testSetup.renderer.destroy();
|
|
155
164
|
testSetup = undefined;
|
|
156
165
|
}
|
|
166
|
+
sharedCoordinator = null;
|
|
167
|
+
setSharedMarketDataCoordinator(null);
|
|
157
168
|
setSharedRegistryForTests(undefined);
|
|
158
169
|
setSharedDataProviderForTests(undefined);
|
|
159
170
|
});
|
|
@@ -178,6 +189,8 @@ describe("comparisonChartPlugin", () => {
|
|
|
178
189
|
title: "AAPL · MSFT · NVDA",
|
|
179
190
|
settings: {
|
|
180
191
|
axisMode: "percent",
|
|
192
|
+
rangePreset: "1Y",
|
|
193
|
+
chartResolution: "1d",
|
|
181
194
|
symbols: ["AAPL", "MSFT", "NVDA"],
|
|
182
195
|
symbolsText: "AAPL, MSFT, NVDA",
|
|
183
196
|
},
|
|
@@ -190,19 +203,24 @@ describe("comparisonChartPlugin", () => {
|
|
|
190
203
|
symbols: ["AAPL", "MSFT"],
|
|
191
204
|
})).toEqual({
|
|
192
205
|
axisMode: "percent",
|
|
206
|
+
rangePreset: "1Y",
|
|
207
|
+
chartResolution: "1d",
|
|
193
208
|
symbols: ["AAPL", "MSFT"],
|
|
194
209
|
symbolsText: "AAPL, MSFT",
|
|
195
210
|
});
|
|
196
211
|
});
|
|
197
212
|
|
|
198
213
|
test("renders one shared overlay chart with the mixed-currency warning", async () => {
|
|
199
|
-
|
|
214
|
+
const provider = createProvider({
|
|
200
215
|
AAPL: [100, 102, 104, 106],
|
|
201
216
|
"7203": [2000, 2020, 2050, 2100],
|
|
202
217
|
}, {
|
|
203
218
|
AAPL: "USD",
|
|
204
219
|
"7203": "JPY",
|
|
205
|
-
})
|
|
220
|
+
});
|
|
221
|
+
setSharedDataProviderForTests(provider);
|
|
222
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
223
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
206
224
|
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
207
225
|
|
|
208
226
|
testSetup = await testRender(
|
|
@@ -224,14 +242,54 @@ describe("comparisonChartPlugin", () => {
|
|
|
224
242
|
|
|
225
243
|
const frame = testSetup.captureCharFrame();
|
|
226
244
|
expect(frame).toContain("Mixed currencies detected; showing percent change.");
|
|
227
|
-
expect(frame).toContain("1:
|
|
245
|
+
expect(frame).toContain("1:1D");
|
|
246
|
+
expect(frame).toContain("2:1W");
|
|
247
|
+
expect(frame).toContain("1D");
|
|
248
|
+
expect(frame).toContain("view:");
|
|
228
249
|
expect(frame).toContain("arrows legend");
|
|
229
250
|
expect(frame).not.toContain("side by side");
|
|
230
251
|
});
|
|
231
252
|
|
|
253
|
+
test("keeps controls visible when comparison history is empty", async () => {
|
|
254
|
+
const provider = createProvider({
|
|
255
|
+
AAPL: [],
|
|
256
|
+
MSFT: [],
|
|
257
|
+
}, {
|
|
258
|
+
AAPL: "USD",
|
|
259
|
+
MSFT: "USD",
|
|
260
|
+
});
|
|
261
|
+
setSharedDataProviderForTests(provider);
|
|
262
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
263
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
264
|
+
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
265
|
+
|
|
266
|
+
testSetup = await testRender(
|
|
267
|
+
createComparisonHarness({
|
|
268
|
+
axisMode: "price",
|
|
269
|
+
symbols: ["AAPL", "MSFT"],
|
|
270
|
+
symbolsText: "AAPL, MSFT",
|
|
271
|
+
}, [
|
|
272
|
+
makeTicker("AAPL", "USD"),
|
|
273
|
+
makeTicker("MSFT", "USD"),
|
|
274
|
+
], [
|
|
275
|
+
["AAPL", makeFinancials("AAPL", "USD", [])],
|
|
276
|
+
["MSFT", makeFinancials("MSFT", "USD", [])],
|
|
277
|
+
]),
|
|
278
|
+
{ width: 120, height: 20 },
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
await flushFrames();
|
|
282
|
+
|
|
283
|
+
const frame = testSetup.captureCharFrame();
|
|
284
|
+
expect(frame).toContain("No chart data yet.");
|
|
285
|
+
expect(frame).toContain("1:1D");
|
|
286
|
+
expect(frame).toContain("2:1W");
|
|
287
|
+
expect(frame).toContain("AUTO");
|
|
288
|
+
});
|
|
289
|
+
|
|
232
290
|
test("moves legend selection with the keyboard and opens the selected ticker on Enter", async () => {
|
|
233
291
|
const spy = { selected: [] as string[], focused: [] as string[] };
|
|
234
|
-
|
|
292
|
+
const provider = createProvider({
|
|
235
293
|
AAPL: [100, 102, 104],
|
|
236
294
|
MSFT: [200, 202, 204],
|
|
237
295
|
NVDA: [300, 305, 310],
|
|
@@ -239,7 +297,10 @@ describe("comparisonChartPlugin", () => {
|
|
|
239
297
|
AAPL: "USD",
|
|
240
298
|
MSFT: "USD",
|
|
241
299
|
NVDA: "USD",
|
|
242
|
-
})
|
|
300
|
+
});
|
|
301
|
+
setSharedDataProviderForTests(provider);
|
|
302
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
303
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
243
304
|
setSharedRegistryForTests(createRegistrySpy(spy));
|
|
244
305
|
|
|
245
306
|
testSetup = await testRender(
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { EmptyState } from "../../components";
|
|
3
3
|
import { ComparisonStockChart } from "../../components/chart/comparison-stock-chart";
|
|
4
|
-
import type { ChartAxisMode } from "../../components/chart/chart-types";
|
|
4
|
+
import type { ChartAxisMode, TimeRange } from "../../components/chart/chart-types";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_COMPARISON_CHART_RANGE_PRESET,
|
|
7
|
+
DEFAULT_COMPARISON_CHART_RESOLUTION,
|
|
8
|
+
normalizeChartResolution,
|
|
9
|
+
} from "../../components/chart/chart-resolution";
|
|
5
10
|
import { getSharedRegistry } from "../../plugins/registry";
|
|
6
11
|
import { useAppState } from "../../state/app-context";
|
|
7
12
|
import { colors } from "../../theme/colors";
|
|
@@ -13,6 +18,8 @@ export const COMPARISON_CHART_TEMPLATE_ID = "comparison-chart-pane";
|
|
|
13
18
|
|
|
14
19
|
interface ComparisonChartPaneSettings {
|
|
15
20
|
axisMode: ChartAxisMode;
|
|
21
|
+
rangePreset: TimeRange;
|
|
22
|
+
chartResolution: ReturnType<typeof normalizeChartResolution>;
|
|
16
23
|
symbols: string[];
|
|
17
24
|
symbolsText: string;
|
|
18
25
|
}
|
|
@@ -46,6 +53,17 @@ export function getComparisonChartPaneSettings(settings: Record<string, unknown>
|
|
|
46
53
|
|
|
47
54
|
return {
|
|
48
55
|
axisMode: isChartAxisMode(settings?.axisMode) ? settings.axisMode : "price",
|
|
56
|
+
rangePreset: settings?.rangePreset === "1D"
|
|
57
|
+
|| settings?.rangePreset === "1W"
|
|
58
|
+
|| settings?.rangePreset === "1M"
|
|
59
|
+
|| settings?.rangePreset === "3M"
|
|
60
|
+
|| settings?.rangePreset === "6M"
|
|
61
|
+
|| settings?.rangePreset === "1Y"
|
|
62
|
+
|| settings?.rangePreset === "5Y"
|
|
63
|
+
|| settings?.rangePreset === "ALL"
|
|
64
|
+
? settings.rangePreset
|
|
65
|
+
: DEFAULT_COMPARISON_CHART_RANGE_PRESET,
|
|
66
|
+
chartResolution: normalizeChartResolution(settings?.chartResolution, DEFAULT_COMPARISON_CHART_RESOLUTION),
|
|
49
67
|
symbols,
|
|
50
68
|
symbolsText: storedText.trim().length > 0 ? storedText : formatTickerListInput(symbols),
|
|
51
69
|
};
|
|
@@ -159,6 +177,8 @@ export const comparisonChartPlugin: GloomPlugin = {
|
|
|
159
177
|
placement: "floating",
|
|
160
178
|
settings: {
|
|
161
179
|
axisMode: "percent",
|
|
180
|
+
rangePreset: DEFAULT_COMPARISON_CHART_RANGE_PRESET,
|
|
181
|
+
chartResolution: DEFAULT_COMPARISON_CHART_RESOLUTION,
|
|
162
182
|
symbols,
|
|
163
183
|
symbolsText: formatTickerListInput(symbols),
|
|
164
184
|
},
|