@agent-native/toolkit 0.214.0 → 0.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-native.eject.json +5 -0
- package/dist/app/agentkit/react/chat.d.ts.map +1 -1
- package/dist/app/agentkit/react/chat.js +2 -2
- package/dist/app/agentkit/react/chat.js.map +1 -1
- package/dist/app/agentkit/react/components.d.ts +4 -1
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +69 -15
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +7 -1
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +5 -1
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/index.d.ts +1 -0
- package/dist/app/agentkit/react/index.d.ts.map +1 -1
- package/dist/app/agentkit/react/index.js +1 -0
- package/dist/app/agentkit/react/index.js.map +1 -1
- package/dist/app/agentkit/react/root.d.ts +4 -1
- package/dist/app/agentkit/react/root.d.ts.map +1 -1
- package/dist/app/agentkit/react/root.js +2 -2
- package/dist/app/agentkit/react/root.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +43 -6
- package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
- package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
- package/dist/app/agentkit/react/transcript-layout.js +34 -0
- package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
- package/dist/app/auth/AuthPage.d.ts.map +1 -1
- package/dist/app/auth/AuthPage.js +33 -44
- package/dist/app/auth/AuthPage.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +3 -3
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentPanel.d.ts +2 -0
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +6 -2
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/chat/index.d.ts +2 -0
- package/dist/app/chat/chat/index.d.ts.map +1 -1
- package/dist/app/chat/chat/index.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +7 -0
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/chat/chat/surface-types.js.map +1 -1
- package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
- package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-call-display.js +10 -9
- package/dist/app/chat/chat/tool-call-display.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.js +196 -26
- package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.js +5 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +2 -2
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +6 -4
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +1 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +1 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +1 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +1 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +1 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +1 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +1 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +1 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +1 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +1 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +1 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.d.ts +35 -0
- package/dist/app/onboarding/GettingStartedCard.d.ts.map +1 -0
- package/dist/app/onboarding/GettingStartedCard.js +38 -0
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -0
- package/dist/app/onboarding/index.d.ts +1 -0
- package/dist/app/onboarding/index.d.ts.map +1 -1
- package/dist/app/onboarding/index.js +1 -0
- package/dist/app/onboarding/index.js.map +1 -1
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +3 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/settings/AccountSettingsCard.d.ts.map +1 -1
- package/dist/app/settings/AccountSettingsCard.js +2 -1
- package/dist/app/settings/AccountSettingsCard.js.map +1 -1
- package/dist/app/settings/index.d.ts +1 -0
- package/dist/app/settings/index.d.ts.map +1 -1
- package/dist/app/settings/index.js +1 -0
- package/dist/app/settings/index.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts +40 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +87 -20
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/settings/shell/bridge.d.ts +5 -3
- package/dist/app/settings/shell/bridge.d.ts.map +1 -1
- package/dist/app/settings/shell/bridge.js +8 -5
- package/dist/app/settings/shell/bridge.js.map +1 -1
- package/dist/app/settings/shell/index.d.ts +2 -2
- package/dist/app/settings/shell/index.d.ts.map +1 -1
- package/dist/app/settings/shell/index.js.map +1 -1
- package/dist/app/settings/shell/registry.d.ts +9 -1
- package/dist/app/settings/shell/registry.d.ts.map +1 -1
- package/dist/app/settings/shell/registry.js +16 -0
- package/dist/app/settings/shell/registry.js.map +1 -1
- package/dist/app/settings/shell/routing.d.ts +5 -4
- package/dist/app/settings/shell/routing.d.ts.map +1 -1
- package/dist/app/settings/shell/routing.js +38 -10
- package/dist/app/settings/shell/routing.js.map +1 -1
- package/dist/app/shared/CommandMenu.d.ts +86 -6
- package/dist/app/shared/CommandMenu.d.ts.map +1 -1
- package/dist/app/shared/CommandMenu.js +303 -41
- package/dist/app/shared/CommandMenu.js.map +1 -1
- package/dist/app/shared/ConsentBanner.d.ts +9 -0
- package/dist/app/shared/ConsentBanner.d.ts.map +1 -0
- package/dist/app/shared/ConsentBanner.js +35 -0
- package/dist/app/shared/ConsentBanner.js.map +1 -0
- package/dist/composer/ComposerContextBanner.d.ts +3 -1
- package/dist/composer/ComposerContextBanner.d.ts.map +1 -1
- package/dist/composer/ComposerContextBanner.js +2 -2
- package/dist/composer/ComposerContextBanner.js.map +1 -1
- package/dist/ui/sonner.d.ts +2 -2
- package/dist/ui/sonner.d.ts.map +1 -1
- package/dist/ui/sonner.js +2 -2
- package/dist/ui/sonner.js.map +1 -1
- package/package.json +3 -3
- package/src/app/agentkit/react/chat.tsx +2 -0
- package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
- package/src/app/agentkit/react/components.spec.tsx +28 -1
- package/src/app/agentkit/react/components.tsx +153 -17
- package/src/app/agentkit/react/context.tsx +12 -0
- package/src/app/agentkit/react/index.ts +1 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +186 -16
- package/src/app/agentkit/react/root.tsx +7 -0
- package/src/app/agentkit/react/styles.css +43 -6
- package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
- package/src/app/agentkit/react/transcript-layout.ts +97 -0
- package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
- package/src/app/auth/AuthPage.interaction.spec.tsx +74 -0
- package/src/app/auth/AuthPage.tsx +32 -39
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +39 -0
- package/src/app/chat/AgentKitAssistantChat.tsx +5 -0
- package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
- package/src/app/chat/AgentPanel.tsx +14 -2
- package/src/app/chat/chat/index.ts +2 -0
- package/src/app/chat/chat/surface-types.ts +10 -0
- package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
- package/src/app/chat/chat/tool-call-display.tsx +40 -8
- package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +632 -0
- package/src/app/chat/chat/widgets/DataChartRenderer.tsx +308 -51
- package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +126 -23
- package/src/app/chat/chat/widgets/DataChartWidget.tsx +10 -0
- package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +26 -6
- package/src/app/i18n/catalogs/ar-SA.ts +1 -0
- package/src/app/i18n/catalogs/de-DE.ts +2 -0
- package/src/app/i18n/catalogs/en-US.ts +2 -0
- package/src/app/i18n/catalogs/es-ES.ts +2 -0
- package/src/app/i18n/catalogs/fr-FR.ts +2 -0
- package/src/app/i18n/catalogs/hi-IN.ts +1 -0
- package/src/app/i18n/catalogs/ja-JP.ts +2 -0
- package/src/app/i18n/catalogs/ko-KR.ts +1 -0
- package/src/app/i18n/catalogs/pt-BR.ts +2 -0
- package/src/app/i18n/catalogs/zh-CN.ts +1 -0
- package/src/app/i18n/catalogs/zh-TW.ts +1 -0
- package/src/app/onboarding/GettingStartedCard.tsx +203 -0
- package/src/app/onboarding/index.ts +1 -0
- package/src/app/providers/AppProviders.tsx +7 -1
- package/src/app/settings/AccountSettingsCard.spec.tsx +31 -0
- package/src/app/settings/AccountSettingsCard.tsx +8 -0
- package/src/app/settings/index.ts +1 -0
- package/src/app/settings/shell/SettingsShell.spec.tsx +472 -1
- package/src/app/settings/shell/SettingsShell.tsx +245 -31
- package/src/app/settings/shell/bridge.spec.ts +12 -0
- package/src/app/settings/shell/bridge.ts +17 -4
- package/src/app/settings/shell/index.ts +6 -1
- package/src/app/settings/shell/registry.ts +33 -1
- package/src/app/settings/shell/routing.spec.ts +92 -1
- package/src/app/settings/shell/routing.ts +52 -9
- package/src/app/shared/CommandMenu.spec.tsx +1662 -1
- package/src/app/shared/CommandMenu.tsx +763 -164
- package/src/app/shared/ConsentBanner.spec.tsx +141 -0
- package/src/app/shared/ConsentBanner.tsx +141 -0
- package/src/composer/ComposerContextBanner.spec.tsx +31 -5
- package/src/composer/ComposerContextBanner.tsx +7 -2
- package/src/ui/sonner.spec.ts +10 -0
- package/src/ui/sonner.tsx +2 -2
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { cn } from "@agent-native/toolkit/utils";
|
|
36
36
|
import {
|
|
37
37
|
IconBook2,
|
|
38
|
+
IconArrowLeft,
|
|
38
39
|
IconExternalLink,
|
|
39
40
|
IconInfoCircle,
|
|
40
41
|
IconMessage,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
IconLogout,
|
|
43
44
|
IconSettings,
|
|
44
45
|
} from "@tabler/icons-react";
|
|
46
|
+
import { useCommandState } from "cmdk";
|
|
45
47
|
import React, {
|
|
46
48
|
createContext,
|
|
47
49
|
useCallback,
|
|
@@ -53,6 +55,7 @@ import React, {
|
|
|
53
55
|
Suspense,
|
|
54
56
|
type ReactNode,
|
|
55
57
|
} from "react";
|
|
58
|
+
import { flushSync } from "react-dom";
|
|
56
59
|
|
|
57
60
|
import { AboutAgentNativeDialog } from "./AboutAgentNativeDialog.js";
|
|
58
61
|
import { LazyChunkErrorBoundary } from "./LazyChunkErrorBoundary.js";
|
|
@@ -67,6 +70,28 @@ interface CommandMenuContextValue {
|
|
|
67
70
|
search: string;
|
|
68
71
|
onOpenChange: (open: boolean) => void;
|
|
69
72
|
registerNestedDialog: (dismiss: () => void) => () => void;
|
|
73
|
+
selectionEnabled: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function CommandSelectionTracker({
|
|
77
|
+
onChange,
|
|
78
|
+
}: {
|
|
79
|
+
onChange: (value: string) => void;
|
|
80
|
+
}) {
|
|
81
|
+
const value = useCommandState((state) => state.value);
|
|
82
|
+
|
|
83
|
+
useEffect(() => onChange(value), [onChange, value]);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stopCommandNavigation(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
88
|
+
const key = event.key.toLowerCase();
|
|
89
|
+
if (
|
|
90
|
+
["arrowdown", "arrowup", "home", "end", "enter"].includes(key) ||
|
|
91
|
+
(event.ctrlKey && ["n", "j", "p", "k"].includes(key))
|
|
92
|
+
) {
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
}
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
const CommandMenuContext = createContext<CommandMenuContextValue | null>(null);
|
|
@@ -145,10 +170,11 @@ function CommandGroup({ heading, children }: CommandGroupProps) {
|
|
|
145
170
|
);
|
|
146
171
|
}
|
|
147
172
|
|
|
148
|
-
|
|
173
|
+
type CommandItemProps = {
|
|
149
174
|
onSelect: () => void;
|
|
150
175
|
children: ReactNode;
|
|
151
176
|
keywords?: string[];
|
|
177
|
+
disabled?: boolean;
|
|
152
178
|
className?: string;
|
|
153
179
|
deferSelect?: boolean;
|
|
154
180
|
/**
|
|
@@ -157,7 +183,14 @@ interface CommandItemProps {
|
|
|
157
183
|
* example a search result gaining a snippet) loses the highlight.
|
|
158
184
|
*/
|
|
159
185
|
value?: string;
|
|
160
|
-
}
|
|
186
|
+
} & (
|
|
187
|
+
| { preview?: never }
|
|
188
|
+
| {
|
|
189
|
+
/** Content shown in the selected row; provide a stable value to keep it out of search text. */
|
|
190
|
+
preview: ReactNode;
|
|
191
|
+
value: string;
|
|
192
|
+
}
|
|
193
|
+
);
|
|
161
194
|
|
|
162
195
|
function CommandItem({
|
|
163
196
|
onSelect,
|
|
@@ -166,8 +199,11 @@ function CommandItem({
|
|
|
166
199
|
className,
|
|
167
200
|
deferSelect = true,
|
|
168
201
|
value,
|
|
202
|
+
disabled = false,
|
|
203
|
+
preview,
|
|
169
204
|
}: CommandItemProps) {
|
|
170
|
-
const { onOpenChange } = useCommandMenuContext();
|
|
205
|
+
const { onOpenChange, selectionEnabled } = useCommandMenuContext();
|
|
206
|
+
const selectedValue = useCommandState((state) => state.value);
|
|
171
207
|
|
|
172
208
|
const handleSelect = () => {
|
|
173
209
|
if (!deferSelect) {
|
|
@@ -185,8 +221,17 @@ function CommandItem({
|
|
|
185
221
|
className={cn("cursor-pointer gap-2", className)}
|
|
186
222
|
onSelect={handleSelect}
|
|
187
223
|
value={value}
|
|
224
|
+
disabled={disabled || !selectionEnabled}
|
|
188
225
|
>
|
|
189
226
|
{children}
|
|
227
|
+
{preview !== undefined && selectedValue === value ? (
|
|
228
|
+
<div
|
|
229
|
+
data-testid="command-menu-item-preview"
|
|
230
|
+
className="ms-auto min-w-0"
|
|
231
|
+
>
|
|
232
|
+
{preview}
|
|
233
|
+
</div>
|
|
234
|
+
) : null}
|
|
190
235
|
</CommandItemPrimitive>
|
|
191
236
|
);
|
|
192
237
|
}
|
|
@@ -215,6 +260,145 @@ export interface CommandMenuDoc {
|
|
|
215
260
|
keywords?: string[];
|
|
216
261
|
}
|
|
217
262
|
|
|
263
|
+
export interface CommandMenuItemMatchContext {
|
|
264
|
+
query: string;
|
|
265
|
+
label: string;
|
|
266
|
+
value?: string;
|
|
267
|
+
keywords: readonly string[];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export type CommandMenuItemMatcher = (
|
|
271
|
+
item: CommandMenuItemMatchContext,
|
|
272
|
+
) => boolean;
|
|
273
|
+
|
|
274
|
+
export interface AppCommandContext<TAppContext = unknown> {
|
|
275
|
+
search: string;
|
|
276
|
+
appContext: TAppContext | undefined;
|
|
277
|
+
close: () => void;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface AppCommandPage<TAppContext = unknown> {
|
|
281
|
+
id: string;
|
|
282
|
+
title: string;
|
|
283
|
+
groups: AppCommandGroup<TAppContext>[];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface AppCommandGroup<TAppContext = unknown> {
|
|
287
|
+
id: string;
|
|
288
|
+
order: number;
|
|
289
|
+
heading?: string;
|
|
290
|
+
keywords?: string[];
|
|
291
|
+
visibleWhen?: (context: AppCommandContext<TAppContext>) => boolean;
|
|
292
|
+
commands: AppCommand<TAppContext>[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface AppCommandBase<TAppContext> {
|
|
296
|
+
id: string;
|
|
297
|
+
order: number;
|
|
298
|
+
label: string;
|
|
299
|
+
keywords?: string[];
|
|
300
|
+
shortcutHint?: string;
|
|
301
|
+
description?: string;
|
|
302
|
+
metadata?: ReactNode;
|
|
303
|
+
icon?: ReactNode;
|
|
304
|
+
preview?:
|
|
305
|
+
| ReactNode
|
|
306
|
+
| ((context: AppCommandContext<TAppContext>) => ReactNode);
|
|
307
|
+
visibleWhen?: (context: AppCommandContext<TAppContext>) => boolean;
|
|
308
|
+
enabledWhen?: (context: AppCommandContext<TAppContext>) => boolean;
|
|
309
|
+
deferSelect?: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export type AppCommand<TAppContext = unknown> =
|
|
313
|
+
| (AppCommandBase<TAppContext> & {
|
|
314
|
+
onSelect: (context: AppCommandContext<TAppContext>) => void;
|
|
315
|
+
page?: never;
|
|
316
|
+
})
|
|
317
|
+
| (AppCommandBase<TAppContext> & {
|
|
318
|
+
page: AppCommandPage<TAppContext>;
|
|
319
|
+
onSelect?: never;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
interface OrderedAppCommandGroup<TAppContext> {
|
|
323
|
+
group: AppCommandGroup<TAppContext>;
|
|
324
|
+
commands: AppCommand<TAppContext>[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface AppCommandEntry<TAppContext> {
|
|
328
|
+
group: AppCommandGroup<TAppContext>;
|
|
329
|
+
command: AppCommand<TAppContext>;
|
|
330
|
+
value: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ordered<T extends { order: number }>(items: T[]): T[] {
|
|
334
|
+
return items
|
|
335
|
+
.map((item, index) => ({ item, index }))
|
|
336
|
+
.sort((a, b) => a.item.order - b.item.order || a.index - b.index)
|
|
337
|
+
.map(({ item }) => item);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function appCommandSearchText<TAppContext>(
|
|
341
|
+
command: AppCommand<TAppContext>,
|
|
342
|
+
): string {
|
|
343
|
+
return [command.label, command.description]
|
|
344
|
+
.concat(command.keywords ?? [])
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join(" ")
|
|
347
|
+
.toLowerCase();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function findAppCommandPage<TAppContext>(
|
|
351
|
+
groups: AppCommandGroup<TAppContext>[],
|
|
352
|
+
pageId: string,
|
|
353
|
+
): AppCommandPage<TAppContext> | undefined {
|
|
354
|
+
for (const group of groups) {
|
|
355
|
+
for (const command of group.commands) {
|
|
356
|
+
if (!("page" in command) || !command.page) continue;
|
|
357
|
+
if (command.page.id === pageId) return command.page;
|
|
358
|
+
const nestedPage = findAppCommandPage(command.page.groups, pageId);
|
|
359
|
+
if (nestedPage) return nestedPage;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function filterAppCommandGroups<TAppContext>(
|
|
366
|
+
groups: AppCommandGroup<TAppContext>[],
|
|
367
|
+
search: string,
|
|
368
|
+
context: AppCommandContext<TAppContext>,
|
|
369
|
+
itemMatcher?: CommandMenuItemMatcher,
|
|
370
|
+
): OrderedAppCommandGroup<TAppContext>[] {
|
|
371
|
+
const searchLower = search.trim().toLowerCase();
|
|
372
|
+
return ordered(groups)
|
|
373
|
+
.filter((group) => group.visibleWhen?.(context) ?? true)
|
|
374
|
+
.map((group) => {
|
|
375
|
+
const groupMatches =
|
|
376
|
+
searchLower.length > 0 &&
|
|
377
|
+
[group.heading, ...(group.keywords ?? [])]
|
|
378
|
+
.filter(Boolean)
|
|
379
|
+
.join(" ")
|
|
380
|
+
.toLowerCase()
|
|
381
|
+
.includes(searchLower);
|
|
382
|
+
const commands = ordered(group.commands).filter((command) => {
|
|
383
|
+
if (!(command.visibleWhen?.(context) ?? true)) return false;
|
|
384
|
+
if (!searchLower) return true;
|
|
385
|
+
if (itemMatcher) {
|
|
386
|
+
return itemMatcher({
|
|
387
|
+
query: search,
|
|
388
|
+
label: command.label,
|
|
389
|
+
value: command.id,
|
|
390
|
+
keywords: command.keywords ?? [],
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return (
|
|
394
|
+
groupMatches || appCommandSearchText(command).includes(searchLower)
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
return { group, commands };
|
|
398
|
+
})
|
|
399
|
+
.filter(({ commands }) => commands.length > 0);
|
|
400
|
+
}
|
|
401
|
+
|
|
218
402
|
interface CommandDocsGroupProps {
|
|
219
403
|
docs: CommandMenuDoc[];
|
|
220
404
|
heading?: string;
|
|
@@ -270,6 +454,26 @@ function CommandDocsGroup({ docs, heading = "Docs" }: CommandDocsGroupProps) {
|
|
|
270
454
|
);
|
|
271
455
|
}
|
|
272
456
|
|
|
457
|
+
function hasCommandItems(children: ReactNode): boolean {
|
|
458
|
+
return React.Children.toArray(children).some((child) => {
|
|
459
|
+
if (!React.isValidElement(child)) return false;
|
|
460
|
+
if (child.type === CommandItem || child.type === CommandItemPrimitive) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (child.type === React.Fragment || child.type === CommandGroup) {
|
|
464
|
+
return hasCommandItems(
|
|
465
|
+
(child.props as { children?: ReactNode }).children,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (child.type === CommandDocsGroup) {
|
|
469
|
+
const docs = (child.props as CommandDocsGroupProps).docs;
|
|
470
|
+
return docs.length > 0;
|
|
471
|
+
}
|
|
472
|
+
if (typeof child.type !== "string") return true;
|
|
473
|
+
return hasCommandItems((child.props as { children?: ReactNode }).children);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
273
477
|
const SETTINGS_SEARCH_TERMS = [
|
|
274
478
|
"settings",
|
|
275
479
|
"preferences",
|
|
@@ -277,6 +481,7 @@ const SETTINGS_SEARCH_TERMS = [
|
|
|
277
481
|
"profile",
|
|
278
482
|
"integrations",
|
|
279
483
|
];
|
|
484
|
+
const NO_COMMAND_SELECTION = "\u0000";
|
|
280
485
|
|
|
281
486
|
function isApplePlatform(): boolean {
|
|
282
487
|
return (
|
|
@@ -285,49 +490,99 @@ function isApplePlatform(): boolean {
|
|
|
285
490
|
);
|
|
286
491
|
}
|
|
287
492
|
|
|
288
|
-
export interface CommandMenuProps {
|
|
493
|
+
export interface CommandMenuProps<TAppContext = unknown> {
|
|
289
494
|
open: boolean;
|
|
290
495
|
onOpenChange: (open: boolean) => void;
|
|
291
496
|
children: ReactNode;
|
|
292
497
|
renderResults?: (search: string) => ReactNode;
|
|
498
|
+
/** Reports whether default `renderResults` output has selectable rows; required with `emptyState` when results use opaque components. */
|
|
499
|
+
renderResultsHasItems?: (search: string) => boolean;
|
|
500
|
+
renderResultsPosition?: "before" | "after";
|
|
293
501
|
renderContent?: (options: {
|
|
294
502
|
search: string;
|
|
295
|
-
renderList: (
|
|
503
|
+
renderList: (
|
|
504
|
+
results?: ReactNode,
|
|
505
|
+
options?: {
|
|
506
|
+
/** Reports rows inside an opaque custom results component. */
|
|
507
|
+
hasSelectableItems?: boolean;
|
|
508
|
+
},
|
|
509
|
+
) => ReactNode;
|
|
296
510
|
}) => ReactNode;
|
|
297
511
|
placeholder?: string;
|
|
298
512
|
inputLabel?: string;
|
|
299
513
|
emptyText?: string;
|
|
514
|
+
emptyState?: ReactNode;
|
|
515
|
+
/** App suggestions shown only when a non-empty query has no selectable results. */
|
|
516
|
+
noMatchSuggestionsSlot?: (search: string) => ReactNode;
|
|
517
|
+
initialSelection?: "first" | "none";
|
|
518
|
+
container?: HTMLElement;
|
|
519
|
+
onSearchChange?: (search: string) => void;
|
|
520
|
+
itemMatcher?: CommandMenuItemMatcher;
|
|
300
521
|
showAgentFallback?: boolean;
|
|
301
522
|
chatStorageKey?: string;
|
|
302
523
|
clearSearchOnEscape?: boolean;
|
|
303
524
|
onCloseAutoFocus?: (event: Event) => void;
|
|
525
|
+
/** Shared dialog width; `wide` is opt-in for palettes with denser app results. */
|
|
526
|
+
size?: "default" | "wide";
|
|
304
527
|
className?: string;
|
|
305
528
|
changelog?: string;
|
|
306
529
|
changelogLabel?: string;
|
|
307
530
|
changelogKey?: string;
|
|
308
531
|
showAbout?: boolean;
|
|
532
|
+
showSettingsCommand?: boolean;
|
|
533
|
+
showSignOutCommand?: boolean;
|
|
534
|
+
/** App-owned entries are additive; shortcut hints are display-only. */
|
|
535
|
+
appCommandGroups?: AppCommandGroup<TAppContext>[];
|
|
536
|
+
/** State read by visibility and enabled predicates for app commands. */
|
|
537
|
+
commandContext?: TAppContext;
|
|
538
|
+
/** Optional app-owned filter controls shown between the search field and results. */
|
|
539
|
+
filterLauncherSlot?: (context: AppCommandContext<TAppContext>) => ReactNode;
|
|
540
|
+
/** Optional app chat surface rendered beneath the command results. */
|
|
541
|
+
embeddedChatSlot?: (context: AppCommandContext<TAppContext>) => ReactNode;
|
|
542
|
+
/** Content shown below the results with shared spacing and note semantics. */
|
|
543
|
+
footerSlot?: ReactNode;
|
|
309
544
|
}
|
|
310
545
|
|
|
311
|
-
export function CommandMenu({
|
|
546
|
+
export function CommandMenu<TAppContext = unknown>({
|
|
312
547
|
open,
|
|
313
548
|
onOpenChange,
|
|
314
549
|
children,
|
|
315
550
|
renderResults,
|
|
551
|
+
renderResultsHasItems,
|
|
552
|
+
renderResultsPosition = "before",
|
|
316
553
|
renderContent,
|
|
317
554
|
placeholder = "Type a command or ask AI...",
|
|
318
555
|
inputLabel = placeholder,
|
|
319
556
|
emptyText: _emptyText = "No commands found.",
|
|
557
|
+
emptyState,
|
|
558
|
+
noMatchSuggestionsSlot,
|
|
559
|
+
initialSelection,
|
|
560
|
+
container,
|
|
561
|
+
onSearchChange,
|
|
562
|
+
itemMatcher,
|
|
320
563
|
showAgentFallback = true,
|
|
321
564
|
chatStorageKey,
|
|
322
565
|
clearSearchOnEscape = false,
|
|
323
566
|
onCloseAutoFocus,
|
|
567
|
+
size = "default",
|
|
324
568
|
className,
|
|
325
569
|
changelog,
|
|
326
570
|
changelogLabel = "What's new",
|
|
327
571
|
changelogKey,
|
|
328
572
|
showAbout: showAboutProp,
|
|
329
|
-
|
|
573
|
+
showSettingsCommand = true,
|
|
574
|
+
showSignOutCommand = true,
|
|
575
|
+
appCommandGroups,
|
|
576
|
+
commandContext,
|
|
577
|
+
filterLauncherSlot,
|
|
578
|
+
embeddedChatSlot,
|
|
579
|
+
footerSlot,
|
|
580
|
+
}: CommandMenuProps<TAppContext>) {
|
|
330
581
|
const [search, setSearch] = useState("");
|
|
582
|
+
const [appPageStack, setAppPageStack] = useState<
|
|
583
|
+
Array<{ pageId: string; parentSearch: string }>
|
|
584
|
+
>([]);
|
|
585
|
+
const [selectedCommandValue, setSelectedCommandValue] = useState("");
|
|
331
586
|
const models = useChatModels({
|
|
332
587
|
enabled: open && showAgentFallback,
|
|
333
588
|
storageKey: chatModelSelectionStorageKey(chatStorageKey),
|
|
@@ -343,6 +598,22 @@ export function CommandMenu({
|
|
|
343
598
|
const chatReady = providerStatus === "configured";
|
|
344
599
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
345
600
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
601
|
+
const searchValueRef = useRef("");
|
|
602
|
+
const selectionEnabledRef = useRef(false);
|
|
603
|
+
const [selectionEnabled, setSelectionEnabled] = useState(false);
|
|
604
|
+
const selectionResetRef = useRef(0);
|
|
605
|
+
const nextEmptySelectionValue = useCallback(
|
|
606
|
+
() => `${NO_COMMAND_SELECTION}${++selectionResetRef.current}`,
|
|
607
|
+
[],
|
|
608
|
+
);
|
|
609
|
+
const onSearchChangeRef = useRef(onSearchChange);
|
|
610
|
+
onSearchChangeRef.current = onSearchChange;
|
|
611
|
+
const updateSearch = useCallback((value: string) => {
|
|
612
|
+
if (searchValueRef.current === value) return;
|
|
613
|
+
searchValueRef.current = value;
|
|
614
|
+
setSearch(value);
|
|
615
|
+
onSearchChangeRef.current?.(value);
|
|
616
|
+
}, []);
|
|
346
617
|
const nestedDialogsRef = useRef<Array<() => void>>([]);
|
|
347
618
|
const t = useT();
|
|
348
619
|
const registerNestedDialog = useCallback((dismiss: () => void) => {
|
|
@@ -362,6 +633,25 @@ export function CommandMenu({
|
|
|
362
633
|
const latestChangelogId = getChangelogLatestId(changelog);
|
|
363
634
|
const { unseen: changelogUnseen, markSeen: markChangelogSeen } =
|
|
364
635
|
useChangelogSeen(changelogKey ?? "app", latestChangelogId);
|
|
636
|
+
const closeMenu = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
637
|
+
const appCommandContext: AppCommandContext<TAppContext> = {
|
|
638
|
+
search,
|
|
639
|
+
appContext: commandContext,
|
|
640
|
+
close: closeMenu,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const resetSelection = useCallback(() => {
|
|
644
|
+
selectionEnabledRef.current = false;
|
|
645
|
+
setSelectionEnabled(false);
|
|
646
|
+
setSelectedCommandValue(
|
|
647
|
+
initialSelection === "none" ? nextEmptySelectionValue() : "",
|
|
648
|
+
);
|
|
649
|
+
}, [initialSelection, nextEmptySelectionValue]);
|
|
650
|
+
const enableSelection = useCallback(() => {
|
|
651
|
+
if (selectionEnabledRef.current) return;
|
|
652
|
+
selectionEnabledRef.current = true;
|
|
653
|
+
flushSync(() => setSelectionEnabled(true));
|
|
654
|
+
}, []);
|
|
365
655
|
|
|
366
656
|
const openChangelog = useCallback(() => {
|
|
367
657
|
onOpenChange(false);
|
|
@@ -376,17 +666,31 @@ export function CommandMenu({
|
|
|
376
666
|
|
|
377
667
|
useEffect(() => {
|
|
378
668
|
if (!open) {
|
|
379
|
-
|
|
669
|
+
updateSearch("");
|
|
670
|
+
setAppPageStack([]);
|
|
671
|
+
resetSelection();
|
|
380
672
|
return;
|
|
381
673
|
}
|
|
382
674
|
|
|
383
675
|
if (open) {
|
|
384
|
-
|
|
676
|
+
updateSearch("");
|
|
677
|
+
setAppPageStack([]);
|
|
678
|
+
resetSelection();
|
|
385
679
|
requestAnimationFrame(() => {
|
|
386
680
|
inputRef.current?.focus();
|
|
387
681
|
});
|
|
388
682
|
}
|
|
389
|
-
}, [open]);
|
|
683
|
+
}, [open, resetSelection, updateSearch]);
|
|
684
|
+
|
|
685
|
+
const handleCommandValueChange = useCallback(
|
|
686
|
+
(value: string) => {
|
|
687
|
+
if (initialSelection === "none" && !selectionEnabledRef.current) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
setSelectedCommandValue(value);
|
|
691
|
+
},
|
|
692
|
+
[initialSelection],
|
|
693
|
+
);
|
|
390
694
|
|
|
391
695
|
const handleSubmitToAgent = useCallback(() => {
|
|
392
696
|
if (!search.trim()) {
|
|
@@ -453,22 +757,24 @@ export function CommandMenu({
|
|
|
453
757
|
const showAboutRow = showAbout && aboutRowMatches;
|
|
454
758
|
const settingsLabel = t("settingsShortcut.command");
|
|
455
759
|
const showSettingsRow =
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
760
|
+
showSettingsCommand &&
|
|
761
|
+
(!search ||
|
|
762
|
+
[settingsLabel, ...SETTINGS_SEARCH_TERMS]
|
|
763
|
+
.join(" ")
|
|
764
|
+
.toLowerCase()
|
|
765
|
+
.includes(search.toLowerCase()));
|
|
461
766
|
const handleOpenSettings = useCallback(() => {
|
|
462
767
|
onOpenChange(false);
|
|
463
768
|
openSettingsPage();
|
|
464
769
|
}, [onOpenChange]);
|
|
465
770
|
const signOutLabel = t("agentChat.auth.logOut");
|
|
466
771
|
const showSignOutRow =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
772
|
+
showSignOutCommand &&
|
|
773
|
+
(!search ||
|
|
774
|
+
[signOutLabel, ...SIGN_OUT_SEARCH_TERMS]
|
|
775
|
+
.join(" ")
|
|
776
|
+
.toLowerCase()
|
|
777
|
+
.includes(search.toLowerCase()));
|
|
472
778
|
const handleSignOut = useCallback(() => {
|
|
473
779
|
onOpenChange(false);
|
|
474
780
|
void signOut();
|
|
@@ -512,12 +818,19 @@ export function CommandMenu({
|
|
|
512
818
|
|
|
513
819
|
if (child.type === CommandItem) {
|
|
514
820
|
if (!search) return child;
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
.
|
|
821
|
+
const label = getTextContent(props.children as ReactNode);
|
|
822
|
+
const value = typeof props.value === "string" ? props.value : undefined;
|
|
823
|
+
const keywords = Array.isArray(props.keywords)
|
|
824
|
+
? props.keywords.filter(
|
|
825
|
+
(keyword): keyword is string => typeof keyword === "string",
|
|
826
|
+
)
|
|
827
|
+
: [];
|
|
519
828
|
const searchLower = search.toLowerCase();
|
|
520
|
-
|
|
829
|
+
const matches = itemMatcher
|
|
830
|
+
? itemMatcher({ query: search, label, value, keywords })
|
|
831
|
+
: label.toLowerCase().includes(searchLower) ||
|
|
832
|
+
keywords.join(" ").toLowerCase().includes(searchLower);
|
|
833
|
+
if (matches) {
|
|
521
834
|
return child;
|
|
522
835
|
}
|
|
523
836
|
return null;
|
|
@@ -531,149 +844,365 @@ export function CommandMenu({
|
|
|
531
844
|
});
|
|
532
845
|
};
|
|
533
846
|
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
(
|
|
537
|
-
|
|
538
|
-
|
|
847
|
+
const activePageId = appPageStack.at(-1)?.pageId;
|
|
848
|
+
const activeAppPage = activePageId
|
|
849
|
+
? findAppCommandPage(appCommandGroups ?? [], activePageId)
|
|
850
|
+
: undefined;
|
|
851
|
+
const currentAppGroups = activeAppPage?.groups ?? appCommandGroups ?? [];
|
|
852
|
+
const filteredAppGroups = filterAppCommandGroups(
|
|
853
|
+
currentAppGroups,
|
|
854
|
+
search,
|
|
855
|
+
appCommandContext,
|
|
856
|
+
itemMatcher,
|
|
539
857
|
);
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
858
|
+
const appCommandEntries: AppCommandEntry<TAppContext>[] =
|
|
859
|
+
filteredAppGroups.flatMap(({ group, commands }) =>
|
|
860
|
+
commands.map((command) => ({
|
|
861
|
+
group,
|
|
862
|
+
command,
|
|
863
|
+
value: `app-command:${group.id}:${command.id}`,
|
|
864
|
+
})),
|
|
865
|
+
);
|
|
866
|
+
const selectedAppCommand = appCommandEntries.find(
|
|
867
|
+
(entry) => entry.value === selectedCommandValue,
|
|
868
|
+
);
|
|
869
|
+
const firstEnabledAppCommand = appCommandEntries.find(
|
|
870
|
+
({ command }) => command.enabledWhen?.(appCommandContext) ?? true,
|
|
871
|
+
);
|
|
872
|
+
const commandMenuValue =
|
|
873
|
+
initialSelection === "none"
|
|
874
|
+
? selectedCommandValue
|
|
875
|
+
: activeAppPage
|
|
876
|
+
? (selectedAppCommand?.value ??
|
|
877
|
+
firstEnabledAppCommand?.value ??
|
|
878
|
+
NO_COMMAND_SELECTION)
|
|
879
|
+
: undefined;
|
|
880
|
+
const previewAppCommand =
|
|
881
|
+
appCommandEntries.find(
|
|
882
|
+
(entry) => entry.value === (commandMenuValue ?? selectedCommandValue),
|
|
883
|
+
) ?? selectedAppCommand;
|
|
884
|
+
const appPreview = previewAppCommand?.command.preview;
|
|
885
|
+
const renderedAppPreview =
|
|
886
|
+
typeof appPreview === "function"
|
|
887
|
+
? appPreview(appCommandContext)
|
|
888
|
+
: appPreview;
|
|
889
|
+
const filteredChildren = activeAppPage ? null : filterChildren(children);
|
|
890
|
+
const hasAppCommandResults = filteredAppGroups.length > 0;
|
|
891
|
+
const hasChildrenResults = hasCommandItems(filteredChildren);
|
|
892
|
+
const hasCommandResults = hasChildrenResults || hasAppCommandResults;
|
|
893
|
+
const dynamicResults =
|
|
894
|
+
open && !activeAppPage ? renderResults?.(search) : null;
|
|
895
|
+
|
|
896
|
+
const goBackToAppPage = useCallback(() => {
|
|
897
|
+
const currentPage = appPageStack.at(-1);
|
|
898
|
+
if (!currentPage) return;
|
|
899
|
+
setAppPageStack((stack) => stack.slice(0, -1));
|
|
900
|
+
updateSearch(currentPage.parentSearch);
|
|
901
|
+
resetSelection();
|
|
902
|
+
requestAnimationFrame(() => inputRef.current?.focus());
|
|
903
|
+
}, [appPageStack, resetSelection, updateSearch]);
|
|
904
|
+
|
|
905
|
+
const openAppPage = (page: AppCommandPage<TAppContext>) => {
|
|
906
|
+
setAppPageStack((stack) => [
|
|
907
|
+
...stack,
|
|
908
|
+
{ pageId: page.id, parentSearch: search },
|
|
909
|
+
]);
|
|
910
|
+
updateSearch("");
|
|
911
|
+
resetSelection();
|
|
912
|
+
};
|
|
585
913
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
)}
|
|
914
|
+
const selectAppCommand = (command: AppCommand<TAppContext>) => {
|
|
915
|
+
if ("page" in command && command.page) {
|
|
916
|
+
openAppPage(command.page);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (!("onSelect" in command) || !command.onSelect) return;
|
|
920
|
+
if (command.enabledWhen && !command.enabledWhen(appCommandContext)) return;
|
|
921
|
+
if (command.deferSelect === false) {
|
|
922
|
+
command.onSelect(appCommandContext);
|
|
923
|
+
closeMenu();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
closeMenu();
|
|
927
|
+
setTimeout(() => command.onSelect?.(appCommandContext), 50);
|
|
928
|
+
};
|
|
602
929
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
930
|
+
const renderList = (
|
|
931
|
+
results?: ReactNode,
|
|
932
|
+
options?: {
|
|
933
|
+
hasSelectableItems?: boolean;
|
|
934
|
+
},
|
|
935
|
+
) => {
|
|
936
|
+
const usesDefaultResults = results === undefined;
|
|
937
|
+
const renderedResults = usesDefaultResults ? dynamicResults : results;
|
|
938
|
+
const hasDynamicItems =
|
|
939
|
+
options?.hasSelectableItems ??
|
|
940
|
+
(usesDefaultResults && !activeAppPage
|
|
941
|
+
? (renderResultsHasItems?.(search) ?? hasCommandItems(renderedResults))
|
|
942
|
+
: hasCommandItems(renderedResults));
|
|
943
|
+
const hasResults = hasCommandResults || hasDynamicItems;
|
|
944
|
+
const noMatchSuggestions =
|
|
945
|
+
!hasResults && search.trim() ? noMatchSuggestionsSlot?.(search) : null;
|
|
946
|
+
const list = (
|
|
947
|
+
<CommandListPrimitive>
|
|
948
|
+
{activeAppPage ? (
|
|
949
|
+
<CommandGroup heading={activeAppPage.title}>
|
|
950
|
+
<button
|
|
951
|
+
type="button"
|
|
952
|
+
aria-label={t("onboarding.back")}
|
|
953
|
+
data-testid="command-menu-page-back"
|
|
954
|
+
className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-2 text-start text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground"
|
|
955
|
+
onClick={goBackToAppPage}
|
|
956
|
+
onKeyDown={(event) => {
|
|
957
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
958
|
+
event.stopPropagation();
|
|
959
|
+
}
|
|
960
|
+
}}
|
|
613
961
|
>
|
|
614
|
-
<
|
|
615
|
-
<span>{
|
|
616
|
-
|
|
962
|
+
<IconArrowLeft className="h-4 w-4 text-muted-foreground" />
|
|
963
|
+
<span>{t("onboarding.back")}</span>
|
|
964
|
+
<CommandShortcutPrimitive>Esc</CommandShortcutPrimitive>
|
|
965
|
+
</button>
|
|
966
|
+
</CommandGroup>
|
|
967
|
+
) : null}
|
|
968
|
+
|
|
969
|
+
{renderResultsPosition === "before" ? renderedResults : null}
|
|
970
|
+
|
|
971
|
+
{filteredAppGroups.map(({ group, commands }) => (
|
|
972
|
+
<CommandGroup key={group.id} heading={group.heading}>
|
|
973
|
+
{commands.map((command) => {
|
|
974
|
+
const value = `app-command:${group.id}:${command.id}`;
|
|
975
|
+
const enabled = command.enabledWhen?.(appCommandContext) ?? true;
|
|
976
|
+
return (
|
|
977
|
+
<CommandItemPrimitive
|
|
978
|
+
key={command.id}
|
|
979
|
+
value={value}
|
|
980
|
+
disabled={
|
|
981
|
+
!enabled ||
|
|
982
|
+
(initialSelection === "none" && !selectionEnabled)
|
|
983
|
+
}
|
|
984
|
+
className="cursor-pointer gap-2 py-2"
|
|
985
|
+
onSelect={() => selectAppCommand(command)}
|
|
986
|
+
>
|
|
987
|
+
{command.icon}
|
|
988
|
+
<span className="min-w-0 flex-1">
|
|
989
|
+
<span className="block truncate">{command.label}</span>
|
|
990
|
+
{command.description ? (
|
|
991
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
992
|
+
{command.description}
|
|
993
|
+
</span>
|
|
994
|
+
) : null}
|
|
995
|
+
</span>
|
|
996
|
+
{command.metadata ? (
|
|
997
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
998
|
+
{command.metadata}
|
|
999
|
+
</span>
|
|
1000
|
+
) : null}
|
|
1001
|
+
{command.shortcutHint ? (
|
|
1002
|
+
<CommandShortcutPrimitive>
|
|
1003
|
+
{command.shortcutHint}
|
|
1004
|
+
</CommandShortcutPrimitive>
|
|
1005
|
+
) : null}
|
|
1006
|
+
</CommandItemPrimitive>
|
|
1007
|
+
);
|
|
1008
|
+
})}
|
|
1009
|
+
</CommandGroup>
|
|
1010
|
+
))}
|
|
1011
|
+
|
|
1012
|
+
{hasChildrenResults && filteredChildren}
|
|
1013
|
+
|
|
1014
|
+
{renderResultsPosition === "after" ? renderedResults : null}
|
|
1015
|
+
|
|
1016
|
+
{!hasResults && (emptyState != null || noMatchSuggestions != null) ? (
|
|
1017
|
+
<div
|
|
1018
|
+
role="status"
|
|
1019
|
+
aria-live="polite"
|
|
1020
|
+
className="px-3 py-6 text-center text-sm text-muted-foreground"
|
|
1021
|
+
>
|
|
1022
|
+
{emptyState}
|
|
1023
|
+
{noMatchSuggestions != null ? (
|
|
1024
|
+
<div className="mt-3">{noMatchSuggestions}</div>
|
|
1025
|
+
) : null}
|
|
617
1026
|
</div>
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
{providerStatus === "unavailable" ? (
|
|
631
|
-
<div
|
|
632
|
-
className="mb-1 flex items-center justify-between gap-3 rounded-md border border-border bg-muted/50 px-3 py-2 text-sm text-muted-foreground"
|
|
633
|
-
role="status"
|
|
634
|
-
>
|
|
635
|
-
<span>{t("agentChat.setup.providerStatusUnavailable")}</span>
|
|
636
|
-
{providerStatus === "unavailable" ? (
|
|
637
|
-
<button
|
|
638
|
-
type="button"
|
|
639
|
-
className="shrink-0 font-medium text-foreground underline-offset-4 hover:underline"
|
|
640
|
-
onClick={retryProviderStatus}
|
|
1027
|
+
) : null}
|
|
1028
|
+
|
|
1029
|
+
{!activeAppPage && (
|
|
1030
|
+
<>
|
|
1031
|
+
{showSettingsRow && (
|
|
1032
|
+
<>
|
|
1033
|
+
{hasResults && <CommandSeparator />}
|
|
1034
|
+
<div className="p-1">
|
|
1035
|
+
<CommandItemPrimitive
|
|
1036
|
+
className="cursor-pointer gap-2 py-2"
|
|
1037
|
+
disabled={initialSelection === "none" && !selectionEnabled}
|
|
1038
|
+
onSelect={handleOpenSettings}
|
|
641
1039
|
>
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1040
|
+
<IconSettings className="h-4 w-4 text-muted-foreground" />
|
|
1041
|
+
<span>{settingsLabel}</span>
|
|
1042
|
+
<CommandShortcutPrimitive>
|
|
1043
|
+
{getSettingsShortcutHint(isApplePlatform())}
|
|
1044
|
+
</CommandShortcutPrimitive>
|
|
1045
|
+
</CommandItemPrimitive>
|
|
1046
|
+
</div>
|
|
1047
|
+
</>
|
|
1048
|
+
)}
|
|
1049
|
+
|
|
1050
|
+
{showChangelogRow && (
|
|
1051
|
+
<>
|
|
1052
|
+
{(hasResults || showSettingsRow) && <CommandSeparator />}
|
|
1053
|
+
<div className="p-1">
|
|
1054
|
+
<CommandItemPrimitive
|
|
1055
|
+
className="cursor-pointer gap-2 py-2"
|
|
1056
|
+
disabled={initialSelection === "none" && !selectionEnabled}
|
|
1057
|
+
onSelect={openChangelog}
|
|
1058
|
+
>
|
|
1059
|
+
<IconHistory className="h-4 w-4 text-muted-foreground" />
|
|
1060
|
+
<span>{changelogLabel}</span>
|
|
1061
|
+
{changelogUnseen && (
|
|
1062
|
+
<span
|
|
1063
|
+
className="ms-auto h-2 w-2 rounded-full bg-primary"
|
|
1064
|
+
aria-label="New updates available"
|
|
1065
|
+
/>
|
|
1066
|
+
)}
|
|
1067
|
+
</CommandItemPrimitive>
|
|
1068
|
+
</div>
|
|
1069
|
+
</>
|
|
1070
|
+
)}
|
|
1071
|
+
|
|
1072
|
+
{showAboutRow && (
|
|
1073
|
+
<>
|
|
1074
|
+
{(hasResults || showSettingsRow || showChangelogRow) && (
|
|
1075
|
+
<CommandSeparator />
|
|
666
1076
|
)}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1077
|
+
<div className="p-1">
|
|
1078
|
+
<CommandItemPrimitive
|
|
1079
|
+
className="cursor-pointer gap-2 py-2"
|
|
1080
|
+
disabled={initialSelection === "none" && !selectionEnabled}
|
|
1081
|
+
onSelect={openAbout}
|
|
1082
|
+
>
|
|
1083
|
+
<IconInfoCircle className="h-4 w-4 text-muted-foreground" />
|
|
1084
|
+
<span>{aboutLabel}</span>
|
|
1085
|
+
</CommandItemPrimitive>
|
|
1086
|
+
</div>
|
|
1087
|
+
</>
|
|
1088
|
+
)}
|
|
1089
|
+
|
|
1090
|
+
{showSignOutRow && (
|
|
1091
|
+
<>
|
|
1092
|
+
{(hasResults ||
|
|
1093
|
+
showSettingsRow ||
|
|
1094
|
+
showChangelogRow ||
|
|
1095
|
+
showAboutRow) && <CommandSeparator />}
|
|
1096
|
+
<div className="p-1">
|
|
1097
|
+
<CommandItemPrimitive
|
|
1098
|
+
className="cursor-pointer gap-2 py-2"
|
|
1099
|
+
disabled={initialSelection === "none" && !selectionEnabled}
|
|
1100
|
+
onSelect={handleSignOut}
|
|
1101
|
+
>
|
|
1102
|
+
<IconLogout className="h-4 w-4 text-muted-foreground rtl:-scale-x-100" />
|
|
1103
|
+
<span>{signOutLabel}</span>
|
|
1104
|
+
</CommandItemPrimitive>
|
|
1105
|
+
</div>
|
|
1106
|
+
</>
|
|
1107
|
+
)}
|
|
1108
|
+
|
|
1109
|
+
{showAgentFallback && (
|
|
1110
|
+
<>
|
|
1111
|
+
{(hasResults ||
|
|
1112
|
+
showSettingsRow ||
|
|
1113
|
+
showChangelogRow ||
|
|
1114
|
+
showAboutRow ||
|
|
1115
|
+
showSignOutRow) && <CommandSeparator />}
|
|
1116
|
+
<div className="p-1">
|
|
1117
|
+
{providerStatus === "unavailable" ? (
|
|
1118
|
+
<div
|
|
1119
|
+
className="mb-1 flex items-center justify-between gap-3 rounded-md border border-border bg-muted/50 px-3 py-2 text-sm text-muted-foreground"
|
|
1120
|
+
role="status"
|
|
1121
|
+
>
|
|
1122
|
+
<span>
|
|
1123
|
+
{t("agentChat.setup.providerStatusUnavailable")}
|
|
1124
|
+
</span>
|
|
1125
|
+
{providerStatus === "unavailable" ? (
|
|
1126
|
+
<button
|
|
1127
|
+
type="button"
|
|
1128
|
+
className="shrink-0 font-medium text-foreground underline-offset-4 hover:underline"
|
|
1129
|
+
onClick={retryProviderStatus}
|
|
1130
|
+
>
|
|
1131
|
+
{t("agentChat.common.retry")}
|
|
1132
|
+
</button>
|
|
1133
|
+
) : null}
|
|
1134
|
+
</div>
|
|
1135
|
+
) : null}
|
|
1136
|
+
<CommandItemPrimitive
|
|
1137
|
+
className={cn(
|
|
1138
|
+
"gap-2 py-2",
|
|
1139
|
+
chatReady
|
|
1140
|
+
? "cursor-pointer"
|
|
1141
|
+
: "cursor-not-allowed opacity-50",
|
|
1142
|
+
)}
|
|
1143
|
+
disabled={
|
|
1144
|
+
!chatReady ||
|
|
1145
|
+
(initialSelection === "none" && !selectionEnabled)
|
|
1146
|
+
}
|
|
1147
|
+
onSelect={handleSubmitToAgent}
|
|
1148
|
+
>
|
|
1149
|
+
<IconMessage className="h-4 w-4 text-muted-foreground" />
|
|
1150
|
+
<span>
|
|
1151
|
+
{search.trim() ? (
|
|
1152
|
+
<>
|
|
1153
|
+
Ask AI:{" "}
|
|
1154
|
+
<span className="text-muted-foreground">
|
|
1155
|
+
"{search}"
|
|
1156
|
+
</span>
|
|
1157
|
+
</>
|
|
1158
|
+
) : (
|
|
1159
|
+
<span className="text-muted-foreground">
|
|
1160
|
+
Ask AI anything...
|
|
1161
|
+
</span>
|
|
1162
|
+
)}
|
|
1163
|
+
</span>
|
|
1164
|
+
{search.trim() && (
|
|
1165
|
+
<span className="ms-auto text-xs text-muted-foreground">
|
|
1166
|
+
↵
|
|
1167
|
+
</span>
|
|
1168
|
+
)}
|
|
1169
|
+
</CommandItemPrimitive>
|
|
1170
|
+
</div>
|
|
1171
|
+
</>
|
|
1172
|
+
)}
|
|
1173
|
+
</>
|
|
1174
|
+
)}
|
|
1175
|
+
</CommandListPrimitive>
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
if (!activeAppPage && appCommandGroups === undefined && !embeddedChatSlot) {
|
|
1179
|
+
return list;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return (
|
|
1183
|
+
<>
|
|
1184
|
+
{list}
|
|
1185
|
+
{renderedAppPreview ? (
|
|
1186
|
+
<div
|
|
1187
|
+
data-testid="command-menu-inline-preview"
|
|
1188
|
+
role="region"
|
|
1189
|
+
aria-label={previewAppCommand?.command.label}
|
|
1190
|
+
className="border-t border-border p-3"
|
|
1191
|
+
>
|
|
1192
|
+
{renderedAppPreview}
|
|
672
1193
|
</div>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1194
|
+
) : null}
|
|
1195
|
+
{embeddedChatSlot ? (
|
|
1196
|
+
<div
|
|
1197
|
+
data-testid="command-menu-embedded-chat"
|
|
1198
|
+
onKeyDown={stopCommandNavigation}
|
|
1199
|
+
>
|
|
1200
|
+
{embeddedChatSlot(appCommandContext)}
|
|
1201
|
+
</div>
|
|
1202
|
+
) : null}
|
|
1203
|
+
</>
|
|
1204
|
+
);
|
|
1205
|
+
};
|
|
677
1206
|
|
|
678
1207
|
return (
|
|
679
1208
|
<>
|
|
@@ -686,7 +1215,24 @@ export function CommandMenu({
|
|
|
686
1215
|
>
|
|
687
1216
|
<DialogContent
|
|
688
1217
|
ref={containerRef}
|
|
1218
|
+
container={container}
|
|
689
1219
|
aria-describedby={undefined}
|
|
1220
|
+
onPointerDownCapture={(event) => {
|
|
1221
|
+
if (
|
|
1222
|
+
initialSelection === "none" &&
|
|
1223
|
+
(event.target as HTMLElement).closest?.("[cmdk-item]")
|
|
1224
|
+
) {
|
|
1225
|
+
enableSelection();
|
|
1226
|
+
}
|
|
1227
|
+
}}
|
|
1228
|
+
onPointerMoveCapture={(event) => {
|
|
1229
|
+
if (
|
|
1230
|
+
initialSelection === "none" &&
|
|
1231
|
+
(event.target as HTMLElement).closest?.("[cmdk-item]")
|
|
1232
|
+
) {
|
|
1233
|
+
enableSelection();
|
|
1234
|
+
}
|
|
1235
|
+
}}
|
|
690
1236
|
onEscapeKeyDown={(event) => {
|
|
691
1237
|
const dismissNested = nestedDialogsRef.current.at(-1);
|
|
692
1238
|
if (dismissNested) {
|
|
@@ -694,9 +1240,14 @@ export function CommandMenu({
|
|
|
694
1240
|
queueMicrotask(dismissNested);
|
|
695
1241
|
return;
|
|
696
1242
|
}
|
|
1243
|
+
if (appPageStack.length > 0) {
|
|
1244
|
+
event.preventDefault();
|
|
1245
|
+
goBackToAppPage();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
697
1248
|
if (clearSearchOnEscape && search.length > 0) {
|
|
698
1249
|
event.preventDefault();
|
|
699
|
-
|
|
1250
|
+
updateSearch("");
|
|
700
1251
|
}
|
|
701
1252
|
}}
|
|
702
1253
|
hideClose
|
|
@@ -709,7 +1260,8 @@ export function CommandMenu({
|
|
|
709
1260
|
transition: "none",
|
|
710
1261
|
}}
|
|
711
1262
|
className={cn(
|
|
712
|
-
"fixed left-1/2 top-[15vh] !max-h-none -translate-x-1/2 !translate-y-0 !gap-0 w-full
|
|
1263
|
+
"fixed left-1/2 top-[15vh] !max-h-none -translate-x-1/2 !translate-y-0 !gap-0 w-full",
|
|
1264
|
+
size === "wide" ? "max-w-3xl" : "max-w-lg",
|
|
713
1265
|
"rounded-lg border border-border bg-popover p-0 text-popover-foreground shadow-lg",
|
|
714
1266
|
className,
|
|
715
1267
|
)}
|
|
@@ -724,22 +1276,69 @@ export function CommandMenu({
|
|
|
724
1276
|
loop
|
|
725
1277
|
shouldFilter={false}
|
|
726
1278
|
className="h-auto rounded-lg"
|
|
1279
|
+
value={commandMenuValue}
|
|
727
1280
|
>
|
|
728
1281
|
<CommandMenuContext.Provider
|
|
729
|
-
value={{
|
|
1282
|
+
value={{
|
|
1283
|
+
search,
|
|
1284
|
+
onOpenChange,
|
|
1285
|
+
registerNestedDialog,
|
|
1286
|
+
selectionEnabled:
|
|
1287
|
+
initialSelection !== "none" || selectionEnabled,
|
|
1288
|
+
}}
|
|
730
1289
|
>
|
|
1290
|
+
{(appCommandGroups !== undefined ||
|
|
1291
|
+
embeddedChatSlot ||
|
|
1292
|
+
initialSelection !== undefined) && (
|
|
1293
|
+
<CommandSelectionTracker onChange={handleCommandValueChange} />
|
|
1294
|
+
)}
|
|
731
1295
|
{/* Search input */}
|
|
732
1296
|
<CommandInputPrimitive
|
|
733
1297
|
ref={inputRef}
|
|
734
1298
|
value={search}
|
|
735
|
-
onValueChange={
|
|
1299
|
+
onValueChange={(value) => {
|
|
1300
|
+
updateSearch(value);
|
|
1301
|
+
selectionEnabledRef.current = false;
|
|
1302
|
+
if (initialSelection === "none") setSelectionEnabled(false);
|
|
1303
|
+
setSelectedCommandValue(
|
|
1304
|
+
initialSelection === "none"
|
|
1305
|
+
? nextEmptySelectionValue()
|
|
1306
|
+
: "",
|
|
1307
|
+
);
|
|
1308
|
+
}}
|
|
1309
|
+
onKeyDown={(event) => {
|
|
1310
|
+
if (
|
|
1311
|
+
initialSelection === "none" &&
|
|
1312
|
+
(event.key === "ArrowDown" || event.key === "ArrowUp")
|
|
1313
|
+
) {
|
|
1314
|
+
enableSelection();
|
|
1315
|
+
}
|
|
1316
|
+
}}
|
|
736
1317
|
placeholder={placeholder}
|
|
737
1318
|
aria-label={inputLabel}
|
|
738
1319
|
/>
|
|
739
1320
|
|
|
1321
|
+
{open && filterLauncherSlot ? (
|
|
1322
|
+
<div
|
|
1323
|
+
data-testid="command-menu-filter-launchers"
|
|
1324
|
+
className="flex flex-wrap items-center gap-2 border-b border-border px-3 py-2"
|
|
1325
|
+
onKeyDown={stopCommandNavigation}
|
|
1326
|
+
>
|
|
1327
|
+
{filterLauncherSlot(appCommandContext)}
|
|
1328
|
+
</div>
|
|
1329
|
+
) : null}
|
|
1330
|
+
|
|
740
1331
|
{open && renderContent
|
|
741
1332
|
? renderContent({ search, renderList })
|
|
742
1333
|
: open && renderList()}
|
|
1334
|
+
{open && footerSlot != null ? (
|
|
1335
|
+
<div
|
|
1336
|
+
role="note"
|
|
1337
|
+
className="border-t border-border px-3 py-2 text-xs text-muted-foreground"
|
|
1338
|
+
>
|
|
1339
|
+
{footerSlot}
|
|
1340
|
+
</div>
|
|
1341
|
+
) : null}
|
|
743
1342
|
</CommandMenuContext.Provider>
|
|
744
1343
|
</CommandPrimitive>
|
|
745
1344
|
</DialogContent>
|