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.
- package/README.md +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +64 -3
- package/src/i18n/ko.ts +64 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +64 -3
- package/src/i18n/zh-tw.ts +64 -3
- package/src/market-data/coordinator/quotes.ts +63 -1
- package/src/market-data/coordinator/subscriptions.test.ts +33 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -124
- package/src/plugins/builtin/options/footer.ts +32 -90
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/view.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +124 -1
- package/src/state/hooks/quote-streaming.ts +90 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- 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({
|
|
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("
|
|
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
|
|
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,
|
|
@@ -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
|
-
|
|
226
|
+
const matchedItems = fuzzyFilter(allItems, rootQuery, (item) => `${item.label} ${item.searchText || ""} ${item.detail} ${item.right || ""}`);
|
|
227
|
+
items.push(...matchedItems);
|
|
209
228
|
}
|
|
210
229
|
|
|
211
|
-
|
|
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
|
}
|