gloomberb 0.10.2 → 0.10.4

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 (164) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +2 -2
  3. package/package.json +2 -1
  4. package/src/api-client/auth.ts +8 -0
  5. package/src/api-client/index.ts +70 -0
  6. package/src/api-client/types.ts +49 -0
  7. package/src/app/runtime/desktop-deeplink.test.ts +8 -0
  8. package/src/app/runtime/desktop-deeplink.ts +14 -1
  9. package/src/components/chart/composite/composite-chart.test.tsx +340 -17
  10. package/src/components/chart/composite/composite-chart.tsx +859 -68
  11. package/src/components/chart/composite/interactions.test.ts +78 -22
  12. package/src/components/chart/composite/interactions.ts +45 -17
  13. package/src/components/chart/composite/rasterizer.ts +8 -2
  14. package/src/components/chart/composite/renderers.test.ts +61 -1
  15. package/src/components/chart/composite/scene.ts +14 -4
  16. package/src/components/chart/composite/tools.test.ts +371 -0
  17. package/src/components/chart/composite/tools.ts +508 -0
  18. package/src/components/chart/composite/types.ts +4 -0
  19. package/src/components/chart/core/pointer.ts +2 -0
  20. package/src/components/chart/native/raster/primitives.ts +18 -0
  21. package/src/components/chart/price-axis-labels.tsx +42 -2
  22. package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
  23. package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
  24. package/src/components/command-bar/action-runtime.ts +3 -0
  25. package/src/components/command-bar/assist/inventory.test.ts +93 -0
  26. package/src/components/command-bar/assist/inventory.ts +92 -0
  27. package/src/components/command-bar/assist/model.test.ts +104 -0
  28. package/src/components/command-bar/assist/model.ts +169 -0
  29. package/src/components/command-bar/assist/runtime.ts +185 -0
  30. package/src/components/command-bar/commands/direct/index.ts +1 -1
  31. package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
  32. package/src/components/command-bar/list/model.ts +15 -2
  33. package/src/components/command-bar/list/navigation.ts +6 -0
  34. package/src/components/command-bar/list/view.tsx +16 -3
  35. package/src/components/command-bar/pane-templates/workflow.ts +1 -0
  36. package/src/components/command-bar/panel/body.tsx +2 -0
  37. package/src/components/command-bar/panel/index.tsx +3 -0
  38. package/src/components/command-bar/panel/runtime.ts +6 -0
  39. package/src/components/command-bar/panel/types.ts +1 -0
  40. package/src/components/command-bar/routes/root/results.ts +28 -2
  41. package/src/components/command-bar/routes/root/runtime.ts +66 -36
  42. package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
  43. package/src/components/command-bar/routing/navigation-state.ts +17 -0
  44. package/src/components/command-bar/selection-runtime.ts +26 -0
  45. package/src/components/command-bar/surface/assist.test.tsx +276 -0
  46. package/src/components/command-bar/surface/index.tsx +93 -2
  47. package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
  48. package/src/components/command-bar/view-model.ts +7 -1
  49. package/src/components/command-bar/workflow/broker.test.ts +22 -0
  50. package/src/components/command-bar/workflow/broker.ts +1 -0
  51. package/src/components/index.ts +1 -1
  52. package/src/components/layout/detached-pane-shell.tsx +40 -2
  53. package/src/components/layout/floating-pane.tsx +4 -1
  54. package/src/components/layout/pane/header.tsx +64 -6
  55. package/src/components/layout/pane/index.tsx +4 -1
  56. package/src/components/layout/shell/index.tsx +23 -0
  57. package/src/components/layout/shell/pane/layers.tsx +5 -0
  58. package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
  59. package/src/components/onboarding/account-step/form-panel.tsx +157 -0
  60. package/src/components/onboarding/account-step/index.tsx +58 -0
  61. package/src/components/onboarding/account-step/model.test.ts +67 -0
  62. package/src/components/onboarding/account-step/model.ts +126 -0
  63. package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
  64. package/src/components/onboarding/onboarding-steps.tsx +91 -55
  65. package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
  66. package/src/components/onboarding/onboarding-wizard.tsx +93 -9
  67. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
  68. package/src/components/onboarding/wizard-account.ts +235 -0
  69. package/src/components/onboarding/wizard-keyboard.ts +67 -1
  70. package/src/components/onboarding/wizard-model.ts +2 -2
  71. package/src/components/ui/data-table/index.tsx +1 -0
  72. package/src/components/ui/data-table/opentui/index.tsx +44 -2
  73. package/src/components/ui/data-table/types.ts +10 -0
  74. package/src/components/ui/data-table/visible-range.test.ts +29 -0
  75. package/src/components/ui/data-table/visible-range.ts +21 -0
  76. package/src/components/ui/ui.test.tsx +23 -1
  77. package/src/i18n/es.ts +1093 -0
  78. package/src/i18n/index.test.ts +5 -2
  79. package/src/i18n/index.ts +3 -0
  80. package/src/i18n/ja.ts +66 -3
  81. package/src/i18n/ko.ts +66 -3
  82. package/src/i18n/languages.test.ts +4 -1
  83. package/src/i18n/languages.ts +12 -1
  84. package/src/i18n/zh-cn.ts +66 -3
  85. package/src/i18n/zh-tw.ts +66 -3
  86. package/src/market-data/coordinator/index.ts +1 -1
  87. package/src/market-data/coordinator/quotes.ts +64 -1
  88. package/src/market-data/coordinator/subscriptions.test.ts +72 -0
  89. package/src/market-data/quotes/cadence.ts +2 -0
  90. package/src/plugins/builtin/account-management/footer.ts +9 -3
  91. package/src/plugins/builtin/account-management/model.ts +53 -1
  92. package/src/plugins/builtin/account-management/pane.tsx +107 -24
  93. package/src/plugins/builtin/ai/index.tsx +7 -2
  94. package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
  95. package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
  96. package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
  97. package/src/plugins/builtin/chart-composer/index.tsx +10 -2
  98. package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
  99. package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
  100. package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
  101. package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
  102. package/src/plugins/builtin/cloud/plugin.tsx +9 -1
  103. package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
  104. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
  105. package/src/plugins/builtin/help/index.tsx +38 -0
  106. package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
  107. package/src/plugins/builtin/options/footer.test.ts +20 -112
  108. package/src/plugins/builtin/options/footer.ts +35 -78
  109. package/src/plugins/builtin/options/index.tsx +6 -0
  110. package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
  111. package/src/plugins/builtin/options/live-quotes.ts +93 -39
  112. package/src/plugins/builtin/options/view.test.tsx +5 -2
  113. package/src/plugins/builtin/options/view.tsx +49 -9
  114. package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
  115. package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
  116. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
  117. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
  118. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
  119. package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
  120. package/src/plugins/builtin/shared/live-streaming.ts +60 -0
  121. package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
  122. package/src/plugins/builtin/shared/plan-access.ts +109 -0
  123. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
  124. package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
  125. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
  126. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
  127. package/src/plugins/registry/index.test.ts +51 -0
  128. package/src/plugins/registry/index.ts +12 -0
  129. package/src/plugins/registry/pane-settings.ts +29 -0
  130. package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
  131. package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
  132. package/src/renderers/electrobun/view/host/box.tsx +3 -0
  133. package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
  134. package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
  135. package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
  136. package/src/renderers/electrobun/view/host/input.tsx +16 -1
  137. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  138. package/src/renderers/electrobun/view/host/style.ts +1 -1
  139. package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
  140. package/src/renderers/electrobun/view/main.tsx +2 -0
  141. package/src/renderers/opentui/chart-surface.tsx +2 -0
  142. package/src/renderers/opentui/host.tsx +3 -0
  143. package/src/sources/gloomberb-cloud/index.test.ts +8 -0
  144. package/src/sources/gloomberb-cloud/index.ts +4 -4
  145. package/src/sources/provider-router/history.test.ts +3 -3
  146. package/src/sources/provider-router/history.ts +15 -4
  147. package/src/sources/yahoo-finance/history.test.ts +47 -0
  148. package/src/sources/yahoo-finance/history.ts +5 -1
  149. package/src/state/hooks/quote-streaming.test.tsx +181 -1
  150. package/src/state/hooks/quote-streaming.ts +93 -7
  151. package/src/theme/colors.ts +20 -1
  152. package/src/theme/themes.test.ts +3 -0
  153. package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
  154. package/src/time-series/resolution.test.ts +51 -0
  155. package/src/time-series/resolution.ts +41 -0
  156. package/src/time-series/resolve.test.ts +207 -1
  157. package/src/time-series/resolve.ts +196 -29
  158. package/src/time-series/spec-studies.test.ts +4 -0
  159. package/src/time-series/spec.ts +15 -1
  160. package/src/time-series/types.ts +7 -0
  161. package/src/time-series/use-chart-resolution.test.tsx +37 -0
  162. package/src/time-series/use-chart-resolution.ts +17 -1
  163. package/src/types/plugin.ts +9 -0
  164. package/src/ui/host.tsx +17 -0
@@ -1,10 +1,15 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
2
  import type { DataProvider } from "../../../types/data-provider";
3
3
  import type { AppTickerRepositoryPort } from "../../../core/app-service-ports";
4
4
  import type { PluginRegistry } from "../../../plugins/registry";
5
5
  import type { LayoutBounds } from "../../../plugins/pane-manager";
6
+ import { usePlanAccess } from "../../../plugins/builtin/shared/plan-access";
7
+ import { buildAssistCommandInventory } from "../assist/inventory";
8
+ import { useCommandBarAssist } from "../assist/runtime";
9
+ import { shouldAutoAskAssist, type AssistRowHandlers } from "../assist/model";
6
10
  import { useRouteListState } from "../routing/list-state";
7
11
  import { useCommandBarRootRuntime } from "../routes/root/runtime";
12
+ import { parseRootShortcutIntent } from "../routes/root/shortcuts";
8
13
  import { useCommandBarThemePreview } from "../theme-preview";
9
14
  import { CommandBarPanel } from "../panel";
10
15
  import { useCommandBarNavigationState } from "../routing/navigation-state";
@@ -71,6 +76,7 @@ export function CommandBar({
71
76
  currentRouteRef,
72
77
  dismissCommandBar,
73
78
  lastMainBrowseRef,
79
+ markRootSelectionNavigated,
74
80
  popRoute,
75
81
  pushRoute,
76
82
  rootHoveredIdx,
@@ -78,6 +84,7 @@ export function CommandBar({
78
84
  rootModeKindRef,
79
85
  rootQuery,
80
86
  rootQueryRef,
87
+ rootSelectionNavigatedRef,
81
88
  rootSelectedIdx,
82
89
  setRootHoveredIdx,
83
90
  setRootQuery,
@@ -104,6 +111,7 @@ export function CommandBar({
104
111
  createPluginCommandItem,
105
112
  executeCollectionCommand,
106
113
  getAvailablePaneShortcutTemplates,
114
+ getAvailablePaneTemplates,
107
115
  getAvailablePluginCommands,
108
116
  ensureRouteFieldFocus,
109
117
  focusWorkflowField,
@@ -115,6 +123,7 @@ export function CommandBar({
115
123
  openInlineConfirm,
116
124
  openModeRoute,
117
125
  openPaneTemplateWorkflow,
126
+ openPluginCommandWorkflow,
118
127
  openWorkflowFieldPicker,
119
128
  paneShortcutItems,
120
129
  persistLayoutChange,
@@ -162,6 +171,82 @@ export function CommandBar({
162
171
  const getTickerSearchTickers = useCallback(() => stateRef.current.tickers, []);
163
172
  const hasPaneSettings = useCallback((paneId: string) => pluginRegistry.hasPaneSettings(paneId), [pluginRegistry]);
164
173
 
174
+ const rootShortcutIntent = useMemo(() => parseRootShortcutIntent({
175
+ query: rootQuery,
176
+ commands: availableCommands,
177
+ pluginCommands: getAvailablePluginCommands(),
178
+ paneTemplates: getAvailablePaneShortcutTemplates(rootQuery),
179
+ activeTicker: activeTickerSymbol,
180
+ }), [activeTickerSymbol, availableCommands, getAvailablePaneShortcutTemplates, getAvailablePluginCommands, rootQuery]);
181
+
182
+ const planAccess = usePlanAccess();
183
+ const buildAssistInventory = useCallback(() => buildAssistCommandInventory({
184
+ commands: availableCommands,
185
+ pluginCommands: getAvailablePluginCommands(),
186
+ paneTemplates: getAvailablePaneTemplates(undefined, { includePromptableTickerTemplates: true }),
187
+ }), [availableCommands, getAvailablePaneTemplates, getAvailablePluginCommands]);
188
+ // Only the root list asks on its own, and only for text the prefix parser
189
+ // could not claim — otherwise the user is mid-command, not mid-question.
190
+ const assistAutoAsk = !currentRoute
191
+ && planAccess.emailVerified
192
+ && shouldAutoAskAssist({ query: rootQuery, hasShortcutIntent: rootShortcutIntent.kind !== "none" });
193
+ const { assistActive, assistState, askAssist, resetAssist } = useCommandBarAssist({
194
+ autoAsk: assistAutoAsk,
195
+ getInventory: buildAssistInventory,
196
+ rootQuery,
197
+ });
198
+ // Filled in below once the selection runtime exists, so an AI candidate runs
199
+ // through the very same submit path as text the user typed.
200
+ const runRootQueryRef = useRef<
201
+ ((query: string, options?: { fallbackPrefix?: string }) => void) | null
202
+ >(null);
203
+ /**
204
+ * Query whose answer the user is already waiting on, set by activating the
205
+ * "Thinking…" row. The row leads the list and holds the default selection, so
206
+ * Enter has to mean something even before the answer is back: it claims the
207
+ * answer, and the best candidate runs the moment it lands.
208
+ */
209
+ const assistPendingRunRef = useRef<string | null>(null);
210
+ const askAssistNow = useCallback(() => {
211
+ assistPendingRunRef.current = rootQueryRef.current.trim();
212
+ askAssist();
213
+ }, [askAssist, rootQueryRef]);
214
+ useEffect(() => {
215
+ const pendingQuery = assistPendingRunRef.current;
216
+ if (!pendingQuery) return;
217
+ // Still the very ask that was claimed; nothing to do until it answers.
218
+ if (assistState.status === "loading" && assistState.query === pendingQuery) return;
219
+ assistPendingRunRef.current = null;
220
+ if (assistState.status !== "answered" || assistState.query !== pendingQuery) return;
221
+ // Typing moved on, so the answer is no longer what the user is looking at.
222
+ if (rootQueryRef.current.trim() !== pendingQuery) return;
223
+ const candidate = assistState.candidates[0];
224
+ if (!candidate) return;
225
+ runRootQueryRef.current?.(
226
+ candidate.input,
227
+ candidate.prefix ? { fallbackPrefix: candidate.prefix } : undefined,
228
+ );
229
+ }, [assistState, rootQueryRef]);
230
+ const startAssistSignUp = useCallback(() => {
231
+ const signUpCommand = getAvailablePluginCommands().find((command) => command.id === "auth-signup");
232
+ if (signUpCommand?.wizard?.length) {
233
+ openPluginCommandWorkflow(signUpCommand);
234
+ return;
235
+ }
236
+ setRootQuery("Sign Up");
237
+ }, [getAvailablePluginCommands, openPluginCommandWorkflow, setRootQuery]);
238
+ const assist = useMemo<AssistRowHandlers>(() => ({
239
+ enabled: planAccess.emailVerified,
240
+ auto: assistAutoAsk && assistActive,
241
+ state: assistState,
242
+ onAsk: askAssistNow,
243
+ onSignUp: startAssistSignUp,
244
+ onRunCandidate: (input: string, prefix?: string) => runRootQueryRef.current?.(
245
+ input,
246
+ prefix ? { fallbackPrefix: prefix } : undefined,
247
+ ),
248
+ }), [askAssistNow, assistActive, assistAutoAsk, assistState, planAccess.emailVerified, startAssistSignUp]);
249
+
165
250
  const {
166
251
  activeMatch,
167
252
  orderedRootResults,
@@ -176,6 +261,7 @@ export function CommandBar({
176
261
  activePortfolio,
177
262
  activeTickerData,
178
263
  activeTickerSymbol,
264
+ assist,
179
265
  availableCommands,
180
266
  buildLayoutItems,
181
267
  buildPaneSettingItems,
@@ -188,7 +274,6 @@ export function CommandBar({
188
274
  dataProvider,
189
275
  executeCollectionCommand,
190
276
  getAvailablePaneShortcutTemplates,
191
- getAvailablePluginCommands,
192
277
  getTickers: getTickerSearchTickers,
193
278
  hasPaneSettings,
194
279
  localTickerSearchResultItems,
@@ -201,6 +286,8 @@ export function CommandBar({
201
286
  readTickerSearchCache,
202
287
  rootModeKind: rootModeInfo.kind,
203
288
  rootQuery,
289
+ rootSelectionNavigatedRef,
290
+ rootShortcutIntent,
204
291
  runDirectCommand,
205
292
  runSecurityDescriptionShortcut,
206
293
  setRootHoveredIdx,
@@ -217,6 +304,7 @@ export function CommandBar({
217
304
  acceptRootShortcutTab,
218
305
  acceptSelectedShortcutTab,
219
306
  activateListSelection,
307
+ runRootQuery,
220
308
  setActiveListQuery,
221
309
  } = useCommandBarSelectionRuntime({
222
310
  activeTickerSymbol,
@@ -251,6 +339,7 @@ export function CommandBar({
251
339
  updateWorkflowValue,
252
340
  visibleListStateRef,
253
341
  });
342
+ runRootQueryRef.current = runRootQuery;
254
343
 
255
344
  const routeListState = useRouteListState({
256
345
  activeMatch,
@@ -301,6 +390,7 @@ export function CommandBar({
301
390
  focusWorkflowField,
302
391
  getWorkflowInputRef,
303
392
  getWorkflowFieldStringValue,
393
+ markRootSelectionNavigated,
304
394
  moveWorkflowFocus,
305
395
  nativeListScrollRef,
306
396
  nativePaneChrome,
@@ -309,6 +399,7 @@ export function CommandBar({
309
399
  persistConfig,
310
400
  pluginRegistry,
311
401
  popRoute,
402
+ resetAssist,
312
403
  rootModeKind: rootModeInfo.kind,
313
404
  rootGhostSuffix,
314
405
  rootQueryLength: rootQuery.length,
@@ -265,7 +265,8 @@ describe("CommandBar portfolio commands", () => {
265
265
 
266
266
  await testSetup.renderOnce();
267
267
  const frame = testSetup.captureCharFrame();
268
- expect(frame).toContain('No matches for "Set Portfolio Position"');
268
+ // Nothing local matches, so the AI fallback row is all that is offered.
269
+ expect(frame).toContain("Ask AI");
269
270
  expect(frame).not.toContain("Create or update a manual position in a portfolio");
270
271
  });
271
272
 
@@ -28,6 +28,8 @@ export interface CommandBarItemView {
28
28
  right?: string;
29
29
  checked?: boolean;
30
30
  current?: boolean;
31
+ accent?: boolean;
32
+ disabled?: boolean;
31
33
  }
32
34
 
33
35
  export interface CommandBarRowPresentation {
@@ -36,6 +38,7 @@ export interface CommandBarRowPresentation {
36
38
  trailing: string;
37
39
  selected: boolean;
38
40
  primaryMuted: boolean;
41
+ trailingAccent: boolean;
39
42
  }
40
43
 
41
44
  export function resolveCommandBarMode(query: string, commandList?: Command[]): CommandBarModeInfo {
@@ -110,7 +113,7 @@ export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?:
110
113
 
111
114
  export function getRowPresentation(item: CommandBarItemView, selected: boolean, showTrailing: boolean): CommandBarRowPresentation {
112
115
  const glyph = selected ? "\u203a" : " ";
113
- const primaryMuted = item.kind === "plugin" && !item.checked;
116
+ const primaryMuted = (item.kind === "plugin" && !item.checked) || item.disabled === true;
114
117
  let trailing = "";
115
118
 
116
119
  if (showTrailing) {
@@ -125,6 +128,7 @@ export function getRowPresentation(item: CommandBarItemView, selected: boolean,
125
128
  trailing: t(trailing),
126
129
  selected,
127
130
  primaryMuted,
131
+ trailingAccent: item.accent === true && trailing.length > 0,
128
132
  };
129
133
  }
130
134
 
@@ -134,6 +138,8 @@ export function truncateText(text: string, width: number): string {
134
138
 
135
139
  function getCategoryPriority(category: string, sectionOrder: CommandBarSectionOrder = "default"): number {
136
140
  const normalized = category.trim().toLowerCase();
141
+ // The AI answers the question the user actually typed, so it leads the list.
142
+ if (normalized === "ask ai") return -100;
137
143
  if (normalized === "exact match") return -50;
138
144
  if (sectionOrder === "app-first") {
139
145
  if (normalized === "saved") return 100;
@@ -0,0 +1,22 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { BrokerAdapter } from "../../../types/broker";
3
+ import { buildBrokerChoices } from "./broker";
4
+
5
+ function createBroker(id: string, name: string): BrokerAdapter {
6
+ return {
7
+ id,
8
+ name,
9
+ configSchema: [{ key: "apiKey", label: "API key", type: "password", required: true }],
10
+ validate: async () => true,
11
+ importPositions: async () => [],
12
+ };
13
+ }
14
+
15
+ test("shows external brokers as sorted top-level choices", () => {
16
+ const choices = buildBrokerChoices(new Map([
17
+ ["ibkr", createBroker("ibkr", "Interactive Brokers")],
18
+ ["newbroker", createBroker("newbroker", "newbroker")],
19
+ ]));
20
+
21
+ expect(choices.map((choice) => choice.id)).toEqual(["ibkr", "newbroker"]);
22
+ });
@@ -23,6 +23,7 @@ export interface CommandBarBrokerChoice {
23
23
  export function buildBrokerChoices(brokers: ReadonlyMap<string, BrokerAdapter>): CommandBarBrokerChoice[] {
24
24
  return [...brokers.values()]
25
25
  .filter((adapter) => adapter.configSchema.length > 0)
26
+ .sort((a, b) => a.name.localeCompare(b.name))
26
27
  .map((adapter) => ({
27
28
  id: adapter.id,
28
29
  label: adapter.name,
@@ -41,7 +41,7 @@ export { Checkbox } from "./ui/checkbox";
41
41
  export { ConfirmDialog } from "./ui/confirm-dialog";
42
42
  export { ChoiceDialog } from "./ui/choice-dialog";
43
43
  export type { ChoiceDialogChoice } from "./ui/choice-dialog";
44
- export type { DataTableCell, DataTableColumn } from "./ui/data-table";
44
+ export type { DataTableCell, DataTableColumn, DataTableVisibleRange } from "./ui/data-table";
45
45
  export { EmptyState } from "./ui/status";
46
46
  export { getMessageComposerBlockHeight, MessageComposer } from "./ui/message-composer";
47
47
  export { NumberField, TextField } from "./ui/fields";
@@ -1,4 +1,4 @@
1
- import { Box, Text, useRendererHost, useUiCapabilities } from "../../ui";
1
+ import { Box, Span, Text, useRendererHost, useUiCapabilities } from "../../ui";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { t } from "../../i18n";
4
4
  import { useShortcut, useViewport } from "../../react/input";
@@ -49,6 +49,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
49
49
  const instance = useAppSelector((state) => findPaneInstance(state.config.layout, desktopWindowBridge.paneId) ?? null);
50
50
  const paneDef = instance ? pluginRegistry.panes.get(instance.paneId) ?? null : null;
51
51
  const hasPaneSettings = !!instance && pluginRegistry.hasPaneSettings(instance.instanceId);
52
+ const quickSettings = instance ? pluginRegistry.resolvePaneQuickSettings(instance.instanceId) : [];
52
53
  const titleState = useMemo(
53
54
  () => ({ config, paneState }) as Parameters<typeof getPaneDisplayTitle>[0],
54
55
  [config, paneState],
@@ -118,6 +119,16 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
118
119
  stopMouse(event);
119
120
  pluginRegistry.openPaneSettingsFn(desktopWindowBridge.paneId);
120
121
  }, [desktopWindowBridge.paneId, pluginRegistry]);
122
+ const toggleQuickSetting = useCallback((key: string, event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
123
+ stopMouse(event);
124
+ focusPane();
125
+ void pluginRegistry.togglePaneQuickSetting(desktopWindowBridge.paneId, key).catch((error) => {
126
+ pluginRegistry.notify({
127
+ body: error instanceof Error ? error.message : "Could not update pane setting.",
128
+ type: "error",
129
+ });
130
+ });
131
+ }, [desktopWindowBridge.paneId, focusPane, pluginRegistry]);
121
132
 
122
133
  if (!instance || !paneDef) {
123
134
  return (
@@ -183,9 +194,36 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
183
194
  paddingRight={showWindowControls ? 0 : 1}
184
195
  style={{ position: "relative" }}
185
196
  >
186
- <Box flexGrow={1} minWidth={0} overflow="hidden">
197
+ <Box minWidth={0} flexShrink={1} overflow="hidden">
187
198
  <Text fg={paneTitleText(focused, true, colors)} selectable={false} data-gloom-role="pane-title">{title}</Text>
188
199
  </Box>
200
+ {quickSettings.map((setting) => (
201
+ <Box
202
+ key={setting.key}
203
+ height={1}
204
+ minWidth={20}
205
+ paddingLeft={1}
206
+ paddingRight={1}
207
+ alignItems="center"
208
+ justifyContent="center"
209
+ className="electrobun-webkit-app-region-no-drag"
210
+ data-gloom-role="pane-quick-setting"
211
+ data-setting-key={setting.key}
212
+ data-gloom-interactive="true"
213
+ aria-label={`${setting.label}: ${setting.value ? "on" : "off"}`}
214
+ aria-pressed={setting.value}
215
+ title={`${setting.label}: ${setting.value ? "on" : "off"}`}
216
+ style={{ cursor: "pointer" }}
217
+ onMouseDown={(event: any) => toggleQuickSetting(setting.key, event)}
218
+ >
219
+ <Span style={{ display: "inline-flex", width: 12, height: 12, color: setting.value ? colors.warning : colors.textDim }}>
220
+ <svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
221
+ <path d="M7.1 1.2 2.7 6.5h3.1l-.7 4.3 4.4-5.5H6.4l.7-4.1Z" fill="currentColor" />
222
+ </svg>
223
+ </Span>
224
+ </Box>
225
+ ))}
226
+ <Box flexGrow={1} minWidth={0} />
189
227
  {hasPaneSettings && (
190
228
  <Text
191
229
  fg={paneTitleText(focused, true, colors)}
@@ -2,7 +2,7 @@ import { Box, Text, useUiCapabilities } from "../../ui";
2
2
  import type { ReactNode } from "react";
3
3
  import { colors, floatingPaneBg } from "../../theme/colors";
4
4
  import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
5
- import { PaneHeader } from "./pane/header";
5
+ import { PaneHeader, type PaneHeaderQuickSetting } from "./pane/header";
6
6
  import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane/footer";
7
7
  import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./pane/sizing";
8
8
 
@@ -17,6 +17,7 @@ interface FloatingPaneWrapperProps {
17
17
  focused: boolean;
18
18
  windowModeSelected?: boolean;
19
19
  showActions?: boolean;
20
+ quickSettings?: PaneHeaderQuickSetting[];
20
21
  onMouseDown?: (event: any) => void;
21
22
  onMouseDownCapture?: (event: any) => void;
22
23
  onHeaderMouseMove?: (event: any) => void;
@@ -63,6 +64,7 @@ export function FloatingPaneWrapper({
63
64
  focused,
64
65
  windowModeSelected = false,
65
66
  showActions = false,
67
+ quickSettings,
66
68
  onMouseDown,
67
69
  onMouseDownCapture,
68
70
  onHeaderMouseMove,
@@ -115,6 +117,7 @@ export function FloatingPaneWrapper({
115
117
  windowModeSelected={windowModeSelected}
116
118
  floating
117
119
  showActions={showActions}
120
+ quickSettings={quickSettings}
118
121
  onHeaderMouseMove={onHeaderMouseMove}
119
122
  onHeaderMouseDown={onHeaderMouseDown}
120
123
  onHeaderMouseDrag={onHeaderMouseDrag}
@@ -15,6 +15,7 @@ interface PaneHeaderProps {
15
15
  windowModeSelected?: boolean;
16
16
  floating?: boolean;
17
17
  showActions?: boolean;
18
+ quickSettings?: PaneHeaderQuickSetting[];
18
19
  onHeaderMouseMove?: (event: any) => void;
19
20
  onHeaderMouseDown?: (event: any) => void;
20
21
  onHeaderMouseDrag?: (event: any) => void;
@@ -24,6 +25,15 @@ interface PaneHeaderProps {
24
25
  onCloseMouseDown?: (event: any) => void;
25
26
  }
26
27
 
28
+ export interface PaneHeaderQuickSetting {
29
+ key: string;
30
+ icon: "zap";
31
+ label: string;
32
+ description?: string;
33
+ active: boolean;
34
+ onMouseDown?: (event: any) => void;
35
+ }
36
+
27
37
  function truncateTitle(title: string, maxWidth: number): string {
28
38
  return truncateToDisplayWidth(title, maxWidth);
29
39
  }
@@ -43,9 +53,15 @@ function captureTerminalPointerDrag(renderer: unknown, renderable: unknown): voi
43
53
  function DesktopPaneButton({
44
54
  icon,
45
55
  onMouseDown,
56
+ color = colors.textDim,
57
+ label,
58
+ pressed,
46
59
  }: {
47
60
  icon: ReactNode;
48
61
  onMouseDown?: (event: any) => void;
62
+ color?: string;
63
+ label?: string;
64
+ pressed?: boolean;
49
65
  }) {
50
66
  return (
51
67
  <Box
@@ -54,6 +70,9 @@ function DesktopPaneButton({
54
70
  justifyContent="center"
55
71
  onMouseDown={onMouseDown}
56
72
  data-gloom-interactive={onMouseDown ? "true" : undefined}
73
+ aria-label={label}
74
+ aria-pressed={pressed}
75
+ title={label}
57
76
  style={{
58
77
  borderRadius: 4,
59
78
  minWidth: 20,
@@ -69,7 +88,7 @@ function DesktopPaneButton({
69
88
  justifyContent: "center",
70
89
  width: 12,
71
90
  height: 12,
72
- color: colors.textDim,
91
+ color,
73
92
  }}
74
93
  >
75
94
  {icon}
@@ -92,7 +111,7 @@ function TerminalPaneButton({
92
111
  return (
93
112
  <Box
94
113
  height={1}
95
- width={text.length}
114
+ width={displayWidth(text)}
96
115
  flexDirection="row"
97
116
  data-gloom-role={role}
98
117
  data-gloom-interactive={onMouseDown ? "true" : undefined}
@@ -110,6 +129,7 @@ export function PaneHeader({
110
129
  windowModeSelected = false,
111
130
  floating = false,
112
131
  showActions = false,
132
+ quickSettings = [],
113
133
  onHeaderMouseMove,
114
134
  onHeaderMouseDown,
115
135
  onHeaderMouseDrag,
@@ -125,6 +145,7 @@ export function PaneHeader({
125
145
  const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
126
146
  const actionText = showActions ? PANE_HEADER_ACTION : " ";
127
147
  const closeText = floating ? PANE_HEADER_CLOSE : "";
148
+ const terminalQuickSettingsWidth = quickSettings.reduce((total) => total + displayWidth(" ⚡ "), 0);
128
149
  const textColor = paneTitleText(visuallyFocused, floating);
129
150
  const handleTerminalHeaderMouseDown = useCallback((event: any) => {
130
151
  captureTerminalPointerDrag(nativeRenderer, terminalHeaderRef.current);
@@ -158,7 +179,7 @@ export function PaneHeader({
158
179
  <Text fg={visuallyFocused ? colors.borderFocused : colors.textMuted} selectable={false} data-gloom-role="pane-grip">
159
180
  {PANE_HEADER_GRIP}
160
181
  </Text>
161
- <Box flexGrow={1} minWidth={0} overflow="hidden">
182
+ <Box minWidth={0} flexShrink={1} overflow="hidden">
162
183
  <Text
163
184
  fg={textColor}
164
185
  selectable={false}
@@ -173,6 +194,25 @@ export function PaneHeader({
173
194
  {title}
174
195
  </Text>
175
196
  </Box>
197
+ {quickSettings.map((setting) => (
198
+ <Box key={setting.key} data-gloom-role="pane-quick-setting" data-setting-key={setting.key}>
199
+ <DesktopPaneButton
200
+ onMouseDown={setting.onMouseDown}
201
+ color={setting.active ? colors.warning : colors.textDim}
202
+ label={`${setting.label}: ${setting.active ? "on" : "off"}`}
203
+ pressed={setting.active}
204
+ icon={(
205
+ <svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
206
+ <path
207
+ d="M7.1 1.2 2.7 6.5h3.1l-.7 4.3 4.4-5.5H6.4l.7-4.1Z"
208
+ fill="currentColor"
209
+ />
210
+ </svg>
211
+ )}
212
+ />
213
+ </Box>
214
+ ))}
215
+ <Box flexGrow={1} minWidth={0} />
176
216
  <Box data-gloom-role="pane-action">
177
217
  {showActions ? (
178
218
  <DesktopPaneButton
@@ -213,10 +253,10 @@ export function PaneHeader({
213
253
  // Reserve 2 for corners, 1 for ─ after ┌, 1 for ─ before ┐
214
254
  const borderColor = visuallyFocused ? colors.borderFocused : colors.border;
215
255
  const innerWidth = Math.max(0, width - 4);
216
- const contentWidth = PANE_HEADER_GRIP.length + closeText.length + actionText.length;
256
+ const contentWidth = PANE_HEADER_GRIP.length + terminalQuickSettingsWidth + closeText.length + actionText.length;
217
257
  const titleWidth = Math.max(0, innerWidth - contentWidth);
218
258
  const clippedTitle = truncateTitle(title, titleWidth);
219
- const fillLen = Math.max(0, innerWidth - PANE_HEADER_GRIP.length - displayWidth(clippedTitle) - actionText.length - closeText.length);
259
+ const fillLen = Math.max(0, innerWidth - PANE_HEADER_GRIP.length - displayWidth(clippedTitle) - terminalQuickSettingsWidth - actionText.length - closeText.length);
220
260
  const fill = "─".repeat(fillLen);
221
261
 
222
262
  return (
@@ -233,6 +273,15 @@ export function PaneHeader({
233
273
  >
234
274
  <Text fg={borderColor} selectable={false}>{"┌─"}</Text>
235
275
  <Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
276
+ {quickSettings.map((setting) => (
277
+ <TerminalPaneButton
278
+ key={setting.key}
279
+ text=" ⚡ "
280
+ fg={setting.active ? colors.warning : colors.textDim}
281
+ role="pane-quick-setting"
282
+ onMouseDown={setting.onMouseDown}
283
+ />
284
+ ))}
236
285
  <Text fg={borderColor} selectable={false}>{fill}</Text>
237
286
  <TerminalPaneButton
238
287
  text={actionText}
@@ -253,7 +302,7 @@ export function PaneHeader({
253
302
  );
254
303
  }
255
304
 
256
- const titleWidth = Math.max(0, width - PANE_HEADER_GRIP.length - actionText.length - closeText.length);
305
+ const titleWidth = Math.max(0, width - PANE_HEADER_GRIP.length - terminalQuickSettingsWidth - actionText.length - closeText.length);
257
306
  const clippedTitle = truncateTitle(title, titleWidth);
258
307
  const padding = " ".repeat(Math.max(0, titleWidth - displayWidth(clippedTitle)));
259
308
 
@@ -272,6 +321,15 @@ export function PaneHeader({
272
321
  <Text fg={textColor} selectable={false}>
273
322
  {`${PANE_HEADER_GRIP}${clippedTitle}${padding}`}
274
323
  </Text>
324
+ {quickSettings.map((setting) => (
325
+ <TerminalPaneButton
326
+ key={setting.key}
327
+ text=" ⚡ "
328
+ fg={setting.active ? colors.warning : colors.textDim}
329
+ role="pane-quick-setting"
330
+ onMouseDown={setting.onMouseDown}
331
+ />
332
+ ))}
275
333
  <TerminalPaneButton
276
334
  text={actionText}
277
335
  fg={textColor}
@@ -2,7 +2,7 @@ import { Box, useUiCapabilities } from "../../../ui";
2
2
  import type { ReactNode } from "react";
3
3
  import { paneBg } from "../../../theme/colors";
4
4
  import { PaneBodyFrame, getPaneWindowAttributes } from "./frame";
5
- import { PaneHeader } from "./header";
5
+ import { PaneHeader, type PaneHeaderQuickSetting } from "./header";
6
6
  import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./footer";
7
7
  import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./sizing";
8
8
 
@@ -15,6 +15,7 @@ interface PaneWrapperProps {
15
15
  height?: number | `${number}%` | "auto";
16
16
  flexGrow?: number;
17
17
  showActions?: boolean;
18
+ quickSettings?: PaneHeaderQuickSetting[];
18
19
  onMouseDown?: (event: any) => void;
19
20
  onMouseDownCapture?: (event: any) => void;
20
21
  onHeaderMouseMove?: (event: any) => void;
@@ -36,6 +37,7 @@ export function PaneWrapper({
36
37
  height,
37
38
  flexGrow,
38
39
  showActions = false,
40
+ quickSettings,
39
41
  onMouseDown,
40
42
  onMouseDownCapture,
41
43
  onHeaderMouseMove,
@@ -87,6 +89,7 @@ export function PaneWrapper({
87
89
  focused={focused}
88
90
  windowModeSelected={windowModeSelected}
89
91
  showActions={showActions}
92
+ quickSettings={quickSettings}
90
93
  onHeaderMouseMove={onHeaderMouseMove}
91
94
  onHeaderMouseDown={onHeaderMouseDown}
92
95
  onHeaderMouseDrag={onHeaderMouseDrag}
@@ -30,6 +30,7 @@ import {
30
30
  } from "../../../state/selectors-ui";
31
31
  import { useThemeColors } from "../../../theme/theme-context";
32
32
  import { getPaneDisplayTitle } from "../pane/title";
33
+ import type { PaneHeaderQuickSetting } from "../pane/header";
33
34
  import { getShortcutDisplayMode } from "../../../utils/shortcut-labels";
34
35
  import {
35
36
  actionMenuWidth,
@@ -423,6 +424,27 @@ export function Shell({
423
424
  (pane: ResolvedPane): string => getPaneDisplayTitle(titleState, pane.instance, pane.def),
424
425
  [titleState],
425
426
  );
427
+ const handlePaneQuickSetting = useCallback((paneId: string, key: string, event: any) => {
428
+ event?.preventDefault?.();
429
+ event?.stopPropagation?.();
430
+ focusPane(paneId);
431
+ void pluginRegistry.togglePaneQuickSetting(paneId, key).catch((error) => {
432
+ pluginRegistry.notify({
433
+ body: error instanceof Error ? error.message : "Could not update pane setting.",
434
+ type: "error",
435
+ });
436
+ });
437
+ }, [focusPane, pluginRegistry]);
438
+ const getPaneQuickSettings = useCallback((paneId: string): PaneHeaderQuickSetting[] => (
439
+ (pluginRegistry.resolvePaneQuickSettings?.(paneId) ?? []).map((setting) => ({
440
+ key: setting.key,
441
+ icon: setting.icon,
442
+ label: setting.label,
443
+ description: setting.description,
444
+ active: setting.value,
445
+ onMouseDown: (event) => handlePaneQuickSetting(paneId, setting.key, event),
446
+ }))
447
+ ), [config, handlePaneQuickSetting, pluginRegistry]);
426
448
 
427
449
  const openPaneMenu = useCallback((paneId: string, rect: LayoutBounds, event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
428
450
  const pane = paneMap.get(paneId);
@@ -561,6 +583,7 @@ export function Shell({
561
583
  dragFloatingRect={dragFloatingRect}
562
584
  focusedPaneId={focusedPaneId}
563
585
  getPaneTitle={getPaneTitle}
586
+ getPaneQuickSettings={getPaneQuickSettings}
564
587
  handleFloatingClose={handleFloatingClose}
565
588
  handleFloatingCloseMouseDown={handleFloatingCloseMouseDown}
566
589
  handleNativeDrag={handleNativeDrag}
@@ -12,6 +12,7 @@ import { pathKey } from "../../window-edit/mode";
12
12
  import { FloatingPaneWrapper } from "../../floating-pane";
13
13
  import { PaneContent } from "../../pane/content";
14
14
  import { PaneWrapper } from "../../pane";
15
+ import type { PaneHeaderQuickSetting } from "../../pane/header";
15
16
  import { hasPaneFooterContent, PaneFooterProvider } from "../../pane/footer";
16
17
  import { resolvePaneBodyFrame, shouldReservePaneFooter } from "../../pane/sizing";
17
18
  import type { DividerPreviewState } from "../native/window-state";
@@ -31,6 +32,7 @@ interface ShellPaneLayersProps {
31
32
  dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
32
33
  focusedPaneId: string | null;
33
34
  getPaneTitle: (pane: ResolvedPane) => string;
35
+ getPaneQuickSettings: (paneId: string) => PaneHeaderQuickSetting[];
34
36
  handleFloatingClose: (paneId: string) => void;
35
37
  handleFloatingCloseMouseDown: (paneId: string, event: any) => void;
36
38
  handleNativeDrag: ShellMouseHandler;
@@ -64,6 +66,7 @@ export function ShellPaneLayers({
64
66
  dragFloatingRect,
65
67
  focusedPaneId,
66
68
  getPaneTitle,
69
+ getPaneQuickSettings,
67
70
  handleFloatingClose,
68
71
  handleFloatingCloseMouseDown,
69
72
  handleNativeDrag,
@@ -129,6 +132,7 @@ export function ShellPaneLayers({
129
132
  width={rect.width}
130
133
  height={rect.height}
131
134
  showActions={showActions}
135
+ quickSettings={getPaneQuickSettings(leaf.instanceId)}
132
136
  windowModeSelected={windowModeSelected}
133
137
  footer={footer}
134
138
  onMouseDownCapture={nativePaneChrome ? (event) => handleNativePaneMouseDown(leaf.instanceId, event) : undefined}
@@ -190,6 +194,7 @@ export function ShellPaneLayers({
190
194
  focused={focused}
191
195
  windowModeSelected={windowModeSelected}
192
196
  showActions={showActions}
197
+ quickSettings={getPaneQuickSettings(pane.instance.instanceId)}
193
198
  footer={footer}
194
199
  onMouseDownCapture={nativePaneChrome ? (event) => handleNativePaneMouseDown(pane.instance.instanceId, event) : undefined}
195
200
  onHeaderMouseMove={() => setHoveredPaneIfChanged(pane.instance.instanceId)}