@agent-native/toolkit 0.9.1 → 0.10.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.
Files changed (162) hide show
  1. package/agent-native.eject.json +71 -0
  2. package/dist/chat-history/ChatHistoryRail.d.ts +15 -1
  3. package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
  4. package/dist/chat-history/ChatHistoryRail.js +26 -13
  5. package/dist/chat-history/ChatHistoryRail.js.map +1 -1
  6. package/dist/chat-history/ChatHistoryRail.spec.js +77 -1
  7. package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -1
  8. package/dist/chat-history/index.d.ts +2 -1
  9. package/dist/chat-history/index.d.ts.map +1 -1
  10. package/dist/chat-history/index.js +2 -1
  11. package/dist/chat-history/index.js.map +1 -1
  12. package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
  13. package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
  14. package/dist/chat-history/useChatHistoryRailController.js +36 -0
  15. package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
  16. package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
  17. package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
  18. package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
  19. package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
  20. package/dist/chat-history.css +2 -0
  21. package/dist/composer/ComposerPlusMenu.js +1 -1
  22. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  23. package/dist/composer/PromptComposer.d.ts.map +1 -1
  24. package/dist/composer/PromptComposer.js +22 -4
  25. package/dist/composer/PromptComposer.js.map +1 -1
  26. package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
  27. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  28. package/dist/composer/useRealtimeVoiceMode.js +91 -19
  29. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  30. package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
  31. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  32. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
  33. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
  34. package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
  35. package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
  36. package/dist/conformance/conformance.spec.d.ts +2 -0
  37. package/dist/conformance/conformance.spec.d.ts.map +1 -0
  38. package/dist/conformance/conformance.spec.js +58 -0
  39. package/dist/conformance/conformance.spec.js.map +1 -0
  40. package/dist/conformance/index.d.ts +4 -0
  41. package/dist/conformance/index.d.ts.map +1 -0
  42. package/dist/conformance/index.js +4 -0
  43. package/dist/conformance/index.js.map +1 -0
  44. package/dist/conformance/runner.d.ts +12 -0
  45. package/dist/conformance/runner.d.ts.map +1 -0
  46. package/dist/conformance/runner.js +441 -0
  47. package/dist/conformance/runner.js.map +1 -0
  48. package/dist/conformance/types.d.ts +26 -0
  49. package/dist/conformance/types.d.ts.map +1 -0
  50. package/dist/conformance/types.js +13 -0
  51. package/dist/conformance/types.js.map +1 -0
  52. package/dist/conformance/version.d.ts +6 -0
  53. package/dist/conformance/version.d.ts.map +1 -0
  54. package/dist/conformance/version.js +14 -0
  55. package/dist/conformance/version.js.map +1 -0
  56. package/dist/design-system/components.d.ts +64 -0
  57. package/dist/design-system/components.d.ts.map +1 -0
  58. package/dist/design-system/components.js +50 -0
  59. package/dist/design-system/components.js.map +1 -0
  60. package/dist/design-system/context.d.ts +11 -0
  61. package/dist/design-system/context.d.ts.map +1 -0
  62. package/dist/design-system/context.js +17 -0
  63. package/dist/design-system/context.js.map +1 -0
  64. package/dist/design-system/default-adapter.d.ts +3 -0
  65. package/dist/design-system/default-adapter.d.ts.map +1 -0
  66. package/dist/design-system/default-adapter.js +166 -0
  67. package/dist/design-system/default-adapter.js.map +1 -0
  68. package/dist/design-system/default-adapter.spec.d.ts +2 -0
  69. package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
  70. package/dist/design-system/default-adapter.spec.js +96 -0
  71. package/dist/design-system/default-adapter.spec.js.map +1 -0
  72. package/dist/design-system/definition.d.ts +9 -0
  73. package/dist/design-system/definition.d.ts.map +1 -0
  74. package/dist/design-system/definition.js +4 -0
  75. package/dist/design-system/definition.js.map +1 -0
  76. package/dist/design-system/design-system.spec.d.ts +2 -0
  77. package/dist/design-system/design-system.spec.d.ts.map +1 -0
  78. package/dist/design-system/design-system.spec.js +121 -0
  79. package/dist/design-system/design-system.spec.js.map +1 -0
  80. package/dist/design-system/error-boundary.d.ts +18 -0
  81. package/dist/design-system/error-boundary.d.ts.map +1 -0
  82. package/dist/design-system/error-boundary.js +16 -0
  83. package/dist/design-system/error-boundary.js.map +1 -0
  84. package/dist/design-system/index.d.ts +8 -0
  85. package/dist/design-system/index.d.ts.map +1 -0
  86. package/dist/design-system/index.js +8 -0
  87. package/dist/design-system/index.js.map +1 -0
  88. package/dist/design-system/theme.d.ts +30 -0
  89. package/dist/design-system/theme.d.ts.map +1 -0
  90. package/dist/design-system/theme.js +95 -0
  91. package/dist/design-system/theme.js.map +1 -0
  92. package/dist/design-system/theme.spec.d.ts +2 -0
  93. package/dist/design-system/theme.spec.d.ts.map +1 -0
  94. package/dist/design-system/theme.spec.js +43 -0
  95. package/dist/design-system/theme.spec.js.map +1 -0
  96. package/dist/design-system/types.d.ts +288 -0
  97. package/dist/design-system/types.d.ts.map +1 -0
  98. package/dist/design-system/types.js +2 -0
  99. package/dist/design-system/types.js.map +1 -0
  100. package/dist/provider.d.ts +4 -3
  101. package/dist/provider.d.ts.map +1 -1
  102. package/dist/provider.js +46 -4
  103. package/dist/provider.js.map +1 -1
  104. package/dist/ui/button.d.ts +5 -0
  105. package/dist/ui/button.d.ts.map +1 -1
  106. package/dist/ui/button.js +25 -4
  107. package/dist/ui/button.js.map +1 -1
  108. package/dist/ui/dialog.d.ts +4 -0
  109. package/dist/ui/dialog.d.ts.map +1 -1
  110. package/dist/ui/dialog.js +2 -2
  111. package/dist/ui/dialog.js.map +1 -1
  112. package/dist/ui/popover.d.ts +1 -0
  113. package/dist/ui/popover.d.ts.map +1 -1
  114. package/dist/ui/popover.js +2 -2
  115. package/dist/ui/popover.js.map +1 -1
  116. package/dist/ui/select.d.ts +3 -1
  117. package/dist/ui/select.d.ts.map +1 -1
  118. package/dist/ui/select.js +1 -1
  119. package/dist/ui/select.js.map +1 -1
  120. package/dist/ui/sidebar.d.ts +6 -2
  121. package/dist/ui/sidebar.d.ts.map +1 -1
  122. package/dist/ui/sidebar.js +12 -15
  123. package/dist/ui/sidebar.js.map +1 -1
  124. package/dist/ui/tooltip.d.ts +3 -1
  125. package/dist/ui/tooltip.d.ts.map +1 -1
  126. package/dist/ui/tooltip.js +2 -2
  127. package/dist/ui/tooltip.js.map +1 -1
  128. package/package.json +31 -1
  129. package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
  130. package/src/chat-history/ChatHistoryRail.tsx +100 -39
  131. package/src/chat-history/index.ts +9 -0
  132. package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
  133. package/src/chat-history/useChatHistoryRailController.ts +76 -0
  134. package/src/chat-history.css +2 -0
  135. package/src/composer/ComposerPlusMenu.tsx +11 -8
  136. package/src/composer/PromptComposer.tsx +27 -6
  137. package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
  138. package/src/composer/useRealtimeVoiceMode.tsx +107 -22
  139. package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
  140. package/src/conformance/conformance.spec.tsx +83 -0
  141. package/src/conformance/index.ts +3 -0
  142. package/src/conformance/runner.tsx +710 -0
  143. package/src/conformance/types.ts +45 -0
  144. package/src/conformance/version.ts +22 -0
  145. package/src/design-system/components.tsx +146 -0
  146. package/src/design-system/context.tsx +31 -0
  147. package/src/design-system/default-adapter.spec.tsx +143 -0
  148. package/src/design-system/default-adapter.tsx +915 -0
  149. package/src/design-system/definition.ts +14 -0
  150. package/src/design-system/design-system.spec.tsx +183 -0
  151. package/src/design-system/error-boundary.tsx +37 -0
  152. package/src/design-system/index.ts +7 -0
  153. package/src/design-system/theme.spec.ts +61 -0
  154. package/src/design-system/theme.ts +145 -0
  155. package/src/design-system/types.ts +363 -0
  156. package/src/provider.tsx +98 -5
  157. package/src/ui/button.tsx +95 -6
  158. package/src/ui/dialog.tsx +48 -23
  159. package/src/ui/popover.tsx +16 -2
  160. package/src/ui/select.tsx +7 -3
  161. package/src/ui/sidebar.tsx +47 -48
  162. package/src/ui/tooltip.tsx +7 -3
@@ -1,11 +1,17 @@
1
1
  import { IconDots, IconPlus } from "@tabler/icons-react";
2
- import { useEffect, useMemo, useState } from "react";
2
+ import type { ReactNode } from "react";
3
3
 
4
+ import { ActionButton, IconButton } from "../design-system/components.js";
5
+ import { DesignSystemErrorBoundary } from "../design-system/error-boundary.js";
4
6
  import {
5
7
  ChatHistoryList,
6
8
  type ChatHistoryItem,
7
9
  type ChatHistoryListProps,
8
10
  } from "./ChatHistoryList.js";
11
+ import {
12
+ type ChatHistoryRailController,
13
+ useChatHistoryRailController,
14
+ } from "./useChatHistoryRailController.js";
9
15
 
10
16
  export interface ChatHistoryRailLabels {
11
17
  newChat: string;
@@ -22,63 +28,71 @@ export interface ChatHistoryRailProps extends Omit<
22
28
  railLabels: ChatHistoryRailLabels;
23
29
  previewCount?: number;
24
30
  expandedCount?: number;
31
+ /**
32
+ * Product-level presentation slot for design systems that want to replace
33
+ * the rail wholesale. State and actions still come from the same controller
34
+ * as the default view.
35
+ */
36
+ renderRail?: (context: ChatHistoryRailRenderContext) => ReactNode;
25
37
  }
26
38
 
27
- /**
28
- * Compact recent-chat rail for app sidebars. Hosts own thread persistence,
29
- * sorting, routing, and mutations; the rail only owns progressive disclosure.
30
- */
31
- export function ChatHistoryRail({
32
- items,
33
- onNewChat,
34
- railLabels,
35
- previewCount = 5,
36
- expandedCount = 15,
37
- className,
38
- emptyLabel,
39
- ...listProps
40
- }: ChatHistoryRailProps) {
41
- const [expanded, setExpanded] = useState(false);
42
- const collapsedLimit = Math.max(1, previewCount);
43
- const expandedLimit = Math.max(collapsedLimit, expandedCount);
44
- const canExpand = items.length > collapsedLimit;
45
- const visibleItems = useMemo(
46
- () => items.slice(0, expanded ? expandedLimit : collapsedLimit),
47
- [collapsedLimit, expanded, expandedLimit, items],
48
- );
39
+ export interface ChatHistoryRailRenderContext {
40
+ controller: ChatHistoryRailController<ChatHistoryItem>;
41
+ listProps: Omit<
42
+ ChatHistoryListProps,
43
+ "footer" | "items" | "sections" | "variant"
44
+ >;
45
+ }
49
46
 
50
- useEffect(() => {
51
- if (!canExpand) setExpanded(false);
52
- }, [canExpand]);
47
+ export type DefaultChatHistoryRailViewProps = ChatHistoryRailRenderContext;
48
+
49
+ export function DefaultChatHistoryRailView({
50
+ controller,
51
+ listProps,
52
+ }: ChatHistoryRailRenderContext) {
53
+ const {
54
+ canExpand,
55
+ disclosureLabel,
56
+ expanded,
57
+ newChatLabel,
58
+ onNewChat,
59
+ toggleExpanded,
60
+ visibleItems,
61
+ } = controller;
62
+ const { className, emptyLabel, ...rest } = listProps;
53
63
 
54
64
  const footer = (
55
65
  <div className="an-chat-history-rail__footer">
56
- <button
66
+ <ActionButton
57
67
  type="button"
58
68
  className="an-chat-history-rail__new-chat"
59
- onClick={onNewChat}
69
+ emphasis="ghost"
70
+ size="compact"
71
+ leadingIcon={
72
+ <IconPlus size={13} strokeWidth={1.8} aria-hidden="true" />
73
+ }
74
+ onPress={onNewChat}
60
75
  >
61
- <IconPlus size={13} strokeWidth={1.8} aria-hidden="true" />
62
- <span>{railLabels.newChat}</span>
63
- </button>
76
+ <span>{newChatLabel}</span>
77
+ </ActionButton>
64
78
  {canExpand && (
65
- <button
79
+ <IconButton
66
80
  type="button"
67
81
  className="an-chat-history-rail__disclosure"
68
- onClick={() => setExpanded((current) => !current)}
82
+ size="compact"
83
+ icon={<IconDots size={14} strokeWidth={1.8} aria-hidden="true" />}
84
+ onPress={toggleExpanded}
69
85
  aria-expanded={expanded}
70
- aria-label={expanded ? railLabels.showLess : railLabels.showMore}
71
- title={expanded ? railLabels.showLess : railLabels.showMore}
72
- >
73
- <IconDots size={14} strokeWidth={1.8} aria-hidden="true" />
74
- </button>
86
+ label={disclosureLabel}
87
+ title={disclosureLabel}
88
+ />
75
89
  )}
76
90
  </div>
77
91
  );
78
92
 
79
93
  return (
80
94
  <ChatHistoryList
81
- {...listProps}
95
+ {...rest}
82
96
  items={visibleItems}
83
97
  footer={footer}
84
98
  emptyLabel={emptyLabel ?? null}
@@ -87,3 +101,50 @@ export function ChatHistoryRail({
87
101
  />
88
102
  );
89
103
  }
104
+
105
+ /**
106
+ * Compact recent-chat rail for app sidebars. Hosts own thread persistence,
107
+ * sorting, routing, and mutations; the rail only owns progressive disclosure.
108
+ */
109
+ export function ChatHistoryRail({
110
+ items,
111
+ onNewChat,
112
+ railLabels,
113
+ previewCount = 5,
114
+ expandedCount = 15,
115
+ renderRail,
116
+ className,
117
+ emptyLabel,
118
+ ...listProps
119
+ }: ChatHistoryRailProps) {
120
+ const controller = useChatHistoryRailController({
121
+ items,
122
+ onNewChat,
123
+ labels: railLabels,
124
+ previewCount,
125
+ expandedCount,
126
+ });
127
+ const context: ChatHistoryRailRenderContext = {
128
+ controller,
129
+ listProps: { ...listProps, className, emptyLabel },
130
+ };
131
+ const fallback = <DefaultChatHistoryRailView {...context} />;
132
+
133
+ return renderRail ? (
134
+ <DesignSystemErrorBoundary component="ChatHistoryRail" fallback={fallback}>
135
+ <ChatHistoryRailCustomView renderRail={renderRail} context={context} />
136
+ </DesignSystemErrorBoundary>
137
+ ) : (
138
+ fallback
139
+ );
140
+ }
141
+
142
+ function ChatHistoryRailCustomView({
143
+ renderRail,
144
+ context,
145
+ }: {
146
+ renderRail: NonNullable<ChatHistoryRailProps["renderRail"]>;
147
+ context: ChatHistoryRailRenderContext;
148
+ }) {
149
+ return renderRail(context);
150
+ }
@@ -7,6 +7,15 @@ export {
7
7
  } from "./ChatHistoryList.js";
8
8
  export {
9
9
  ChatHistoryRail,
10
+ DefaultChatHistoryRailView,
11
+ type DefaultChatHistoryRailViewProps,
10
12
  type ChatHistoryRailLabels,
11
13
  type ChatHistoryRailProps,
14
+ type ChatHistoryRailRenderContext,
12
15
  } from "./ChatHistoryRail.js";
16
+ export {
17
+ type ChatHistoryRailController,
18
+ type ChatHistoryRailControllerLabels,
19
+ type UseChatHistoryRailControllerOptions,
20
+ useChatHistoryRailController,
21
+ } from "./useChatHistoryRailController.js";
@@ -0,0 +1,131 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import {
8
+ type ChatHistoryRailController,
9
+ type UseChatHistoryRailControllerOptions,
10
+ useChatHistoryRailController,
11
+ } from "./useChatHistoryRailController.js";
12
+
13
+ interface Item {
14
+ id: string;
15
+ }
16
+
17
+ const labels = {
18
+ newChat: "New chat",
19
+ showMore: "Show more chats",
20
+ showLess: "Show fewer chats",
21
+ };
22
+
23
+ function makeItems(count: number): Item[] {
24
+ return Array.from({ length: count }, (_, index) => ({
25
+ id: `thread-${index + 1}`,
26
+ }));
27
+ }
28
+
29
+ describe("useChatHistoryRailController", () => {
30
+ let container: HTMLDivElement;
31
+ let root: Root;
32
+ let controller: ChatHistoryRailController<Item>;
33
+
34
+ function Harness(props: UseChatHistoryRailControllerOptions<Item>) {
35
+ controller = useChatHistoryRailController(props);
36
+ return null;
37
+ }
38
+
39
+ function render(options: Partial<UseChatHistoryRailControllerOptions<Item>>) {
40
+ const props: UseChatHistoryRailControllerOptions<Item> = {
41
+ items: makeItems(20),
42
+ labels,
43
+ onNewChat: () => {},
44
+ ...options,
45
+ };
46
+ act(() => root.render(<Harness {...props} />));
47
+ }
48
+
49
+ beforeEach(() => {
50
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
51
+ container = document.createElement("div");
52
+ document.body.appendChild(container);
53
+ root = createRoot(container);
54
+ });
55
+
56
+ afterEach(() => {
57
+ act(() => root.unmount());
58
+ container.remove();
59
+ vi.restoreAllMocks();
60
+ vi.unstubAllGlobals();
61
+ });
62
+
63
+ it("progressively discloses the default five and fifteen item limits", () => {
64
+ const items = makeItems(20);
65
+ render({ items });
66
+
67
+ expect(controller.expanded).toBe(false);
68
+ expect(controller.collapsedLimit).toBe(5);
69
+ expect(controller.expandedLimit).toBe(15);
70
+ expect(controller.visibleItems).toEqual(items.slice(0, 5));
71
+ expect(controller.canExpand).toBe(true);
72
+ expect(controller.disclosureLabel).toBe("Show more chats");
73
+
74
+ act(() => controller.toggleExpanded());
75
+
76
+ expect(controller.expanded).toBe(true);
77
+ expect(controller.visibleItems).toEqual(items.slice(0, 15));
78
+ expect(controller.disclosureLabel).toBe("Show fewer chats");
79
+ });
80
+
81
+ it("clamps finite limits while preserving legacy disclosure behavior", () => {
82
+ render({ previewCount: -2.5, expandedCount: 1.9 });
83
+
84
+ expect(controller.collapsedLimit).toBe(1);
85
+ expect(controller.expandedLimit).toBe(1);
86
+ expect(controller.visibleItems).toHaveLength(1);
87
+ expect(controller.canExpand).toBe(true);
88
+
89
+ act(() => controller.toggleExpanded());
90
+ expect(controller.expanded).toBe(true);
91
+ });
92
+
93
+ it("uses defaults for non-finite limits and never puts expanded below preview", () => {
94
+ render({
95
+ previewCount: Number.NaN,
96
+ expandedCount: Number.POSITIVE_INFINITY,
97
+ });
98
+ expect(controller.collapsedLimit).toBe(5);
99
+ expect(controller.expandedLimit).toBe(15);
100
+
101
+ render({ previewCount: 8, expandedCount: 3 });
102
+ expect(controller.collapsedLimit).toBe(8);
103
+ expect(controller.expandedLimit).toBe(8);
104
+ });
105
+
106
+ it("resets disclosure explicitly and when the collection becomes too short", () => {
107
+ render({});
108
+ act(() => controller.toggleExpanded());
109
+ expect(controller.expanded).toBe(true);
110
+
111
+ act(() => controller.resetExpanded());
112
+ expect(controller.expanded).toBe(false);
113
+
114
+ act(() => controller.toggleExpanded());
115
+ expect(controller.expanded).toBe(true);
116
+
117
+ render({ items: makeItems(3) });
118
+ expect(controller.expanded).toBe(false);
119
+ expect(controller.canExpand).toBe(false);
120
+ expect(controller.visibleItems).toHaveLength(3);
121
+ });
122
+
123
+ it("exposes view labels and the host new-chat action without UI assumptions", () => {
124
+ const onNewChat = vi.fn();
125
+ render({ onNewChat });
126
+
127
+ expect(controller.newChatLabel).toBe("New chat");
128
+ act(() => controller.onNewChat());
129
+ expect(onNewChat).toHaveBeenCalledOnce();
130
+ });
131
+ });
@@ -0,0 +1,76 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ export interface ChatHistoryRailControllerLabels {
4
+ newChat: string;
5
+ showMore: string;
6
+ showLess: string;
7
+ }
8
+
9
+ export interface UseChatHistoryRailControllerOptions<Item> {
10
+ items: readonly Item[];
11
+ onNewChat: () => void;
12
+ labels: ChatHistoryRailControllerLabels;
13
+ previewCount?: number;
14
+ expandedCount?: number;
15
+ }
16
+
17
+ export interface ChatHistoryRailController<Item> {
18
+ expanded: boolean;
19
+ collapsedLimit: number;
20
+ expandedLimit: number;
21
+ visibleItems: Item[];
22
+ canExpand: boolean;
23
+ newChatLabel: string;
24
+ disclosureLabel: string;
25
+ onNewChat: () => void;
26
+ toggleExpanded: () => void;
27
+ resetExpanded: () => void;
28
+ }
29
+
30
+ function normalizeLimit(value: number | undefined, fallback: number): number {
31
+ if (value === undefined || !Number.isFinite(value)) return fallback;
32
+ return Math.max(1, Math.floor(value));
33
+ }
34
+
35
+ export function useChatHistoryRailController<Item>({
36
+ items,
37
+ onNewChat,
38
+ labels,
39
+ previewCount,
40
+ expandedCount,
41
+ }: UseChatHistoryRailControllerOptions<Item>): ChatHistoryRailController<Item> {
42
+ const [expandedState, setExpandedState] = useState(false);
43
+ const collapsedLimit = normalizeLimit(previewCount, 5);
44
+ const expandedLimit = Math.max(
45
+ collapsedLimit,
46
+ normalizeLimit(expandedCount, 15),
47
+ );
48
+ const canExpand = items.length > collapsedLimit;
49
+ const expanded = canExpand && expandedState;
50
+ const visibleItems = useMemo(
51
+ () => items.slice(0, expanded ? expandedLimit : collapsedLimit),
52
+ [collapsedLimit, expanded, expandedLimit, items],
53
+ );
54
+
55
+ useEffect(() => {
56
+ if (!canExpand) setExpandedState(false);
57
+ }, [canExpand]);
58
+
59
+ const toggleExpanded = useCallback(() => {
60
+ if (canExpand) setExpandedState((current) => !current);
61
+ }, [canExpand]);
62
+ const resetExpanded = useCallback(() => setExpandedState(false), []);
63
+
64
+ return {
65
+ expanded,
66
+ collapsedLimit,
67
+ expandedLimit,
68
+ visibleItems,
69
+ canExpand,
70
+ newChatLabel: labels.newChat,
71
+ disclosureLabel: expanded ? labels.showLess : labels.showMore,
72
+ onNewChat,
73
+ toggleExpanded,
74
+ resetExpanded,
75
+ };
76
+ }
@@ -113,12 +113,14 @@
113
113
  justify-content: center;
114
114
  flex: 0 0 auto;
115
115
  width: 30px;
116
+ height: auto;
116
117
  min-height: 30px;
117
118
  }
118
119
 
119
120
  .an-chat-history-rail__new-chat {
120
121
  flex: 1 1 auto;
121
122
  gap: 8px;
123
+ height: auto;
122
124
  min-width: 0;
123
125
  min-height: 30px;
124
126
  padding: 6px 10px;
@@ -490,14 +490,17 @@ function ComposerPlusMenuFull({
490
490
  <Popover open={open} onOpenChange={setOpen}>
491
491
  <Tooltip>
492
492
  <TooltipTrigger asChild>
493
- <PopoverTrigger asChild>
494
- <button
495
- type="button"
496
- className="shrink-0 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 disabled:opacity-30 disabled:cursor-not-allowed"
497
- >
498
- <IconPlus className="h-4 w-4" />
499
- </button>
500
- </PopoverTrigger>
493
+ <span className="inline-flex shrink-0">
494
+ <PopoverTrigger asChild>
495
+ <button
496
+ type="button"
497
+ aria-label="Add..."
498
+ className="shrink-0 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 disabled:opacity-30 disabled:cursor-not-allowed"
499
+ >
500
+ <IconPlus className="h-4 w-4" />
501
+ </button>
502
+ </PopoverTrigger>
503
+ </span>
501
504
  </TooltipTrigger>
502
505
  <TooltipContent>Add...</TooltipContent>
503
506
  </Tooltip>
@@ -519,6 +519,7 @@ function PromptComposerInner({
519
519
  resolvedModelStatusChecksEnabled,
520
520
  );
521
521
  const missingApiKey = agentEngineConfigured.missing;
522
+ const agentEngineUnavailable = agentEngineConfigured.state === "unavailable";
522
523
  const [missingKeyBouncePulse, setMissingKeyBouncePulse] = useState(0);
523
524
  const bounceMissingKeySetup = useCallback(() => {
524
525
  setMissingKeyBouncePulse((pulse) => pulse + 1);
@@ -542,8 +543,13 @@ function PromptComposerInner({
542
543
  timeoutMs: SUBMIT_ENGINE_STATUS_TIMEOUT_MS,
543
544
  },
544
545
  );
545
- if (state !== "missing") return true;
546
- bounceMissingKeySetup();
546
+ if (state === "missing") {
547
+ bounceMissingKeySetup();
548
+ return false;
549
+ }
550
+ if (typeof window !== "undefined") {
551
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
552
+ }
547
553
  return false;
548
554
  }, [
549
555
  agentEngineConfigured.state,
@@ -551,6 +557,11 @@ function PromptComposerInner({
551
557
  modelsAdapter,
552
558
  resolvedModelStatusChecksEnabled,
553
559
  ]);
560
+ const retryAgentEngineStatus = useCallback(() => {
561
+ if (typeof window !== "undefined") {
562
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
563
+ }
564
+ }, []);
554
565
 
555
566
  useEffect(() => {
556
567
  if (!autoFocus) return;
@@ -613,21 +624,31 @@ function PromptComposerInner({
613
624
  <AgentComposerFrame
614
625
  className={cn(
615
626
  "text-start",
616
- missingApiKey && "cursor-pointer",
627
+ (missingApiKey || agentEngineUnavailable) && "cursor-pointer",
617
628
  className,
618
629
  )}
619
630
  rootClassName={rootClassName}
620
631
  style={style}
621
632
  rootStyle={rootStyle}
622
633
  layoutVariant={layoutVariant}
623
- onClick={missingApiKey ? bounceMissingKeySetup : undefined}
634
+ onClick={
635
+ missingApiKey
636
+ ? bounceMissingKeySetup
637
+ : agentEngineUnavailable
638
+ ? retryAgentEngineStatus
639
+ : undefined
640
+ }
624
641
  >
625
642
  <PromptAttachmentStrip />
626
643
  <TiptapComposer
627
644
  focusRef={handleRef}
628
- disabled={disabled || missingApiKey}
645
+ disabled={disabled || missingApiKey || agentEngineUnavailable}
629
646
  placeholder={
630
- missingApiKey ? "Connect AI above to continue..." : placeholder
647
+ missingApiKey
648
+ ? "Connect AI above to continue..."
649
+ : agentEngineUnavailable
650
+ ? "Unable to check AI connection. Click to retry."
651
+ : placeholder
631
652
  }
632
653
  initialText={initialText}
633
654
  initialTextKey={initialTextKey}
@@ -6,6 +6,7 @@ import {
6
6
  createRealtimeVoiceGreetingEvent,
7
7
  createRealtimeVoiceGreetingStarter,
8
8
  createRealtimeVoicePreferenceUpdate,
9
+ createRealtimeVoiceResponseCoordinator,
9
10
  createRealtimeVoiceSession,
10
11
  createRealtimeVoiceSessionWithCapability,
11
12
  createRealtimeVoiceTranscriptSequencer,
@@ -521,7 +522,7 @@ describe("Realtime voice dynamic tool manifests", () => {
521
522
  });
522
523
 
523
524
  describe("extractRealtimeVoiceFunctionCalls", () => {
524
- it("uses the low-latency completed-arguments event", () => {
525
+ it("waits for the terminal response event before returning a function call", () => {
525
526
  expect(
526
527
  extractRealtimeVoiceFunctionCalls({
527
528
  type: "response.function_call_arguments.done",
@@ -529,13 +530,7 @@ describe("extractRealtimeVoiceFunctionCalls", () => {
529
530
  call_id: "call-1",
530
531
  arguments: '{"path":"/inbox"}',
531
532
  }),
532
- ).toEqual([
533
- {
534
- name: "navigate",
535
- callId: "call-1",
536
- argumentsText: '{"path":"/inbox"}',
537
- },
538
- ]);
533
+ ).toEqual([]);
539
534
  });
540
535
 
541
536
  it("falls back to completed function items on response.done", () => {
@@ -543,6 +538,7 @@ describe("extractRealtimeVoiceFunctionCalls", () => {
543
538
  extractRealtimeVoiceFunctionCalls({
544
539
  type: "response.done",
545
540
  response: {
541
+ status: "completed",
546
542
  output: [
547
543
  { type: "message", role: "assistant" },
548
544
  {
@@ -562,6 +558,29 @@ describe("extractRealtimeVoiceFunctionCalls", () => {
562
558
  },
563
559
  ]);
564
560
  });
561
+
562
+ it("ignores function items from failed or cancelled responses", () => {
563
+ const output = [
564
+ {
565
+ type: "function_call",
566
+ name: "navigate",
567
+ call_id: "call-3",
568
+ arguments: "{}",
569
+ },
570
+ ];
571
+ expect(
572
+ extractRealtimeVoiceFunctionCalls({
573
+ type: "response.done",
574
+ response: { status: "failed", output },
575
+ }),
576
+ ).toEqual([]);
577
+ expect(
578
+ extractRealtimeVoiceFunctionCalls({
579
+ type: "response.done",
580
+ response: { status: "cancelled", output },
581
+ }),
582
+ ).toEqual([]);
583
+ });
565
584
  });
566
585
 
567
586
  describe("extractCompletedRealtimeVoiceTranscript", () => {
@@ -632,6 +651,61 @@ describe("Realtime voice startup and transcript ordering", () => {
632
651
  expect(send).toHaveBeenCalledTimes(2);
633
652
  });
634
653
 
654
+ it("queues response requests behind the active response and retries a rejected request", () => {
655
+ const sent: Record<string, unknown>[] = [];
656
+ const coordinator = createRealtimeVoiceResponseCoordinator((event) =>
657
+ sent.push(event),
658
+ );
659
+
660
+ coordinator.request();
661
+ coordinator.handleEvent({
662
+ type: "response.created",
663
+ response: { id: "resp-active" },
664
+ });
665
+ coordinator.request();
666
+ expect(sent).toHaveLength(1);
667
+
668
+ expect(
669
+ coordinator.handleError(
670
+ "Conversation already has an active response in progress: resp-active",
671
+ ),
672
+ ).toBe(true);
673
+ coordinator.handleEvent({
674
+ type: "response.done",
675
+ response: { id: "resp-active", status: "completed" },
676
+ });
677
+
678
+ expect(sent).toHaveLength(2);
679
+ expect(sent[1]).toEqual({ type: "response.create" });
680
+ });
681
+
682
+ it("drops queued work after a failed response so the next turn can start", () => {
683
+ const sent: Record<string, unknown>[] = [];
684
+ const coordinator = createRealtimeVoiceResponseCoordinator((event) =>
685
+ sent.push(event),
686
+ );
687
+
688
+ coordinator.request();
689
+ coordinator.handleEvent({ type: "response.created" });
690
+ coordinator.request();
691
+ coordinator.handleEvent({
692
+ type: "response.done",
693
+ response: { status: "failed" },
694
+ });
695
+
696
+ expect(sent).toHaveLength(1);
697
+ coordinator.request();
698
+ expect(sent).toHaveLength(2);
699
+ });
700
+
701
+ it("ignores unrelated realtime errors", () => {
702
+ const coordinator = createRealtimeVoiceResponseCoordinator(vi.fn());
703
+
704
+ expect(coordinator.handleError("The microphone was disconnected.")).toBe(
705
+ false,
706
+ );
707
+ });
708
+
635
709
  it("publishes in conversation order when user ASR finishes after the assistant", () => {
636
710
  const published: Array<{ role: string; text: string }> = [];
637
711
  const sequencer = createRealtimeVoiceTranscriptSequencer((transcript) => {