gloomberb 0.10.3 → 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 (152) 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/layout/detached-pane-shell.tsx +40 -2
  52. package/src/components/layout/floating-pane.tsx +4 -1
  53. package/src/components/layout/pane/header.tsx +64 -6
  54. package/src/components/layout/pane/index.tsx +4 -1
  55. package/src/components/layout/shell/index.tsx +23 -0
  56. package/src/components/layout/shell/pane/layers.tsx +5 -0
  57. package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
  58. package/src/components/onboarding/account-step/form-panel.tsx +157 -0
  59. package/src/components/onboarding/account-step/index.tsx +58 -0
  60. package/src/components/onboarding/account-step/model.test.ts +67 -0
  61. package/src/components/onboarding/account-step/model.ts +126 -0
  62. package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
  63. package/src/components/onboarding/onboarding-steps.tsx +91 -55
  64. package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
  65. package/src/components/onboarding/onboarding-wizard.tsx +93 -9
  66. package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
  67. package/src/components/onboarding/wizard-account.ts +235 -0
  68. package/src/components/onboarding/wizard-keyboard.ts +67 -1
  69. package/src/components/onboarding/wizard-model.ts +2 -2
  70. package/src/i18n/es.ts +1093 -0
  71. package/src/i18n/index.test.ts +5 -2
  72. package/src/i18n/index.ts +3 -0
  73. package/src/i18n/ja.ts +64 -3
  74. package/src/i18n/ko.ts +64 -3
  75. package/src/i18n/languages.test.ts +4 -1
  76. package/src/i18n/languages.ts +12 -1
  77. package/src/i18n/zh-cn.ts +64 -3
  78. package/src/i18n/zh-tw.ts +64 -3
  79. package/src/market-data/coordinator/quotes.ts +63 -1
  80. package/src/market-data/coordinator/subscriptions.test.ts +33 -0
  81. package/src/market-data/quotes/cadence.ts +2 -0
  82. package/src/plugins/builtin/account-management/footer.ts +9 -3
  83. package/src/plugins/builtin/account-management/model.ts +53 -1
  84. package/src/plugins/builtin/account-management/pane.tsx +107 -24
  85. package/src/plugins/builtin/ai/index.tsx +7 -2
  86. package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
  87. package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
  88. package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
  89. package/src/plugins/builtin/chart-composer/index.tsx +10 -2
  90. package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
  91. package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
  92. package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
  93. package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
  94. package/src/plugins/builtin/cloud/plugin.tsx +9 -1
  95. package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
  96. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
  97. package/src/plugins/builtin/help/index.tsx +38 -0
  98. package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
  99. package/src/plugins/builtin/options/footer.test.ts +20 -124
  100. package/src/plugins/builtin/options/footer.ts +32 -90
  101. package/src/plugins/builtin/options/index.tsx +6 -0
  102. package/src/plugins/builtin/options/view.tsx +3 -0
  103. package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
  104. package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
  105. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
  106. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
  107. package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
  108. package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
  109. package/src/plugins/builtin/shared/live-streaming.ts +60 -0
  110. package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
  111. package/src/plugins/builtin/shared/plan-access.ts +109 -0
  112. package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
  113. package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
  114. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
  115. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
  116. package/src/plugins/registry/index.test.ts +51 -0
  117. package/src/plugins/registry/index.ts +12 -0
  118. package/src/plugins/registry/pane-settings.ts +29 -0
  119. package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
  120. package/src/renderers/electrobun/view/host/box.tsx +3 -0
  121. package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
  122. package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
  123. package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
  124. package/src/renderers/electrobun/view/host/input.tsx +16 -1
  125. package/src/renderers/electrobun/view/host/mouse.ts +1 -0
  126. package/src/renderers/electrobun/view/host/style.ts +1 -1
  127. package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
  128. package/src/renderers/electrobun/view/main.tsx +2 -0
  129. package/src/renderers/opentui/chart-surface.tsx +2 -0
  130. package/src/renderers/opentui/host.tsx +3 -0
  131. package/src/sources/gloomberb-cloud/index.test.ts +8 -0
  132. package/src/sources/gloomberb-cloud/index.ts +4 -4
  133. package/src/sources/provider-router/history.test.ts +3 -3
  134. package/src/sources/provider-router/history.ts +15 -4
  135. package/src/sources/yahoo-finance/history.test.ts +47 -0
  136. package/src/sources/yahoo-finance/history.ts +5 -1
  137. package/src/state/hooks/quote-streaming.test.tsx +124 -1
  138. package/src/state/hooks/quote-streaming.ts +90 -7
  139. package/src/theme/colors.ts +20 -1
  140. package/src/theme/themes.test.ts +3 -0
  141. package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
  142. package/src/time-series/resolution.test.ts +51 -0
  143. package/src/time-series/resolution.ts +41 -0
  144. package/src/time-series/resolve.test.ts +207 -1
  145. package/src/time-series/resolve.ts +196 -29
  146. package/src/time-series/spec-studies.test.ts +4 -0
  147. package/src/time-series/spec.ts +15 -1
  148. package/src/time-series/types.ts +7 -0
  149. package/src/time-series/use-chart-resolution.test.tsx +37 -0
  150. package/src/time-series/use-chart-resolution.ts +17 -1
  151. package/src/types/plugin.ts +9 -0
  152. package/src/ui/host.tsx +17 -0
@@ -0,0 +1,185 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ apiClient,
4
+ type AssistCommandCandidate,
5
+ type AssistCommandDescriptor,
6
+ } from "../../../api-client";
7
+ import { ApiRequestError } from "../../../api-client/errors";
8
+ import type { AssistErrorKind, AssistRequestSource, AssistRequestState } from "./model";
9
+
10
+ /** Quiet period after the last keystroke before the query is sent. */
11
+ const ASSIST_DEBOUNCE_MS = 600;
12
+ /** How long background asks stay off after the server rate-limits us. */
13
+ const ASSIST_RATE_LIMIT_BACKOFF_MS = 60_000;
14
+
15
+ /** Maps a failed `/assist/command` call onto the row the user should see. */
16
+ function classifyAssistError(error: unknown): AssistErrorKind {
17
+ const status = error instanceof ApiRequestError ? error.status : undefined;
18
+ if (status === 503) return "unavailable";
19
+ if (status === 429) return "rate-limited";
20
+ return "failed";
21
+ }
22
+
23
+ /**
24
+ * Owns the `/assist/command` request for the root command-bar list. A query the
25
+ * prefix parser cannot claim is sent on its own once typing settles; answers are
26
+ * memoized for the life of the bar so backspacing never re-asks, and a rate
27
+ * limit silently parks background asks for a minute.
28
+ */
29
+ export function useCommandBarAssist({
30
+ autoAsk,
31
+ getInventory,
32
+ rootQuery,
33
+ }: {
34
+ /** Whether this query qualifies for a background ask right now. */
35
+ autoAsk: boolean;
36
+ getInventory: () => AssistCommandDescriptor[];
37
+ rootQuery: string;
38
+ }): {
39
+ /** False once Esc has dismissed the section for the query still in the bar. */
40
+ assistActive: boolean;
41
+ assistState: AssistRequestState;
42
+ askAssist: () => void;
43
+ resetAssist: () => boolean;
44
+ } {
45
+ const [assistState, setAssistState] = useState<AssistRequestState>({ status: "idle" });
46
+ const assistStateRef = useRef(assistState);
47
+ assistStateRef.current = assistState;
48
+ const abortRef = useRef<AbortController | null>(null);
49
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
50
+ const answersRef = useRef(new Map<string, AssistCommandCandidate[]>());
51
+ /**
52
+ * The query the runtime has already acted on, updated the moment a request
53
+ * starts. Rendered state lags a tick behind, and reading it instead would let
54
+ * a re-render slip a second debounce past an ask that is already in flight.
55
+ */
56
+ const handledQueryRef = useRef<string | null>(null);
57
+ /** Query the user dismissed with Esc; the section stays gone until it changes. */
58
+ const dismissedQueryRef = useRef<string | null>(null);
59
+ /** Query the user asked for themselves, so its failures are worth a row. */
60
+ const explicitQueryRef = useRef<string | null>(null);
61
+ const rateLimitedUntilRef = useRef(0);
62
+ const rootQueryRef = useRef(rootQuery);
63
+ rootQueryRef.current = rootQuery;
64
+ const getInventoryRef = useRef(getInventory);
65
+ getInventoryRef.current = getInventory;
66
+
67
+ const cancelPending = useCallback(() => {
68
+ if (debounceRef.current) {
69
+ clearTimeout(debounceRef.current);
70
+ debounceRef.current = null;
71
+ }
72
+ abortRef.current?.abort();
73
+ abortRef.current = null;
74
+ }, []);
75
+
76
+ const resetAssist = useCallback((): boolean => {
77
+ const active = assistStateRef.current;
78
+ // Background rows are ambient: Esc belongs to whoever is listening next.
79
+ if (active.status === "idle" || active.source === "auto") return false;
80
+ cancelPending();
81
+ dismissedQueryRef.current = active.query;
82
+ setAssistState({ status: "idle" });
83
+ return true;
84
+ }, [cancelPending]);
85
+
86
+ const runAssist = useCallback((query: string, source: AssistRequestSource) => {
87
+ const trimmed = query.trim();
88
+ if (!trimmed) return;
89
+ cancelPending();
90
+ handledQueryRef.current = trimmed;
91
+ // A background ask the user has since claimed answers to them, not to the
92
+ // debounce, so its outcome is reported rather than swallowed.
93
+ const resolveSource = (): AssistRequestSource => (
94
+ explicitQueryRef.current === trimmed ? "explicit" : source
95
+ );
96
+
97
+ const cached = answersRef.current.get(trimmed);
98
+ if (cached) {
99
+ setAssistState({ status: "answered", query: trimmed, source: resolveSource(), candidates: cached });
100
+ return;
101
+ }
102
+
103
+ const controller = new AbortController();
104
+ abortRef.current = controller;
105
+ setAssistState({ status: "loading", query: trimmed, source: resolveSource() });
106
+
107
+ void (async () => {
108
+ try {
109
+ const response = await apiClient.assistCommand(trimmed, getInventoryRef.current(), {
110
+ signal: controller.signal,
111
+ });
112
+ if (controller.signal.aborted) return;
113
+ const candidates = response?.candidates ?? [];
114
+ answersRef.current.set(trimmed, candidates);
115
+ setAssistState({ status: "answered", query: trimmed, source: resolveSource(), candidates });
116
+ } catch (error) {
117
+ if (controller.signal.aborted) return;
118
+ const kind = classifyAssistError(error);
119
+ if (kind === "rate-limited") {
120
+ rateLimitedUntilRef.current = Date.now() + ASSIST_RATE_LIMIT_BACKOFF_MS;
121
+ }
122
+ setAssistState({ status: "error", query: trimmed, source: resolveSource(), kind });
123
+ } finally {
124
+ if (abortRef.current === controller) abortRef.current = null;
125
+ }
126
+ })();
127
+ }, [cancelPending]);
128
+
129
+ const askAssist = useCallback(() => {
130
+ const trimmed = rootQueryRef.current.trim();
131
+ if (!trimmed) return;
132
+ explicitQueryRef.current = trimmed;
133
+ const active = assistStateRef.current;
134
+ // The background ask already on the wire asks this exact question; asking
135
+ // again would only abort it and start the wait over.
136
+ if (active.status === "loading" && active.query === trimmed) {
137
+ setAssistState({ ...active, source: "explicit" });
138
+ return;
139
+ }
140
+ runAssist(trimmed, "explicit");
141
+ }, [runAssist]);
142
+
143
+ useEffect(() => {
144
+ const trimmed = rootQuery.trim();
145
+ const active = assistStateRef.current;
146
+ if (active.status !== "idle" && active.query !== trimmed) {
147
+ setAssistState({ status: "idle" });
148
+ }
149
+ if (handledQueryRef.current !== null && handledQueryRef.current !== trimmed) {
150
+ // Whatever is in flight describes text the user has already moved past.
151
+ cancelPending();
152
+ handledQueryRef.current = null;
153
+ }
154
+
155
+ if (!autoAsk || dismissedQueryRef.current === trimmed) return;
156
+ // An answer, a failure, or an in-flight ask for this exact text stands.
157
+ if (handledQueryRef.current === trimmed) return;
158
+ const cached = answersRef.current.get(trimmed);
159
+ if (cached) {
160
+ handledQueryRef.current = trimmed;
161
+ setAssistState({ status: "answered", query: trimmed, source: "auto", candidates: cached });
162
+ return;
163
+ }
164
+ if (Date.now() < rateLimitedUntilRef.current) return;
165
+
166
+ debounceRef.current = setTimeout(() => {
167
+ debounceRef.current = null;
168
+ runAssist(trimmed, "auto");
169
+ }, ASSIST_DEBOUNCE_MS);
170
+ return () => {
171
+ if (!debounceRef.current) return;
172
+ clearTimeout(debounceRef.current);
173
+ debounceRef.current = null;
174
+ };
175
+ }, [autoAsk, cancelPending, rootQuery, runAssist]);
176
+
177
+ useEffect(() => cancelPending, [cancelPending]);
178
+
179
+ return {
180
+ assistActive: dismissedQueryRef.current !== rootQuery.trim(),
181
+ assistState,
182
+ askAssist,
183
+ resetAssist,
184
+ };
185
+ }
@@ -238,7 +238,7 @@ export function runDirectCommandAction(options: {
238
238
  const next = resolveLanguageCommandPreference(current, arg);
239
239
  if (!next) {
240
240
  notify(
241
- `${t("Unsupported language")}: ${arg.trim()}. LANG auto | en | zh-CN | zh-TW | ja | ko`,
241
+ `${t("Unsupported language")}: ${arg.trim()}. LANG auto | en | es | zh-CN | zh-TW | ja | ko`,
242
242
  { type: "error" },
243
243
  );
244
244
  return;
@@ -44,6 +44,8 @@ interface CommandBarKeyboardShortcutArgs {
44
44
  field: CommandBarWorkflowField,
45
45
  ) => void;
46
46
  popRoute: () => void;
47
+ /** Clears an AI assist request; returns true when Esc was spent on it. */
48
+ resetAssist: () => boolean;
47
49
  rootModeKind: string;
48
50
  setActiveListQuery: (query: string) => void;
49
51
  submitWorkflowRoute: (route: CommandBarWorkflowRoute) => void | Promise<void>;
@@ -70,6 +72,7 @@ export function useCommandBarKeyboardShortcuts({
70
72
  nativePaneChrome,
71
73
  openWorkflowFieldPicker,
72
74
  popRoute,
75
+ resetAssist,
73
76
  rootModeKind,
74
77
  setActiveListQuery,
75
78
  submitWorkflowRoute,
@@ -83,6 +86,8 @@ export function useCommandBarKeyboardShortcuts({
83
86
  if (event.name === "escape" || event.name === "`") {
84
87
  event.stopPropagation();
85
88
  event.preventDefault();
89
+ // Esc first backs out of an AI answer, leaving the query and bar intact.
90
+ if (event.name === "escape" && !currentRoute && resetAssist()) return;
86
91
  dismissCommandBar();
87
92
  return;
88
93
  }
@@ -9,6 +9,14 @@ export interface ResultItem {
9
9
  right?: string;
10
10
  shortcutQuery?: string;
11
11
  searchText?: string;
12
+ /** Tints the trailing marker and the section heading with the AI accent. */
13
+ accent?: boolean;
14
+ /**
15
+ * Set false for rows that answer nothing on their own — a placeholder, or an
16
+ * offer the user never asked for. The list skips them when it picks the
17
+ * selection for an untouched query, so plain Enter always runs a real match.
18
+ */
19
+ defaultSelectable?: boolean;
12
20
  pluginToggle?: () => void | Promise<void>;
13
21
  secondaryAction?: () => void | Promise<void>;
14
22
  checked?: boolean;
@@ -37,7 +45,7 @@ export interface ListScreenState {
37
45
 
38
46
  export type CommandBarListRow =
39
47
  | { kind: "spacer"; id: string }
40
- | { kind: "heading"; id: string; label: string }
48
+ | { kind: "heading"; id: string; label: string; accent?: boolean }
41
49
  | { kind: "item"; item: ResultItem; globalIdx: number }
42
50
  | { kind: "message"; id: string; label: string; dim?: boolean }
43
51
  | { kind: "spinner"; id: string; label: string }
@@ -58,7 +66,12 @@ export function buildListRows(listState: ListScreenState): CommandBarListRow[] {
58
66
  if (sectionIndex > 0) {
59
67
  rows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
60
68
  }
61
- rows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
69
+ rows.push({
70
+ kind: "heading",
71
+ id: `heading:${sectionIndex}:${section.category}`,
72
+ label: section.category,
73
+ accent: section.items.some((item) => item.accent),
74
+ });
62
75
  for (const item of section.items) {
63
76
  rows.push({ kind: "item", item, globalIdx });
64
77
  globalIdx += 1;
@@ -6,6 +6,7 @@ import type { CommandBarListScrollEvent } from "./view";
6
6
  interface CommandBarListNavigationOptions {
7
7
  activateListSelectionRef: RefObject<(options?: { secondary?: boolean; item?: ResultItem }) => void>;
8
8
  currentRouteRef: RefObject<CommandBarRoute | null>;
9
+ markRootSelectionNavigated: () => void;
9
10
  setRootHoveredIdx: Dispatch<SetStateAction<number | null>>;
10
11
  setRootSelectedIdx: Dispatch<SetStateAction<number>>;
11
12
  setRouteStack: Dispatch<SetStateAction<CommandBarRoute[]>>;
@@ -19,6 +20,7 @@ function clampListIndex(index: number, length: number): number {
19
20
  export function useCommandBarListNavigation({
20
21
  activateListSelectionRef,
21
22
  currentRouteRef,
23
+ markRootSelectionNavigated,
22
24
  setRootHoveredIdx,
23
25
  setRootSelectedIdx,
24
26
  setRouteStack,
@@ -35,6 +37,7 @@ export function useCommandBarListNavigation({
35
37
  visibleListStateRef.current = nextListState;
36
38
 
37
39
  if (!currentRouteRef.current) {
40
+ markRootSelectionNavigated();
38
41
  setRootSelectedIdx((current) => (current === nextIndex ? current : nextIndex));
39
42
  setRootHoveredIdx((current) => (current === null ? current : null));
40
43
  return;
@@ -52,6 +55,7 @@ export function useCommandBarListNavigation({
52
55
  });
53
56
  }, [
54
57
  currentRouteRef,
58
+ markRootSelectionNavigated,
55
59
  setRootHoveredIdx,
56
60
  setRootSelectedIdx,
57
61
  setRouteStack,
@@ -94,6 +98,7 @@ export function useCommandBarListNavigation({
94
98
  event.stopPropagation?.();
95
99
  event.preventDefault?.();
96
100
  if (!currentRouteRef.current) {
101
+ markRootSelectionNavigated();
97
102
  setRootSelectedIdx((current) => (current === globalIdx ? current : globalIdx));
98
103
  } else {
99
104
  setRouteStack((current) => {
@@ -112,6 +117,7 @@ export function useCommandBarListNavigation({
112
117
  }, [
113
118
  activateListSelectionRef,
114
119
  currentRouteRef,
120
+ markRootSelectionNavigated,
115
121
  setRootSelectedIdx,
116
122
  setRouteStack,
117
123
  ]);
@@ -81,7 +81,7 @@ export const CommandBarListHeader = memo(function CommandBarListHeader({
81
81
  ref={inputRef}
82
82
  value={query}
83
83
  onInput={onQueryChange}
84
- placeholder={kind === "root" ? t("Search commands") : title === "Security Description" ? t("Search tickers") : t("Filter")}
84
+ placeholder={kind === "root" ? t("Command or plain English…") : title === "Security Description" ? t("Search tickers") : t("Filter")}
85
85
  focused
86
86
  data-gloom-remote-scope="command-bar"
87
87
  data-gloom-remote-surface="command-bar"
@@ -136,6 +136,7 @@ interface CommandBarListItemRowProps {
136
136
  labelWidth: number;
137
137
  trailingWidth: number;
138
138
  nativePaneChrome: boolean;
139
+ paletteAccentText: string;
139
140
  paletteBg: string;
140
141
  paletteHoverBg: string;
141
142
  paletteSelectedBg: string;
@@ -160,6 +161,7 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
160
161
  labelWidth,
161
162
  trailingWidth,
162
163
  nativePaneChrome,
164
+ paletteAccentText,
163
165
  paletteBg,
164
166
  paletteHoverBg,
165
167
  paletteSelectedBg,
@@ -234,7 +236,15 @@ const CommandBarListItemRow = memo(function CommandBarListItemRow({
234
236
  </Text>
235
237
  </Box>
236
238
  <Box width={trailingWidth}>
237
- <Text fg={isSelected ? paletteSelectedText : paletteSubtleText}>{trailing}</Text>
239
+ <Text
240
+ fg={isSelected
241
+ ? paletteSelectedText
242
+ : presentation.trailingAccent
243
+ ? paletteAccentText
244
+ : paletteSubtleText}
245
+ >
246
+ {trailing}
247
+ </Text>
238
248
  </Box>
239
249
  </Box>
240
250
  );
@@ -248,6 +258,7 @@ interface CommandBarListBodyProps {
248
258
  labelWidth: number;
249
259
  nativePaneChrome: boolean;
250
260
  nativeListScrollRef: RefObject<ScrollBoxRenderable | null>;
261
+ paletteAccentText: string;
251
262
  paletteBg: string;
252
263
  paletteHeadingText: string;
253
264
  paletteHoverBg: string;
@@ -271,6 +282,7 @@ export const CommandBarListBody = memo(function CommandBarListBody({
271
282
  labelWidth,
272
283
  nativePaneChrome,
273
284
  nativeListScrollRef,
285
+ paletteAccentText,
274
286
  paletteBg,
275
287
  paletteHeadingText,
276
288
  paletteHoverBg,
@@ -322,7 +334,7 @@ export const CommandBarListBody = memo(function CommandBarListBody({
322
334
  if (row.kind === "heading") {
323
335
  return (
324
336
  <Box key={row.id} height={1} paddingX={contentPadding} {...(!nativePaneChrome ? { onMouseScroll: onListScroll } : {})}>
325
- <Text attributes={TextAttributes.BOLD} fg={paletteHeadingText}>
337
+ <Text attributes={TextAttributes.BOLD} fg={row.accent ? paletteAccentText : paletteHeadingText}>
326
338
  {truncateText(t(row.label), queryDisplayWidth)}
327
339
  </Text>
328
340
  </Box>
@@ -346,6 +358,7 @@ export const CommandBarListBody = memo(function CommandBarListBody({
346
358
  labelWidth={labelWidth}
347
359
  trailingWidth={trailingWidth}
348
360
  nativePaneChrome={nativePaneChrome}
361
+ paletteAccentText={paletteAccentText}
349
362
  paletteBg={paletteBg}
350
363
  paletteHoverBg={paletteHoverBg}
351
364
  paletteSelectedBg={paletteSelectedBg}
@@ -275,6 +275,7 @@ export function useCommandBarPaneTemplateActions({
275
275
  adaptTickerSearchRouteResult,
276
276
  createPaneTemplateItem,
277
277
  getAvailablePaneShortcutTemplates,
278
+ getAvailablePaneTemplates,
278
279
  nonShortcutPaneTemplateItems,
279
280
  openPaneTemplateWorkflow,
280
281
  paneShortcutItems,
@@ -77,6 +77,7 @@ export function CommandBarPanelBody({
77
77
  const themeColors = useThemeColors();
78
78
  const {
79
79
  inputBg,
80
+ paletteAccentText,
80
81
  paletteBg,
81
82
  paletteHeadingText,
82
83
  paletteHoverBg,
@@ -179,6 +180,7 @@ export function CommandBarPanelBody({
179
180
  labelWidth={labelWidth}
180
181
  nativePaneChrome={nativePaneChrome}
181
182
  nativeListScrollRef={nativeListScrollRef}
183
+ paletteAccentText={paletteAccentText}
182
184
  paletteBg={paletteBg}
183
185
  paletteHeadingText={paletteHeadingText}
184
186
  paletteHoverBg={paletteHoverBg}
@@ -1,6 +1,7 @@
1
1
  import { useLayoutEffect } from "react";
2
2
  import { Box } from "../../../ui";
3
3
  import {
4
+ commandBarAccentText,
4
5
  commandBarBg,
5
6
  commandBarHeadingText,
6
7
  commandBarHoverBg,
@@ -70,6 +71,7 @@ export function CommandBarPanel({
70
71
  workflowScrollRef,
71
72
  }: CommandBarPanelProps) {
72
73
  const colors = useThemeColors();
74
+ const paletteAccentText = commandBarAccentText(colors);
73
75
  const paletteBg = commandBarBg(colors);
74
76
  const paletteHeadingText = commandBarHeadingText(colors);
75
77
  const paletteHoverBg = commandBarHoverBg(colors);
@@ -172,6 +174,7 @@ export function CommandBarPanel({
172
174
  onWorkflowSubmit={onWorkflowSubmit}
173
175
  palette={{
174
176
  inputBg,
177
+ paletteAccentText,
175
178
  paletteBg,
176
179
  paletteHeadingText,
177
180
  paletteHoverBg,
@@ -50,6 +50,7 @@ interface CommandBarPanelRuntimeOptions {
50
50
  value: CommandBarFieldValue | undefined,
51
51
  ) => string;
52
52
  getWorkflowInputRef: CommandBarPanelProps["getWorkflowInputRef"];
53
+ markRootSelectionNavigated: () => void;
53
54
  moveWorkflowFocus: (delta: number) => void;
54
55
  nativeListScrollRef: RefObject<ScrollBoxRenderable | null>;
55
56
  nativePaneChrome: boolean;
@@ -58,6 +59,7 @@ interface CommandBarPanelRuntimeOptions {
58
59
  persistConfig: (nextConfig: AppState["config"]) => void;
59
60
  pluginRegistry: PluginRegistry;
60
61
  popRoute: () => void;
62
+ resetAssist: () => boolean;
61
63
  rootGhostSuffix: string | null;
62
64
  rootModeKind: string;
63
65
  rootQueryLength: number;
@@ -101,6 +103,7 @@ export function useCommandBarPanelRuntime({
101
103
  focusWorkflowField,
102
104
  getWorkflowFieldStringValue,
103
105
  getWorkflowInputRef,
106
+ markRootSelectionNavigated,
104
107
  moveWorkflowFocus,
105
108
  nativeListScrollRef,
106
109
  nativePaneChrome,
@@ -109,6 +112,7 @@ export function useCommandBarPanelRuntime({
109
112
  persistConfig,
110
113
  pluginRegistry,
111
114
  popRoute,
115
+ resetAssist,
112
116
  rootGhostSuffix,
113
117
  rootModeKind,
114
118
  rootQueryLength,
@@ -145,6 +149,7 @@ export function useCommandBarPanelRuntime({
145
149
  } = useCommandBarListNavigation({
146
150
  activateListSelectionRef,
147
151
  currentRouteRef,
152
+ markRootSelectionNavigated,
148
153
  setRootHoveredIdx,
149
154
  setRootSelectedIdx,
150
155
  setRouteStack,
@@ -185,6 +190,7 @@ export function useCommandBarPanelRuntime({
185
190
  nativePaneChrome,
186
191
  openWorkflowFieldPicker,
187
192
  popRoute,
193
+ resetAssist,
188
194
  rootModeKind,
189
195
  setActiveListQuery,
190
196
  submitWorkflowRoute,
@@ -18,6 +18,7 @@ import type {
18
18
 
19
19
  export interface CommandBarPanelPalette {
20
20
  inputBg: string;
21
+ paletteAccentText: string;
21
22
  paletteBg: string;
22
23
  paletteHeadingText: string;
23
24
  paletteHoverBg: string;
@@ -6,6 +6,11 @@ import type {
6
6
  } from "../../../../types/plugin";
7
7
  import type { TickerRecord } from "../../../../types/ticker";
8
8
  import { fuzzyFilter } from "../../../../utils/fuzzy-search";
9
+ import {
10
+ buildAssistResultItems,
11
+ shouldShowAssistRow,
12
+ type AssistRowHandlers,
13
+ } from "../../assist/model";
9
14
  import { matchPrefix, type Command } from "../../commands/registry";
10
15
  import { isCollectionCommand } from "../../helpers";
11
16
  import type { ResultItem } from "../../list/model";
@@ -38,6 +43,8 @@ export interface RootResultModelOptions {
38
43
  activeCollectionId: string | null;
39
44
  activeTickerData: TickerRecord | null | undefined;
40
45
  activeTickerSymbol: string | null;
46
+ /** Natural-language fallback rows; omit to build the list without an AI section. */
47
+ assist?: AssistRowHandlers | null;
41
48
  availableCommands: Command[];
42
49
  buildLayoutItems: (query: string, options?: { confirmDangerousActions?: boolean }) => ResultItem[];
43
50
  buildPaneSettingItems: (paneId: string | null, query: string) => ResultItem[];
@@ -66,11 +73,22 @@ export interface RootResultModelOptions {
66
73
  tickerActionItems: () => ResultItem[];
67
74
  }
68
75
 
76
+ /** An in-flight or answered request keeps its rows even if the heuristic lapses. */
77
+ function isAssistSectionVisible(assist: AssistRowHandlers, query: string, resultCount: number): boolean {
78
+ if (!query.trim()) return false;
79
+ if (assist.state.status !== "idle" && assist.state.query === query.trim()) return true;
80
+ // Signed out there is nothing to wait for, so the older heuristic still picks
81
+ // the queries worth offering a sign-up row for.
82
+ if (!assist.enabled) return shouldShowAssistRow({ query, resultCount });
83
+ return assist.auto;
84
+ }
85
+
69
86
  export function buildRootResultModel(options: RootResultModelOptions): RootResultModel {
70
87
  const {
71
88
  activeCollectionId,
72
89
  activeTickerData,
73
90
  activeTickerSymbol,
91
+ assist,
74
92
  availableCommands,
75
93
  buildLayoutItems,
76
94
  buildPaneSettingItems,
@@ -205,8 +223,16 @@ export function buildRootResultModel(options: RootResultModelOptions): RootResul
205
223
  ...tickerActionItems(),
206
224
  ...pluginCommandItems(),
207
225
  ];
208
- items.push(...fuzzyFilter(allItems, rootQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`));
226
+ const matchedItems = fuzzyFilter(allItems, rootQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`);
227
+ items.push(...matchedItems);
209
228
  }
210
229
 
211
- return { items, initialIdx };
230
+ // Built from the local matches, then moved above them: the AI answers the
231
+ // question the user typed, so it leads the list. Rows landing here renumber
232
+ // everything below, which the root selection effect absorbs by identity.
233
+ const assistItems = assist && isAssistSectionVisible(assist, rootQuery, items.length)
234
+ ? buildAssistResultItems({ ...assist, query: rootQuery })
235
+ : [];
236
+
237
+ return { items: [...assistItems, ...items], initialIdx };
212
238
  }