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,98 @@
1
+ import type { AppState } from "../state/app/context";
2
+ import type { RemoteUiNodeSnapshot } from "./types";
3
+
4
+ export function commandBarResultsFromNodes(nodes: RemoteUiNodeSnapshot[]) {
5
+ const directResults = nodes
6
+ .filter((node) => node.role === "command-bar-result")
7
+ .map((node) => {
8
+ const metadata = node.metadata ?? {};
9
+ const item = metadata.item && typeof metadata.item === "object"
10
+ ? metadata.item as Record<string, unknown>
11
+ : {};
12
+ return {
13
+ nodeId: node.id,
14
+ index: metadata.index,
15
+ label: node.label ?? item.label,
16
+ detail: item.detail,
17
+ category: item.category,
18
+ kind: item.kind,
19
+ right: item.right,
20
+ selected: metadata.selected === true,
21
+ disabled: node.disabled === true,
22
+ actions: node.actions,
23
+ itemId: item.id,
24
+ actionInput: undefined,
25
+ metadata,
26
+ };
27
+ });
28
+
29
+ const listResults = nodes.flatMap((node) => {
30
+ const metadata = node.metadata ?? {};
31
+ if (node.role !== "list" && node.role !== "command-bar-list") return [];
32
+ if (metadata.scope !== "command-bar") return [];
33
+ const rawItems = Array.isArray(metadata.items) ? metadata.items : [];
34
+ const selectedIndex = typeof metadata.selectedIndex === "number" ? metadata.selectedIndex : -1;
35
+ return rawItems
36
+ .map((entry, fallbackIndex) => (
37
+ entry && typeof entry === "object"
38
+ ? entry as Record<string, unknown>
39
+ : { label: String(entry), index: fallbackIndex }
40
+ ))
41
+ .map((item, fallbackIndex) => {
42
+ const index = typeof item.index === "number" ? item.index : fallbackIndex;
43
+ return {
44
+ nodeId: node.id,
45
+ index,
46
+ label: item.label,
47
+ detail: item.detail,
48
+ category: item.category ?? metadata.category,
49
+ kind: item.kind ?? metadata.itemKind,
50
+ right: item.right,
51
+ selected: selectedIndex === index,
52
+ disabled: item.disabled === true || node.disabled === true,
53
+ actions: node.actions,
54
+ itemId: item.id,
55
+ actionInput: {
56
+ index,
57
+ id: item.id,
58
+ label: item.label,
59
+ },
60
+ metadata: {
61
+ ...metadata,
62
+ item,
63
+ },
64
+ };
65
+ });
66
+ });
67
+
68
+ return [...directResults, ...listResults].sort((left, right) => {
69
+ const leftIndex = typeof left.index === "number" ? left.index : Number.MAX_SAFE_INTEGER;
70
+ const rightIndex = typeof right.index === "number" ? right.index : Number.MAX_SAFE_INTEGER;
71
+ return leftIndex - rightIndex;
72
+ });
73
+ }
74
+
75
+ export function isCommandBarInputNode(node: RemoteUiNodeSnapshot): boolean {
76
+ return (
77
+ (node.role === "input" || node.role === "command-bar-input")
78
+ && node.metadata?.scope === "command-bar"
79
+ );
80
+ }
81
+
82
+ function commandBarInputValue(nodes: RemoteUiNodeSnapshot[], fallback: string): string {
83
+ const focusedInput = nodes.find((node) => isCommandBarInputNode(node) && node.metadata?.focused === true);
84
+ const value = focusedInput?.metadata?.value;
85
+ return typeof value === "string" ? value : fallback;
86
+ }
87
+
88
+ export function commandBarSnapshot(state: AppState, nodes: RemoteUiNodeSnapshot[]) {
89
+ const results = commandBarResultsFromNodes(nodes);
90
+ return {
91
+ open: state.commandBarOpen,
92
+ query: commandBarInputValue(nodes, state.commandBarQuery),
93
+ stateQuery: state.commandBarQuery,
94
+ launchRequest: state.commandBarLaunchRequest,
95
+ selectedIndex: results.find((result) => result.selected)?.index ?? null,
96
+ results,
97
+ };
98
+ }
@@ -0,0 +1,111 @@
1
+ import { getDockedPaneIds } from "../plugins/pane-manager";
2
+ import type { AppState } from "../state/app/context";
3
+ import type { LayoutConfig, PaneInstanceConfig } from "../types/config";
4
+ import { revisionFor } from "./revision";
5
+ import type { RemoteControlResponse, RemoteIncludedState, RemoteStateInclude } from "./types";
6
+
7
+ export interface PatchTarget<T> {
8
+ value: T;
9
+ apply(value: T): Promise<void> | void;
10
+ }
11
+
12
+ export function ok<T>(data: T, rev?: string, state?: RemoteIncludedState): RemoteControlResponse<T> {
13
+ return {
14
+ ok: true,
15
+ data,
16
+ ...(rev ? { rev } : {}),
17
+ ...(state ? { state } : {}),
18
+ };
19
+ }
20
+
21
+ export function fail(code: string, error: unknown): RemoteControlResponse {
22
+ return {
23
+ ok: false,
24
+ error: {
25
+ code,
26
+ message: error instanceof Error ? error.message : String(error),
27
+ },
28
+ };
29
+ }
30
+
31
+ export function asRecord(input: unknown): Record<string, unknown> {
32
+ return input && typeof input === "object" && !Array.isArray(input) ? input as Record<string, unknown> : {};
33
+ }
34
+
35
+ export function stringInput(input: Record<string, unknown>, key: string): string {
36
+ const value = input[key];
37
+ if (typeof value !== "string" || value.length === 0) {
38
+ throw new Error(`${key} is required.`);
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function optionalString(input: Record<string, unknown>, key: string): string | undefined {
44
+ const value = input[key];
45
+ return typeof value === "string" && value.length > 0 ? value : undefined;
46
+ }
47
+
48
+ export function numberInput(input: Record<string, unknown>, key: string): number {
49
+ const value = input[key];
50
+ if (!Number.isInteger(value)) {
51
+ throw new Error(`${key} must be an integer.`);
52
+ }
53
+ return value as number;
54
+ }
55
+
56
+ export function optionalNumber(input: Record<string, unknown>, key: string): number | undefined {
57
+ const value = input[key];
58
+ return Number.isInteger(value) ? value as number : undefined;
59
+ }
60
+
61
+ export function activeLayoutRev(state: AppState): string {
62
+ return revisionFor({
63
+ layout: state.config.layout,
64
+ paneState: state.paneState,
65
+ focusedPaneId: state.focusedPaneId,
66
+ activePanel: state.activePanel,
67
+ });
68
+ }
69
+
70
+ export function panePlacement(layout: LayoutConfig, instanceId: string): "docked" | "floating" | "detached" | "hidden" {
71
+ if (getDockedPaneIds(layout).includes(instanceId)) return "docked";
72
+ if (layout.floating.some((entry) => entry.instanceId === instanceId)) return "floating";
73
+ if ((layout.detached ?? []).some((entry) => entry.instanceId === instanceId)) return "detached";
74
+ return "hidden";
75
+ }
76
+
77
+ export function paneSnapshot(state: AppState, pane: PaneInstanceConfig) {
78
+ return {
79
+ ...pane,
80
+ placement: panePlacement(state.config.layout, pane.instanceId),
81
+ focused: state.focusedPaneId === pane.instanceId,
82
+ runtimeState: state.paneState[pane.instanceId] ?? {},
83
+ };
84
+ }
85
+
86
+ export function mutationSummary(state: AppState, extra: Record<string, unknown> = {}) {
87
+ return {
88
+ ok: true,
89
+ rev: activeLayoutRev(state),
90
+ activeLayoutIndex: state.config.activeLayoutIndex,
91
+ activeLayoutName: state.config.layouts[state.config.activeLayoutIndex]?.name ?? null,
92
+ focusedPaneId: state.focusedPaneId,
93
+ commandBarOpen: state.commandBarOpen,
94
+ commandBarQuery: state.commandBarQuery,
95
+ dockedPaneIds: getDockedPaneIds(state.config.layout),
96
+ floatingPaneIds: state.config.layout.floating.map((entry) => entry.instanceId),
97
+ detachedPaneIds: (state.config.layout.detached ?? []).map((entry) => entry.instanceId),
98
+ ...extra,
99
+ };
100
+ }
101
+
102
+ export function normalizeIncludes(
103
+ include: RemoteStateInclude[] | undefined,
104
+ defaults: RemoteStateInclude[] = [],
105
+ ): RemoteStateInclude[] {
106
+ const raw = include ?? defaults;
107
+ if (raw.includes("all")) {
108
+ return ["app", "layout", "panes", "commandBar", "ui", "schema", "help"];
109
+ }
110
+ return [...new Set(raw)];
111
+ }
@@ -0,0 +1,365 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Dispatch } from "react";
3
+ import { appReducer, createInitialState, type AppAction, type AppState } from "../core/state/app/state";
4
+ import type { PluginRegistry } from "../plugins/registry";
5
+ import { createDefaultConfig } from "../types/config";
6
+ import { createAppRemoteController } from "./controller";
7
+ import type { RemoteControlSchema, RemoteUiNodeSnapshot } from "./types";
8
+ import type { RemoteUiRegistry } from "./semantic-tree";
9
+
10
+ function createRegistryHarness() {
11
+ let state = createInitialState({
12
+ ...createDefaultConfig("/tmp/gloom-remote-controller"),
13
+ onboardingComplete: true,
14
+ });
15
+ const actions: AppAction[] = [];
16
+ const dispatch: Dispatch<AppAction> = (action) => {
17
+ actions.push(action);
18
+ state = appReducer(state, action);
19
+ };
20
+ const registry = {
21
+ panes: new Map(),
22
+ paneTemplates: new Map(),
23
+ commands: new Map(),
24
+ capabilities: {
25
+ manifests: () => [],
26
+ invoke: async () => ({ invoked: true }),
27
+ },
28
+ resolvePaneSettings: () => null,
29
+ showPane: () => {},
30
+ focusPane: () => {},
31
+ hidePane: () => {},
32
+ createPaneFromTemplateAsyncFn: async () => {},
33
+ navigateTicker: () => {},
34
+ pinTicker: () => {},
35
+ selectTicker: () => {},
36
+ switchTab: () => {},
37
+ getTermSizeFn: () => ({ width: 120, height: 40 }),
38
+ updateLayoutFn: (layout: AppState["config"]["layout"]) => {
39
+ dispatch({ type: "UPDATE_LAYOUT", layout });
40
+ },
41
+ applyPaneSettingValueFn: async () => {},
42
+ notify: () => {},
43
+ } as unknown as PluginRegistry;
44
+ let uiNodes: RemoteUiNodeSnapshot[] = [{ id: "ui:test", role: "button", label: "Test", actions: ["press"] }];
45
+ const invokedUiActions: Array<{ nodeId: string; action: string; input?: unknown }> = [];
46
+ const uiRegistry: RemoteUiRegistry = {
47
+ register: () => {},
48
+ unregister: () => {},
49
+ snapshot: () => uiNodes,
50
+ invoke: async (nodeId, action, input) => {
51
+ invokedUiActions.push({ nodeId, action, input });
52
+ return { nodeId, action, input };
53
+ },
54
+ };
55
+ const controller = createAppRemoteController({
56
+ dispatch,
57
+ getState: () => state,
58
+ pluginRegistry: registry,
59
+ uiRegistry,
60
+ });
61
+ return {
62
+ actions,
63
+ controller,
64
+ getState: () => state,
65
+ invokedUiActions,
66
+ setUiNodes: (nodes: RemoteUiNodeSnapshot[]) => {
67
+ uiNodes = nodes;
68
+ },
69
+ };
70
+ }
71
+
72
+ describe("createAppRemoteController", () => {
73
+ test("exposes schema, app snapshot, and semantic UI tree", async () => {
74
+ const { controller } = createRegistryHarness();
75
+
76
+ const schema = await controller.handle({ type: "schema" });
77
+ expect(schema.ok).toBe(true);
78
+ if (schema.ok) {
79
+ const data = schema.data as RemoteControlSchema;
80
+ expect(data.resources.some((resource) => resource.uri === "ui://tree")).toBe(true);
81
+ expect(data.operations.some((operation) => operation.id === "ui.invoke")).toBe(true);
82
+ expect(data.help).toMatchObject({
83
+ title: "Gloomberb remote control guide",
84
+ });
85
+ }
86
+
87
+ const help = await controller.handle({ type: "help" });
88
+ expect(help.ok).toBe(true);
89
+ if (help.ok) {
90
+ expect(help.data).toMatchObject({
91
+ batching: expect.any(Object),
92
+ });
93
+ }
94
+
95
+ const snapshot = await controller.handle({ type: "get", resource: "app://snapshot" });
96
+ expect(snapshot.ok).toBe(true);
97
+ if (snapshot.ok) {
98
+ const data = snapshot.data as { ui: unknown[] };
99
+ expect(data.ui).toEqual([{ id: "ui:test", role: "button", label: "Test", actions: ["press"] }]);
100
+ expect(typeof snapshot.rev).toBe("string");
101
+ }
102
+ });
103
+
104
+ test("dispatches semantic app operations", async () => {
105
+ const { actions, controller, getState } = createRegistryHarness();
106
+
107
+ const response = await controller.handle({
108
+ type: "call",
109
+ operation: "app.openCommandBar",
110
+ input: { query: "NVDA" },
111
+ });
112
+
113
+ expect(response.ok).toBe(true);
114
+ expect(actions).toContainEqual({ type: "SET_COMMAND_BAR", open: true, query: "NVDA" });
115
+ expect(getState().commandBarOpen).toBe(true);
116
+ expect(getState().commandBarQuery).toBe("NVDA");
117
+ if (response.ok) {
118
+ expect(response.state?.commandBar).toMatchObject({
119
+ open: true,
120
+ stateQuery: "NVDA",
121
+ });
122
+ }
123
+ });
124
+
125
+ test("opens ticker search without requiring command-bar prefix syntax", async () => {
126
+ const { actions, controller, getState } = createRegistryHarness();
127
+
128
+ const response = await controller.handle({
129
+ type: "call",
130
+ operation: "app.search",
131
+ input: { mode: "ticker", query: "google" },
132
+ });
133
+
134
+ expect(response.ok).toBe(true);
135
+ expect(actions).toContainEqual({
136
+ type: "SET_COMMAND_BAR",
137
+ open: true,
138
+ launch: { kind: "ticker-search", query: "google" },
139
+ });
140
+ expect(getState().commandBarOpen).toBe(true);
141
+ expect(getState().commandBarLaunchRequest).toMatchObject({
142
+ kind: "ticker-search",
143
+ query: "google",
144
+ });
145
+ });
146
+
147
+ test("exposes and activates semantic command-bar results", async () => {
148
+ const { controller, invokedUiActions, setUiNodes } = createRegistryHarness();
149
+ setUiNodes([
150
+ {
151
+ id: "ui:input",
152
+ role: "input",
153
+ actions: ["setValue"],
154
+ metadata: { value: "T google", focused: true },
155
+ },
156
+ {
157
+ id: "ui:goog",
158
+ role: "command-bar-result",
159
+ label: "GOOG",
160
+ actions: ["activate"],
161
+ metadata: {
162
+ index: 1,
163
+ selected: false,
164
+ item: {
165
+ id: "ticker:GOOG",
166
+ label: "GOOG",
167
+ detail: "Alphabet Inc.",
168
+ category: "Primary Listing",
169
+ kind: "ticker",
170
+ right: "Equity NASDAQ",
171
+ },
172
+ },
173
+ },
174
+ ]);
175
+
176
+ const results = await controller.handle({ type: "get", resource: "app://command-bar/results" });
177
+ expect(results.ok).toBe(true);
178
+ if (results.ok) {
179
+ expect(results.data).toMatchObject([
180
+ {
181
+ nodeId: "ui:goog",
182
+ index: 1,
183
+ label: "GOOG",
184
+ kind: "ticker",
185
+ right: "Equity NASDAQ",
186
+ },
187
+ ]);
188
+ }
189
+
190
+ const activated = await controller.handle({
191
+ type: "call",
192
+ operation: "commandBar.activateResult",
193
+ input: { index: 1 },
194
+ });
195
+ expect(activated.ok).toBe(true);
196
+ expect(invokedUiActions).toContainEqual({ nodeId: "ui:goog", action: "activate", input: undefined });
197
+ });
198
+
199
+ test("scopes command-bar query reads to the command-bar input", async () => {
200
+ const { controller, setUiNodes } = createRegistryHarness();
201
+ setUiNodes([
202
+ {
203
+ id: "ui:pane-input",
204
+ role: "input",
205
+ actions: ["setValue"],
206
+ metadata: { value: "pane search", focused: true },
207
+ },
208
+ {
209
+ id: "ui:command-input",
210
+ role: "input",
211
+ actions: ["setValue"],
212
+ metadata: { value: "theme", focused: true, scope: "command-bar" },
213
+ },
214
+ ]);
215
+
216
+ const response = await controller.handle({ type: "get", resource: "app://command-bar" });
217
+ expect(response.ok).toBe(true);
218
+ if (response.ok) {
219
+ expect((response.data as { query: string }).query).toBe("theme");
220
+ }
221
+ });
222
+
223
+ test("exposes shared list items as command-bar results", async () => {
224
+ const { controller, invokedUiActions, setUiNodes } = createRegistryHarness();
225
+ setUiNodes([
226
+ {
227
+ id: "ui:theme-list",
228
+ role: "list",
229
+ label: "Theme picker",
230
+ actions: ["activate", "select"],
231
+ metadata: {
232
+ scope: "command-bar",
233
+ selectedIndex: 1,
234
+ itemKind: "theme",
235
+ items: [
236
+ { index: 0, id: "github-dark", label: "GitHub Dark", kind: "theme", category: "Themes" },
237
+ { index: 1, id: "github-light", label: "GitHub Light", kind: "theme", category: "Themes", current: true },
238
+ ],
239
+ },
240
+ },
241
+ ]);
242
+
243
+ const results = await controller.handle({ type: "get", resource: "app://command-bar/results" });
244
+ expect(results.ok).toBe(true);
245
+ if (results.ok) {
246
+ expect(results.data).toMatchObject([
247
+ { nodeId: "ui:theme-list", index: 0, label: "GitHub Dark", kind: "theme", itemId: "github-dark" },
248
+ { nodeId: "ui:theme-list", index: 1, label: "GitHub Light", kind: "theme", itemId: "github-light", selected: true },
249
+ ]);
250
+ }
251
+
252
+ const activated = await controller.handle({
253
+ type: "call",
254
+ operation: "commandBar.activateResult",
255
+ input: { label: "GitHub Light" },
256
+ });
257
+ expect(activated.ok).toBe(true);
258
+ expect(invokedUiActions).toContainEqual({
259
+ nodeId: "ui:theme-list",
260
+ action: "activate",
261
+ input: { index: 1, id: "github-light", label: "GitHub Light" },
262
+ });
263
+ });
264
+
265
+ test("invokes semantic UI nodes by selector", async () => {
266
+ const { controller, invokedUiActions, setUiNodes } = createRegistryHarness();
267
+ setUiNodes([
268
+ { id: "ui:cancel", role: "button", label: "Cancel", actions: ["press"] },
269
+ { id: "ui:done", role: "button", label: "Done", actions: ["press"], metadata: { scope: "command-bar" } },
270
+ ]);
271
+
272
+ const response = await controller.handle({
273
+ type: "call",
274
+ operation: "ui.invokeMatching",
275
+ input: { role: "button", label: "Done", action: "press" },
276
+ });
277
+
278
+ expect(response.ok).toBe(true);
279
+ expect(invokedUiActions).toContainEqual({ nodeId: "ui:done", action: "press", input: undefined });
280
+
281
+ const directResponse = await controller.handle({
282
+ type: "call",
283
+ operation: "ui.invoke",
284
+ input: { nodeId: "ui:done", action: "press" },
285
+ });
286
+ expect(directResponse.ok).toBe(true);
287
+ if (directResponse.ok) {
288
+ expect(directResponse.data).toMatchObject({
289
+ ok: true,
290
+ result: { nodeId: "ui:done", action: "press" },
291
+ });
292
+ }
293
+ });
294
+
295
+ test("runs sequential batches with halt-on-error and final state", async () => {
296
+ const { controller } = createRegistryHarness();
297
+
298
+ const response = await controller.handle({
299
+ type: "batch",
300
+ include: ["commandBar"],
301
+ requests: [
302
+ { type: "call", operation: "app.openCommandBar", input: { query: "theme" } },
303
+ { type: "call", operation: "missing.operation", input: {} },
304
+ { type: "call", operation: "app.closeCommandBar", input: {} },
305
+ ],
306
+ });
307
+
308
+ expect(response.ok).toBe(true);
309
+ if (response.ok) {
310
+ expect(response.data).toMatchObject({
311
+ ok: false,
312
+ haltedAt: 1,
313
+ });
314
+ expect((response.data as { responses: unknown[] }).responses).toHaveLength(2);
315
+ expect(response.state?.commandBar).toMatchObject({
316
+ open: true,
317
+ });
318
+ }
319
+ });
320
+
321
+ test("patches pane runtime state with replace semantics", async () => {
322
+ const { controller, getState } = createRegistryHarness();
323
+ const paneId = getState().config.layout.instances[0]!.instanceId;
324
+ await controller.handle({
325
+ type: "call",
326
+ operation: "pane.setState",
327
+ input: { paneId, patch: { cursorSymbol: "NVDA", stale: true } },
328
+ });
329
+
330
+ const response = await controller.handle({
331
+ type: "patch",
332
+ resource: `app://pane-state/${encodeURIComponent(paneId)}`,
333
+ patch: [{ op: "remove", path: "/stale" }],
334
+ });
335
+
336
+ expect(response.ok).toBe(true);
337
+ expect(getState().paneState[paneId]).toMatchObject({ cursorSymbol: "NVDA" });
338
+ expect(getState().paneState[paneId]?.stale).toBeUndefined();
339
+ });
340
+
341
+ test("closes floating panes and grids visible panes through layout helpers", async () => {
342
+ const { controller, getState } = createRegistryHarness();
343
+ expect(getState().config.layout.floating.length).toBeGreaterThan(0);
344
+
345
+ const closeResponse = await controller.handle({
346
+ type: "call",
347
+ operation: "layout.closeFloating",
348
+ input: {},
349
+ });
350
+ expect(closeResponse.ok).toBe(true);
351
+ expect(getState().config.layout.floating).toEqual([]);
352
+
353
+ const paneIds = getState().config.layout.instances.slice(0, 4).map((pane) => pane.instanceId);
354
+ const gridResponse = await controller.handle({
355
+ type: "call",
356
+ operation: "layout.setGrid",
357
+ input: { paneIds, columns: 2 },
358
+ });
359
+ expect(gridResponse.ok).toBe(true);
360
+ expect(getState().config.layout.dockRoot).toMatchObject({
361
+ kind: "split",
362
+ axis: "vertical",
363
+ });
364
+ });
365
+ });