@agent-native/toolkit 0.218.0 → 0.220.0-nightly-20261023183804
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/dist/app/agentkit/react/styles.css +13 -6
- package/dist/app/chat/AgentKitAssistantChat.d.ts +2 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +11 -3
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentPanel.js +6 -6
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +5 -3
- package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/action-widget.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/action-widget.js +1 -0
- package/dist/app/chat/agentkit-chat/action-widget.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +2 -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 +3 -1
- package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-call-display.js +5 -4
- package/dist/app/chat/chat/tool-call-display.js.map +1 -1
- package/dist/app/chat/chat/tool-render-registry.d.ts +2 -0
- package/dist/app/chat/chat/tool-render-registry.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-render-registry.js.map +1 -1
- package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
- package/dist/app/chat/conversation/AgentConversation.js +6 -1
- package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
- package/dist/app/shared/CommandMenu.d.ts +2 -0
- package/dist/app/shared/CommandMenu.d.ts.map +1 -1
- package/dist/app/shared/CommandMenu.js +11 -6
- package/dist/app/shared/CommandMenu.js.map +1 -1
- package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
- package/dist/composer/ComposerContextMenu.js +27 -2
- package/dist/composer/ComposerContextMenu.js.map +1 -1
- package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/composer/ComposerPlusMenu.js +50 -4
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/TiptapComposer.js +1 -1
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/package.json +4 -4
- package/src/app/agentkit/react/components.spec.tsx +7 -5
- package/src/app/agentkit/react/styles.css +13 -6
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +258 -27
- package/src/app/chat/AgentKitAssistantChat.tsx +15 -2
- package/src/app/chat/AgentPanel.tsx +8 -8
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +17 -0
- package/src/app/chat/MultiTabAssistantChat.tsx +7 -2
- package/src/app/chat/agentkit-chat/action-widget.spec.tsx +62 -0
- package/src/app/chat/agentkit-chat/action-widget.tsx +1 -0
- package/src/app/chat/chat/surface-types.ts +2 -0
- package/src/app/chat/chat/tool-call-display.tsx +10 -1
- package/src/app/chat/chat/tool-render-registry.tsx +2 -0
- package/src/app/chat/conversation/AgentConversation.spec.tsx +51 -1
- package/src/app/chat/conversation/AgentConversation.tsx +11 -2
- package/src/app/shared/CommandMenu.spec.tsx +175 -6
- package/src/app/shared/CommandMenu.tsx +24 -7
- package/src/composer/ComposerContextMenu.tsx +32 -3
- package/src/composer/ComposerPlusMenu.parity.spec.tsx +96 -2
- package/src/composer/ComposerPlusMenu.tsx +48 -4
- package/src/composer/TiptapComposer.tsx +1 -1
|
@@ -128,6 +128,8 @@ interface PendingSend {
|
|
|
128
128
|
actionScope?: AgentActionScope;
|
|
129
129
|
/** See `AgentChatMessage.approvedToolCalls`. */
|
|
130
130
|
approvedToolCalls?: string[];
|
|
131
|
+
/** See `AssistantChatSendOptions.surface`. */
|
|
132
|
+
surface?: string;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
/**
|
|
@@ -269,6 +271,7 @@ function deliverPendingSend(
|
|
|
269
271
|
: {}),
|
|
270
272
|
...(send.usageLabel ? { usageLabel: send.usageLabel } : {}),
|
|
271
273
|
...(send.actionScope ? { actionScope: send.actionScope } : {}),
|
|
274
|
+
...(send.surface ? { surface: send.surface } : {}),
|
|
272
275
|
// An approval resume is a protocol continuation, not a visible prompt.
|
|
273
276
|
...(send.approvedToolCalls
|
|
274
277
|
? { approvedToolCalls: send.approvedToolCalls, hideUserMessage: true }
|
|
@@ -2408,6 +2411,7 @@ export function MultiTabAssistantChat({
|
|
|
2408
2411
|
usageLabel,
|
|
2409
2412
|
actionScope,
|
|
2410
2413
|
approvedToolCalls,
|
|
2414
|
+
surface,
|
|
2411
2415
|
} = parsed;
|
|
2412
2416
|
const requestedTabId = parsed.tabId;
|
|
2413
2417
|
const requestMode =
|
|
@@ -2471,6 +2475,7 @@ export function MultiTabAssistantChat({
|
|
|
2471
2475
|
? { submissionExpiresAt: parsed.submissionExpiresAt }
|
|
2472
2476
|
: {}),
|
|
2473
2477
|
...(approvedToolCalls ? { approvedToolCalls } : {}),
|
|
2478
|
+
...(surface ? { surface } : {}),
|
|
2474
2479
|
};
|
|
2475
2480
|
// Resolved once, up front, and carried with the send until a thread
|
|
2476
2481
|
// exists to key it by. Applying it only when `model` matched
|
|
@@ -3680,7 +3685,7 @@ export function MultiTabAssistantChat({
|
|
|
3680
3685
|
key={tab.id}
|
|
3681
3686
|
ref={isActive ? activeTabRefCb : undefined}
|
|
3682
3687
|
className={cn(
|
|
3683
|
-
"agent-tab agent-tab-group relative flex items-center rounded-md text-[11px] font-medium shrink
|
|
3688
|
+
"agent-tab agent-tab-group relative flex items-center rounded-md text-[11px] font-medium shrink min-w-[56px] max-w-[130px]",
|
|
3684
3689
|
isActive
|
|
3685
3690
|
? "bg-accent text-foreground ring-1 ring-inset ring-border/60 shadow-sm"
|
|
3686
3691
|
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
@@ -3782,7 +3787,7 @@ export function MultiTabAssistantChat({
|
|
|
3782
3787
|
: undefined
|
|
3783
3788
|
}
|
|
3784
3789
|
className={cn(
|
|
3785
|
-
"agent-tab agent-tab-group relative flex shrink
|
|
3790
|
+
"agent-tab agent-tab-group relative flex shrink items-center rounded-md text-[10px] font-medium min-w-[48px] max-w-[130px]",
|
|
3786
3791
|
tab.id === activeThreadId
|
|
3787
3792
|
? "bg-accent text-foreground"
|
|
3788
3793
|
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
@@ -32,6 +32,68 @@ import { AgentKitActionWidget } from "./action-widget.js";
|
|
|
32
32
|
const catalog = createToolkitI18nCatalog({ messages: {} });
|
|
33
33
|
|
|
34
34
|
describe("AgentKitActionWidget", () => {
|
|
35
|
+
it.each(["running", "completed", "failed", "cancelled"] as const)(
|
|
36
|
+
"exposes the %s tool status to renderers while retaining isRunning",
|
|
37
|
+
async (status) => {
|
|
38
|
+
const toolCall: AgentToolCall = {
|
|
39
|
+
id: "tool-1",
|
|
40
|
+
name: "preview-inbox",
|
|
41
|
+
input: {},
|
|
42
|
+
output: { ok: true },
|
|
43
|
+
status,
|
|
44
|
+
};
|
|
45
|
+
const widget: AgentWidget = {
|
|
46
|
+
id: "tool-1:chat-ui",
|
|
47
|
+
kind: "mail.inbox-preview",
|
|
48
|
+
data: { toolCallId: toolCall.id, toolName: toolCall.name },
|
|
49
|
+
};
|
|
50
|
+
const client = new AgentKitClient({
|
|
51
|
+
transport: {
|
|
52
|
+
async startRun() {
|
|
53
|
+
return { runId: "run-1" };
|
|
54
|
+
},
|
|
55
|
+
async *subscribeToRun() {},
|
|
56
|
+
async cancelRun() {},
|
|
57
|
+
async getThreadSnapshot() {
|
|
58
|
+
return {
|
|
59
|
+
id: "thread-1",
|
|
60
|
+
createdAt: "2026-09-26T00:00:00.000Z",
|
|
61
|
+
updatedAt: "2026-09-26T00:00:00.000Z",
|
|
62
|
+
messages: [],
|
|
63
|
+
toolCalls: [toolCall],
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
await client.loadThread("thread-1");
|
|
69
|
+
const unregister = registerActionChatRenderer({
|
|
70
|
+
id: `test.agentkit-action-widget-status-${status}`,
|
|
71
|
+
renderer: widget.kind,
|
|
72
|
+
Component: ({ context }: ToolRendererProps) => (
|
|
73
|
+
<output
|
|
74
|
+
data-status={context.status}
|
|
75
|
+
data-running={String(context.isRunning)}
|
|
76
|
+
/>
|
|
77
|
+
),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const html = renderToStaticMarkup(
|
|
82
|
+
<AgentKitProvider controller={client} threadId="thread-1">
|
|
83
|
+
<AgentKitActionWidget value={widget} threadId="thread-1" />
|
|
84
|
+
</AgentKitProvider>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(html).toContain(`data-status="${status}"`);
|
|
88
|
+
expect(html).toContain(
|
|
89
|
+
`data-running="${String(status === "running")}"`,
|
|
90
|
+
);
|
|
91
|
+
} finally {
|
|
92
|
+
unregister();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
35
97
|
it("resolves the renderer with stored action args and structured result", async () => {
|
|
36
98
|
const toolCall: AgentToolCall = {
|
|
37
99
|
id: "tool-1",
|
|
@@ -62,6 +62,8 @@ export interface AssistantChatSendOptions {
|
|
|
62
62
|
approvedToolCalls?: string[];
|
|
63
63
|
/** Send as a protocol continuation that stays out of visible history. */
|
|
64
64
|
hideUserMessage?: boolean;
|
|
65
|
+
/** The app surface this send came from, reported with the turn's analytics. */
|
|
66
|
+
surface?: string;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export type AssistantChatSubmitResult =
|
|
@@ -79,7 +79,10 @@ import {
|
|
|
79
79
|
IntegrationToolBadge,
|
|
80
80
|
resolveToolIntegration,
|
|
81
81
|
} from "./tool-integration.js";
|
|
82
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
resolveToolRenderer,
|
|
84
|
+
type ToolRendererContext,
|
|
85
|
+
} from "./tool-render-registry.js";
|
|
83
86
|
import {
|
|
84
87
|
isBuiltinDataWidgetActionRenderer,
|
|
85
88
|
isBuiltinWorkspaceFileResult,
|
|
@@ -636,6 +639,7 @@ export function ToolCallDisplay({
|
|
|
636
639
|
argsText,
|
|
637
640
|
args,
|
|
638
641
|
result,
|
|
642
|
+
status,
|
|
639
643
|
mcpApp,
|
|
640
644
|
chatUI,
|
|
641
645
|
isRunning,
|
|
@@ -656,6 +660,7 @@ export function ToolCallDisplay({
|
|
|
656
660
|
argsText?: string;
|
|
657
661
|
args: Record<string, unknown>;
|
|
658
662
|
result?: string;
|
|
663
|
+
status?: ToolRendererContext["status"];
|
|
659
664
|
mcpApp?: AgentMcpAppPayload;
|
|
660
665
|
chatUI?: ActionChatUIConfig;
|
|
661
666
|
isRunning: boolean;
|
|
@@ -750,6 +755,7 @@ export function ToolCallDisplay({
|
|
|
750
755
|
argsText={argsText}
|
|
751
756
|
args={args}
|
|
752
757
|
result={result}
|
|
758
|
+
status={status}
|
|
753
759
|
mcpApp={mcpApp}
|
|
754
760
|
chatUI={chatUI}
|
|
755
761
|
isRunning={effectiveIsRunning}
|
|
@@ -775,6 +781,7 @@ function ToolCallDisplayGeneric({
|
|
|
775
781
|
argsText,
|
|
776
782
|
args,
|
|
777
783
|
result,
|
|
784
|
+
status,
|
|
778
785
|
mcpApp,
|
|
779
786
|
chatUI,
|
|
780
787
|
isRunning,
|
|
@@ -794,6 +801,7 @@ function ToolCallDisplayGeneric({
|
|
|
794
801
|
argsText?: string;
|
|
795
802
|
args: Record<string, unknown>;
|
|
796
803
|
result?: string;
|
|
804
|
+
status?: ToolRendererContext["status"];
|
|
797
805
|
mcpApp?: AgentMcpAppPayload;
|
|
798
806
|
chatUI?: ActionChatUIConfig;
|
|
799
807
|
isRunning: boolean;
|
|
@@ -923,6 +931,7 @@ function ToolCallDisplayGeneric({
|
|
|
923
931
|
args,
|
|
924
932
|
resultText: result,
|
|
925
933
|
resultJson: parsedResult,
|
|
934
|
+
status,
|
|
926
935
|
isRunning,
|
|
927
936
|
isActiveTail,
|
|
928
937
|
dataChartLayout,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentToolCall } from "@agent-native/agentkit/protocol";
|
|
1
2
|
import type { ActionChatUIConfig } from "@agent-native/core/action-ui";
|
|
2
3
|
import type { ComponentType } from "react";
|
|
3
4
|
|
|
@@ -14,6 +15,7 @@ export interface ToolRendererContext {
|
|
|
14
15
|
toolName?: string;
|
|
15
16
|
}>;
|
|
16
17
|
widgetId?: string;
|
|
18
|
+
status?: AgentToolCall["status"];
|
|
17
19
|
isRunning: boolean;
|
|
18
20
|
isActiveTail?: boolean;
|
|
19
21
|
chatUI?: ActionChatUIConfig;
|
|
@@ -116,7 +116,9 @@ describe("AgentConversationMessageView", () => {
|
|
|
116
116
|
id: "example-renderer",
|
|
117
117
|
match: "example-tool",
|
|
118
118
|
Component: ({ context }) => (
|
|
119
|
-
<div data-testid="example-renderer">
|
|
119
|
+
<div data-testid="example-renderer">
|
|
120
|
+
Custom renderer: {context.resultText}
|
|
121
|
+
</div>
|
|
120
122
|
),
|
|
121
123
|
});
|
|
122
124
|
act(() =>
|
|
@@ -144,11 +146,59 @@ describe("AgentConversationMessageView", () => {
|
|
|
144
146
|
expect(container.querySelector('[data-testid="example-renderer"]')).toBe(
|
|
145
147
|
null,
|
|
146
148
|
);
|
|
149
|
+
expect(container.textContent).not.toContain("Custom renderer:");
|
|
150
|
+
expect(container.querySelector("pre span")?.textContent).toBe(
|
|
151
|
+
"Example Document failed.",
|
|
152
|
+
);
|
|
147
153
|
expect(
|
|
148
154
|
container.querySelector("pre span")?.hasAttribute("data-an-mask"),
|
|
149
155
|
).toBe(true);
|
|
150
156
|
});
|
|
151
157
|
|
|
158
|
+
it.each([
|
|
159
|
+
["running", "running"],
|
|
160
|
+
["activity", "running"],
|
|
161
|
+
["completed", "completed"],
|
|
162
|
+
] as const)(
|
|
163
|
+
"passes %s status to conversation custom renderers as %s",
|
|
164
|
+
(state, status) => {
|
|
165
|
+
registerToolRenderer({
|
|
166
|
+
id: "example-status-renderer",
|
|
167
|
+
match: "example-status-tool",
|
|
168
|
+
Component: ({ context }) => (
|
|
169
|
+
<div data-testid="example-tool-status" data-status={context.status} />
|
|
170
|
+
),
|
|
171
|
+
});
|
|
172
|
+
act(() =>
|
|
173
|
+
root.render(
|
|
174
|
+
<AgentConversationMessageView
|
|
175
|
+
message={{
|
|
176
|
+
id: "message-status",
|
|
177
|
+
role: "assistant",
|
|
178
|
+
parts: [
|
|
179
|
+
{
|
|
180
|
+
id: "tool-status",
|
|
181
|
+
type: "tool",
|
|
182
|
+
tool: {
|
|
183
|
+
id: "tool-status",
|
|
184
|
+
name: "example-status-tool",
|
|
185
|
+
state,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}}
|
|
190
|
+
/>,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
container
|
|
196
|
+
.querySelector('[data-testid="example-tool-status"]')
|
|
197
|
+
?.getAttribute("data-status"),
|
|
198
|
+
).toBe(status);
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
152
202
|
it.each(["errored", "completed"] as const)(
|
|
153
203
|
"blocks only an errored tool's MCP App from replays (%s)",
|
|
154
204
|
(state) => {
|
|
@@ -17,7 +17,10 @@ import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
|
|
|
17
17
|
import remarkGfm from "remark-gfm";
|
|
18
18
|
|
|
19
19
|
import { ActionChatUiSurface } from "../chat/action-chat-ui-surface.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
resolveToolRenderer,
|
|
22
|
+
type ToolRendererContext,
|
|
23
|
+
} from "../chat/tool-render-registry.js";
|
|
21
24
|
import {
|
|
22
25
|
isBuiltinConnectRequiredResult,
|
|
23
26
|
resolveBuiltinActionChatRenderer,
|
|
@@ -407,11 +410,17 @@ function parseJsonObject(value: string | undefined): Record<string, unknown> {
|
|
|
407
410
|
function ConversationToolCall({ tool }: { tool: AgentConversationToolCall }) {
|
|
408
411
|
const t = useT();
|
|
409
412
|
const resultJson = parseJsonText(tool.result);
|
|
410
|
-
const nativeToolContext = {
|
|
413
|
+
const nativeToolContext: ToolRendererContext = {
|
|
411
414
|
toolName: tool.name,
|
|
412
415
|
args: tool.args ?? parseJsonObject(tool.input),
|
|
413
416
|
resultText: tool.result,
|
|
414
417
|
resultJson: tool.resultJson ?? resultJson,
|
|
418
|
+
status:
|
|
419
|
+
tool.state === "errored"
|
|
420
|
+
? undefined
|
|
421
|
+
: tool.state === "activity"
|
|
422
|
+
? "running"
|
|
423
|
+
: tool.state,
|
|
415
424
|
isRunning: tool.state === "running" || tool.state === "activity",
|
|
416
425
|
chatUI: tool.chatUI,
|
|
417
426
|
};
|
|
@@ -9,13 +9,19 @@ const agentEngineStatusMock = vi.hoisted(() => ({
|
|
|
9
9
|
state: "missing" as "unknown" | "configured" | "missing" | "unavailable",
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
vi.mock(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
vi.mock(
|
|
13
|
+
"@agent-native/core/client/use-agent-engine-configured",
|
|
14
|
+
async (importOriginal) => ({
|
|
15
|
+
...(await importOriginal<
|
|
16
|
+
typeof import("@agent-native/core/client/use-agent-engine-configured")
|
|
17
|
+
>()),
|
|
18
|
+
useAgentEngineConfigured: () => ({
|
|
19
|
+
canChat: agentEngineStatusMock.state === "configured",
|
|
20
|
+
missing: agentEngineStatusMock.state === "missing",
|
|
21
|
+
state: agentEngineStatusMock.state,
|
|
22
|
+
}),
|
|
17
23
|
}),
|
|
18
|
-
|
|
24
|
+
);
|
|
19
25
|
|
|
20
26
|
vi.mock("../chat/chat/run-recovery.js", async () => {
|
|
21
27
|
const { createElement } = await import("react");
|
|
@@ -44,6 +50,7 @@ import {
|
|
|
44
50
|
openAgentSettings,
|
|
45
51
|
useCommandMenuNestedDialog,
|
|
46
52
|
useCommandMenuShortcut,
|
|
53
|
+
type AppCommandContext,
|
|
47
54
|
type AppCommandGroup,
|
|
48
55
|
type CommandMenuItemMatchContext,
|
|
49
56
|
type CommandMenuItemMatcher,
|
|
@@ -1836,6 +1843,168 @@ describe("CommandMenu", () => {
|
|
|
1836
1843
|
).toContain("Selected preview");
|
|
1837
1844
|
});
|
|
1838
1845
|
|
|
1846
|
+
it("renders opt-in app command previews in only the selected row", () => {
|
|
1847
|
+
renderAppCommands({
|
|
1848
|
+
groups: [
|
|
1849
|
+
{
|
|
1850
|
+
id: "results",
|
|
1851
|
+
order: 1,
|
|
1852
|
+
commands: [
|
|
1853
|
+
{
|
|
1854
|
+
id: "record",
|
|
1855
|
+
order: 1,
|
|
1856
|
+
label: "Meeting notes",
|
|
1857
|
+
inlinePreview: <span>First row preview</span>,
|
|
1858
|
+
onSelect() {},
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
id: "meeting-agenda",
|
|
1862
|
+
order: 2,
|
|
1863
|
+
label: "Meeting agenda",
|
|
1864
|
+
inlinePreview: <span>Second row preview</span>,
|
|
1865
|
+
onSelect() {},
|
|
1866
|
+
},
|
|
1867
|
+
],
|
|
1868
|
+
},
|
|
1869
|
+
],
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
const firstRow = document.querySelector(
|
|
1873
|
+
'[cmdk-item][data-value="app-command:results:record"]',
|
|
1874
|
+
);
|
|
1875
|
+
const secondRow = document.querySelector(
|
|
1876
|
+
'[cmdk-item][data-value="app-command:results:meeting-agenda"]',
|
|
1877
|
+
);
|
|
1878
|
+
expect(
|
|
1879
|
+
firstRow?.querySelector(
|
|
1880
|
+
'[data-testid="command-menu-app-command-preview"]',
|
|
1881
|
+
)?.textContent,
|
|
1882
|
+
).toBe("First row preview");
|
|
1883
|
+
expect(
|
|
1884
|
+
secondRow?.querySelector(
|
|
1885
|
+
'[data-testid="command-menu-app-command-preview"]',
|
|
1886
|
+
),
|
|
1887
|
+
).toBeNull();
|
|
1888
|
+
|
|
1889
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1890
|
+
expect(input).toBeTruthy();
|
|
1891
|
+
act(() =>
|
|
1892
|
+
input!.dispatchEvent(
|
|
1893
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
1894
|
+
),
|
|
1895
|
+
);
|
|
1896
|
+
|
|
1897
|
+
expect(
|
|
1898
|
+
firstRow?.querySelector(
|
|
1899
|
+
'[data-testid="command-menu-app-command-preview"]',
|
|
1900
|
+
),
|
|
1901
|
+
).toBeNull();
|
|
1902
|
+
expect(
|
|
1903
|
+
secondRow?.querySelector(
|
|
1904
|
+
'[data-testid="command-menu-app-command-preview"]',
|
|
1905
|
+
)?.textContent,
|
|
1906
|
+
).toBe("Second row preview");
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
it("keeps the selected app preview when search retains the highlighted row", () => {
|
|
1910
|
+
renderAppCommands({
|
|
1911
|
+
groups: [
|
|
1912
|
+
{
|
|
1913
|
+
id: "results",
|
|
1914
|
+
order: 1,
|
|
1915
|
+
commands: [
|
|
1916
|
+
{
|
|
1917
|
+
id: "meeting-notes",
|
|
1918
|
+
order: 1,
|
|
1919
|
+
label: "Meeting notes",
|
|
1920
|
+
inlinePreview: <span>Meeting preview</span>,
|
|
1921
|
+
onSelect() {},
|
|
1922
|
+
},
|
|
1923
|
+
{
|
|
1924
|
+
id: "other-task",
|
|
1925
|
+
order: 2,
|
|
1926
|
+
label: "Other task",
|
|
1927
|
+
inlinePreview: <span>Other preview</span>,
|
|
1928
|
+
onSelect() {},
|
|
1929
|
+
},
|
|
1930
|
+
],
|
|
1931
|
+
},
|
|
1932
|
+
],
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
const selectedPreview = () =>
|
|
1936
|
+
document.querySelector(
|
|
1937
|
+
'[cmdk-item][data-value="app-command:results:meeting-notes"] [data-testid="command-menu-app-command-preview"]',
|
|
1938
|
+
)?.textContent;
|
|
1939
|
+
|
|
1940
|
+
expect(selectedPreview()).toBe("Meeting preview");
|
|
1941
|
+
search("Meeting");
|
|
1942
|
+
expect(selectedPreview()).toBe("Meeting preview");
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
it("passes app context and search to an app command row preview", () => {
|
|
1946
|
+
const renderPreview = vi.fn(
|
|
1947
|
+
({
|
|
1948
|
+
search: currentSearch,
|
|
1949
|
+
appContext,
|
|
1950
|
+
}: AppCommandContext<{ workspace: string }>) => (
|
|
1951
|
+
<span>{`${currentSearch}|${appContext?.workspace}`}</span>
|
|
1952
|
+
),
|
|
1953
|
+
);
|
|
1954
|
+
renderAppCommands({
|
|
1955
|
+
initialSelection: "none",
|
|
1956
|
+
context: { workspace: "alpha" },
|
|
1957
|
+
groups: [
|
|
1958
|
+
{
|
|
1959
|
+
id: "results",
|
|
1960
|
+
order: 1,
|
|
1961
|
+
commands: [
|
|
1962
|
+
{
|
|
1963
|
+
id: "workspace",
|
|
1964
|
+
order: 1,
|
|
1965
|
+
label: "Open workspace",
|
|
1966
|
+
inlinePreview: renderPreview,
|
|
1967
|
+
onSelect() {},
|
|
1968
|
+
},
|
|
1969
|
+
],
|
|
1970
|
+
},
|
|
1971
|
+
],
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
const input = document.querySelector<HTMLInputElement>("[cmdk-input]");
|
|
1975
|
+
expect(input).toBeTruthy();
|
|
1976
|
+
const moveDown = () =>
|
|
1977
|
+
act(() =>
|
|
1978
|
+
input!.dispatchEvent(
|
|
1979
|
+
new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
|
1980
|
+
),
|
|
1981
|
+
);
|
|
1982
|
+
moveDown();
|
|
1983
|
+
|
|
1984
|
+
expect(renderPreview).toHaveBeenCalledWith(
|
|
1985
|
+
expect.objectContaining({
|
|
1986
|
+
search: "",
|
|
1987
|
+
appContext: { workspace: "alpha" },
|
|
1988
|
+
}),
|
|
1989
|
+
);
|
|
1990
|
+
expect(
|
|
1991
|
+
document.querySelector('[data-testid="command-menu-app-command-preview"]')
|
|
1992
|
+
?.textContent,
|
|
1993
|
+
).toBe("|alpha");
|
|
1994
|
+
|
|
1995
|
+
search("workspace");
|
|
1996
|
+
moveDown();
|
|
1997
|
+
expect(
|
|
1998
|
+
document.querySelector(
|
|
1999
|
+
'[cmdk-item][data-value="app-command:results:workspace"]',
|
|
2000
|
+
),
|
|
2001
|
+
).toBeTruthy();
|
|
2002
|
+
expect(
|
|
2003
|
+
document.querySelector('[data-testid="command-menu-app-command-preview"]')
|
|
2004
|
+
?.textContent,
|
|
2005
|
+
).toBe("workspace|alpha");
|
|
2006
|
+
});
|
|
2007
|
+
|
|
1839
2008
|
it("hides the app preview when a non-app result is selected", () => {
|
|
1840
2009
|
renderAppCommands({
|
|
1841
2010
|
groups: [
|
|
@@ -304,6 +304,10 @@ interface AppCommandBase<TAppContext> {
|
|
|
304
304
|
preview?:
|
|
305
305
|
| ReactNode
|
|
306
306
|
| ((context: AppCommandContext<TAppContext>) => ReactNode);
|
|
307
|
+
/** Content rendered at the end of this command's selected row. */
|
|
308
|
+
inlinePreview?:
|
|
309
|
+
| ReactNode
|
|
310
|
+
| ((context: AppCommandContext<TAppContext>) => ReactNode);
|
|
307
311
|
visibleWhen?: (context: AppCommandContext<TAppContext>) => boolean;
|
|
308
312
|
enabledWhen?: (context: AppCommandContext<TAppContext>) => boolean;
|
|
309
313
|
deferSelect?: boolean;
|
|
@@ -877,9 +881,10 @@ export function CommandMenu<TAppContext = unknown>({
|
|
|
877
881
|
firstEnabledAppCommand?.value ??
|
|
878
882
|
NO_COMMAND_SELECTION)
|
|
879
883
|
: undefined;
|
|
884
|
+
const selectedAppCommandValue = commandMenuValue ?? selectedCommandValue;
|
|
880
885
|
const previewAppCommand =
|
|
881
886
|
appCommandEntries.find(
|
|
882
|
-
(entry) => entry.value ===
|
|
887
|
+
(entry) => entry.value === selectedAppCommandValue,
|
|
883
888
|
) ?? selectedAppCommand;
|
|
884
889
|
const appPreview = previewAppCommand?.command.preview;
|
|
885
890
|
const renderedAppPreview =
|
|
@@ -973,6 +978,12 @@ export function CommandMenu<TAppContext = unknown>({
|
|
|
973
978
|
{commands.map((command) => {
|
|
974
979
|
const value = `app-command:${group.id}:${command.id}`;
|
|
975
980
|
const enabled = command.enabledWhen?.(appCommandContext) ?? true;
|
|
981
|
+
const inlinePreview =
|
|
982
|
+
value === selectedAppCommandValue
|
|
983
|
+
? typeof command.inlinePreview === "function"
|
|
984
|
+
? command.inlinePreview(appCommandContext)
|
|
985
|
+
: command.inlinePreview
|
|
986
|
+
: null;
|
|
976
987
|
return (
|
|
977
988
|
<CommandItemPrimitive
|
|
978
989
|
key={command.id}
|
|
@@ -1003,6 +1014,14 @@ export function CommandMenu<TAppContext = unknown>({
|
|
|
1003
1014
|
{command.shortcutHint}
|
|
1004
1015
|
</CommandShortcutPrimitive>
|
|
1005
1016
|
) : null}
|
|
1017
|
+
{inlinePreview != null ? (
|
|
1018
|
+
<div
|
|
1019
|
+
data-testid="command-menu-app-command-preview"
|
|
1020
|
+
className="ms-auto min-w-0"
|
|
1021
|
+
>
|
|
1022
|
+
{inlinePreview}
|
|
1023
|
+
</div>
|
|
1024
|
+
) : null}
|
|
1006
1025
|
</CommandItemPrimitive>
|
|
1007
1026
|
);
|
|
1008
1027
|
})}
|
|
@@ -1299,12 +1318,10 @@ export function CommandMenu<TAppContext = unknown>({
|
|
|
1299
1318
|
onValueChange={(value) => {
|
|
1300
1319
|
updateSearch(value);
|
|
1301
1320
|
selectionEnabledRef.current = false;
|
|
1302
|
-
if (initialSelection === "none")
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
: "",
|
|
1307
|
-
);
|
|
1321
|
+
if (initialSelection === "none") {
|
|
1322
|
+
setSelectionEnabled(false);
|
|
1323
|
+
setSelectedCommandValue(nextEmptySelectionValue());
|
|
1324
|
+
}
|
|
1308
1325
|
}}
|
|
1309
1326
|
onKeyDown={(event) => {
|
|
1310
1327
|
if (
|
|
@@ -215,7 +215,7 @@ function ContextSubmenu({
|
|
|
215
215
|
icon?: ReactNode;
|
|
216
216
|
disabled?: boolean;
|
|
217
217
|
open: boolean;
|
|
218
|
-
onOpenChange: (open: boolean) => void;
|
|
218
|
+
onOpenChange: (open: boolean, reason?: "escape") => void;
|
|
219
219
|
children: ReactNode;
|
|
220
220
|
}) {
|
|
221
221
|
const trigger = useRef<HTMLDivElement>(null);
|
|
@@ -244,7 +244,7 @@ function ContextSubmenu({
|
|
|
244
244
|
onEscapeKeyDown={(event) => {
|
|
245
245
|
event.preventDefault();
|
|
246
246
|
event.stopPropagation();
|
|
247
|
-
onOpenChange(false);
|
|
247
|
+
onOpenChange(false, "escape");
|
|
248
248
|
trigger.current?.focus();
|
|
249
249
|
}}
|
|
250
250
|
>
|
|
@@ -312,10 +312,14 @@ export function ComposerContextMenu({
|
|
|
312
312
|
const onDisabledFocusRef = useRef(onDisabledFocus);
|
|
313
313
|
onDisabledFocusRef.current = onDisabledFocus;
|
|
314
314
|
const disabledFocusFrame = useRef<number | null>(null);
|
|
315
|
+
const resumedPageClose = useRef<string | null>(null);
|
|
316
|
+
const resumedPageCloseFrame = useRef<number | null>(null);
|
|
315
317
|
useEffect(
|
|
316
318
|
() => () => {
|
|
317
319
|
if (disabledFocusFrame.current !== null)
|
|
318
320
|
window.cancelAnimationFrame(disabledFocusFrame.current);
|
|
321
|
+
if (resumedPageCloseFrame.current !== null)
|
|
322
|
+
window.cancelAnimationFrame(resumedPageCloseFrame.current);
|
|
319
323
|
},
|
|
320
324
|
[],
|
|
321
325
|
);
|
|
@@ -545,7 +549,19 @@ export function ComposerContextMenu({
|
|
|
545
549
|
label={entry.label}
|
|
546
550
|
disabled={entry.disabled}
|
|
547
551
|
open={expanded}
|
|
548
|
-
onOpenChange={(next) => {
|
|
552
|
+
onOpenChange={(next, reason) => {
|
|
553
|
+
if (
|
|
554
|
+
!next &&
|
|
555
|
+
reason !== "escape" &&
|
|
556
|
+
resumedPageClose.current === entry.id
|
|
557
|
+
) {
|
|
558
|
+
resumedPageClose.current = null;
|
|
559
|
+
if (resumedPageCloseFrame.current !== null) {
|
|
560
|
+
window.cancelAnimationFrame(resumedPageCloseFrame.current);
|
|
561
|
+
resumedPageCloseFrame.current = null;
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
549
565
|
const isOpen = branch.every(
|
|
550
566
|
(id, index) => pathRef.current[index] === id,
|
|
551
567
|
);
|
|
@@ -588,6 +604,19 @@ export function ComposerContextMenu({
|
|
|
588
604
|
);
|
|
589
605
|
return;
|
|
590
606
|
}
|
|
607
|
+
// Radix can emit one stale close after native-picker focus returns.
|
|
608
|
+
resumedPageClose.current = entry.id;
|
|
609
|
+
if (resumedPageCloseFrame.current !== null)
|
|
610
|
+
window.cancelAnimationFrame(
|
|
611
|
+
resumedPageCloseFrame.current,
|
|
612
|
+
);
|
|
613
|
+
resumedPageCloseFrame.current =
|
|
614
|
+
window.requestAnimationFrame(() => {
|
|
615
|
+
resumedPageCloseFrame.current = null;
|
|
616
|
+
if (resumedPageClose.current === entry.id)
|
|
617
|
+
resumedPageClose.current = null;
|
|
618
|
+
});
|
|
619
|
+
triggerRef.current?.focus();
|
|
591
620
|
changeOpen(true);
|
|
592
621
|
activate(action, origin, false);
|
|
593
622
|
},
|