gloomberb 0.9.7 → 0.9.8
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/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.ts +48 -0
- package/src/api-client/types.ts +45 -0
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/surface/index.test.tsx +52 -0
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/index.ts +1 -0
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -0
- package/src/market-data/coordinator/index.ts +40 -3
- package/src/market-data/coordinator/quotes.ts +13 -5
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +94 -8
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -3
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +260 -24
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +263 -3
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/chat/chat.test.tsx +125 -0
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +154 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +21 -11
- package/src/plugins/builtin/cloud/plugin.tsx +7 -0
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/portfolio-list/index.test.ts +51 -1
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/index.ts +31 -0
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +55 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +147 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.ts +296 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +14 -1
- package/src/ui/index.tsx +1 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/version.ts +1 -1
|
@@ -4,9 +4,11 @@ import { Box, Input, Text, Textarea, editableTextContextMenuItems, useRendererHo
|
|
|
4
4
|
import { TextAttributes, type InputRenderable } from "../../../../ui";
|
|
5
5
|
import { useShortcut } from "../../../../react/input";
|
|
6
6
|
import { blendHex, colors } from "../../../../theme/colors";
|
|
7
|
+
import { contrastRatio } from "../../../../theme/color-utils";
|
|
7
8
|
import { useThemeColors } from "../../../../theme/theme-context";
|
|
8
9
|
import { isDetailBackNavigationKey } from "../../../../utils/back-navigation";
|
|
9
10
|
import type { ButtonProps } from "../../../../components/ui/button";
|
|
11
|
+
import type { CheckboxProps } from "../../../../components/ui/checkbox";
|
|
10
12
|
import type { TextFieldProps } from "../../../../components/ui/fields";
|
|
11
13
|
import type { DialogFrameProps } from "../../../../components/ui/frame";
|
|
12
14
|
import type { MessageComposerProps } from "../../../../components/ui/message-composer";
|
|
@@ -31,6 +33,7 @@ export function WebButton({
|
|
|
31
33
|
active = false,
|
|
32
34
|
shortcut,
|
|
33
35
|
width,
|
|
36
|
+
height,
|
|
34
37
|
}: ButtonProps) {
|
|
35
38
|
useThemeColors();
|
|
36
39
|
const palette = buttonPalette({ variant, active, disabled });
|
|
@@ -38,7 +41,7 @@ export function WebButton({
|
|
|
38
41
|
return (
|
|
39
42
|
<Box
|
|
40
43
|
width={width}
|
|
41
|
-
height={1}
|
|
44
|
+
height={height ?? 1}
|
|
42
45
|
flexDirection="row"
|
|
43
46
|
alignItems="center"
|
|
44
47
|
justifyContent="center"
|
|
@@ -51,7 +54,8 @@ export function WebButton({
|
|
|
51
54
|
style={{
|
|
52
55
|
border: `1px solid ${palette.border}`,
|
|
53
56
|
borderRadius: CONTROL_RADIUS,
|
|
54
|
-
|
|
57
|
+
paddingLeft: 8,
|
|
58
|
+
paddingRight: 8,
|
|
55
59
|
boxShadow: controlShadow(active),
|
|
56
60
|
cursor: disabled ? "default" : "pointer",
|
|
57
61
|
}}
|
|
@@ -59,7 +63,9 @@ export function WebButton({
|
|
|
59
63
|
<Text
|
|
60
64
|
fg={palette.fg}
|
|
61
65
|
attributes={active ? TextAttributes.BOLD : 0}
|
|
62
|
-
style={{
|
|
66
|
+
style={{
|
|
67
|
+
fontWeight: active || variant === "primary" ? 700 : 600,
|
|
68
|
+
}}
|
|
63
69
|
>
|
|
64
70
|
{label}
|
|
65
71
|
</Text>
|
|
@@ -75,6 +81,129 @@ export function WebButton({
|
|
|
75
81
|
);
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
function checkboxAccentColor(): string {
|
|
85
|
+
const base = colors.borderFocused;
|
|
86
|
+
const nativeCheckmark = "#ffffff";
|
|
87
|
+
const candidates = [
|
|
88
|
+
blendHex(base, colors.selected, 0.55),
|
|
89
|
+
blendHex(base, colors.selected, 0.65),
|
|
90
|
+
blendHex(base, "#000000", 0.45),
|
|
91
|
+
blendHex(base, "#000000", 0.52),
|
|
92
|
+
base,
|
|
93
|
+
];
|
|
94
|
+
return candidates.find((candidate) => (
|
|
95
|
+
contrastRatio(candidate, nativeCheckmark) >= 4.5
|
|
96
|
+
&& contrastRatio(candidate, colors.panel) >= 2.2
|
|
97
|
+
)) ?? candidates[1]!;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkboxCheckImage(): string {
|
|
101
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="M3 7.3 5.8 10 11.2 3.7" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
102
|
+
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function WebCheckbox({
|
|
106
|
+
label,
|
|
107
|
+
displayLabel,
|
|
108
|
+
checked,
|
|
109
|
+
onChange,
|
|
110
|
+
disabled = false,
|
|
111
|
+
active = false,
|
|
112
|
+
description,
|
|
113
|
+
width,
|
|
114
|
+
}: CheckboxProps) {
|
|
115
|
+
useThemeColors();
|
|
116
|
+
const textColor = disabled
|
|
117
|
+
? colors.textMuted
|
|
118
|
+
: active
|
|
119
|
+
? colors.textBright
|
|
120
|
+
: colors.text;
|
|
121
|
+
const visibleLabel = displayLabel ?? label;
|
|
122
|
+
const accentColor = checkboxAccentColor();
|
|
123
|
+
const borderColor = checked ? accentColor : controlBorderColor(active, false);
|
|
124
|
+
return (
|
|
125
|
+
<Box
|
|
126
|
+
flexDirection="column"
|
|
127
|
+
width={width}
|
|
128
|
+
data-gloom-role="desktop-checkbox"
|
|
129
|
+
data-gloom-interactive={disabled ? undefined : "true"}
|
|
130
|
+
style={{ opacity: disabled ? 0.55 : 1 }}
|
|
131
|
+
>
|
|
132
|
+
<label
|
|
133
|
+
onMouseDown={(event) => {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
event.stopPropagation();
|
|
136
|
+
}}
|
|
137
|
+
onClick={(event) => {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
event.stopPropagation();
|
|
140
|
+
if (!disabled) onChange?.(!checked);
|
|
141
|
+
}}
|
|
142
|
+
style={{
|
|
143
|
+
display: "flex",
|
|
144
|
+
alignItems: "center",
|
|
145
|
+
gap: 8,
|
|
146
|
+
width: "100%",
|
|
147
|
+
minWidth: 0,
|
|
148
|
+
cursor: disabled ? "default" : "pointer",
|
|
149
|
+
color: textColor,
|
|
150
|
+
fontWeight: active ? 700 : 500,
|
|
151
|
+
lineHeight: "20px",
|
|
152
|
+
userSelect: "none",
|
|
153
|
+
position: "relative",
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<input
|
|
157
|
+
type="checkbox"
|
|
158
|
+
checked={checked}
|
|
159
|
+
readOnly
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
style={{
|
|
162
|
+
appearance: "none",
|
|
163
|
+
WebkitAppearance: "none",
|
|
164
|
+
width: 14,
|
|
165
|
+
height: 14,
|
|
166
|
+
margin: 0,
|
|
167
|
+
backgroundColor: checked ? accentColor : panelFill(),
|
|
168
|
+
backgroundImage: checked ? checkboxCheckImage() : undefined,
|
|
169
|
+
backgroundPosition: "center",
|
|
170
|
+
backgroundRepeat: "no-repeat",
|
|
171
|
+
backgroundSize: "12px 12px",
|
|
172
|
+
border: `1px solid ${borderColor}`,
|
|
173
|
+
borderRadius: 4,
|
|
174
|
+
boxShadow: active
|
|
175
|
+
? `0 0 0 2px ${blendHex(colors.bg, colors.borderFocused, 0.28)}, inset 0 1px 0 rgba(255,255,255,0.16)`
|
|
176
|
+
: "inset 0 1px 0 rgba(255,255,255,0.10)",
|
|
177
|
+
boxSizing: "border-box",
|
|
178
|
+
flexShrink: 0,
|
|
179
|
+
cursor: disabled ? "default" : "pointer",
|
|
180
|
+
verticalAlign: "middle",
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
<span
|
|
184
|
+
style={{
|
|
185
|
+
overflow: "hidden",
|
|
186
|
+
textOverflow: "ellipsis",
|
|
187
|
+
whiteSpace: "nowrap",
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{visibleLabel}
|
|
191
|
+
</span>
|
|
192
|
+
</label>
|
|
193
|
+
{description ? (
|
|
194
|
+
<Text
|
|
195
|
+
fg={colors.textMuted}
|
|
196
|
+
wrapText
|
|
197
|
+
width={width}
|
|
198
|
+
style={{ paddingLeft: 22, lineHeight: "18px" }}
|
|
199
|
+
>
|
|
200
|
+
{description}
|
|
201
|
+
</Text>
|
|
202
|
+
) : null}
|
|
203
|
+
</Box>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
78
207
|
export function WebTextField({
|
|
79
208
|
label,
|
|
80
209
|
value,
|
|
@@ -184,6 +313,7 @@ export function WebMessageComposer({
|
|
|
184
313
|
height = 2,
|
|
185
314
|
onFocusRequest,
|
|
186
315
|
onInput,
|
|
316
|
+
onCursorChange,
|
|
187
317
|
onSubmit,
|
|
188
318
|
keyBindings,
|
|
189
319
|
wrapText = false,
|
|
@@ -234,6 +364,7 @@ export function WebMessageComposer({
|
|
|
234
364
|
onMouseDown={requestFocus}
|
|
235
365
|
onFocus={requestFocus}
|
|
236
366
|
onInput={handleInput}
|
|
367
|
+
onCursorChange={onCursorChange}
|
|
237
368
|
keyBindings={keyBindings}
|
|
238
369
|
onSubmit={onSubmit}
|
|
239
370
|
wrapText={wrapText}
|
|
@@ -295,7 +426,7 @@ export function WebDialogFrame({
|
|
|
295
426
|
title,
|
|
296
427
|
children,
|
|
297
428
|
footer,
|
|
298
|
-
showTitleDivider =
|
|
429
|
+
showTitleDivider = false,
|
|
299
430
|
}: DialogFrameProps) {
|
|
300
431
|
useThemeColors();
|
|
301
432
|
return (
|
|
@@ -20,24 +20,29 @@ function DefaultDesktopRow({
|
|
|
20
20
|
selected: boolean;
|
|
21
21
|
}) {
|
|
22
22
|
return (
|
|
23
|
-
<Box
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
<Box
|
|
24
|
+
flexDirection="row"
|
|
25
|
+
justifyContent="space-between"
|
|
26
|
+
width="100%"
|
|
27
|
+
minWidth={0}
|
|
28
|
+
alignItems="center"
|
|
29
|
+
style={{ boxSizing: "border-box" }}
|
|
30
|
+
>
|
|
31
|
+
<Box flexDirection="row" alignItems="center" minWidth={0} flexShrink={1}>
|
|
32
32
|
<Text
|
|
33
33
|
fg={selected ? colors.text : colors.textDim}
|
|
34
34
|
attributes={selected ? TextAttributes.BOLD : 0}
|
|
35
|
+
style={{
|
|
36
|
+
overflow: "hidden",
|
|
37
|
+
textOverflow: "ellipsis",
|
|
38
|
+
whiteSpace: "nowrap",
|
|
39
|
+
}}
|
|
35
40
|
>
|
|
36
41
|
{item.label}
|
|
37
42
|
</Text>
|
|
38
43
|
</Box>
|
|
39
44
|
{item.detail && (
|
|
40
|
-
<Text fg={colors.textMuted}>
|
|
45
|
+
<Text fg={colors.textMuted} style={{ flexShrink: 0, marginLeft: 12 }}>
|
|
41
46
|
{item.detail}
|
|
42
47
|
</Text>
|
|
43
48
|
)}
|
|
@@ -50,7 +55,11 @@ function listRowStyle(selected: boolean): CSSProperties {
|
|
|
50
55
|
borderRadius: CONTROL_RADIUS,
|
|
51
56
|
border: `1px solid ${selected ? colors.borderFocused : "transparent"}`,
|
|
52
57
|
boxShadow: selected ? `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.06)}` : undefined,
|
|
58
|
+
boxSizing: "border-box",
|
|
53
59
|
cursor: "pointer",
|
|
60
|
+
maxWidth: "100%",
|
|
61
|
+
minWidth: 0,
|
|
62
|
+
paddingInline: 10,
|
|
54
63
|
};
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -69,10 +78,11 @@ export function WebListView({
|
|
|
69
78
|
hoverBgColor,
|
|
70
79
|
rowGap = 1,
|
|
71
80
|
rowHeight = 1,
|
|
72
|
-
surface
|
|
81
|
+
surface,
|
|
73
82
|
height,
|
|
74
83
|
flexGrow,
|
|
75
84
|
scrollable = false,
|
|
85
|
+
selectOnHover = false,
|
|
76
86
|
autoScrollToIndex = true,
|
|
77
87
|
}: ListViewProps) {
|
|
78
88
|
useThemeColors();
|
|
@@ -83,6 +93,20 @@ export function WebListView({
|
|
|
83
93
|
const rowHoverBg = hoverBgColor ?? hoverBg();
|
|
84
94
|
const selectedItem = selectedIndex >= 0 ? items[selectedIndex] : undefined;
|
|
85
95
|
const activeScrollIndex = scrollIndex ?? selectedIndex;
|
|
96
|
+
const effectiveSurface = surface ?? (scrollable ? "framed" : "plain");
|
|
97
|
+
const frameStyle = effectiveSurface === "plain"
|
|
98
|
+
? {
|
|
99
|
+
border: "none",
|
|
100
|
+
borderRadius: 0,
|
|
101
|
+
padding: 0,
|
|
102
|
+
backgroundColor: "transparent",
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
border: `1px solid ${panelBorder()}`,
|
|
106
|
+
borderRadius: CONTROL_RADIUS,
|
|
107
|
+
padding: 4,
|
|
108
|
+
backgroundColor: subtlePanelFill(),
|
|
109
|
+
};
|
|
86
110
|
|
|
87
111
|
useEffect(() => {
|
|
88
112
|
if (!scrollable || !autoScrollToIndex || activeScrollIndex < 0) return;
|
|
@@ -130,6 +154,7 @@ export function WebListView({
|
|
|
130
154
|
onMouseMove={() => {
|
|
131
155
|
if (!disabled) {
|
|
132
156
|
setHoveredIndex((current) => (current === index ? current : index));
|
|
157
|
+
if (selectOnHover) onSelect?.(index);
|
|
133
158
|
}
|
|
134
159
|
}}
|
|
135
160
|
onMouseDown={() => {
|
|
@@ -156,26 +181,14 @@ export function WebListView({
|
|
|
156
181
|
flexGrow={flexGrow}
|
|
157
182
|
scrollY
|
|
158
183
|
focusable={false}
|
|
159
|
-
style={
|
|
160
|
-
? {
|
|
161
|
-
border: "none",
|
|
162
|
-
borderRadius: 0,
|
|
163
|
-
padding: 0,
|
|
164
|
-
backgroundColor: "transparent",
|
|
165
|
-
}
|
|
166
|
-
: {
|
|
167
|
-
border: `1px solid ${panelBorder()}`,
|
|
168
|
-
borderRadius: CONTROL_RADIUS,
|
|
169
|
-
padding: 4,
|
|
170
|
-
backgroundColor: subtlePanelFill(),
|
|
171
|
-
}}
|
|
184
|
+
style={frameStyle}
|
|
172
185
|
>
|
|
173
186
|
<Box flexDirection="column" gap={rowGap}>
|
|
174
187
|
{rows}
|
|
175
188
|
</Box>
|
|
176
189
|
</ScrollBox>
|
|
177
190
|
) : (
|
|
178
|
-
<Box flexDirection="column" gap={rowGap}>
|
|
191
|
+
<Box flexDirection="column" gap={rowGap} style={frameStyle}>
|
|
179
192
|
{rows}
|
|
180
193
|
</Box>
|
|
181
194
|
)}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DesktopDeepLinkBridge } from "../../../types/desktop-deeplink";
|
|
2
|
+
import { onDesktopDeepLink } from "./backend-rpc";
|
|
3
|
+
|
|
4
|
+
export function createDesktopDeepLinkBridge(): DesktopDeepLinkBridge {
|
|
5
|
+
return {
|
|
6
|
+
subscribe(listener) {
|
|
7
|
+
return onDesktopDeepLink(listener);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
cancelWebFrame,
|
|
20
20
|
cellBoundsForElement,
|
|
21
21
|
cellMouseEvent,
|
|
22
|
-
hasDirectMouseHandler,
|
|
23
22
|
requestWebFrame,
|
|
24
23
|
} from "./mouse";
|
|
25
24
|
import { cleanDomProps, commonStyle } from "./style";
|
|
@@ -148,7 +147,6 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
|
|
|
148
147
|
<div
|
|
149
148
|
{...cleanDomProps(props)}
|
|
150
149
|
data-gloom-hover-bg={hoverBackgroundColor ? "true" : undefined}
|
|
151
|
-
data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
|
|
152
150
|
ref={elementRef}
|
|
153
151
|
onMouseDown={handleMouseDown}
|
|
154
152
|
onMouseOver={handleMouseOver}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import type { ScrollBoxRenderable } from "../../../../ui/host";
|
|
16
16
|
import { WEB_CELL_HEIGHT, WEB_CELL_WIDTH } from "../input-host";
|
|
17
|
-
import { callMouseHandler
|
|
17
|
+
import { callMouseHandler } from "./mouse";
|
|
18
18
|
import { cleanDomProps, commonStyle } from "./style";
|
|
19
19
|
import { useScrollbarActivity } from "../scrollbar-activity";
|
|
20
20
|
|
|
@@ -254,7 +254,6 @@ export const WebScrollBox = forwardRef<ScrollBoxRenderable, Record<string, unkno
|
|
|
254
254
|
data-gloom-scrollbar-x={props.scrollX === true ? (horizontalScrollBarVisible ? "visible" : "hidden") : undefined}
|
|
255
255
|
data-gloom-scrollbar-y={props.scrollY === true ? (verticalScrollBarVisible ? "visible" : "hidden") : undefined}
|
|
256
256
|
data-gloom-scrollbar-active={scrollbarActive ? "true" : undefined}
|
|
257
|
-
data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
|
|
258
257
|
onMouseDown={(event) => callMouseHandler(props.onMouseDown, event, "down")}
|
|
259
258
|
onMouseMove={(event) => callMouseHandler(props.onMouseMove, event, "move")}
|
|
260
259
|
onMouseUp={(event) => callMouseHandler(props.onMouseUp, event, "up")}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "../../../../ui/host";
|
|
10
10
|
import { WEB_CELL_WIDTH } from "../input-host";
|
|
11
11
|
import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
|
|
12
|
-
import {
|
|
12
|
+
import { mouseHandlers } from "./mouse";
|
|
13
13
|
import { cleanDomProps, commonStyle, textStyle } from "./style";
|
|
14
14
|
|
|
15
15
|
const WEB_WORDMARK_CHAR_WIDTH_PX = Math.max(10, WEB_CELL_WIDTH);
|
|
@@ -56,7 +56,6 @@ export function WebText({ children, ...props }: TextProps) {
|
|
|
56
56
|
<span
|
|
57
57
|
{...cleanDomProps(props)}
|
|
58
58
|
{...mouseHandlers(props)}
|
|
59
|
-
data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
|
|
60
59
|
style={{ ...textStyle(props), ...(props.style as CSSProperties | undefined) }}
|
|
61
60
|
>
|
|
62
61
|
{renderTextContent(children, props.content, props)}
|
|
@@ -21,6 +21,7 @@ import { webNativeRenderer } from "./native-renderer";
|
|
|
21
21
|
import { WebToastHostProvider } from "./toast-host";
|
|
22
22
|
import { createWebUiHost, webRendererHost } from "./ui-host";
|
|
23
23
|
import { createApplicationMenuBridge } from "./application-menu-bridge";
|
|
24
|
+
import { createDesktopDeepLinkBridge } from "./desktop-deeplink-bridge";
|
|
24
25
|
import { createDesktopWindowBridge } from "./desktop/window/bridge";
|
|
25
26
|
import { prepareDetachedSnapshot } from "./desktop/window/snapshot";
|
|
26
27
|
|
|
@@ -88,6 +89,7 @@ async function boot() {
|
|
|
88
89
|
const config = desktopSnapshot?.config ?? init.config;
|
|
89
90
|
const desktopWindowBridge = createDesktopWindowBridge(init.windowKind, init.paneId);
|
|
90
91
|
const desktopApplicationMenuBridge = createApplicationMenuBridge();
|
|
92
|
+
const desktopDeepLinkBridge = createDesktopDeepLinkBridge();
|
|
91
93
|
const webUiHost = createWebUiHost(init.desktopPlatform);
|
|
92
94
|
const remoteControlAdapter = init.windowKind === "main"
|
|
93
95
|
? { registerHandler: setElectrobunRemoteRequestHandler }
|
|
@@ -103,6 +105,7 @@ async function boot() {
|
|
|
103
105
|
config={config}
|
|
104
106
|
desktopWindowBridge={desktopWindowBridge}
|
|
105
107
|
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
108
|
+
desktopDeepLinkBridge={desktopDeepLinkBridge}
|
|
106
109
|
desktopSnapshot={desktopSnapshot}
|
|
107
110
|
desktopThemePreview={init.desktopThemePreview}
|
|
108
111
|
remoteControlAdapter={remoteControlAdapter}
|
|
@@ -7,6 +7,7 @@ import { backendRequest } from "./backend-rpc";
|
|
|
7
7
|
import { WebDataTable } from "./data-table";
|
|
8
8
|
import {
|
|
9
9
|
WebButton,
|
|
10
|
+
WebCheckbox,
|
|
10
11
|
WebDialogFrame,
|
|
11
12
|
WebListView,
|
|
12
13
|
WebMessageComposer,
|
|
@@ -82,6 +83,7 @@ export function createWebUiHost(desktopPlatform?: string): UiHost {
|
|
|
82
83
|
Input: WebInput,
|
|
83
84
|
Textarea: WebTextarea,
|
|
84
85
|
Button: WebButton,
|
|
86
|
+
Checkbox: WebCheckbox,
|
|
85
87
|
TextField: WebTextField,
|
|
86
88
|
MessageComposer: WebMessageComposer,
|
|
87
89
|
ListView: WebListView,
|
|
@@ -12,8 +12,10 @@ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshnes
|
|
|
12
12
|
import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution";
|
|
13
13
|
import { selectCachedResource } from "./cache";
|
|
14
14
|
import {
|
|
15
|
+
dropUnusableProviderQuote,
|
|
15
16
|
hasDeepStatementHistory,
|
|
16
17
|
hasDetailedStatementRows,
|
|
18
|
+
isProviderQuoteUsableForCurrentSession,
|
|
17
19
|
quoteWithFreshnessExchange,
|
|
18
20
|
type CachedFinancialsSelection,
|
|
19
21
|
} from "./financials";
|
|
@@ -52,8 +54,9 @@ export class ProviderRouterBatchRoutes {
|
|
|
52
54
|
const context = target.context;
|
|
53
55
|
const entityKey = this.deps.getEntityKey(target.symbol, context?.instrument);
|
|
54
56
|
const variantKeys = this.deps.getTickerVariantCandidates(target.exchange);
|
|
57
|
+
const brokerSourceKeys = this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate));
|
|
55
58
|
const sourceKeys = [
|
|
56
|
-
...
|
|
59
|
+
...brokerSourceKeys,
|
|
57
60
|
...this.deps.getProviderSourceKeys(),
|
|
58
61
|
];
|
|
59
62
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
@@ -61,6 +64,10 @@ export class ProviderRouterBatchRoutes {
|
|
|
61
64
|
? rawCached
|
|
62
65
|
: null;
|
|
63
66
|
if (cached && !forceRefresh && !cached.stale) {
|
|
67
|
+
if (brokerSourceKeys.includes(cached.sourceKey)) {
|
|
68
|
+
misses.push({ index, target });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
results[index] = { target, quote: cached.value };
|
|
65
72
|
return;
|
|
66
73
|
}
|
|
@@ -80,7 +87,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
80
87
|
const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
|
|
81
88
|
const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
|
|
82
89
|
for (const item of batchResults) {
|
|
83
|
-
if (!item.quote
|
|
90
|
+
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange)) continue;
|
|
84
91
|
const key = this.quoteBatchKey(item.target);
|
|
85
92
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
86
93
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -142,7 +149,8 @@ export class ProviderRouterBatchRoutes {
|
|
|
142
149
|
for (const item of batchResults) {
|
|
143
150
|
if (!item.financials) continue;
|
|
144
151
|
const key = this.cachedFinancialsBatchKey(item.target);
|
|
145
|
-
|
|
152
|
+
let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
|
|
153
|
+
if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
|
|
146
154
|
if (!value) continue;
|
|
147
155
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
148
156
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -405,6 +405,129 @@ describe("AssetDataRouter cached financials", () => {
|
|
|
405
405
|
persistence.close();
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
test("uses symbol provider quote reference for broker-linked snapshot reads", async () => {
|
|
409
|
+
const dbPath = createTempDbPath("cached-symbol-reference-for-contract-snapshot");
|
|
410
|
+
const persistence = new AppPersistence(dbPath);
|
|
411
|
+
const now = Date.now();
|
|
412
|
+
|
|
413
|
+
persistence.resources.set(
|
|
414
|
+
{
|
|
415
|
+
namespace: "market",
|
|
416
|
+
kind: "financials",
|
|
417
|
+
entityKey: "contract:275759",
|
|
418
|
+
variantKey: "exchange=NASDAQ",
|
|
419
|
+
sourceKey: "provider:gloomberb-cloud",
|
|
420
|
+
},
|
|
421
|
+
makeFinancials({
|
|
422
|
+
quote: makeQuote({
|
|
423
|
+
symbol: "VICR",
|
|
424
|
+
providerId: "gloomberb-cloud",
|
|
425
|
+
price: 292.83,
|
|
426
|
+
currency: "USD",
|
|
427
|
+
previousClose: 380.07,
|
|
428
|
+
change: -87.24,
|
|
429
|
+
changePercent: -22.95,
|
|
430
|
+
lastUpdated: now,
|
|
431
|
+
marketState: "REGULAR",
|
|
432
|
+
exchangeName: "NASDAQ",
|
|
433
|
+
listingExchangeName: "NASDAQ",
|
|
434
|
+
dataSource: "live",
|
|
435
|
+
}),
|
|
436
|
+
profile: {
|
|
437
|
+
description: "Cloud profile",
|
|
438
|
+
},
|
|
439
|
+
fundamentals: {
|
|
440
|
+
trailingPE: 94.3,
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
{
|
|
444
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
445
|
+
fetchedAt: now,
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
persistence.resources.set(
|
|
449
|
+
{
|
|
450
|
+
namespace: "market",
|
|
451
|
+
kind: "financials",
|
|
452
|
+
entityKey: "contract:275759",
|
|
453
|
+
variantKey: "exchange=NASDAQ",
|
|
454
|
+
sourceKey: "provider:yahoo",
|
|
455
|
+
},
|
|
456
|
+
makeFinancials({
|
|
457
|
+
annualStatements: [{ date: "2025-12-31", totalRevenue: 100 }],
|
|
458
|
+
}),
|
|
459
|
+
{
|
|
460
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
461
|
+
fetchedAt: now + 1_000,
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
persistence.resources.set(
|
|
465
|
+
{
|
|
466
|
+
namespace: "market",
|
|
467
|
+
kind: "financials",
|
|
468
|
+
entityKey: "VICR",
|
|
469
|
+
variantKey: "exchange=NASDAQ",
|
|
470
|
+
sourceKey: "provider:yahoo",
|
|
471
|
+
},
|
|
472
|
+
makeFinancials({
|
|
473
|
+
quote: makeQuote({
|
|
474
|
+
symbol: "VICR",
|
|
475
|
+
providerId: "yahoo",
|
|
476
|
+
price: 294.39,
|
|
477
|
+
currency: "USD",
|
|
478
|
+
previousClose: 282.95,
|
|
479
|
+
change: 11.44,
|
|
480
|
+
changePercent: 4.04,
|
|
481
|
+
lastUpdated: now - 1_000,
|
|
482
|
+
marketState: "REGULAR",
|
|
483
|
+
exchangeName: "NASDAQ",
|
|
484
|
+
listingExchangeName: "NASDAQ",
|
|
485
|
+
dataSource: "delayed",
|
|
486
|
+
}),
|
|
487
|
+
}),
|
|
488
|
+
{
|
|
489
|
+
cachePolicy: { staleMs: 60_000, expireMs: 7 * 24 * 60 * 60_000 },
|
|
490
|
+
fetchedAt: now - 1_000,
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const router = new AssetDataRouter({
|
|
495
|
+
...fallbackProvider,
|
|
496
|
+
id: "yahoo",
|
|
497
|
+
name: "Yahoo",
|
|
498
|
+
async getTickerFinancials() {
|
|
499
|
+
throw new Error("should not fetch financials");
|
|
500
|
+
},
|
|
501
|
+
}, [{
|
|
502
|
+
...fallbackProvider,
|
|
503
|
+
id: "gloomberb-cloud",
|
|
504
|
+
name: "Cloud",
|
|
505
|
+
priority: 100,
|
|
506
|
+
async getTickerFinancials() {
|
|
507
|
+
throw new Error("should not fetch financials");
|
|
508
|
+
},
|
|
509
|
+
}], persistence.resources);
|
|
510
|
+
|
|
511
|
+
const financials = await router.getTickerFinancials("VICR", "NASDAQ", {
|
|
512
|
+
brokerId: "ibkr",
|
|
513
|
+
brokerInstanceId: "ibkr-work",
|
|
514
|
+
instrument: {
|
|
515
|
+
brokerId: "ibkr",
|
|
516
|
+
brokerInstanceId: "ibkr-work",
|
|
517
|
+
conId: 275759,
|
|
518
|
+
symbol: "VICR",
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(financials.quote?.providerId).toBe("gloomberb-cloud");
|
|
523
|
+
expect(financials.quote?.price).toBe(292.83);
|
|
524
|
+
expect(financials.quote?.previousClose).toBe(282.95);
|
|
525
|
+
expect(financials.quote?.changePercent).toBeCloseTo(((292.83 - 282.95) / 282.95) * 100, 10);
|
|
526
|
+
expect(financials.quote?.provenance?.fields?.previousClose?.providerId).toBe("yahoo");
|
|
527
|
+
|
|
528
|
+
persistence.close();
|
|
529
|
+
});
|
|
530
|
+
|
|
408
531
|
test("overlays standalone quote cache on stale financial snapshots during startup priming", () => {
|
|
409
532
|
const dbPath = createTempDbPath("cached-quote-over-financial-snapshot");
|
|
410
533
|
const persistence = new AppPersistence(dbPath);
|