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
package/src/updater.ts CHANGED
@@ -91,10 +91,6 @@ function normalizePath(value: string): string {
91
91
  return value.replace(/\\/g, "/").toLowerCase();
92
92
  }
93
93
 
94
- function tryRealpath(value: string): string {
95
- return value;
96
- }
97
-
98
94
  function basename(value: string): string {
99
95
  const normalized = value.replace(/\\/g, "/");
100
96
  return normalized.slice(normalized.lastIndexOf("/") + 1);
@@ -124,7 +120,7 @@ const runtimeExecutables = new Set([
124
120
  ]);
125
121
 
126
122
  function resolveEntrypointPath(argv = process.argv): string {
127
- return tryRealpath(argv[1] ?? "");
123
+ return argv[1] ?? "";
128
124
  }
129
125
 
130
126
  function isSourceEntrypoint(entrypoint: string): boolean {
@@ -138,7 +134,7 @@ function isMacAppBundleExecutable(execPath: string): boolean {
138
134
  }
139
135
 
140
136
  function isBundledDesktopTuiRuntime(execPath: string, argv: string[]): boolean {
141
- const normalizedExecPath = normalizePath(tryRealpath(execPath));
137
+ const normalizedExecPath = normalizePath(execPath);
142
138
  const entrypoint = normalizePath(resolveEntrypointPath(argv));
143
139
  if (!entrypoint.endsWith("/resources/gloomberb-tui/tui-entry.js")) return false;
144
140
  return normalizedExecPath.endsWith("/contents/macos/bun")
@@ -150,7 +146,7 @@ export function resolveSelfUpdateTargetPath(
150
146
  execPath = getRuntimeProcess()?.execPath ?? "",
151
147
  argv = getRuntimeProcess()?.argv ?? [],
152
148
  ): string | null {
153
- const resolvedExecPath = tryRealpath(execPath);
149
+ const resolvedExecPath = execPath;
154
150
  const normalizedExecPath = normalizePath(resolvedExecPath);
155
151
  if (isMacAppBundleExecutable(resolvedExecPath)) return null;
156
152
 
@@ -171,7 +167,7 @@ export function detectUpdateAction(
171
167
  if (isMacAppBundleExecutable(execPath)) return null;
172
168
  if (isBundledDesktopTuiRuntime(execPath, argv)) return null;
173
169
 
174
- const normalizedExecPath = normalizePath(tryRealpath(execPath));
170
+ const normalizedExecPath = normalizePath(execPath);
175
171
  const execBase = basename(normalizedExecPath);
176
172
  const entrypoint = normalizePath(resolveEntrypointPath(argv));
177
173
  if (execBase === "gloomberb.exe") return null;
@@ -344,7 +340,6 @@ export async function performUpdate(
344
340
  const fsModulePath = "fs";
345
341
  const zlibModulePath = "zlib";
346
342
  const {
347
- writeFileSync,
348
343
  renameSync,
349
344
  unlinkSync,
350
345
  chmodSync,
@@ -377,12 +372,16 @@ export async function performUpdate(
377
372
  }
378
373
  }
379
374
 
380
- // Write to temp file
381
- const blob = new Blob(chunks);
382
- const buffer = await blob.arrayBuffer();
383
- const downloaded = Buffer.from(buffer);
384
- const nextBinary = release.compressed ? gunzipSync(downloaded) : downloaded;
385
- writeFileSync(updatePath, nextBinary);
375
+ const downloaded = new Uint8Array(received);
376
+ let offset = 0;
377
+ for (const chunk of chunks) {
378
+ downloaded.set(chunk, offset);
379
+ offset += chunk.byteLength;
380
+ }
381
+ const nextBinary = release.compressed
382
+ ? new Uint8Array(gunzipSync(downloaded))
383
+ : downloaded;
384
+ await Bun.write(updatePath, nextBinary);
386
385
  chmodSync(updatePath, 0o755);
387
386
 
388
387
  // Swap binaries
@@ -391,7 +390,12 @@ export async function performUpdate(
391
390
  unlinkSync(oldPath);
392
391
  } catch {}
393
392
  renameSync(execPath, oldPath);
394
- renameSync(updatePath, execPath);
393
+ try {
394
+ renameSync(updatePath, execPath);
395
+ } catch (error) {
396
+ renameSync(oldPath, execPath);
397
+ throw error;
398
+ }
395
399
  try {
396
400
  unlinkSync(oldPath);
397
401
  } catch {}
@@ -0,0 +1,26 @@
1
+ export class OperationTimeoutError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "TimeoutError";
5
+ }
6
+ }
7
+
8
+ export function withDeadline<T>(
9
+ promise: Promise<T>,
10
+ timeoutMs: number,
11
+ message: string,
12
+ onTimeout?: (error: OperationTimeoutError) => void,
13
+ ): Promise<T> {
14
+ let timer: ReturnType<typeof setTimeout> | null = null;
15
+ const timeout = new Promise<never>((_resolve, reject) => {
16
+ timer = setTimeout(() => {
17
+ const error = new OperationTimeoutError(message);
18
+ reject(error);
19
+ onTimeout?.(error);
20
+ }, timeoutMs);
21
+ });
22
+
23
+ return Promise.race([promise, timeout]).finally(() => {
24
+ if (timer) clearTimeout(timer);
25
+ });
26
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { CANONICAL_EXCHANGE_ALIASES, EXCHANGE_TIME_ZONES } from "./exchanges";
2
+ import { canonicalExchange, CANONICAL_EXCHANGE_ALIASES, EXCHANGE_TIME_ZONES, resolveExchangeTimeZone } from "./exchanges";
3
3
 
4
4
  describe("exchange metadata", () => {
5
5
  test("has a valid timezone for every canonical exchange", () => {
@@ -13,4 +13,14 @@ describe("exchange metadata", () => {
13
13
  })).not.toThrow();
14
14
  }
15
15
  });
16
+
17
+ test("normalizes broker venue aliases used by portfolio rows", () => {
18
+ expect(canonicalExchange("LSEETF")).toBe("LSE");
19
+ expect(canonicalExchange("TSE")).toBe("TSX");
20
+ expect(canonicalExchange("AEB")).toBe("AMS");
21
+ expect(canonicalExchange("EURONEXT")).toBe("AMS");
22
+ expect(resolveExchangeTimeZone("LSEETF")).toBe("Europe/London");
23
+ expect(resolveExchangeTimeZone("TSE")).toBe("America/Toronto");
24
+ expect(resolveExchangeTimeZone("AEB")).toBe("Europe/Amsterdam");
25
+ });
16
26
  });
@@ -17,7 +17,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
17
17
  ARCX: "ARCA",
18
18
  BATS: "BATS",
19
19
  TYO: "JPX",
20
- TSE: "JPX",
20
+ TSE: "TSX",
21
21
  TSEJ: "JPX",
22
22
  JPX: "JPX",
23
23
  HKG: "HKEX",
@@ -26,6 +26,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
26
26
  XHKG: "HKEX",
27
27
  LON: "LSE",
28
28
  LSE: "LSE",
29
+ LSEETF: "LSE",
29
30
  XLON: "LSE",
30
31
  TOR: "TSX",
31
32
  TSX: "TSX",
@@ -56,8 +57,11 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
56
57
  EPA: "EPA",
57
58
  XPAR: "EPA",
58
59
  PAR: "EPA",
60
+ SBF: "EPA",
59
61
  AMS: "AMS",
60
62
  XAMS: "AMS",
63
+ AEB: "AMS",
64
+ EURONEXT: "AMS",
61
65
  BRU: "BRU",
62
66
  XBRU: "BRU",
63
67
  LIS: "LIS",
@@ -65,7 +65,7 @@ export function copyActiveSelection(
65
65
  export function isCopyShortcut(
66
66
  event: Pick<KeyEvent, "name" | "super" | "meta" | "ctrl" | "shift">,
67
67
  ): boolean {
68
- const keyName = event.name.toLowerCase();
68
+ const keyName = event.name?.toLowerCase() ?? "";
69
69
  return keyName === "c" && (
70
70
  event.super === true
71
71
  || event.meta === true
@@ -76,7 +76,7 @@ export function isCopyShortcut(
76
76
  export function isPasteShortcut(
77
77
  event: Pick<KeyEvent, "name" | "super" | "meta" | "ctrl" | "shift">,
78
78
  ): boolean {
79
- const keyName = event.name.toLowerCase();
79
+ const keyName = event.name?.toLowerCase() ?? "";
80
80
  return keyName === "v" && (
81
81
  event.super === true
82
82
  || event.meta === true
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.9.7";
1
+ export const VERSION = "0.9.9";
@@ -1,39 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { act } from "react";
3
- import { testRender } from "../../../renderers/opentui/test-utils";
4
- import { PaneHeader } from "./header";
5
-
6
- let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
7
-
8
- afterEach(() => {
9
- if (testSetup) {
10
- testSetup.renderer.destroy();
11
- testSetup = undefined;
12
- }
13
- });
14
-
15
- describe("PaneHeader", () => {
16
- test("fires the terminal action handler from the visible action text", async () => {
17
- let actions = 0;
18
- testSetup = await testRender(
19
- <PaneHeader
20
- title="Portfolio"
21
- width={40}
22
- focused
23
- showActions
24
- onActionMouseDown={() => { actions += 1; }}
25
- />,
26
- { width: 40, height: 3 },
27
- );
28
-
29
- await testSetup.renderOnce();
30
- const actionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("...");
31
- expect(actionCol).toBeGreaterThanOrEqual(0);
32
-
33
- await act(async () => {
34
- await testSetup!.mockMouse.click(actionCol! + 1, 0);
35
- });
36
-
37
- expect(actions).toBe(1);
38
- });
39
- });
@@ -1,28 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ChatChannel } from "../../../api-client";
3
- import {
4
- channelPrefix,
5
- formatChannelLabel,
6
- formatChatPaneTitle,
7
- } from "./channels";
8
-
9
- describe("chat channel labels", () => {
10
- test("formats direct-message labels and titles with one mention prefix", () => {
11
- const channel: ChatChannel = {
12
- id: "dm:c7f89ce26f9da83d6be08a4838738074",
13
- name: "@risto",
14
- kind: "direct",
15
- created_at: "2026-05-27T10:30:03.712Z",
16
- dmUser: { id: "u2", username: "@risto", displayName: "Risto" },
17
- };
18
-
19
- expect(formatChannelLabel(channel, channel.id)).toBe("@risto");
20
- expect(channelPrefix(channel, true)).toBe(" ");
21
- expect(formatChatPaneTitle(channel, channel.id)).toBe("@risto");
22
- expect(formatChatPaneTitle(undefined, channel.id)).toBe("DM");
23
- });
24
-
25
- test("uses a neutral group title before private channel metadata loads", () => {
26
- expect(formatChatPaneTitle(undefined, "grp:93f14c6b9827c30f")).toBe("Group");
27
- });
28
- });
@@ -1,4 +0,0 @@
1
- import { ChatContent } from "./content";
2
- import { ChatStatusWidget } from "./status-widget";
3
-
4
- export { ChatContent, ChatStatusWidget };
@@ -1 +0,0 @@
1
- export { registerTwitterFeedFeature } from "./registration";
@@ -1,12 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { resolveEarningsMonitorSymbols } from "./model";
3
-
4
- describe("earnings monitor scope", () => {
5
- test("uses explicit EM tickers before collection or app fallback tickers", () => {
6
- expect(resolveEarningsMonitorSymbols(["AAPL", "MSFT"], ["NVDA"])).toEqual(["AAPL", "MSFT"]);
7
- });
8
-
9
- test("falls back to existing earnings monitor ticker universe when EM has no tickers", () => {
10
- expect(resolveEarningsMonitorSymbols([], ["NVDA", "AMD"])).toEqual(["NVDA", "AMD"]);
11
- });
12
- });
@@ -1,242 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { act } from "react";
3
- import { testRender } from "../../../renderers/opentui/test-utils";
4
- import { createTestPluginRuntime } from "../../../test-support/plugin-runtime";
5
- import { setSharedRegistryForTests } from "../../registry";
6
- import { PluginRenderProvider } from "../../runtime";
7
- import { HelpPane } from ".";
8
-
9
- let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
10
-
11
- afterEach(() => {
12
- if (testSetup) {
13
- testSetup.renderer.destroy();
14
- testSetup = undefined;
15
- }
16
- setSharedRegistryForTests(undefined);
17
- });
18
-
19
- async function renderHelpPane(
20
- runtime = createTestPluginRuntime(),
21
- size = { width: 88, height: 36 },
22
- ) {
23
- testSetup = await testRender(
24
- <PluginRenderProvider pluginId="help" runtime={runtime}>
25
- <HelpPane
26
- paneId="help:main"
27
- paneType="help"
28
- focused
29
- width={size.width}
30
- height={size.height}
31
- close={() => {}}
32
- />
33
- </PluginRenderProvider>,
34
- size,
35
- );
36
-
37
- await testSetup.renderOnce();
38
- await testSetup.renderOnce();
39
- return testSetup;
40
- }
41
-
42
- async function moveHelpTabRight(count = 1) {
43
- await act(async () => {
44
- for (let index = 0; index < count; index += 1) {
45
- testSetup!.mockInput.pressArrow("right");
46
- }
47
- await testSetup!.renderOnce();
48
- await testSetup!.renderOnce();
49
- });
50
- }
51
-
52
- async function resetRenderedHelpPane(
53
- runtime = createTestPluginRuntime(),
54
- size = { width: 88, height: 36 },
55
- ) {
56
- if (testSetup) {
57
- act(() => {
58
- testSetup!.renderer.destroy();
59
- });
60
- testSetup = undefined;
61
- }
62
- return renderHelpPane(runtime, size);
63
- }
64
-
65
- function withoutScrollbar(line: string) {
66
- const scrollThumb = String.fromCharCode(0x2588);
67
- return (line.endsWith(scrollThumb) ? line.slice(0, -1) : line).trimEnd();
68
- }
69
-
70
- describe("HelpPane", () => {
71
- test("starts on basics and puts section spacing before headings instead of after them", async () => {
72
- await renderHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
73
-
74
- const lines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
75
- const commandBarRow = lines.findIndex((line) => line.includes("Command Bar"));
76
- const layoutRow = lines.findIndex((line) => line.includes("Layout Basics"));
77
-
78
- expect(lines[0]).toContain("Basics");
79
- expect(testSetup!.captureCharFrame()).toContain("Basics");
80
- expect(testSetup!.captureCharFrame()).toContain("Functions");
81
- expect(testSetup!.captureCharFrame()).toContain("Issues");
82
- expect(testSetup!.captureCharFrame()).not.toContain("Issues/Debug");
83
- expect(testSetup!.captureCharFrame()).not.toContain("Window Templates");
84
- expect(testSetup!.captureCharFrame()).not.toContain("Search for a ticker or open the best matching security.");
85
- expect(commandBarRow).toBeGreaterThan(0);
86
- expect(layoutRow).toBeGreaterThan(commandBarRow);
87
- expect(lines[commandBarRow - 1]?.trim()).toBe("");
88
- expect(lines[commandBarRow + 1]).toContain("Open command mode");
89
- expect(testSetup!.captureCharFrame()).toContain("Open ticker search directly.");
90
- expect(testSetup!.captureCharFrame()).toContain("DES");
91
- expect(testSetup!.captureCharFrame()).toContain("Clear command text.");
92
- expect(testSetup!.captureCharFrame()).toContain("Delete the previous word");
93
- expect(testSetup!.captureCharFrame()).toContain("Submit multiline command forms.");
94
- expect(testSetup!.captureCharFrame()).toContain("close all floating panes");
95
- expect(lines[layoutRow - 1]?.trim()).toBe("");
96
- expect(lines[layoutRow + 1]?.trim()).not.toBe("");
97
- });
98
-
99
- test("separates functions from keyboard shortcuts", async () => {
100
- setSharedRegistryForTests({
101
- commands: new Map([[
102
- "set-alert",
103
- {
104
- id: "set-alert",
105
- label: "Add Alert",
106
- description: "Create a price alert",
107
- keywords: [],
108
- category: "data",
109
- shortcut: "SA",
110
- shortcutArg: { placeholder: "symbol condition price" },
111
- execute: () => {},
112
- },
113
- ]]),
114
- paneTemplates: new Map([[
115
- "quote",
116
- {
117
- id: "quote",
118
- paneId: "ticker-detail",
119
- label: "Quote Monitor",
120
- description: "Open ticker detail",
121
- shortcut: { prefix: "QQ", argPlaceholder: "tickers", argKind: "ticker-list" },
122
- },
123
- ]]),
124
- shortcuts: new Map([[
125
- "sync-data",
126
- {
127
- id: "sync-data",
128
- key: "x",
129
- shift: true,
130
- description: "Sync data",
131
- execute: () => {},
132
- },
133
- ]]),
134
- allPlugins: new Map([
135
- ["alerts", { name: "Alerts" }],
136
- ["ticker-research", { name: "Ticker Research" }],
137
- ["gloomberb-cloud", { name: "Gloom Cloud" }],
138
- ]),
139
- getConfigFn: () => ({ disabledPlugins: [] }),
140
- getCommandPluginId: () => "alerts",
141
- getPaneTemplatePluginId: () => "ticker-research",
142
- getShortcutPluginId: () => "gloomberb-cloud",
143
- } as any);
144
-
145
- await renderHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
146
- await moveHelpTabRight();
147
-
148
- const functionsFrame = testSetup!.captureCharFrame();
149
- expect(functionsFrame).toContain("Manage Plugins");
150
- expect(functionsFrame).toMatch(/AW\s+<ticker>/);
151
- expect(functionsFrame).toMatch(/SA\s+<symbol condition price>/);
152
- expect(functionsFrame).toMatch(/QQ\s+<tickers>/);
153
- expect(functionsFrame).toContain("Alerts");
154
- expect(functionsFrame).toContain("Add Alert");
155
- expect(functionsFrame).toContain("Shift+X");
156
- expect(functionsFrame).toContain("Gloom Cloud");
157
- expect(functionsFrame).toContain("Sync data");
158
- expect(functionsFrame).toContain("Ticker Research");
159
- expect(functionsFrame).toContain("Quote Monitor");
160
- expect(functionsFrame).not.toContain("Add Alert (Alerts)");
161
- expect(functionsFrame).not.toContain("Sync data (Gloom Cloud)");
162
- expect(functionsFrame).not.toContain("Quote Monitor (Ticker Research)");
163
- expect(functionsFrame).not.toContain("Ctrl+W");
164
-
165
- await resetRenderedHelpPane(createTestPluginRuntime(), { width: 88, height: 80 });
166
- await moveHelpTabRight(2);
167
-
168
- const shortcutsFrame = testSetup!.captureCharFrame();
169
- expect(shortcutsFrame).toContain("Ctrl+W");
170
- expect(shortcutsFrame).toContain("Ctrl+Alt+W");
171
- expect(shortcutsFrame).toContain("Ctrl+Shift+M");
172
- expect(shortcutsFrame).toContain("Ctrl+Shift+R");
173
- expect(shortcutsFrame).toContain("Ctrl+,");
174
- expect(shortcutsFrame).toContain("Navigation");
175
- expect(shortcutsFrame).toContain("Up/Down");
176
- expect(shortcutsFrame).toContain("j/k");
177
- expect(shortcutsFrame).toContain("Left/Right");
178
- expect(shortcutsFrame).toContain("h/l");
179
- expect(shortcutsFrame).toContain("PageUp/PageDown");
180
- expect(shortcutsFrame).toContain("Window Mode");
181
- expect(shortcutsFrame).toContain("h/j/k/l");
182
- expect(shortcutsFrame).toContain("Close all floating panes.");
183
- expect(shortcutsFrame).not.toContain("AW");
184
- expect(shortcutsFrame).not.toContain("Ctrl+Shift+O");
185
- expect(shortcutsFrame).not.toContain("Pop the focused pane");
186
- expect(shortcutsFrame).not.toContain("Cmd+W");
187
- expect(shortcutsFrame).not.toContain("Cmd+,");
188
- expect(shortcutsFrame).not.toContain("Cmd+K");
189
- expect(shortcutsFrame).not.toContain("Cmd/Ctrl");
190
- expect(shortcutsFrame).not.toContain("CmdOrCtrl");
191
- expect(shortcutsFrame).not.toContain("Open ticker actions for the focused ticker.");
192
- });
193
-
194
- test("keeps shortcuts and descriptions on the same row when narrow", async () => {
195
- await renderHelpPane(createTestPluginRuntime(), { width: 70, height: 80 });
196
-
197
- const lines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
198
- const closeCommandRow = lines.findIndex((line) => line.includes("Close the command bar."));
199
-
200
- expect(closeCommandRow).toBeGreaterThanOrEqual(0);
201
- expect(lines[closeCommandRow]).toContain("Esc");
202
- expect(lines[closeCommandRow]).toContain("`");
203
-
204
- await moveHelpTabRight();
205
-
206
- const functionLines = testSetup!.captureCharFrame().split("\n").map(withoutScrollbar);
207
- const securityDetailsRow = functionLines.findIndex((line) => line.includes("Open security details"));
208
- expect(securityDetailsRow).toBeGreaterThanOrEqual(0);
209
- expect(functionLines[securityDetailsRow]).toContain("DES");
210
- expect(functionLines[securityDetailsRow]).toContain("<ticker>");
211
- });
212
-
213
- test("opens the debug log from the mouse action", async () => {
214
- const calls: string[] = [];
215
- const runtime = createTestPluginRuntime({
216
- openCommandBar: (query?: string) => calls.push(`command:${query ?? ""}`),
217
- showPane: (paneId: string) => calls.push(`pane:${paneId}`),
218
- });
219
-
220
- await renderHelpPane(runtime);
221
- await moveHelpTabRight(3);
222
-
223
- const frame = testSetup!.captureCharFrame();
224
- expect(frame).toContain("GitHub Issues");
225
- expect(frame).not.toContain("Issues/Debug");
226
- expect(frame).not.toContain("plugin,\n");
227
-
228
- const lines = frame.split("\n");
229
- const row = lines.findIndex((line) => line.includes("Open Debug Log"));
230
- const col = lines[row]?.indexOf("Open Debug Log") ?? -1;
231
-
232
- expect(row).toBeGreaterThanOrEqual(0);
233
- expect(col).toBeGreaterThanOrEqual(0);
234
-
235
- await act(async () => {
236
- await testSetup!.mockMouse.click(col + 1, row);
237
- await testSetup!.renderOnce();
238
- });
239
-
240
- expect(calls).toEqual(["pane:debug"]);
241
- });
242
- });
@@ -1,10 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { resolveArticleImageSize } from "./article-detail";
3
-
4
- describe("Substack article image sizing", () => {
5
- test("uses extra detail pane room without shrinking compact panes", () => {
6
- expect(resolveArticleImageSize(40)).toEqual({ width: 40, height: 12 });
7
- expect(resolveArticleImageSize(86)).toEqual({ width: 86, height: 15 });
8
- expect(resolveArticleImageSize(220)).toEqual({ width: 152, height: 26 });
9
- });
10
- });
@@ -1,21 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { buildApplicationMenu, buildDesktopApplicationMenu } from "./index";
3
-
4
- describe("desktop application menu", () => {
5
- test("hides the application menu on Windows to preserve vertical space", () => {
6
- expect(buildDesktopApplicationMenu("win32")).toEqual([]);
7
- });
8
-
9
- test("keeps the native application menu on macOS", () => {
10
- expect(buildDesktopApplicationMenu("darwin")).toEqual(buildApplicationMenu());
11
- });
12
-
13
- test("routes macOS quit through an explicit command item", () => {
14
- const appMenu = buildApplicationMenu()[0]?.submenu;
15
- expect(appMenu?.at(-1)).toMatchObject({
16
- label: "Quit Gloomberb",
17
- accelerator: "CmdOrCtrl+Q",
18
- data: { type: "quit" },
19
- });
20
- });
21
- });
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { detachedRpcKey, focusWindowForRpcKey, MAIN_WINDOW_RPC_KEY, type FocusableElectrobunWindow } from "./focus";
3
-
4
- describe("Electrobun window focus routing", () => {
5
- test("uses activate for the main window", () => {
6
- const calls: string[] = [];
7
- const mainWindow: FocusableElectrobunWindow = {
8
- activate: () => calls.push("activate"),
9
- focus: () => calls.push("focus"),
10
- };
11
-
12
- expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
13
- expect(calls).toEqual(["activate"]);
14
- });
15
-
16
- test("uses activate for detached windows", () => {
17
- const calls: string[] = [];
18
- const detachedWindows = new Map<string, FocusableElectrobunWindow>([
19
- ["pane:1", { activate: () => calls.push("detached") }],
20
- ]);
21
-
22
- expect(focusWindowForRpcKey(detachedRpcKey("pane:1"), null, detachedWindows)).toBe(true);
23
- expect(calls).toEqual(["detached"]);
24
- });
25
-
26
- test("falls back to focus for legacy window objects", () => {
27
- const calls: string[] = [];
28
- const mainWindow: FocusableElectrobunWindow = {
29
- focus: () => calls.push("focus"),
30
- };
31
-
32
- expect(focusWindowForRpcKey(MAIN_WINDOW_RPC_KEY, mainWindow, new Map())).toBe(true);
33
- expect(calls).toEqual(["focus"]);
34
- });
35
- });
@@ -1,37 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { hasDirectMouseHandler, mouseHandlers } from "./mouse";
3
-
4
- function fakeMouseEvent() {
5
- return {
6
- clientX: 16,
7
- clientY: 36,
8
- button: 0,
9
- detail: 0,
10
- timeStamp: 1,
11
- shiftKey: false,
12
- altKey: false,
13
- ctrlKey: false,
14
- metaKey: false,
15
- preventDefault() {},
16
- stopPropagation() {},
17
- };
18
- }
19
-
20
- describe("desktop web mouse host", () => {
21
- test("forwards mouse over handlers", () => {
22
- let cellX = -1;
23
- let cellY = -1;
24
- const handlers = mouseHandlers({
25
- onMouseOver: (event: { x: number; y: number }) => {
26
- cellX = event.x;
27
- cellY = event.y;
28
- },
29
- });
30
-
31
- handlers.onMouseOver?.(fakeMouseEvent() as never);
32
-
33
- expect(cellX).toBe(2);
34
- expect(cellY).toBe(2);
35
- expect(hasDirectMouseHandler({ onMouseOver: () => {} })).toBe(true);
36
- });
37
- });
@@ -1,22 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { renderAsciiText } from "../../../../ui/ascii-font";
3
- import { webAsciiTextLines, webAsciiTextWordmarkVariant } from "./ascii-text";
4
-
5
- describe("desktop web ASCII text", () => {
6
- test("keeps the legacy Gloomberb wordmark on macOS", () => {
7
- expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "darwin")).toBe("legacy");
8
- expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "MacIntel")).toBe("legacy");
9
- expect(webAsciiTextLines("Gloomberb", "wordmark", "darwin")).toEqual(
10
- renderAsciiText("Gloomberb", "wordmark"),
11
- );
12
- });
13
-
14
- test("uses the web-safe Gloomberb wordmark off macOS", () => {
15
- const lines = webAsciiTextLines("Gloomberb", "wordmark", "win32");
16
-
17
- expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "win32")).toBe("compat");
18
- expect(lines).toHaveLength(5);
19
- expect(lines[0]).toContain("____");
20
- expect(lines).not.toEqual(renderAsciiText("Gloomberb", "wordmark"));
21
- });
22
- });