gloomberb 0.6.1 → 0.7.0

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 (139) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +48 -27
  14. package/src/components/chart/static-chart-surface.tsx +28 -4
  15. package/src/components/chart/stock-chart.test.ts +0 -16
  16. package/src/components/chart/stock-chart.tsx +38 -21
  17. package/src/components/chart/time-axis-label.tsx +43 -0
  18. package/src/components/command-bar/command-bar.test.tsx +71 -8
  19. package/src/components/command-bar/command-bar.tsx +388 -713
  20. package/src/components/command-bar/confirm-body.tsx +51 -0
  21. package/src/components/command-bar/list-model.ts +77 -0
  22. package/src/components/command-bar/view-model.ts +0 -19
  23. package/src/components/command-bar/workflow-ops.ts +4 -0
  24. package/src/components/data-table-stack-view.test.tsx +39 -4
  25. package/src/components/data-table-stack-view.tsx +11 -104
  26. package/src/components/data-table-view.tsx +43 -79
  27. package/src/components/feed-data-table-stack-view.tsx +10 -17
  28. package/src/components/feed-stack-controller.ts +39 -0
  29. package/src/components/index.ts +5 -1
  30. package/src/components/layout/floating-pane.tsx +30 -2
  31. package/src/components/layout/header.tsx +10 -7
  32. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  33. package/src/components/layout/pane-cycle-order.ts +32 -0
  34. package/src/components/layout/pane-footer.tsx +11 -4
  35. package/src/components/layout/pane-header.tsx +5 -5
  36. package/src/components/layout/shell.test.tsx +66 -22
  37. package/src/components/layout/shell.tsx +13 -6
  38. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  39. package/src/components/pane-settings-dialog.tsx +12 -4
  40. package/src/components/pane-template-wizard.test.tsx +3 -4
  41. package/src/components/speedometer-gauge.test.tsx +45 -0
  42. package/src/components/speedometer-gauge.tsx +558 -0
  43. package/src/components/table-view-shared.tsx +158 -0
  44. package/src/components/ticker-list-table-view.tsx +42 -72
  45. package/src/components/ui/confirm-dialog.tsx +66 -0
  46. package/src/components/ui/data-table.tsx +18 -10
  47. package/src/components/ui/index.ts +0 -1
  48. package/src/components/ui/page-stack-view.tsx +39 -10
  49. package/src/components/ui/table-layout.test.ts +16 -0
  50. package/src/components/ui/table-layout.ts +10 -3
  51. package/src/components/ui/ui.test.tsx +100 -20
  52. package/src/core/state/app-state.ts +6 -3
  53. package/src/market-data/coordinator.test.ts +1 -1
  54. package/src/market-data/coordinator.ts +0 -1
  55. package/src/news/aggregator.ts +0 -3
  56. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  57. package/src/plugins/builtin/ai/providers.ts +0 -17
  58. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  59. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  60. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  61. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  62. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  63. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  64. package/src/plugins/builtin/chat-controller.ts +75 -6
  65. package/src/plugins/builtin/chat.test.tsx +6 -6
  66. package/src/plugins/builtin/chat.tsx +29 -20
  67. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  68. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  69. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  70. package/src/plugins/builtin/correlation/index.tsx +1 -6
  71. package/src/plugins/builtin/debug.tsx +1 -3
  72. package/src/plugins/builtin/econ/index.tsx +1 -2
  73. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  74. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  75. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  76. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  77. package/src/plugins/builtin/help.test.tsx +2 -2
  78. package/src/plugins/builtin/help.tsx +2 -2
  79. package/src/plugins/builtin/holders/index.tsx +10 -15
  80. package/src/plugins/builtin/insider/index.tsx +22 -89
  81. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  82. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  83. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  84. package/src/plugins/builtin/news.tsx +4 -16
  85. package/src/plugins/builtin/notes.tsx +18 -55
  86. package/src/plugins/builtin/options.test.tsx +58 -2
  87. package/src/plugins/builtin/options.tsx +108 -49
  88. package/src/plugins/builtin/plugin-groups.ts +2 -0
  89. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  90. package/src/plugins/builtin/research/index.tsx +5 -21
  91. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  92. package/src/plugins/builtin/sec.tsx +13 -66
  93. package/src/plugins/builtin/sectors/index.tsx +1 -1
  94. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  95. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  96. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  97. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  98. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  99. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  100. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  101. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  102. package/src/plugins/ibkr/gateway-service.ts +1 -1
  103. package/src/plugins/ibkr/index.test.tsx +0 -2
  104. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  105. package/src/plugins/pane-manager.ts +0 -13
  106. package/src/plugins/plugin-runtime.test.tsx +6 -6
  107. package/src/plugins/plugin-runtime.tsx +13 -8
  108. package/src/plugins/prediction-markets/cli.ts +0 -1
  109. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  110. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  111. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  112. package/src/plugins/registry.ts +10 -39
  113. package/src/react/input.ts +1 -0
  114. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  115. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  116. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  117. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  118. package/src/renderers/electrobun/view/key-event.ts +1 -1
  119. package/src/renderers/electrobun/view/styles.css +331 -0
  120. package/src/renderers/opentui/chart-surface.tsx +179 -0
  121. package/src/renderers/opentui/start.tsx +48 -30
  122. package/src/renderers/opentui/test-utils.tsx +17 -0
  123. package/src/renderers/opentui/ui-host.tsx +5 -2
  124. package/src/sources/provider-router.ts +0 -4
  125. package/src/state/app-context.test.ts +45 -0
  126. package/src/state/use-inline-tickers.test.tsx +73 -0
  127. package/src/state/use-inline-tickers.ts +7 -1
  128. package/src/test-support/plugin-runtime.ts +3 -2
  129. package/src/theme/colors.ts +12 -3
  130. package/src/theme/themes.test.ts +23 -1
  131. package/src/theme/themes.ts +115 -0
  132. package/src/types/broker.ts +1 -1
  133. package/src/types/data-provider.ts +0 -1
  134. package/src/types/plugin.ts +0 -2
  135. package/src/ui/host.tsx +1 -0
  136. package/src/utils/back-navigation.ts +24 -1
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  139. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -34,38 +34,56 @@ export async function startOpenTuiApp(): Promise<void> {
34
34
  cliLaunchRequest = dispatchResult.request;
35
35
  }
36
36
  }
37
- startMainThreadMonitor("opentui");
37
+ const stopMainThreadMonitor = startMainThreadMonitor("opentui");
38
38
 
39
- let dataDir = await getDataDir();
40
- if (!dataDir) {
41
- dataDir = join(process.env.HOME || "~", ".gloomberb");
42
- }
39
+ let host: Awaited<ReturnType<typeof createOpenTuiHost>> | null = null;
40
+ let exitTimer: ReturnType<typeof setTimeout> | null = null;
41
+ const finishProcessExit = () => {
42
+ stopMainThreadMonitor();
43
+ if (exitTimer) return;
44
+ exitTimer = setTimeout(() => {
45
+ process.exit(process.exitCode ?? 0);
46
+ }, 0);
47
+ };
48
+ try {
49
+ let dataDir = await getDataDir();
50
+ if (!dataDir) {
51
+ dataDir = join(process.env.HOME || "~", ".gloomberb");
52
+ }
43
53
 
44
- if (!existsSync(dataDir)) {
45
- mkdirSync(dataDir, { recursive: true });
46
- }
54
+ if (!existsSync(dataDir)) {
55
+ mkdirSync(dataDir, { recursive: true });
56
+ }
47
57
 
48
- const config = await measurePerfAsync("startup.opentui.init-data-dir", () => initDataDir(dataDir));
49
- const host = await measurePerfAsync("startup.opentui.create-host", () => createOpenTuiHost());
58
+ const config = await measurePerfAsync("startup.opentui.init-data-dir", () => initDataDir(dataDir));
59
+ host = await measurePerfAsync("startup.opentui.create-host", () => createOpenTuiHost());
60
+ host.renderer.once("destroy", finishProcessExit);
50
61
 
51
- host.render(
52
- <UiHostProvider ui={openTuiUiHost} renderer={host.rendererHost} nativeRenderer={host.nativeRenderer}>
53
- <OpenTuiInputHostProvider>
54
- <ToastHostProvider host={openTuiToastHost}>
55
- <OpenTuiDialogHostProvider
56
- size="medium"
57
- dialogOptions={{ style: { backgroundColor: colors.bg, borderColor: colors.borderFocused, borderStyle: "single", paddingX: 2, paddingY: 1 } }}
58
- backdropColor="#000000"
59
- backdropOpacity={0.8}
60
- >
61
- <App
62
- config={config}
63
- externalPlugins={externalPlugins}
64
- cliLaunchRequest={cliLaunchRequest}
65
- />
66
- </OpenTuiDialogHostProvider>
67
- </ToastHostProvider>
68
- </OpenTuiInputHostProvider>
69
- </UiHostProvider>,
70
- );
62
+ host.render(
63
+ <UiHostProvider ui={openTuiUiHost} renderer={host.rendererHost} nativeRenderer={host.nativeRenderer}>
64
+ <OpenTuiInputHostProvider>
65
+ <ToastHostProvider host={openTuiToastHost}>
66
+ <OpenTuiDialogHostProvider
67
+ size="medium"
68
+ dialogOptions={{ style: { backgroundColor: colors.bg, borderColor: colors.borderFocused, borderStyle: "single", paddingX: 2, paddingY: 1 } }}
69
+ backdropColor="#000000"
70
+ backdropOpacity={0.8}
71
+ >
72
+ <App
73
+ config={config}
74
+ externalPlugins={externalPlugins}
75
+ cliLaunchRequest={cliLaunchRequest}
76
+ />
77
+ </OpenTuiDialogHostProvider>
78
+ </ToastHostProvider>
79
+ </OpenTuiInputHostProvider>
80
+ </UiHostProvider>,
81
+ );
82
+ } catch (error) {
83
+ if (exitTimer) clearTimeout(exitTimer);
84
+ stopMainThreadMonitor();
85
+ host?.renderer.off("destroy", finishProcessExit);
86
+ host?.destroy();
87
+ throw error;
88
+ }
71
89
  }
@@ -1,4 +1,5 @@
1
1
  import { useMemo, type ReactNode } from "react";
2
+ import { DialogProvider } from "@opentui-ui/dialog/react";
2
3
  import { testRender as openTuiTestRender } from "@opentui/react/test-utils";
3
4
  import { createRoot as openTuiCreateRoot, useRenderer } from "@opentui/react";
4
5
  import { UiHostProvider, type NativeRendererHost, type RendererHost } from "../../ui";
@@ -8,6 +9,22 @@ import { openTuiUiHost } from "./ui-host";
8
9
  import { OpenTuiDialogHostProvider } from "./dialog-host";
9
10
  import { openTuiToastHost } from "./toast-host";
10
11
 
12
+ export function TestDialogProvider({ children }: { children: ReactNode }) {
13
+ return (
14
+ <DialogProvider
15
+ dialogOptions={{
16
+ style: {
17
+ backgroundColor: "#000000",
18
+ borderColor: "#ffffff",
19
+ borderStyle: "single",
20
+ },
21
+ }}
22
+ >
23
+ {children}
24
+ </DialogProvider>
25
+ );
26
+ }
27
+
11
28
  function createTestNativeRendererHost(renderer: any): NativeRendererHost {
12
29
  if (typeof renderer.write !== "function") {
13
30
  renderer.write = (data: string | Uint8Array) => {
@@ -4,6 +4,7 @@ import "opentui-spinner/react";
4
4
  import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
5
5
  import { renderAsciiText } from "../../ui/ascii-font";
6
6
  import { OpenTuiImageSurface } from "./image-surface";
7
+ import { OpenTuiChartSurface } from "./chart-surface";
7
8
 
8
9
  function mapTextAttributes(appAttributes: number | undefined, props?: TextProps): number | undefined {
9
10
  const flags = typeof appAttributes === "number" ? appAttributes : 0;
@@ -43,7 +44,9 @@ function mapTextContent(content: unknown): unknown {
43
44
 
44
45
  export const openTuiUiHost: UiHost = {
45
46
  kind: "opentui",
46
- capabilities: {},
47
+ capabilities: {
48
+ nativeCharts: true,
49
+ },
47
50
  Box: ({ children, ...props }) => <box {...props}>{children}</box>,
48
51
  Text: ({ children, ...props }) => {
49
52
  const textProps = stripTextProps(props);
@@ -66,7 +69,7 @@ export const openTuiUiHost: UiHost = {
66
69
  ScrollBox: ({ children, ...props }) => <scrollbox {...props}>{children}</scrollbox>,
67
70
  Input: ({ children, ...props }) => createElement("input" as any, props, children),
68
71
  Textarea: ({ children, ...props }) => createElement("textarea" as any, props, children),
69
- ChartSurface: ({ children, bitmap: _bitmap, bitmaps: _bitmaps, crosshair: _crosshair, ...props }) => <box {...props}>{children}</box>,
72
+ ChartSurface: OpenTuiChartSurface,
70
73
  ImageSurface: OpenTuiImageSurface,
71
74
  SpinnerMark: ({ children, ...props }) => createElement("spinner" as any, props, children),
72
75
  AsciiText: ({ text, font = "tiny", color, fg, bg, backgroundColor, selectable = false, ...props }) => {
@@ -134,10 +134,6 @@ function isIntradayRange(range: TimeRange): boolean {
134
134
  return range === "1D" || range === "1W" || range === "1M" || range === "3M";
135
135
  }
136
136
 
137
- function hasMeaningfulFundamentals(data: TickerFinancials | null | undefined): boolean {
138
- return !!data && Object.keys(data.fundamentals ?? {}).length > 0;
139
- }
140
-
141
137
  function hasMeaningfulProfile(data: TickerFinancials | null | undefined): boolean {
142
138
  return !!data && !!(
143
139
  data.profile?.description
@@ -111,6 +111,38 @@ describe("resolveTickerForPane", () => {
111
111
  )?.zIndex).toBe(21);
112
112
  });
113
113
 
114
+ test("raises the session-focused floating pane above tied saved z-order", () => {
115
+ const config = createDefaultConfig("/tmp/gloomberb-test");
116
+ config.layout = {
117
+ ...config.layout,
118
+ dockRoot: null,
119
+ floating: [
120
+ { instanceId: "ticker-detail:main", x: 4, y: 2, width: 60, height: 22, zIndex: 50 },
121
+ { instanceId: "chat:main", x: 8, y: 4, width: 48, height: 18, zIndex: 50 },
122
+ ],
123
+ };
124
+ config.layouts = config.layouts.map((entry, index) => (
125
+ index === config.activeLayoutIndex ? { ...entry, layout: cloneLayout(config.layout) } : entry
126
+ ));
127
+ const sessionSnapshot: AppSessionSnapshot = {
128
+ paneState: {},
129
+ focusedPaneId: "ticker-detail:main",
130
+ activePanel: "right",
131
+ statusBarVisible: true,
132
+ openPaneIds: ["ticker-detail:main", "chat:main"],
133
+ hydrationTargets: [],
134
+ exchangeCurrencies: ["USD"],
135
+ savedAt: Date.now(),
136
+ };
137
+
138
+ const state = createInitialState(config, sessionSnapshot);
139
+ const tickerEntry = state.config.layout.floating.find((entry) => entry.instanceId === "ticker-detail:main");
140
+ const chatEntry = state.config.layout.floating.find((entry) => entry.instanceId === "chat:main");
141
+
142
+ expect(tickerEntry?.zIndex).toBe(51);
143
+ expect((tickerEntry?.zIndex ?? 0) > (chatEntry?.zIndex ?? 0)).toBe(true);
144
+ });
145
+
114
146
  test("raises the desktop-snapshot focused floating pane during hydration", () => {
115
147
  const config = createDefaultConfig("/tmp/gloomberb-test");
116
148
  config.layout = {
@@ -455,6 +487,19 @@ describe("quote merging", () => {
455
487
  });
456
488
 
457
489
  describe("layout focus fallback", () => {
490
+ test("cycles backward from a stale focused pane to the last visible pane", () => {
491
+ const config = createDefaultConfig("/tmp/gloomberb-test");
492
+ const state = createInitialState(config);
493
+ state.focusedPaneId = "hidden:stale";
494
+
495
+ const next = appReducer(state, {
496
+ type: "FOCUS_PREV",
497
+ paneOrder: ["portfolio-list:main", "ticker-detail:main", "ticker-detail:nvda"],
498
+ });
499
+
500
+ expect(next.focusedPaneId).toBe("ticker-detail:nvda");
501
+ });
502
+
458
503
  test("focuses the highest remaining floating pane when the focused floating pane closes", () => {
459
504
  const config = createDefaultConfig("/tmp/gloomberb-test");
460
505
  const backgroundPane = createPaneInstance("chat", {
@@ -0,0 +1,73 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { testRender } from "../renderers/opentui/test-utils";
4
+ import { setSharedMarketDataCoordinator } from "../market-data/coordinator";
5
+ import { setSharedRegistryForTests, type PluginRegistry } from "../plugins/registry";
6
+ import { createDefaultConfig } from "../types/config";
7
+ import { AppContext, createInitialState } from "./app-context";
8
+ import { useInlineTickers } from "./use-inline-tickers";
9
+
10
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
11
+
12
+ function InlineTickerHarness() {
13
+ const { catalog } = useInlineTickers(["$LGD1L"]);
14
+ return <text>{catalog.LGD1L?.status ?? "none"}</text>;
15
+ }
16
+
17
+ afterEach(() => {
18
+ testSetup?.renderer.destroy();
19
+ testSetup = undefined;
20
+ setSharedMarketDataCoordinator(null);
21
+ setSharedRegistryForTests(undefined);
22
+ });
23
+
24
+ describe("useInlineTickers", () => {
25
+ test("does not leak rejected quote lookups for existing inline tickers", async () => {
26
+ const config = createDefaultConfig("/tmp/gloomberb-inline-tickers-test");
27
+ const state = createInitialState(config);
28
+ state.tickers.set("LGD1L", {
29
+ metadata: {
30
+ ticker: "LGD1L",
31
+ exchange: "NASDAQ",
32
+ currency: "USD",
33
+ name: "Unsupported quote",
34
+ portfolios: [],
35
+ watchlists: [],
36
+ positions: [],
37
+ broker_contracts: [],
38
+ custom: {},
39
+ tags: [],
40
+ },
41
+ });
42
+ const actions: unknown[] = [];
43
+ setSharedRegistryForTests({
44
+ marketData: {
45
+ getQuote: async () => {
46
+ throw new Error("No quote provider available for LGD1L");
47
+ },
48
+ },
49
+ pinTicker: () => {},
50
+ } as unknown as PluginRegistry);
51
+
52
+ await act(async () => {
53
+ testSetup = await testRender(
54
+ <AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
55
+ <InlineTickerHarness />
56
+ </AppContext>,
57
+ { width: 20, height: 1 },
58
+ );
59
+ });
60
+
61
+ await act(async () => {
62
+ await testSetup!.renderOnce();
63
+ await Promise.resolve();
64
+ await Promise.resolve();
65
+ });
66
+ await act(async () => {
67
+ await testSetup!.renderOnce();
68
+ });
69
+
70
+ expect(testSetup.captureCharFrame()).toContain("missing");
71
+ expect(actions).toEqual([]);
72
+ });
73
+ });
@@ -18,6 +18,7 @@ export interface InlineTickerCatalogEntry {
18
18
  const resolutionInFlight = new Map<string, Promise<void>>();
19
19
  const quoteInFlight = new Map<string, Promise<void>>();
20
20
  const missingSymbols = new Set<string>();
21
+ const quoteMissingSymbols = new Set<string>();
21
22
  const initialQuoteRequested = new Set<string>();
22
23
 
23
24
  function normalizeSymbols(texts: readonly string[]): string[] {
@@ -54,6 +55,7 @@ export function useInlineTickers(texts: readonly string[]): {
54
55
 
55
56
  if (currentTicker && currentQuote) {
56
57
  missingSymbols.delete(symbol);
58
+ quoteMissingSymbols.delete(symbol);
57
59
  initialQuoteRequested.delete(symbol);
58
60
  continue;
59
61
  }
@@ -123,8 +125,12 @@ export function useInlineTickers(texts: readonly string[]): {
123
125
  }
124
126
  : undefined,
125
127
  );
128
+ quoteMissingSymbols.delete(symbol);
126
129
  current.dispatch({ type: "MERGE_QUOTE", symbol, quote });
127
130
  })()
131
+ .catch(() => {
132
+ quoteMissingSymbols.add(symbol);
133
+ })
128
134
  .finally(() => {
129
135
  quoteInFlight.delete(symbol);
130
136
  });
@@ -167,7 +173,7 @@ export function useInlineTickers(texts: readonly string[]): {
167
173
  const ticker = tickers.get(symbol) ?? null;
168
174
  const quote = financials.get(symbol)?.quote ?? null;
169
175
  const status: InlineTickerStatus = ticker
170
- ? (quote ? "ready" : "loading")
176
+ ? (quote ? "ready" : quoteMissingSymbols.has(symbol) ? "missing" : "loading")
171
177
  : (missingSymbols.has(symbol) ? "missing" : "loading");
172
178
  catalog[symbol] = { status, ticker, quote };
173
179
  }
@@ -23,8 +23,9 @@ export function createTestPluginRuntime(
23
23
  switchTab() {},
24
24
  switchPanel() {},
25
25
  openCommandBar() {},
26
- showWidget() {},
27
- hideWidget() {},
26
+ showPane() {},
27
+ hidePane() {},
28
+ openPaneSettings() {},
28
29
  openPluginCommandWorkflow() {},
29
30
  notify() {},
30
31
  subscribeResumeState: () => () => {},
@@ -74,7 +74,7 @@ export function commandBarOverlayBg(): string {
74
74
  export function commandBarSelectedBg(): string {
75
75
  const base = commandBarBg();
76
76
  const accent = higherContrast(colors.selectedText, colors.textBright, colors.selected);
77
- return blendForContrast(colors.selected, base, accent, 1.6);
77
+ return blendForContrast(colors.selected, base, accent, 1.45);
78
78
  }
79
79
 
80
80
  export function commandBarHoverBg(): string {
@@ -89,7 +89,12 @@ export function commandBarText(): string {
89
89
 
90
90
  export function commandBarHeadingText(): string {
91
91
  const base = commandBarBg();
92
- return blendForContrast("#8f959e", base, "#b7bec8", 3.6);
92
+ const fallback = higherContrast(
93
+ higherContrast(colors.textDim, colors.text, base),
94
+ higherContrast(colors.textBright, colors.selectedText, base),
95
+ base,
96
+ );
97
+ return blendForContrast(colors.textMuted, base, fallback, 3.6);
93
98
  }
94
99
 
95
100
  export function commandBarSubtleText(): string {
@@ -101,7 +106,11 @@ export function commandBarSubtleText(): string {
101
106
  export function commandBarSelectedText(): string {
102
107
  const base = commandBarSelectedBg();
103
108
  const preferred = higherContrast(colors.selectedText, colors.text, base);
104
- const fallback = higherContrast(colors.textBright, "#f2f2f2", base);
109
+ const fallback = higherContrast(
110
+ higherContrast(colors.textBright, colors.text, base),
111
+ higherContrast("#ffffff", "#000000", base),
112
+ base,
113
+ );
105
114
  return blendForContrast(preferred, base, fallback, 4.5);
106
115
  }
107
116
 
@@ -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
  }
@@ -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,
@@ -455,8 +455,6 @@ export interface GloomPluginContext {
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
@@ -245,6 +245,7 @@ export interface UiHost {
245
245
  cellHeightPx?: number;
246
246
  pixelRatio?: number;
247
247
  canvasCharts?: boolean;
248
+ nativeCharts?: boolean;
248
249
  nativeContextMenu?: boolean;
249
250
  };
250
251
  Box: ComponentType<BoxProps>;
@@ -1,11 +1,20 @@
1
1
  export interface BackNavigationEventLike {
2
2
  name?: string;
3
+ key?: string;
3
4
  ctrl?: boolean;
4
5
  meta?: boolean;
5
6
  option?: boolean;
6
7
  shift?: boolean;
7
8
  }
8
9
 
10
+ export interface BackNavigationMouseEventLike {
11
+ button?: unknown;
12
+ }
13
+
14
+ export const MOUSE_BACK_NAVIGATION_EVENT_NAME = "mouse-back";
15
+ export const WEB_MOUSE_BACK_NAVIGATION_BUTTON = 3;
16
+ export const TERMINAL_MOUSE_BACK_NAVIGATION_BUTTON = 8;
17
+
9
18
  export function isPlainBackspace(event: BackNavigationEventLike): boolean {
10
19
  return event.name === "backspace"
11
20
  && !event.ctrl
@@ -29,5 +38,19 @@ export function isBackNavigationKey(event: BackNavigationEventLike): boolean {
29
38
  export function isDetailBackNavigationKey(
30
39
  event: BackNavigationEventLike,
31
40
  ): boolean {
32
- return isPlainBackspace(event);
41
+ return isPlainBackspace(event)
42
+ || isPlainEscape(event)
43
+ || event.name === MOUSE_BACK_NAVIGATION_EVENT_NAME
44
+ || event.key === MOUSE_BACK_NAVIGATION_EVENT_NAME;
45
+ }
46
+
47
+ export function isMouseBackNavigationButton(button: unknown): boolean {
48
+ return button === WEB_MOUSE_BACK_NAVIGATION_BUTTON
49
+ || button === TERMINAL_MOUSE_BACK_NAVIGATION_BUTTON;
50
+ }
51
+
52
+ export function isMouseBackNavigationEvent(
53
+ event: BackNavigationMouseEventLike | null | undefined,
54
+ ): boolean {
55
+ return isMouseBackNavigationButton(event?.button);
33
56
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.1";
1
+ export const VERSION = "0.7.0";
@@ -1,21 +0,0 @@
1
- import type { PaneProps } from "../../../types/plugin";
2
- import { NEWS_QUERY_PRESETS } from "./news-query-presets";
3
- import { NewsPresetPane } from "./news-preset-pane";
4
- import type { NewsSortPreference } from "./news-table";
5
-
6
- const DEFAULT_SORT: NewsSortPreference = { columnId: "time", direction: "desc" };
7
-
8
- export function FeedPane(props: PaneProps) {
9
- return (
10
- <NewsPresetPane
11
- {...props}
12
- paneKey="feed"
13
- title="News Feed"
14
- query={NEWS_QUERY_PRESETS.feed}
15
- columns={["time", "source", "title", "tickers", "categories"]}
16
- defaultSort={DEFAULT_SORT}
17
- emptyStateTitle="No feed stories yet"
18
- emptyStateHint="Try refreshing later as wire stories arrive."
19
- />
20
- );
21
- }
@@ -1,21 +0,0 @@
1
- import type { PaneProps } from "../../../types/plugin";
2
- import { NEWS_QUERY_PRESETS } from "./news-query-presets";
3
- import { NewsPresetPane } from "./news-preset-pane";
4
- import type { NewsSortPreference } from "./news-table";
5
-
6
- const DEFAULT_SORT: NewsSortPreference = { columnId: "importance", direction: "desc" };
7
-
8
- export function TopPane(props: PaneProps) {
9
- return (
10
- <NewsPresetPane
11
- {...props}
12
- paneKey="top"
13
- title="Top News"
14
- query={NEWS_QUERY_PRESETS.top}
15
- columns={["rank", "time", "source", "title", "tickers", "importance"]}
16
- defaultSort={DEFAULT_SORT}
17
- emptyStateTitle="No top stories yet"
18
- emptyStateHint="Try refreshing later as new headlines are ranked."
19
- />
20
- );
21
- }