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.
Files changed (126) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.ts +48 -0
  4. package/src/api-client/types.ts +45 -0
  5. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  6. package/src/app/runtime/desktop-deeplink.ts +496 -0
  7. package/src/app.tsx +34 -0
  8. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  9. package/src/components/chart/bar-chart-renderer.ts +111 -55
  10. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  11. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  12. package/src/components/chart/stock-chart/footer.ts +68 -13
  13. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  14. package/src/components/command-bar/pane-templates/items.ts +11 -3
  15. package/src/components/command-bar/routes/root/results.ts +1 -1
  16. package/src/components/command-bar/surface/index.test.tsx +52 -0
  17. package/src/components/command-bar/workflow/ops.ts +81 -20
  18. package/src/components/index.ts +1 -0
  19. package/src/components/toggle-list.tsx +29 -73
  20. package/src/components/ui/button.tsx +4 -1
  21. package/src/components/ui/checkbox.tsx +76 -0
  22. package/src/components/ui/choice-dialog.tsx +15 -3
  23. package/src/components/ui/frame.tsx +1 -1
  24. package/src/components/ui/index.ts +2 -0
  25. package/src/components/ui/message-composer.tsx +4 -0
  26. package/src/components/ui/native-select.tsx +4 -2
  27. package/src/market-data/coordinator/events.test.ts +49 -0
  28. package/src/market-data/coordinator/events.ts +25 -4
  29. package/src/market-data/coordinator/index.test.ts +223 -0
  30. package/src/market-data/coordinator/index.ts +40 -3
  31. package/src/market-data/coordinator/quotes.ts +13 -5
  32. package/src/market-data/hooks.tsx +17 -13
  33. package/src/market-data/market/freshness.ts +58 -1
  34. package/src/market-data/quotes/freshness.test.ts +49 -1
  35. package/src/market-data/quotes/freshness.ts +8 -0
  36. package/src/market-data/quotes/resolution.test.ts +185 -0
  37. package/src/market-data/quotes/resolution.ts +94 -8
  38. package/src/market-data/request-types.test.ts +61 -1
  39. package/src/market-data/request-types.ts +42 -3
  40. package/src/plugins/builtin/account-management/footer.ts +2 -5
  41. package/src/plugins/builtin/account-management/form-components.tsx +260 -24
  42. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  43. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  44. package/src/plugins/builtin/account-management/model.ts +263 -3
  45. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  46. package/src/plugins/builtin/chat/chat.test.tsx +125 -0
  47. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  48. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  49. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  50. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  51. package/src/plugins/builtin/chat/content/index.tsx +154 -16
  52. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  53. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  54. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  55. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  56. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  57. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  58. package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
  59. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  60. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  61. package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
  62. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  63. package/src/plugins/builtin/chat/test-harness.tsx +21 -11
  64. package/src/plugins/builtin/cloud/plugin.tsx +7 -0
  65. package/src/plugins/builtin/correlation/index.tsx +4 -1
  66. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  67. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  68. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  69. package/src/plugins/builtin/portfolio-list/index.test.ts +51 -1
  70. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  71. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  72. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  73. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  74. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  75. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
  76. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  77. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  78. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  79. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  80. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  81. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  82. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  83. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  84. package/src/plugins/registry/context.ts +15 -0
  85. package/src/plugins/registry/index.ts +31 -0
  86. package/src/renderers/electrobun/bun/index.ts +42 -0
  87. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  88. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  89. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  90. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  91. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  92. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  93. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  94. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  95. package/src/renderers/electrobun/view/main.tsx +3 -0
  96. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  97. package/src/sources/provider-router/batches.ts +11 -3
  98. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  99. package/src/sources/provider-router/financial-routes.ts +55 -5
  100. package/src/sources/provider-router/financials.test.ts +49 -0
  101. package/src/sources/provider-router/financials.ts +40 -3
  102. package/src/sources/provider-router/index.test.ts +147 -0
  103. package/src/sources/provider-router/primary.ts +17 -3
  104. package/src/sources/provider-router/streaming.ts +6 -1
  105. package/src/sources/sec-edgar.test.ts +42 -0
  106. package/src/sources/sec-edgar.ts +1 -2
  107. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  108. package/src/sources/yahoo-finance/mappers.ts +2 -1
  109. package/src/sources/yahoo-finance/requests.ts +2 -1
  110. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  111. package/src/sources/yahoo-finance.ts +6 -2
  112. package/src/state/hooks/quote-streaming.test.ts +14 -1
  113. package/src/state/hooks/quote-streaming.ts +5 -4
  114. package/src/sync/controller.ts +296 -0
  115. package/src/sync/core-contributors.test.ts +251 -0
  116. package/src/sync/core-contributors.ts +400 -0
  117. package/src/sync/profile-analytics.ts +39 -0
  118. package/src/sync/react.ts +53 -0
  119. package/src/sync/types.ts +77 -0
  120. package/src/types/desktop-deeplink.ts +7 -0
  121. package/src/types/plugin.ts +3 -0
  122. package/src/ui/host.tsx +14 -1
  123. package/src/ui/index.tsx +1 -0
  124. package/src/utils/exchanges.test.ts +11 -1
  125. package/src/utils/exchanges.ts +5 -1
  126. 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
- paddingInline: 8,
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={{ fontWeight: active || variant === "primary" ? 700 : 600 }}
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 = true,
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 flexDirection="row" justifyContent="space-between" width="100%" alignItems="center">
24
- <Box flexDirection="row" alignItems="center" minWidth={0}>
25
- <Box
26
- width={1}
27
- height={1}
28
- marginRight={1}
29
- backgroundColor={selected ? colors.borderFocused : "transparent"}
30
- style={{ borderRadius: CONTROL_RADIUS }}
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 = "framed",
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={surface === "plain"
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, hasDirectMouseHandler } from "./mouse";
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 { hasDirectMouseHandler, mouseHandlers } from "./mouse";
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
- ...this.deps.getBrokerCandidatesForContext(context, false).map((candidate) => this.deps.brokerSourceKey(candidate)),
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 || isQuoteStaleForCurrentSession(item.quote)) continue;
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
- const value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
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);