@agent-native/toolkit 0.209.1 → 0.209.2-nightly-20261016005836
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 +3 -0
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +8 -3
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +2 -0
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.d.ts.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.js +11 -2
- package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
- package/dist/app/chat/agent-chat-presence.d.ts +3 -0
- package/dist/app/chat/agent-chat-presence.d.ts.map +1 -0
- package/dist/app/chat/agent-chat-presence.js +24 -0
- package/dist/app/chat/agent-chat-presence.js.map +1 -0
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +15 -3
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/feedback/ReportProblemButton.d.ts +6 -0
- package/dist/app/feedback/ReportProblemButton.d.ts.map +1 -0
- package/dist/app/feedback/ReportProblemButton.js +17 -0
- package/dist/app/feedback/ReportProblemButton.js.map +1 -0
- package/dist/app/feedback/ReportProblemDialog.d.ts +6 -0
- package/dist/app/feedback/ReportProblemDialog.d.ts.map +1 -0
- package/dist/app/feedback/ReportProblemDialog.js +90 -0
- package/dist/app/feedback/ReportProblemDialog.js.map +1 -0
- package/dist/app/feedback/index.d.ts +1 -0
- package/dist/app/feedback/index.d.ts.map +1 -1
- package/dist/app/feedback/index.js +1 -0
- package/dist/app/feedback/index.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +13 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +9 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +9 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +9 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +10 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +10 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +9 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +8 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +8 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +10 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +8 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +8 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/settings/BuilderConnectPopover.d.ts.map +1 -1
- package/dist/app/settings/BuilderConnectPopover.js +13 -7
- package/dist/app/settings/BuilderConnectPopover.js.map +1 -1
- package/dist/app/shared/AppSidebar.d.ts.map +1 -1
- package/dist/app/shared/AppSidebar.js +11 -3
- package/dist/app/shared/AppSidebar.js.map +1 -1
- package/dist/app/shared/CommandMenu.d.ts.map +1 -1
- package/dist/app/shared/CommandMenu.js +21 -4
- package/dist/app/shared/CommandMenu.js.map +1 -1
- package/dist/design-tweaks/scrub-input.d.ts +6 -0
- package/dist/design-tweaks/scrub-input.d.ts.map +1 -1
- package/dist/design-tweaks/scrub-input.js +12 -0
- package/dist/design-tweaks/scrub-input.js.map +1 -1
- package/dist/design-tweaks/visual-style-controls.d.ts +1 -1
- package/dist/design-tweaks/visual-style-controls.d.ts.map +1 -1
- package/dist/design-tweaks/visual-style-controls.js +1 -1
- package/dist/design-tweaks/visual-style-controls.js.map +1 -1
- package/package.json +4 -4
- package/src/app/chat/AgentPanel.header.spec.ts +1 -1
- package/src/app/chat/AgentPanel.tsx +23 -0
- package/src/app/chat/AgentSidebar.tsx +2 -0
- package/src/app/chat/FileStorageSetupPopover.spec.tsx +66 -5
- package/src/app/chat/FileStorageSetupPopover.tsx +15 -6
- package/src/app/chat/agent-chat-presence.ts +30 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +60 -0
- package/src/app/chat/chat/run-recovery.tsx +28 -2
- package/src/app/feedback/ReportProblemButton.tsx +59 -0
- package/src/app/feedback/ReportProblemDialog.spec.tsx +190 -0
- package/src/app/feedback/ReportProblemDialog.tsx +212 -0
- package/src/app/feedback/index.ts +4 -0
- package/src/app/i18n/catalogs/ar-SA.ts +20 -0
- package/src/app/i18n/catalogs/de-DE.ts +12 -0
- package/src/app/i18n/catalogs/en-US.ts +12 -0
- package/src/app/i18n/catalogs/es-ES.ts +14 -0
- package/src/app/i18n/catalogs/fr-FR.ts +14 -0
- package/src/app/i18n/catalogs/hi-IN.ts +12 -0
- package/src/app/i18n/catalogs/ja-JP.ts +10 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +14 -0
- package/src/app/i18n/catalogs/zh-CN.ts +9 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/settings/BuilderConnectPopover.spec.tsx +19 -14
- package/src/app/settings/BuilderConnectPopover.tsx +80 -64
- package/src/app/shared/AppSidebar.spec.tsx +22 -1
- package/src/app/shared/AppSidebar.tsx +24 -3
- package/src/app/shared/CommandMenu.tsx +41 -1
- package/src/design-tweaks/scrub-input.tsx +19 -0
- package/src/design-tweaks/visual-style-controls.spec.tsx +31 -0
- package/src/design-tweaks/visual-style-controls.tsx +2 -0
|
@@ -10,6 +10,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
10
10
|
|
|
11
11
|
import { FileStorageSetupPopover } from "./FileStorageSetupPopover.js";
|
|
12
12
|
|
|
13
|
+
const setupMocks = vi.hoisted(() => ({
|
|
14
|
+
connectFlow: null as null | {
|
|
15
|
+
connecting: boolean;
|
|
16
|
+
start: (options: { provisionAccount: boolean }) => void;
|
|
17
|
+
cancel?: () => void;
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
13
21
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
14
22
|
useT: () => (key: string) =>
|
|
15
23
|
(
|
|
@@ -19,6 +27,7 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
19
27
|
"Use Builder.io (free) or configure your own S3-compatible object storage.",
|
|
20
28
|
"onboarding.fileStorage.custom": "Use custom keys",
|
|
21
29
|
"composer.connectBuilder": "Connect Builder.io",
|
|
30
|
+
"common.cancel": "Cancel",
|
|
22
31
|
}) as Record<string, string>
|
|
23
32
|
)[key] ?? key,
|
|
24
33
|
}));
|
|
@@ -56,9 +65,20 @@ vi.mock("@tabler/icons-react", () => ({ IconCloudUpload: () => null }));
|
|
|
56
65
|
vi.mock("../settings/deferred-builder-connect-popover.js", () => ({
|
|
57
66
|
DeferredBuilderConnectPopover: ({
|
|
58
67
|
children,
|
|
68
|
+
flow,
|
|
59
69
|
}: {
|
|
60
70
|
children: React.ReactNode;
|
|
61
|
-
|
|
71
|
+
flow: { cancel?: () => void; connecting: boolean };
|
|
72
|
+
}) => (
|
|
73
|
+
<div>
|
|
74
|
+
{children}
|
|
75
|
+
{flow.connecting ? (
|
|
76
|
+
<button type="button" onClick={flow.cancel}>
|
|
77
|
+
Cancel
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
),
|
|
62
82
|
}));
|
|
63
83
|
|
|
64
84
|
vi.mock("../setup-connections/BuilderConnectCard.js", () => ({
|
|
@@ -66,11 +86,19 @@ vi.mock("../setup-connections/BuilderConnectCard.js", () => ({
|
|
|
66
86
|
render,
|
|
67
87
|
}: {
|
|
68
88
|
render: (context: {
|
|
69
|
-
viewModel: {
|
|
89
|
+
viewModel: {
|
|
90
|
+
connectFlow: typeof setupMocks.connectFlow;
|
|
91
|
+
pending: false;
|
|
92
|
+
error: null;
|
|
93
|
+
};
|
|
70
94
|
}) => React.ReactNode;
|
|
71
95
|
}) =>
|
|
72
96
|
render({
|
|
73
|
-
viewModel: {
|
|
97
|
+
viewModel: {
|
|
98
|
+
connectFlow: setupMocks.connectFlow,
|
|
99
|
+
pending: false,
|
|
100
|
+
error: null,
|
|
101
|
+
},
|
|
74
102
|
}),
|
|
75
103
|
}));
|
|
76
104
|
|
|
@@ -80,6 +108,7 @@ describe("FileStorageSetupPopover", () => {
|
|
|
80
108
|
|
|
81
109
|
beforeEach(() => {
|
|
82
110
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
111
|
+
setupMocks.connectFlow = null;
|
|
83
112
|
container = document.createElement("div");
|
|
84
113
|
document.body.appendChild(container);
|
|
85
114
|
root = createRoot(container);
|
|
@@ -122,8 +151,40 @@ describe("FileStorageSetupPopover", () => {
|
|
|
122
151
|
expect(container.querySelector("h2")?.parentElement?.className).toBe(
|
|
123
152
|
"grid gap-1",
|
|
124
153
|
);
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
const choices = container.querySelector("button")?.parentElement;
|
|
155
|
+
expect(choices?.className).toBe("grid grid-cols-2 gap-2");
|
|
156
|
+
expect(choices?.querySelectorAll("button")).toHaveLength(2);
|
|
157
|
+
for (const button of choices?.querySelectorAll("button") ?? []) {
|
|
158
|
+
expect(button.className).toContain("whitespace-normal");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("closes the storage popover immediately when Builder setup is canceled", () => {
|
|
163
|
+
const cancel = vi.fn();
|
|
164
|
+
setupMocks.connectFlow = {
|
|
165
|
+
connecting: true,
|
|
166
|
+
start: vi.fn(),
|
|
167
|
+
cancel,
|
|
168
|
+
};
|
|
169
|
+
const onOpenChange = vi.fn();
|
|
170
|
+
|
|
171
|
+
act(() => {
|
|
172
|
+
root.render(
|
|
173
|
+
<FileStorageSetupPopover
|
|
174
|
+
open
|
|
175
|
+
onOpenChange={onOpenChange}
|
|
176
|
+
status="missing"
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const cancelButton = [...container.querySelectorAll("button")].find(
|
|
182
|
+
(button) => button.textContent === "Cancel",
|
|
127
183
|
);
|
|
184
|
+
expect(cancelButton).toBeDefined();
|
|
185
|
+
act(() => cancelButton?.click());
|
|
186
|
+
|
|
187
|
+
expect(cancel).toHaveBeenCalledOnce();
|
|
188
|
+
expect(onOpenChange).toHaveBeenCalledExactlyOnceWith(false, "dismiss");
|
|
128
189
|
});
|
|
129
190
|
});
|
|
@@ -127,11 +127,20 @@ export function FileStorageSetupPopover(props: FileStorageSetupPopoverProps) {
|
|
|
127
127
|
onConnected={onConnected}
|
|
128
128
|
render={({ viewModel }) => {
|
|
129
129
|
const flow = viewModel.connectFlow;
|
|
130
|
+
const connectFlow = flow
|
|
131
|
+
? {
|
|
132
|
+
...flow,
|
|
133
|
+
cancel: () => {
|
|
134
|
+
flow.cancel?.();
|
|
135
|
+
onOpenChange(false, "dismiss");
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: null;
|
|
130
139
|
const connectButton = (
|
|
131
140
|
<Button
|
|
132
141
|
type="button"
|
|
133
142
|
size="sm"
|
|
134
|
-
className="min-w-0
|
|
143
|
+
className="h-auto min-h-9 w-full min-w-0 break-words whitespace-normal px-2 py-2 text-center text-xs leading-4"
|
|
135
144
|
disabled={!flow || viewModel.pending}
|
|
136
145
|
aria-busy={viewModel.pending}
|
|
137
146
|
>
|
|
@@ -147,12 +156,12 @@ export function FileStorageSetupPopover(props: FileStorageSetupPopoverProps) {
|
|
|
147
156
|
{description}
|
|
148
157
|
</p>
|
|
149
158
|
</div>
|
|
150
|
-
<div className="
|
|
151
|
-
{
|
|
159
|
+
<div className="grid grid-cols-2 gap-2">
|
|
160
|
+
{connectFlow ? (
|
|
152
161
|
<BuilderConnectPopover
|
|
153
|
-
flow={
|
|
162
|
+
flow={connectFlow}
|
|
154
163
|
onConnect={(provisionAccount) =>
|
|
155
|
-
|
|
164
|
+
connectFlow.start({ provisionAccount })
|
|
156
165
|
}
|
|
157
166
|
>
|
|
158
167
|
{connectButton}
|
|
@@ -164,7 +173,7 @@ export function FileStorageSetupPopover(props: FileStorageSetupPopoverProps) {
|
|
|
164
173
|
type="button"
|
|
165
174
|
size="sm"
|
|
166
175
|
variant="outline"
|
|
167
|
-
className="min-w-0
|
|
176
|
+
className="h-auto min-h-9 w-full min-w-0 break-words whitespace-normal px-2 py-2 text-center text-xs leading-4"
|
|
168
177
|
onClick={() => {
|
|
169
178
|
onOpenChange(false, "setup");
|
|
170
179
|
if (typeof window !== "undefined") {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
let mountedSurfaces = 0;
|
|
4
|
+
const listeners = new Set<() => void>();
|
|
5
|
+
|
|
6
|
+
function subscribe(listener: () => void): () => void {
|
|
7
|
+
listeners.add(listener);
|
|
8
|
+
return () => listeners.delete(listener);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setMountedSurfaces(next: number): void {
|
|
12
|
+
mountedSurfaces = next;
|
|
13
|
+
for (const listener of listeners) listener();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useRegisterAgentChatSurface(enabled: boolean): void {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!enabled) return;
|
|
19
|
+
setMountedSurfaces(mountedSurfaces + 1);
|
|
20
|
+
return () => setMountedSurfaces(mountedSurfaces - 1);
|
|
21
|
+
}, [enabled]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useHasAgentChat(): boolean {
|
|
25
|
+
return useSyncExternalStore(
|
|
26
|
+
subscribe,
|
|
27
|
+
() => mountedSurfaces > 0,
|
|
28
|
+
() => false,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -700,6 +700,66 @@ describe("run recovery surfaces", () => {
|
|
|
700
700
|
expect(newChatButton?.textContent).toBe("");
|
|
701
701
|
});
|
|
702
702
|
|
|
703
|
+
it.each([
|
|
704
|
+
"http_429",
|
|
705
|
+
"http_529",
|
|
706
|
+
"provider_rate_limited",
|
|
707
|
+
"provider_transient_rejection",
|
|
708
|
+
])("shows a visible Retry action for %s", async (errorCode) => {
|
|
709
|
+
const onRetry = vi.fn();
|
|
710
|
+
|
|
711
|
+
await act(async () => {
|
|
712
|
+
root.render(
|
|
713
|
+
<AgentNativeI18nProvider
|
|
714
|
+
initialLocale="en-US"
|
|
715
|
+
initialPreference="en-US"
|
|
716
|
+
persistPreference={false}
|
|
717
|
+
>
|
|
718
|
+
<RunErrorRecoveryCard
|
|
719
|
+
info={{
|
|
720
|
+
message: "The model provider temporarily rejected this request.",
|
|
721
|
+
errorCode,
|
|
722
|
+
}}
|
|
723
|
+
onContinue={vi.fn()}
|
|
724
|
+
onRetry={onRetry}
|
|
725
|
+
onDismiss={vi.fn()}
|
|
726
|
+
/>
|
|
727
|
+
</AgentNativeI18nProvider>,
|
|
728
|
+
);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const retryButton = container.querySelector<HTMLButtonElement>(
|
|
732
|
+
'button[aria-label="Retry"]',
|
|
733
|
+
);
|
|
734
|
+
expect(retryButton?.textContent?.trim()).toBe("Retry");
|
|
735
|
+
await act(async () => retryButton?.click());
|
|
736
|
+
expect(onRetry).toHaveBeenCalledOnce();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("does not offer Retry for a daily provider request cap", async () => {
|
|
740
|
+
await act(async () => {
|
|
741
|
+
root.render(
|
|
742
|
+
<AgentNativeI18nProvider
|
|
743
|
+
initialLocale="en-US"
|
|
744
|
+
initialPreference="en-US"
|
|
745
|
+
persistPreference={false}
|
|
746
|
+
>
|
|
747
|
+
<RunErrorRecoveryCard
|
|
748
|
+
info={{
|
|
749
|
+
message: "The daily model request cap was reached.",
|
|
750
|
+
errorCode: "rate_limit_exceeded",
|
|
751
|
+
}}
|
|
752
|
+
onContinue={vi.fn()}
|
|
753
|
+
onRetry={vi.fn()}
|
|
754
|
+
onDismiss={vi.fn()}
|
|
755
|
+
/>
|
|
756
|
+
</AgentNativeI18nProvider>,
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
expect(container.querySelector('button[aria-label="Retry"]')).toBeNull();
|
|
761
|
+
});
|
|
762
|
+
|
|
703
763
|
it("does not offer Retry for a terminal invalid-attachment provider error", async () => {
|
|
704
764
|
const onRetry = vi.fn();
|
|
705
765
|
|
|
@@ -203,6 +203,17 @@ export function runErrorHeadline(
|
|
|
203
203
|
return info.recoverable === true ? labels.recoverable : labels.terminal;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
function isManuallyRetryableProviderError(errorCode?: string): boolean {
|
|
207
|
+
const code = errorCode?.trim().toLowerCase();
|
|
208
|
+
// Keep daily/account caps terminal; these codes represent transient rejections.
|
|
209
|
+
return (
|
|
210
|
+
code === "http_429" ||
|
|
211
|
+
code === "http_529" ||
|
|
212
|
+
code === "provider_rate_limited" ||
|
|
213
|
+
code === "provider_transient_rejection"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
206
217
|
export function getRequestModeMetadata(
|
|
207
218
|
message: unknown,
|
|
208
219
|
): "act" | "plan" | null {
|
|
@@ -794,6 +805,9 @@ export function RunErrorRecoveryCard({
|
|
|
794
805
|
// Retrying or continuing sends the rejected attachment again.
|
|
795
806
|
const attachmentRejected = info.errorCode === "invalid_attachment";
|
|
796
807
|
const canRecover = info.recoverable === true && !attachmentRejected;
|
|
808
|
+
const manuallyRetryableProviderError = isManuallyRetryableProviderError(
|
|
809
|
+
info.errorCode,
|
|
810
|
+
);
|
|
797
811
|
const retryWithoutAttachments =
|
|
798
812
|
onRetryWithoutAttachments ?? onRetryWithoutAttachment;
|
|
799
813
|
const canRetryWithoutAttachments =
|
|
@@ -840,7 +854,11 @@ export function RunErrorRecoveryCard({
|
|
|
840
854
|
// credentials. Without this the common case — a rejected workspace or
|
|
841
855
|
// deployment credential the reader cannot see, let alone edit — rendered a
|
|
842
856
|
// "Connected ✓" panel with no action at all.
|
|
843
|
-
const canRetry =
|
|
857
|
+
const canRetry =
|
|
858
|
+
canRecover ||
|
|
859
|
+
isUnblockableExternally ||
|
|
860
|
+
isProviderAuthError ||
|
|
861
|
+
manuallyRetryableProviderError;
|
|
844
862
|
const builderReconnectResolved =
|
|
845
863
|
shouldShowBuilderReconnect &&
|
|
846
864
|
builderReconnect.hasFetchedStatus &&
|
|
@@ -1158,9 +1176,17 @@ export function RunErrorRecoveryCard({
|
|
|
1158
1176
|
? t("agentChat.recovery.diagnoseRetry")
|
|
1159
1177
|
: t("agentChat.common.retry")
|
|
1160
1178
|
}
|
|
1161
|
-
className=
|
|
1179
|
+
className={cn(
|
|
1180
|
+
"inline-flex h-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
1181
|
+
manuallyRetryableProviderError ? "gap-1.5 px-2" : "w-7",
|
|
1182
|
+
)}
|
|
1162
1183
|
>
|
|
1163
1184
|
<IconRefresh size={14} />
|
|
1185
|
+
{manuallyRetryableProviderError && (
|
|
1186
|
+
<span className="text-[11px] font-medium">
|
|
1187
|
+
{t("agentChat.common.retry")}
|
|
1188
|
+
</span>
|
|
1189
|
+
)}
|
|
1164
1190
|
</button>
|
|
1165
1191
|
)}
|
|
1166
1192
|
{canRecover && isConnectionRecoveryError && (
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import {
|
|
3
|
+
Tooltip,
|
|
4
|
+
TooltipContent,
|
|
5
|
+
TooltipProvider,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
} from "@agent-native/toolkit/ui/tooltip";
|
|
8
|
+
import { cn } from "@agent-native/toolkit/utils";
|
|
9
|
+
import { IconMessageReport } from "@tabler/icons-react";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
|
|
12
|
+
import { ReportProblemDialog } from "./ReportProblemDialog.js";
|
|
13
|
+
|
|
14
|
+
export interface ReportProblemButtonProps {
|
|
15
|
+
variant?: "sidebar" | "icon";
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ReportProblemButton({
|
|
20
|
+
variant = "sidebar",
|
|
21
|
+
className,
|
|
22
|
+
}: ReportProblemButtonProps) {
|
|
23
|
+
const t = useT();
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const label = t("agentChat.reportProblem.menuItem");
|
|
26
|
+
|
|
27
|
+
const button = (
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
aria-label={variant === "icon" ? label : undefined}
|
|
31
|
+
onClick={() => setOpen(true)}
|
|
32
|
+
className={cn(
|
|
33
|
+
variant === "icon"
|
|
34
|
+
? "flex size-9 items-center justify-center rounded-md bg-transparent text-primary hover:bg-accent/60 hover:text-primary"
|
|
35
|
+
: "flex h-auto w-full items-center justify-start gap-2 rounded bg-transparent px-2 py-1.5 text-xs font-normal text-primary hover:bg-accent/60 hover:text-primary",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<IconMessageReport className="size-4 shrink-0 text-primary" />
|
|
40
|
+
{variant === "sidebar" && <span>{label}</span>}
|
|
41
|
+
</button>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
{variant === "icon" ? (
|
|
47
|
+
<TooltipProvider delayDuration={200}>
|
|
48
|
+
<Tooltip>
|
|
49
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
50
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
</TooltipProvider>
|
|
53
|
+
) : (
|
|
54
|
+
button
|
|
55
|
+
)}
|
|
56
|
+
<ReportProblemDialog open={open} onOpenChange={setOpen} />
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import type { RecorderReport } from "@agent-native/core/client/recorder";
|
|
4
|
+
import React, { act } from "react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
report: vi.fn(),
|
|
10
|
+
uploadProblemReport: vi.fn(),
|
|
11
|
+
sendToAgentChat: vi.fn(),
|
|
12
|
+
writeClipboardText: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/client/recorder", () => ({
|
|
16
|
+
recorder: { report: mocks.report },
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("@agent-native/core/client/problem-report", async (importOriginal) => ({
|
|
19
|
+
...(await importOriginal<
|
|
20
|
+
typeof import("@agent-native/core/client/problem-report")
|
|
21
|
+
>()),
|
|
22
|
+
uploadProblemReport: mocks.uploadProblemReport,
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
25
|
+
sendToAgentChat: mocks.sendToAgentChat,
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("@agent-native/toolkit/clipboard", () => ({
|
|
28
|
+
writeClipboardText: mocks.writeClipboardText,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { useRegisterAgentChatSurface } from "../chat/agent-chat-presence.js";
|
|
32
|
+
import { ReportProblemDialog } from "./ReportProblemDialog.js";
|
|
33
|
+
|
|
34
|
+
const REF = "attachment:v1:report-ref";
|
|
35
|
+
|
|
36
|
+
function fixture(reason: RecorderReport["reason"]): RecorderReport {
|
|
37
|
+
return {
|
|
38
|
+
app: "mail",
|
|
39
|
+
build: "abc123",
|
|
40
|
+
url: "/inbox",
|
|
41
|
+
at: "2026-10-14T16:20:05.120Z",
|
|
42
|
+
reason,
|
|
43
|
+
windowSeconds: 120,
|
|
44
|
+
events: [
|
|
45
|
+
{ t: -3, at: 1, type: "action", name: "list-labels", ok: true },
|
|
46
|
+
{
|
|
47
|
+
t: -0.8,
|
|
48
|
+
at: 2,
|
|
49
|
+
type: "action",
|
|
50
|
+
name: "list-folders",
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 403,
|
|
53
|
+
},
|
|
54
|
+
{ t: -0.5, at: 3, type: "error", name: "TypeError" },
|
|
55
|
+
],
|
|
56
|
+
watched: {},
|
|
57
|
+
pending: [],
|
|
58
|
+
dropped: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function AgentChatStandIn() {
|
|
63
|
+
useRegisterAgentChatSurface(true);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let container: HTMLDivElement;
|
|
68
|
+
let root: Root;
|
|
69
|
+
const onOpenChange = vi.fn();
|
|
70
|
+
|
|
71
|
+
function render({ withAgentChat }: { withAgentChat: boolean }) {
|
|
72
|
+
act(() => {
|
|
73
|
+
root.render(
|
|
74
|
+
<>
|
|
75
|
+
{withAgentChat ? <AgentChatStandIn /> : null}
|
|
76
|
+
<ReportProblemDialog open onOpenChange={onOpenChange} />
|
|
77
|
+
</>,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function button(name: string): HTMLButtonElement {
|
|
83
|
+
const match = [...document.body.querySelectorAll("button")].find(
|
|
84
|
+
(candidate) => candidate.textContent?.trim() === name,
|
|
85
|
+
);
|
|
86
|
+
if (!match) throw new Error(`No button named ${name}`);
|
|
87
|
+
return match;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function click(name: string) {
|
|
91
|
+
await act(async () => {
|
|
92
|
+
button(name).click();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
98
|
+
mocks.report.mockReset();
|
|
99
|
+
mocks.report.mockImplementation((options?: { reason?: string }) =>
|
|
100
|
+
fixture(options?.reason === "user" ? "user" : "manual"),
|
|
101
|
+
);
|
|
102
|
+
mocks.uploadProblemReport.mockReset();
|
|
103
|
+
mocks.sendToAgentChat.mockReset();
|
|
104
|
+
mocks.writeClipboardText.mockReset();
|
|
105
|
+
onOpenChange.mockReset();
|
|
106
|
+
container = document.createElement("div");
|
|
107
|
+
document.body.appendChild(container);
|
|
108
|
+
root = createRoot(container);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
act(() => root.unmount());
|
|
113
|
+
container.remove();
|
|
114
|
+
document.body.innerHTML = "";
|
|
115
|
+
vi.unstubAllGlobals();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("ReportProblemDialog", () => {
|
|
119
|
+
it("previews counts, failures and first events without building a user report", () => {
|
|
120
|
+
render({ withAgentChat: true });
|
|
121
|
+
|
|
122
|
+
const text = document.body.textContent ?? "";
|
|
123
|
+
expect(text).toContain("Attach what the app did in the last 120 seconds?");
|
|
124
|
+
expect(text).toContain("action 2 · error 1");
|
|
125
|
+
expect(text).toContain("-0.8s action list-folders failed 403");
|
|
126
|
+
expect(text).toContain("-3.0s action list-labels ok");
|
|
127
|
+
expect(mocks.report).toHaveBeenCalledWith();
|
|
128
|
+
expect(mocks.report).not.toHaveBeenCalledWith({ reason: "user" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("attaches the user report and sends the bounded summary to the agent", async () => {
|
|
132
|
+
mocks.uploadProblemReport.mockResolvedValue({ status: "ok", ref: REF });
|
|
133
|
+
render({ withAgentChat: true });
|
|
134
|
+
|
|
135
|
+
await click("Send to agent");
|
|
136
|
+
|
|
137
|
+
expect(mocks.uploadProblemReport).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({ reason: "user" }),
|
|
139
|
+
);
|
|
140
|
+
expect(mocks.sendToAgentChat).toHaveBeenCalledWith(
|
|
141
|
+
expect.objectContaining({
|
|
142
|
+
context: expect.stringContaining(REF),
|
|
143
|
+
submit: true,
|
|
144
|
+
openSidebar: true,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("says the report was not attached and retries the same report", async () => {
|
|
151
|
+
mocks.uploadProblemReport.mockResolvedValue({
|
|
152
|
+
status: "failed",
|
|
153
|
+
errorCode: "attachment_storage_unavailable",
|
|
154
|
+
message: "No object storage is connected.",
|
|
155
|
+
});
|
|
156
|
+
render({ withAgentChat: true });
|
|
157
|
+
|
|
158
|
+
await click("Send to agent");
|
|
159
|
+
|
|
160
|
+
expect(document.body.querySelector('[role="alert"]')?.textContent).toBe(
|
|
161
|
+
"The report was not attached. No object storage is connected.",
|
|
162
|
+
);
|
|
163
|
+
expect(mocks.sendToAgentChat).not.toHaveBeenCalled();
|
|
164
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
165
|
+
|
|
166
|
+
mocks.uploadProblemReport.mockResolvedValue({ status: "ok", ref: REF });
|
|
167
|
+
await click("Retry");
|
|
168
|
+
|
|
169
|
+
const [first, second] = mocks.uploadProblemReport.mock.calls;
|
|
170
|
+
expect(second[0]).toBe(first[0]);
|
|
171
|
+
expect(
|
|
172
|
+
mocks.report.mock.calls.filter(([options]) => options?.reason === "user"),
|
|
173
|
+
).toHaveLength(1);
|
|
174
|
+
expect(mocks.sendToAgentChat).toHaveBeenCalledTimes(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("copies the summary with its ref when the app has no agent chat", async () => {
|
|
178
|
+
mocks.uploadProblemReport.mockResolvedValue({ status: "ok", ref: REF });
|
|
179
|
+
mocks.writeClipboardText.mockResolvedValue(true);
|
|
180
|
+
render({ withAgentChat: false });
|
|
181
|
+
|
|
182
|
+
await click("Copy problem report");
|
|
183
|
+
|
|
184
|
+
expect(mocks.writeClipboardText).toHaveBeenCalledWith(
|
|
185
|
+
expect.stringContaining(REF),
|
|
186
|
+
);
|
|
187
|
+
expect(mocks.sendToAgentChat).not.toHaveBeenCalled();
|
|
188
|
+
expect(button("Copied")).toBeTruthy();
|
|
189
|
+
});
|
|
190
|
+
});
|