@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.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/CHANGELOG.md +26 -55
- package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
- package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/get-hosted-apps-settings.js +25 -0
- package/dist/actions/get-hosted-apps-settings.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +4 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +11 -10
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list_apps.js +3 -2
- package/dist/actions/list_apps.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
- package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
- package/dist/actions/set-hosted-apps-settings.js +35 -0
- package/dist/actions/set-hosted-apps-settings.js.map +1 -0
- package/dist/components/app-card-tiers.d.ts +13 -0
- package/dist/components/app-card-tiers.d.ts.map +1 -0
- package/dist/components/app-card-tiers.js +22 -0
- package/dist/components/app-card-tiers.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -4
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +19 -4
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +4 -0
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +9 -0
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-pin-button.d.ts +9 -0
- package/dist/components/app-pin-button.d.ts.map +1 -0
- package/dist/components/app-pin-button.js +29 -0
- package/dist/components/app-pin-button.js.map +1 -0
- package/dist/components/app-tier-sections.d.ts +27 -0
- package/dist/components/app-tier-sections.d.ts.map +1 -0
- package/dist/components/app-tier-sections.js +44 -0
- package/dist/components/app-tier-sections.js.map +1 -0
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +19 -26
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/connected-app-card.d.ts +3 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +6 -3
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts +4 -0
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +6 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +25 -35
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/hosted-apps-settings-group.d.ts +6 -0
- package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
- package/dist/components/hosted-apps-settings-group.js +76 -0
- package/dist/components/hosted-apps-settings-group.js.map +1 -0
- package/dist/components/layout/Layout.d.ts +6 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +97 -80
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +9 -20
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-layout-notice.d.ts +9 -0
- package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
- package/dist/components/workspace-app-layout-notice.js +27 -0
- package/dist/components/workspace-app-layout-notice.js.map +1 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -1
- package/dist/components/workspace-app-search.js +11 -5
- package/dist/components/workspace-app-search.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +7 -2
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/lib/other-apps.d.ts +10 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +7 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +62 -6
- package/dist/lib/workspace-app-layout.d.ts.map +1 -1
- package/dist/lib/workspace-app-layout.js +196 -41
- package/dist/lib/workspace-app-layout.js.map +1 -1
- package/dist/lib/workspace-app-lists.d.ts +17 -0
- package/dist/lib/workspace-app-lists.d.ts.map +1 -0
- package/dist/lib/workspace-app-lists.js +21 -0
- package/dist/lib/workspace-app-lists.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +3 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +5 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -35
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/settings-page.d.ts.map +1 -1
- package/dist/routes/pages/settings-page.js +24 -3
- package/dist/routes/pages/settings-page.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +13 -18
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/app-roles.d.ts +2 -0
- package/dist/server/lib/app-roles.d.ts.map +1 -1
- package/dist/server/lib/app-roles.js +26 -7
- package/dist/server/lib/app-roles.js.map +1 -1
- package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
- package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
- package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
- package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
- package/dist/server/lib/hosted-apps-settings.js +72 -0
- package/dist/server/lib/hosted-apps-settings.js.map +1 -0
- package/dist/server/plugins/agent-chat.js +6 -3
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/hosted-apps-settings.d.ts +3 -0
- package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
- package/dist/shared/hosted-apps-settings.js +3 -0
- package/dist/shared/hosted-apps-settings.js.map +1 -0
- package/package.json +4 -4
- package/src/actions/get-hosted-apps-settings.ts +29 -0
- package/src/actions/hosted-apps-settings.spec.ts +100 -0
- package/src/actions/index.ts +4 -0
- package/src/actions/list-connected-agents.spec.ts +67 -0
- package/src/actions/list-connected-agents.ts +14 -15
- package/src/actions/list_apps.spec.ts +16 -0
- package/src/actions/list_apps.ts +5 -2
- package/src/actions/set-hosted-apps-settings.ts +42 -0
- package/src/components/app-card-tiers.tsx +85 -0
- package/src/components/app-list-row.spec.tsx +54 -0
- package/src/components/app-list-row.tsx +50 -8
- package/src/components/app-open-actions.tsx +16 -0
- package/src/components/app-pin-button.spec.tsx +68 -0
- package/src/components/app-pin-button.tsx +66 -0
- package/src/components/app-tier-sections.spec.tsx +155 -0
- package/src/components/app-tier-sections.tsx +158 -0
- package/src/components/chat-home-apps.spec.tsx +135 -29
- package/src/components/chat-home-apps.tsx +115 -111
- package/src/components/connected-app-card.spec.tsx +18 -2
- package/src/components/connected-app-card.tsx +18 -1
- package/src/components/create-app-popover.tsx +22 -0
- package/src/components/dispatch-control-plane.spec.tsx +89 -20
- package/src/components/dispatch-control-plane.tsx +91 -125
- package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
- package/src/components/hosted-apps-settings-group.tsx +189 -0
- package/src/components/layout/Layout.spec.tsx +356 -49
- package/src/components/layout/Layout.tsx +189 -142
- package/src/components/workspace-app-card.spec.tsx +33 -29
- package/src/components/workspace-app-card.tsx +13 -31
- package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
- package/src/components/workspace-app-layout-notice.tsx +65 -0
- package/src/components/workspace-app-search.spec.tsx +53 -0
- package/src/components/workspace-app-search.tsx +39 -32
- package/src/components/workspace-template-card.spec.tsx +10 -0
- package/src/components/workspace-template-card.tsx +7 -2
- package/src/db/index.spec.ts +9 -0
- package/src/db/index.ts +1 -0
- package/src/lib/other-apps.spec.ts +25 -0
- package/src/lib/other-apps.ts +9 -0
- package/src/lib/workspace-app-layout.spec.ts +61 -3
- package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
- package/src/lib/workspace-app-layout.ts +265 -48
- package/src/lib/workspace-app-lists.ts +32 -0
- package/src/lib/workspace-apps.ts +10 -2
- package/src/routes/pages/apps.spec.tsx +133 -5
- package/src/routes/pages/apps.tsx +79 -140
- package/src/routes/pages/chat.spec.tsx +66 -49
- package/src/routes/pages/settings-page.tsx +31 -3
- package/src/routes/pages/settings.spec.tsx +37 -0
- package/src/server/lib/app-creation-store.spec.ts +14 -0
- package/src/server/lib/app-creation-store.ts +16 -29
- package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
- package/src/server/lib/app-roles.spec.ts +48 -1
- package/src/server/lib/app-roles.ts +44 -12
- package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
- package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
- package/src/server/lib/hosted-apps-settings.ts +108 -0
- package/src/server/plugins/agent-chat.spec.ts +16 -0
- package/src/server/plugins/agent-chat.ts +7 -2
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/hosted-apps-settings.ts +3 -0
- package/src/styles/dispatch.css +40 -11
- package/dist/components/available-apps-section.d.ts +0 -9
- package/dist/components/available-apps-section.d.ts.map +0 -1
- package/dist/components/available-apps-section.js +0 -97
- package/dist/components/available-apps-section.js.map +0 -1
- package/dist/components/other-apps-section.d.ts +0 -35
- package/dist/components/other-apps-section.d.ts.map +0 -1
- package/dist/components/other-apps-section.js +0 -79
- package/dist/components/other-apps-section.js.map +0 -1
- package/src/components/available-apps-section.tsx +0 -239
- package/src/components/other-apps-section.spec.ts +0 -50
- package/src/components/other-apps-section.tsx +0 -254
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useActionMutation,
|
|
3
|
+
useActionQuery,
|
|
4
|
+
} from "@agent-native/core/client/hooks";
|
|
5
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
6
|
+
import { SettingsGroup, SettingsRow } from "@agent-native/toolkit/app/settings";
|
|
7
|
+
import { IconArrowUpRight } from "@tabler/icons-react";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
|
|
11
|
+
import { useWorkspaceAppLists } from "../lib/workspace-app-lists";
|
|
12
|
+
import { HOSTED_APPS_SETTINGS_INVALID_ERROR_CODE } from "../shared/hosted-apps-settings";
|
|
13
|
+
import { ActionQueryError } from "./action-query-error";
|
|
14
|
+
import { Button } from "./ui/button";
|
|
15
|
+
import { Skeleton } from "./ui/skeleton";
|
|
16
|
+
import { Switch } from "./ui/switch";
|
|
17
|
+
|
|
18
|
+
export interface HostedAppsSettings {
|
|
19
|
+
showHostedApps: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isInvalidStoredSetting(error: unknown): boolean {
|
|
23
|
+
return (
|
|
24
|
+
!!error &&
|
|
25
|
+
typeof error === "object" &&
|
|
26
|
+
(error as { errorCode?: unknown }).errorCode ===
|
|
27
|
+
HOSTED_APPS_SETTINGS_INVALID_ERROR_CODE
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hostOf(url: string): string {
|
|
32
|
+
try {
|
|
33
|
+
return new URL(url).host;
|
|
34
|
+
} catch {
|
|
35
|
+
// coercion-ok: a URL the browser cannot parse is shown as stored.
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Settings › Organization › Apps: show or hide hosted apps for everyone. */
|
|
41
|
+
export function HostedAppsSettingsGroup() {
|
|
42
|
+
const t = useT();
|
|
43
|
+
const query = useActionQuery<HostedAppsSettings>(
|
|
44
|
+
"get-hosted-apps-settings",
|
|
45
|
+
{},
|
|
46
|
+
);
|
|
47
|
+
// The switched value holds until the setting reads back: a save invalidates
|
|
48
|
+
// action queries, and the next read replaces `query.data`.
|
|
49
|
+
const [optimistic, setOptimistic] = useState<{
|
|
50
|
+
showHostedApps: boolean;
|
|
51
|
+
readFrom: HostedAppsSettings | undefined;
|
|
52
|
+
} | null>(null);
|
|
53
|
+
const save = useActionMutation<
|
|
54
|
+
HostedAppsSettings,
|
|
55
|
+
{ showHostedApps: boolean }
|
|
56
|
+
>("set-hosted-apps-settings", {
|
|
57
|
+
onError: (error) => {
|
|
58
|
+
setOptimistic(null);
|
|
59
|
+
toast.error(t("dispatch.pages.hostedAppsSaveFailed"), {
|
|
60
|
+
description: error.message,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const title = t("dispatch.pages.hostedApps");
|
|
65
|
+
// An invalid stored value has no state to show; saving any value repairs it.
|
|
66
|
+
const storedInvalid = query.isError && isInvalidStoredSetting(query.error);
|
|
67
|
+
const showHostedApps =
|
|
68
|
+
optimistic && optimistic.readFrom === query.data
|
|
69
|
+
? optimistic.showHostedApps
|
|
70
|
+
: storedInvalid
|
|
71
|
+
? false
|
|
72
|
+
: query.data?.showHostedApps;
|
|
73
|
+
// The launchers' Hosted apps tier, including the apps this setting hides.
|
|
74
|
+
const lists = useWorkspaceAppLists({ includeHiddenFromLaunchers: true });
|
|
75
|
+
const listsFailed =
|
|
76
|
+
lists.status === "error" ||
|
|
77
|
+
(lists.connectedAppsQuery.isError &&
|
|
78
|
+
lists.connectedAppsQuery.data === undefined);
|
|
79
|
+
const listsLoading =
|
|
80
|
+
!listsFailed &&
|
|
81
|
+
(lists.status === "loading" || lists.connectedAppsQuery.data === undefined);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<SettingsGroup id="hosted-apps" title={title}>
|
|
85
|
+
{query.isError && !storedInvalid && query.data === undefined ? (
|
|
86
|
+
<div className="px-5 py-4 sm:px-6">
|
|
87
|
+
<ActionQueryError
|
|
88
|
+
error={query.error}
|
|
89
|
+
onRetry={() => void query.refetch()}
|
|
90
|
+
queryName="get-hosted-apps-settings"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
) : showHostedApps === undefined ? (
|
|
94
|
+
<div
|
|
95
|
+
className="flex items-center justify-between gap-4 px-5 py-4 sm:px-6"
|
|
96
|
+
aria-busy="true"
|
|
97
|
+
>
|
|
98
|
+
<Skeleton className="h-3.5 w-32" />
|
|
99
|
+
<Skeleton className="h-5 w-9 rounded-full" />
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<>
|
|
103
|
+
<SettingsRow
|
|
104
|
+
id="show-hosted-apps"
|
|
105
|
+
label={t("dispatch.pages.showHostedApps")}
|
|
106
|
+
control={
|
|
107
|
+
<Switch
|
|
108
|
+
aria-label={t("dispatch.pages.showHostedApps")}
|
|
109
|
+
checked={showHostedApps}
|
|
110
|
+
disabled={save.isPending}
|
|
111
|
+
onCheckedChange={(checked) => {
|
|
112
|
+
setOptimistic({
|
|
113
|
+
showHostedApps: checked,
|
|
114
|
+
readFrom: query.data,
|
|
115
|
+
});
|
|
116
|
+
save.mutate({ showHostedApps: checked });
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{storedInvalid ? (
|
|
122
|
+
<p className="text-xs text-destructive" role="alert">
|
|
123
|
+
{t("dispatch.pages.hostedAppsSettingInvalid")}
|
|
124
|
+
</p>
|
|
125
|
+
) : null}
|
|
126
|
+
</SettingsRow>
|
|
127
|
+
{listsFailed ? (
|
|
128
|
+
<div className="px-5 py-4 sm:px-6">
|
|
129
|
+
<ActionQueryError
|
|
130
|
+
error={
|
|
131
|
+
lists.status === "error"
|
|
132
|
+
? lists.appsQuery.error
|
|
133
|
+
: lists.connectedAppsQuery.error
|
|
134
|
+
}
|
|
135
|
+
onRetry={() => {
|
|
136
|
+
void lists.appsQuery.refetch();
|
|
137
|
+
void lists.connectedAppsQuery.refetch();
|
|
138
|
+
}}
|
|
139
|
+
queryName={
|
|
140
|
+
lists.status === "error"
|
|
141
|
+
? "list-workspace-apps"
|
|
142
|
+
: "list-connected-agents"
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
) : listsLoading ? (
|
|
147
|
+
<div
|
|
148
|
+
className="flex items-center justify-between gap-4 px-5 py-4 sm:px-6"
|
|
149
|
+
aria-busy="true"
|
|
150
|
+
data-hosted-apps-loading
|
|
151
|
+
>
|
|
152
|
+
<Skeleton className="h-3.5 w-24" />
|
|
153
|
+
<Skeleton className="h-8 w-16 rounded-md" />
|
|
154
|
+
</div>
|
|
155
|
+
) : lists.hostedApps.length > 0 ? (
|
|
156
|
+
<div
|
|
157
|
+
data-state={showHostedApps ? "shown" : "hidden"}
|
|
158
|
+
className="divide-y divide-border/60 transition-opacity data-[state=hidden]:opacity-50"
|
|
159
|
+
>
|
|
160
|
+
{lists.hostedApps.map((app) => (
|
|
161
|
+
<SettingsRow
|
|
162
|
+
key={app.id}
|
|
163
|
+
id={`hosted-app-${app.id}`}
|
|
164
|
+
label={app.name}
|
|
165
|
+
description={hostOf(app.homeUrl?.trim() || app.url)}
|
|
166
|
+
control={
|
|
167
|
+
<Button variant="outline" size="sm" asChild>
|
|
168
|
+
<a
|
|
169
|
+
href={app.homeUrl?.trim() || app.url}
|
|
170
|
+
target="_blank"
|
|
171
|
+
rel="noopener noreferrer"
|
|
172
|
+
aria-label={t("dispatch.pages.chatFirstOpenApp", {
|
|
173
|
+
name: app.name,
|
|
174
|
+
})}
|
|
175
|
+
>
|
|
176
|
+
{t("dispatch.pages.openApp")}
|
|
177
|
+
<IconArrowUpRight size={13} aria-hidden="true" />
|
|
178
|
+
</a>
|
|
179
|
+
</Button>
|
|
180
|
+
}
|
|
181
|
+
/>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
) : null}
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</SettingsGroup>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -7,6 +7,7 @@ import { MemoryRouter, useLocation, useNavigate } from "react-router";
|
|
|
7
7
|
import { toast } from "sonner";
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
9
|
|
|
10
|
+
import { resetWorkspaceAppLayoutStoreForTests } from "../../lib/workspace-app-layout";
|
|
10
11
|
import { CHAT_FIRST_PANE_STATE_KEY } from "../../shared/chat-first-pane";
|
|
11
12
|
import { AdminShell } from "../admin-navigation";
|
|
12
13
|
import { Tooltip, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
@@ -43,6 +44,7 @@ const clientState = vi.hoisted(() => ({
|
|
|
43
44
|
connectedApps: [] as Array<Record<string, unknown>>,
|
|
44
45
|
connectedAppsError: null as Error | null,
|
|
45
46
|
connectedAppsLoading: false,
|
|
47
|
+
railRenders: 0,
|
|
46
48
|
appState: {} as Record<string, unknown>,
|
|
47
49
|
appStatePromises: {} as Record<string, Promise<unknown>>,
|
|
48
50
|
basePath: "",
|
|
@@ -143,6 +145,15 @@ vi.mock("@agent-native/toolkit/app/chat/AgentSidebar", () => ({
|
|
|
143
145
|
vi.mock("../deferred-chat-components.js", async (importOriginal) => ({
|
|
144
146
|
...(await importOriginal<typeof import("../deferred-chat-components.js")>()),
|
|
145
147
|
AgentChatSurface: () => null,
|
|
148
|
+
ChatFirstAppsRail: await (async () => {
|
|
149
|
+
const { createElement, memo } = await import("react");
|
|
150
|
+
const { ChatFirstAppsRail: Rail } =
|
|
151
|
+
await import("../../../../toolkit/src/app/chat/chat-first/apps-rail.tsx");
|
|
152
|
+
return memo(function CountedRail(props: Parameters<typeof Rail>[0]) {
|
|
153
|
+
clientState.railRenders += 1;
|
|
154
|
+
return createElement(Rail, props);
|
|
155
|
+
});
|
|
156
|
+
})(),
|
|
146
157
|
ChatFirstChatHistory: (
|
|
147
158
|
await import("../../../../toolkit/src/app/chat/chat-first/chat-history.tsx")
|
|
148
159
|
).ChatFirstChatHistory,
|
|
@@ -222,47 +233,51 @@ vi.mock("@agent-native/core/client/i18n", () => ({
|
|
|
222
233
|
DEFAULT_LOCALE: "en-US",
|
|
223
234
|
useOptionalLocale: () => ({ locale: "en-US" }),
|
|
224
235
|
useFormatters: () => ({ formatNumber: (value: number) => String(value) }),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"dispatch.nav.chat": "Chat",
|
|
228
|
-
"dispatch.nav.overview": "Overview",
|
|
229
|
-
"dispatch.nav.apps": "Apps",
|
|
230
|
-
"dispatch.nav.agents": "Agents",
|
|
231
|
-
"dispatch.pages.workspaceApps": "Workspace apps",
|
|
232
|
-
"dispatch.nav.operate": "Operate",
|
|
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.onlyYou": "Only you",
|
|
241
|
-
"agentChat.history.hide": "Hide from my list",
|
|
242
|
-
"agentChat.history.showAll": "Show all",
|
|
243
|
-
"agentChat.history.showLess": "Show less",
|
|
244
|
-
"agentChat.history.loadMore": "Load more",
|
|
245
|
-
"agentChat.history.loadingMore": "Loading...",
|
|
246
|
-
"agentChat.history.emptyMine": "No chats yet",
|
|
247
|
-
"dispatch.sidebar.chats": "Chats",
|
|
248
|
-
"dispatch.sidebar.showLocalChats": "Show local chats",
|
|
249
|
-
"dispatch.sidebar.showAllChats": "Show all chats",
|
|
250
|
-
"agentChat.contextXray.pin": "Pin",
|
|
251
|
-
"agentChat.contextXray.unpin": "Unpin",
|
|
252
|
-
"extensions.delete": "Archive",
|
|
253
|
-
"dispatch.integrations.failedToSave": "Failed to save",
|
|
254
|
-
"dispatch.sidebar.newChat": "New chat",
|
|
255
|
-
"dispatch.sidebar.newDispatchChat": "New Dispatch chat",
|
|
256
|
-
"dispatch.sidebar.renameChat": "Rename chat",
|
|
257
|
-
"dispatch.sidebar.chatOptions": `Options for ${values?.title ?? ""}`,
|
|
258
|
-
"dispatch.sidebar.renameThread": `Rename ${values?.title ?? ""}`,
|
|
259
|
-
"sidebar.collapseSidebar": "Collapse sidebar",
|
|
260
|
-
"sidebar.expandSidebar": "Expand sidebar",
|
|
261
|
-
};
|
|
262
|
-
return messages[key] ?? String(values?.defaultValue ?? key);
|
|
263
|
-
},
|
|
236
|
+
// Stable like the real useT, so memoized copy stays memoized.
|
|
237
|
+
useT: () => translate,
|
|
264
238
|
}));
|
|
265
239
|
|
|
240
|
+
function translate(key: string, values?: Record<string, unknown>) {
|
|
241
|
+
const messages: Record<string, string> = {
|
|
242
|
+
"dispatch.nav.chat": "Chat",
|
|
243
|
+
"dispatch.nav.overview": "Overview",
|
|
244
|
+
"dispatch.nav.apps": "Apps",
|
|
245
|
+
"dispatch.nav.agents": "Agents",
|
|
246
|
+
"dispatch.pages.workspaceApps": "Workspace apps",
|
|
247
|
+
"dispatch.nav.operate": "Operate",
|
|
248
|
+
"dispatch.nav.advanced": "Advanced",
|
|
249
|
+
"agentChat.history.mine": "My chats",
|
|
250
|
+
"agentChat.history.sharedWithYou": "Shared with you",
|
|
251
|
+
"agentChat.history.ownedBy": `Owner: ${values?.name ?? ""}`,
|
|
252
|
+
"agentChat.history.accessDirect": "Shared with you",
|
|
253
|
+
"agentChat.history.accessOrganization": "Shared with your organization",
|
|
254
|
+
"agentChat.history.copyLink": "Copy link",
|
|
255
|
+
"agentChat.history.onlyYou": "Only you",
|
|
256
|
+
"agentChat.history.hide": "Hide from my list",
|
|
257
|
+
"agentChat.history.showAll": "Show all",
|
|
258
|
+
"agentChat.history.showLess": "Show less",
|
|
259
|
+
"agentChat.history.loadMore": "Load more",
|
|
260
|
+
"agentChat.history.loadingMore": "Loading...",
|
|
261
|
+
"agentChat.history.emptyMine": "No chats yet",
|
|
262
|
+
"dispatch.sidebar.chats": "Chats",
|
|
263
|
+
"dispatch.sidebar.showLocalChats": "Show local chats",
|
|
264
|
+
"dispatch.sidebar.showAllChats": "Show all chats",
|
|
265
|
+
"agentChat.contextXray.pin": "Pin",
|
|
266
|
+
"agentChat.contextXray.unpin": "Unpin",
|
|
267
|
+
"extensions.delete": "Archive",
|
|
268
|
+
"dispatch.integrations.failedToSave": "Failed to save",
|
|
269
|
+
"dispatch.sidebar.newChat": "New chat",
|
|
270
|
+
"dispatch.sidebar.newDispatchChat": "New Dispatch chat",
|
|
271
|
+
"dispatch.sidebar.renameChat": "Rename chat",
|
|
272
|
+
"dispatch.sidebar.chatOptions": `Options for ${values?.title ?? ""}`,
|
|
273
|
+
"dispatch.sidebar.renameThread": `Rename ${values?.title ?? ""}`,
|
|
274
|
+
"agentChat.chatFirst.showMoreCount": `${values?.count ?? ""} more apps`,
|
|
275
|
+
"sidebar.collapseSidebar": "Collapse sidebar",
|
|
276
|
+
"sidebar.expandSidebar": "Expand sidebar",
|
|
277
|
+
};
|
|
278
|
+
return messages[key] ?? String(values?.defaultValue ?? key);
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
vi.mock("@agent-native/toolkit/app/shared", async (importOriginal) => ({
|
|
267
282
|
...(await importOriginal<
|
|
268
283
|
typeof import("@agent-native/toolkit/app/shared")
|
|
@@ -447,6 +462,7 @@ describe("Dispatch NavContent", () => {
|
|
|
447
462
|
clientState.connectedAppsLoading = false;
|
|
448
463
|
clientState.appState = {};
|
|
449
464
|
clientState.appStatePromises = {};
|
|
465
|
+
resetWorkspaceAppLayoutStoreForTests();
|
|
450
466
|
workspaceAppNavigation.navigateToWorkspaceApp.mockClear();
|
|
451
467
|
container = document.createElement("div");
|
|
452
468
|
document.body.appendChild(container);
|
|
@@ -595,6 +611,46 @@ describe("Dispatch NavContent", () => {
|
|
|
595
611
|
).not.toBeNull();
|
|
596
612
|
});
|
|
597
613
|
|
|
614
|
+
it("keeps hosted apps the organization hides out of the rail but openable", async () => {
|
|
615
|
+
clientState.workspaceApps = [
|
|
616
|
+
{ id: "notes", name: "Notes", path: "/notes", status: "ready" },
|
|
617
|
+
];
|
|
618
|
+
clientState.connectedApps = [
|
|
619
|
+
{
|
|
620
|
+
id: "clips",
|
|
621
|
+
name: "Clips",
|
|
622
|
+
url: "https://clips.agent-native.com",
|
|
623
|
+
source: "builtin",
|
|
624
|
+
hiddenFromLaunchers: true,
|
|
625
|
+
},
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
await act(async () => {
|
|
629
|
+
root.render(
|
|
630
|
+
<MemoryRouter initialEntries={["/chat/hosted-hidden"]}>
|
|
631
|
+
<Layout extensions={{ chatFirst: true }}>
|
|
632
|
+
<div />
|
|
633
|
+
</Layout>
|
|
634
|
+
</MemoryRouter>,
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(
|
|
639
|
+
container.querySelector('[data-chat-first-app][data-app-id="notes"]'),
|
|
640
|
+
).not.toBeNull();
|
|
641
|
+
expect(
|
|
642
|
+
container.querySelector('[data-chat-first-app][data-app-id="clips"]'),
|
|
643
|
+
).toBeNull();
|
|
644
|
+
|
|
645
|
+
await act(async () => {
|
|
646
|
+
emitChatFirstOpenApp({ app: "clips" });
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(
|
|
650
|
+
container.querySelector("[data-chat-first-app-pane]"),
|
|
651
|
+
).not.toBeNull();
|
|
652
|
+
});
|
|
653
|
+
|
|
598
654
|
it("retries an app open after connected built-in discovery recovers", async () => {
|
|
599
655
|
clientState.workspaceApps = [
|
|
600
656
|
{
|
|
@@ -646,6 +702,77 @@ describe("Dispatch NavContent", () => {
|
|
|
646
702
|
).not.toBeNull();
|
|
647
703
|
});
|
|
648
704
|
|
|
705
|
+
it("keeps saved pins when the rail pins an app before workspace state loads", async () => {
|
|
706
|
+
clientState.workspaceApps = ["alpha", "beta"].map((id) => ({
|
|
707
|
+
id,
|
|
708
|
+
name: id,
|
|
709
|
+
path: `/${id}`,
|
|
710
|
+
url: `https://${id}.example.com`,
|
|
711
|
+
status: "ready",
|
|
712
|
+
}));
|
|
713
|
+
let resolveLayout: (value: unknown) => void = () => {};
|
|
714
|
+
clientState.appStatePromises["chat-first-app-layout"] = new Promise(
|
|
715
|
+
(resolve) => {
|
|
716
|
+
resolveLayout = resolve;
|
|
717
|
+
},
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
await act(async () => {
|
|
721
|
+
root.render(
|
|
722
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
723
|
+
<Layout extensions={{ chatFirst: true }}>
|
|
724
|
+
<div />
|
|
725
|
+
</Layout>
|
|
726
|
+
</MemoryRouter>,
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
const pinAlpha = container.querySelector<HTMLButtonElement>(
|
|
730
|
+
'[data-chat-first-app][data-app-id="alpha"] button[aria-pressed]',
|
|
731
|
+
);
|
|
732
|
+
expect(pinAlpha).not.toBeNull();
|
|
733
|
+
await act(async () => pinAlpha!.click());
|
|
734
|
+
await act(async () => {
|
|
735
|
+
resolveLayout({ pinnedIds: ["beta"], orderedIds: [] });
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
expect(clientState.appState["chat-first-app-layout"]).toEqual({
|
|
739
|
+
pinnedIds: ["alpha", "beta"],
|
|
740
|
+
orderedIds: [],
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("loads saved pins for the rail on non-chat routes", async () => {
|
|
745
|
+
clientState.workspaceApps = ["alpha", "beta"].map((id) => ({
|
|
746
|
+
id,
|
|
747
|
+
name: id,
|
|
748
|
+
path: `/${id}`,
|
|
749
|
+
url: `https://${id}.example.com`,
|
|
750
|
+
status: "ready",
|
|
751
|
+
}));
|
|
752
|
+
clientState.appState["chat-first-app-layout"] = {
|
|
753
|
+
pinnedIds: ["beta"],
|
|
754
|
+
orderedIds: [],
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
await act(async () => {
|
|
758
|
+
root.render(
|
|
759
|
+
<MemoryRouter initialEntries={["/admin/automations"]}>
|
|
760
|
+
<Layout extensions={{ chatFirst: true }}>
|
|
761
|
+
<div />
|
|
762
|
+
</Layout>
|
|
763
|
+
</MemoryRouter>,
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
expect(
|
|
768
|
+
container
|
|
769
|
+
.querySelector(
|
|
770
|
+
'[data-chat-first-app][data-app-id="beta"] button[aria-pressed]',
|
|
771
|
+
)
|
|
772
|
+
?.getAttribute("aria-pressed"),
|
|
773
|
+
).toBe("true");
|
|
774
|
+
});
|
|
775
|
+
|
|
649
776
|
it("keeps hidden Dispatch registered for agent app targets", async () => {
|
|
650
777
|
clientState.workspaceApps = [
|
|
651
778
|
{
|
|
@@ -671,7 +798,7 @@ describe("Dispatch NavContent", () => {
|
|
|
671
798
|
emitChatFirstOpenApp({ app: "dispatch" });
|
|
672
799
|
});
|
|
673
800
|
|
|
674
|
-
expect(container.querySelector("
|
|
801
|
+
expect(container.querySelector("[data-app-notice]")).toBeNull();
|
|
675
802
|
});
|
|
676
803
|
|
|
677
804
|
it("does not restore a deferred pane after switching chats", async () => {
|
|
@@ -1168,15 +1295,19 @@ describe("Dispatch NavContent", () => {
|
|
|
1168
1295
|
);
|
|
1169
1296
|
});
|
|
1170
1297
|
|
|
1171
|
-
expect(container.querySelectorAll("[data-chat-first-app]")).toHaveLength(
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
expect(
|
|
1298
|
+
expect(container.querySelectorAll("[data-chat-first-app]")).toHaveLength(3);
|
|
1299
|
+
expect(
|
|
1300
|
+
container.querySelector('[data-chat-first-app-section="apps"]')
|
|
1301
|
+
?.textContent,
|
|
1302
|
+
).toContain("Apps");
|
|
1303
|
+
expect(
|
|
1304
|
+
container.querySelector('[data-chat-first-app][draggable="true"]'),
|
|
1305
|
+
).toBeNull();
|
|
1306
|
+
expect(
|
|
1307
|
+
container.querySelector(
|
|
1308
|
+
"[data-chat-first-apps-rail] button[aria-expanded]",
|
|
1309
|
+
),
|
|
1310
|
+
).toBeNull();
|
|
1180
1311
|
|
|
1181
1312
|
const allApps = container.querySelector<HTMLButtonElement>(
|
|
1182
1313
|
"[data-chat-first-all-apps]",
|
|
@@ -1188,6 +1319,147 @@ describe("Dispatch NavContent", () => {
|
|
|
1188
1319
|
expect(paths.at(-1)).toBe("/apps");
|
|
1189
1320
|
});
|
|
1190
1321
|
|
|
1322
|
+
describe.each([false, true])(
|
|
1323
|
+
"the chat-first rail's apps (collapsed %s)",
|
|
1324
|
+
(collapsed) => {
|
|
1325
|
+
const workspace = ["zeta", "eta", "beta", "delta", "alpha"].map((id) => ({
|
|
1326
|
+
id,
|
|
1327
|
+
name: id[0]!.toUpperCase() + id.slice(1),
|
|
1328
|
+
}));
|
|
1329
|
+
const hosted = ["mail", "clips", "slides", "design"].map((id) => ({
|
|
1330
|
+
id,
|
|
1331
|
+
name: id,
|
|
1332
|
+
source: "builtin" as const,
|
|
1333
|
+
}));
|
|
1334
|
+
async function renderRail(
|
|
1335
|
+
apps: Array<{ id: string; name: string; source?: "builtin" }>,
|
|
1336
|
+
pinnedIds: string[],
|
|
1337
|
+
activeAppId?: string,
|
|
1338
|
+
) {
|
|
1339
|
+
await act(async () => {
|
|
1340
|
+
root.render(
|
|
1341
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
1342
|
+
<TooltipProvider>
|
|
1343
|
+
<NavContent
|
|
1344
|
+
chatFirstMode
|
|
1345
|
+
collapsed={collapsed}
|
|
1346
|
+
chatFirstApps={apps}
|
|
1347
|
+
chatFirstAppLayout={{ pinnedIds, orderedIds: [] }}
|
|
1348
|
+
{...(activeAppId
|
|
1349
|
+
? { chatFirstActiveAppId: activeAppId }
|
|
1350
|
+
: {})}
|
|
1351
|
+
/>
|
|
1352
|
+
</TooltipProvider>
|
|
1353
|
+
</MemoryRouter>,
|
|
1354
|
+
);
|
|
1355
|
+
});
|
|
1356
|
+
const sections = [
|
|
1357
|
+
...container.querySelectorAll<HTMLElement>(
|
|
1358
|
+
"[data-chat-first-app-section]",
|
|
1359
|
+
),
|
|
1360
|
+
].map((section) => section.dataset.chatFirstAppSection);
|
|
1361
|
+
return {
|
|
1362
|
+
sections,
|
|
1363
|
+
ids: [
|
|
1364
|
+
...container.querySelectorAll<HTMLElement>("[data-chat-first-app]"),
|
|
1365
|
+
].map((row) => row.dataset.appId),
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
it("shows only pins, in pin order, once something is pinned", async () => {
|
|
1370
|
+
const rail = await renderRail(
|
|
1371
|
+
[...workspace, ...hosted],
|
|
1372
|
+
["clips", "beta"],
|
|
1373
|
+
);
|
|
1374
|
+
expect(rail.ids).toEqual(["clips", "beta"]);
|
|
1375
|
+
if (!collapsed) {
|
|
1376
|
+
expect(rail.sections).toEqual(["pinned"]);
|
|
1377
|
+
expect(
|
|
1378
|
+
container
|
|
1379
|
+
.querySelector("[data-chat-first-app-section]")
|
|
1380
|
+
?.getAttribute("aria-label"),
|
|
1381
|
+
).toBe("dispatch.pages.pinnedApps");
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it("suggests the first three workspace apps A to Z with no pins", async () => {
|
|
1386
|
+
const rail = await renderRail([...workspace, ...hosted], []);
|
|
1387
|
+
expect(rail.ids).toEqual(["alpha", "beta", "delta"]);
|
|
1388
|
+
if (!collapsed) expect(rail.sections).toEqual(["apps"]);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
it("suggests hosted apps when the workspace has none", async () => {
|
|
1392
|
+
const rail = await renderRail(hosted, []);
|
|
1393
|
+
expect(rail.ids).toEqual(["clips", "design", "mail"]);
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
it("adds the open app when it is not shown", async () => {
|
|
1397
|
+
const rail = await renderRail(
|
|
1398
|
+
[...workspace, ...hosted],
|
|
1399
|
+
["clips"],
|
|
1400
|
+
"zeta",
|
|
1401
|
+
);
|
|
1402
|
+
expect(rail.ids).toEqual(["clips", "zeta"]);
|
|
1403
|
+
});
|
|
1404
|
+
},
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
it.each([
|
|
1408
|
+
["loading", false],
|
|
1409
|
+
["error", false],
|
|
1410
|
+
["ready", true],
|
|
1411
|
+
] as const)(
|
|
1412
|
+
"says there are no apps only once the list arrived (%s)",
|
|
1413
|
+
async (status, saysEmpty) => {
|
|
1414
|
+
await act(async () => {
|
|
1415
|
+
root.render(
|
|
1416
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
1417
|
+
<TooltipProvider>
|
|
1418
|
+
<NavContent
|
|
1419
|
+
chatFirstMode
|
|
1420
|
+
chatFirstApps={[]}
|
|
1421
|
+
chatFirstAppsStatus={status}
|
|
1422
|
+
/>
|
|
1423
|
+
</TooltipProvider>
|
|
1424
|
+
</MemoryRouter>,
|
|
1425
|
+
);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
const appsText =
|
|
1429
|
+
container.querySelector('[data-chat-first-app-section="apps"]')
|
|
1430
|
+
?.textContent ?? "";
|
|
1431
|
+
if (saysEmpty) expect(appsText).toContain("No apps yet");
|
|
1432
|
+
else expect(appsText).not.toContain("No apps yet");
|
|
1433
|
+
},
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
it("does not re-render the apps rail when the sidebar re-renders with the same apps", async () => {
|
|
1437
|
+
const apps = [
|
|
1438
|
+
{ id: "alpha", name: "Alpha" },
|
|
1439
|
+
{ id: "mail", name: "Mail", source: "builtin" as const },
|
|
1440
|
+
];
|
|
1441
|
+
const layout = { pinnedIds: [], orderedIds: [] };
|
|
1442
|
+
const renderNav = () =>
|
|
1443
|
+
root.render(
|
|
1444
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
1445
|
+
<TooltipProvider>
|
|
1446
|
+
<NavContent
|
|
1447
|
+
chatFirstMode
|
|
1448
|
+
chatFirstApps={apps}
|
|
1449
|
+
chatFirstAppLayout={layout}
|
|
1450
|
+
/>
|
|
1451
|
+
</TooltipProvider>
|
|
1452
|
+
</MemoryRouter>,
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
await act(async () => renderNav());
|
|
1456
|
+
const rendersAfterMount = clientState.railRenders;
|
|
1457
|
+
expect(rendersAfterMount).toBeGreaterThan(0);
|
|
1458
|
+
await act(async () => renderNav());
|
|
1459
|
+
|
|
1460
|
+
expect(clientState.railRenders).toBe(rendersAfterMount);
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1191
1463
|
it("omits the Chats section when chat-first has no chats", async () => {
|
|
1192
1464
|
clientState.threads = [];
|
|
1193
1465
|
await act(async () => {
|
|
@@ -1409,6 +1681,41 @@ describe("Dispatch NavContent", () => {
|
|
|
1409
1681
|
},
|
|
1410
1682
|
);
|
|
1411
1683
|
|
|
1684
|
+
it("lists chats above the apps rail", async () => {
|
|
1685
|
+
await act(async () => {
|
|
1686
|
+
root.render(
|
|
1687
|
+
<MemoryRouter initialEntries={["/chat/active-thread"]}>
|
|
1688
|
+
<TooltipProvider>
|
|
1689
|
+
<NavContent
|
|
1690
|
+
chatFirstMode
|
|
1691
|
+
chatFirstApps={[{ id: "alpha", name: "Alpha" }]}
|
|
1692
|
+
/>
|
|
1693
|
+
</TooltipProvider>
|
|
1694
|
+
</MemoryRouter>,
|
|
1695
|
+
);
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
const chat = [...container.querySelectorAll("button, a")].find((node) =>
|
|
1699
|
+
node.textContent?.includes("Current Dispatch work"),
|
|
1700
|
+
);
|
|
1701
|
+
const rail = container.querySelector("[data-chat-first-apps-rail]");
|
|
1702
|
+
expect(chat).toBeDefined();
|
|
1703
|
+
expect(rail).not.toBeNull();
|
|
1704
|
+
expect(
|
|
1705
|
+
chat!.compareDocumentPosition(rail!) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1706
|
+
).toBeTruthy();
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it("puts the chat source toggle in the first chat section header", async () => {
|
|
1710
|
+
await renderHistory(true);
|
|
1711
|
+
|
|
1712
|
+
const toggles = container.querySelectorAll(
|
|
1713
|
+
"[data-dispatch-chat-source-toggle]",
|
|
1714
|
+
);
|
|
1715
|
+
expect(toggles).toHaveLength(1);
|
|
1716
|
+
expect(toggles[0]!.parentElement!.textContent).toBe("My chats");
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1412
1719
|
it.each([false, true])(
|
|
1413
1720
|
"passes the persisted external source toggle to both ownership queries (chat-first %s)",
|
|
1414
1721
|
async (chatFirstMode) => {
|