@agent-native/dispatch 0.40.36 → 0.40.38
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/CHANGELOG.md +25 -14
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/components/layout/Layout.d.ts +0 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +57 -101
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/routes/pages/overview.d.ts +2 -2
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +9 -5
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.js +1 -1
- package/dist/server/lib/community-apps.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +2 -2
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/layout/Layout.spec.tsx +348 -26
- package/src/components/layout/Layout.tsx +119 -193
- package/src/server/lib/app-creation-store.spec.ts +26 -5
- package/src/server/lib/app-creation-store.ts +10 -6
- package/src/server/lib/community-apps.ts +1 -1
- package/src/server/lib/usage-metrics-store.spec.ts +6 -2
- package/src/server/lib/usage-metrics-store.ts +2 -2
|
@@ -4,6 +4,7 @@ import { isSettingsPathname } from "@agent-native/toolkit/app/settings";
|
|
|
4
4
|
import React, { act } from "react";
|
|
5
5
|
import { createRoot, type Root } from "react-dom/client";
|
|
6
6
|
import { MemoryRouter, useLocation, useNavigate } from "react-router";
|
|
7
|
+
import { toast } from "sonner";
|
|
7
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
9
|
|
|
9
10
|
import { CHAT_FIRST_PANE_STATE_KEY } from "../../shared/chat-first-pane";
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
buildChatFirstEmbedSessionInput,
|
|
14
15
|
CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME,
|
|
15
16
|
dispatchNavLinkTarget,
|
|
16
|
-
formatThreadAge,
|
|
17
17
|
isElectronEmbeddedSearch,
|
|
18
18
|
isRedesignedSettingsPath,
|
|
19
19
|
Layout,
|
|
@@ -26,6 +26,16 @@ import {
|
|
|
26
26
|
const clientState = vi.hoisted(() => ({
|
|
27
27
|
createThread: vi.fn<() => Promise<string | null>>(),
|
|
28
28
|
switchThread: vi.fn(),
|
|
29
|
+
renameThread: vi.fn().mockResolvedValue(true),
|
|
30
|
+
pinThread: vi.fn().mockResolvedValue(true),
|
|
31
|
+
archiveThread: vi.fn().mockResolvedValue(true),
|
|
32
|
+
isNewThread: () => false,
|
|
33
|
+
mineThreads: null as Array<Record<string, unknown>> | null,
|
|
34
|
+
sharedThreads: [] as Array<Record<string, unknown>>,
|
|
35
|
+
sharedHasMore: false,
|
|
36
|
+
loadMore: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
listQueries: vi.fn(),
|
|
38
|
+
lifecycleOptions: vi.fn(),
|
|
29
39
|
threads: [] as Array<Record<string, unknown>>,
|
|
30
40
|
workspaceApps: [] as Array<Record<string, unknown>>,
|
|
31
41
|
workspaceAppsError: null as Error | null,
|
|
@@ -70,15 +80,40 @@ vi.mock("@agent-native/core/client/agent-chat", async (importOriginal) => ({
|
|
|
70
80
|
onEffortChange: vi.fn(),
|
|
71
81
|
refreshEngines: vi.fn(),
|
|
72
82
|
}),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
useChatThreadList: (options: {
|
|
84
|
+
ownership: string;
|
|
85
|
+
includeExternal?: boolean;
|
|
86
|
+
}) => {
|
|
87
|
+
clientState.listQueries(options);
|
|
88
|
+
return {
|
|
89
|
+
threads:
|
|
90
|
+
options.ownership === "shared"
|
|
91
|
+
? clientState.sharedThreads
|
|
92
|
+
: (clientState.mineThreads ?? clientState.threads),
|
|
93
|
+
loading: false,
|
|
94
|
+
loadingMore: false,
|
|
95
|
+
error: null,
|
|
96
|
+
hasMore: options.ownership === "shared" && clientState.sharedHasMore,
|
|
97
|
+
loadMore: clientState.loadMore,
|
|
98
|
+
refresh: async () => {},
|
|
99
|
+
refreshWithResult: async () => "success",
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
useChatThreads: (...args: unknown[]) => {
|
|
103
|
+
clientState.lifecycleOptions(args[3]);
|
|
104
|
+
return {
|
|
105
|
+
threads: clientState.threads,
|
|
106
|
+
activeThreadId: "active-thread",
|
|
107
|
+
isLoading: false,
|
|
108
|
+
createThread: clientState.createThread,
|
|
109
|
+
switchThread: clientState.switchThread,
|
|
110
|
+
renameThread: clientState.renameThread,
|
|
111
|
+
pinThread: clientState.pinThread,
|
|
112
|
+
archiveThread: clientState.archiveThread,
|
|
113
|
+
isNewThread: clientState.isNewThread,
|
|
114
|
+
refreshThreads: vi.fn(),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
82
117
|
useChatModels: () => ({
|
|
83
118
|
availableModels: [],
|
|
84
119
|
defaultModel: "auto",
|
|
@@ -89,6 +124,14 @@ vi.mock("@agent-native/core/client/agent-chat", async (importOriginal) => ({
|
|
|
89
124
|
}),
|
|
90
125
|
}));
|
|
91
126
|
|
|
127
|
+
vi.mock(
|
|
128
|
+
"@agent-native/toolkit/app/chat/ownership-history",
|
|
129
|
+
async () =>
|
|
130
|
+
import("../../../../toolkit/src/app/chat/ownership-history/index.tsx"),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
vi.mock("sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } }));
|
|
134
|
+
|
|
92
135
|
vi.mock("@agent-native/toolkit/app/chat/AgentSidebar", () => ({
|
|
93
136
|
AgentToggleButton: () => null,
|
|
94
137
|
AgentSidebar: ({ children }: { children: React.ReactNode }) => (
|
|
@@ -100,6 +143,9 @@ vi.mock("@agent-native/toolkit/app/chat/AgentSidebar", () => ({
|
|
|
100
143
|
vi.mock("../deferred-chat-components.js", async (importOriginal) => ({
|
|
101
144
|
...(await importOriginal<typeof import("../deferred-chat-components.js")>()),
|
|
102
145
|
AgentChatSurface: () => null,
|
|
146
|
+
ChatFirstChatHistory: (
|
|
147
|
+
await import("../../../../toolkit/src/app/chat/chat-first/chat-history.tsx")
|
|
148
|
+
).ChatFirstChatHistory,
|
|
103
149
|
}));
|
|
104
150
|
|
|
105
151
|
vi.mock("@agent-native/core/client/api-path", () => ({
|
|
@@ -173,6 +219,8 @@ vi.mock("next-themes", () => ({
|
|
|
173
219
|
}));
|
|
174
220
|
|
|
175
221
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
222
|
+
DEFAULT_LOCALE: "en-US",
|
|
223
|
+
useOptionalLocale: () => ({ locale: "en-US" }),
|
|
176
224
|
useFormatters: () => ({ formatNumber: (value: number) => String(value) }),
|
|
177
225
|
useT: () => (key: string, values?: Record<string, unknown>) => {
|
|
178
226
|
const messages: Record<string, string> = {
|
|
@@ -183,6 +231,24 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
183
231
|
"dispatch.pages.workspaceApps": "Workspace apps",
|
|
184
232
|
"dispatch.nav.operate": "Operate",
|
|
185
233
|
"dispatch.nav.advanced": "Advanced",
|
|
234
|
+
"agentChat.history.mine": "My chats",
|
|
235
|
+
"agentChat.history.sharedWithYou": "Shared with you",
|
|
236
|
+
"agentChat.history.ownedBy": `Owner: ${values?.name ?? ""}`,
|
|
237
|
+
"agentChat.history.accessDirect": "Shared with you",
|
|
238
|
+
"agentChat.history.accessOrganization": "Shared with your organization",
|
|
239
|
+
"agentChat.history.copyLink": "Copy link",
|
|
240
|
+
"agentChat.history.showAll": "Show all",
|
|
241
|
+
"agentChat.history.showLess": "Show less",
|
|
242
|
+
"agentChat.history.loadMore": "Load more",
|
|
243
|
+
"agentChat.history.loadingMore": "Loading...",
|
|
244
|
+
"agentChat.history.emptyMine": "No chats yet",
|
|
245
|
+
"dispatch.sidebar.chats": "Chats",
|
|
246
|
+
"dispatch.sidebar.showLocalChats": "Show local chats",
|
|
247
|
+
"dispatch.sidebar.showAllChats": "Show all chats",
|
|
248
|
+
"agentChat.contextXray.pin": "Pin",
|
|
249
|
+
"agentChat.contextXray.unpin": "Unpin",
|
|
250
|
+
"extensions.delete": "Archive",
|
|
251
|
+
"dispatch.integrations.failedToSave": "Failed to save",
|
|
186
252
|
"dispatch.sidebar.newChat": "New chat",
|
|
187
253
|
"dispatch.sidebar.newDispatchChat": "New Dispatch chat",
|
|
188
254
|
"dispatch.sidebar.renameChat": "Rename chat",
|
|
@@ -248,20 +314,6 @@ describe("Dispatch navigation paths", () => {
|
|
|
248
314
|
});
|
|
249
315
|
});
|
|
250
316
|
|
|
251
|
-
describe("formatThreadAge", () => {
|
|
252
|
-
const now = 2_000_000_000_000;
|
|
253
|
-
|
|
254
|
-
it.each([
|
|
255
|
-
[0, "now"],
|
|
256
|
-
[2 * 60 * 60_000, "2h"],
|
|
257
|
-
[7 * 24 * 60 * 60_000, "7d"],
|
|
258
|
-
[21 * 24 * 60 * 60_000, "3w"],
|
|
259
|
-
[365 * 24 * 60 * 60_000, "1y"],
|
|
260
|
-
])("formats %i milliseconds as %s", (elapsed, expected) => {
|
|
261
|
-
expect(formatThreadAge(now - elapsed, now)).toBe(expected);
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
317
|
describe("chat-first embed sessions", () => {
|
|
266
318
|
it("keeps the granted app id on app-relative embed requests", () => {
|
|
267
319
|
expect(buildChatFirstEmbedSessionInput("mail", "/mail/inbox")).toEqual({
|
|
@@ -347,8 +399,26 @@ describe("Dispatch NavContent", () => {
|
|
|
347
399
|
});
|
|
348
400
|
clientState.createThread.mockResolvedValue("new-thread");
|
|
349
401
|
clientState.switchThread.mockReset();
|
|
402
|
+
clientState.createThread.mockClear();
|
|
403
|
+
clientState.renameThread.mockReset().mockResolvedValue(true);
|
|
404
|
+
clientState.pinThread.mockReset().mockResolvedValue(true);
|
|
405
|
+
clientState.archiveThread.mockReset().mockResolvedValue(true);
|
|
406
|
+
clientState.sharedThreads = [];
|
|
407
|
+
clientState.mineThreads = null;
|
|
408
|
+
clientState.sharedHasMore = false;
|
|
409
|
+
clientState.loadMore.mockClear();
|
|
410
|
+
clientState.listQueries.mockClear();
|
|
411
|
+
clientState.lifecycleOptions.mockClear();
|
|
412
|
+
localStorage.clear();
|
|
413
|
+
vi.stubGlobal(
|
|
414
|
+
"fetch",
|
|
415
|
+
vi.fn().mockResolvedValue(new Response("{}", { status: 200 })),
|
|
416
|
+
);
|
|
350
417
|
clientState.threads = [
|
|
351
418
|
{
|
|
419
|
+
ownerEmail: "probe@example.com",
|
|
420
|
+
accessBasis: "owner",
|
|
421
|
+
visibility: "private",
|
|
352
422
|
id: "active-thread",
|
|
353
423
|
title: "Current Dispatch work",
|
|
354
424
|
messageCount: 2,
|
|
@@ -356,6 +426,9 @@ describe("Dispatch NavContent", () => {
|
|
|
356
426
|
createdAt: Date.now(),
|
|
357
427
|
},
|
|
358
428
|
{
|
|
429
|
+
ownerEmail: "probe@example.com",
|
|
430
|
+
accessBasis: "owner",
|
|
431
|
+
visibility: "private",
|
|
359
432
|
id: "older-thread",
|
|
360
433
|
title: "Earlier Dispatch work",
|
|
361
434
|
messageCount: 1,
|
|
@@ -1166,6 +1239,255 @@ describe("Dispatch NavContent", () => {
|
|
|
1166
1239
|
expect(paths).toEqual(["/apps/mail"]);
|
|
1167
1240
|
});
|
|
1168
1241
|
|
|
1242
|
+
async function renderHistory(
|
|
1243
|
+
chatFirstMode = false,
|
|
1244
|
+
path = "/chat/active-thread",
|
|
1245
|
+
) {
|
|
1246
|
+
await act(async () => {
|
|
1247
|
+
root.render(
|
|
1248
|
+
<MemoryRouter initialEntries={[path]}>
|
|
1249
|
+
<TooltipProvider>
|
|
1250
|
+
<NavContent chatFirstMode={chatFirstMode} />
|
|
1251
|
+
</TooltipProvider>
|
|
1252
|
+
</MemoryRouter>,
|
|
1253
|
+
);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async function clickText(scope: ParentNode, text: string) {
|
|
1258
|
+
const button = [
|
|
1259
|
+
...scope.querySelectorAll<HTMLButtonElement>("button"),
|
|
1260
|
+
].find((button) => button.textContent?.trim() === text);
|
|
1261
|
+
expect(button, text).toBeDefined();
|
|
1262
|
+
await act(async () => {
|
|
1263
|
+
button!.click();
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async function openThreadMenu(title: string) {
|
|
1268
|
+
const row = [
|
|
1269
|
+
...container.querySelectorAll<HTMLElement>(".an-chat-history-row"),
|
|
1270
|
+
].find((row) => row.textContent?.includes(title));
|
|
1271
|
+
expect(row).toBeDefined();
|
|
1272
|
+
await act(async () => {
|
|
1273
|
+
row!.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true }));
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
it.each([false, true])(
|
|
1278
|
+
"keeps the proven-owned active empty chat and hides inactive blanks (chat-first %s)",
|
|
1279
|
+
async (chatFirstMode) => {
|
|
1280
|
+
clientState.mineThreads = [];
|
|
1281
|
+
clientState.threads = clientState.threads.map((thread) => ({
|
|
1282
|
+
...thread,
|
|
1283
|
+
messageCount: 0,
|
|
1284
|
+
}));
|
|
1285
|
+
await renderHistory(chatFirstMode, "/chat");
|
|
1286
|
+
|
|
1287
|
+
expect(container.textContent).toContain("Current Dispatch work");
|
|
1288
|
+
expect(container.textContent).not.toContain("Earlier Dispatch work");
|
|
1289
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(
|
|
1290
|
+
1,
|
|
1291
|
+
);
|
|
1292
|
+
expect(clientState.switchThread).not.toHaveBeenCalled();
|
|
1293
|
+
expect(clientState.lifecycleOptions).toHaveBeenLastCalledWith({
|
|
1294
|
+
autoCreate: false,
|
|
1295
|
+
includeExternal: false,
|
|
1296
|
+
});
|
|
1297
|
+
},
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
it.each(
|
|
1301
|
+
[false, true].flatMap((chatFirstMode) =>
|
|
1302
|
+
["direct", "organization", "unproven"].map(
|
|
1303
|
+
(basis) => [chatFirstMode, basis] as const,
|
|
1304
|
+
),
|
|
1305
|
+
),
|
|
1306
|
+
)(
|
|
1307
|
+
"does not promote an active empty %s/%s lifecycle row into Mine",
|
|
1308
|
+
async (chatFirstMode, basis) => {
|
|
1309
|
+
clientState.mineThreads = [];
|
|
1310
|
+
const active = clientState.threads[0];
|
|
1311
|
+
clientState.threads = [
|
|
1312
|
+
{
|
|
1313
|
+
...active,
|
|
1314
|
+
messageCount: 0,
|
|
1315
|
+
accessBasis: basis === "unproven" ? undefined : basis,
|
|
1316
|
+
ownerEmail:
|
|
1317
|
+
basis === "unproven" ? undefined : "colleague@example.com",
|
|
1318
|
+
visibility: basis === "unproven" ? undefined : "organization",
|
|
1319
|
+
},
|
|
1320
|
+
];
|
|
1321
|
+
await renderHistory(chatFirstMode);
|
|
1322
|
+
|
|
1323
|
+
expect(container.textContent).not.toContain("Current Dispatch work");
|
|
1324
|
+
expect(container.querySelector(".an-chat-history-row")).toBeNull();
|
|
1325
|
+
expect(clientState.switchThread).not.toHaveBeenCalled();
|
|
1326
|
+
},
|
|
1327
|
+
);
|
|
1328
|
+
|
|
1329
|
+
it.each([false, true])(
|
|
1330
|
+
"renders server Shared attribution, five-row disclosure and paging (chat-first %s)",
|
|
1331
|
+
async (chatFirstMode) => {
|
|
1332
|
+
clientState.sharedThreads = Array.from({ length: 6 }, (_, index) => ({
|
|
1333
|
+
id: `shared-${index}`,
|
|
1334
|
+
title: `Shared work ${index}`,
|
|
1335
|
+
preview: "",
|
|
1336
|
+
messageCount: 1,
|
|
1337
|
+
createdAt: Date.now(),
|
|
1338
|
+
updatedAt: Date.now(),
|
|
1339
|
+
ownerEmail: "colleague@example.com",
|
|
1340
|
+
accessBasis: index === 0 ? "direct" : "organization",
|
|
1341
|
+
visibility: "organization",
|
|
1342
|
+
pinnedAt: Date.now(),
|
|
1343
|
+
}));
|
|
1344
|
+
clientState.sharedHasMore = true;
|
|
1345
|
+
await renderHistory(chatFirstMode, "/chat/shared-0");
|
|
1346
|
+
const shared = container.querySelector(
|
|
1347
|
+
'[data-section-id="shared"].an-chat-history__section',
|
|
1348
|
+
)!;
|
|
1349
|
+
expect(shared).not.toBeNull();
|
|
1350
|
+
expect(shared.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
|
|
1351
|
+
expect(
|
|
1352
|
+
shared.querySelector('[role="img"]')?.getAttribute("aria-label"),
|
|
1353
|
+
).toBe("Owner: colleague@example.com");
|
|
1354
|
+
for (const basis of ["direct", "organization"]) {
|
|
1355
|
+
expect(
|
|
1356
|
+
shared.querySelector(
|
|
1357
|
+
`.an-chat-history-row__owner-mark[data-access-basis="${basis}"]`,
|
|
1358
|
+
),
|
|
1359
|
+
).not.toBeNull();
|
|
1360
|
+
}
|
|
1361
|
+
expect(shared.querySelector(".an-chat-history-row--pinned")).toBeNull();
|
|
1362
|
+
expect(
|
|
1363
|
+
shared.querySelector(".an-chat-history-row--active")?.textContent,
|
|
1364
|
+
).toContain("Shared work 0");
|
|
1365
|
+
await clickText(shared, "Show all");
|
|
1366
|
+
expect(shared.querySelectorAll(".an-chat-history-row")).toHaveLength(6);
|
|
1367
|
+
clientState.sharedThreads = clientState.sharedThreads.map((row) => ({
|
|
1368
|
+
...row,
|
|
1369
|
+
}));
|
|
1370
|
+
await renderHistory(chatFirstMode, "/chat/shared-0");
|
|
1371
|
+
expect(shared.querySelectorAll(".an-chat-history-row")).toHaveLength(6);
|
|
1372
|
+
expect(
|
|
1373
|
+
shared.querySelector(".an-chat-history-row--active")?.textContent,
|
|
1374
|
+
).toContain("Shared work 0");
|
|
1375
|
+
expect(clientState.switchThread).not.toHaveBeenCalled();
|
|
1376
|
+
await clickText(shared, "Load more");
|
|
1377
|
+
expect(clientState.loadMore).toHaveBeenCalledOnce();
|
|
1378
|
+
await clickText(shared, "Show less");
|
|
1379
|
+
expect(shared.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
|
|
1380
|
+
await act(async () => {
|
|
1381
|
+
shared
|
|
1382
|
+
.querySelector<HTMLButtonElement>(".an-chat-history-row__button")!
|
|
1383
|
+
.click();
|
|
1384
|
+
});
|
|
1385
|
+
expect(clientState.switchThread).toHaveBeenCalledWith("shared-0");
|
|
1386
|
+
expect(clientState.lifecycleOptions).toHaveBeenLastCalledWith({
|
|
1387
|
+
autoCreate: false,
|
|
1388
|
+
includeExternal: false,
|
|
1389
|
+
});
|
|
1390
|
+
await openThreadMenu("Shared work 0");
|
|
1391
|
+
expect(
|
|
1392
|
+
document.body.querySelector(".an-chat-history-row__menu-owner")
|
|
1393
|
+
?.textContent,
|
|
1394
|
+
).toContain("colleague@example.comShared with you");
|
|
1395
|
+
const actions = [
|
|
1396
|
+
...document.body.querySelectorAll(".an-chat-history-row__menu-item"),
|
|
1397
|
+
].map((item) => item.textContent);
|
|
1398
|
+
expect(actions).toContain("Copy link");
|
|
1399
|
+
for (const ownerAction of ["Rename chat", "Pin", "Unpin", "Archive"]) {
|
|
1400
|
+
expect(actions).not.toContain(ownerAction);
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
it.each([false, true])(
|
|
1406
|
+
"passes the persisted external source toggle to both ownership queries (chat-first %s)",
|
|
1407
|
+
async (chatFirstMode) => {
|
|
1408
|
+
await renderHistory(chatFirstMode);
|
|
1409
|
+
const toggle = container.querySelector<HTMLButtonElement>(
|
|
1410
|
+
"[data-dispatch-chat-source-toggle]",
|
|
1411
|
+
)!;
|
|
1412
|
+
expect(toggle.getAttribute("aria-pressed")).toBe("false");
|
|
1413
|
+
await act(async () => toggle.click());
|
|
1414
|
+
for (const ownership of ["mine", "shared"]) {
|
|
1415
|
+
expect(clientState.listQueries).toHaveBeenCalledWith(
|
|
1416
|
+
expect.objectContaining({ ownership, includeExternal: true }),
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
expect(localStorage.getItem("dispatch.chat-history.source")).toBe("all");
|
|
1420
|
+
},
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
it.each([false, true])(
|
|
1424
|
+
"archives the active owned chat before opening New chat (chat-first %s)",
|
|
1425
|
+
async (chatFirstMode) => {
|
|
1426
|
+
let settle!: (value: boolean) => void;
|
|
1427
|
+
clientState.archiveThread.mockReturnValue(
|
|
1428
|
+
new Promise<boolean>((resolve) => {
|
|
1429
|
+
settle = resolve;
|
|
1430
|
+
}),
|
|
1431
|
+
);
|
|
1432
|
+
await renderHistory(chatFirstMode);
|
|
1433
|
+
await openThreadMenu("Current Dispatch work");
|
|
1434
|
+
await clickText(document.body, "Archive");
|
|
1435
|
+
expect(clientState.archiveThread).toHaveBeenCalledWith("active-thread");
|
|
1436
|
+
expect(clientState.createThread).not.toHaveBeenCalled();
|
|
1437
|
+
await act(async () => settle(true));
|
|
1438
|
+
expect(clientState.createThread).toHaveBeenCalledOnce();
|
|
1439
|
+
expect(clientState.switchThread).toHaveBeenCalledWith("new-thread");
|
|
1440
|
+
},
|
|
1441
|
+
);
|
|
1442
|
+
|
|
1443
|
+
it.each([false, true])(
|
|
1444
|
+
"rolls back a failed archive, toasts once and keeps the active chat (chat-first %s)",
|
|
1445
|
+
async (chatFirstMode) => {
|
|
1446
|
+
vi.mocked(toast.error).mockClear();
|
|
1447
|
+
clientState.archiveThread.mockResolvedValue(false);
|
|
1448
|
+
await renderHistory(chatFirstMode);
|
|
1449
|
+
await openThreadMenu("Current Dispatch work");
|
|
1450
|
+
await clickText(document.body, "Archive");
|
|
1451
|
+
expect(container.textContent).toContain("Current Dispatch work");
|
|
1452
|
+
expect(clientState.createThread).not.toHaveBeenCalled();
|
|
1453
|
+
expect(clientState.switchThread).not.toHaveBeenCalled();
|
|
1454
|
+
expect(toast.error).toHaveBeenCalledExactlyOnceWith("Failed to save");
|
|
1455
|
+
},
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
it.each([false, true])(
|
|
1459
|
+
"pins and renames owned rows through the adapter (chat-first %s)",
|
|
1460
|
+
async (chatFirstMode) => {
|
|
1461
|
+
await renderHistory(chatFirstMode);
|
|
1462
|
+
await openThreadMenu("Earlier Dispatch work");
|
|
1463
|
+
await clickText(document.body, "Pin");
|
|
1464
|
+
expect(clientState.pinThread).toHaveBeenCalledWith("older-thread", true);
|
|
1465
|
+
await openThreadMenu("Earlier Dispatch work");
|
|
1466
|
+
await clickText(document.body, "Rename chat");
|
|
1467
|
+
const input = container.querySelector<HTMLInputElement>(
|
|
1468
|
+
".an-chat-history-row__rename-input",
|
|
1469
|
+
)!;
|
|
1470
|
+
await act(async () => {
|
|
1471
|
+
Object.getOwnPropertyDescriptor(
|
|
1472
|
+
HTMLInputElement.prototype,
|
|
1473
|
+
"value",
|
|
1474
|
+
)!.set!.call(input, "Revised work");
|
|
1475
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1476
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1477
|
+
});
|
|
1478
|
+
await act(async () => {
|
|
1479
|
+
input.dispatchEvent(
|
|
1480
|
+
new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
|
|
1481
|
+
);
|
|
1482
|
+
});
|
|
1483
|
+
expect(clientState.renameThread).toHaveBeenCalledWith(
|
|
1484
|
+
"older-thread",
|
|
1485
|
+
"Revised work",
|
|
1486
|
+
);
|
|
1487
|
+
expect(clientState.createThread).not.toHaveBeenCalled();
|
|
1488
|
+
},
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1169
1491
|
it("uses the shared chat history rail and retains thread actions", async () => {
|
|
1170
1492
|
await act(async () => {
|
|
1171
1493
|
root.render(
|
|
@@ -1181,7 +1503,7 @@ describe("Dispatch NavContent", () => {
|
|
|
1181
1503
|
expect(container.textContent).toContain("Current Dispatch work");
|
|
1182
1504
|
expect(container.textContent).toContain("Earlier Dispatch work");
|
|
1183
1505
|
expect(container.textContent).toContain("New chat");
|
|
1184
|
-
expect(container.textContent).toContain("5m");
|
|
1506
|
+
expect(container.textContent).toContain("5m ago");
|
|
1185
1507
|
expect(container.querySelector('[aria-label="Slack"]')).not.toBeNull();
|
|
1186
1508
|
const sourceToggle = container.querySelector(
|
|
1187
1509
|
"[data-dispatch-chat-source-toggle]",
|
|
@@ -1192,7 +1514,7 @@ describe("Dispatch NavContent", () => {
|
|
|
1192
1514
|
});
|
|
1193
1515
|
expect(sourceToggle.getAttribute("aria-pressed")).toBe("true");
|
|
1194
1516
|
const age = [...container.querySelectorAll("span")].find(
|
|
1195
|
-
(element) => element.textContent === "5m",
|
|
1517
|
+
(element) => element.textContent === "5m ago",
|
|
1196
1518
|
);
|
|
1197
1519
|
expect(age?.className).toContain("an-chat-history-row__timestamp");
|
|
1198
1520
|
const historyList = container.querySelector(
|