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.
Files changed (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /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
- 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}
@@ -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, 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)}
@@ -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(url: string, init?: RequestInit): Promise<ElectrobunHttpFetchResponse> {
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
- const response = await requestBackendHttpFetch(url, init);
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
- `${target.symbol}:${target.exchange ?? ""}:${target.context?.brokerId ?? ""}:${target.context?.brokerInstanceId ?? ""}`,
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
- alt: event.alt ?? false,
41
- meta: event.meta ?? event.super ?? false,
42
- super: event.super ?? event.meta ?? false,
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",