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
@@ -2,7 +2,6 @@ import {
2
2
  useCallback,
3
3
  useEffect,
4
4
  useRef,
5
- useState,
6
5
  type ReactNode,
7
6
  type RefObject,
8
7
  } from "react";
@@ -114,23 +113,16 @@ export function useTableViewState({
114
113
  headerScrollRef,
115
114
  scrollRef,
116
115
  syncHeaderScroll,
117
- hoveredIdx,
118
- setHoveredIdx,
119
116
  }: {
120
117
  headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
121
118
  scrollRef?: RefObject<ScrollBoxRenderable | null>;
122
119
  syncHeaderScroll?: () => void;
123
- hoveredIdx?: number | null;
124
- setHoveredIdx?: (index: number | null) => void;
125
120
  }) {
126
121
  const internalHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
127
122
  const internalScrollRef = useRef<ScrollBoxRenderable>(null);
128
- const [internalHoveredIdx, setInternalHoveredIdx] = useState<number | null>(null);
129
123
 
130
124
  const effectiveHeaderScrollRef = headerScrollRef ?? internalHeaderScrollRef;
131
125
  const effectiveScrollRef = scrollRef ?? internalScrollRef;
132
- const effectiveHoveredIdx = hoveredIdx !== undefined ? hoveredIdx : internalHoveredIdx;
133
- const effectiveSetHoveredIdx = setHoveredIdx ?? setInternalHoveredIdx;
134
126
 
135
127
  const defaultSyncHeaderScroll = useCallback(() => {
136
128
  const body = effectiveScrollRef.current;
@@ -144,8 +136,6 @@ export function useTableViewState({
144
136
  return {
145
137
  effectiveHeaderScrollRef,
146
138
  effectiveScrollRef,
147
- effectiveHoveredIdx,
148
- effectiveSetHoveredIdx,
149
139
  effectiveSyncHeaderScroll: syncHeaderScroll ?? defaultSyncHeaderScroll,
150
140
  };
151
141
  }
@@ -56,8 +56,6 @@ export interface TickerListTableViewProps {
56
56
  scrollRef?: RefObject<ScrollBoxRenderable | null>;
57
57
  syncHeaderScroll?: () => void;
58
58
  onBodyScrollActivity?: () => void;
59
- hoveredIdx?: number | null;
60
- setHoveredIdx?: (index: number | null) => void;
61
59
  keyboardNavigation?: boolean;
62
60
  onRootKeyDown?: (event: DataTableKeyEvent) => boolean | void;
63
61
  onVisibleRangeChange?: (range: TickerListVisibleRange) => void;
@@ -119,8 +117,6 @@ export function TickerListTableView({
119
117
  scrollRef,
120
118
  syncHeaderScroll,
121
119
  onBodyScrollActivity,
122
- hoveredIdx,
123
- setHoveredIdx,
124
120
  keyboardNavigation = true,
125
121
  onRootKeyDown,
126
122
  onVisibleRangeChange,
@@ -187,7 +183,7 @@ export function TickerListTableView({
187
183
  ticker: TickerRecord,
188
184
  column: ColumnConfig,
189
185
  _index: number,
190
- rowState: { selected: boolean; hovered: boolean },
186
+ rowState: { selected: boolean },
191
187
  ) => {
192
188
  const financials = financialsMap.get(ticker.metadata.ticker);
193
189
  if (column.id === PRICE_SPARKLINE_COLUMN_ID) {
@@ -279,8 +275,6 @@ export function TickerListTableView({
279
275
  scrollRef={effectiveScrollRef}
280
276
  syncHeaderScroll={syncHeaderScroll}
281
277
  onBodyScrollActivity={handleBodyScrollActivity}
282
- hoveredIdx={hoveredIdx}
283
- setHoveredIdx={setHoveredIdx}
284
278
  keyboardNavigation={keyboardNavigation}
285
279
  onRootKeyDown={onRootKeyDown}
286
280
  resetScrollKey={resetScrollKey}
@@ -32,8 +32,6 @@ const manyTickers: TickerRecord[] = Array.from({ length: 1000 }, (_, index) => (
32
32
  },
33
33
  }));
34
34
 
35
- function noop(_index: number | null): void {}
36
-
37
35
  function resolveCell(_column: ColumnConfig, ticker: TickerRecord, _financials: TickerFinancials | undefined): TickerTableCell {
38
36
  resolveCellCallCount += 1;
39
37
  return { text: ticker.metadata.ticker };
@@ -69,8 +67,6 @@ function LargeTickerListTableHarness() {
69
67
  columns={columns}
70
68
  tickers={manyTickers}
71
69
  cursorSymbol={cursorSymbol}
72
- hoveredIdx={null}
73
- setHoveredIdx={noop}
74
70
  setCursorSymbol={setCursorSymbol}
75
71
  resolveCell={resolveCell}
76
72
  financialsMap={financialsMap}
@@ -26,6 +26,9 @@ export interface ToggleListProps {
26
26
  rowGap?: number;
27
27
  rowHeight?: number;
28
28
  surface?: "framed" | "plain";
29
+ remoteLabel?: string;
30
+ remoteScope?: string;
31
+ remoteMetadata?: Record<string, unknown>;
29
32
  }
30
33
 
31
34
  function DesktopToggleRow({
@@ -124,6 +127,9 @@ export function ToggleList({
124
127
  rowGap,
125
128
  rowHeight,
126
129
  surface,
130
+ remoteLabel,
131
+ remoteScope,
132
+ remoteMetadata,
127
133
  }: ToggleListProps) {
128
134
  const isDesktopWeb = useUiHost().kind === "desktop-web";
129
135
  const listItems: ListViewItem[] = items.map((item) => ({
@@ -145,6 +151,10 @@ export function ToggleList({
145
151
  rowGap={rowGap ?? (isDesktopWeb ? 0 : undefined)}
146
152
  rowHeight={rowHeight}
147
153
  surface={surface}
154
+ remoteLabel={remoteLabel}
155
+ remoteScope={remoteScope}
156
+ remoteMetadata={remoteMetadata}
157
+ remoteItemKind="toggle"
148
158
  onSelect={onSelect}
149
159
  onActivate={(item) => {
150
160
  onToggle?.(item.id);
@@ -2,6 +2,7 @@ import { Box, Text, useUiHost } from "../../ui";
2
2
  import { TextAttributes } from "../../ui";
3
3
  import { type ComponentType } from "react";
4
4
  import { colors } from "../../theme/colors";
5
+ import { useRemoteUiNode } from "../../remote/semantic-tree";
5
6
 
6
7
  export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
7
8
 
@@ -45,6 +46,17 @@ export function Button({
45
46
  shortcut,
46
47
  width,
47
48
  }: ButtonProps) {
49
+ useRemoteUiNode({
50
+ role: "button",
51
+ label,
52
+ disabled,
53
+ actions: {
54
+ press: () => {
55
+ if (!disabled) onPress?.();
56
+ },
57
+ },
58
+ metadata: { variant, active, shortcut },
59
+ });
48
60
  const HostButton = useUiHost().Button as ComponentType<ButtonProps> | undefined;
49
61
  if (HostButton) {
50
62
  return (
@@ -5,6 +5,8 @@ import type {
5
5
  DataTableColumn,
6
6
  DataTableProps,
7
7
  } from "./types";
8
+ import { useRemoteUiNode } from "../../../remote/semantic-tree";
9
+ import { resolveRemoteItemIndex } from "../../../remote/semantic-helpers";
8
10
 
9
11
  export type {
10
12
  DataTableCell,
@@ -16,6 +18,46 @@ export type {
16
18
  export function DataTable<T, C extends DataTableColumn = DataTableColumn>(
17
19
  props: DataTableProps<T, C>,
18
20
  ) {
21
+ useRemoteUiNode({
22
+ role: "table",
23
+ label: "Data table",
24
+ actions: {
25
+ selectRow: (input) => {
26
+ const index = resolveTableIndex(input, props);
27
+ const item = index >= 0 ? props.items[index] : undefined;
28
+ if (item) props.onSelect(item, index);
29
+ },
30
+ activateRow: (input) => {
31
+ const index = resolveTableIndex(input, props);
32
+ const item = index >= 0 ? props.items[index] : undefined;
33
+ if (item) {
34
+ props.onSelect(item, index);
35
+ props.onActivate?.(item, index);
36
+ }
37
+ },
38
+ sort: (input) => {
39
+ const columnId = typeof input === "string"
40
+ ? input
41
+ : input && typeof input === "object" && typeof (input as { columnId?: unknown }).columnId === "string"
42
+ ? (input as { columnId: string }).columnId
43
+ : null;
44
+ if (columnId && props.columns.some((column) => column.id === columnId)) {
45
+ props.onHeaderClick(columnId);
46
+ }
47
+ },
48
+ },
49
+ metadata: {
50
+ sortColumnId: props.sortColumnId,
51
+ sortDirection: props.sortDirection,
52
+ columns: props.columns.map((column) => ({ id: column.id, label: column.label })),
53
+ rows: props.items.slice(0, 200).map((item, index) => ({
54
+ index,
55
+ key: props.getItemKey(item, index),
56
+ selected: props.isSelected(item, index),
57
+ })),
58
+ rowCount: props.items.length,
59
+ },
60
+ });
19
61
  const HostDataTable = useUiHost().DataTable as
20
62
  | ComponentType<DataTableProps<T, C>>
21
63
  | undefined;
@@ -24,3 +66,12 @@ export function DataTable<T, C extends DataTableColumn = DataTableColumn>(
24
66
  }
25
67
  return <OpenTuiDataTable {...props} />;
26
68
  }
69
+
70
+ function resolveTableIndex<T, C extends DataTableColumn>(
71
+ input: unknown,
72
+ props: DataTableProps<T, C>,
73
+ ): number {
74
+ return resolveRemoteItemIndex(input, props.items, {
75
+ key: (item, index) => props.getItemKey(item, index),
76
+ });
77
+ }
@@ -40,8 +40,6 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
40
40
  scrollRef,
41
41
  syncHeaderScroll,
42
42
  onBodyScrollActivity,
43
- hoveredIdx,
44
- setHoveredIdx,
45
43
  headerScrollId,
46
44
  bodyScrollId,
47
45
  getItemKey,
@@ -369,8 +367,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
369
367
  }
370
368
 
371
369
  const selected = isSelected(item, index);
372
- const hovered = hoveredIdx === index && !selected;
373
- const rowState = { selected, hovered };
370
+ const rowState = { selected };
374
371
  const rowBackgroundColor = getRowBackgroundColor?.(
375
372
  item,
376
373
  index,
@@ -378,9 +375,8 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
378
375
  );
379
376
  const rowBg = selected
380
377
  ? colors.selected
381
- : hovered
382
- ? hoverBg()
383
- : rowBackgroundColor ?? colors.bg;
378
+ : rowBackgroundColor ?? colors.bg;
379
+ const rowHoverBg = selected ? undefined : hoverBg();
384
380
 
385
381
  return (
386
382
  <Box
@@ -390,10 +386,8 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
390
386
  {...tableContentWidthProps(contentWidth)}
391
387
  paddingX={horizontalPadding}
392
388
  backgroundColor={rowBg}
389
+ hoverBackgroundColor={rowHoverBg}
393
390
  data-gloom-context-menu-surface={rowContextMenuSurface ? "true" : undefined}
394
- onMouseMove={() => {
395
- if (hoveredIdx !== index) setHoveredIdx(index);
396
- }}
397
391
  onMouseDown={(event: any) => {
398
392
  focusPane();
399
393
  onTableMouseDown?.(event);
@@ -29,6 +29,10 @@ export interface DataTableSectionHeader {
29
29
 
30
30
  export type DataTableScrollAlign = "nearest" | "center";
31
31
 
32
+ export interface DataTableRowState {
33
+ selected: boolean;
34
+ }
35
+
32
36
  export interface DataTableProps<
33
37
  T,
34
38
  C extends DataTableColumn = DataTableColumn,
@@ -42,8 +46,6 @@ export interface DataTableProps<
42
46
  scrollRef: RefObject<ScrollBoxRenderable | null>;
43
47
  syncHeaderScroll: () => void;
44
48
  onBodyScrollActivity: () => void;
45
- hoveredIdx: number | null;
46
- setHoveredIdx: (index: number | null) => void;
47
49
  headerScrollId?: string;
48
50
  bodyScrollId?: string;
49
51
  getItemKey: (item: T, index: number) => string;
@@ -58,7 +60,7 @@ export interface DataTableProps<
58
60
  item: T,
59
61
  column: C,
60
62
  index: number,
61
- rowState: { selected: boolean; hovered: boolean },
63
+ rowState: DataTableRowState,
62
64
  ) => DataTableCell;
63
65
  renderSectionHeader?: (
64
66
  item: T,
@@ -67,7 +69,7 @@ export interface DataTableProps<
67
69
  getRowBackgroundColor?: (
68
70
  item: T,
69
71
  index: number,
70
- rowState: { selected: boolean; hovered: boolean },
72
+ rowState: DataTableRowState,
71
73
  ) => string | undefined;
72
74
  emptyContent?: ReactNode;
73
75
  bodyAfter?: ReactNode;
@@ -2,6 +2,8 @@ import { Box, Input, Span, Text, useUiHost } from "../../ui";
2
2
  import { useEffect, useRef, useState, type ComponentType, type RefObject } from "react";
3
3
  import { type InputRenderable } from "../../ui";
4
4
  import { colors } from "../../theme/colors";
5
+ import { useRemoteUiNode } from "../../remote/semantic-tree";
6
+ import { remoteStringValue } from "../../remote/semantic-helpers";
5
7
 
6
8
  export interface TextFieldProps {
7
9
  label?: string;
@@ -46,6 +48,22 @@ export function TextField({
46
48
  placeholderColor = colors.textDim,
47
49
  onMouseDown,
48
50
  }: TextFieldProps) {
51
+ useRemoteUiNode({
52
+ role: "text-field",
53
+ label: label ?? placeholder,
54
+ actions: {
55
+ setValue: (input) => {
56
+ const nextValue = remoteStringValue(input);
57
+ onChange?.(nextValue);
58
+ },
59
+ submit: (input) => {
60
+ const nextValue = remoteStringValue(input, value ?? "");
61
+ onSubmit?.(nextValue);
62
+ },
63
+ blur: () => onBlur?.(value ?? ""),
64
+ },
65
+ metadata: { value, placeholder, focused, type, variant },
66
+ });
49
67
  const HostTextField = useUiHost().TextField as ComponentType<TextFieldProps> | undefined;
50
68
  if (HostTextField) {
51
69
  return (
@@ -2,12 +2,19 @@ import { Box, ScrollBox, Text, useUiHost } from "../../ui";
2
2
  import { useEffect, useRef, useState, type ComponentType, type ReactNode } from "react";
3
3
  import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
4
4
  import { colors, hoverBg } from "../../theme/colors";
5
+ import { useRemoteUiNode } from "../../remote/semantic-tree";
6
+ import { resolveRemoteItemIndex } from "../../remote/semantic-helpers";
5
7
 
6
8
  export interface ListViewItem {
7
9
  id: string;
8
10
  label: string;
9
11
  description?: string;
10
12
  detail?: string;
13
+ category?: string;
14
+ kind?: string;
15
+ right?: string;
16
+ checked?: boolean;
17
+ current?: boolean;
11
18
  disabled?: boolean;
12
19
  }
13
20
 
@@ -38,6 +45,13 @@ export interface ListViewProps {
38
45
  scrollable?: boolean;
39
46
  selectOnHover?: boolean;
40
47
  autoScrollToIndex?: boolean;
48
+ onMouseScroll?: (event: any) => void;
49
+ remoteRole?: string;
50
+ remoteLabel?: string;
51
+ remoteScope?: string;
52
+ remoteItemKind?: string;
53
+ remoteItemCategory?: string;
54
+ remoteMetadata?: Record<string, unknown>;
41
55
  }
42
56
 
43
57
  function DefaultRow({
@@ -88,7 +102,49 @@ export function ListView({
88
102
  scrollable = false,
89
103
  selectOnHover = false,
90
104
  autoScrollToIndex = true,
105
+ onMouseScroll,
106
+ remoteRole = "list",
107
+ remoteLabel,
108
+ remoteScope,
109
+ remoteItemKind,
110
+ remoteItemCategory,
111
+ remoteMetadata,
91
112
  }: ListViewProps) {
113
+ useRemoteUiNode({
114
+ role: remoteRole,
115
+ label: remoteLabel ?? emptyMessage,
116
+ actions: {
117
+ select: (input) => {
118
+ const index = resolveListIndex(input, items);
119
+ if (index >= 0 && !items[index]?.disabled) onSelect?.(index);
120
+ },
121
+ activate: (input) => {
122
+ const index = resolveListIndex(input, items);
123
+ const item = index >= 0 ? items[index] : undefined;
124
+ if (item && !item.disabled) {
125
+ onSelect?.(index);
126
+ onActivate?.(item, index);
127
+ }
128
+ },
129
+ },
130
+ metadata: {
131
+ ...remoteMetadata,
132
+ scope: remoteScope,
133
+ selectedIndex,
134
+ items: items.map((item, index) => ({
135
+ index,
136
+ id: item.id,
137
+ label: item.label,
138
+ detail: item.detail,
139
+ category: item.category ?? remoteItemCategory,
140
+ kind: item.kind ?? remoteItemKind,
141
+ right: item.right,
142
+ checked: item.checked,
143
+ current: item.current,
144
+ disabled: item.disabled === true,
145
+ })),
146
+ },
147
+ });
92
148
  const HostListView = useUiHost().ListView as ComponentType<ListViewProps> | undefined;
93
149
  if (HostListView) {
94
150
  return (
@@ -113,6 +169,13 @@ export function ListView({
113
169
  scrollable={scrollable}
114
170
  selectOnHover={selectOnHover}
115
171
  autoScrollToIndex={autoScrollToIndex}
172
+ onMouseScroll={onMouseScroll}
173
+ remoteRole={remoteRole}
174
+ remoteLabel={remoteLabel}
175
+ remoteScope={remoteScope}
176
+ remoteItemKind={remoteItemKind}
177
+ remoteItemCategory={remoteItemCategory}
178
+ remoteMetadata={remoteMetadata}
116
179
  />
117
180
  );
118
181
  }
@@ -130,7 +193,7 @@ export function ListView({
130
193
  const sb = scrollRef.current;
131
194
  if (!sb) return;
132
195
  const safeIndex = Math.min(activeScrollIndex, items.length - 1);
133
- const viewportH = Math.max(sb.viewport.height, 1);
196
+ const viewportH = Math.max(sb.viewport?.height ?? 1, 1);
134
197
  if (safeIndex < sb.scrollTop) {
135
198
  sb.scrollTo(safeIndex);
136
199
  } else if (safeIndex >= sb.scrollTop + viewportH) {
@@ -142,7 +205,9 @@ export function ListView({
142
205
  if (!scrollable) return;
143
206
  const sb = scrollRef.current;
144
207
  if (!sb) return;
145
- sb.verticalScrollBar.visible = items.length > sb.viewport.height;
208
+ if (sb.verticalScrollBar) {
209
+ sb.verticalScrollBar.visible = items.length > (sb.viewport?.height ?? 0);
210
+ }
146
211
  }, [items.length, height, flexGrow, scrollable]);
147
212
 
148
213
  if (items.length === 0) {
@@ -166,12 +231,13 @@ export function ListView({
166
231
  key={item.id}
167
232
  height={1}
168
233
  backgroundColor={rowBg}
169
- onMouseMove={() => {
234
+ onMouseOver={() => {
170
235
  if (!disabled) {
171
236
  setHoveredIndex((current) => (current === index ? current : index));
172
237
  if (selectOnHover) onSelect?.(index);
173
238
  }
174
239
  }}
240
+ {...(onMouseScroll ? { onMouseScroll } : {})}
175
241
  onMouseDown={() => {
176
242
  if (disabled) return;
177
243
  onSelect?.(index);
@@ -188,7 +254,14 @@ export function ListView({
188
254
  return (
189
255
  <Box flexDirection="column" height={height} flexGrow={flexGrow}>
190
256
  {scrollable ? (
191
- <ScrollBox ref={scrollRef} height={height} flexGrow={flexGrow} scrollY focusable={false}>
257
+ <ScrollBox
258
+ ref={scrollRef}
259
+ height={height}
260
+ flexGrow={flexGrow}
261
+ scrollY
262
+ focusable={false}
263
+ {...(onMouseScroll ? { onMouseScroll } : {})}
264
+ >
192
265
  {rows}
193
266
  </ScrollBox>
194
267
  ) : rows}
@@ -204,3 +277,10 @@ export function ListView({
204
277
  </Box>
205
278
  );
206
279
  }
280
+
281
+ function resolveListIndex(input: unknown, items: ListViewItem[]): number {
282
+ return resolveRemoteItemIndex(input, items, {
283
+ id: (item) => item.id,
284
+ label: (item) => item.label,
285
+ });
286
+ }
@@ -2,6 +2,8 @@ import { Box, Text, Textarea, useUiHost } from "../../ui";
2
2
  import { type ComponentType, type RefObject } from "react";
3
3
  import { type TextareaRenderable } from "../../ui";
4
4
  import { colors } from "../../theme/colors";
5
+ import { useRemoteUiNode } from "../../remote/semantic-tree";
6
+ import { remoteStringValue } from "../../remote/semantic-helpers";
5
7
 
6
8
  export interface MessageComposerProps {
7
9
  inputRef?: RefObject<TextareaRenderable | null>;
@@ -54,6 +56,26 @@ export function MessageComposer({
54
56
  keyBindings,
55
57
  wrapText = false,
56
58
  }: MessageComposerProps) {
59
+ useRemoteUiNode({
60
+ role: "message-composer",
61
+ label: placeholder || "Message composer",
62
+ actions: {
63
+ setValue: (input) => {
64
+ const value = remoteStringValue(input);
65
+ onFocusRequest?.();
66
+ onInput?.(value);
67
+ },
68
+ submit: () => {
69
+ onFocusRequest?.();
70
+ onSubmit?.();
71
+ },
72
+ focus: () => {
73
+ onFocusRequest?.();
74
+ inputRef?.current?.focus?.();
75
+ },
76
+ },
77
+ metadata: { focused, placeholder, height },
78
+ });
57
79
  const HostMessageComposer = useUiHost().MessageComposer as ComponentType<MessageComposerProps> | undefined;
58
80
  if (HostMessageComposer) {
59
81
  return (
@@ -3,6 +3,7 @@ import { useShortcut } from "../../react/input";
3
3
  import { Box, ScrollBox, Text, useUiHost } from "../../ui";
4
4
  import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
5
5
  import { colors, hoverBg } from "../../theme/colors";
6
+ import { useRemoteUiNode } from "../../remote/semantic-tree";
6
7
 
7
8
  type TabPointerEvent = {
8
9
  button?: number;
@@ -76,6 +77,36 @@ export function Tabs({
76
77
  navigationValueRef.current = value;
77
78
  onSelect(value);
78
79
  }, [onSelect]);
80
+ useRemoteUiNode({
81
+ role: "tabs",
82
+ label: "Tabs",
83
+ actions: {
84
+ select: (input) => {
85
+ const value = resolveTabValue(input, tabs);
86
+ if (!value) return;
87
+ const tab = tabs.find((entry) => entry.value === value);
88
+ if (!tab || tab.disabled) return;
89
+ handleSelect(value);
90
+ },
91
+ add: onAdd ? () => onAdd() : undefined,
92
+ close: (input) => {
93
+ const value = resolveTabValue(input, tabs);
94
+ if (!value) return;
95
+ const tab = tabs.find((entry) => entry.value === value);
96
+ if (!tab || tab.disabled) return;
97
+ tab.onClose?.(value);
98
+ },
99
+ },
100
+ metadata: {
101
+ activeValue,
102
+ tabs: tabs.map((tab) => ({
103
+ label: tab.label,
104
+ value: tab.value,
105
+ disabled: tab.disabled === true,
106
+ closeable: !!tab.onClose,
107
+ })),
108
+ },
109
+ });
79
110
 
80
111
  const selectAdjacentTab = useCallback((direction: -1 | 1) => {
81
112
  const enabledTabs = tabs.filter((tab) => !tab.disabled);
@@ -144,6 +175,20 @@ export function Tabs({
144
175
  );
145
176
  }
146
177
 
178
+ function resolveTabValue(input: unknown, tabs: TabItem[]): string | null {
179
+ if (typeof input === "string") return input;
180
+ if (typeof input === "number") return tabs[input]?.value ?? null;
181
+ if (input && typeof input === "object") {
182
+ const value = input as { value?: unknown; label?: unknown; index?: unknown };
183
+ if (typeof value.value === "string") return value.value;
184
+ if (typeof value.label === "string") {
185
+ return tabs.find((tab) => tab.label === value.label)?.value ?? null;
186
+ }
187
+ if (typeof value.index === "number") return tabs[value.index]?.value ?? null;
188
+ }
189
+ return null;
190
+ }
191
+
147
192
  function tabWidth(
148
193
  tab: TabItem,
149
194
  active: boolean,
@@ -287,7 +332,6 @@ function OpenTuiTabs({
287
332
  flexDirection="row"
288
333
  backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
289
334
  onMouseOver={startHover}
290
- onMouseMove={startHover}
291
335
  onMouseOut={endHover}
292
336
  onMouseDown={selectTab}
293
337
  onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
@@ -319,7 +363,7 @@ function OpenTuiTabs({
319
363
  width={addWidth}
320
364
  height={1}
321
365
  backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
322
- onMouseMove={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
366
+ onMouseOver={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
323
367
  onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
324
368
  onMouseDown={(event: TabPointerEvent) => {
325
369
  event.preventDefault();
@@ -51,7 +51,6 @@ function DataTableActivationHarness() {
51
51
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
52
52
  const scrollRef = useRef<ScrollBoxRenderable>(null);
53
53
  const [selectedId, setSelectedId] = useState<string | null>(null);
54
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
55
54
 
56
55
  return (
57
56
  <DataTable
@@ -64,8 +63,6 @@ function DataTableActivationHarness() {
64
63
  scrollRef={scrollRef}
65
64
  syncHeaderScroll={() => {}}
66
65
  onBodyScrollActivity={() => {}}
67
- hoveredIdx={hoveredIdx}
68
- setHoveredIdx={setHoveredIdx}
69
66
  getItemKey={(row) => row.id}
70
67
  isSelected={(row) => selectedId === row.id}
71
68
  onSelect={(row) => {
@@ -89,7 +86,6 @@ function DataTableSectionHarness() {
89
86
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
90
87
  const scrollRef = useRef<ScrollBoxRenderable>(null);
91
88
  const [selectedId, setSelectedId] = useState<string | null>(null);
92
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
93
89
  const rows: SectionTableRow[] = [
94
90
  { kind: "header", id: "macro", label: "Macro Releases" },
95
91
  { kind: "row", id: "cpi", name: "CPI" },
@@ -106,8 +102,6 @@ function DataTableSectionHarness() {
106
102
  scrollRef={scrollRef}
107
103
  syncHeaderScroll={() => {}}
108
104
  onBodyScrollActivity={() => {}}
109
- hoveredIdx={hoveredIdx}
110
- setHoveredIdx={setHoveredIdx}
111
105
  getItemKey={(row) => row.id}
112
106
  isSelected={(row) => row.kind === "row" && row.id === selectedId}
113
107
  onSelect={(row) => {
@@ -211,7 +205,6 @@ function DataTableColumnGapHarness() {
211
205
  function DataTableVirtualizationHarness() {
212
206
  const headerScrollRef = useRef<ScrollBoxRenderable>(null);
213
207
  const scrollRef = useRef<ScrollBoxRenderable>(null);
214
- const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
215
208
  const rows = Array.from({ length: 100 }, (_, index) => ({
216
209
  id: `row-${index}`,
217
210
  name: `Row ${index}`,
@@ -237,8 +230,6 @@ function DataTableVirtualizationHarness() {
237
230
  scrollRef={scrollRef}
238
231
  syncHeaderScroll={() => {}}
239
232
  onBodyScrollActivity={() => {}}
240
- hoveredIdx={hoveredIdx}
241
- setHoveredIdx={setHoveredIdx}
242
233
  getItemKey={(row) => row.id}
243
234
  isSelected={() => false}
244
235
  onSelect={() => {}}
@@ -64,7 +64,13 @@ export function reduceLayoutAction(state: AppState, action: AppAction): AppState
64
64
  }
65
65
 
66
66
  case "UPDATE_LAYOUT":
67
- return withFocusedPane(state, { ...state.config, layout: action.layout });
67
+ return withFocusedPane(
68
+ state,
69
+ { ...state.config, layout: action.layout },
70
+ Object.prototype.hasOwnProperty.call(action, "focusedPaneId")
71
+ ? { focusedPaneId: action.focusedPaneId ?? null }
72
+ : {},
73
+ );
68
74
 
69
75
  case "SWITCH_LAYOUT": {
70
76
  if (action.index < 0 || action.index >= state.config.layouts.length) return state;