gloomberb 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +158 -57
  4. package/src/capabilities/factories.ts +3 -0
  5. package/src/cli/registry.test.ts +0 -1
  6. package/src/components/chart/chart-controller.ts +0 -1
  7. package/src/components/chart/chart-data.ts +1 -31
  8. package/src/components/chart/chart-renderer.ts +94 -12
  9. package/src/components/chart/chart-resolution.test.ts +0 -36
  10. package/src/components/chart/chart-resolution.ts +0 -27
  11. package/src/components/chart/chart.test.ts +60 -45
  12. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  13. package/src/components/chart/comparison-chart-data.ts +1 -4
  14. package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
  15. package/src/components/chart/comparison-chart-renderer.ts +13 -6
  16. package/src/components/chart/comparison-stock-chart.tsx +63 -39
  17. package/src/components/chart/price-axis-labels.tsx +114 -0
  18. package/src/components/chart/static-chart-surface.tsx +30 -4
  19. package/src/components/chart/stock-chart.test.ts +0 -16
  20. package/src/components/chart/stock-chart.tsx +69 -41
  21. package/src/components/chart/time-axis-label.tsx +120 -0
  22. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
  23. package/src/components/command-bar/command-bar.test.tsx +182 -36
  24. package/src/components/command-bar/command-bar.tsx +943 -1016
  25. package/src/components/command-bar/command-registry.ts +0 -10
  26. package/src/components/command-bar/confirm-body.tsx +51 -0
  27. package/src/components/command-bar/helpers.ts +2 -5
  28. package/src/components/command-bar/list-model.ts +76 -0
  29. package/src/components/command-bar/theme-picker.tsx +254 -0
  30. package/src/components/command-bar/view-model.test.ts +4 -4
  31. package/src/components/command-bar/view-model.ts +1 -20
  32. package/src/components/command-bar/workflow-ops.ts +4 -0
  33. package/src/components/command-bar/workflow-types.ts +1 -2
  34. package/src/components/data-table-stack-view.test.tsx +39 -4
  35. package/src/components/data-table-stack-view.tsx +11 -104
  36. package/src/components/data-table-view.tsx +43 -79
  37. package/src/components/feed-data-table-stack-view.tsx +10 -17
  38. package/src/components/feed-stack-controller.ts +39 -0
  39. package/src/components/index.ts +5 -1
  40. package/src/components/layout/detached-pane-shell.tsx +29 -10
  41. package/src/components/layout/floating-pane.tsx +35 -4
  42. package/src/components/layout/header.tsx +12 -7
  43. package/src/components/layout/pane-content.test.tsx +64 -0
  44. package/src/components/layout/pane-content.tsx +2 -0
  45. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  46. package/src/components/layout/pane-cycle-order.ts +32 -0
  47. package/src/components/layout/pane-footer.tsx +11 -4
  48. package/src/components/layout/pane-header.tsx +5 -5
  49. package/src/components/layout/pane-sizing.ts +9 -2
  50. package/src/components/layout/pane-title.ts +4 -1
  51. package/src/components/layout/pane.tsx +11 -5
  52. package/src/components/layout/shell.test.tsx +264 -22
  53. package/src/components/layout/shell.tsx +46 -8
  54. package/src/components/layout/status-bar.tsx +2 -0
  55. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  56. package/src/components/pane-settings-dialog.tsx +12 -4
  57. package/src/components/pane-template-wizard.test.tsx +3 -4
  58. package/src/components/speedometer-gauge.test.tsx +45 -0
  59. package/src/components/speedometer-gauge.tsx +562 -0
  60. package/src/components/table-view-shared.tsx +218 -0
  61. package/src/components/ticker-list-table-view.tsx +57 -73
  62. package/src/components/ticker-list-table.test.tsx +82 -1
  63. package/src/components/ticker-list-table.tsx +22 -9
  64. package/src/components/ui/confirm-dialog.tsx +66 -0
  65. package/src/components/ui/data-table.tsx +30 -19
  66. package/src/components/ui/frame.tsx +2 -0
  67. package/src/components/ui/index.ts +0 -1
  68. package/src/components/ui/page-stack-view.tsx +39 -10
  69. package/src/components/ui/table-layout.test.ts +42 -0
  70. package/src/components/ui/table-layout.ts +49 -4
  71. package/src/components/ui/tabs.tsx +34 -1
  72. package/src/components/ui/ui.test.tsx +168 -21
  73. package/src/core/state/app-state.ts +10 -17
  74. package/src/market-data/coordinator.test.ts +76 -1
  75. package/src/market-data/coordinator.ts +218 -49
  76. package/src/news/aggregator.test.ts +49 -1
  77. package/src/news/aggregator.ts +48 -3
  78. package/src/news/hooks.ts +6 -2
  79. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  80. package/src/plugins/builtin/ai/providers.ts +0 -17
  81. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  82. package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
  83. package/src/plugins/builtin/analytics/index.tsx +1 -18
  84. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  85. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  86. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  87. package/src/plugins/builtin/chat-controller.test.ts +188 -1
  88. package/src/plugins/builtin/chat-controller.ts +537 -220
  89. package/src/plugins/builtin/chat.test.tsx +431 -13
  90. package/src/plugins/builtin/chat.tsx +1134 -227
  91. package/src/plugins/builtin/cloud-tweets.tsx +1 -1
  92. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  93. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  94. package/src/plugins/builtin/correlation/index.tsx +1 -6
  95. package/src/plugins/builtin/debug.tsx +1 -3
  96. package/src/plugins/builtin/econ/index.tsx +1 -2
  97. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  98. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  99. package/src/plugins/builtin/fear-greed/index.tsx +587 -0
  100. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  101. package/src/plugins/builtin/help.test.tsx +2 -2
  102. package/src/plugins/builtin/help.tsx +2 -2
  103. package/src/plugins/builtin/holders/index.test.ts +13 -1
  104. package/src/plugins/builtin/holders/index.tsx +64 -29
  105. package/src/plugins/builtin/insider/index.tsx +22 -89
  106. package/src/plugins/builtin/market-movers/index.tsx +13 -19
  107. package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
  108. package/src/plugins/builtin/market-movers/screener.ts +70 -17
  109. package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
  110. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  111. package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
  112. package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
  113. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
  114. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  115. package/src/plugins/builtin/news.tsx +4 -16
  116. package/src/plugins/builtin/notes.tsx +19 -55
  117. package/src/plugins/builtin/options.test.tsx +58 -2
  118. package/src/plugins/builtin/options.tsx +110 -65
  119. package/src/plugins/builtin/plugin-groups.ts +2 -0
  120. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  121. package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
  122. package/src/plugins/builtin/research/index.test.ts +94 -0
  123. package/src/plugins/builtin/research/index.tsx +164 -33
  124. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  125. package/src/plugins/builtin/sec.tsx +13 -66
  126. package/src/plugins/builtin/sectors/index.tsx +1 -1
  127. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  128. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  129. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  130. package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
  131. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  132. package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
  133. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  134. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  135. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  136. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  137. package/src/plugins/ibkr/gateway-service.ts +1 -1
  138. package/src/plugins/ibkr/index.test.tsx +0 -2
  139. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  140. package/src/plugins/pane-manager.ts +0 -13
  141. package/src/plugins/plugin-runtime.test.tsx +40 -6
  142. package/src/plugins/plugin-runtime.tsx +20 -10
  143. package/src/plugins/prediction-markets/cli.ts +0 -1
  144. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  145. package/src/plugins/prediction-markets/controller.ts +0 -15
  146. package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
  147. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  148. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  149. package/src/plugins/registry.ts +13 -42
  150. package/src/plugins/ticker-navigation.test.ts +66 -0
  151. package/src/plugins/ticker-navigation.ts +44 -0
  152. package/src/react/input.ts +2 -1
  153. package/src/renderers/electrobun/bun/index.ts +32 -11
  154. package/src/renderers/electrobun/bun/window-focus.ts +37 -0
  155. package/src/renderers/electrobun/shared/protocol.ts +7 -1
  156. package/src/renderers/electrobun/view/app-services.ts +88 -31
  157. package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
  158. package/src/renderers/electrobun/view/data-table.tsx +138 -101
  159. package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
  160. package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
  161. package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
  162. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  163. package/src/renderers/electrobun/view/key-event.ts +1 -1
  164. package/src/renderers/electrobun/view/main.tsx +18 -1
  165. package/src/renderers/electrobun/view/styles.css +329 -0
  166. package/src/renderers/electrobun/view/toast-host.tsx +29 -3
  167. package/src/renderers/electrobun/view/ui-host.tsx +42 -0
  168. package/src/renderers/opentui/chart-surface.tsx +179 -0
  169. package/src/renderers/opentui/input-host.tsx +1 -1
  170. package/src/renderers/opentui/start.tsx +48 -30
  171. package/src/renderers/opentui/test-utils.tsx +17 -0
  172. package/src/renderers/opentui/ui-host.tsx +5 -2
  173. package/src/sources/gloomberb-cloud.test.ts +86 -1
  174. package/src/sources/gloomberb-cloud.ts +117 -21
  175. package/src/sources/provider-router.test.ts +122 -0
  176. package/src/sources/provider-router.ts +198 -15
  177. package/src/sources/yahoo-finance.test.ts +44 -0
  178. package/src/sources/yahoo-finance.ts +6 -0
  179. package/src/state/app-bootstrap.ts +20 -7
  180. package/src/state/app-context-selector.test.tsx +189 -4
  181. package/src/state/app-context.test.ts +65 -0
  182. package/src/state/app-context.tsx +48 -16
  183. package/src/state/use-inline-tickers.test.tsx +73 -0
  184. package/src/state/use-inline-tickers.ts +19 -10
  185. package/src/test-support/plugin-runtime.ts +3 -2
  186. package/src/theme/colors.test.ts +75 -0
  187. package/src/theme/colors.ts +76 -6
  188. package/src/theme/theme-context.tsx +26 -0
  189. package/src/theme/themes.test.ts +23 -1
  190. package/src/theme/themes.ts +115 -0
  191. package/src/types/broker.ts +1 -1
  192. package/src/types/capability-route-source.ts +1 -0
  193. package/src/types/data-provider.ts +15 -2
  194. package/src/types/desktop-window.ts +6 -0
  195. package/src/types/financials.ts +2 -0
  196. package/src/types/plugin.ts +1 -3
  197. package/src/ui/host.tsx +7 -0
  198. package/src/utils/api-client.test.ts +32 -0
  199. package/src/utils/api-client.ts +119 -18
  200. package/src/utils/back-navigation.ts +24 -1
  201. package/src/utils/double-escape-close.test.ts +33 -0
  202. package/src/utils/double-escape-close.ts +40 -0
  203. package/src/utils/throttled-fetch.test.ts +22 -0
  204. package/src/utils/throttled-fetch.ts +3 -1
  205. package/src/utils/ticker-search.ts +53 -9
  206. package/src/version.ts +1 -1
  207. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  208. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -0,0 +1,75 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ applyTheme,
4
+ clearTransientThemePreview,
5
+ getCurrentThemeId,
6
+ previewTheme,
7
+ syncTheme,
8
+ } from "./colors";
9
+ import { DEFAULT_THEME, getTheme } from "./themes";
10
+
11
+ const originalDocument = (globalThis as Record<string, unknown>).document;
12
+
13
+ function setDocument(value: unknown): void {
14
+ Object.defineProperty(globalThis, "document", {
15
+ configurable: true,
16
+ writable: true,
17
+ value,
18
+ });
19
+ }
20
+
21
+ function installDocumentStyleMock() {
22
+ const values = new Map<string, string>();
23
+ setDocument({
24
+ documentElement: {
25
+ style: {
26
+ setProperty(name: string, value: string) {
27
+ values.set(name, value);
28
+ },
29
+ },
30
+ },
31
+ });
32
+ return values;
33
+ }
34
+
35
+ afterEach(() => {
36
+ if (originalDocument === undefined) {
37
+ Reflect.deleteProperty(globalThis, "document");
38
+ } else {
39
+ setDocument(originalDocument);
40
+ }
41
+ clearTransientThemePreview();
42
+ syncTheme(DEFAULT_THEME);
43
+ });
44
+
45
+ describe("theme colors", () => {
46
+ test("syncs CSS variables when a theme is applied", () => {
47
+ const values = installDocumentStyleMock();
48
+ const theme = getTheme("midnight");
49
+
50
+ applyTheme("midnight");
51
+
52
+ expect(values.get("--gloom-bg")).toBe(theme.bg);
53
+ expect(values.get("--gloom-panel")).toBe(theme.panel);
54
+ expect(values.get("--gloom-text-dim")).toBe(theme.textDim);
55
+ expect(values.get("--gloom-selected")).toBe(theme.selected);
56
+ expect(values.get("--gloom-hover-bg")).toBeString();
57
+ });
58
+
59
+ test("does not let provider sync clobber a pending preview", () => {
60
+ const values = installDocumentStyleMock();
61
+ const preview = getTheme("midnight");
62
+
63
+ previewTheme("midnight");
64
+ syncTheme(DEFAULT_THEME);
65
+
66
+ expect(getCurrentThemeId()).toBe("midnight");
67
+ expect(values.get("--gloom-bg")).toBe(preview.bg);
68
+
69
+ clearTransientThemePreview();
70
+ syncTheme(DEFAULT_THEME);
71
+
72
+ expect(getCurrentThemeId()).toBe(DEFAULT_THEME);
73
+ expect(values.get("--gloom-bg")).toBe(getTheme(DEFAULT_THEME).bg);
74
+ });
75
+ });
@@ -5,7 +5,33 @@ import { getTheme, DEFAULT_THEME, type Theme } from "./themes";
5
5
  // React components re-read these on each render triggered by the SET_THEME action.
6
6
  export const colors: Omit<Theme, "name" | "description"> = { ...getTheme(DEFAULT_THEME) };
7
7
 
8
+ export type ColorKey = keyof typeof colors;
9
+
8
10
  let currentThemeId = DEFAULT_THEME;
11
+ let transientPreviewThemeId: string | null = null;
12
+ let cssThemeId: string | null = null;
13
+ let cssThemeDocument: unknown = null;
14
+
15
+ const THEME_CSS_VARIABLES: Array<[ColorKey, string]> = [
16
+ ["bg", "--gloom-bg"],
17
+ ["panel", "--gloom-panel"],
18
+ ["border", "--gloom-border"],
19
+ ["borderFocused", "--gloom-border-focused"],
20
+ ["text", "--gloom-text"],
21
+ ["textDim", "--gloom-text-dim"],
22
+ ["textBright", "--gloom-text-bright"],
23
+ ["textMuted", "--gloom-text-muted"],
24
+ ["positive", "--gloom-positive"],
25
+ ["negative", "--gloom-negative"],
26
+ ["neutral", "--gloom-neutral"],
27
+ ["warning", "--gloom-warning"],
28
+ ["header", "--gloom-header"],
29
+ ["headerText", "--gloom-header-text"],
30
+ ["selected", "--gloom-selected"],
31
+ ["selectedText", "--gloom-selected-text"],
32
+ ["commandBg", "--gloom-command-bg"],
33
+ ["commandBorder", "--gloom-command-border"],
34
+ ];
9
35
 
10
36
  export function getCurrentThemeId(): string {
11
37
  return currentThemeId;
@@ -16,14 +42,34 @@ export function applyTheme(id: string): void {
16
42
  currentThemeId = id;
17
43
  // Mutate in-place so every existing import sees the new values
18
44
  Object.assign(colors, theme);
45
+ syncThemeCssVariables();
19
46
  }
20
47
 
21
- export function syncTheme(id: string): void {
22
- if (currentThemeId === id) return;
48
+ export function previewTheme(id: string): void {
49
+ transientPreviewThemeId = id;
23
50
  applyTheme(id);
24
51
  }
25
52
 
26
- export type ColorKey = keyof typeof colors;
53
+ export function clearTransientThemePreview(): void {
54
+ transientPreviewThemeId = null;
55
+ }
56
+
57
+ export function syncTheme(id: string): void {
58
+ if (
59
+ transientPreviewThemeId
60
+ && currentThemeId === transientPreviewThemeId
61
+ && id !== transientPreviewThemeId
62
+ ) {
63
+ syncThemeCssVariables();
64
+ return;
65
+ }
66
+ transientPreviewThemeId = null;
67
+ if (currentThemeId === id) {
68
+ syncThemeCssVariables();
69
+ return;
70
+ }
71
+ applyTheme(id);
72
+ }
27
73
 
28
74
  export { blendHex } from "./color-utils";
29
75
 
@@ -49,6 +95,21 @@ export function hoverBg(): string {
49
95
  return blendHex(colors.bg, colors.selected, 0.5);
50
96
  }
51
97
 
98
+ function syncThemeCssVariables(): void {
99
+ const documentLike = (globalThis as {
100
+ document?: { documentElement?: { style?: { setProperty: (name: string, value: string) => void } } };
101
+ }).document;
102
+ const style = documentLike?.documentElement?.style;
103
+ if (!style) return;
104
+ if (cssThemeId === currentThemeId && cssThemeDocument === documentLike) return;
105
+ cssThemeId = currentThemeId;
106
+ cssThemeDocument = documentLike;
107
+ for (const [key, name] of THEME_CSS_VARIABLES) {
108
+ style.setProperty(name, colors[key]);
109
+ }
110
+ style.setProperty("--gloom-hover-bg", hoverBg());
111
+ }
112
+
52
113
  export function commandBarBg(): string {
53
114
  const base = higherContrast(colors.commandBg, colors.panel, colors.bg);
54
115
  const accent = higherContrast(colors.textBright, colors.borderFocused, base);
@@ -74,7 +135,7 @@ export function commandBarOverlayBg(): string {
74
135
  export function commandBarSelectedBg(): string {
75
136
  const base = commandBarBg();
76
137
  const accent = higherContrast(colors.selectedText, colors.textBright, colors.selected);
77
- return blendForContrast(colors.selected, base, accent, 1.6);
138
+ return blendForContrast(colors.selected, base, accent, 1.45);
78
139
  }
79
140
 
80
141
  export function commandBarHoverBg(): string {
@@ -89,7 +150,12 @@ export function commandBarText(): string {
89
150
 
90
151
  export function commandBarHeadingText(): string {
91
152
  const base = commandBarBg();
92
- return blendForContrast("#8f959e", base, "#b7bec8", 3.6);
153
+ const fallback = higherContrast(
154
+ higherContrast(colors.textDim, colors.text, base),
155
+ higherContrast(colors.textBright, colors.selectedText, base),
156
+ base,
157
+ );
158
+ return blendForContrast(colors.textMuted, base, fallback, 3.6);
93
159
  }
94
160
 
95
161
  export function commandBarSubtleText(): string {
@@ -101,7 +167,11 @@ export function commandBarSubtleText(): string {
101
167
  export function commandBarSelectedText(): string {
102
168
  const base = commandBarSelectedBg();
103
169
  const preferred = higherContrast(colors.selectedText, colors.text, base);
104
- const fallback = higherContrast(colors.textBright, "#f2f2f2", base);
170
+ const fallback = higherContrast(
171
+ higherContrast(colors.textBright, colors.text, base),
172
+ higherContrast("#ffffff", "#000000", base),
173
+ base,
174
+ );
105
175
  return blendForContrast(preferred, base, fallback, 4.5);
106
176
  }
107
177
 
@@ -0,0 +1,26 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import { colors, getCurrentThemeId, syncTheme } from "./colors";
3
+ import type { Theme } from "./themes";
4
+
5
+ type ThemeColors = Omit<Theme, "name" | "description">;
6
+
7
+ interface ThemeContextValue {
8
+ themeId: string;
9
+ colors: ThemeColors;
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
13
+
14
+ export function ThemeProvider({ themeId, children }: { themeId: string; children: ReactNode }) {
15
+ syncTheme(themeId);
16
+ const value = useMemo(() => ({ themeId, colors }), [themeId]);
17
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
18
+ }
19
+
20
+ export function useThemeId(): string {
21
+ return useContext(ThemeContext)?.themeId ?? getCurrentThemeId();
22
+ }
23
+
24
+ export function useThemeColors(): ThemeColors {
25
+ return useContext(ThemeContext)?.colors ?? colors;
26
+ }
@@ -1,5 +1,17 @@
1
1
  import { afterEach, describe, test } from "bun:test";
2
- import { applyTheme, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "./colors";
2
+ import {
3
+ applyTheme,
4
+ commandBarBg,
5
+ commandBarHeadingText,
6
+ commandBarPanelBg,
7
+ commandBarSelectedBg,
8
+ commandBarSelectedText,
9
+ commandBarSubtleText,
10
+ commandBarText,
11
+ floatingPaneTitleBg,
12
+ paneTitleBg,
13
+ paneTitleText,
14
+ } from "./colors";
3
15
  import { contrastRatio } from "./color-utils";
4
16
  import { DEFAULT_THEME, themes } from "./themes";
5
17
 
@@ -44,4 +56,14 @@ describe("theme contrast", () => {
44
56
  assertMinContrast(id, "floatingPaneTitleText/unfocused", paneTitleText(false, true), floatingPaneTitleBg(false), CHROME_TEXT_MIN);
45
57
  }
46
58
  });
59
+
60
+ test("keeps command bar text readable", () => {
61
+ for (const id of Object.keys(themes)) {
62
+ applyTheme(id);
63
+ assertMinContrast(id, "commandBarText/bg", commandBarText(), commandBarBg(), BODY_TEXT_MIN);
64
+ assertMinContrast(id, "commandBarHeadingText/bg", commandBarHeadingText(), commandBarBg(), SUBTLE_TEXT_MIN);
65
+ assertMinContrast(id, "commandBarSubtleText/panel", commandBarSubtleText(), commandBarPanelBg(), SUBTLE_TEXT_MIN);
66
+ assertMinContrast(id, "commandBarSelectedText/selected", commandBarSelectedText(), commandBarSelectedBg(), BODY_TEXT_MIN);
67
+ }
68
+ });
47
69
  });
@@ -305,6 +305,121 @@ const rawThemes: Record<string, Theme> = {
305
305
  commandBorder: "#cccccc",
306
306
  },
307
307
 
308
+ paper: {
309
+ name: "Paper",
310
+ description: "Warm light terminal palette",
311
+ bg: "#f7f3e8",
312
+ panel: "#fffaf0",
313
+ border: "#d8cdb7",
314
+ borderFocused: "#a05a2c",
315
+ text: "#2f2a23",
316
+ textDim: "#5f5548",
317
+ textBright: "#17130f",
318
+ textMuted: "#776b5d",
319
+ positive: "#2f7d4a",
320
+ negative: "#b33630",
321
+ neutral: "#6e6256",
322
+ warning: "#946200",
323
+ header: "#eadcc4",
324
+ headerText: "#7a3f1f",
325
+ selected: "#ead1ab",
326
+ selectedText: "#211810",
327
+ commandBg: "#fff7e8",
328
+ commandBorder: "#a05a2c",
329
+ },
330
+
331
+ "github-light": {
332
+ name: "GitHub Light",
333
+ description: "Clean light code palette",
334
+ bg: "#ffffff",
335
+ panel: "#f6f8fa",
336
+ border: "#d0d7de",
337
+ borderFocused: "#0969da",
338
+ text: "#24292f",
339
+ textDim: "#57606a",
340
+ textBright: "#0b1220",
341
+ textMuted: "#6e7781",
342
+ positive: "#1a7f37",
343
+ negative: "#cf222e",
344
+ neutral: "#57606a",
345
+ warning: "#9a6700",
346
+ header: "#eaeef2",
347
+ headerText: "#0969da",
348
+ selected: "#ddf4ff",
349
+ selectedText: "#0b1220",
350
+ commandBg: "#f6f8fa",
351
+ commandBorder: "#0969da",
352
+ },
353
+
354
+ "solarized-light": {
355
+ name: "Solarized Light",
356
+ description: "Ethan Schoonover's light palette",
357
+ bg: "#fdf6e3",
358
+ panel: "#eee8d5",
359
+ border: "#93a1a1",
360
+ borderFocused: "#b58900",
361
+ text: "#586e75",
362
+ textDim: "#657b83",
363
+ textBright: "#073642",
364
+ textMuted: "#7c8a8a",
365
+ positive: "#859900",
366
+ negative: "#dc322f",
367
+ neutral: "#657b83",
368
+ warning: "#b58900",
369
+ header: "#e6dfc9",
370
+ headerText: "#268bd2",
371
+ selected: "#dce6d6",
372
+ selectedText: "#073642",
373
+ commandBg: "#eee8d5",
374
+ commandBorder: "#b58900",
375
+ },
376
+
377
+ "gruvbox-light": {
378
+ name: "Gruvbox Light",
379
+ description: "Retro earthy light palette",
380
+ bg: "#fbf1c7",
381
+ panel: "#f2e5bc",
382
+ border: "#d5c4a1",
383
+ borderFocused: "#af3a03",
384
+ text: "#3c3836",
385
+ textDim: "#665c54",
386
+ textBright: "#282828",
387
+ textMuted: "#7c6f64",
388
+ positive: "#79740e",
389
+ negative: "#9d0006",
390
+ neutral: "#665c54",
391
+ warning: "#b57614",
392
+ header: "#ebdbb2",
393
+ headerText: "#af3a03",
394
+ selected: "#d5c4a1",
395
+ selectedText: "#282828",
396
+ commandBg: "#f2e5bc",
397
+ commandBorder: "#af3a03",
398
+ },
399
+
400
+ "nord-light": {
401
+ name: "Nord Light",
402
+ description: "Soft arctic light palette",
403
+ bg: "#eceff4",
404
+ panel: "#e5e9f0",
405
+ border: "#d8dee9",
406
+ borderFocused: "#5e81ac",
407
+ text: "#2e3440",
408
+ textDim: "#4c566a",
409
+ textBright: "#1f2530",
410
+ textMuted: "#687286",
411
+ positive: "#4b7f36",
412
+ negative: "#bf616a",
413
+ neutral: "#4c566a",
414
+ warning: "#9a7500",
415
+ header: "#d8dee9",
416
+ headerText: "#5e81ac",
417
+ selected: "#c8d5e8",
418
+ selectedText: "#1f2530",
419
+ commandBg: "#e5e9f0",
420
+ commandBorder: "#5e81ac",
421
+ },
422
+
308
423
  tokyo: {
309
424
  name: "Tokyo Night",
310
425
  description: "Cool blue-purple palette",
@@ -67,7 +67,7 @@ export interface BrokerConnectionStatus {
67
67
  export interface BrokerProfileAction {
68
68
  id: string;
69
69
  label: string;
70
- widgetId?: string;
70
+ paneId?: string;
71
71
  disabled?: boolean;
72
72
  disabledReason?: string;
73
73
  }
@@ -6,6 +6,7 @@ export interface NewsDataProvider {
6
6
  supports?(query: NewsQuery): boolean;
7
7
  getCachedNews?(query: NewsQuery): NewsArticle[];
8
8
  fetchNews(query: NewsQuery): Promise<NewsArticle[]>;
9
+ fetchNewsStory?(storyId: string): Promise<NewsArticle | null>;
9
10
  }
10
11
 
11
12
  export interface CapabilityRouteSource {
@@ -2,7 +2,6 @@ import type {
2
2
  AnalystResearchData,
3
3
  CorporateActionsData,
4
4
  Quote,
5
- Fundamentals,
6
5
  TickerFinancials,
7
6
  PricePoint,
8
7
  OptionsChain,
@@ -93,6 +92,18 @@ export interface QuoteSubscriptionTarget {
93
92
  route?: "auto" | "provider" | "broker";
94
93
  }
95
94
 
95
+ export interface QuoteBatchResult {
96
+ target: QuoteSubscriptionTarget;
97
+ quote: Quote | null;
98
+ error?: unknown;
99
+ }
100
+
101
+ export interface TickerFinancialsBatchResult {
102
+ target: CachedFinancialsTarget;
103
+ financials: TickerFinancials | null;
104
+ error?: unknown;
105
+ }
106
+
96
107
  export interface AssetDataProvider {
97
108
  readonly id: string;
98
109
  readonly name: string;
@@ -100,7 +111,9 @@ export interface AssetDataProvider {
100
111
  readonly cachePolicy?: CachePolicyMap;
101
112
 
102
113
  canProvide?(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<boolean> | boolean;
103
- getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean }): Map<string, TickerFinancials>;
114
+ getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean }): Map<string, TickerFinancials> | Promise<Map<string, TickerFinancials>>;
115
+ getQuotesBatch?(targets: QuoteSubscriptionTarget[], options?: { forceRefresh?: boolean }): Promise<QuoteBatchResult[]>;
116
+ getTickerFinancialsBatch?(targets: CachedFinancialsTarget[], options?: { forceRefresh?: boolean }): Promise<TickerFinancialsBatchResult[]>;
104
117
  getTickerFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<TickerFinancials>;
105
118
  getQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<Quote>;
106
119
  getExchangeRate(fromCurrency: string): Promise<number>;
@@ -15,10 +15,15 @@ export interface DesktopDockPreviewState {
15
15
  edge: "left" | "right" | "top" | "bottom" | null;
16
16
  }
17
17
 
18
+ export interface DesktopThemePreviewState {
19
+ theme: string | null;
20
+ }
21
+
18
22
  export interface DesktopWindowBridge {
19
23
  kind: "main" | "detached";
20
24
  paneId?: string;
21
25
  syncMainState?(snapshot: DesktopSharedStateSnapshot): Promise<void>;
26
+ syncThemePreview?(preview: DesktopThemePreviewState): Promise<void>;
22
27
  replaceDetachedPaneState?(paneId: string, paneState: PaneRuntimeState): Promise<void>;
23
28
  popOutPane?(paneId: string): Promise<void>;
24
29
  dockDetachedPane?(paneId: string): Promise<void>;
@@ -26,4 +31,5 @@ export interface DesktopWindowBridge {
26
31
  focusDetachedPane?(paneId: string): Promise<void>;
27
32
  subscribeState(listener: (snapshot: DesktopSharedStateSnapshot) => void): () => void;
28
33
  subscribeDockPreview?(listener: (preview: DesktopDockPreviewState) => void): () => void;
34
+ subscribeThemePreview?(listener: (preview: DesktopThemePreviewState) => void): () => void;
29
35
  }
@@ -141,6 +141,8 @@ export interface AnalystRatingRecord {
141
141
  action?: string;
142
142
  current?: string;
143
143
  prior?: string;
144
+ currentPriceTarget?: number;
145
+ priorPriceTarget?: number;
144
146
  }
145
147
 
146
148
  export interface AnalystEstimateRecord {
@@ -449,14 +449,12 @@ export interface GloomPluginContext {
449
449
  hidePane(paneId: string): void;
450
450
  focusPane(paneId: string): void;
451
451
  pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
452
- navigateTicker(symbol: string): void;
452
+ navigateTicker(symbol: string, options?: { sourcePaneId?: string | null }): void;
453
453
  openPaneSettings(paneId?: string): void;
454
454
 
455
455
  on<K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void): () => void;
456
456
  emit<K extends keyof PluginEvents>(event: K, payload: PluginEvents[K]): void;
457
457
 
458
- showWidget(widgetId: string): void;
459
- hideWidget(widgetId: string): void;
460
458
  notify(notification: AppNotificationRequest): void;
461
459
  }
462
460
 
package/src/ui/host.tsx CHANGED
@@ -98,13 +98,19 @@ export interface ScrollBoxRenderable {
98
98
  scrollTop: number;
99
99
  scrollLeft?: number;
100
100
  scrollHeight: number;
101
+ scrollTopPx?: number;
102
+ scrollLeftPx?: number;
103
+ scrollHeightPx?: number;
104
+ scrollWidthPx?: number;
101
105
  viewport?: { width: number; height: number };
106
+ viewportPx?: { width: number; height: number };
102
107
  visible?: boolean;
103
108
  parent?: unknown;
104
109
  getBoundingClientRect?: () => { x: number; y: number; width: number; height: number };
105
110
  horizontalScrollBar?: { visible: boolean };
106
111
  verticalScrollBar?: { visible: boolean };
107
112
  scrollTo(target: number | { x?: number; y?: number }, y?: number): void;
113
+ scrollToPixels?(target: number | { x?: number; y?: number }, y?: number): void;
108
114
  }
109
115
 
110
116
  export interface InputRenderable {
@@ -245,6 +251,7 @@ export interface UiHost {
245
251
  cellHeightPx?: number;
246
252
  pixelRatio?: number;
247
253
  canvasCharts?: boolean;
254
+ nativeCharts?: boolean;
248
255
  nativeContextMenu?: boolean;
249
256
  };
250
257
  Box: ComponentType<BoxProps>;
@@ -320,4 +320,36 @@ describe("apiClient cloud news", () => {
320
320
  expect(url.searchParams.get("cursor")).toBe("cursor-1");
321
321
  expect(result).toEqual({ items: [], nextCursor: null });
322
322
  });
323
+
324
+ test("fetches news story details from the story route", async () => {
325
+ let seenUrl = "";
326
+ globalThis.fetch = mockFetch(async (input: Request | string | URL) => {
327
+ seenUrl = String(input);
328
+ return createResponse({
329
+ id: "story-1",
330
+ headline: "Story headline",
331
+ summary: "Story summary",
332
+ category: "general",
333
+ sentiment: "neutral",
334
+ sectors: [],
335
+ firstPublishedAt: "2026-04-01T10:00:00.000Z",
336
+ lastPublishedAt: "2026-04-01T10:05:00.000Z",
337
+ firstSeenAt: "2026-04-01T10:00:10.000Z",
338
+ lastSeenAt: "2026-04-01T10:05:10.000Z",
339
+ primaryUrl: "https://example.com/story",
340
+ primarySource: "example-wire",
341
+ variantCount: 2,
342
+ sourceCount: 2,
343
+ sources: ["example-wire"],
344
+ entities: [],
345
+ tickerLinks: [],
346
+ items: [],
347
+ });
348
+ });
349
+
350
+ const story = await apiClient.getCloudNewsStory("story-1");
351
+
352
+ expect(new URL(seenUrl).pathname).toBe("/news/story-1");
353
+ expect(story.id).toBe("story-1");
354
+ });
323
355
  });