@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.
- package/agent-native.eject.json +71 -0
- package/dist/chat-history/ChatHistoryRail.d.ts +15 -1
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +26 -13
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.spec.js +77 -1
- package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -1
- package/dist/chat-history/index.d.ts +2 -1
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +2 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.js +36 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
- package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
- package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
- package/dist/chat-history.css +2 -0
- package/dist/composer/ComposerPlusMenu.js +1 -1
- package/dist/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +22 -4
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +91 -19
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
- package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
- package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
- package/dist/conformance/conformance.spec.d.ts +2 -0
- package/dist/conformance/conformance.spec.d.ts.map +1 -0
- package/dist/conformance/conformance.spec.js +58 -0
- package/dist/conformance/conformance.spec.js.map +1 -0
- package/dist/conformance/index.d.ts +4 -0
- package/dist/conformance/index.d.ts.map +1 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/index.js.map +1 -0
- package/dist/conformance/runner.d.ts +12 -0
- package/dist/conformance/runner.d.ts.map +1 -0
- package/dist/conformance/runner.js +441 -0
- package/dist/conformance/runner.js.map +1 -0
- package/dist/conformance/types.d.ts +26 -0
- package/dist/conformance/types.d.ts.map +1 -0
- package/dist/conformance/types.js +13 -0
- package/dist/conformance/types.js.map +1 -0
- package/dist/conformance/version.d.ts +6 -0
- package/dist/conformance/version.d.ts.map +1 -0
- package/dist/conformance/version.js +14 -0
- package/dist/conformance/version.js.map +1 -0
- package/dist/design-system/components.d.ts +64 -0
- package/dist/design-system/components.d.ts.map +1 -0
- package/dist/design-system/components.js +50 -0
- package/dist/design-system/components.js.map +1 -0
- package/dist/design-system/context.d.ts +11 -0
- package/dist/design-system/context.d.ts.map +1 -0
- package/dist/design-system/context.js +17 -0
- package/dist/design-system/context.js.map +1 -0
- package/dist/design-system/default-adapter.d.ts +3 -0
- package/dist/design-system/default-adapter.d.ts.map +1 -0
- package/dist/design-system/default-adapter.js +166 -0
- package/dist/design-system/default-adapter.js.map +1 -0
- package/dist/design-system/default-adapter.spec.d.ts +2 -0
- package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
- package/dist/design-system/default-adapter.spec.js +96 -0
- package/dist/design-system/default-adapter.spec.js.map +1 -0
- package/dist/design-system/definition.d.ts +9 -0
- package/dist/design-system/definition.d.ts.map +1 -0
- package/dist/design-system/definition.js +4 -0
- package/dist/design-system/definition.js.map +1 -0
- package/dist/design-system/design-system.spec.d.ts +2 -0
- package/dist/design-system/design-system.spec.d.ts.map +1 -0
- package/dist/design-system/design-system.spec.js +121 -0
- package/dist/design-system/design-system.spec.js.map +1 -0
- package/dist/design-system/error-boundary.d.ts +18 -0
- package/dist/design-system/error-boundary.d.ts.map +1 -0
- package/dist/design-system/error-boundary.js +16 -0
- package/dist/design-system/error-boundary.js.map +1 -0
- package/dist/design-system/index.d.ts +8 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +8 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/theme.d.ts +30 -0
- package/dist/design-system/theme.d.ts.map +1 -0
- package/dist/design-system/theme.js +95 -0
- package/dist/design-system/theme.js.map +1 -0
- package/dist/design-system/theme.spec.d.ts +2 -0
- package/dist/design-system/theme.spec.d.ts.map +1 -0
- package/dist/design-system/theme.spec.js +43 -0
- package/dist/design-system/theme.spec.js.map +1 -0
- package/dist/design-system/types.d.ts +288 -0
- package/dist/design-system/types.d.ts.map +1 -0
- package/dist/design-system/types.js +2 -0
- package/dist/design-system/types.js.map +1 -0
- package/dist/provider.d.ts +4 -3
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +46 -4
- package/dist/provider.js.map +1 -1
- package/dist/ui/button.d.ts +5 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +25 -4
- package/dist/ui/button.js.map +1 -1
- package/dist/ui/dialog.d.ts +4 -0
- package/dist/ui/dialog.d.ts.map +1 -1
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/dialog.js.map +1 -1
- package/dist/ui/popover.d.ts +1 -0
- package/dist/ui/popover.d.ts.map +1 -1
- package/dist/ui/popover.js +2 -2
- package/dist/ui/popover.js.map +1 -1
- package/dist/ui/select.d.ts +3 -1
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +1 -1
- package/dist/ui/select.js.map +1 -1
- package/dist/ui/sidebar.d.ts +6 -2
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +12 -15
- package/dist/ui/sidebar.js.map +1 -1
- package/dist/ui/tooltip.d.ts +3 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +2 -2
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +31 -1
- package/src/chat-history/ChatHistoryRail.spec.tsx +174 -0
- package/src/chat-history/ChatHistoryRail.tsx +100 -39
- package/src/chat-history/index.ts +9 -0
- package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
- package/src/chat-history/useChatHistoryRailController.ts +76 -0
- package/src/chat-history.css +2 -0
- package/src/composer/ComposerPlusMenu.tsx +11 -8
- package/src/composer/PromptComposer.tsx +27 -6
- package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
- package/src/composer/useRealtimeVoiceMode.tsx +107 -22
- package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
- package/src/conformance/conformance.spec.tsx +83 -0
- package/src/conformance/index.ts +3 -0
- package/src/conformance/runner.tsx +710 -0
- package/src/conformance/types.ts +45 -0
- package/src/conformance/version.ts +22 -0
- package/src/design-system/components.tsx +146 -0
- package/src/design-system/context.tsx +31 -0
- package/src/design-system/default-adapter.spec.tsx +143 -0
- package/src/design-system/default-adapter.tsx +915 -0
- package/src/design-system/definition.ts +14 -0
- package/src/design-system/design-system.spec.tsx +183 -0
- package/src/design-system/error-boundary.tsx +37 -0
- package/src/design-system/index.ts +7 -0
- package/src/design-system/theme.spec.ts +61 -0
- package/src/design-system/theme.ts +145 -0
- package/src/design-system/types.ts +363 -0
- package/src/provider.tsx +98 -5
- package/src/ui/button.tsx +95 -6
- package/src/ui/dialog.tsx +48 -23
- package/src/ui/popover.tsx +16 -2
- package/src/ui/select.tsx +7 -3
- package/src/ui/sidebar.tsx +47 -48
- package/src/ui/tooltip.tsx +7 -3
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { IconDots, IconPlus } from "@tabler/icons-react";
|
|
2
|
-
import {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
<
|
|
66
|
+
<ActionButton
|
|
57
67
|
type="button"
|
|
58
68
|
className="an-chat-history-rail__new-chat"
|
|
59
|
-
|
|
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
|
-
<
|
|
62
|
-
|
|
63
|
-
</button>
|
|
76
|
+
<span>{newChatLabel}</span>
|
|
77
|
+
</ActionButton>
|
|
64
78
|
{canExpand && (
|
|
65
|
-
<
|
|
79
|
+
<IconButton
|
|
66
80
|
type="button"
|
|
67
81
|
className="an-chat-history-rail__disclosure"
|
|
68
|
-
|
|
82
|
+
size="compact"
|
|
83
|
+
icon={<IconDots size={14} strokeWidth={1.8} aria-hidden="true" />}
|
|
84
|
+
onPress={toggleExpanded}
|
|
69
85
|
aria-expanded={expanded}
|
|
70
|
-
|
|
71
|
-
title={
|
|
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
|
-
{...
|
|
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
|
+
}
|
package/src/chat-history.css
CHANGED
|
@@ -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
|
-
<
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
|
546
|
-
|
|
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={
|
|
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
|
|
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("
|
|
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) => {
|