@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.
Files changed (59) hide show
  1. package/dist/app/agentkit/react/styles.css +13 -6
  2. package/dist/app/chat/AgentKitAssistantChat.d.ts +2 -0
  3. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  4. package/dist/app/chat/AgentKitAssistantChat.js +11 -3
  5. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  6. package/dist/app/chat/AgentPanel.js +6 -6
  7. package/dist/app/chat/AgentPanel.js.map +1 -1
  8. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  9. package/dist/app/chat/MultiTabAssistantChat.js +5 -3
  10. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  11. package/dist/app/chat/agentkit-chat/action-widget.d.ts.map +1 -1
  12. package/dist/app/chat/agentkit-chat/action-widget.js +1 -0
  13. package/dist/app/chat/agentkit-chat/action-widget.js.map +1 -1
  14. package/dist/app/chat/chat/surface-types.d.ts +2 -0
  15. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  16. package/dist/app/chat/chat/surface-types.js.map +1 -1
  17. package/dist/app/chat/chat/tool-call-display.d.ts +3 -1
  18. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  19. package/dist/app/chat/chat/tool-call-display.js +5 -4
  20. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  21. package/dist/app/chat/chat/tool-render-registry.d.ts +2 -0
  22. package/dist/app/chat/chat/tool-render-registry.d.ts.map +1 -1
  23. package/dist/app/chat/chat/tool-render-registry.js.map +1 -1
  24. package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
  25. package/dist/app/chat/conversation/AgentConversation.js +6 -1
  26. package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
  27. package/dist/app/shared/CommandMenu.d.ts +2 -0
  28. package/dist/app/shared/CommandMenu.d.ts.map +1 -1
  29. package/dist/app/shared/CommandMenu.js +11 -6
  30. package/dist/app/shared/CommandMenu.js.map +1 -1
  31. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  32. package/dist/composer/ComposerContextMenu.js +27 -2
  33. package/dist/composer/ComposerContextMenu.js.map +1 -1
  34. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  35. package/dist/composer/ComposerPlusMenu.js +50 -4
  36. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  37. package/dist/composer/TiptapComposer.js +1 -1
  38. package/dist/composer/TiptapComposer.js.map +1 -1
  39. package/package.json +4 -4
  40. package/src/app/agentkit/react/components.spec.tsx +7 -5
  41. package/src/app/agentkit/react/styles.css +13 -6
  42. package/src/app/chat/AgentKitAssistantChat.spec.tsx +258 -27
  43. package/src/app/chat/AgentKitAssistantChat.tsx +15 -2
  44. package/src/app/chat/AgentPanel.tsx +8 -8
  45. package/src/app/chat/MultiTabAssistantChat.spec.tsx +17 -0
  46. package/src/app/chat/MultiTabAssistantChat.tsx +7 -2
  47. package/src/app/chat/agentkit-chat/action-widget.spec.tsx +62 -0
  48. package/src/app/chat/agentkit-chat/action-widget.tsx +1 -0
  49. package/src/app/chat/chat/surface-types.ts +2 -0
  50. package/src/app/chat/chat/tool-call-display.tsx +10 -1
  51. package/src/app/chat/chat/tool-render-registry.tsx +2 -0
  52. package/src/app/chat/conversation/AgentConversation.spec.tsx +51 -1
  53. package/src/app/chat/conversation/AgentConversation.tsx +11 -2
  54. package/src/app/shared/CommandMenu.spec.tsx +175 -6
  55. package/src/app/shared/CommandMenu.tsx +24 -7
  56. package/src/composer/ComposerContextMenu.tsx +32 -3
  57. package/src/composer/ComposerPlusMenu.parity.spec.tsx +96 -2
  58. package/src/composer/ComposerPlusMenu.tsx +48 -4
  59. 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-0 min-w-[56px] max-w-[130px]",
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-0 items-center rounded-md text-[10px] font-medium min-w-[48px] max-w-[130px]",
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",
@@ -94,6 +94,7 @@ export function AgentKitActionWidget({
94
94
  resultJson,
95
95
  ...(relatedResults.length > 1 ? { relatedResults } : {}),
96
96
  widgetId: widget.id,
97
+ status: tool.status,
97
98
  isRunning: tool.status === "running",
98
99
  dataChartLayout,
99
100
  chatUI: {
@@ -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 { resolveToolRenderer } from "./tool-render-registry.js";
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">{context.resultText}</div>
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 { resolveToolRenderer } from "../chat/tool-render-registry.js";
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("@agent-native/core/client/use-agent-engine-configured", () => ({
13
- useAgentEngineConfigured: () => ({
14
- canChat: agentEngineStatusMock.state === "configured",
15
- missing: agentEngineStatusMock.state === "missing",
16
- state: agentEngineStatusMock.state,
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 === (commandMenuValue ?? selectedCommandValue),
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") setSelectionEnabled(false);
1303
- setSelectedCommandValue(
1304
- initialSelection === "none"
1305
- ? nextEmptySelectionValue()
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
  },