gloomberb 0.9.5 → 0.9.7

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 (99) hide show
  1. package/README.md +49 -8
  2. package/package.json +2 -2
  3. package/src/app/global-shortcuts.test.tsx +20 -1
  4. package/src/app/global-shortcuts.ts +2 -0
  5. package/src/app/pane-runtime/index.tsx +11 -4
  6. package/src/app/pane-runtime/pane-settings-runtime.tsx +1 -1
  7. package/src/app.tsx +41 -17
  8. package/src/cli/commands/remote.ts +206 -0
  9. package/src/cli/index.ts +2 -0
  10. package/src/components/chart/core/keyboard.ts +10 -1
  11. package/src/components/chart/static/bar-chart-surface.test.tsx +4 -0
  12. package/src/components/chart/static/chart/surface.test.tsx +59 -4
  13. package/src/components/chart/static/chart/surface.tsx +4 -0
  14. package/src/components/chart/stock-chart/index.test.ts +7 -0
  15. package/src/components/command-bar/list/view.tsx +47 -0
  16. package/src/components/command-bar/multi-select-picker.tsx +7 -0
  17. package/src/components/command-bar/routing/navigation-state.ts +10 -1
  18. package/src/components/command-bar/surface/index.test.tsx +14 -0
  19. package/src/components/command-bar/theme-picker.tsx +91 -79
  20. package/src/components/command-bar/workflow/field-row.tsx +30 -5
  21. package/src/components/index.ts +7 -0
  22. package/src/components/layout/pane/footer/index.tsx +23 -0
  23. package/src/components/layout/shell/index.test.tsx +2 -0
  24. package/src/components/layout/shell/index.tsx +8 -3
  25. package/src/components/layout/shell/pane/actions.ts +21 -7
  26. package/src/components/metric-treemap/index.tsx +317 -0
  27. package/src/components/metric-treemap/layout.test.ts +77 -0
  28. package/src/components/metric-treemap/layout.ts +476 -0
  29. package/src/components/toggle-list.tsx +10 -0
  30. package/src/components/ui/button.tsx +12 -0
  31. package/src/components/ui/data-table/index.tsx +51 -0
  32. package/src/components/ui/fields.tsx +18 -0
  33. package/src/components/ui/list-view.tsx +83 -3
  34. package/src/components/ui/message-composer.tsx +22 -0
  35. package/src/components/ui/tabs.tsx +45 -0
  36. package/src/core/state/app/layout-reducer.ts +7 -1
  37. package/src/core/state/app/layout.ts +6 -0
  38. package/src/core/state/app/state.ts +23 -1
  39. package/src/core/state/app/types.ts +4 -2
  40. package/src/pane-settings.test.ts +1 -0
  41. package/src/plugins/builtin/chat/channels.ts +5 -18
  42. package/src/plugins/builtin/chat/chat.test.tsx +1 -7
  43. package/src/plugins/builtin/chat/status-widget.tsx +1 -7
  44. package/src/plugins/builtin/cloud/plugin.tsx +4 -13
  45. package/src/plugins/builtin/holders/treemap.tsx +28 -297
  46. package/src/plugins/builtin/holders/types.ts +0 -34
  47. package/src/plugins/builtin/market-heatmap/data.test.ts +121 -0
  48. package/src/plugins/builtin/market-heatmap/data.ts +296 -0
  49. package/src/plugins/builtin/market-heatmap/index.tsx +320 -0
  50. package/src/plugins/builtin/plugin-groups.ts +2 -0
  51. package/src/plugins/builtin/portfolio-list/grid.tsx +197 -0
  52. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -1
  53. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  54. package/src/plugins/builtin/portfolio-list/pane/data.ts +15 -1
  55. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +149 -0
  56. package/src/plugins/builtin/portfolio-list/pane/index.tsx +67 -26
  57. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +54 -4
  58. package/src/plugins/builtin/portfolio-list/settings.ts +36 -1
  59. package/src/plugins/ibkr/gateway/service/index.test.ts +22 -0
  60. package/src/plugins/ibkr/gateway/service/index.ts +6 -9
  61. package/src/plugins/ibkr/gateway/service/native-loader.ts +12 -0
  62. package/src/remote/app-host.tsx +77 -0
  63. package/src/remote/client.ts +34 -0
  64. package/src/remote/command-bar.ts +98 -0
  65. package/src/remote/controller-utils.ts +111 -0
  66. package/src/remote/controller.test.ts +365 -0
  67. package/src/remote/controller.ts +434 -0
  68. package/src/remote/endpoint.ts +41 -0
  69. package/src/remote/json-patch.test.ts +31 -0
  70. package/src/remote/json-patch.ts +89 -0
  71. package/src/remote/layout-helpers.ts +62 -0
  72. package/src/remote/resources.ts +186 -0
  73. package/src/remote/revision.ts +18 -0
  74. package/src/remote/schema.ts +143 -0
  75. package/src/remote/semantic-helpers.ts +161 -0
  76. package/src/remote/semantic-tree.tsx +112 -0
  77. package/src/remote/server.test.ts +50 -0
  78. package/src/remote/server.ts +121 -0
  79. package/src/remote/types.ts +119 -0
  80. package/src/renderers/electrobun/bun/index.ts +55 -1
  81. package/src/renderers/electrobun/shared/protocol.ts +11 -1
  82. package/src/renderers/electrobun/view/backend-rpc.ts +28 -1
  83. package/src/renderers/electrobun/view/build-assets.ts +1 -0
  84. package/src/renderers/electrobun/view/host/box.tsx +5 -0
  85. package/src/renderers/electrobun/view/host/mouse.test.ts +37 -0
  86. package/src/renderers/electrobun/view/host/mouse.ts +2 -0
  87. package/src/renderers/electrobun/view/main.tsx +5 -1
  88. package/src/renderers/electrobun/view/native-stubs/ibkr-native-loader.ts +3 -0
  89. package/src/renderers/opentui/chart-surface.test.tsx +101 -0
  90. package/src/renderers/opentui/chart-surface.tsx +15 -20
  91. package/src/renderers/opentui/start.tsx +32 -0
  92. package/src/sources/yahoo-finance/http.ts +31 -4
  93. package/src/state/app/context/index.test.tsx +47 -0
  94. package/src/state/app/context/index.tsx +48 -18
  95. package/src/types/config.ts +1 -0
  96. package/src/ui/host.tsx +1 -0
  97. package/src/ui/index.tsx +183 -7
  98. package/src/version.ts +1 -1
  99. package/src/plugins/builtin/holders/treemap-layout.ts +0 -231
@@ -253,6 +253,10 @@ export function StaticChartSurface({
253
253
  flexDirection="column"
254
254
  bitmaps={bitmap ? [bitmap] : null}
255
255
  crosshair={canvasCrosshair}
256
+ onMouseMove={handleCursorEvent}
257
+ onMouseDown={handleCursorEvent}
258
+ onMouseOut={clearCursor}
259
+ data-gloom-remote-kind="static-chart"
256
260
  >
257
261
  {textResult.lines.map((line, index) => (
258
262
  <Text key={index} content={line} />
@@ -159,6 +159,13 @@ describe("stock chart keyboard helpers", () => {
159
159
  expect(resolveChartKeyboardKey({ name: "minus" })).toBe("zoom-out");
160
160
  expect(resolveChartKeyboardKey({ name: "A" })).toBe("a");
161
161
  });
162
+
163
+ test("ignores shortcut-modified keys so app shortcuts can handle them", () => {
164
+ expect(resolveChartKeyboardKey({ name: "1", ctrl: true })).toBe("");
165
+ expect(resolveChartKeyboardKey({ name: "2", meta: true })).toBe("");
166
+ expect(resolveChartKeyboardKey({ name: "3", super: true })).toBe("");
167
+ expect(resolveChartKeyboardKey({ name: "4", alt: true })).toBe("");
168
+ });
162
169
  });
163
170
 
164
171
  describe("stock chart viewport helpers", () => {
@@ -10,6 +10,7 @@ import {
10
10
  import { Spinner } from "../../ui";
11
11
  import type { CommandBarListRow, ListScreenState, ResultItem } from "./model";
12
12
  import { getRowPresentation, truncateText } from "../view-model";
13
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
13
14
 
14
15
  export type CommandBarListScrollEvent = {
15
16
  stopPropagation: () => void;
@@ -70,6 +71,8 @@ export const CommandBarListHeader = memo(function CommandBarListHeader({
70
71
  onChange={onQueryChange}
71
72
  placeholder={kind === "root" ? "Search commands" : title === "Security Description" ? "Search tickers" : "Filter"}
72
73
  focused
74
+ data-gloom-remote-scope="command-bar"
75
+ data-gloom-remote-surface="command-bar"
73
76
  width={nativePaneChrome ? "100%" : queryDisplayWidth}
74
77
  backgroundColor={nativePaneChrome ? "transparent" : paletteBg}
75
78
  focusedBackgroundColor={nativePaneChrome ? "transparent" : paletteBg}
@@ -114,6 +117,9 @@ interface CommandBarListItemRowProps {
114
117
  globalIdx: number;
115
118
  isSelected: boolean;
116
119
  isHovered: boolean;
120
+ listKind: ListScreenState["kind"];
121
+ listTitle: string;
122
+ listQuery: string;
117
123
  contentPadding: number;
118
124
  labelWidth: number;
119
125
  trailingWidth: number;
@@ -135,6 +141,9 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
135
141
  globalIdx,
136
142
  isSelected,
137
143
  isHovered,
144
+ listKind,
145
+ listTitle,
146
+ listQuery,
138
147
  contentPadding,
139
148
  labelWidth,
140
149
  trailingWidth,
@@ -153,6 +162,41 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
153
162
  const presentation = getRowPresentation(item, isSelected, trailingWidth > 0);
154
163
  const label = truncateText(presentation.label, labelWidth);
155
164
  const trailing = truncateText(presentation.trailing, trailingWidth);
165
+ const activate = () => onRowMouseDown({
166
+ preventDefault() {},
167
+ stopPropagation() {},
168
+ }, item, globalIdx);
169
+ useRemoteUiNode({
170
+ role: "command-bar-result",
171
+ label: item.label,
172
+ disabled: item.disabled === true,
173
+ actions: {
174
+ activate,
175
+ press: activate,
176
+ secondary: item.secondaryAction ? () => item.secondaryAction?.() : undefined,
177
+ },
178
+ metadata: {
179
+ index: globalIdx,
180
+ selected: isSelected,
181
+ hovered: isHovered,
182
+ listKind,
183
+ listTitle,
184
+ listQuery,
185
+ item: {
186
+ id: item.id,
187
+ label: item.label,
188
+ detail: item.detail,
189
+ category: item.category,
190
+ kind: item.kind,
191
+ right: item.right,
192
+ shortcutQuery: item.shortcutQuery,
193
+ searchText: item.searchText,
194
+ checked: item.checked,
195
+ current: item.current,
196
+ disabled: item.disabled === true,
197
+ },
198
+ },
199
+ });
156
200
 
157
201
  return (
158
202
  <Box
@@ -283,6 +327,9 @@ export const CommandBarListBody = memo(function CommandBarListBody({
283
327
  globalIdx={row.globalIdx}
284
328
  isSelected={isSelected}
285
329
  isHovered={isHovered}
330
+ listKind={visibleListState.kind}
331
+ listTitle={visibleListState.title}
332
+ listQuery={visibleListState.query}
286
333
  contentPadding={contentPadding}
287
334
  labelWidth={labelWidth}
288
335
  trailingWidth={trailingWidth}
@@ -197,6 +197,13 @@ export function CommandBarMultiSelectBody({
197
197
  onSelect={onSelect}
198
198
  onToggle={onToggle}
199
199
  bgColor={nativePaneChrome ? panelBg : paletteBg}
200
+ remoteLabel={route.title}
201
+ remoteScope="command-bar"
202
+ remoteMetadata={{
203
+ surface: "multi-select-picker",
204
+ routeKind: route.kind,
205
+ pickerId: route.pickerId,
206
+ }}
200
207
  />
201
208
  <Box flexDirection="row" gap={1}>
202
209
  <Button label="Done" variant="primary" onPress={onCommit} />
@@ -1,4 +1,4 @@
1
- import { useCallback, useRef, useState, type Dispatch, type RefObject, type SetStateAction } from "react";
1
+ import { useCallback, useEffect, useRef, useState, type Dispatch, type RefObject, type SetStateAction } from "react";
2
2
  import type { AppAction } from "../../../state/app/context";
3
3
  import type { Command } from "../commands/registry";
4
4
  import { resolveCommandBarMode } from "../view-model";
@@ -63,6 +63,15 @@ export function useCommandBarNavigationState({
63
63
  const currentRouteRef = useRef<CommandBarRoute | null>(currentRoute);
64
64
  currentRouteRef.current = currentRoute;
65
65
 
66
+ useEffect(() => {
67
+ if (currentRouteRef.current) return;
68
+ if (initialQuery === rootQueryRef.current) return;
69
+ rootQueryRef.current = initialQuery;
70
+ setRootQueryValue(initialQuery);
71
+ setRootSelectedIdx(0);
72
+ setRootHoveredIdx(null);
73
+ }, [initialQuery]);
74
+
66
75
  const closeAll = useCallback((options?: CloseCommandBarOptions) => {
67
76
  if (options?.revertThemePreview !== false) {
68
77
  restoreThemePreview();
@@ -129,6 +129,20 @@ describe("CommandBar", () => {
129
129
  expect(testSetup.captureCharFrame()).not.toContain("Commands");
130
130
  });
131
131
 
132
+ test("shows theme picker rows and commits a filtered light theme", async () => {
133
+ testSetup = await testRender(<CommandBarHarness query="TH light" live />, {
134
+ width: 80,
135
+ height: 24,
136
+ });
137
+
138
+ await testSetup.renderOnce();
139
+ expect(testSetup.captureCharFrame()).toContain("GitHub Light");
140
+
141
+ await clickFrameText("GitHub Light");
142
+ await waitForFrameToContain("theme:github-light");
143
+ expect(testSetup.captureCharFrame()).not.toContain("GitHub Light");
144
+ });
145
+
132
146
  test("runs plugin command shortcuts from the root query", async () => {
133
147
  const calls: string[] = [];
134
148
 
@@ -4,13 +4,14 @@ import {
4
4
  useCallback,
5
5
  useEffect,
6
6
  useImperativeHandle,
7
- useLayoutEffect,
8
7
  useMemo,
9
8
  useRef,
10
9
  useState,
11
10
  } from "react";
12
- import { Box, ScrollBox, Text } from "../../ui";
13
- import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
11
+ import { Box, Text } from "../../ui";
12
+ import { TextAttributes } from "../../ui";
13
+ import { ListView, type ListViewItem } from "../ui";
14
+ import type { ListRowState } from "../ui/list-view";
14
15
  import { getThemeIds, themes as themeRegistry } from "../../theme/themes";
15
16
  import { truncateText } from "./view-model";
16
17
 
@@ -75,7 +76,6 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
75
76
  onPreview,
76
77
  onCommit,
77
78
  }: ThemePickerProps, ref) {
78
- const scrollRef = useRef<ScrollBoxRenderable | null>(null);
79
79
  const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
80
80
  const pendingPreviewIdRef = useRef<string | null>(null);
81
81
  const committedThemeIdRef = useRef(committedThemeId);
@@ -92,9 +92,20 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
92
92
  const [selectedIndex, setSelectedIndex] = useState(() => (
93
93
  Math.max(0, themes.findIndex((theme) => theme.id === committedThemeId))
94
94
  ));
95
- const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
96
95
  const themesRef = useRef(themes);
97
96
  const selectedIndexRef = useRef(selectedIndex);
97
+ const items = useMemo<ListViewItem[]>(() => themes.map((theme) => {
98
+ const current = theme.id === committedThemeId;
99
+ return {
100
+ id: theme.id,
101
+ label: theme.name,
102
+ detail: current ? "current" : "",
103
+ category: "Themes",
104
+ kind: "theme",
105
+ right: current ? "current" : "",
106
+ current,
107
+ };
108
+ }), [committedThemeId, themes]);
98
109
 
99
110
  themesRef.current = themes;
100
111
  selectedIndexRef.current = selectedIndex;
@@ -131,7 +142,6 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
131
142
  if (nextIndex === selectedIndexRef.current) return false;
132
143
  selectedIndexRef.current = nextIndex;
133
144
  setSelectedIndex(nextIndex);
134
- setHoveredIndex(null);
135
145
  requestPreview(options[nextIndex]!.id);
136
146
  return true;
137
147
  }, [requestPreview]);
@@ -155,24 +165,10 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
155
165
  const nextIndex = preferredIndex >= 0 ? preferredIndex : 0;
156
166
  selectedIndexRef.current = nextIndex;
157
167
  setSelectedIndex(nextIndex);
158
- setHoveredIndex(null);
159
168
  }, [committedThemeId, themes]);
160
169
 
161
170
  useEffect(() => cancelPreview, [cancelPreview]);
162
171
 
163
- useLayoutEffect(() => {
164
- const scrollBox = scrollRef.current;
165
- if (!scrollBox) return;
166
- if (scrollBox.verticalScrollBar) scrollBox.verticalScrollBar.visible = false;
167
- if (themes.length === 0) return;
168
- const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? height);
169
- if (selectedIndex < scrollBox.scrollTop) {
170
- scrollBox.scrollTo(selectedIndex);
171
- } else if (selectedIndex >= scrollBox.scrollTop + viewportHeight) {
172
- scrollBox.scrollTo(selectedIndex - viewportHeight + 1);
173
- }
174
- }, [height, selectedIndex, themes.length]);
175
-
176
172
  const handleScroll = useCallback((event: ThemePickerScrollEvent) => {
177
173
  event.stopPropagation();
178
174
  event.preventDefault();
@@ -185,70 +181,86 @@ export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(
185
181
  }
186
182
  }, [move]);
187
183
 
188
- if (themes.length === 0) {
184
+ const handleSelect = useCallback((index: number) => {
185
+ const selected = themesRef.current[index];
186
+ if (!selected) return;
187
+ selectedIndexRef.current = index;
188
+ setSelectedIndex(index);
189
+ requestPreview(selected.id);
190
+ }, [requestPreview]);
191
+
192
+ const handleActivate = useCallback((item: ListViewItem, index: number) => {
193
+ const selected = themesRef.current[index] ?? themesRef.current.find((theme) => theme.id === item.id);
194
+ if (!selected) return;
195
+ selectedIndexRef.current = index;
196
+ setSelectedIndex(index);
197
+ cancelPreview();
198
+ onCommitRef.current(selected.id);
199
+ }, [cancelPreview]);
200
+
201
+ const renderRow = useCallback((item: ListViewItem, state: ListRowState) => {
202
+ const label = truncateText(item.label, labelWidth);
203
+ const trailing = item.current ? "current" : "";
189
204
  return (
190
- <Box height={height} paddingX={contentPadding}>
191
- <Text fg={paletteSubtleText}>{truncateText("No themes match", queryDisplayWidth)}</Text>
205
+ <Box
206
+ flexDirection="row"
207
+ height={1}
208
+ paddingX={contentPadding}
209
+ width="100%"
210
+ data-command-bar-row-selected={nativePaneChrome && state.selected ? "true" : undefined}
211
+ style={nativePaneChrome ? { borderRadius: 6 } : undefined}
212
+ >
213
+ <Box width={labelWidth}>
214
+ <Text
215
+ fg={state.selected ? paletteSelectedText : paletteText}
216
+ attributes={item.current ? TextAttributes.BOLD : undefined}
217
+ >
218
+ {label}
219
+ </Text>
220
+ </Box>
221
+ <Box width={trailingWidth}>
222
+ <Text fg={state.selected ? paletteSelectedText : paletteSubtleText}>
223
+ {truncateText(trailing, trailingWidth)}
224
+ </Text>
225
+ </Box>
192
226
  </Box>
193
227
  );
194
- }
228
+ }, [
229
+ contentPadding,
230
+ labelWidth,
231
+ nativePaneChrome,
232
+ paletteSelectedText,
233
+ paletteSubtleText,
234
+ paletteText,
235
+ trailingWidth,
236
+ ]);
195
237
 
196
238
  return (
197
- <ScrollBox
198
- ref={scrollRef}
199
- flexDirection="column"
239
+ <ListView
240
+ items={items}
241
+ selectedIndex={selectedIndex}
200
242
  height={height}
201
- scrollY
202
- focusable={false}
203
- {...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
204
- >
205
- {themes.map((theme, index) => {
206
- const selected = index === selectedIndex;
207
- const hovered = index === hoveredIndex && !selected;
208
- const current = theme.id === committedThemeId;
209
- const label = truncateText(theme.name, labelWidth);
210
- const trailing = current ? "current" : "";
211
- return (
212
- <Box
213
- key={theme.id}
214
- flexDirection="row"
215
- height={1}
216
- paddingX={contentPadding}
217
- backgroundColor={selected
218
- ? paletteSelectedBg
219
- : hovered
220
- ? paletteHoverBg
221
- : (nativePaneChrome ? panelBg : paletteBg)}
222
- onMouseOver={() => setHoveredIndex(index)}
223
- onMouseOut={() => setHoveredIndex(null)}
224
- onMouseDown={(event: any) => {
225
- event.stopPropagation?.();
226
- event.preventDefault?.();
227
- selectedIndexRef.current = index;
228
- setSelectedIndex(index);
229
- cancelPreview();
230
- onCommitRef.current(theme.id);
231
- }}
232
- {...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
233
- data-command-bar-row-selected={nativePaneChrome && selected ? "true" : undefined}
234
- style={nativePaneChrome ? { borderRadius: 6 } : undefined}
235
- >
236
- <Box width={labelWidth}>
237
- <Text
238
- fg={selected ? paletteSelectedText : paletteText}
239
- attributes={current ? TextAttributes.BOLD : undefined}
240
- >
241
- {label}
242
- </Text>
243
- </Box>
244
- <Box width={trailingWidth}>
245
- <Text fg={selected ? paletteSelectedText : paletteSubtleText}>
246
- {truncateText(trailing, trailingWidth)}
247
- </Text>
248
- </Box>
249
- </Box>
250
- );
251
- })}
252
- </ScrollBox>
243
+ scrollable
244
+ rowGap={0}
245
+ rowHeight={1}
246
+ surface="plain"
247
+ bgColor={nativePaneChrome ? panelBg : paletteBg}
248
+ selectedBgColor={paletteSelectedBg}
249
+ hoverBgColor={paletteHoverBg}
250
+ emptyMessage={truncateText("No themes match", queryDisplayWidth)}
251
+ showSelectedDescription={false}
252
+ onSelect={handleSelect}
253
+ onActivate={handleActivate}
254
+ onMouseScroll={!nativePaneChrome ? handleScroll : undefined}
255
+ renderRow={renderRow}
256
+ remoteLabel="Theme picker"
257
+ remoteScope="command-bar"
258
+ remoteItemKind="theme"
259
+ remoteItemCategory="Themes"
260
+ remoteMetadata={{
261
+ surface: "theme-picker",
262
+ filter: normalizedFilter,
263
+ }}
264
+ />
253
265
  );
254
266
  }));
@@ -15,6 +15,7 @@ import {
15
15
  isWorkflowTextField,
16
16
  summarizeWorkflowFieldValue,
17
17
  } from "../helpers";
18
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
18
19
  import type {
19
20
  CommandBarFieldValue,
20
21
  CommandBarWorkflowField,
@@ -84,6 +85,34 @@ export function CommandBarWorkflowFieldRow({
84
85
  onMoveFieldFocus(1);
85
86
  }
86
87
  };
88
+ const focusOrOpenField = () => {
89
+ onActiveTextareaSync(route);
90
+ onFieldFocus(field.id);
91
+ if (!isWorkflowTextField(field) && !useNativeSelect) {
92
+ onFieldPickerOpen(route, field);
93
+ }
94
+ };
95
+
96
+ useRemoteUiNode({
97
+ role: "command-bar-field",
98
+ label: field.label,
99
+ disabled: route.pending,
100
+ actions: {
101
+ focus: focusOrOpenField,
102
+ open: !isWorkflowTextField(field) && !useNativeSelect ? focusOrOpenField : undefined,
103
+ submit: () => void onSubmit(route),
104
+ },
105
+ metadata: {
106
+ scope: "command-bar",
107
+ surface: "workflow",
108
+ routeTitle: route.title,
109
+ fieldId: field.id,
110
+ fieldType: field.type,
111
+ active,
112
+ value: summarizeWorkflowFieldValue(field, value),
113
+ description: fieldDescription,
114
+ },
115
+ });
87
116
 
88
117
  return (
89
118
  <Box
@@ -93,11 +122,7 @@ export function CommandBarWorkflowFieldRow({
93
122
  backgroundColor={fieldBg}
94
123
  onMouseDown={(event: any) => {
95
124
  event.stopPropagation?.();
96
- onActiveTextareaSync(route);
97
- onFieldFocus(field.id);
98
- if (!isWorkflowTextField(field) && !useNativeSelect) {
99
- onFieldPickerOpen(route, field);
100
- }
125
+ focusOrOpenField();
101
126
  }}
102
127
  style={nativePaneChrome ? {
103
128
  marginBottom: isLastField ? 8 : 10,
@@ -1,6 +1,13 @@
1
1
 
2
2
  export { PriceSelectorDialog } from "./price-selector-dialog";
3
3
  export { StaticChartSurface } from "./chart/static/chart/surface";
4
+ export {
5
+ buildMetricTreemapNavigationTiles,
6
+ findMetricTreemapNeighbor,
7
+ MetricTreemapSurface,
8
+ type MetricTreemapDirection,
9
+ type MetricTreemapItem,
10
+ } from "./metric-treemap";
4
11
  export { SpeedometerGauge } from "./speedometer-gauge";
5
12
  export type { SpeedometerSegment } from "./speedometer-gauge";
6
13
  export { TickerListTableView } from "./ticker/list-table-view";
@@ -2,6 +2,7 @@ import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../../
2
2
  import { useRef } from "react";
3
3
  import { colors, blendHex } from "../../../../theme/colors";
4
4
  import { getShortcutHintWidth, ShortcutHint } from "../../../ui/shortcut-hint";
5
+ import { useRemoteUiNode } from "../../../../remote/semantic-tree";
5
6
  import {
6
7
  EMPTY_FOOTER,
7
8
  hasPaneFooterContent,
@@ -50,6 +51,15 @@ function stopMouseEvent(event?: { stopPropagation?: () => void; preventDefault?:
50
51
 
51
52
  function SegmentView({ segment }: { segment: PaneFooterSegment }) {
52
53
  const interactive = !!segment.onPress && !segment.disabled;
54
+ useRemoteUiNode(interactive ? {
55
+ role: "pane-footer-segment",
56
+ label: segment.parts.map((part) => part.text).join(" "),
57
+ disabled: segment.disabled,
58
+ actions: {
59
+ press: () => segment.onPress?.(),
60
+ },
61
+ metadata: { id: segment.id },
62
+ } : null);
53
63
  const attributes = segment.parts.some((part) => part.bold) || interactive ? TextAttributes.BOLD : 0;
54
64
  const triggerMouseDownRef = useRef(false);
55
65
  const startSegmentPress = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
@@ -93,6 +103,19 @@ function totalHintsWidth(hints: PaneHint[]): number {
93
103
  }
94
104
 
95
105
  function HintView({ hint, prefixSpace }: { hint: PaneHint; prefixSpace: boolean }) {
106
+ useRemoteUiNode({
107
+ role: "pane-hint",
108
+ label: `${hint.key}${hint.label}`,
109
+ disabled: hint.disabled,
110
+ actions: {
111
+ press: hint.onPress ? () => hint.onPress?.() : undefined,
112
+ },
113
+ metadata: {
114
+ id: hint.id,
115
+ key: hint.key,
116
+ label: hint.label,
117
+ },
118
+ });
96
119
  return (
97
120
  <ShortcutHint
98
121
  hotkey={hint.key}
@@ -1271,6 +1271,7 @@ describe("Shell", () => {
1271
1271
  layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
1272
1272
  }),
1273
1273
  focusedPaneId: "ticker-detail:main",
1274
+ previousFocusedPaneId: "portfolio-list:main",
1274
1275
  };
1275
1276
  const actions: Array<any> = [];
1276
1277
 
@@ -1294,6 +1295,7 @@ describe("Shell", () => {
1294
1295
  expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
1295
1296
  expect(updateLayout?.layout.floating).toEqual([]);
1296
1297
  expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
1298
+ expect(updateLayout?.focusedPaneId).toBe("portfolio-list:main");
1297
1299
  });
1298
1300
 
1299
1301
  test("closes the focused floating pane with Cmd+W", async () => {
@@ -95,6 +95,7 @@ export function Shell({
95
95
  const config = useAppSelector((state) => state.config);
96
96
  const paneState = useAppSelector((state) => state.paneState);
97
97
  const focusedPaneId = useAppSelector(selectFocusedPaneId);
98
+ const previousFocusedPaneId = useAppSelector((state) => state.previousFocusedPaneId);
98
99
  const activePanel = useAppSelector((state) => state.activePanel);
99
100
  const commandBarOpen = useAppSelector(selectCommandBarOpen);
100
101
  const inputCaptured = useAppSelector((state) => state.inputCaptured);
@@ -154,15 +155,18 @@ export function Shell({
154
155
  ), [nativePaneChrome]);
155
156
  const bounds = useMemo<LayoutBounds>(() => ({ x: 0, y: 0, width, height: contentHeight }), [contentHeight, width]);
156
157
 
157
- const persistLayout = useCallback((nextLayout: LayoutConfig, options?: { pushHistory?: boolean }) => {
158
+ const persistLayout = useCallback((nextLayout: LayoutConfig, options?: { pushHistory?: boolean; focusedPaneId?: string | null }) => {
158
159
  if (options?.pushHistory !== false) {
159
160
  dispatch({ type: "PUSH_LAYOUT_HISTORY" });
160
161
  }
161
- dispatch({ type: "UPDATE_LAYOUT", layout: nextLayout });
162
+ const hasFocusTarget = !!options && Object.prototype.hasOwnProperty.call(options, "focusedPaneId");
163
+ dispatch(hasFocusTarget
164
+ ? { type: "UPDATE_LAYOUT", layout: nextLayout, focusedPaneId: options.focusedPaneId ?? null }
165
+ : { type: "UPDATE_LAYOUT", layout: nextLayout });
162
166
  scheduleConfigSave(syncConfigActiveLayoutState(
163
167
  { ...config, layout: nextLayout },
164
168
  paneState,
165
- focusedPaneId,
169
+ hasFocusTarget ? (options.focusedPaneId ?? null) : focusedPaneId,
166
170
  activePanel,
167
171
  ));
168
172
  }, [activePanel, config, dispatch, focusedPaneId, paneState]);
@@ -261,6 +265,7 @@ export function Shell({
261
265
  nativePaneChrome,
262
266
  paneMap,
263
267
  persistLayout,
268
+ previousFocusedPaneId,
264
269
  pluginRegistry,
265
270
  rendererHost,
266
271
  visibleLayout,
@@ -14,6 +14,15 @@ import type { LayoutConfig } from "../../../../types/config";
14
14
  import type { RendererHost } from "../../../../ui";
15
15
  import { capturePaneScreenshotPngBase64 } from "../../../../utils/dom-screenshot";
16
16
 
17
+ function removedFocusRestoreOptions(
18
+ layout: LayoutConfig,
19
+ currentFocusedPaneId: string | null,
20
+ paneId: string | null,
21
+ ): { focusedPaneId: string } | undefined {
22
+ if (currentFocusedPaneId && isPaneInLayout(layout, currentFocusedPaneId)) return undefined;
23
+ return paneId && isPaneInLayout(layout, paneId) ? { focusedPaneId: paneId } : undefined;
24
+ }
25
+
17
26
  interface UseShellPaneActionsOptions {
18
27
  closePaneMenu: () => void;
19
28
  contentHeight: number;
@@ -22,7 +31,8 @@ interface UseShellPaneActionsOptions {
22
31
  focusPane: (paneId: string) => void;
23
32
  nativePaneChrome: boolean;
24
33
  paneMap: Map<string, ResolvedPane>;
25
- persistLayout: (nextLayout: LayoutConfig, options?: { pushHistory?: boolean }) => void;
34
+ persistLayout: (nextLayout: LayoutConfig, options?: { pushHistory?: boolean; focusedPaneId?: string | null }) => void;
35
+ previousFocusedPaneId: string | null;
26
36
  pluginRegistry: PluginRegistry;
27
37
  rendererHost: RendererHost;
28
38
  visibleLayout: LayoutConfig;
@@ -38,6 +48,7 @@ export function useShellPaneActions({
38
48
  nativePaneChrome,
39
49
  paneMap,
40
50
  persistLayout,
51
+ previousFocusedPaneId,
41
52
  pluginRegistry,
42
53
  rendererHost,
43
54
  visibleLayout,
@@ -70,15 +81,17 @@ export function useShellPaneActions({
70
81
 
71
82
  const closeFocusedPane = useCallback(() => {
72
83
  if (!focusedPaneId || !isPaneInLayout(visibleLayout, focusedPaneId)) return false;
73
- persistLayout(removePane(visibleLayout, focusedPaneId));
84
+ const nextLayout = removePane(visibleLayout, focusedPaneId);
85
+ persistLayout(nextLayout, removedFocusRestoreOptions(nextLayout, focusedPaneId, previousFocusedPaneId));
74
86
  return true;
75
- }, [focusedPaneId, persistLayout, visibleLayout]);
87
+ }, [focusedPaneId, persistLayout, previousFocusedPaneId, visibleLayout]);
76
88
 
77
89
  const closeAllFloatingPanes = useCallback(() => {
78
90
  if (visibleLayout.floating.length === 0) return false;
79
- persistLayout(removeFloatingPanes(visibleLayout));
91
+ const nextLayout = removeFloatingPanes(visibleLayout);
92
+ persistLayout(nextLayout, removedFocusRestoreOptions(nextLayout, focusedPaneId, previousFocusedPaneId));
80
93
  return true;
81
- }, [persistLayout, visibleLayout]);
94
+ }, [focusedPaneId, persistLayout, previousFocusedPaneId, visibleLayout]);
82
95
 
83
96
  const copyFocusedPaneScreenshot = useCallback(() => {
84
97
  if (!focusedPaneId || !nativePaneChrome || !rendererHost.copyPngImage) return false;
@@ -118,8 +131,9 @@ export function useShellPaneActions({
118
131
  }, [contentHeight, persistLayout, visibleLayout, width]);
119
132
 
120
133
  const handleFloatingClose = useCallback((paneId: string) => {
121
- persistLayout(removePane(visibleLayout, paneId));
122
- }, [persistLayout, visibleLayout]);
134
+ const nextLayout = removePane(visibleLayout, paneId);
135
+ persistLayout(nextLayout, removedFocusRestoreOptions(nextLayout, focusedPaneId, previousFocusedPaneId));
136
+ }, [focusedPaneId, persistLayout, previousFocusedPaneId, visibleLayout]);
123
137
 
124
138
  return {
125
139
  closeAllFloatingPanes,