gloomberb 0.7.2 → 0.7.3

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 (129) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +49 -15
  7. package/src/components/chart/chart-types.ts +2 -2
  8. package/src/components/chart/chart.test.ts +27 -8
  9. package/src/components/chart/comparison-stock-chart.tsx +84 -121
  10. package/src/components/chart/native/chart-rasterizer.ts +43 -17
  11. package/src/components/chart/price-axis-labels.tsx +25 -9
  12. package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
  13. package/src/components/chart/stock-chart.tsx +59 -41
  14. package/src/components/command-bar/command-bar.test.tsx +446 -120
  15. package/src/components/command-bar/command-bar.tsx +528 -383
  16. package/src/components/command-bar/command-registry.ts +16 -6
  17. package/src/components/command-bar/root-shortcuts.ts +4 -5
  18. package/src/components/command-bar/view-model.test.ts +1 -0
  19. package/src/components/command-bar/view-model.ts +2 -1
  20. package/src/components/command-bar/workflow-ops.ts +11 -32
  21. package/src/components/data-table-view.test.tsx +24 -1
  22. package/src/components/data-table-view.tsx +3 -2
  23. package/src/components/feed-data-table-stack-view.tsx +3 -2
  24. package/src/components/layout/detached-pane-shell.tsx +2 -2
  25. package/src/components/layout/floating-pane.tsx +9 -2
  26. package/src/components/layout/pane-footer.tsx +1 -1
  27. package/src/components/layout/pane-header.tsx +4 -2
  28. package/src/components/layout/pane-sizing.ts +12 -0
  29. package/src/components/layout/pane.tsx +24 -2
  30. package/src/components/layout/shell.tsx +24 -10
  31. package/src/components/layout/status-bar.test.tsx +4 -1
  32. package/src/components/layout/status-bar.tsx +4 -24
  33. package/src/components/markdown-text.tsx +53 -8
  34. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  35. package/src/components/pane-settings-dialog.tsx +9 -6
  36. package/src/components/pane-template-wizard.tsx +3 -2
  37. package/src/components/price-selector-dialog.tsx +3 -2
  38. package/src/components/price-sparkline-view.tsx +463 -0
  39. package/src/components/price-sparkline.ts +46 -0
  40. package/src/components/table-view-shared.tsx +21 -3
  41. package/src/components/ticker-list-table-view.tsx +3 -2
  42. package/src/components/ticker-list-table.tsx +11 -51
  43. package/src/components/toggle-list.tsx +3 -3
  44. package/src/components/ui/choice-dialog.tsx +3 -2
  45. package/src/components/ui/multi-select-dialog.tsx +3 -2
  46. package/src/components/ui/native-select.tsx +3 -3
  47. package/src/core/state/app-state.ts +210 -80
  48. package/src/data/config-store-node.ts +113 -25
  49. package/src/data/config-store.test.ts +65 -3
  50. package/src/data/plugin-state-store.ts +39 -13
  51. package/src/data/sqlite-cache.test.ts +13 -0
  52. package/src/market-data/coordinator.ts +8 -1
  53. package/src/news/aggregator.test.ts +41 -0
  54. package/src/news/aggregator.ts +61 -9
  55. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  56. package/src/plugins/builtin/ai/settings.ts +3 -0
  57. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  58. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  59. package/src/plugins/builtin/chat-controller.ts +258 -44
  60. package/src/plugins/builtin/chat.test.tsx +257 -6
  61. package/src/plugins/builtin/chat.tsx +225 -60
  62. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  63. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  64. package/src/plugins/builtin/correlation/colors.ts +87 -0
  65. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  66. package/src/plugins/builtin/correlation/index.tsx +92 -21
  67. package/src/plugins/builtin/debug.tsx +4 -3
  68. package/src/plugins/builtin/econ/index.tsx +3 -2
  69. package/src/plugins/builtin/help.test.tsx +2 -2
  70. package/src/plugins/builtin/holders/index.tsx +25 -13
  71. package/src/plugins/builtin/layout-manager.tsx +1 -7
  72. package/src/plugins/builtin/news-wire/index.tsx +1 -1
  73. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  74. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  75. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  76. package/src/plugins/builtin/options.tsx +9 -17
  77. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  78. package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
  79. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  80. package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
  81. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  82. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  83. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  84. package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
  85. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  86. package/src/plugins/builtin/sectors/index.tsx +252 -41
  87. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  88. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  89. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  90. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  91. package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
  92. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
  93. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  94. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  95. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  96. package/src/plugins/plugin-runtime.tsx +2 -2
  97. package/src/plugins/prediction-markets/settings.ts +4 -0
  98. package/src/plugins/registry.test.ts +55 -0
  99. package/src/plugins/registry.ts +11 -4
  100. package/src/plugins/ticker-navigation.test.ts +35 -5
  101. package/src/plugins/ticker-navigation.ts +16 -22
  102. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  103. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  104. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  105. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  106. package/src/renderers/electrobun/bun/index.ts +52 -13
  107. package/src/renderers/electrobun/view/app-services.ts +51 -9
  108. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  109. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  110. package/src/renderers/electrobun/view/main.tsx +0 -16
  111. package/src/renderers/electrobun/view/styles.css +23 -21
  112. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  113. package/src/renderers/opentui/host.tsx +2 -1
  114. package/src/renderers/opentui/start.tsx +1 -1
  115. package/src/renderers/opentui/test-utils.tsx +6 -5
  116. package/src/state/app-bootstrap.ts +3 -3
  117. package/src/state/app-context.test.ts +113 -0
  118. package/src/state/app-context.tsx +8 -4
  119. package/src/theme/colors.ts +13 -14
  120. package/src/types/config.ts +9 -4
  121. package/src/types/plugin.ts +8 -1
  122. package/src/ui/context-menu.tsx +4 -4
  123. package/src/utils/api-client.test.ts +128 -0
  124. package/src/utils/api-client.ts +112 -7
  125. package/src/utils/fuzzy-search.ts +12 -1
  126. package/src/utils/keyboard.ts +31 -0
  127. package/src/utils/ticker-search.test.ts +2 -2
  128. package/src/utils/ticker-search.ts +48 -1
  129. package/src/version.ts +1 -1
@@ -10,16 +10,15 @@ import {
10
10
  import { createAppServices, type AppServices } from "../../../core/app-services";
11
11
  import { getDataDir, initDataDir, saveConfig, resetAllData, exportConfig, importConfig, setConfigStoreHost } from "../../../data/config-store";
12
12
  import * as nodeConfigStoreHost from "../../../data/config-store-node";
13
- import { cloneLayout, findPaneInstance, type AppConfig } from "../../../types/config";
13
+ import { findPaneInstance, type AppConfig } from "../../../types/config";
14
14
  import type { AppSessionSnapshot } from "../../../core/state/session-persistence";
15
- import type { PaneRuntimeState } from "../../../core/state/app-state";
15
+ import { syncConfigActiveLayoutState, type PaneRuntimeState } from "../../../core/state/app-state";
16
16
  import type { DesktopDockPreviewState, DesktopSharedStateSnapshot, DesktopThemePreviewState } from "../../../types/desktop-window";
17
17
  import type { ReleaseInfo, UpdateCheckResult, UpdateProgress } from "../../../updater";
18
18
  import { buildSoundCommand } from "../../../notifications/app-notifier";
19
19
  import { isPaneDetached } from "../../../plugins/pane-manager";
20
20
  import { ELECTROBUN_CONTEXT_MENU_ACTION, type ElectrobunDesktopRpcSchema } from "../shared/protocol";
21
21
  import { decodeRpcValue, encodeRpcValue } from "../view/rpc-codec";
22
- import { startMainThreadMonitor } from "../../../utils/main-thread-monitor";
23
22
  import { contextMenuSelectionMessage } from "./context-menu-click";
24
23
  import { getContextMenuRequestId, normalizeContextMenuItems } from "./context-menu-normalize";
25
24
  import { createDesktopWorkspace, type DesktopWorkspace } from "./desktop-workspace";
@@ -56,7 +55,6 @@ console.log = (...args) => console.error(...args);
56
55
  console.info = (...args) => console.error(...args);
57
56
  console.warn = (...args) => console.error(...args);
58
57
 
59
- startMainThreadMonitor("electrobun.bun", { mirrorToConsole: true });
60
58
  setConfigStoreHost(nodeConfigStoreHost);
61
59
  setNativeIbkrGatewayModuleLoader(() => import("../../../plugins/ibkr/gateway-service-native"));
62
60
 
@@ -136,17 +134,21 @@ function forEachReadyWindowRpc(callback: (rpc: DesktopRpc) => void): void {
136
134
  }
137
135
  }
138
136
 
137
+ function openMainWindowDevTools(): void {
138
+ mainWindow?.webview.openDevTools();
139
+ }
140
+
139
141
  function scopeClientId(rpc: DesktopRpc, id: string): string {
140
142
  return `${getRpcWindowKey(rpc) ?? "window"}:${id}`;
141
143
  }
142
144
 
143
- function syncActiveLayout(config: AppConfig): AppConfig {
144
- return {
145
- ...config,
146
- layouts: config.layouts.map((entry, index) => (
147
- index === config.activeLayoutIndex ? { ...entry, layout: cloneLayout(config.layout) } : entry
148
- )),
149
- };
145
+ function syncActiveLayout(
146
+ config: AppConfig,
147
+ paneState: Record<string, PaneRuntimeState> = config.layouts[config.activeLayoutIndex]?.paneState ?? {},
148
+ focusedPaneId: string | null = config.layouts[config.activeLayoutIndex]?.focusedPaneId ?? null,
149
+ activePanel: "left" | "right" = config.layouts[config.activeLayoutIndex]?.activePanel ?? "left",
150
+ ): AppConfig {
151
+ return syncConfigActiveLayoutState(config, paneState, focusedPaneId, activePanel);
150
152
  }
151
153
 
152
154
  function normalizeInitWindowTarget(
@@ -416,6 +418,25 @@ async function runDesktopUpdate(rpc: DesktopRpc, currentVersion: string): Promis
416
418
  }
417
419
  }
418
420
 
421
+ interface PluginStateBackendSetEntry {
422
+ pluginId: string;
423
+ key: string;
424
+ value: unknown;
425
+ schemaVersion?: number;
426
+ }
427
+
428
+ function normalizePluginStateSetEntry(entry: unknown): PluginStateBackendSetEntry | null {
429
+ if (!entry || typeof entry !== "object") return null;
430
+ const record = entry as Record<string, unknown>;
431
+ if (typeof record.pluginId !== "string" || typeof record.key !== "string") return null;
432
+ return {
433
+ pluginId: record.pluginId,
434
+ key: record.key,
435
+ value: record.value,
436
+ schemaVersion: typeof record.schemaVersion === "number" ? record.schemaVersion : undefined,
437
+ };
438
+ }
439
+
419
440
  function syncBackendCloudAuthState(pluginId: string, key: string, value: unknown): void {
420
441
  if (pluginId !== "gloomberb-cloud" || (key !== "session" && key !== "resume:session")) return;
421
442
 
@@ -670,7 +691,7 @@ async function commitDesktopSnapshot(
670
691
  snapshot: DesktopSharedStateSnapshot,
671
692
  options: { persistConfig?: boolean; reconcileWindows?: boolean } = {},
672
693
  ): Promise<DesktopSharedStateSnapshot> {
673
- const nextConfig = syncActiveLayout(snapshot.config);
694
+ const nextConfig = syncActiveLayout(snapshot.config, snapshot.paneState, snapshot.focusedPaneId, snapshot.activePanel);
674
695
  setCurrentConfig(nextConfig);
675
696
  desktopWorkspace = requireDesktopWorkspace();
676
697
  desktopWorkspace.replaceConfig(nextConfig, { layoutChanged: snapshot.layoutChanged });
@@ -1049,6 +1070,19 @@ async function handleBackendRequest(
1049
1070
  requireServices().persistence.pluginState.set(payload.pluginId as string, payload.key as string, payload.value, payload.schemaVersion as number | undefined);
1050
1071
  syncBackendCloudAuthState(payload.pluginId as string, payload.key as string, payload.value);
1051
1072
  return null;
1073
+ case "pluginState.setMany": {
1074
+ const entries = Array.isArray(payload.entries)
1075
+ ? payload.entries.flatMap((entry) => {
1076
+ const normalized = normalizePluginStateSetEntry(entry);
1077
+ return normalized ? [normalized] : [];
1078
+ })
1079
+ : [];
1080
+ requireServices().persistence.pluginState.setMany(entries);
1081
+ for (const entry of entries) {
1082
+ syncBackendCloudAuthState(entry.pluginId, entry.key, entry.value);
1083
+ }
1084
+ return null;
1085
+ }
1052
1086
  case "pluginState.delete":
1053
1087
  requireServices().persistence.pluginState.delete(payload.pluginId as string, payload.key as string);
1054
1088
  syncBackendCloudAuthState(payload.pluginId as string, payload.key as string, null);
@@ -1142,7 +1176,12 @@ Electrobun.events.on("context-menu-clicked", (event: unknown) => {
1142
1176
 
1143
1177
  ApplicationMenu.on("application-menu-clicked", (event: unknown) => {
1144
1178
  const command = applicationMenuCommand(event);
1145
- if (!command || !readyWindowRpcs.has(MAIN_WINDOW_RPC_KEY)) return;
1179
+ if (!command) return;
1180
+ if (command.type === "open-devtools") {
1181
+ openMainWindowDevTools();
1182
+ return;
1183
+ }
1184
+ if (!readyWindowRpcs.has(MAIN_WINDOW_RPC_KEY)) return;
1146
1185
  windowRpcs.get(MAIN_WINDOW_RPC_KEY)?.send["application-menu.select"]({ command });
1147
1186
  });
1148
1187
 
@@ -24,6 +24,7 @@ import { getRendererBuiltinPlugins } from "../../../plugins/catalog-ui";
24
24
  const servicesLog = debugLog.createLogger("services");
25
25
  const ASSET_DATA_CAPABILITY_ID = "asset-data.asset-data-router";
26
26
  const NEWS_CAPABILITY_ID = "news.core";
27
+ const PLUGIN_STATE_BACKEND_FLUSH_DELAY_MS = 25;
27
28
 
28
29
  class RemoteTickerRepository {
29
30
  async loadAllTickers(): Promise<TickerRecord[]> {
@@ -274,14 +275,19 @@ class RemoteSessionStore {
274
275
  }
275
276
  }
276
277
 
278
+ interface PluginStatePersistEntry {
279
+ pluginId: string;
280
+ key: string;
281
+ value: unknown;
282
+ schemaVersion: number;
283
+ }
284
+
277
285
  class RemotePluginStateStore {
278
286
  private readonly state = new Map<string, Map<string, unknown>>();
279
- private readonly schedulers = new Map<string, ReturnType<typeof createPersistScheduler<{
280
- pluginId: string;
281
- key: string;
282
- value: unknown;
283
- schemaVersion: number;
284
- }>>>();
287
+ private readonly schedulers = new Map<string, ReturnType<typeof createPersistScheduler<PluginStatePersistEntry>>>();
288
+ private readonly pendingBackendSaves = new Map<string, PluginStatePersistEntry>();
289
+ private backendSaveTimer: ReturnType<typeof setTimeout> | null = null;
290
+ private backendSaveInFlight: Promise<void> = Promise.resolve();
285
291
 
286
292
  constructor(initial: Record<string, Record<string, unknown>>) {
287
293
  for (const [pluginId, values] of Object.entries(initial)) {
@@ -304,7 +310,11 @@ class RemotePluginStateStore {
304
310
  delete(pluginId: string, key: string): void {
305
311
  this.state.get(pluginId)?.delete(key);
306
312
  this.getScheduler(pluginId, key).cancel();
307
- void backendRequest("pluginState.delete", { pluginId, key }).catch(() => {});
313
+ this.pendingBackendSaves.delete(this.schedulerKey(pluginId, key));
314
+ void this.backendSaveInFlight
315
+ .catch(() => {})
316
+ .then(() => backendRequest("pluginState.delete", { pluginId, key }))
317
+ .catch(() => {});
308
318
  }
309
319
 
310
320
  keys(pluginId: string): string[] {
@@ -317,20 +327,52 @@ class RemotePluginStateStore {
317
327
 
318
328
  async flush(): Promise<void> {
319
329
  await Promise.all([...this.schedulers.values()].map((scheduler) => scheduler.flush()));
330
+ await this.flushBackendSaves();
320
331
  }
321
332
 
322
333
  private getScheduler(pluginId: string, key: string) {
323
- const schedulerKey = `${pluginId}\u0000${key}`;
334
+ const schedulerKey = this.schedulerKey(pluginId, key);
324
335
  let scheduler = this.schedulers.get(schedulerKey);
325
336
  if (!scheduler) {
326
337
  scheduler = createPersistScheduler({
327
338
  delayMs: PLUGIN_STATE_SAVE_DEBOUNCE_MS,
328
- save: (entry) => backendRequest("pluginState.set", entry),
339
+ save: (entry) => {
340
+ this.scheduleBackendSave(entry);
341
+ },
329
342
  });
330
343
  this.schedulers.set(schedulerKey, scheduler);
331
344
  }
332
345
  return scheduler;
333
346
  }
347
+
348
+ private schedulerKey(pluginId: string, key: string): string {
349
+ return `${pluginId}\u0000${key}`;
350
+ }
351
+
352
+ private scheduleBackendSave(entry: PluginStatePersistEntry): void {
353
+ this.pendingBackendSaves.set(this.schedulerKey(entry.pluginId, entry.key), entry);
354
+ if (this.backendSaveTimer) return;
355
+ this.backendSaveTimer = setTimeout(() => {
356
+ void this.flushBackendSaves();
357
+ }, PLUGIN_STATE_BACKEND_FLUSH_DELAY_MS);
358
+ }
359
+
360
+ private async flushBackendSaves(): Promise<void> {
361
+ if (this.backendSaveTimer) {
362
+ clearTimeout(this.backendSaveTimer);
363
+ this.backendSaveTimer = null;
364
+ }
365
+ if (this.pendingBackendSaves.size === 0) return this.backendSaveInFlight;
366
+
367
+ const entries = [...this.pendingBackendSaves.values()];
368
+ this.pendingBackendSaves.clear();
369
+ const save = this.backendSaveInFlight
370
+ .catch(() => {})
371
+ .then(() => backendRequest<void>("pluginState.setMany", { entries }))
372
+ .catch(() => {});
373
+ this.backendSaveInFlight = save;
374
+ return save;
375
+ }
334
376
  }
335
377
 
336
378
  class RemotePersistence {
@@ -1,6 +1,5 @@
1
1
  /// <reference lib="dom" />
2
2
  import { Electroview } from "electrobun/view";
3
- import { measurePerfAsync } from "../../../utils/perf-marks";
4
3
  import {
5
4
  type ApplicationMenuSelectMessage,
6
5
  type CapabilityEventMessage,
@@ -119,13 +118,11 @@ async function waitForBridgeReady(): Promise<void> {
119
118
  }
120
119
 
121
120
  export async function backendRequest<T = unknown>(method: string, payload: unknown = null): Promise<T> {
122
- const result = await measurePerfAsync("electrobun.rpc.request", async () => {
123
- await waitForBridgeReady();
124
- return rpc.request["backend.request"]({
125
- method,
126
- payload: encodeRpcValue(payload),
127
- });
128
- }, { method });
121
+ await waitForBridgeReady();
122
+ const result = await rpc.request["backend.request"]({
123
+ method,
124
+ payload: encodeRpcValue(payload),
125
+ });
129
126
  return decodeRpcValue<T>(result);
130
127
  }
131
128
 
@@ -40,8 +40,8 @@ function controlBorderColor(focused = false, active = false): string {
40
40
 
41
41
  function controlShadow(active = false): string {
42
42
  return active
43
- ? "0 0 0 1px rgba(84, 201, 159, 0.18), inset 0 1px 0 rgba(255,255,255,0.06)"
44
- : "inset 0 1px 0 rgba(255,255,255,0.04)";
43
+ ? `0 0 0 1px ${blendHex(colors.bg, colors.borderFocused, 0.18)}, inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.06)}`
44
+ : `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.04)}`;
45
45
  }
46
46
 
47
47
  function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled">) {
@@ -76,7 +76,7 @@ function buttonPalette(props: Pick<ButtonProps, "variant" | "active" | "disabled
76
76
  case "ghost":
77
77
  return {
78
78
  fg: colors.textDim,
79
- bg: "rgba(0, 0, 0, 0)",
79
+ bg: "transparent",
80
80
  border: panelBorder(),
81
81
  };
82
82
  case "secondary":
@@ -339,7 +339,7 @@ function listRowStyle(selected: boolean): CSSProperties {
339
339
  return {
340
340
  borderRadius: CONTROL_RADIUS,
341
341
  border: `1px solid ${selected ? colors.borderFocused : "transparent"}`,
342
- boxShadow: selected ? "inset 0 1px 0 rgba(255,255,255,0.06)" : undefined,
342
+ boxShadow: selected ? `inset 0 1px 0 ${blendHex(colors.bg, colors.textBright, 0.06)}` : undefined,
343
343
  cursor: "pointer",
344
344
  };
345
345
  }
@@ -5,7 +5,6 @@ import { App } from "../../../app";
5
5
  import { UiHostProvider } from "../../../ui/host";
6
6
  import { debugLog } from "../../../utils/debug-log";
7
7
  import { measurePerfAsync } from "../../../utils/perf-marks";
8
- import { startMainThreadMonitor } from "../../../utils/main-thread-monitor";
9
8
  import { backendRequest, initElectrobunBackend } from "./backend-rpc";
10
9
  import { installElectrobunAiHost } from "./ai-host";
11
10
  import { installElectrobunBrokerRemoteClient } from "./broker-remote-client";
@@ -31,23 +30,9 @@ if (!rootElement) {
31
30
 
32
31
  const root = createRoot(rootElement);
33
32
  const bootLog = debugLog.createLogger("electrobun-web-boot");
34
- const ELECTROBUN_WEB_CONSOLE_LOG_SOURCES = [
35
- "app",
36
- "main-thread",
37
- "perf",
38
- "refresh-queue",
39
- "services",
40
- "startup",
41
- "electrobun-web-boot",
42
- ];
43
- debugLog.mirrorToConsole({
44
- sources: ELECTROBUN_WEB_CONSOLE_LOG_SOURCES,
45
- });
46
- const stopMainThreadMonitor = startMainThreadMonitor("electrobun.web", { mirrorToConsole: true });
47
33
 
48
34
  rootElement.tabIndex = -1;
49
35
  root.render(<div className="gloom-loading">Starting Gloomberb...</div>);
50
- bootLog.warn("diagnostic console mirror enabled", { sources: ELECTROBUN_WEB_CONSOLE_LOG_SOURCES });
51
36
 
52
37
  function focusWebSurface(): void {
53
38
  window.focus();
@@ -111,7 +96,6 @@ async function boot() {
111
96
  }
112
97
 
113
98
  boot().catch((error) => {
114
- stopMainThreadMonitor();
115
99
  root.render(
116
100
  <div className="gloom-fatal">
117
101
  <h1>Gloomberb failed to start</h1>
@@ -1,5 +1,5 @@
1
1
  @keyframes gloom-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
2
- html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: #000; }
2
+ html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: var(--gloom-bg); }
3
3
  #root:focus { outline: none; }
4
4
  body {
5
5
  --cell-w: 8px;
@@ -8,7 +8,7 @@ body {
8
8
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
9
9
  font-size: 12px;
10
10
  line-height: var(--cell-h);
11
- color: #d8dde3;
11
+ color: var(--gloom-text);
12
12
  cursor: default;
13
13
  user-select: none;
14
14
  color-scheme: dark;
@@ -62,26 +62,26 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
62
62
  border: 0;
63
63
  border-radius: 6px;
64
64
  overflow: hidden;
65
- box-shadow: 0 18px 38px rgba(0, 0, 0, .46);
65
+ box-shadow: 0 18px 38px color-mix(in srgb, var(--gloom-bg) 46%, transparent);
66
66
  }
67
67
  [data-gloom-role="pane-window"][data-floating="true"]::after {
68
68
  content: "";
69
69
  position: absolute;
70
70
  inset: 0;
71
- border: 1px solid var(--pane-border-color, #3a4148);
71
+ border: 1px solid var(--pane-border-color, var(--gloom-border));
72
72
  border-radius: inherit;
73
73
  pointer-events: none;
74
74
  z-index: 100;
75
75
  }
76
76
  [data-gloom-role="pane-window"][data-floating="true"][data-focused="true"] {
77
- box-shadow: 0 18px 40px rgba(0, 0, 0, .5);
77
+ box-shadow: 0 18px 40px color-mix(in srgb, var(--gloom-bg) 50%, transparent);
78
78
  }
79
79
  [data-gloom-role="pane-window"][data-floating="false"] {
80
80
  border: 0;
81
81
  border-radius: 0;
82
82
  }
83
83
  [data-gloom-role="pane-window"][data-floating="false"][data-focused="true"] {
84
- box-shadow: inset 0 0 0 1px var(--pane-border-color, #54c99f);
84
+ box-shadow: inset 0 0 0 1px var(--pane-border-color, var(--gloom-border-focused));
85
85
  }
86
86
  [data-gloom-role="pane-header"] {
87
87
  align-items: center;
@@ -125,7 +125,7 @@ body.gloom-dragging {
125
125
  min-height: var(--cell-h);
126
126
  max-height: var(--cell-h);
127
127
  align-items: center;
128
- border-top: 1px solid color-mix(in srgb, #3a4148 72%, transparent);
128
+ border-top: 1px solid color-mix(in srgb, var(--gloom-border) 72%, transparent);
129
129
  font-size: 11.5px;
130
130
  }
131
131
  [data-gloom-role="status-bar"] span {
@@ -137,7 +137,7 @@ body.gloom-dragging {
137
137
  min-height: var(--cell-h);
138
138
  max-height: var(--cell-h);
139
139
  align-items: center;
140
- border-top: 1px solid color-mix(in srgb, var(--pane-footer-border-color, #3a4148) 72%, transparent);
140
+ border-top: 1px solid color-mix(in srgb, var(--pane-footer-border-color, var(--gloom-border)) 72%, transparent);
141
141
  font-size: 11.5px;
142
142
  overflow: hidden;
143
143
  }
@@ -153,7 +153,7 @@ body.gloom-dragging {
153
153
  border-radius: 4px;
154
154
  }
155
155
  [data-gloom-role="pane-hint"]:hover {
156
- background: rgba(255,255,255,.08);
156
+ background: color-mix(in srgb, var(--gloom-text-bright) 8%, transparent);
157
157
  }
158
158
  [data-gloom-role="pane-action"],
159
159
  [data-gloom-role="pane-close"] {
@@ -162,7 +162,7 @@ body.gloom-dragging {
162
162
  }
163
163
  [data-gloom-role="pane-action"]:hover,
164
164
  [data-gloom-role="pane-close"]:hover {
165
- background: rgba(255,255,255,.08);
165
+ background: color-mix(in srgb, var(--gloom-text-bright) 8%, transparent);
166
166
  }
167
167
  [data-gloom-role="resize-handle"] {
168
168
  cursor: nwse-resize;
@@ -191,7 +191,7 @@ body.gloom-dragging {
191
191
  cursor: pointer;
192
192
  }
193
193
  [data-gloom-role="tab-close"]:hover {
194
- background: rgba(255,255,255,.1);
194
+ background: color-mix(in srgb, var(--gloom-text-bright) 10%, transparent);
195
195
  }
196
196
  [data-gloom-scrollbar-x],
197
197
  [data-gloom-scrollbar-y] {
@@ -199,7 +199,7 @@ body.gloom-dragging {
199
199
  scrollbar-width: thin;
200
200
  }
201
201
  [data-gloom-scrollbar-active="true"] {
202
- scrollbar-color: rgba(216,221,227,.26) transparent;
202
+ scrollbar-color: color-mix(in srgb, var(--gloom-text) 26%, transparent) transparent;
203
203
  }
204
204
  [data-gloom-scrollbar-x]::-webkit-scrollbar,
205
205
  [data-gloom-scrollbar-y]::-webkit-scrollbar {
@@ -227,7 +227,7 @@ body.gloom-dragging {
227
227
  }
228
228
  [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb,
229
229
  [data-gloom-scrollbar-active="true"]::-webkit-scrollbar-thumb:hover {
230
- background: rgba(216,221,227,.24);
230
+ background: color-mix(in srgb, var(--gloom-text) 24%, transparent);
231
231
  background-clip: content-box;
232
232
  }
233
233
  [data-gloom-scrollbar-x]::-webkit-scrollbar-corner,
@@ -284,8 +284,8 @@ body.gloom-dragging {
284
284
  top: 50%;
285
285
  height: 1px;
286
286
  }
287
- .gloom-loading, .gloom-fatal { padding: 24px; color: #d8dde3; }
288
- .gloom-fatal pre { white-space: pre-wrap; color: #ffb4a8; }
287
+ .gloom-loading, .gloom-fatal { padding: 24px; color: var(--gloom-text); }
288
+ .gloom-fatal pre { white-space: pre-wrap; color: var(--gloom-negative); }
289
289
  .gloom-toast-viewport {
290
290
  position: fixed;
291
291
  right: 16px;
@@ -300,7 +300,7 @@ body.gloom-dragging {
300
300
  max-width: 420px;
301
301
  padding: 10px 12px;
302
302
  border: 1px solid;
303
- box-shadow: 0 12px 24px rgba(0,0,0,.28);
303
+ box-shadow: 0 12px 24px color-mix(in srgb, var(--gloom-bg) 28%, transparent);
304
304
  }
305
305
  .gloom-toast-action {
306
306
  margin-top: 8px;
@@ -313,17 +313,19 @@ body.gloom-dragging {
313
313
  display: flex;
314
314
  align-items: center;
315
315
  justify-content: center;
316
- background: rgba(0,0,0,.64);
316
+ background: color-mix(in srgb, var(--gloom-bg) 64%, transparent);
317
317
  z-index: 9999;
318
318
  }
319
319
  .gloom-dialog {
320
320
  max-width: min(920px, calc(100vw - 48px));
321
321
  max-height: calc(100vh - 48px);
322
322
  overflow: auto;
323
- border: 1px solid #3a4148;
323
+ border: 1px solid var(--gloom-border);
324
324
  border-radius: 6px;
325
- background: #101417;
326
- color: #d8dde3;
325
+ background: var(--gloom-panel);
326
+ color: var(--gloom-text);
327
327
  padding: 0;
328
- box-shadow: 0 18px 48px rgba(0,0,0,.46), inset 0 1px 0 rgba(255,255,255,.05);
328
+ box-shadow:
329
+ 0 18px 48px color-mix(in srgb, var(--gloom-bg) 46%, transparent),
330
+ inset 0 1px 0 color-mix(in srgb, var(--gloom-text-bright) 5%, transparent);
329
331
  }
@@ -129,7 +129,7 @@ function commonStyle(props: Record<string, unknown>): CSSProperties {
129
129
  marginBottom: cellInset(props.marginBottom ?? props.marginY ?? props.margin, "y"),
130
130
  gap: typeof props.gap === "number" ? `${props.gap * gapUnit}px` : props.gap as CSSProperties["gap"],
131
131
  overflow: props.overflow as CSSProperties["overflow"],
132
- border: props.border ? `1px solid ${typeof props.borderColor === "string" ? props.borderColor : "#3a4148"}` : undefined,
132
+ border: props.border ? `1px solid ${typeof props.borderColor === "string" ? props.borderColor : "var(--gloom-border)"}` : undefined,
133
133
  boxSizing: "border-box",
134
134
  minInlineSize: zeroMinInlineSize ? 0 : undefined,
135
135
  minBlockSize: zeroMinBlockSize ? 0 : undefined,
@@ -551,7 +551,7 @@ function ChartCrosshair({
551
551
  height: 7,
552
552
  borderRadius: 7,
553
553
  border: `1px solid ${crosshair.color}`,
554
- backgroundColor: "rgba(255, 255, 255, 0.16)",
554
+ backgroundColor: `color-mix(in srgb, ${crosshair.color} 16%, transparent)`,
555
555
  boxSizing: "border-box",
556
556
  transform: "translate(-50%, -50%)",
557
557
  pointerEvents: "none",
@@ -610,7 +610,7 @@ function textInputStyle(props: Record<string, unknown>, multiline: boolean): CSS
610
610
  resize: "none",
611
611
  border: "none",
612
612
  outline: "none",
613
- color: typeof textColor === "string" ? textColor : "#d8dde3",
613
+ color: typeof textColor === "string" ? textColor : "var(--gloom-text)",
614
614
  backgroundColor: typeof backgroundColor === "string" ? backgroundColor : "transparent",
615
615
  whiteSpace: multiline && props.wrapText ? "pre-wrap" : "pre",
616
616
  overflow: multiline ? "auto" : "hidden",
@@ -1209,7 +1209,9 @@ function WebTabs({
1209
1209
 
1210
1210
  const resolveTabBackground = (active: boolean, hovered: boolean) => {
1211
1211
  if (active && variant === "pill") {
1212
- return hovered ? "rgba(84, 201, 159, 0.24)" : palette.activeBg;
1212
+ return hovered
1213
+ ? `color-mix(in srgb, ${palette.activeBg} 76%, ${palette.hoverBg})`
1214
+ : palette.activeBg;
1213
1215
  }
1214
1216
  return hovered ? palette.hoverBg : "transparent";
1215
1217
  };
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
4
4
  import { resetTerminalInputState } from "../../utils/terminal-input-reset";
5
5
  import type { KeyEventLike } from "../../react/input";
6
6
  import type { NativeRendererHost, RendererHost } from "../../ui/host";
7
+ import { colors } from "../../theme/colors";
7
8
 
8
9
  export { useKeyboard, useRenderer, useTerminalDimensions };
9
10
  export type { CliRenderer };
@@ -54,7 +55,7 @@ export async function createOpenTuiHost(): Promise<OpenTuiHost> {
54
55
 
55
56
  const renderer = await createCliRenderer({
56
57
  exitOnCtrlC: true,
57
- backgroundColor: "#000000",
58
+ backgroundColor: colors.bg,
58
59
  enableMouseMovement: true,
59
60
  });
60
61
  const root = createRoot(renderer);
@@ -66,7 +66,7 @@ export async function startOpenTuiApp(): Promise<void> {
66
66
  <OpenTuiDialogHostProvider
67
67
  size="medium"
68
68
  dialogOptions={{ style: { backgroundColor: colors.bg, borderColor: colors.borderFocused, borderStyle: "single", paddingX: 2, paddingY: 1 } }}
69
- backdropColor="#000000"
69
+ backdropColor={colors.bg}
70
70
  backdropOpacity={0.8}
71
71
  >
72
72
  <App
@@ -4,6 +4,7 @@ import { testRender as openTuiTestRender } from "@opentui/react/test-utils";
4
4
  import { createRoot as openTuiCreateRoot, useRenderer } from "@opentui/react";
5
5
  import { UiHostProvider, type NativeRendererHost, type RendererHost } from "../../ui";
6
6
  import { ToastHostProvider } from "../../ui/toast";
7
+ import { colors } from "../../theme/colors";
7
8
  import { OpenTuiInputHostProvider } from "./input-host";
8
9
  import { openTuiUiHost } from "./ui-host";
9
10
  import { OpenTuiDialogHostProvider } from "./dialog-host";
@@ -14,8 +15,8 @@ export function TestDialogProvider({ children }: { children: ReactNode }) {
14
15
  <DialogProvider
15
16
  dialogOptions={{
16
17
  style: {
17
- backgroundColor: "#000000",
18
- borderColor: "#ffffff",
18
+ backgroundColor: colors.bg,
19
+ borderColor: colors.borderFocused,
19
20
  borderStyle: "single",
20
21
  },
21
22
  }}
@@ -54,9 +55,9 @@ export function OpenTuiTestProviders({ children }: { children: ReactNode }) {
54
55
  <OpenTuiInputHostProvider>
55
56
  <ToastHostProvider host={openTuiToastHost}>
56
57
  <OpenTuiDialogHostProvider
57
- backgroundColor="#000000"
58
- containerBorderColor="#555555"
59
- focusedBorderColor="#9cdcfe"
58
+ backgroundColor={colors.bg}
59
+ containerBorderColor={colors.border}
60
+ focusedBorderColor={colors.borderFocused}
60
61
  >
61
62
  {children}
62
63
  </OpenTuiDialogHostProvider>
@@ -37,11 +37,11 @@ interface RefreshPlanEntry {
37
37
  }
38
38
 
39
39
  const MAX_BACKGROUND_WARMUP_TICKERS = 12;
40
- const PORTFOLIO_FINANCIAL_COLUMN_IDS = new Set([
41
- "market_cap",
40
+ const PORTFOLIO_FULL_SNAPSHOT_COLUMN_IDS = new Set([
42
41
  "pe",
43
42
  "forward_pe",
44
43
  "dividend_yield",
44
+ "sparkline",
45
45
  ]);
46
46
  const startupLog = debugLog.createLogger("startup");
47
47
 
@@ -157,7 +157,7 @@ function buildRefreshPlan(
157
157
  const columnIds = Array.isArray(instance.settings?.columnIds)
158
158
  ? instance.settings.columnIds.filter((value): value is string => typeof value === "string")
159
159
  : [];
160
- return columnIds.some((columnId) => PORTFOLIO_FINANCIAL_COLUMN_IDS.has(columnId))
160
+ return columnIds.some((columnId) => PORTFOLIO_FULL_SNAPSHOT_COLUMN_IDS.has(columnId))
161
161
  ? "financials"
162
162
  : "quote";
163
163
  };