gloomberb 0.9.7 → 0.9.9
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.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- 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/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- 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/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- 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/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- 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/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- 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 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- 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 +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- 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 +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- 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 +153 -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 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- 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 +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- 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/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- 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/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- 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/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- 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/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- 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/mouse.ts +0 -11
- 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/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- 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 +77 -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 +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +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.test.ts +31 -0
- package/src/sync/controller.ts +300 -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/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -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}
|
|
@@ -90,17 +90,6 @@ export function callCellMouseHandler(handler: unknown, event: CellMouseEvent): v
|
|
|
90
90
|
(handler as (event: unknown) => void)(event);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function hasDirectMouseHandler(props: Record<string, unknown>): boolean {
|
|
94
|
-
return typeof props.onMouseDown === "function"
|
|
95
|
-
|| typeof props.onMouseOver === "function"
|
|
96
|
-
|| typeof props.onMouseUp === "function"
|
|
97
|
-
|| typeof props.onMouseMove === "function"
|
|
98
|
-
|| typeof props.onMouseOut === "function"
|
|
99
|
-
|| typeof props.onMouseDrag === "function"
|
|
100
|
-
|| typeof props.onMouseDragEnd === "function"
|
|
101
|
-
|| typeof props.onMouseScroll === "function";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
93
|
export function cellBoundsForElement(
|
|
105
94
|
getElement: () => HTMLElement | null,
|
|
106
95
|
getProps: () => Record<string, unknown>,
|
|
@@ -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)}
|
|
@@ -11,6 +11,8 @@ interface ElectrobunHttpFetchResponse {
|
|
|
11
11
|
body: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const CLOUD_MARKET_HTTP_TIMEOUT_MS = 10_000;
|
|
15
|
+
|
|
14
16
|
function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
15
17
|
const normalized: Record<string, string> = {};
|
|
16
18
|
if (!headers) return normalized;
|
|
@@ -75,7 +77,11 @@ async function electrobunHttpFetch(url: string, init?: RequestInit): Promise<Res
|
|
|
75
77
|
return fetchResponse;
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
async function requestBackendHttpFetch(
|
|
80
|
+
async function requestBackendHttpFetch(
|
|
81
|
+
url: string,
|
|
82
|
+
init?: RequestInit,
|
|
83
|
+
timeoutMs?: number,
|
|
84
|
+
): Promise<ElectrobunHttpFetchResponse> {
|
|
79
85
|
return backendRequest<ElectrobunHttpFetchResponse>("http.fetch", {
|
|
80
86
|
url,
|
|
81
87
|
init: {
|
|
@@ -83,6 +89,7 @@ async function requestBackendHttpFetch(url: string, init?: RequestInit): Promise
|
|
|
83
89
|
headers: normalizeHeaders(init?.headers),
|
|
84
90
|
body: await serializeBody(init?.body),
|
|
85
91
|
redirect: init?.redirect,
|
|
92
|
+
timeoutMs,
|
|
86
93
|
},
|
|
87
94
|
});
|
|
88
95
|
}
|
|
@@ -100,7 +107,13 @@ function createResponseHeaders(headers: Record<string, string>, setCookie: strin
|
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
async function electrobunCloudApiFetch(url: string, init?: RequestInit): Promise<Response> {
|
|
103
|
-
|
|
110
|
+
if (init?.signal?.aborted) {
|
|
111
|
+
throw createAbortError();
|
|
112
|
+
}
|
|
113
|
+
const timeoutMs = new URL(url).pathname.startsWith("/market/")
|
|
114
|
+
? CLOUD_MARKET_HTTP_TIMEOUT_MS
|
|
115
|
+
: undefined;
|
|
116
|
+
const response = await withAbort(requestBackendHttpFetch(url, init, timeoutMs), init?.signal);
|
|
104
117
|
|
|
105
118
|
return {
|
|
106
119
|
ok: response.status >= 200 && response.status < 300,
|
|
@@ -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}
|
|
@@ -3,9 +3,11 @@ import type { NewsArticle, NewsQuery } from "../../../../news/types";
|
|
|
3
3
|
import type { DataProvider, QuoteSubscriptionTarget } from "../../../../types/data-provider";
|
|
4
4
|
import type { Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
5
|
import { backendRequest, getElectrobunBackendInitSnapshot, onCapabilityEvent } from "../backend-rpc";
|
|
6
|
+
import { createCapabilityInvoker } from "./capability-invoker";
|
|
6
7
|
|
|
7
8
|
const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
|
|
8
9
|
const NEWS_CAPABILITY_ID = "news.core";
|
|
10
|
+
const ASSET_DATA_REQUEST_TIMEOUT_MS = 115_000;
|
|
9
11
|
|
|
10
12
|
export type RemoteAssetDataClient = DataProvider & {
|
|
11
13
|
getNews(query: NewsQuery): Promise<NewsArticle[]>;
|
|
@@ -64,23 +66,12 @@ function hasRendererOperation(operations: Set<string> | null, operationId: strin
|
|
|
64
66
|
return operations === null || operations.has(operationId);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function createCapabilityInvoker() {
|
|
68
|
-
const inFlightRequests = new Map<string, Promise<unknown>>();
|
|
69
|
-
return function invoke<T>(capabilityId: string, operationId: string, payload: unknown): Promise<T> {
|
|
70
|
-
const requestPayload = { capabilityId, operationId, payload };
|
|
71
|
-
const key = JSON.stringify(requestPayload);
|
|
72
|
-
const inFlight = inFlightRequests.get(key);
|
|
73
|
-
if (inFlight) return inFlight as Promise<T>;
|
|
74
|
-
const promise = backendRequest<T>("capability.invoke", requestPayload).finally(() => {
|
|
75
|
-
inFlightRequests.delete(key);
|
|
76
|
-
});
|
|
77
|
-
inFlightRequests.set(key, promise);
|
|
78
|
-
return promise;
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
69
|
export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
83
|
-
const invoke = createCapabilityInvoker(
|
|
70
|
+
const invoke = createCapabilityInvoker({
|
|
71
|
+
request: backendRequest,
|
|
72
|
+
shouldApplyDeadline: (capabilityId) => capabilityId === ASSET_DATA_CAPABILITY_ID,
|
|
73
|
+
timeoutMs: ASSET_DATA_REQUEST_TIMEOUT_MS,
|
|
74
|
+
});
|
|
84
75
|
const assetDataOperations = getRendererOperationIds(ASSET_DATA_CAPABILITY_ID);
|
|
85
76
|
const newsOperations = getRendererOperationIds(NEWS_CAPABILITY_ID);
|
|
86
77
|
let nextSubscriptionId = 1;
|
|
@@ -93,7 +84,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
93
84
|
let quoteBackendFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
94
85
|
|
|
95
86
|
const quoteTargetKey = (target: QuoteSubscriptionTarget) =>
|
|
96
|
-
`${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? ""}`;
|
|
87
|
+
`${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}:${target.context?.instrument?.conId ?? target.context?.instrument?.localSymbol ?? target.context?.instrument?.symbol ?? ""}`;
|
|
97
88
|
|
|
98
89
|
const scheduleQuoteBackendSubscriptionFlush = () => {
|
|
99
90
|
if (quoteBackendFlushTimer) return;
|
|
@@ -156,7 +147,7 @@ export function createRemoteAssetDataClient(): RemoteAssetDataClient {
|
|
|
156
147
|
if (!hasRendererOperation(assetDataOperations, "subscribeQuotes")) return () => {};
|
|
157
148
|
const uniqueTargets = [...new Map(
|
|
158
149
|
targets.map((target) => [
|
|
159
|
-
|
|
150
|
+
quoteTargetKey(target),
|
|
160
151
|
target,
|
|
161
152
|
] as const),
|
|
162
153
|
).values()];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createCapabilityInvoker } from "./capability-invoker";
|
|
3
|
+
|
|
4
|
+
describe("createCapabilityInvoker", () => {
|
|
5
|
+
test("keeps timed-out calls deduped until the raw backend request settles", async () => {
|
|
6
|
+
let requestCount = 0;
|
|
7
|
+
const rawResolvers: Array<(value: unknown) => void> = [];
|
|
8
|
+
const invoke = createCapabilityInvoker({
|
|
9
|
+
request: <T>() => {
|
|
10
|
+
requestCount += 1;
|
|
11
|
+
return new Promise<T>((resolve) => {
|
|
12
|
+
rawResolvers.push((value) => resolve(value as T));
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
shouldApplyDeadline: (capabilityId) => capabilityId.startsWith("asset-data."),
|
|
16
|
+
timeoutMs: 10,
|
|
17
|
+
});
|
|
18
|
+
const payload = { ticker: "AAPL", exchange: "NASDAQ" };
|
|
19
|
+
|
|
20
|
+
const first = invoke("asset-data.asset-data-router", "getQuote", payload);
|
|
21
|
+
const duplicate = invoke("asset-data.asset-data-router", "getQuote", payload);
|
|
22
|
+
expect(duplicate).toBe(first);
|
|
23
|
+
expect(requestCount).toBe(1);
|
|
24
|
+
await expect(first).rejects.toThrow("Asset data request timed out after 10ms");
|
|
25
|
+
|
|
26
|
+
const retryWhileRawPending = invoke(
|
|
27
|
+
"asset-data.asset-data-router",
|
|
28
|
+
"getQuote",
|
|
29
|
+
payload,
|
|
30
|
+
);
|
|
31
|
+
expect(retryWhileRawPending).toBe(first);
|
|
32
|
+
await expect(retryWhileRawPending).rejects.toThrow(
|
|
33
|
+
"Asset data request timed out after 10ms",
|
|
34
|
+
);
|
|
35
|
+
expect(requestCount).toBe(1);
|
|
36
|
+
|
|
37
|
+
rawResolvers[0]?.({ price: 100 });
|
|
38
|
+
await Promise.resolve();
|
|
39
|
+
await Promise.resolve();
|
|
40
|
+
|
|
41
|
+
const retryAfterRawSettles = invoke(
|
|
42
|
+
"asset-data.asset-data-router",
|
|
43
|
+
"getQuote",
|
|
44
|
+
payload,
|
|
45
|
+
);
|
|
46
|
+
expect(requestCount).toBe(2);
|
|
47
|
+
rawResolvers[1]?.({ price: 101 });
|
|
48
|
+
await expect(retryAfterRawSettles).resolves.toEqual({ price: 101 });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { withDeadline } from "../../../../utils/async-deadline";
|
|
2
|
+
|
|
3
|
+
type CapabilityRequest = <T>(method: string, payload: unknown) => Promise<T>;
|
|
4
|
+
|
|
5
|
+
export function createCapabilityInvoker(options: {
|
|
6
|
+
request: CapabilityRequest;
|
|
7
|
+
shouldApplyDeadline(capabilityId: string): boolean;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
}) {
|
|
10
|
+
const inFlightRequests = new Map<string, Promise<unknown>>();
|
|
11
|
+
|
|
12
|
+
return function invoke<T>(capabilityId: string, operationId: string, payload: unknown): Promise<T> {
|
|
13
|
+
const requestPayload = { capabilityId, operationId, payload };
|
|
14
|
+
const key = JSON.stringify(requestPayload);
|
|
15
|
+
const inFlight = inFlightRequests.get(key);
|
|
16
|
+
if (inFlight) return inFlight as Promise<T>;
|
|
17
|
+
|
|
18
|
+
const request = options.request<T>("capability.invoke", requestPayload);
|
|
19
|
+
const boundedRequest = options.shouldApplyDeadline(capabilityId)
|
|
20
|
+
? withDeadline(
|
|
21
|
+
request,
|
|
22
|
+
options.timeoutMs,
|
|
23
|
+
`Asset data request timed out after ${options.timeoutMs}ms: ${operationId}`,
|
|
24
|
+
)
|
|
25
|
+
: request;
|
|
26
|
+
inFlightRequests.set(key, boundedRequest);
|
|
27
|
+
void request.finally(() => {
|
|
28
|
+
if (inFlightRequests.get(key) === boundedRequest) {
|
|
29
|
+
inFlightRequests.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}).catch(() => {
|
|
32
|
+
// The returned bounded request owns error delivery to the caller.
|
|
33
|
+
});
|
|
34
|
+
return boundedRequest;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -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,
|
|
@@ -22,6 +22,7 @@ export function toKeyEventLike(event: {
|
|
|
22
22
|
shift?: boolean;
|
|
23
23
|
alt?: boolean;
|
|
24
24
|
meta?: boolean;
|
|
25
|
+
option?: boolean;
|
|
25
26
|
preventDefault?: () => void;
|
|
26
27
|
stopPropagation?: () => void;
|
|
27
28
|
defaultPrevented?: boolean;
|
|
@@ -37,9 +38,12 @@ export function toKeyEventLike(event: {
|
|
|
37
38
|
sequence: event.sequence,
|
|
38
39
|
ctrl: event.ctrl ?? false,
|
|
39
40
|
shift: event.shift ?? false,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// OpenTUI follows terminal naming: `meta` is Alt/Option, while the
|
|
42
|
+
// kitty keyboard protocol exposes the Command/Windows modifier as
|
|
43
|
+
// `super`. Keep the shared input model aligned with browser semantics.
|
|
44
|
+
alt: event.alt === true || event.meta === true || event.option === true,
|
|
45
|
+
meta: event.super === true,
|
|
46
|
+
super: event.super === true,
|
|
43
47
|
get defaultPrevented() {
|
|
44
48
|
return event.defaultPrevented ?? false;
|
|
45
49
|
},
|
|
@@ -101,6 +105,9 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
101
105
|
await proc.exited;
|
|
102
106
|
},
|
|
103
107
|
async copyText(text) {
|
|
108
|
+
if (!text) return;
|
|
109
|
+
if (renderer.copyToClipboardOSC52(text)) return;
|
|
110
|
+
if (process.platform !== "darwin") return;
|
|
104
111
|
const proc = Bun.spawn(["pbcopy"], {
|
|
105
112
|
stdin: "pipe",
|
|
106
113
|
stdout: "ignore",
|
|
@@ -111,6 +118,7 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
|
|
|
111
118
|
await proc.exited;
|
|
112
119
|
},
|
|
113
120
|
async readText() {
|
|
121
|
+
if (process.platform !== "darwin") return "";
|
|
114
122
|
const proc = Bun.spawn(["pbpaste"], {
|
|
115
123
|
stdout: "pipe",
|
|
116
124
|
stderr: "ignore",
|