gloomberb 0.6.1 → 0.7.1
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/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState, type ForwardedRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
computeBitmapSize,
|
|
4
|
+
intersectCellRects,
|
|
5
|
+
type CellRect,
|
|
6
|
+
type NativeChartBitmap,
|
|
7
|
+
} from "../../components/chart/native/chart-rasterizer";
|
|
8
|
+
import { getCachedKittySupport, ensureKittySupport } from "../../components/chart/native/kitty-support";
|
|
9
|
+
import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
|
|
10
|
+
import {
|
|
11
|
+
getRenderableCellRect,
|
|
12
|
+
resolveNativeSurfaceVisibleRect,
|
|
13
|
+
type NativeSurfaceRenderableNode,
|
|
14
|
+
} from "../../components/chart/native/surface-visibility";
|
|
15
|
+
import { useOptionalPaneInstanceId } from "../../state/app-context";
|
|
16
|
+
import { useNativeRenderer, type BoxRenderable, type ChartSurfaceProps } from "../../ui";
|
|
17
|
+
|
|
18
|
+
interface NativeRenderableNode extends BoxRenderable, NativeSurfaceRenderableNode {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
parent: NativeRenderableNode | null;
|
|
24
|
+
onLifecyclePass: (() => void) | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SurfaceTarget {
|
|
28
|
+
rect: CellRect;
|
|
29
|
+
visibleRect: CellRect | null;
|
|
30
|
+
bitmapKey: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let nextChartSurfaceId = 1;
|
|
34
|
+
|
|
35
|
+
function assignRef(ref: ForwardedRef<unknown>, value: unknown) {
|
|
36
|
+
if (typeof ref === "function") {
|
|
37
|
+
ref(value);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (ref) {
|
|
41
|
+
(ref as { current: unknown }).current = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameRect(left: CellRect | null, right: CellRect | null): boolean {
|
|
46
|
+
if (left === right) return true;
|
|
47
|
+
if (!left || !right) return false;
|
|
48
|
+
return left.x === right.x
|
|
49
|
+
&& left.y === right.y
|
|
50
|
+
&& left.width === right.width
|
|
51
|
+
&& left.height === right.height;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sameTarget(left: SurfaceTarget | null, right: SurfaceTarget | null): boolean {
|
|
55
|
+
if (left === right) return true;
|
|
56
|
+
if (!left || !right) return false;
|
|
57
|
+
return left.bitmapKey === right.bitmapKey
|
|
58
|
+
&& sameRect(left.rect, right.rect)
|
|
59
|
+
&& sameRect(left.visibleRect, right.visibleRect);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hashBitmap(bitmap: NativeChartBitmap): string {
|
|
63
|
+
let hash = 0x811c9dc5;
|
|
64
|
+
for (let index = 0; index < bitmap.pixels.length; index += 1) {
|
|
65
|
+
hash ^= bitmap.pixels[index]!;
|
|
66
|
+
hash = Math.imul(hash, 0x01000193);
|
|
67
|
+
}
|
|
68
|
+
return (hash >>> 0).toString(16);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function bitmapKey(bitmap: NativeChartBitmap): string {
|
|
72
|
+
return `${bitmap.width}x${bitmap.height}:${hashBitmap(bitmap)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const OpenTuiChartSurface = forwardRef<unknown, ChartSurfaceProps>(function OpenTuiChartSurface(
|
|
76
|
+
{ children, bitmap, bitmaps, crosshair: _crosshair, ...props },
|
|
77
|
+
forwardedRef,
|
|
78
|
+
) {
|
|
79
|
+
const renderer = useNativeRenderer();
|
|
80
|
+
const paneId = useOptionalPaneInstanceId();
|
|
81
|
+
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
82
|
+
const surfaceId = useRef(`opentui-chart:${nextChartSurfaceId++}`).current;
|
|
83
|
+
const renderableRef = useRef<NativeRenderableNode | null>(null);
|
|
84
|
+
const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
|
|
85
|
+
const [target, setTarget] = useState<SurfaceTarget | null>(null);
|
|
86
|
+
const nativeBitmap = (bitmaps?.[0] ?? bitmap ?? null) as NativeChartBitmap | null;
|
|
87
|
+
const nativeBitmapKey = useMemo(() => (nativeBitmap ? bitmapKey(nativeBitmap) : null), [nativeBitmap]);
|
|
88
|
+
|
|
89
|
+
const setRenderableRef = useCallback((node: unknown) => {
|
|
90
|
+
renderableRef.current = node as NativeRenderableNode | null;
|
|
91
|
+
assignRef(forwardedRef, node);
|
|
92
|
+
}, [forwardedRef]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
let cancelled = false;
|
|
96
|
+
setKittySupport(getCachedKittySupport(renderer));
|
|
97
|
+
ensureKittySupport(renderer).then((supported) => {
|
|
98
|
+
if (!cancelled) setKittySupport(supported);
|
|
99
|
+
});
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
}, [renderer]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const renderable = renderableRef.current;
|
|
107
|
+
if (!renderable || !nativeBitmap || !nativeBitmapKey || kittySupport !== true) {
|
|
108
|
+
setTarget(null);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let mountTimer: Timer | null = null;
|
|
113
|
+
const previousLifecyclePass = renderable.onLifecyclePass;
|
|
114
|
+
const syncTarget = () => {
|
|
115
|
+
const rect = getRenderableCellRect(renderable);
|
|
116
|
+
if (!renderer.resolution || renderer.terminalWidth <= 0 || renderer.terminalHeight <= 0) {
|
|
117
|
+
setTarget((current) => (current === null ? current : null));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const visibleRect = resolveNativeSurfaceVisibleRect(renderable, renderer.terminalWidth, renderer.terminalHeight);
|
|
122
|
+
const clippedVisibleRect = visibleRect ? intersectCellRects(rect, visibleRect) : null;
|
|
123
|
+
const expectedSize = computeBitmapSize(rect, renderer.resolution, renderer.terminalWidth, renderer.terminalHeight);
|
|
124
|
+
const nextTarget: SurfaceTarget = {
|
|
125
|
+
rect,
|
|
126
|
+
visibleRect: clippedVisibleRect,
|
|
127
|
+
bitmapKey: `${nativeBitmapKey}:${expectedSize.pixelWidth}x${expectedSize.pixelHeight}`,
|
|
128
|
+
};
|
|
129
|
+
setTarget((current) => (sameTarget(current, nextTarget) ? current : nextTarget));
|
|
130
|
+
};
|
|
131
|
+
const lifecyclePass = () => {
|
|
132
|
+
previousLifecyclePass?.();
|
|
133
|
+
syncTarget();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
renderable.onLifecyclePass = lifecyclePass;
|
|
137
|
+
renderer.registerLifecyclePass(renderable);
|
|
138
|
+
syncTarget();
|
|
139
|
+
mountTimer = setTimeout(() => {
|
|
140
|
+
syncTarget();
|
|
141
|
+
renderer.requestRender();
|
|
142
|
+
}, 0);
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
if (mountTimer) clearTimeout(mountTimer);
|
|
146
|
+
if (renderable.onLifecyclePass === lifecyclePass) {
|
|
147
|
+
renderable.onLifecyclePass = previousLifecyclePass;
|
|
148
|
+
}
|
|
149
|
+
renderer.unregisterLifecyclePass(renderable);
|
|
150
|
+
setTarget(null);
|
|
151
|
+
};
|
|
152
|
+
}, [kittySupport, nativeBitmap, nativeBitmapKey, renderer]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
return () => {
|
|
156
|
+
nativeSurfaceManager.removeSurface(surfaceId);
|
|
157
|
+
};
|
|
158
|
+
}, [nativeSurfaceManager, surfaceId]);
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (kittySupport !== true || !target?.visibleRect || !nativeBitmap) {
|
|
162
|
+
nativeSurfaceManager.removeSurface(surfaceId);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
nativeSurfaceManager.upsertSurface({
|
|
167
|
+
id: surfaceId,
|
|
168
|
+
paneId: paneId ?? "__global__",
|
|
169
|
+
rect: target.rect,
|
|
170
|
+
visibleRect: target.visibleRect,
|
|
171
|
+
bitmap: nativeBitmap,
|
|
172
|
+
bitmapKey: target.bitmapKey,
|
|
173
|
+
});
|
|
174
|
+
renderer.requestRender();
|
|
175
|
+
}, [kittySupport, nativeBitmap, nativeSurfaceManager, paneId, renderer, surfaceId, target]);
|
|
176
|
+
|
|
177
|
+
const showFallback = kittySupport !== true || !target || !nativeBitmap;
|
|
178
|
+
return createElement("box" as any, { ...props, ref: setRenderableRef }, showFallback ? children as ReactNode : null);
|
|
179
|
+
});
|
|
@@ -15,7 +15,7 @@ export function OpenTuiInputHostProvider({ children }: { children: ReactNode })
|
|
|
15
15
|
const shortcutsRef = useRef<ShortcutEntry[]>([]);
|
|
16
16
|
const dispatchShortcut = (event: KeyEventLike) => {
|
|
17
17
|
const shortcuts = shortcutsRef.current;
|
|
18
|
-
for (const phase of ["normal", "after"] as const) {
|
|
18
|
+
for (const phase of ["before", "normal", "after"] as const) {
|
|
19
19
|
if (phase === "after" && (event.defaultPrevented || event.propagationStopped)) return;
|
|
20
20
|
for (const entry of shortcuts) {
|
|
21
21
|
if (entry.phase !== phase || !entry.enabledRef.current) continue;
|
|
@@ -34,38 +34,56 @@ export async function startOpenTuiApp(): Promise<void> {
|
|
|
34
34
|
cliLaunchRequest = dispatchResult.request;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
startMainThreadMonitor("opentui");
|
|
37
|
+
const stopMainThreadMonitor = startMainThreadMonitor("opentui");
|
|
38
38
|
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
let host: Awaited<ReturnType<typeof createOpenTuiHost>> | null = null;
|
|
40
|
+
let exitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
const finishProcessExit = () => {
|
|
42
|
+
stopMainThreadMonitor();
|
|
43
|
+
if (exitTimer) return;
|
|
44
|
+
exitTimer = setTimeout(() => {
|
|
45
|
+
process.exit(process.exitCode ?? 0);
|
|
46
|
+
}, 0);
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
let dataDir = await getDataDir();
|
|
50
|
+
if (!dataDir) {
|
|
51
|
+
dataDir = join(process.env.HOME || "~", ".gloomberb");
|
|
52
|
+
}
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
if (!existsSync(dataDir)) {
|
|
55
|
+
mkdirSync(dataDir, { recursive: true });
|
|
56
|
+
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
const config = await measurePerfAsync("startup.opentui.init-data-dir", () => initDataDir(dataDir));
|
|
59
|
+
host = await measurePerfAsync("startup.opentui.create-host", () => createOpenTuiHost());
|
|
60
|
+
host.renderer.once("destroy", finishProcessExit);
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
host.render(
|
|
63
|
+
<UiHostProvider ui={openTuiUiHost} renderer={host.rendererHost} nativeRenderer={host.nativeRenderer}>
|
|
64
|
+
<OpenTuiInputHostProvider>
|
|
65
|
+
<ToastHostProvider host={openTuiToastHost}>
|
|
66
|
+
<OpenTuiDialogHostProvider
|
|
67
|
+
size="medium"
|
|
68
|
+
dialogOptions={{ style: { backgroundColor: colors.bg, borderColor: colors.borderFocused, borderStyle: "single", paddingX: 2, paddingY: 1 } }}
|
|
69
|
+
backdropColor="#000000"
|
|
70
|
+
backdropOpacity={0.8}
|
|
71
|
+
>
|
|
72
|
+
<App
|
|
73
|
+
config={config}
|
|
74
|
+
externalPlugins={externalPlugins}
|
|
75
|
+
cliLaunchRequest={cliLaunchRequest}
|
|
76
|
+
/>
|
|
77
|
+
</OpenTuiDialogHostProvider>
|
|
78
|
+
</ToastHostProvider>
|
|
79
|
+
</OpenTuiInputHostProvider>
|
|
80
|
+
</UiHostProvider>,
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (exitTimer) clearTimeout(exitTimer);
|
|
84
|
+
stopMainThreadMonitor();
|
|
85
|
+
host?.renderer.off("destroy", finishProcessExit);
|
|
86
|
+
host?.destroy();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
71
89
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from "react";
|
|
2
|
+
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
2
3
|
import { testRender as openTuiTestRender } from "@opentui/react/test-utils";
|
|
3
4
|
import { createRoot as openTuiCreateRoot, useRenderer } from "@opentui/react";
|
|
4
5
|
import { UiHostProvider, type NativeRendererHost, type RendererHost } from "../../ui";
|
|
@@ -8,6 +9,22 @@ import { openTuiUiHost } from "./ui-host";
|
|
|
8
9
|
import { OpenTuiDialogHostProvider } from "./dialog-host";
|
|
9
10
|
import { openTuiToastHost } from "./toast-host";
|
|
10
11
|
|
|
12
|
+
export function TestDialogProvider({ children }: { children: ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<DialogProvider
|
|
15
|
+
dialogOptions={{
|
|
16
|
+
style: {
|
|
17
|
+
backgroundColor: "#000000",
|
|
18
|
+
borderColor: "#ffffff",
|
|
19
|
+
borderStyle: "single",
|
|
20
|
+
},
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</DialogProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
function createTestNativeRendererHost(renderer: any): NativeRendererHost {
|
|
12
29
|
if (typeof renderer.write !== "function") {
|
|
13
30
|
renderer.write = (data: string | Uint8Array) => {
|
|
@@ -4,6 +4,7 @@ import "opentui-spinner/react";
|
|
|
4
4
|
import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
|
|
5
5
|
import { renderAsciiText } from "../../ui/ascii-font";
|
|
6
6
|
import { OpenTuiImageSurface } from "./image-surface";
|
|
7
|
+
import { OpenTuiChartSurface } from "./chart-surface";
|
|
7
8
|
|
|
8
9
|
function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
|
|
9
10
|
const flags = typeof appAttributes === "number" ? appAttributes : 0;
|
|
@@ -43,7 +44,9 @@ function mapTextContent(content: unknown): unknown {
|
|
|
43
44
|
|
|
44
45
|
export const openTuiUiHost: UiHost = {
|
|
45
46
|
kind: "opentui",
|
|
46
|
-
capabilities: {
|
|
47
|
+
capabilities: {
|
|
48
|
+
nativeCharts: true,
|
|
49
|
+
},
|
|
47
50
|
Box: ({ children, ...props }) => <box {...props}>{children}</box>,
|
|
48
51
|
Text: ({ children, ...props }) => {
|
|
49
52
|
const textProps = stripTextProps(props);
|
|
@@ -66,7 +69,7 @@ export const openTuiUiHost: UiHost = {
|
|
|
66
69
|
ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
|
|
67
70
|
Input: ({ children, ...props }) => createElement("input" as any, props, children),
|
|
68
71
|
Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
|
|
69
|
-
ChartSurface:
|
|
72
|
+
ChartSurface: OpenTuiChartSurface,
|
|
70
73
|
ImageSurface: OpenTuiImageSurface,
|
|
71
74
|
SpinnerMark: ({ children, ...props }) => createElement("spinner" as any, props, children),
|
|
72
75
|
AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createGloomberbCloudCapabilities, GloomberbCloudProvider } from "./gloomberb-cloud";
|
|
3
3
|
import type { NewsCapability } from "../capabilities";
|
|
4
|
-
import { apiClient, type AuthUser } from "../utils/api-client";
|
|
4
|
+
import { apiClient, type AuthUser, type CloudNewsPayload } from "../utils/api-client";
|
|
5
5
|
|
|
6
6
|
const verifiedUser: AuthUser = {
|
|
7
7
|
id: "user-1",
|
|
@@ -21,8 +21,56 @@ const originalGetCloudHolders = apiClient.getCloudHolders.bind(apiClient);
|
|
|
21
21
|
const originalGetCloudAnalystResearch = apiClient.getCloudAnalystResearch.bind(apiClient);
|
|
22
22
|
const originalGetCloudCorporateActions = apiClient.getCloudCorporateActions.bind(apiClient);
|
|
23
23
|
const originalGetCloudNews = apiClient.getCloudNews.bind(apiClient);
|
|
24
|
+
const originalGetCloudNewsStory = apiClient.getCloudNewsStory.bind(apiClient);
|
|
24
25
|
const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
|
|
25
26
|
|
|
27
|
+
function makeCloudNewsPayload(overrides: Partial<CloudNewsPayload> = {}): CloudNewsPayload {
|
|
28
|
+
return {
|
|
29
|
+
id: "story-1",
|
|
30
|
+
headline: "Apple raises guidance",
|
|
31
|
+
summary: "Apple lifted its outlook after stronger iPhone demand.",
|
|
32
|
+
topic: "guidance",
|
|
33
|
+
topics: ["guidance"],
|
|
34
|
+
category: "guidance",
|
|
35
|
+
sentiment: "positive",
|
|
36
|
+
sectors: ["information_technology"],
|
|
37
|
+
firstPublishedAt: "2026-04-01T10:00:00.000Z",
|
|
38
|
+
lastPublishedAt: "2026-04-01T10:05:00.000Z",
|
|
39
|
+
firstSeenAt: "2026-04-01T10:00:10.000Z",
|
|
40
|
+
lastSeenAt: "2026-04-01T10:05:10.000Z",
|
|
41
|
+
primaryUrl: "https://example.com/aapl-guidance",
|
|
42
|
+
primarySource: "example-wire",
|
|
43
|
+
scores: {
|
|
44
|
+
importance: 91,
|
|
45
|
+
urgency: 74,
|
|
46
|
+
marketImpact: 88,
|
|
47
|
+
novelty: 86,
|
|
48
|
+
confidence: 95,
|
|
49
|
+
},
|
|
50
|
+
flags: {
|
|
51
|
+
breaking: true,
|
|
52
|
+
developing: false,
|
|
53
|
+
stale: false,
|
|
54
|
+
},
|
|
55
|
+
variantCount: 1,
|
|
56
|
+
sourceCount: 1,
|
|
57
|
+
sources: ["example-wire"],
|
|
58
|
+
entities: [],
|
|
59
|
+
tickerLinks: [{
|
|
60
|
+
symbol: "AAPL",
|
|
61
|
+
exchange: "XNAS",
|
|
62
|
+
canonicalTicker: "AAPL:XNAS",
|
|
63
|
+
relationType: "direct",
|
|
64
|
+
displayTier: "primary",
|
|
65
|
+
confidence: 0.98,
|
|
66
|
+
relevanceScore: 95,
|
|
67
|
+
impactScore: 93,
|
|
68
|
+
sentiment: "positive",
|
|
69
|
+
}],
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
afterEach(() => {
|
|
27
75
|
apiClient.ensureVerifiedSession = originalEnsureVerifiedSession;
|
|
28
76
|
apiClient.getCloudHistory = originalGetCloudHistory;
|
|
@@ -31,6 +79,7 @@ afterEach(() => {
|
|
|
31
79
|
apiClient.getCloudAnalystResearch = originalGetCloudAnalystResearch;
|
|
32
80
|
apiClient.getCloudCorporateActions = originalGetCloudCorporateActions;
|
|
33
81
|
apiClient.getCloudNews = originalGetCloudNews;
|
|
82
|
+
apiClient.getCloudNewsStory = originalGetCloudNewsStory;
|
|
34
83
|
apiClient.subscribeQuotes = originalSubscribeQuotes;
|
|
35
84
|
});
|
|
36
85
|
|
|
@@ -496,4 +545,40 @@ describe("GloomberbCloudProvider", () => {
|
|
|
496
545
|
confidence: 93,
|
|
497
546
|
});
|
|
498
547
|
});
|
|
548
|
+
|
|
549
|
+
test("fetches story detail with ordered source items", async () => {
|
|
550
|
+
let requestedStoryId = "";
|
|
551
|
+
apiClient.getCloudNewsStory = async (storyId) => {
|
|
552
|
+
requestedStoryId = storyId;
|
|
553
|
+
return makeCloudNewsPayload({
|
|
554
|
+
id: storyId,
|
|
555
|
+
items: [{
|
|
556
|
+
id: "item-2",
|
|
557
|
+
sourceKey: "wire-b",
|
|
558
|
+
sourceName: "Wire B",
|
|
559
|
+
title: "Follow-up",
|
|
560
|
+
summary: "More details emerged.",
|
|
561
|
+
url: "https://example.com/follow-up",
|
|
562
|
+
publishedAt: "2026-04-01T10:05:00.000Z",
|
|
563
|
+
hasArticleText: true,
|
|
564
|
+
}, {
|
|
565
|
+
id: "item-1",
|
|
566
|
+
sourceKey: "wire-a",
|
|
567
|
+
sourceName: "Wire A",
|
|
568
|
+
title: "Original",
|
|
569
|
+
summary: "The first report.",
|
|
570
|
+
url: "https://example.com/original",
|
|
571
|
+
publishedAt: "2026-04-01T10:00:00.000Z",
|
|
572
|
+
hasArticleText: false,
|
|
573
|
+
}],
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const source = createGloomberbCloudCapabilities().find((capability) => capability.kind === "news") as NewsCapability;
|
|
578
|
+
const story = await source.provider.fetchNewsStory?.("story-1");
|
|
579
|
+
|
|
580
|
+
expect(requestedStoryId).toBe("story-1");
|
|
581
|
+
expect(story?.items?.map((item) => item.id)).toEqual(["item-2", "item-1"]);
|
|
582
|
+
expect(story?.items?.[0]?.publishedAt).toEqual(new Date("2026-04-01T10:05:00.000Z"));
|
|
583
|
+
});
|
|
499
584
|
});
|
|
@@ -5,15 +5,24 @@ import {
|
|
|
5
5
|
type ManualChartResolution,
|
|
6
6
|
} from "../components/chart/chart-resolution";
|
|
7
7
|
import { assetDataProvider, newsProvider, type PluginCapability } from "../capabilities";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
AssetDataProvider,
|
|
10
|
+
CachedFinancialsTarget,
|
|
11
|
+
MarketDataRequestContext,
|
|
12
|
+
NewsItem,
|
|
13
|
+
QuoteBatchResult,
|
|
14
|
+
QuoteSubscriptionTarget,
|
|
15
|
+
SearchRequestContext,
|
|
16
|
+
SecFilingItem,
|
|
17
|
+
TickerFinancialsBatchResult,
|
|
18
|
+
} from "../types/data-provider";
|
|
9
19
|
import type { AnalystResearchData, CorporateActionsData, HolderData, OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
|
|
10
20
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
11
21
|
import {
|
|
12
22
|
apiClient,
|
|
13
23
|
type CloudAnalystResearchPayload,
|
|
14
|
-
type CloudCompanyProfile,
|
|
15
24
|
type CloudCorporateActionsPayload,
|
|
16
|
-
type
|
|
25
|
+
type CloudMarketBatchItem,
|
|
17
26
|
type CloudHoldersPayload,
|
|
18
27
|
type CloudMarketResponse,
|
|
19
28
|
type CloudNewsPayload,
|
|
@@ -103,6 +112,34 @@ function mapPricePoint(point: CloudPricePointPayload, divisor = 1): PricePoint {
|
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
function mapCloudFinancials(financials: TickerFinancials): TickerFinancials {
|
|
116
|
+
const quote = financials.quote ? mapQuote(financials.quote as CloudQuotePayload) : undefined;
|
|
117
|
+
const divisor = quote ? resolveCurrencyUnit(quote.currency).divisor : 1;
|
|
118
|
+
return {
|
|
119
|
+
quote,
|
|
120
|
+
quoteContributions: financials.quoteContributions,
|
|
121
|
+
profile: financials.profile,
|
|
122
|
+
fundamentals: financials.fundamentals,
|
|
123
|
+
annualStatements: financials.annualStatements ?? [],
|
|
124
|
+
quarterlyStatements: financials.quarterlyStatements ?? [],
|
|
125
|
+
priceHistory: (financials.priceHistory ?? []).map((point) =>
|
|
126
|
+
point.date instanceof Date
|
|
127
|
+
? point
|
|
128
|
+
: mapPricePoint(point as unknown as CloudPricePointPayload, divisor)
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mapBatchError<T>(
|
|
134
|
+
item: CloudMarketBatchItem<T>,
|
|
135
|
+
fallbackMessage: string,
|
|
136
|
+
): Error {
|
|
137
|
+
if (isEmptyCloudStatus(item.status)) {
|
|
138
|
+
return createProviderMiss(item.reasonCode ?? fallbackMessage);
|
|
139
|
+
}
|
|
140
|
+
return new Error(item.reasonCode ?? fallbackMessage);
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
function normalizeStringArray(value: unknown): string[] {
|
|
107
144
|
return Array.isArray(value)
|
|
108
145
|
? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
|
@@ -358,27 +395,45 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
358
395
|
async getTickerFinancials(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<TickerFinancials> {
|
|
359
396
|
await requireVerifiedSession();
|
|
360
397
|
return withCloudFallback(async () => {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
apiClient.getCloudProfile(ticker, exchange),
|
|
364
|
-
apiClient.getCloudFundamentals(ticker, exchange),
|
|
365
|
-
apiClient.getCloudStatements(ticker, exchange, "both"),
|
|
366
|
-
]);
|
|
367
|
-
const quote = mapQuote(unwrapRequiredCloudResponse(quoteResponse, `Cloud quote is unavailable for ${ticker}`));
|
|
368
|
-
const profile = unwrapOptionalCloudResponse(profileResponse) as CloudCompanyProfile | null;
|
|
369
|
-
const fundamentals = unwrapOptionalCloudResponse(fundamentalsResponse) as CloudFundamentals | null;
|
|
370
|
-
const statements = unwrapOptionalCloudResponse(statementsResponse);
|
|
371
|
-
return {
|
|
372
|
-
quote,
|
|
373
|
-
profile: profile ?? undefined,
|
|
374
|
-
fundamentals: fundamentals ?? undefined,
|
|
375
|
-
annualStatements: statements?.annualStatements ?? [],
|
|
376
|
-
quarterlyStatements: statements?.quarterlyStatements ?? [],
|
|
377
|
-
priceHistory: [],
|
|
378
|
-
};
|
|
398
|
+
const response = await apiClient.getCloudFinancials(ticker, exchange);
|
|
399
|
+
return mapCloudFinancials(unwrapRequiredCloudResponse(response, `Cloud financials are unavailable for ${ticker}`));
|
|
379
400
|
}, `Cloud financials are unavailable for ${ticker}`);
|
|
380
401
|
}
|
|
381
402
|
|
|
403
|
+
async getTickerFinancialsBatch(
|
|
404
|
+
targets: CachedFinancialsTarget[],
|
|
405
|
+
options: { forceRefresh?: boolean } = {},
|
|
406
|
+
): Promise<TickerFinancialsBatchResult[]> {
|
|
407
|
+
await requireVerifiedSession();
|
|
408
|
+
return withCloudFallback(async () => {
|
|
409
|
+
const response = await apiClient.getCloudFinancialsBatch(
|
|
410
|
+
targets.map((target) => ({
|
|
411
|
+
symbol: target.symbol,
|
|
412
|
+
exchange: target.exchange,
|
|
413
|
+
})),
|
|
414
|
+
options.forceRefresh ? "refresh" : "cache-first",
|
|
415
|
+
);
|
|
416
|
+
const payload = unwrapRequiredCloudResponse(response, "Cloud financials are unavailable");
|
|
417
|
+
return payload.items.map((item, index) => {
|
|
418
|
+
const target = targets[index] ?? {
|
|
419
|
+
symbol: item.symbol,
|
|
420
|
+
exchange: item.exchange,
|
|
421
|
+
};
|
|
422
|
+
if ((item.status === "success" || item.status === "partial") && item.data) {
|
|
423
|
+
return {
|
|
424
|
+
target,
|
|
425
|
+
financials: mapCloudFinancials(item.data),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
target,
|
|
430
|
+
financials: null,
|
|
431
|
+
error: mapBatchError(item, `Cloud financials are unavailable for ${target.symbol}`),
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}, "Cloud financials are unavailable");
|
|
435
|
+
}
|
|
436
|
+
|
|
382
437
|
async getQuote(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<Quote> {
|
|
383
438
|
await requireVerifiedSession();
|
|
384
439
|
return withCloudFallback(
|
|
@@ -390,6 +445,40 @@ export class GloomberbCloudProvider implements AssetDataProvider {
|
|
|
390
445
|
);
|
|
391
446
|
}
|
|
392
447
|
|
|
448
|
+
async getQuotesBatch(
|
|
449
|
+
targets: QuoteSubscriptionTarget[],
|
|
450
|
+
options: { forceRefresh?: boolean } = {},
|
|
451
|
+
): Promise<QuoteBatchResult[]> {
|
|
452
|
+
await requireVerifiedSession();
|
|
453
|
+
return withCloudFallback(async () => {
|
|
454
|
+
const response = await apiClient.getCloudQuotesBatch(
|
|
455
|
+
targets.map((target) => ({
|
|
456
|
+
symbol: target.symbol,
|
|
457
|
+
exchange: target.exchange,
|
|
458
|
+
})),
|
|
459
|
+
options.forceRefresh ? "refresh" : "cache-first",
|
|
460
|
+
);
|
|
461
|
+
const payload = unwrapRequiredCloudResponse(response, "Cloud quotes are unavailable");
|
|
462
|
+
return payload.items.map((item, index) => {
|
|
463
|
+
const target = targets[index] ?? {
|
|
464
|
+
symbol: item.symbol,
|
|
465
|
+
exchange: item.exchange,
|
|
466
|
+
};
|
|
467
|
+
if ((item.status === "success" || item.status === "partial") && item.data) {
|
|
468
|
+
return {
|
|
469
|
+
target,
|
|
470
|
+
quote: mapQuote(item.data),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
target,
|
|
475
|
+
quote: null,
|
|
476
|
+
error: mapBatchError(item, `Cloud quotes are unavailable for ${target.symbol}`),
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
}, "Cloud quotes are unavailable");
|
|
480
|
+
}
|
|
481
|
+
|
|
393
482
|
async getExchangeRate(fromCurrency: string): Promise<number> {
|
|
394
483
|
await requireVerifiedSession();
|
|
395
484
|
const response = await apiClient.getCloudExchangeRate(fromCurrency);
|
|
@@ -575,6 +664,13 @@ export function createGloomberbCloudCapabilities(provider = createGloomberbCloud
|
|
|
575
664
|
);
|
|
576
665
|
return response.items.map((item) => mapCloudNewsArticle(item, query.ticker));
|
|
577
666
|
},
|
|
667
|
+
async fetchNewsStory(storyId: string): Promise<NewsArticle | null> {
|
|
668
|
+
const story = await withCloudFallback(
|
|
669
|
+
() => apiClient.getCloudNewsStory(storyId),
|
|
670
|
+
"Cloud news story is unavailable",
|
|
671
|
+
);
|
|
672
|
+
return mapCloudNewsArticle(story);
|
|
673
|
+
},
|
|
578
674
|
},
|
|
579
675
|
}),
|
|
580
676
|
];
|