gloomberb 0.9.4 → 0.9.6

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 (138) hide show
  1. package/README.md +49 -8
  2. package/package.json +4 -3
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/global-shortcuts.test.tsx +20 -1
  6. package/src/app/global-shortcuts.ts +2 -0
  7. package/src/app/pane-runtime/index.tsx +11 -4
  8. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  9. package/src/app.tsx +41 -17
  10. package/src/cli/commands/remote.ts +206 -0
  11. package/src/cli/entry.ts +8 -1
  12. package/src/cli/index.ts +2 -0
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/components/chart/core/keyboard.ts +10 -1
  15. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  16. package/src/components/chart/native/bitmap-support.ts +55 -0
  17. package/src/components/chart/native/kitty/index.test.ts +47 -0
  18. package/src/components/chart/native/surface/visibility.ts +8 -1
  19. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  20. package/src/components/chart/static/chart/bitmap.ts +16 -9
  21. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  22. package/src/components/chart/static/chart/surface.tsx +4 -0
  23. package/src/components/chart/stock-chart/index.test.ts +7 -0
  24. package/src/components/command-bar/list/view.tsx +48 -1
  25. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  26. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  27. package/src/components/command-bar/surface/index.test.tsx +14 -0
  28. package/src/components/command-bar/theme-picker.tsx +91 -79
  29. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  30. package/src/components/data-table/view.tsx +0 -12
  31. package/src/components/index.ts +7 -0
  32. package/src/components/layout/floating-pane.tsx +3 -3
  33. package/src/components/layout/pane/footer/index.tsx +23 -0
  34. package/src/components/layout/pane/header.tsx +5 -0
  35. package/src/components/layout/pane/index.tsx +3 -3
  36. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  37. package/src/components/layout/shell/cursor-occlusion.ts +9 -1
  38. package/src/components/layout/shell/index.test.tsx +2 -0
  39. package/src/components/layout/shell/index.tsx +16 -4
  40. package/src/components/layout/shell/pane/actions.ts +21 -7
  41. package/src/components/layout/shell/pane/layers.tsx +2 -2
  42. package/src/components/layout/status-bar.tsx +5 -5
  43. package/src/components/metric-treemap/index.tsx +317 -0
  44. package/src/components/metric-treemap/layout.test.ts +77 -0
  45. package/src/components/metric-treemap/layout.ts +476 -0
  46. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  47. package/src/components/price-performance.tsx +1 -1
  48. package/src/components/price-sparkline/view.tsx +15 -14
  49. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  50. package/src/components/table-view-shared.tsx +0 -10
  51. package/src/components/ticker/list-table-view.tsx +1 -7
  52. package/src/components/ticker/list-table.test.tsx +0 -4
  53. package/src/components/toggle-list.tsx +10 -0
  54. package/src/components/ui/button.tsx +12 -0
  55. package/src/components/ui/data-table/index.tsx +51 -0
  56. package/src/components/ui/data-table/opentui/index.tsx +4 -10
  57. package/src/components/ui/data-table/types.ts +6 -4
  58. package/src/components/ui/fields.tsx +18 -0
  59. package/src/components/ui/list-view.tsx +84 -4
  60. package/src/components/ui/message-composer.tsx +22 -0
  61. package/src/components/ui/tabs.tsx +46 -2
  62. package/src/components/ui/ui.test.tsx +0 -9
  63. package/src/core/state/app/layout-reducer.ts +7 -1
  64. package/src/core/state/app/layout.ts +6 -0
  65. package/src/core/state/app/state.ts +23 -1
  66. package/src/core/state/app/types.ts +4 -2
  67. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  68. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  69. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  70. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  71. package/src/plugins/builtin/chat/channels.ts +5 -18
  72. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  73. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  74. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  75. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  76. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  77. package/src/plugins/builtin/chat/status-widget.tsx +2 -8
  78. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  79. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  80. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  81. package/src/plugins/builtin/debug/index.tsx +3 -4
  82. package/src/plugins/builtin/help/components.tsx +1 -1
  83. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  84. package/src/plugins/builtin/holders/types.ts +0 -34
  85. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  86. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  87. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  88. package/src/plugins/builtin/options/table.ts +5 -5
  89. package/src/plugins/builtin/plugin-groups.ts +2 -0
  90. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  91. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +41 -0
  92. package/src/plugins/builtin/portfolio-list/pane/index.tsx +78 -22
  93. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  94. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  95. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  96. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  97. package/src/remote/app-host.tsx +77 -0
  98. package/src/remote/client.ts +34 -0
  99. package/src/remote/command-bar.ts +98 -0
  100. package/src/remote/controller-utils.ts +111 -0
  101. package/src/remote/controller.test.ts +365 -0
  102. package/src/remote/controller.ts +434 -0
  103. package/src/remote/endpoint.ts +41 -0
  104. package/src/remote/json-patch.test.ts +31 -0
  105. package/src/remote/json-patch.ts +89 -0
  106. package/src/remote/layout-helpers.ts +62 -0
  107. package/src/remote/resources.ts +186 -0
  108. package/src/remote/revision.ts +18 -0
  109. package/src/remote/schema.ts +143 -0
  110. package/src/remote/semantic-helpers.ts +161 -0
  111. package/src/remote/semantic-tree.tsx +112 -0
  112. package/src/remote/server.test.ts +50 -0
  113. package/src/remote/server.ts +121 -0
  114. package/src/remote/types.ts +119 -0
  115. package/src/renderers/electrobun/bun/index.ts +55 -1
  116. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  117. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  118. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  119. package/src/renderers/electrobun/view/data-table/index.tsx +0 -8
  120. package/src/renderers/electrobun/view/data-table/row.tsx +2 -12
  121. package/src/renderers/electrobun/view/host/box.tsx +11 -1
  122. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  124. package/src/renderers/electrobun/view/host/style.ts +1 -0
  125. package/src/renderers/electrobun/view/main.tsx +5 -1
  126. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  127. package/src/renderers/electrobun/view/styles.css +7 -0
  128. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  129. package/src/renderers/opentui/chart-surface.tsx +15 -20
  130. package/src/renderers/opentui/native-smoke.ts +1 -0
  131. package/src/renderers/opentui/start.tsx +32 -0
  132. package/src/sources/yahoo-finance/http.ts +31 -4
  133. package/src/state/app/context/index.test.tsx +47 -0
  134. package/src/state/app/context/index.tsx +48 -18
  135. package/src/ui/host.tsx +1 -0
  136. package/src/ui/index.tsx +183 -7
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
@@ -0,0 +1,121 @@
1
+ import { randomUUID } from "crypto";
2
+ import { createServer, type IncomingMessage, type ServerResponse } from "http";
3
+ import type { RemoteAppKind, RemoteControlRequest, RemoteControlResponse, RemoteEndpoint } from "./types";
4
+ import { REMOTE_CONTROL_PROTOCOL_VERSION } from "./types";
5
+ import { removeRemoteEndpointFiles, writeRemoteEndpointFiles } from "./endpoint";
6
+
7
+ export interface RemoteControlServerOptions {
8
+ dataDir: string;
9
+ appKind: RemoteAppKind;
10
+ handle: (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
11
+ }
12
+
13
+ export interface RemoteControlServer {
14
+ endpoint: RemoteEndpoint;
15
+ close(): Promise<void>;
16
+ }
17
+
18
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
19
+
20
+ export async function startRemoteControlServer(options: RemoteControlServerOptions): Promise<RemoteControlServer> {
21
+ const token = randomUUID();
22
+ const httpServer = createServer((request, response) => {
23
+ void handleHttpRequest(options.handle, token, request, response);
24
+ });
25
+
26
+ await new Promise<void>((resolve, reject) => {
27
+ httpServer.once("error", reject);
28
+ httpServer.listen(0, "127.0.0.1", () => {
29
+ httpServer.off("error", reject);
30
+ resolve();
31
+ });
32
+ });
33
+
34
+ const address = httpServer.address();
35
+ if (!address || typeof address === "string") {
36
+ httpServer.close();
37
+ throw new Error("Remote control server did not bind to a TCP port.");
38
+ }
39
+
40
+ const endpoint: RemoteEndpoint = {
41
+ protocolVersion: REMOTE_CONTROL_PROTOCOL_VERSION,
42
+ appKind: options.appKind,
43
+ pid: process.pid,
44
+ port: address.port,
45
+ token,
46
+ startedAt: new Date().toISOString(),
47
+ };
48
+ await writeRemoteEndpointFiles(options.dataDir, endpoint);
49
+
50
+ return {
51
+ endpoint,
52
+ close: async () => {
53
+ await new Promise<void>((resolve, reject) => {
54
+ httpServer.close((error) => (error ? reject(error) : resolve()));
55
+ }).catch((error) => {
56
+ if ((error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING") throw error;
57
+ });
58
+ await removeRemoteEndpointFiles(options.dataDir, endpoint);
59
+ },
60
+ };
61
+ }
62
+
63
+ async function handleHttpRequest(
64
+ handle: RemoteControlServerOptions["handle"],
65
+ token: string,
66
+ request: IncomingMessage,
67
+ response: ServerResponse,
68
+ ): Promise<void> {
69
+ try {
70
+ if (request.method === "GET" && request.url === "/health") {
71
+ sendJson(response, 200, { ok: true });
72
+ return;
73
+ }
74
+ if (request.method !== "POST" || request.url !== "/rpc") {
75
+ sendJson(response, 404, { ok: false, error: { code: "not_found", message: "Unknown remote control endpoint." } });
76
+ return;
77
+ }
78
+ if (request.headers.authorization !== `Bearer ${token}`) {
79
+ sendJson(response, 401, { ok: false, error: { code: "unauthorized", message: "Invalid remote control token." } });
80
+ return;
81
+ }
82
+
83
+ const body = await readBody(request);
84
+ const remoteRequest = JSON.parse(body) as RemoteControlRequest;
85
+ sendJson(response, 200, await handle(remoteRequest));
86
+ } catch (error) {
87
+ sendJson(response, 500, {
88
+ ok: false,
89
+ error: {
90
+ code: "remote_transport_error",
91
+ message: error instanceof Error ? error.message : String(error),
92
+ },
93
+ });
94
+ }
95
+ }
96
+
97
+ async function readBody(request: IncomingMessage): Promise<string> {
98
+ let total = 0;
99
+ const chunks: Uint8Array[] = [];
100
+ for await (const chunk of request) {
101
+ const bytes = chunk instanceof Uint8Array ? new Uint8Array(chunk) : new TextEncoder().encode(String(chunk));
102
+ total += bytes.byteLength;
103
+ if (total > MAX_BODY_BYTES) throw new Error("Remote control request is too large.");
104
+ chunks.push(bytes);
105
+ }
106
+ const body = new Uint8Array(total);
107
+ let offset = 0;
108
+ for (const chunk of chunks) {
109
+ body.set(chunk, offset);
110
+ offset += chunk.byteLength;
111
+ }
112
+ return new TextDecoder().decode(body);
113
+ }
114
+
115
+ function sendJson(response: ServerResponse, status: number, body: unknown): void {
116
+ response.writeHead(status, {
117
+ "content-type": "application/json; charset=utf-8",
118
+ "cache-control": "no-store",
119
+ });
120
+ response.end(JSON.stringify(body));
121
+ }
@@ -0,0 +1,119 @@
1
+ export const REMOTE_CONTROL_PROTOCOL_VERSION = 1;
2
+
3
+ export type RemoteAppKind = "tui" | "desktop";
4
+
5
+ export interface RemoteEndpoint {
6
+ protocolVersion: number;
7
+ appKind: RemoteAppKind;
8
+ pid: number;
9
+ port: number;
10
+ token: string;
11
+ startedAt: string;
12
+ }
13
+
14
+ export interface RemoteJsonPatchOperation {
15
+ op: "add" | "replace" | "remove";
16
+ path: string;
17
+ value?: unknown;
18
+ }
19
+
20
+ export type RemoteStateInclude =
21
+ | "app"
22
+ | "layout"
23
+ | "panes"
24
+ | "commandBar"
25
+ | "commandBar.results"
26
+ | "ui"
27
+ | "schema"
28
+ | "help"
29
+ | "all";
30
+
31
+ export type RemoteControlRequest =
32
+ | { type: "help"; topic?: string }
33
+ | { type: "schema" }
34
+ | { type: "get"; resource: string; include?: RemoteStateInclude[] }
35
+ | { type: "call"; operation: string; input?: unknown; dryRun?: boolean; include?: RemoteStateInclude[] }
36
+ | {
37
+ type: "patch";
38
+ resource: string;
39
+ patch: RemoteJsonPatchOperation[];
40
+ expectRev?: string;
41
+ dryRun?: boolean;
42
+ include?: RemoteStateInclude[];
43
+ }
44
+ | {
45
+ type: "batch";
46
+ requests: RemoteControlRequest[];
47
+ dryRun?: boolean;
48
+ haltOnError?: boolean;
49
+ settle?: "none" | "afterEach" | "afterBatch";
50
+ include?: RemoteStateInclude[];
51
+ };
52
+
53
+ export interface RemoteIncludedState {
54
+ rev: string;
55
+ included: RemoteStateInclude[];
56
+ app?: unknown;
57
+ layout?: unknown;
58
+ panes?: unknown;
59
+ commandBar?: unknown;
60
+ ui?: RemoteUiNodeSnapshot[];
61
+ schema?: RemoteControlSchema;
62
+ help?: unknown;
63
+ }
64
+
65
+ export interface RemoteControlSuccess<T = unknown> {
66
+ ok: true;
67
+ data: T;
68
+ rev?: string;
69
+ state?: RemoteIncludedState;
70
+ warnings?: string[];
71
+ }
72
+
73
+ export interface RemoteControlFailure {
74
+ ok: false;
75
+ error: {
76
+ code: string;
77
+ message: string;
78
+ details?: unknown;
79
+ };
80
+ }
81
+
82
+ export type RemoteControlResponse<T = unknown> =
83
+ | RemoteControlSuccess<T>
84
+ | RemoteControlFailure;
85
+
86
+ export interface RemoteResourceSchema {
87
+ uri: string;
88
+ description: string;
89
+ patchable?: boolean;
90
+ }
91
+
92
+ export interface RemoteOperationSchema {
93
+ id: string;
94
+ description: string;
95
+ inputShape?: string;
96
+ outputShape?: string;
97
+ returns?: string;
98
+ recommendedNextReads?: string[];
99
+ examples?: unknown[];
100
+ sideEffectLevel: "none" | "local-write" | "network-write" | "external-side-effect" | "external-trade";
101
+ requiresConfirmation?: boolean;
102
+ dryRun?: boolean;
103
+ }
104
+
105
+ export interface RemoteControlSchema {
106
+ protocolVersion: number;
107
+ resources: RemoteResourceSchema[];
108
+ operations: RemoteOperationSchema[];
109
+ help: unknown;
110
+ }
111
+
112
+ export interface RemoteUiNodeSnapshot {
113
+ id: string;
114
+ role: string;
115
+ label?: string;
116
+ disabled?: boolean;
117
+ actions: string[];
118
+ metadata?: Record<string, unknown>;
119
+ }
@@ -55,6 +55,8 @@ import {
55
55
  desktopWindowStyleMask,
56
56
  } from "./desktop/window-style";
57
57
  import { applyDesktopWindowControl, type DesktopWindowControlAction } from "./desktop/window-controls";
58
+ import { startRemoteControlServer, type RemoteControlServer } from "../../../remote/server";
59
+ import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
58
60
 
59
61
  type DesktopRpc = ReturnType<typeof BrowserView.defineRPC<ElectrobunDesktopRpcSchema>>;
60
62
 
@@ -70,6 +72,7 @@ let services: AppServices | null = null;
70
72
  let mainWindow: BrowserWindow | null = null;
71
73
  let desktopWorkspace: DesktopWorkspace | null = null;
72
74
  let desktopRestartInProgress = false;
75
+ let desktopRemoteControlServer: RemoteControlServer | null = null;
73
76
 
74
77
  const windowRpcRegistry = createDesktopRpcRegistry<DesktopRpc>();
75
78
  const contextMenuRequestRpcs = new Map<string, DesktopRpc>();
@@ -100,6 +103,10 @@ function requireDesktopWorkspace(): DesktopWorkspace {
100
103
  return desktopWorkspace;
101
104
  }
102
105
 
106
+ function remoteFailure(code: string, message: string): RemoteControlResponse {
107
+ return { ok: false, error: { code, message } };
108
+ }
109
+
103
110
  const {
104
111
  forEachReadyWindowRpc,
105
112
  getRpcWindowKey,
@@ -130,6 +137,46 @@ const detachedWindowManager = new DesktopDetachedWindowManager<DesktopRpc>({
130
137
  stateBroadcaster: desktopStateBroadcaster,
131
138
  });
132
139
 
140
+ async function forwardRemoteControlRequest(request: RemoteControlRequest): Promise<RemoteControlResponse> {
141
+ const rpc = getWindowRpc(MAIN_WINDOW_RPC_KEY);
142
+ if (!rpc || !isWindowRpcReady(MAIN_WINDOW_RPC_KEY)) {
143
+ return remoteFailure("remote_unavailable", "The main desktop window is not ready for remote control requests.");
144
+ }
145
+ try {
146
+ const response = await rpc.request["remote.request"]({
147
+ request: encodeRpcValue(request) as RemoteControlRequest,
148
+ });
149
+ return decodeRpcValue<RemoteControlResponse>(response);
150
+ } catch (error) {
151
+ return remoteFailure(
152
+ "remote_forward_error",
153
+ error instanceof Error ? error.message : String(error),
154
+ );
155
+ }
156
+ }
157
+
158
+ async function ensureDesktopRemoteControlServer(): Promise<void> {
159
+ if (desktopRemoteControlServer) return;
160
+ const config = requireConfig();
161
+ desktopRemoteControlServer = await startRemoteControlServer({
162
+ dataDir: config.dataDir,
163
+ appKind: "desktop",
164
+ handle: forwardRemoteControlRequest,
165
+ });
166
+ console.error("[remote] desktop control endpoint started", {
167
+ port: desktopRemoteControlServer.endpoint.port,
168
+ });
169
+ }
170
+
171
+ function stopDesktopRemoteControlServer(): void {
172
+ const server = desktopRemoteControlServer;
173
+ desktopRemoteControlServer = null;
174
+ if (!server) return;
175
+ void server.close().catch((error) => {
176
+ console.error("[remote] desktop control endpoint cleanup failed", summarizeError(error));
177
+ });
178
+ }
179
+
133
180
  function openMainWindowDevTools(): void {
134
181
  mainWindow?.webview.openDevTools();
135
182
  }
@@ -228,6 +275,7 @@ function disposeWindowScopedResources(windowKey: string): void {
228
275
  }
229
276
 
230
277
  function teardownServices(): void {
278
+ stopDesktopRemoteControlServer();
231
279
  capabilityBridge.disposeAll();
232
280
  services?.destroy();
233
281
  services = null;
@@ -306,7 +354,7 @@ async function initialize(
306
354
  rpc: DesktopRpc,
307
355
  payload: Record<string, unknown>,
308
356
  ) {
309
- return initializeDesktopBackend({
357
+ const init = await initializeDesktopBackend({
310
358
  getCurrentConfig: () => currentConfig,
311
359
  getCurrentServices: () => services,
312
360
  getDesktopSnapshot,
@@ -328,6 +376,12 @@ async function initialize(
328
376
  },
329
377
  syncConfigAccessors,
330
378
  });
379
+ if (init.windowKind === "main") {
380
+ void ensureDesktopRemoteControlServer().catch((error) => {
381
+ console.error("[remote] desktop control endpoint failed", summarizeError(error));
382
+ });
383
+ }
384
+ return init;
331
385
  }
332
386
 
333
387
  async function handleBackendRequest(
@@ -4,6 +4,7 @@ import type { DesktopApplicationMenuCommand } from "../../../types/desktop-menu"
4
4
  import type { AppConfig } from "../../../types/config";
5
5
  import type { UpdateProgress } from "../../../updater";
6
6
  import type { CapabilityManifest } from "../../../capabilities";
7
+ import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
7
8
 
8
9
  export const ELECTROBUN_CONTEXT_MENU_ACTION = "gloom.context-menu.select";
9
10
 
@@ -59,6 +60,10 @@ export interface DesktopRestartMessage {
59
60
  source?: string;
60
61
  }
61
62
 
63
+ export interface RemoteControlRequestMessage {
64
+ request: RemoteControlRequest;
65
+ }
66
+
62
67
  export interface ElectrobunDesktopRpcSchema {
63
68
  bun: {
64
69
  requests: {
@@ -72,7 +77,12 @@ export interface ElectrobunDesktopRpcSchema {
72
77
  };
73
78
  };
74
79
  webview: {
75
- requests: {};
80
+ requests: {
81
+ "remote.request": {
82
+ params: RemoteControlRequestMessage;
83
+ response: RemoteControlResponse;
84
+ };
85
+ };
76
86
  messages: {
77
87
  "context-menu.select": ContextMenuSelectMessage;
78
88
  "application-menu.select": ApplicationMenuSelectMessage;
@@ -9,10 +9,12 @@ import {
9
9
  type DesktopStateMessage,
10
10
  type DesktopThemePreviewMessage,
11
11
  type ElectrobunBackendInit,
12
+ type RemoteControlRequestMessage,
12
13
  type ElectrobunDesktopRpcSchema,
13
14
  type UpdateProgressMessage,
14
15
  } from "../shared/protocol";
15
16
  import { decodeRpcValue, encodeRpcValue } from "./rpc-codec";
17
+ import type { RemoteControlRequest, RemoteControlResponse } from "../../../remote/types";
16
18
 
17
19
  type ContextMenuSelectListener = (message: ContextMenuSelectMessage) => void;
18
20
  type ApplicationMenuSelectListener = (message: ApplicationMenuSelectMessage) => void;
@@ -21,8 +23,10 @@ type DesktopDockPreviewListener = (message: DesktopDockPreviewMessage) => void;
21
23
  type DesktopThemePreviewListener = (message: DesktopThemePreviewMessage) => void;
22
24
  type UpdateProgressListener = (message: UpdateProgressMessage) => void;
23
25
  type CapabilityEventListener = (message: CapabilityEventMessage) => void;
26
+ type RemoteControlRequestHandler = (request: RemoteControlRequest) => Promise<RemoteControlResponse>;
24
27
 
25
28
  let initSnapshot: ElectrobunBackendInit | null = null;
29
+ let remoteControlRequestHandler: RemoteControlRequestHandler | null = null;
26
30
  const contextMenuSelectListeners = new Map<string, Set<ContextMenuSelectListener>>();
27
31
  const applicationMenuSelectListeners = new Set<ApplicationMenuSelectListener>();
28
32
  const desktopStateListeners = new Set<DesktopStateListener>();
@@ -63,7 +67,21 @@ function subscribe<T>(
63
67
  const rpc = Electroview.defineRPC<ElectrobunDesktopRpcSchema>({
64
68
  maxRequestTime: 120_000,
65
69
  handlers: {
66
- requests: {},
70
+ requests: {
71
+ "remote.request": async (message: RemoteControlRequestMessage) => {
72
+ if (!remoteControlRequestHandler) {
73
+ return {
74
+ ok: false,
75
+ error: {
76
+ code: "remote_unavailable",
77
+ message: "The desktop app has not installed a remote control handler yet.",
78
+ },
79
+ };
80
+ }
81
+ const decoded = decodeRpcValue<RemoteControlRequest>(message.request);
82
+ return encodeRpcValue(await remoteControlRequestHandler(decoded)) as RemoteControlResponse;
83
+ },
84
+ },
67
85
  messages: {
68
86
  "context-menu.select": (message) => {
69
87
  dispatch(contextMenuSelectListeners, message.requestId, message);
@@ -140,6 +158,15 @@ export function getElectrobunBackendInitSnapshot(): ElectrobunBackendInit | null
140
158
  return initSnapshot;
141
159
  }
142
160
 
161
+ export function setElectrobunRemoteRequestHandler(handler: RemoteControlRequestHandler | null): () => void {
162
+ remoteControlRequestHandler = handler;
163
+ return () => {
164
+ if (remoteControlRequestHandler === handler) {
165
+ remoteControlRequestHandler = null;
166
+ }
167
+ };
168
+ }
169
+
143
170
  export function onCapabilityEvent(
144
171
  subscriptionId: string,
145
172
  listener: (message: CapabilityEventMessage) => void,
@@ -23,6 +23,7 @@ const COMMON_ALIAS_RULES: AliasRule[] = [
23
23
  ["./kitty/support", "components/chart/native/renderer-selection.ts", "native-stubs/chart/kitty-support.ts"],
24
24
  ["native/surface/manager", "native-stubs/chart/surface-manager.ts"],
25
25
  ["native/surface/sync", "native-stubs/chart/surface-sync.ts"],
26
+ ["./native-loader", "plugins/ibkr/gateway/service/index.ts", "native-stubs/ibkr-native-loader.ts"],
26
27
  ];
27
28
 
28
29
  export function electrobunViewPath(...parts: string[]): string {
@@ -53,8 +53,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
53
53
  scrollRef,
54
54
  syncHeaderScroll: _syncHeaderScroll,
55
55
  onBodyScrollActivity,
56
- hoveredIdx,
57
- setHoveredIdx,
58
56
  getItemKey,
59
57
  isSelected,
60
58
  onSelect,
@@ -257,9 +255,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
257
255
  focusPane();
258
256
  onTableMouseDown?.({});
259
257
  }}
260
- onMouseLeave={() => {
261
- if (hoveredIdx !== null) setHoveredIdx(null);
262
- }}
263
258
  onScroll={() => {
264
259
  markScrollbarActive();
265
260
  scheduleBodyScrollActivity();
@@ -315,7 +310,6 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
315
310
  const item = items[row.index];
316
311
  if (!item) return null;
317
312
  const selected = isSelected(item, row.index);
318
- const hovered = hoveredIdx === row.index && !selected;
319
313
  const itemKey = getItemKey(item, row.index);
320
314
  return (
321
315
  <WebDataTableRow<T, C>
@@ -335,13 +329,11 @@ export function WebDataTable<T, C extends DataTableColumn = DataTableColumn>({
335
329
  onRowContextMenu={onRowContextMenu}
336
330
  onRowMouseDown={onRowMouseDown}
337
331
  onSelectRow={selectRow}
338
- hovered={hovered}
339
332
  getRowBackgroundColor={getRowBackgroundColor}
340
333
  renderCell={renderCell}
341
334
  renderSectionHeader={renderSectionHeader}
342
335
  rowContextMenuSurface={rowContextMenuSurface}
343
336
  selected={selected}
344
- setHoveredIdx={setHoveredIdx}
345
337
  />
346
338
  );
347
339
  }),
@@ -12,7 +12,6 @@ import type {
12
12
  import { WEB_CELL_HEIGHT } from "../input-host";
13
13
  import {
14
14
  CSS_BG,
15
- CSS_HOVER_BG,
16
15
  CSS_PANEL,
17
16
  CSS_SELECTED,
18
17
  CSS_SELECTED_TEXT,
@@ -151,7 +150,6 @@ function WebDataTableRowInner<
151
150
  onRowContextMenu,
152
151
  onRowMouseDown,
153
152
  onSelectRow,
154
- hovered,
155
153
  index,
156
154
  item,
157
155
  itemKey,
@@ -163,7 +161,6 @@ function WebDataTableRowInner<
163
161
  rowStart,
164
162
  rowContextMenuSurface,
165
163
  selected,
166
- setHoveredIdx,
167
164
  }: {
168
165
  columns: C[];
169
166
  columnGap: number;
@@ -174,7 +171,6 @@ function WebDataTableRowInner<
174
171
  onRowContextMenu?: DataTableProps<T, C>["onRowContextMenu"];
175
172
  onRowMouseDown?: DataTableProps<T, C>["onRowMouseDown"];
176
173
  onSelectRow: (item: T, index: number) => void;
177
- hovered: boolean;
178
174
  index: number;
179
175
  item: T;
180
176
  itemKey: string;
@@ -186,7 +182,6 @@ function WebDataTableRowInner<
186
182
  rowStart: number;
187
183
  rowContextMenuSurface: boolean;
188
184
  selected: boolean;
189
- setHoveredIdx: (index: number | null) => void;
190
185
  }) {
191
186
  useThemeColors();
192
187
  const sectionHeader: DataTableSectionHeader | null =
@@ -242,13 +237,11 @@ function WebDataTableRowInner<
242
237
  );
243
238
  }
244
239
 
245
- const rowState = { selected, hovered };
240
+ const rowState = { selected };
246
241
  const rowBackgroundColor = getRowBackgroundColor?.(item, index, rowState);
247
242
  const rowBg = selected
248
243
  ? CSS_SELECTED
249
- : hovered
250
- ? CSS_HOVER_BG
251
- : rowBackgroundColor ?? CSS_BG;
244
+ : rowBackgroundColor ?? CSS_BG;
252
245
 
253
246
  return (
254
247
  <div
@@ -260,9 +253,6 @@ function WebDataTableRowInner<
260
253
  ...baseRowStyle,
261
254
  backgroundColor: rowBg,
262
255
  }}
263
- onMouseMove={() => {
264
- if (!hovered) setHoveredIdx(index);
265
- }}
266
256
  onMouseDown={(event) => {
267
257
  focusPane();
268
258
  onTableMouseDown?.(event);
@@ -33,6 +33,9 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
33
33
  const pendingDragRef = useRef<CellMouseEvent | null>(null);
34
34
  const propsRef = useRef(props);
35
35
  propsRef.current = props;
36
+ const hoverBackgroundColor = typeof props.hoverBackgroundColor === "string"
37
+ ? props.hoverBackgroundColor
38
+ : undefined;
36
39
 
37
40
  useImperativeHandle(ref, () => cellBoundsForElement(() => elementRef.current, () => propsRef.current) as unknown as HTMLDivElement, []);
38
41
 
@@ -132,6 +135,10 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
132
135
  callMouseHandler(propsRef.current.onMouseScroll, event, "scroll");
133
136
  };
134
137
 
138
+ const handleMouseOver = (event: MouseEvent) => {
139
+ callMouseHandler(propsRef.current.onMouseOver, event, "over");
140
+ };
141
+
135
142
  const handleMouseOut = (event: MouseEvent) => {
136
143
  pendingMoveRef.current = null;
137
144
  callMouseHandler(propsRef.current.onMouseOut, event, "out");
@@ -140,18 +147,21 @@ export const WebBox = forwardRef<HTMLDivElement, Record<string, unknown> & { chi
140
147
  return (
141
148
  <div
142
149
  {...cleanDomProps(props)}
150
+ data-gloom-hover-bg={hoverBackgroundColor ? "true" : undefined}
143
151
  data-gloom-interactive={hasDirectMouseHandler(props) ? "true" : undefined}
144
152
  ref={elementRef}
145
153
  onMouseDown={handleMouseDown}
154
+ onMouseOver={handleMouseOver}
146
155
  onMouseMove={(event) => scheduleFrameMouseHandler(event, "move")}
147
156
  onMouseUp={(event) => callMouseHandler(propsRef.current.onMouseUp, event, "up")}
148
157
  onMouseOut={handleMouseOut}
149
158
  onWheel={typeof props.onMouseScroll === "function" ? handleWheel : undefined}
150
159
  style={{
151
160
  ...commonStyle(props),
161
+ "--gloom-box-hover-bg": hoverBackgroundColor,
152
162
  ...(props.style as CSSProperties | undefined),
153
163
  ...(props.visible === false ? { display: "none" } : undefined),
154
- }}
164
+ } as CSSProperties}
155
165
  >
156
166
  {children as ReactNode}
157
167
  </div>
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { hasDirectMouseHandler, mouseHandlers } from "./mouse";
3
+
4
+ function fakeMouseEvent() {
5
+ return {
6
+ clientX: 16,
7
+ clientY: 36,
8
+ button: 0,
9
+ detail: 0,
10
+ timeStamp: 1,
11
+ shiftKey: false,
12
+ altKey: false,
13
+ ctrlKey: false,
14
+ metaKey: false,
15
+ preventDefault() {},
16
+ stopPropagation() {},
17
+ };
18
+ }
19
+
20
+ describe("desktop web mouse host", () => {
21
+ test("forwards mouse over handlers", () => {
22
+ let cellX = -1;
23
+ let cellY = -1;
24
+ const handlers = mouseHandlers({
25
+ onMouseOver: (event: { x: number; y: number }) => {
26
+ cellX = event.x;
27
+ cellY = event.y;
28
+ },
29
+ });
30
+
31
+ handlers.onMouseOver?.(fakeMouseEvent() as never);
32
+
33
+ expect(cellX).toBe(2);
34
+ expect(cellY).toBe(2);
35
+ expect(hasDirectMouseHandler({ onMouseOver: () => {} })).toBe(true);
36
+ });
37
+ });
@@ -73,6 +73,7 @@ export function mouseHandlers(props: Record<string, unknown>) {
73
73
  );
74
74
  return {
75
75
  onMouseDown: wrap(props.onMouseDown),
76
+ onMouseOver: wrap(props.onMouseOver),
76
77
  onMouseMove: wrap(props.onMouseMove),
77
78
  onMouseUp: wrap(props.onMouseUp),
78
79
  onMouseOut: wrap(props.onMouseOut),
@@ -91,6 +92,7 @@ export function callCellMouseHandler(handler: unknown, event: CellMouseEvent): v
91
92
 
92
93
  export function hasDirectMouseHandler(props: Record<string, unknown>): boolean {
93
94
  return typeof props.onMouseDown === "function"
95
+ || typeof props.onMouseOver === "function"
94
96
  || typeof props.onMouseUp === "function"
95
97
  || typeof props.onMouseMove === "function"
96
98
  || typeof props.onMouseOut === "function"
@@ -104,6 +104,7 @@ export function cleanDomProps(props: Record<string, unknown>): Record<string, un
104
104
  "cursorColor", "selectionBg", "selectionFg", "showCursor", "keyBindings", "wrapText", "wrapMode",
105
105
  "initialValue", "value", "onInput", "onChange", "onSubmit", "onCursorChange", "onMouse",
106
106
  "scrollX", "scrollY", "focusable", "bitmap", "bitmaps", "crosshair", "text", "font",
107
+ "hoverBackgroundColor",
107
108
  ]) {
108
109
  delete next[key];
109
110
  }
@@ -5,7 +5,7 @@ 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 { backendRequest, initElectrobunBackend } from "./backend-rpc";
8
+ import { backendRequest, initElectrobunBackend, setElectrobunRemoteRequestHandler } from "./backend-rpc";
9
9
  import { installElectrobunAiHost } from "./ai-host";
10
10
  import { installElectrobunBrokerRemoteClient } from "./broker-remote-client";
11
11
  import { installElectrobunConfigStoreHost } from "./config-host";
@@ -89,6 +89,9 @@ async function boot() {
89
89
  const desktopWindowBridge = createDesktopWindowBridge(init.windowKind, init.paneId);
90
90
  const desktopApplicationMenuBridge = createApplicationMenuBridge();
91
91
  const webUiHost = createWebUiHost(init.desktopPlatform);
92
+ const remoteControlAdapter = init.windowKind === "main"
93
+ ? { registerHandler: setElectrobunRemoteRequestHandler }
94
+ : undefined;
92
95
  measurePerfAsync("startup.electrobun.root-render", async () => {
93
96
  root.render(
94
97
  <ElectrobunErrorBoundary>
@@ -102,6 +105,7 @@ async function boot() {
102
105
  desktopApplicationMenuBridge={desktopApplicationMenuBridge}
103
106
  desktopSnapshot={desktopSnapshot}
104
107
  desktopThemePreview={init.desktopThemePreview}
108
+ remoteControlAdapter={remoteControlAdapter}
105
109
  />
106
110
  </WebDialogHostProvider>
107
111
  </WebToastHostProvider>