gloomberb 0.8.1 → 0.9.0

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 (196) hide show
  1. package/README.md +106 -54
  2. package/package.json +14 -13
  3. package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
  4. package/src/api-client/chat.ts +8 -0
  5. package/src/api-client/index.test.ts +30 -0
  6. package/src/api-client/index.ts +4 -0
  7. package/src/api-client/normalizers.ts +1 -0
  8. package/src/api-client/types.ts +1 -0
  9. package/src/app/global-shortcuts.test.tsx +54 -4
  10. package/src/app/global-shortcuts.ts +11 -5
  11. package/src/capabilities/factories.ts +4 -0
  12. package/src/components/chart/comparison/data.test.ts +54 -0
  13. package/src/components/chart/comparison/data.ts +47 -24
  14. package/src/components/chart/core/projection.ts +51 -27
  15. package/src/components/command-bar/action-runtime.ts +1 -0
  16. package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
  17. package/src/components/command-bar/layout-items/current-layout.ts +33 -0
  18. package/src/components/command-bar/list/view.tsx +3 -1
  19. package/src/components/command-bar/pane-templates/items.ts +4 -1
  20. package/src/components/command-bar/panel/body.tsx +1 -0
  21. package/src/components/command-bar/routes/root/results.ts +0 -30
  22. package/src/components/command-bar/routes/root/runtime.ts +2 -20
  23. package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
  24. package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
  25. package/src/components/command-bar/routing/launch-request.ts +14 -7
  26. package/src/components/command-bar/routing/list-state.ts +1 -3
  27. package/src/components/command-bar/surface/index.test.tsx +76 -4
  28. package/src/components/command-bar/surface/index.tsx +0 -2
  29. package/src/components/command-bar/surface/pane.test.tsx +1 -0
  30. package/src/components/command-bar/surface/test-harness.tsx +1 -0
  31. package/src/components/command-bar/view-model.ts +3 -3
  32. package/src/components/data-table/stack-view.test.tsx +14 -11
  33. package/src/components/data-table/stack-view.tsx +1 -2
  34. package/src/components/data-table/view.test.tsx +24 -18
  35. package/src/components/data-table/view.tsx +313 -83
  36. package/src/components/feed-data-table/stack-view.tsx +6 -8
  37. package/src/components/layout/pane/footer/index.test.tsx +6 -0
  38. package/src/components/layout/pane/footer/index.tsx +14 -2
  39. package/src/components/layout/pane/title.ts +5 -1
  40. package/src/components/layout/shell/index.test.tsx +120 -2
  41. package/src/components/layout/shell/index.tsx +2 -0
  42. package/src/components/layout/shell/menu.ts +1 -7
  43. package/src/components/layout/shell/pane/actions.ts +8 -0
  44. package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
  45. package/src/components/layout/shell/shortcuts.ts +14 -6
  46. package/src/components/layout/shell/window-mode/index.ts +8 -3
  47. package/src/components/layout/window-edit/presentation.ts +4 -4
  48. package/src/components/onboarding/onboarding-steps.tsx +3 -2
  49. package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
  50. package/src/components/pane-settings-dialog/tui.tsx +1 -1
  51. package/src/components/ticker/list-table-view.tsx +17 -25
  52. package/src/components/ui/choice-dialog.tsx +12 -2
  53. package/src/components/ui/multi-select/dialog.tsx +17 -5
  54. package/src/components/ui/remote-image.tsx +1 -14
  55. package/src/components/ui/shortcut-hint.tsx +14 -2
  56. package/src/components/ui/table-layout.test.ts +2 -2
  57. package/src/components/ui/table-layout.ts +8 -2
  58. package/src/components/ui/tabs.tsx +115 -86
  59. package/src/components/ui/ui.test.tsx +83 -4
  60. package/src/core/state/app/types.ts +12 -4
  61. package/src/market-data/coordinator/auxiliary.ts +29 -1
  62. package/src/market-data/coordinator/index.ts +17 -1
  63. package/src/market-data/hooks.tsx +17 -1
  64. package/src/market-data/selectors.ts +4 -0
  65. package/src/plugins/builtin/account-management/form-components.tsx +0 -4
  66. package/src/plugins/builtin/account-management/model.test.ts +54 -0
  67. package/src/plugins/builtin/account-management/model.ts +13 -3
  68. package/src/plugins/builtin/account-management/pane.tsx +5 -11
  69. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  70. package/src/plugins/builtin/alerts/pane.tsx +5 -3
  71. package/src/plugins/builtin/analytics/index.tsx +0 -3
  72. package/src/plugins/builtin/analytics/view.tsx +6 -5
  73. package/src/plugins/builtin/broker-manager/index.tsx +6 -6
  74. package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
  75. package/src/plugins/builtin/changelog/index.tsx +40 -27
  76. package/src/plugins/builtin/changelog/model.test.ts +77 -0
  77. package/src/plugins/builtin/changelog/model.ts +98 -0
  78. package/src/plugins/builtin/chat/channel-labels.ts +53 -0
  79. package/src/plugins/builtin/chat/channels.test.ts +28 -0
  80. package/src/plugins/builtin/chat/channels.ts +36 -19
  81. package/src/plugins/builtin/chat/chat.test.tsx +264 -5
  82. package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
  83. package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
  84. package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
  85. package/src/plugins/builtin/chat/content/composer.tsx +23 -2
  86. package/src/plugins/builtin/chat/content/index.tsx +66 -4
  87. package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
  88. package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
  89. package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
  90. package/src/plugins/builtin/chat/content/types.ts +1 -0
  91. package/src/plugins/builtin/chat/controller/index.ts +41 -0
  92. package/src/plugins/builtin/chat/controller/messages.ts +18 -0
  93. package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
  94. package/src/plugins/builtin/chat/controller/utils.ts +6 -3
  95. package/src/plugins/builtin/chat/controller/view.ts +1 -1
  96. package/src/plugins/builtin/chat/controller.test.ts +189 -0
  97. package/src/plugins/builtin/chat/edit-window.ts +24 -0
  98. package/src/plugins/builtin/chat/layout.ts +94 -31
  99. package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
  100. package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
  101. package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
  102. package/src/plugins/builtin/chat/message/render-state.ts +5 -1
  103. package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
  104. package/src/plugins/builtin/chat/message/types.ts +2 -0
  105. package/src/plugins/builtin/chat/pane.tsx +35 -7
  106. package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
  107. package/src/plugins/builtin/chat/status-widget.tsx +29 -11
  108. package/src/plugins/builtin/chat/test-harness.tsx +2 -0
  109. package/src/plugins/builtin/cloud/plugin.tsx +3 -0
  110. package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
  111. package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
  112. package/src/plugins/builtin/earnings/index.tsx +8 -7
  113. package/src/plugins/builtin/econ/index.tsx +6 -8
  114. package/src/plugins/builtin/help/components.tsx +1 -1
  115. package/src/plugins/builtin/help/index.test.tsx +105 -30
  116. package/src/plugins/builtin/help/index.tsx +326 -164
  117. package/src/plugins/builtin/help/shortcut-model.ts +5 -6
  118. package/src/plugins/builtin/holders/pane.tsx +7 -6
  119. package/src/plugins/builtin/market-movers/index.tsx +6 -3
  120. package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
  121. package/src/plugins/builtin/options/view.tsx +16 -8
  122. package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
  123. package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
  124. package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
  125. package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
  126. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
  127. package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
  128. package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
  129. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
  130. package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
  131. package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
  132. package/src/plugins/builtin/research/index.test.ts +30 -1
  133. package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
  134. package/src/plugins/builtin/sec/filing-documents.ts +66 -0
  135. package/src/plugins/builtin/sec/index.tsx +132 -20
  136. package/src/plugins/builtin/sectors/index.tsx +13 -23
  137. package/src/plugins/builtin/shared/ticker-request.ts +1 -0
  138. package/src/plugins/builtin/substack/api/auth.ts +185 -0
  139. package/src/plugins/builtin/substack/api/cache.ts +78 -0
  140. package/src/plugins/builtin/substack/api/loaders.ts +146 -0
  141. package/src/plugins/builtin/substack/api/store.ts +186 -0
  142. package/src/plugins/builtin/substack/api/types.ts +40 -0
  143. package/src/plugins/builtin/substack/api.test.ts +298 -0
  144. package/src/plugins/builtin/substack/article-detail.tsx +359 -0
  145. package/src/plugins/builtin/substack/article-stack.tsx +129 -0
  146. package/src/plugins/builtin/substack/content.ts +478 -0
  147. package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
  148. package/src/plugins/builtin/substack/index.tsx +49 -0
  149. package/src/plugins/builtin/substack/login-view.tsx +126 -0
  150. package/src/plugins/builtin/substack/model.test.ts +155 -0
  151. package/src/plugins/builtin/substack/normalize.ts +343 -0
  152. package/src/plugins/builtin/substack/pane-footer.ts +71 -0
  153. package/src/plugins/builtin/substack/pane-state.ts +170 -0
  154. package/src/plugins/builtin/substack/pane.tsx +500 -0
  155. package/src/plugins/builtin/substack/table.ts +111 -0
  156. package/src/plugins/builtin/substack/types.ts +71 -0
  157. package/src/plugins/builtin/substack/utils.ts +131 -0
  158. package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
  161. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
  162. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
  163. package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
  164. package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
  165. package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
  166. package/src/plugins/builtin/world-indices/index.tsx +9 -6
  167. package/src/plugins/catalog-ui.ts +2 -0
  168. package/src/plugins/pane-manager/layout-state.ts +11 -0
  169. package/src/plugins/pane-manager.ts +1 -1
  170. package/src/plugins/prediction-markets/pane.tsx +10 -9
  171. package/src/plugins/registry/index.ts +3 -0
  172. package/src/plugins/runtime/context.tsx +1 -0
  173. package/src/plugins/runtime/index.tsx +4 -0
  174. package/src/react/use-throttled-commit-value.ts +116 -0
  175. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
  176. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
  177. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  178. package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
  179. package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
  180. package/src/renderers/electrobun/view/host/input.tsx +23 -2
  181. package/src/renderers/electrobun/view/http-fetch.ts +10 -3
  182. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  183. package/src/sources/gloomberb-cloud/index.ts +4 -0
  184. package/src/sources/provider-router/cache.ts +1 -0
  185. package/src/sources/provider-router/document-routes.ts +45 -0
  186. package/src/sources/provider-router/index.ts +4 -0
  187. package/src/sources/sec-edgar/content.ts +9 -3
  188. package/src/sources/sec-edgar.test.ts +108 -1
  189. package/src/sources/sec-edgar.ts +105 -1
  190. package/src/sources/yahoo-finance.ts +4 -0
  191. package/src/state/app/context/index.test.tsx +16 -0
  192. package/src/test-support/plugin-runtime.ts +1 -0
  193. package/src/tickers/search/index.ts +1 -0
  194. package/src/types/data-provider.ts +11 -0
  195. package/src/ui/host.tsx +1 -0
  196. package/src/version.ts +1 -1
@@ -24,6 +24,41 @@ import {
24
24
 
25
25
  export type DataTableKeyEvent = TableViewKeyEvent;
26
26
 
27
+ const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
28
+
29
+ export type DataTableSelectionChangeReason =
30
+ | "keyboard"
31
+ | "pointer"
32
+ | "activation";
33
+
34
+ export type DataTableSelection<T> =
35
+ | { kind: "none" }
36
+ | {
37
+ kind: "index";
38
+ selectedIndex: number | null;
39
+ onChange: (
40
+ index: number,
41
+ item: T,
42
+ reason: DataTableSelectionChangeReason,
43
+ ) => void;
44
+ }
45
+ | {
46
+ kind: "id";
47
+ selectedId: string | null;
48
+ getId: (item: T, index: number) => string;
49
+ onChange: (
50
+ id: string,
51
+ item: T,
52
+ index: number,
53
+ reason: DataTableSelectionChangeReason,
54
+ ) => void;
55
+ };
56
+
57
+ interface SelectionCommitTarget {
58
+ index: number;
59
+ id?: string;
60
+ }
61
+
27
62
  export interface DataTableViewProps<
28
63
  T,
29
64
  C extends DataTableColumn = DataTableColumn,
@@ -35,11 +70,18 @@ export interface DataTableViewProps<
35
70
  | "onBodyScrollActivity"
36
71
  | "hoveredIdx"
37
72
  | "setHoveredIdx"
73
+ | "isSelected"
74
+ | "onSelect"
75
+ | "onActivate"
38
76
  > {
39
77
  focused?: boolean;
40
- selectedIndex?: number | null;
41
- onSelectIndex?: (index: number, item: T) => void;
42
- onActivateIndex?: (index: number, item: T) => void;
78
+ selection: DataTableSelection<T>;
79
+ onActivate?: (item: T, index: number) => void;
80
+ onCursorChange?: (
81
+ item: T,
82
+ index: number,
83
+ reason: DataTableSelectionChangeReason,
84
+ ) => void;
43
85
  isNavigable?: (item: T, index: number) => boolean;
44
86
  rootBefore?: ReactNode;
45
87
  rootAfter?: ReactNode;
@@ -62,9 +104,9 @@ export function DataTableView<
62
104
  C extends DataTableColumn = DataTableColumn,
63
105
  >({
64
106
  focused = false,
65
- selectedIndex,
66
- onSelectIndex,
67
- onActivateIndex,
107
+ selection,
108
+ onActivate,
109
+ onCursorChange,
68
110
  isNavigable,
69
111
  rootBefore,
70
112
  rootAfter,
@@ -97,29 +139,32 @@ export function DataTableView<
97
139
  hoveredIdx,
98
140
  setHoveredIdx,
99
141
  });
100
- const [fallbackSelectedIndex, setFallbackSelectedIndex] = useState<
101
- number | null
102
- >(null);
103
- const selectedIndexFromPredicate = useMemo(
104
- () => selectedIndex == null
105
- ? tableProps.items.findIndex(tableProps.isSelected)
106
- : -1,
107
- [selectedIndex, tableProps.items, tableProps.isSelected],
108
- );
109
- const usesFallbackSelection =
110
- selectedIndex == null && selectedIndexFromPredicate < 0;
111
- const effectiveSelectedIndex = selectedIndex
112
- ?? (selectedIndexFromPredicate >= 0
113
- ? selectedIndexFromPredicate
114
- : fallbackSelectedIndex ?? -1);
115
- const effectiveSelectedIndexRef = useRef(effectiveSelectedIndex);
116
- effectiveSelectedIndexRef.current = effectiveSelectedIndex;
117
- const effectiveScrollToIndex = scrollToIndex ?? (
118
- effectiveSelectedIndex >= 0 ? effectiveSelectedIndex : null
142
+ const [cursorIndex, setCursorIndex] = useState<number | null>(null);
143
+ const pendingCommitRef = useRef(false);
144
+ const pendingCommitTargetRef = useRef<SelectionCommitTarget | null>(null);
145
+ const pendingCommitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
146
+ null,
119
147
  );
120
- const [selectionScrollVersion, setSelectionScrollVersion] = useState(0);
121
- const lastSelectionScrollIndexRef = useRef<number | null>(null);
122
-
148
+ const selectionKey = selection.kind === "id"
149
+ ? selection.selectedId
150
+ : selection.kind === "index"
151
+ ? selection.selectedIndex
152
+ : null;
153
+ const selectedIndexFromSelection = useMemo(() => {
154
+ if (selection.kind === "none") return -1;
155
+ if (selection.kind === "index") {
156
+ const index = selection.selectedIndex;
157
+ return typeof index === "number"
158
+ && index >= 0
159
+ && index < tableProps.items.length
160
+ ? index
161
+ : -1;
162
+ }
163
+ if (selection.selectedId == null) return -1;
164
+ return tableProps.items.findIndex(
165
+ (item, index) => selection.getId(item, index) === selection.selectedId,
166
+ );
167
+ }, [selection, selectionKey, tableProps.items]);
123
168
  const navigableIndices = useMemo(() => {
124
169
  if (!isNavigable) return null;
125
170
  return tableProps.items.reduce<number[]>((indices, item, index) => {
@@ -128,39 +173,82 @@ export function DataTableView<
128
173
  }, []);
129
174
  }, [isNavigable, tableProps.items]);
130
175
 
131
- useEffect(() => {
132
- if (!usesFallbackSelection) {
133
- if (fallbackSelectedIndex !== null) setFallbackSelectedIndex(null);
134
- return;
176
+ const isValidCursorIndex = useCallback((index: number | null) => {
177
+ if (index == null || index < 0 || index >= tableProps.items.length) {
178
+ return false;
135
179
  }
136
- if (fallbackSelectedIndex == null) return;
137
- if (!navigableIndices) {
138
- if (
139
- fallbackSelectedIndex >= 0
140
- && fallbackSelectedIndex < tableProps.items.length
141
- ) {
142
- return;
143
- }
144
- setFallbackSelectedIndex(tableProps.items.length > 0
145
- ? Math.min(fallbackSelectedIndex, tableProps.items.length - 1)
146
- : null);
180
+ if (!isNavigable) return true;
181
+ const item = tableProps.items[index];
182
+ return item !== undefined && isNavigable(item, index);
183
+ }, [isNavigable, tableProps.items]);
184
+
185
+ const defaultCursorIndex = selection.kind === "none"
186
+ ? -1
187
+ : isValidCursorIndex(selectedIndexFromSelection)
188
+ ? selectedIndexFromSelection
189
+ : navigableIndices
190
+ ? navigableIndices[0] ?? -1
191
+ : tableProps.items.length > 0
192
+ ? 0
193
+ : -1;
194
+ const effectiveSelectedIndex = selection.kind === "none"
195
+ ? -1
196
+ : isValidCursorIndex(cursorIndex)
197
+ ? cursorIndex!
198
+ : defaultCursorIndex;
199
+ const effectiveSelectedIndexRef = useRef(effectiveSelectedIndex);
200
+ effectiveSelectedIndexRef.current = effectiveSelectedIndex;
201
+ const [selectionScrollVersion, setSelectionScrollVersion] = useState(0);
202
+ const [selectionScrollTarget, setSelectionScrollTarget] = useState<number | null>(null);
203
+ const lastExternalSelectionScrollRef = useRef<{
204
+ kind: DataTableSelection<T>["kind"];
205
+ key: string | number | null;
206
+ resolved: boolean;
207
+ } | null>(null);
208
+ const effectiveScrollToIndex = scrollToIndex !== undefined
209
+ ? scrollToIndex
210
+ : selectionScrollTarget;
211
+
212
+ const requestSelectionScroll = useCallback((index: number) => {
213
+ if (scrollToIndex !== undefined || index < 0) return;
214
+ setSelectionScrollTarget(index);
215
+ setSelectionScrollVersion((current) => current + 1);
216
+ }, [scrollToIndex]);
217
+
218
+ const clearPendingCommit = useCallback(() => {
219
+ if (pendingCommitTimerRef.current) {
220
+ clearTimeout(pendingCommitTimerRef.current);
221
+ pendingCommitTimerRef.current = null;
222
+ }
223
+ pendingCommitRef.current = false;
224
+ pendingCommitTargetRef.current = null;
225
+ }, []);
226
+
227
+ useEffect(() => {
228
+ if (selection.kind === "none") {
229
+ clearPendingCommit();
230
+ setCursorIndex(null);
147
231
  return;
148
232
  }
149
- if (navigableIndices.includes(fallbackSelectedIndex)) return;
150
-
151
- const nextIndex = navigableIndices.find(
152
- (index) => index >= fallbackSelectedIndex,
153
- )
154
- ?? navigableIndices.at(-1)
155
- ?? null;
156
- setFallbackSelectedIndex(nextIndex);
233
+ if (pendingCommitRef.current) return;
234
+ setCursorIndex(defaultCursorIndex >= 0 ? defaultCursorIndex : null);
157
235
  }, [
158
- fallbackSelectedIndex,
236
+ clearPendingCommit,
237
+ defaultCursorIndex,
159
238
  navigableIndices,
239
+ selectedIndexFromSelection,
240
+ selection.kind,
241
+ selectionKey,
160
242
  tableProps.items.length,
161
- usesFallbackSelection,
162
243
  ]);
163
244
 
245
+ useEffect(() => () => {
246
+ if (pendingCommitTimerRef.current) {
247
+ clearTimeout(pendingCommitTimerRef.current);
248
+ pendingCommitTimerRef.current = null;
249
+ }
250
+ }, []);
251
+
164
252
  const handleBodyScrollActivity = useTableBodyScrollActivity({
165
253
  onBodyScrollActivity,
166
254
  syncHeaderScroll: effectiveSyncHeaderScroll,
@@ -173,40 +261,156 @@ export function DataTableView<
173
261
  });
174
262
 
175
263
  useEffect(() => {
176
- if (effectiveScrollToIndex == null) {
177
- lastSelectionScrollIndexRef.current = null;
264
+ const current = {
265
+ kind: selection.kind,
266
+ key: selectionKey,
267
+ resolved: selectedIndexFromSelection >= 0,
268
+ };
269
+ const previous = lastExternalSelectionScrollRef.current;
270
+ lastExternalSelectionScrollRef.current = current;
271
+
272
+ if (selection.kind === "none") {
273
+ if (scrollToIndex === undefined) setSelectionScrollTarget(null);
178
274
  return;
179
275
  }
180
- if (lastSelectionScrollIndexRef.current === effectiveScrollToIndex) return;
181
- lastSelectionScrollIndexRef.current = effectiveScrollToIndex;
182
- setSelectionScrollVersion((current) => current + 1);
183
- }, [effectiveScrollToIndex]);
276
+ if (effectiveSelectedIndex < 0) return;
184
277
 
185
- const selectIndex = useCallback((index: number) => {
278
+ const selectionChanged = !previous
279
+ || previous.kind !== current.kind
280
+ || previous.key !== current.key;
281
+ const selectedRowAppeared = !previous?.resolved && current.resolved;
282
+ if (selectionChanged || selectedRowAppeared) {
283
+ requestSelectionScroll(effectiveSelectedIndex);
284
+ }
285
+ }, [
286
+ effectiveSelectedIndex,
287
+ requestSelectionScroll,
288
+ scrollToIndex,
289
+ selectedIndexFromSelection,
290
+ selection.kind,
291
+ selectionKey,
292
+ ]);
293
+
294
+ const commitIndex = useCallback((
295
+ index: number,
296
+ reason: DataTableSelectionChangeReason,
297
+ ) => {
298
+ if (selection.kind === "none") return;
186
299
  if (index < 0 || index >= tableProps.items.length) return;
187
300
  const item = tableProps.items[index]!;
188
301
  if (isNavigable && !isNavigable(item, index)) return;
189
- effectiveSelectedIndexRef.current = index;
190
- if (usesFallbackSelection) {
191
- setFallbackSelectedIndex(index);
302
+
303
+ if (selection.kind === "index") {
304
+ selection.onChange(index, item, reason);
305
+ return;
192
306
  }
193
- if (onSelectIndex) {
194
- onSelectIndex(index, item);
307
+
308
+ selection.onChange(selection.getId(item, index), item, index, reason);
309
+ }, [isNavigable, selection, tableProps.items]);
310
+ const commitIndexRef = useRef(commitIndex);
311
+
312
+ useEffect(() => {
313
+ commitIndexRef.current = commitIndex;
314
+ }, [commitIndex]);
315
+
316
+ const getCommitTarget = useCallback((index: number): SelectionCommitTarget | null => {
317
+ if (selection.kind === "none") return null;
318
+ if (index < 0 || index >= tableProps.items.length) return null;
319
+ const item = tableProps.items[index]!;
320
+ if (isNavigable && !isNavigable(item, index)) return null;
321
+ return selection.kind === "id"
322
+ ? { index, id: selection.getId(item, index) }
323
+ : { index };
324
+ }, [isNavigable, selection, tableProps.items]);
325
+
326
+ const commitTarget = useCallback((
327
+ target: SelectionCommitTarget | null,
328
+ reason: DataTableSelectionChangeReason,
329
+ ) => {
330
+ if (!target) return;
331
+ if (selection.kind !== "id" || target.id == null) {
332
+ commitIndexRef.current(target.index, reason);
195
333
  return;
196
334
  }
197
- tableProps.onSelect(item, index);
198
- }, [isNavigable, onSelectIndex, tableProps, usesFallbackSelection]);
335
+ const currentIndex = tableProps.items.findIndex(
336
+ (item, index) => selection.getId(item, index) === target.id,
337
+ );
338
+ if (currentIndex < 0) return;
339
+ commitIndexRef.current(currentIndex, reason);
340
+ }, [selection, tableProps.items]);
199
341
 
200
- const activateIndex = useCallback((index: number) => {
342
+ const commitIndexImmediately = useCallback((
343
+ index: number,
344
+ reason: DataTableSelectionChangeReason,
345
+ ) => {
346
+ const target = getCommitTarget(index);
347
+ clearPendingCommit();
348
+ commitTarget(target, reason);
349
+ }, [clearPendingCommit, commitTarget, getCommitTarget]);
350
+
351
+ const scheduleCommitIndex = useCallback((index: number) => {
352
+ if (selection.kind === "none") return;
353
+ const target = getCommitTarget(index);
354
+ if (!target) return;
355
+ if (pendingCommitTimerRef.current) {
356
+ clearTimeout(pendingCommitTimerRef.current);
357
+ }
358
+ pendingCommitRef.current = true;
359
+ pendingCommitTargetRef.current = target;
360
+ pendingCommitTimerRef.current = setTimeout(() => {
361
+ pendingCommitTimerRef.current = null;
362
+ pendingCommitRef.current = false;
363
+ const pendingTarget = pendingCommitTargetRef.current;
364
+ pendingCommitTargetRef.current = null;
365
+ commitTarget(pendingTarget, "keyboard");
366
+ }, DATA_TABLE_SELECTION_COMMIT_DELAY_MS);
367
+ }, [commitTarget, getCommitTarget, selection.kind]);
368
+
369
+ const updateCursorIndex = useCallback((
370
+ index: number,
371
+ options: {
372
+ commit: "deferred" | "immediate" | "none";
373
+ reason?: DataTableSelectionChangeReason;
374
+ },
375
+ ) => {
376
+ if (selection.kind === "none") return;
201
377
  if (index < 0 || index >= tableProps.items.length) return;
202
378
  const item = tableProps.items[index]!;
203
379
  if (isNavigable && !isNavigable(item, index)) return;
204
- if (onActivateIndex) {
205
- onActivateIndex(index, item);
206
- return;
380
+ effectiveSelectedIndexRef.current = index;
381
+ setCursorIndex(index);
382
+ const reason = options.reason ?? (options.commit === "deferred"
383
+ ? "keyboard"
384
+ : options.commit === "immediate"
385
+ ? "pointer"
386
+ : "keyboard");
387
+ onCursorChange?.(item, index, reason);
388
+ if (options.commit === "deferred") {
389
+ requestSelectionScroll(index);
207
390
  }
208
- tableProps.onActivate?.(item, index);
209
- }, [isNavigable, onActivateIndex, tableProps]);
391
+ if (options.commit === "immediate") {
392
+ commitIndexImmediately(index, reason);
393
+ } else if (options.commit === "deferred") {
394
+ scheduleCommitIndex(index);
395
+ }
396
+ }, [
397
+ commitIndexImmediately,
398
+ isNavigable,
399
+ onCursorChange,
400
+ requestSelectionScroll,
401
+ scheduleCommitIndex,
402
+ selection.kind,
403
+ tableProps.items,
404
+ ]);
405
+
406
+ const activateIndex = useCallback((index: number) => {
407
+ if (index < 0 || index >= tableProps.items.length) return;
408
+ const item = tableProps.items[index]!;
409
+ if (isNavigable && !isNavigable(item, index)) return;
410
+ updateCursorIndex(index, { commit: "none", reason: "activation" });
411
+ commitIndexImmediately(index, "activation");
412
+ onActivate?.(item, index);
413
+ }, [commitIndexImmediately, isNavigable, onActivate, tableProps.items, updateCursorIndex]);
210
414
 
211
415
  const selectByOffset = useCallback((offset: -1 | 1) => {
212
416
  if (!navigableIndices) {
@@ -218,7 +422,7 @@ export function DataTableView<
218
422
  Math.min(selectedIndex + offset, tableProps.items.length - 1),
219
423
  )
220
424
  : 0;
221
- selectIndex(nextIndex);
425
+ updateCursorIndex(nextIndex, { commit: "deferred" });
222
426
  return;
223
427
  }
224
428
  if (navigableIndices.length === 0) return;
@@ -230,8 +434,10 @@ export function DataTableView<
230
434
  )
231
435
  : 0;
232
436
  const nextIndex = navigableIndices[nextPosition];
233
- if (nextIndex !== undefined) selectIndex(nextIndex);
234
- }, [navigableIndices, selectIndex, tableProps.items.length]);
437
+ if (nextIndex !== undefined) {
438
+ updateCursorIndex(nextIndex, { commit: "deferred" });
439
+ }
440
+ }, [navigableIndices, tableProps.items.length, updateCursorIndex]);
235
441
 
236
442
  const activateSelection = useCallback(() => {
237
443
  if (!navigableIndices) {
@@ -250,6 +456,32 @@ export function DataTableView<
250
456
  activateIndex(selectedIsNavigable ? selectedIndex : navigableIndices[0]!);
251
457
  }, [activateIndex, navigableIndices, tableProps.items.length]);
252
458
 
459
+ const isItemSelected = useCallback((item: T, index: number) => (
460
+ index === effectiveSelectedIndex
461
+ && (!isNavigable || isNavigable(item, index))
462
+ ), [effectiveSelectedIndex, isNavigable]);
463
+
464
+ const handleTableSelect = useCallback((_item: T, index: number) => {
465
+ updateCursorIndex(index, { commit: "immediate" });
466
+ }, [updateCursorIndex]);
467
+
468
+ const handleTableActivate = useCallback((_item: T, index: number) => {
469
+ activateIndex(index);
470
+ }, [activateIndex]);
471
+
472
+ const handleRowMouseDown = useCallback((item: T, index: number, event: any) => {
473
+ const handled = tableProps.onRowMouseDown?.(item, index, event);
474
+ if (handled === true) {
475
+ updateCursorIndex(index, { commit: "immediate" });
476
+ }
477
+ return handled;
478
+ }, [tableProps.onRowMouseDown, updateCursorIndex]);
479
+
480
+ const handleRowContextMenu = useCallback((item: T, index: number, event: any) => {
481
+ updateCursorIndex(index, { commit: "immediate" });
482
+ tableProps.onRowContextMenu?.(item, index, event);
483
+ }, [tableProps.onRowContextMenu, updateCursorIndex]);
484
+
253
485
  useShortcut((event) => {
254
486
  if (event.defaultPrevented || event.propagationStopped) return;
255
487
  if (!focused || !keyboardNavigation) return;
@@ -293,13 +525,11 @@ export function DataTableView<
293
525
  setHoveredIdx={effectiveSetHoveredIdx}
294
526
  scrollToIndex={effectiveScrollToIndex}
295
527
  scrollToIndexVersion={scrollToIndexVersion + selectionScrollVersion}
296
- isSelected={(item, index) => (
297
- tableProps.isSelected(item, index)
298
- || (
299
- index === effectiveSelectedIndex
300
- && (!isNavigable || isNavigable(item, index))
301
- )
302
- )}
528
+ isSelected={isItemSelected}
529
+ onSelect={handleTableSelect}
530
+ onActivate={handleTableActivate}
531
+ onRowMouseDown={handleRowMouseDown}
532
+ onRowContextMenu={handleRowContextMenu}
303
533
  />
304
534
  </TableViewFrame>
305
535
  );
@@ -327,9 +327,12 @@ export function FeedDataTableStackView({
327
327
  onBack={() => setOpenItemId(null)}
328
328
  detailContent={detailContent}
329
329
  detailTitle={openItem ? openItem.detailTitle ?? openItem.title : undefined}
330
- selectedIndex={activeRowIndex}
331
- onSelectIndex={(_index, row) => onSelect(row.itemIndex)}
332
- onActivateIndex={(_index, row) => openRow(row)}
330
+ selection={{
331
+ kind: "index",
332
+ selectedIndex: activeRowIndex,
333
+ onChange: (_index, row) => onSelect(row.itemIndex),
334
+ }}
335
+ onActivate={(row) => openRow(row)}
333
336
  rootBefore={rootBefore}
334
337
  rootAfter={rootAfter}
335
338
  rootWidth={width}
@@ -345,11 +348,6 @@ export function FeedDataTableStackView({
345
348
  nextSortPreference(current, columnId as DetailColumnId)
346
349
  )}
347
350
  getItemKey={(row) => row.item.id}
348
- isSelected={(row, index) =>
349
- row.itemIndex === selectedIdx || (selectedIdx < 0 && index === 0)
350
- }
351
- onSelect={(row) => onSelect(row.itemIndex)}
352
- onActivate={openRow}
353
351
  renderCell={renderCell}
354
352
  emptyStateTitle={emptyStateTitle}
355
353
  emptyStateHint={emptyStateHint}
@@ -122,6 +122,12 @@ describe("PaneFooterBar", () => {
122
122
  const col = line.indexOf("[r]efresh");
123
123
  expect(col).toBeGreaterThanOrEqual(0);
124
124
 
125
+ await act(async () => {
126
+ await testSetup!.mockMouse.release(col + 1, 0);
127
+ await testSetup!.renderOnce();
128
+ });
129
+ expect(refreshCount).toBe(0);
130
+
125
131
  await act(async () => {
126
132
  await testSetup!.mockMouse.click(col + 1, 0);
127
133
  await testSetup!.renderOnce();
@@ -1,4 +1,5 @@
1
1
  import { Box, Span, Text, TextAttributes, useUiCapabilities } from "../../../../ui";
2
+ import { useRef } from "react";
2
3
  import { colors, blendHex } from "../../../../theme/colors";
3
4
  import { getShortcutHintWidth, ShortcutHint } from "../../../ui/shortcut-hint";
4
5
  import {
@@ -50,13 +51,24 @@ function stopMouseEvent(event?: { stopPropagation?: () => void; preventDefault?:
50
51
  function SegmentView({ segment }: { segment: PaneFooterSegment }) {
51
52
  const interactive = !!segment.onPress && !segment.disabled;
52
53
  const attributes = segment.parts.some((part) => part.bold) || interactive ? TextAttributes.BOLD : 0;
54
+ const triggerMouseDownRef = useRef(false);
55
+ const startSegmentPress = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
56
+ triggerMouseDownRef.current = true;
57
+ stopMouseEvent(event);
58
+ };
59
+ const finishSegmentPress = (event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
60
+ const startedOnTrigger = triggerMouseDownRef.current;
61
+ triggerMouseDownRef.current = false;
62
+ if (startedOnTrigger) segment.onPress?.();
63
+ else stopMouseEvent(event);
64
+ };
53
65
 
54
66
  return (
55
67
  <Text
56
68
  fg={segment.disabled ? colors.textMuted : colors.textDim}
57
69
  attributes={attributes}
58
- onMouseDown={interactive ? stopMouseEvent : undefined}
59
- onMouseUp={interactive ? segment.onPress : undefined}
70
+ onMouseDown={interactive ? startSegmentPress : undefined}
71
+ onMouseUp={interactive ? finishSegmentPress : undefined}
60
72
  {...(interactive ? { "data-gloom-interactive": "true" } : {})}
61
73
  >
62
74
  {segment.parts.map((part, index) => (
@@ -11,7 +11,11 @@ export function getPaneDisplayTitle(
11
11
  const channelId = typeof instance.settings?.channelId === "string" && instance.settings.channelId.trim()
12
12
  ? instance.settings.channelId.trim()
13
13
  : "everyone";
14
- return instance.title ?? `#${channelId}`;
14
+ const title = typeof instance.title === "string" ? instance.title.trim() : "";
15
+ const displayTitle = title && !title.startsWith("dm:") && !title.startsWith("group:") ? title : undefined;
16
+ if (channelId.startsWith("dm:")) return displayTitle ?? "DM";
17
+ if (channelId.startsWith("group:")) return displayTitle ?? "Group";
18
+ return displayTitle ?? `#${channelId}`;
15
19
  }
16
20
 
17
21
  if (instance.paneId === TICKER_RESEARCH_PANE_ID) {