@agent-native/dispatch 0.27.0 → 0.27.2
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/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/components/app-keys-popover.d.ts.map +1 -1
- package/dist/components/app-keys-popover.js +16 -9
- package/dist/components/app-keys-popover.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +7 -1
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +14 -10
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +40 -9
- package/dist/components/workspace-app-host.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +6 -2
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/server/lib/vault-store.d.ts.map +1 -1
- package/dist/server/lib/vault-store.js +1 -1
- package/dist/server/lib/vault-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/app-keys-popover.spec.tsx +60 -2
- package/src/components/app-keys-popover.tsx +23 -10
- package/src/components/create-app-popover.spec.tsx +20 -0
- package/src/components/create-app-popover.tsx +7 -1
- package/src/components/layout/Layout.spec.tsx +27 -0
- package/src/components/layout/Layout.tsx +35 -30
- package/src/components/workspace-app-host.spec.tsx +140 -28
- package/src/components/workspace-app-host.tsx +59 -9
- package/src/lib/workspace-apps.spec.ts +51 -0
- package/src/lib/workspace-apps.ts +7 -2
- package/src/server/lib/vault-store.spec.ts +5 -3
- package/src/server/lib/vault-store.ts +5 -2
|
@@ -216,6 +216,26 @@ describe("CreateAppFlow", () => {
|
|
|
216
216
|
).toBe(false);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
it("reuses an empty local chat for direct dev-mode app creation", async () => {
|
|
220
|
+
devState.isDevMode = true;
|
|
221
|
+
|
|
222
|
+
await renderAndSubmit("Build a quality dashboard");
|
|
223
|
+
|
|
224
|
+
expect(sendToAgentChatMock).toHaveBeenCalledWith(
|
|
225
|
+
expect.objectContaining({
|
|
226
|
+
submit: true,
|
|
227
|
+
type: "code",
|
|
228
|
+
newTab: true,
|
|
229
|
+
reuseEmptyTab: true,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
expect(
|
|
233
|
+
fetchSpy.mock.calls.some(([input]) =>
|
|
234
|
+
String(input).includes("start-workspace-app-creation"),
|
|
235
|
+
),
|
|
236
|
+
).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
219
239
|
it("opens a fresh local chat when the server hands off app creation", async () => {
|
|
220
240
|
startWorkspaceAppCreationResponse.result = {
|
|
221
241
|
mode: "local-agent",
|
|
@@ -253,7 +253,13 @@ export function CreateAppFlow({
|
|
|
253
253
|
setStatusMessage("Sent to Builder chat.");
|
|
254
254
|
onClose?.();
|
|
255
255
|
} else if (isDevMode) {
|
|
256
|
-
sendToAgentChat({
|
|
256
|
+
sendToAgentChat({
|
|
257
|
+
message,
|
|
258
|
+
submit: true,
|
|
259
|
+
type: "code",
|
|
260
|
+
newTab: true,
|
|
261
|
+
reuseEmptyTab: true,
|
|
262
|
+
});
|
|
257
263
|
setStatusMessage("Sent to the local agent.");
|
|
258
264
|
onClose?.();
|
|
259
265
|
} else {
|
|
@@ -220,6 +220,33 @@ describe("Dispatch NavContent", () => {
|
|
|
220
220
|
expect(lists[0].querySelector("a")?.className).toContain("size-9");
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
it("keeps chat-first primary actions in the collapsed sidebar", async () => {
|
|
224
|
+
await act(async () => {
|
|
225
|
+
root.render(
|
|
226
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
227
|
+
<TooltipProvider>
|
|
228
|
+
<NavContent
|
|
229
|
+
chatFirstMode
|
|
230
|
+
collapsed
|
|
231
|
+
chatFirstApps={[{ id: "mail", name: "Mail" }]}
|
|
232
|
+
/>
|
|
233
|
+
</TooltipProvider>
|
|
234
|
+
</MemoryRouter>,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
for (const label of ["New chat", "Integrations", "Search"]) {
|
|
239
|
+
expect(
|
|
240
|
+
[...container.querySelectorAll("button")].find(
|
|
241
|
+
(button) => button.textContent?.trim() === label,
|
|
242
|
+
),
|
|
243
|
+
).toBeDefined();
|
|
244
|
+
}
|
|
245
|
+
expect(
|
|
246
|
+
container.querySelector("[data-chat-first-apps-rail]"),
|
|
247
|
+
).not.toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
223
250
|
it("keeps management routes out of the primary navigation", async () => {
|
|
224
251
|
await act(async () => {
|
|
225
252
|
root.render(
|
|
@@ -569,6 +569,7 @@ function DispatchChatsSection({
|
|
|
569
569
|
prelude,
|
|
570
570
|
chatFirstMode = false,
|
|
571
571
|
chatFirstEmbedded = false,
|
|
572
|
+
collapsed = false,
|
|
572
573
|
chatFirstNavigation,
|
|
573
574
|
}: {
|
|
574
575
|
onNavigate?: () => void;
|
|
@@ -576,6 +577,7 @@ function DispatchChatsSection({
|
|
|
576
577
|
prelude?: ReactNode;
|
|
577
578
|
chatFirstMode?: boolean;
|
|
578
579
|
chatFirstEmbedded?: boolean;
|
|
580
|
+
collapsed?: boolean;
|
|
579
581
|
chatFirstNavigation?: {
|
|
580
582
|
activeTab?: ChatFirstPrimaryTab;
|
|
581
583
|
onNewChat?: () => void;
|
|
@@ -722,6 +724,8 @@ function DispatchChatsSection({
|
|
|
722
724
|
if (threadId) openThread(threadId, { isNew: true });
|
|
723
725
|
}
|
|
724
726
|
|
|
727
|
+
const collapsedChatFirst = collapsed && chatFirstMode;
|
|
728
|
+
|
|
725
729
|
return (
|
|
726
730
|
<div
|
|
727
731
|
className={cn(
|
|
@@ -730,7 +734,7 @@ function DispatchChatsSection({
|
|
|
730
734
|
)}
|
|
731
735
|
>
|
|
732
736
|
{showNewChat && chatFirstNavigation ? (
|
|
733
|
-
<nav className="space-y-0.5
|
|
737
|
+
<nav className={cn("space-y-0.5 py-2", collapsed ? "px-1.5" : "px-2")}>
|
|
734
738
|
<ChatFirstPrimaryNavigation
|
|
735
739
|
copy={chatFirstCopy}
|
|
736
740
|
onNewChat={() => {
|
|
@@ -741,11 +745,12 @@ function DispatchChatsSection({
|
|
|
741
745
|
onOpenScheduled={chatFirstNavigation.onOpenScheduled}
|
|
742
746
|
onSearch={openCommandMenu}
|
|
743
747
|
activeTab={chatFirstNavigation.activeTab}
|
|
748
|
+
collapsed={collapsed}
|
|
744
749
|
/>
|
|
745
750
|
</nav>
|
|
746
751
|
) : null}
|
|
747
752
|
{prelude}
|
|
748
|
-
{!chatFirstMode ? (
|
|
753
|
+
{!collapsedChatFirst && !chatFirstMode ? (
|
|
749
754
|
<div className="flex justify-end px-2 pt-0.5">
|
|
750
755
|
<Tooltip>
|
|
751
756
|
<TooltipTrigger asChild>
|
|
@@ -784,7 +789,8 @@ function DispatchChatsSection({
|
|
|
784
789
|
</Tooltip>
|
|
785
790
|
</div>
|
|
786
791
|
) : null}
|
|
787
|
-
{!
|
|
792
|
+
{!collapsedChatFirst &&
|
|
793
|
+
!chatFirstMode &&
|
|
788
794
|
chatsLoading &&
|
|
789
795
|
visibleThreads.length === 0 &&
|
|
790
796
|
Array.from({ length: 3 }).map((_, index) => (
|
|
@@ -796,7 +802,9 @@ function DispatchChatsSection({
|
|
|
796
802
|
<Skeleton className="h-3 w-3/4 rounded" />
|
|
797
803
|
</div>
|
|
798
804
|
))}
|
|
799
|
-
{
|
|
805
|
+
{!collapsedChatFirst &&
|
|
806
|
+
chatFirstMode &&
|
|
807
|
+
(chatsLoading || visibleThreads.length > 0) ? (
|
|
800
808
|
<ChatFirstChatHistory
|
|
801
809
|
items={chatItems}
|
|
802
810
|
activeId={displayedActiveThreadId}
|
|
@@ -870,7 +878,7 @@ function DispatchChatsSection({
|
|
|
870
878
|
)}
|
|
871
879
|
className="min-w-0 px-2"
|
|
872
880
|
/>
|
|
873
|
-
) : (
|
|
881
|
+
) : !collapsedChatFirst ? (
|
|
874
882
|
<ChatHistoryRail
|
|
875
883
|
items={chatItems}
|
|
876
884
|
activeId={displayedActiveThreadId}
|
|
@@ -938,7 +946,7 @@ function DispatchChatsSection({
|
|
|
938
946
|
)}
|
|
939
947
|
className="min-w-0 px-2"
|
|
940
948
|
/>
|
|
941
|
-
)}
|
|
949
|
+
) : null}
|
|
942
950
|
</div>
|
|
943
951
|
);
|
|
944
952
|
}
|
|
@@ -1273,29 +1281,26 @@ export function NavContent({
|
|
|
1273
1281
|
|
|
1274
1282
|
{chatFirstMode ? (
|
|
1275
1283
|
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
) : (
|
|
1297
|
-
chatFirstAppsRail
|
|
1298
|
-
)}
|
|
1284
|
+
<DispatchChatsSection
|
|
1285
|
+
onNavigate={onNavigate}
|
|
1286
|
+
showNewChat
|
|
1287
|
+
chatFirstMode
|
|
1288
|
+
chatFirstEmbedded={chatFirstEmbedded}
|
|
1289
|
+
collapsed={collapsed}
|
|
1290
|
+
chatFirstNavigation={{
|
|
1291
|
+
activeTab: chatFirstActivePrimaryTab,
|
|
1292
|
+
onNewChat: onChatFirstNewChat,
|
|
1293
|
+
onOpenIntegrations: () => {
|
|
1294
|
+
navigate(dispatchNavLinkTarget("/admin/integrations"));
|
|
1295
|
+
onNavigate?.();
|
|
1296
|
+
},
|
|
1297
|
+
onOpenScheduled: () => {
|
|
1298
|
+
navigate(dispatchNavLinkTarget("/admin/automations"));
|
|
1299
|
+
onNavigate?.();
|
|
1300
|
+
},
|
|
1301
|
+
}}
|
|
1302
|
+
prelude={chatFirstAppsRail}
|
|
1303
|
+
/>
|
|
1299
1304
|
</div>
|
|
1300
1305
|
) : null}
|
|
1301
1306
|
<div
|
|
@@ -2374,7 +2379,7 @@ export function Layout({
|
|
|
2374
2379
|
const content = isChatRoute ? (
|
|
2375
2380
|
<div
|
|
2376
2381
|
className={cn(
|
|
2377
|
-
"agent-layout-main-surface flex min-w-0 flex-1 overflow-hidden",
|
|
2382
|
+
"agent-layout-main-surface flex h-full min-w-0 flex-1 overflow-hidden",
|
|
2378
2383
|
chatFirstMode && "dispatch-chat-first-surface",
|
|
2379
2384
|
)}
|
|
2380
2385
|
>
|
|
@@ -11,10 +11,22 @@ const clientState = vi.hoisted(() => {
|
|
|
11
11
|
const workspaceSsoMutateAsync = vi.fn().mockImplementation(async () => ({
|
|
12
12
|
startUrl: "about:blank",
|
|
13
13
|
}));
|
|
14
|
+
const legacyErrorMutateAsync = vi.fn();
|
|
14
15
|
const actionNames: string[] = [];
|
|
15
16
|
return {
|
|
16
17
|
actionNames,
|
|
18
|
+
grantedApps: [
|
|
19
|
+
{
|
|
20
|
+
id: "analytics.agent-native.com",
|
|
21
|
+
name: "Analytics",
|
|
22
|
+
url: "https://analytics.agent-native.com",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
17
25
|
legacyMutateAsync,
|
|
26
|
+
legacyMutateError: null as Error | null,
|
|
27
|
+
legacyErrorMutateAsync,
|
|
28
|
+
frameLoadHandler: null as (() => void) | null,
|
|
29
|
+
suppressFrameLoad: false,
|
|
18
30
|
theme: "dark" as "dark" | "light",
|
|
19
31
|
workspaceSsoEnabled: false,
|
|
20
32
|
workspaceSsoMutateAsync,
|
|
@@ -22,20 +34,43 @@ const clientState = vi.hoisted(() => {
|
|
|
22
34
|
});
|
|
23
35
|
|
|
24
36
|
vi.mock("@agent-native/core/client/chat-first", () => ({
|
|
37
|
+
CHAT_FIRST_DEFAULT_APP_IDS: [
|
|
38
|
+
"content",
|
|
39
|
+
"design",
|
|
40
|
+
"mail",
|
|
41
|
+
"calendar",
|
|
42
|
+
"clips",
|
|
43
|
+
],
|
|
25
44
|
ChatFirstAppPane: ({
|
|
26
45
|
app,
|
|
27
46
|
embedUrl,
|
|
47
|
+
errorMessage,
|
|
28
48
|
renderEmbed,
|
|
29
49
|
status,
|
|
30
50
|
}: {
|
|
31
51
|
app: { name: string } | null;
|
|
32
52
|
embedUrl?: string | null;
|
|
53
|
+
errorMessage?: string;
|
|
33
54
|
renderEmbed: (target: { url: string; title?: string }) => React.ReactNode;
|
|
34
55
|
status: string;
|
|
35
56
|
}) => (
|
|
36
|
-
<div
|
|
57
|
+
<div
|
|
58
|
+
data-chat-first-app-error={errorMessage ?? ""}
|
|
59
|
+
data-chat-first-app-status={status}
|
|
60
|
+
>
|
|
37
61
|
{status === "ready" && embedUrl
|
|
38
|
-
?
|
|
62
|
+
? (() => {
|
|
63
|
+
const frame = renderEmbed({ url: embedUrl, title: app?.name });
|
|
64
|
+
if (clientState.suppressFrameLoad && React.isValidElement(frame)) {
|
|
65
|
+
clientState.frameLoadHandler =
|
|
66
|
+
(frame.props as { onLoad?: () => void }).onLoad ?? null;
|
|
67
|
+
return React.cloneElement(frame, {
|
|
68
|
+
onLoad: undefined,
|
|
69
|
+
src: undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return frame;
|
|
73
|
+
})()
|
|
39
74
|
: null}
|
|
40
75
|
</div>
|
|
41
76
|
),
|
|
@@ -53,34 +88,45 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
53
88
|
mutateAsync:
|
|
54
89
|
name === "create-workspace-app-embed-session"
|
|
55
90
|
? clientState.workspaceSsoMutateAsync
|
|
56
|
-
: clientState.
|
|
91
|
+
: clientState.legacyMutateError
|
|
92
|
+
? clientState.legacyErrorMutateAsync
|
|
93
|
+
: clientState.legacyMutateAsync,
|
|
57
94
|
};
|
|
58
95
|
},
|
|
59
|
-
useActionQuery: () => ({
|
|
60
|
-
data:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
useActionQuery: (name: string) => ({
|
|
97
|
+
data:
|
|
98
|
+
name === "list_apps"
|
|
99
|
+
? { apps: clientState.grantedApps }
|
|
100
|
+
: [
|
|
101
|
+
{
|
|
102
|
+
id: "mail",
|
|
103
|
+
name: "Mail",
|
|
104
|
+
path: "/mail",
|
|
105
|
+
url: null,
|
|
106
|
+
status: "ready",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "calendar",
|
|
110
|
+
name: "Calendar",
|
|
111
|
+
path: "/calendar",
|
|
112
|
+
url: null,
|
|
113
|
+
status: "ready",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "documents",
|
|
117
|
+
name: "Documents",
|
|
118
|
+
path: "/documents",
|
|
119
|
+
url: null,
|
|
120
|
+
status: "ready",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "settings",
|
|
124
|
+
name: "Settings",
|
|
125
|
+
path: "/settings",
|
|
126
|
+
url: null,
|
|
127
|
+
status: "ready",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
84
130
|
isError: false,
|
|
85
131
|
isLoading: false,
|
|
86
132
|
refetch: vi.fn(),
|
|
@@ -108,6 +154,10 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
108
154
|
root = createRoot(container);
|
|
109
155
|
clientState.actionNames.length = 0;
|
|
110
156
|
clientState.legacyMutateAsync.mockClear();
|
|
157
|
+
clientState.legacyMutateError = null;
|
|
158
|
+
clientState.legacyErrorMutateAsync.mockReset();
|
|
159
|
+
clientState.frameLoadHandler = null;
|
|
160
|
+
clientState.suppressFrameLoad = false;
|
|
111
161
|
clientState.workspaceSsoMutateAsync.mockClear();
|
|
112
162
|
clientState.theme = "dark";
|
|
113
163
|
clientState.workspaceSsoEnabled = false;
|
|
@@ -150,6 +200,30 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
150
200
|
expect(container.querySelectorAll("iframe")).toHaveLength(2);
|
|
151
201
|
});
|
|
152
202
|
|
|
203
|
+
it("resolves a granted external app instead of showing app not found", async () => {
|
|
204
|
+
await act(async () => {
|
|
205
|
+
root.render(
|
|
206
|
+
<WorkspaceAppKeepAlive activeAppId="analytics.agent-native.com" />,
|
|
207
|
+
);
|
|
208
|
+
await Promise.resolve();
|
|
209
|
+
await Promise.resolve();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(
|
|
213
|
+
container.querySelector(
|
|
214
|
+
'[data-dispatch-workspace-app-cache-entry="analytics.agent-native.com"]',
|
|
215
|
+
),
|
|
216
|
+
).not.toBeNull();
|
|
217
|
+
expect(
|
|
218
|
+
container.querySelector('[data-chat-first-app-status="ready"]'),
|
|
219
|
+
).not.toBeNull();
|
|
220
|
+
expect(clientState.legacyMutateAsync).toHaveBeenCalledWith({
|
|
221
|
+
app: "analytics.agent-native.com",
|
|
222
|
+
url: "https://analytics.agent-native.com",
|
|
223
|
+
chrome: "minimal",
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
153
227
|
it("uses the app-scoped workspace session action when the rollout is enabled", async () => {
|
|
154
228
|
clientState.workspaceSsoEnabled = true;
|
|
155
229
|
|
|
@@ -220,6 +294,44 @@ describe("WorkspaceAppKeepAlive", () => {
|
|
|
220
294
|
);
|
|
221
295
|
});
|
|
222
296
|
|
|
297
|
+
it("clears the embed error after a direct fallback iframe loads", async () => {
|
|
298
|
+
clientState.legacyMutateError = new Error("Restore request failed");
|
|
299
|
+
clientState.legacyErrorMutateAsync.mockRejectedValue(
|
|
300
|
+
clientState.legacyMutateError,
|
|
301
|
+
);
|
|
302
|
+
clientState.suppressFrameLoad = true;
|
|
303
|
+
|
|
304
|
+
await act(async () => {
|
|
305
|
+
root.render(
|
|
306
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
307
|
+
);
|
|
308
|
+
await Promise.resolve();
|
|
309
|
+
await Promise.resolve();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const iframe = container.querySelector<HTMLIFrameElement>("iframe");
|
|
313
|
+
expect(iframe).not.toBeNull();
|
|
314
|
+
expect(
|
|
315
|
+
container
|
|
316
|
+
.querySelector("[data-chat-first-app-error]")
|
|
317
|
+
?.getAttribute("data-chat-first-app-error"),
|
|
318
|
+
).toBe("Restore request failed");
|
|
319
|
+
|
|
320
|
+
if (!iframe) throw new Error("Workspace app iframe was not rendered");
|
|
321
|
+
expect(clientState.frameLoadHandler).not.toBeNull();
|
|
322
|
+
|
|
323
|
+
await act(async () => {
|
|
324
|
+
clientState.frameLoadHandler?.();
|
|
325
|
+
await Promise.resolve();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(
|
|
329
|
+
container
|
|
330
|
+
.querySelector("[data-chat-first-app-error]")
|
|
331
|
+
?.getAttribute("data-chat-first-app-error"),
|
|
332
|
+
).toBe("");
|
|
333
|
+
});
|
|
334
|
+
|
|
223
335
|
it("evicts the oldest inactive app after reaching the keep-alive limit", async () => {
|
|
224
336
|
await act(async () => {
|
|
225
337
|
root.render(<WorkspaceAppKeepAlive activeAppId="mail" />);
|
|
@@ -23,6 +23,7 @@ import { Link } from "react-router";
|
|
|
23
23
|
|
|
24
24
|
import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
|
|
25
25
|
import {
|
|
26
|
+
mergeChatFirstWorkspaceApps,
|
|
26
27
|
workspaceAppDirectHref,
|
|
27
28
|
workspaceAppEmbedTarget,
|
|
28
29
|
workspaceAppHref,
|
|
@@ -45,6 +46,16 @@ interface EmbedSessionInput {
|
|
|
45
46
|
chrome: "minimal";
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
interface GrantedWorkspaceAppSummary {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
url?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface GrantedWorkspaceAppsResult {
|
|
56
|
+
apps: GrantedWorkspaceAppSummary[];
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
type WorkspaceAppTheme = "light" | "dark";
|
|
49
60
|
type WorkspaceAppAuthState = "unknown" | "authenticated" | "unauthenticated";
|
|
50
61
|
|
|
@@ -118,6 +129,7 @@ export function WorkspaceAppFrame({
|
|
|
118
129
|
: "light";
|
|
119
130
|
const [embedUrl, setEmbedUrl] = useState<string | null>(null);
|
|
120
131
|
const [embedError, setEmbedError] = useState<Error | null>(null);
|
|
132
|
+
const [isDirectFallback, setIsDirectFallback] = useState(false);
|
|
121
133
|
const [embedAttempt, setEmbedAttempt] = useState(0);
|
|
122
134
|
const [authState, setAuthState] = useState<WorkspaceAppAuthState>("unknown");
|
|
123
135
|
const embedFrameRef = useRef<HTMLIFrameElement>(null);
|
|
@@ -129,6 +141,7 @@ export function WorkspaceAppFrame({
|
|
|
129
141
|
}, [theme]);
|
|
130
142
|
const handleFrameLoad = useCallback(() => {
|
|
131
143
|
postThemeToFrame();
|
|
144
|
+
if (isDirectFallback) setEmbedError(null);
|
|
132
145
|
const frame = embedFrameRef.current;
|
|
133
146
|
let frameUrl = embedUrl;
|
|
134
147
|
try {
|
|
@@ -139,7 +152,7 @@ export function WorkspaceAppFrame({
|
|
|
139
152
|
}
|
|
140
153
|
const nextAuthState = resolveWorkspaceAppAuthState(frameUrl);
|
|
141
154
|
if (nextAuthState !== "unknown") setAuthState(nextAuthState);
|
|
142
|
-
}, [embedUrl, postThemeToFrame]);
|
|
155
|
+
}, [embedUrl, isDirectFallback, postThemeToFrame]);
|
|
143
156
|
const workspaceSsoEnabled = useFeatureFlag(DISPATCH_WORKSPACE_SSO_FLAG.key);
|
|
144
157
|
const createEmbedSession = useActionMutation<
|
|
145
158
|
EmbedSessionResult,
|
|
@@ -176,6 +189,7 @@ export function WorkspaceAppFrame({
|
|
|
176
189
|
let cancelled = false;
|
|
177
190
|
setEmbedUrl(null);
|
|
178
191
|
setEmbedError(null);
|
|
192
|
+
setIsDirectFallback(false);
|
|
179
193
|
setAuthState("unknown");
|
|
180
194
|
const createSession = workspaceSsoEnabled
|
|
181
195
|
? createWorkspaceSsoEmbedSession
|
|
@@ -187,6 +201,7 @@ export function WorkspaceAppFrame({
|
|
|
187
201
|
})
|
|
188
202
|
.catch((cause: unknown) => {
|
|
189
203
|
if (cancelled) return;
|
|
204
|
+
setIsDirectFallback(true);
|
|
190
205
|
setEmbedUrl(
|
|
191
206
|
workspaceAppDirectHref(
|
|
192
207
|
{ path: app.path ?? "", url: app.url },
|
|
@@ -326,23 +341,58 @@ export function WorkspaceAppFrame({
|
|
|
326
341
|
|
|
327
342
|
export function WorkspaceAppHost({ appId }: { appId?: string }) {
|
|
328
343
|
const t = useT();
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
const workspaceAppsQuery = useActionQuery<WorkspaceAppSummary[]>(
|
|
345
|
+
"list-workspace-apps",
|
|
346
|
+
{ includeAgentCards: false },
|
|
347
|
+
);
|
|
348
|
+
const grantedAppsQuery = useActionQuery<GrantedWorkspaceAppsResult>(
|
|
349
|
+
"list_apps",
|
|
350
|
+
{},
|
|
351
|
+
);
|
|
352
|
+
const apps = useMemo(() => {
|
|
353
|
+
const merged = new Map<string, WorkspaceAppSummary>();
|
|
354
|
+
|
|
355
|
+
for (const app of mergeChatFirstWorkspaceApps(workspaceAppsQuery.data)) {
|
|
356
|
+
merged.set(app.id.trim().toLowerCase(), app);
|
|
357
|
+
}
|
|
358
|
+
for (const app of grantedAppsQuery.data?.apps ?? []) {
|
|
359
|
+
const id = app.id.trim();
|
|
360
|
+
if (!id || merged.has(id.toLowerCase())) continue;
|
|
361
|
+
merged.set(id.toLowerCase(), {
|
|
362
|
+
id,
|
|
363
|
+
name: app.name.trim() || id,
|
|
364
|
+
path: "",
|
|
365
|
+
url: app.url?.trim() || null,
|
|
366
|
+
status: "ready",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return [...merged.values()];
|
|
371
|
+
}, [grantedAppsQuery.data?.apps, workspaceAppsQuery.data]);
|
|
333
372
|
const app = useMemo(
|
|
334
373
|
() =>
|
|
335
|
-
|
|
374
|
+
apps.find(
|
|
375
|
+
(item) => item.id.trim().toLowerCase() === appId?.trim().toLowerCase(),
|
|
376
|
+
) ?? null,
|
|
336
377
|
[appId, apps],
|
|
337
378
|
);
|
|
379
|
+
const isLoading = workspaceAppsQuery.isLoading || grantedAppsQuery.isLoading;
|
|
380
|
+
const queryError = workspaceAppsQuery.isError
|
|
381
|
+
? workspaceAppsQuery.error
|
|
382
|
+
: grantedAppsQuery.isError
|
|
383
|
+
? grantedAppsQuery.error
|
|
384
|
+
: null;
|
|
338
385
|
|
|
339
|
-
if (
|
|
386
|
+
if (queryError && !app) {
|
|
340
387
|
return (
|
|
341
388
|
<div className="flex h-full min-h-0 items-center justify-center p-6">
|
|
342
389
|
<div className="w-full max-w-2xl">
|
|
343
390
|
<ActionQueryError
|
|
344
|
-
error={
|
|
345
|
-
onRetry={() =>
|
|
391
|
+
error={queryError}
|
|
392
|
+
onRetry={() => {
|
|
393
|
+
void workspaceAppsQuery.refetch();
|
|
394
|
+
void grantedAppsQuery.refetch();
|
|
395
|
+
}}
|
|
346
396
|
/>
|
|
347
397
|
</div>
|
|
348
398
|
</div>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isDefaultWorkspaceAppHiddenId,
|
|
5
5
|
isDispatchWorkspaceAppId,
|
|
6
6
|
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
7
|
+
mergeChatFirstWorkspaceApps,
|
|
7
8
|
workspaceAppIdFromRoute,
|
|
8
9
|
workspaceAppRoute,
|
|
9
10
|
} from "./workspace-apps";
|
|
@@ -41,4 +42,54 @@ describe("workspace app routes", () => {
|
|
|
41
42
|
).toBe(false);
|
|
42
43
|
expect(isWorkspaceAppVisibleInDefaultLaunchers({ id: "mail" })).toBe(true);
|
|
43
44
|
});
|
|
45
|
+
|
|
46
|
+
it("maps default first-party apps to their canonical hosted origins", () => {
|
|
47
|
+
const apps = mergeChatFirstWorkspaceApps(undefined);
|
|
48
|
+
expect(apps).toEqual(
|
|
49
|
+
expect.arrayContaining([
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
id: "content",
|
|
52
|
+
path: "/",
|
|
53
|
+
url: "https://content.agent-native.com",
|
|
54
|
+
}),
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
id: "design",
|
|
57
|
+
path: "/",
|
|
58
|
+
url: "https://design.agent-native.com",
|
|
59
|
+
}),
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
id: "mail",
|
|
62
|
+
path: "/",
|
|
63
|
+
url: "https://mail.agent-native.com",
|
|
64
|
+
}),
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
id: "calendar",
|
|
67
|
+
path: "/",
|
|
68
|
+
url: "https://calendar.agent-native.com",
|
|
69
|
+
}),
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
id: "clips",
|
|
72
|
+
path: "/",
|
|
73
|
+
url: "https://clips.agent-native.com",
|
|
74
|
+
}),
|
|
75
|
+
]),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("lets a mounted workspace app override a default row", () => {
|
|
80
|
+
const apps = mergeChatFirstWorkspaceApps([
|
|
81
|
+
{
|
|
82
|
+
id: "mail",
|
|
83
|
+
name: "Internal Mail",
|
|
84
|
+
path: "/internal-mail",
|
|
85
|
+
url: null,
|
|
86
|
+
status: "ready",
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
expect(apps.find((app) => app.id === "mail")).toMatchObject({
|
|
90
|
+
name: "Internal Mail",
|
|
91
|
+
path: "/internal-mail",
|
|
92
|
+
url: null,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
44
95
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { CHAT_FIRST_DEFAULT_APP_IDS } from "@agent-native/core/client/chat-first";
|
|
2
2
|
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
3
3
|
|
|
4
|
+
import { CANONICAL_WORKSPACE_SSO_APP_ORIGINS } from "../shared/workspace-sso";
|
|
5
|
+
|
|
4
6
|
export interface WorkspaceAppSummary {
|
|
5
7
|
id: string;
|
|
6
8
|
name: string;
|
|
@@ -169,8 +171,11 @@ export function mergeChatFirstWorkspaceApps(
|
|
|
169
171
|
merged.set(id, {
|
|
170
172
|
id,
|
|
171
173
|
name: id.charAt(0).toUpperCase() + id.slice(1),
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
// The five default rows are hosted sibling apps, not routes owned by
|
|
175
|
+
// Dispatch. Keep a mounted path for legacy callers, but give embed
|
|
176
|
+
// session resolution the exact canonical origin.
|
|
177
|
+
path: "/",
|
|
178
|
+
url: CANONICAL_WORKSPACE_SSO_APP_ORIGINS[id],
|
|
174
179
|
status: "ready",
|
|
175
180
|
});
|
|
176
181
|
}
|
|
@@ -102,9 +102,11 @@ describe("vault authorization", () => {
|
|
|
102
102
|
execute: vi.fn().mockResolvedValue({ rows: [{ role: "member" }] }),
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
await expect(assertCanManageVault()).rejects.
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
await expect(assertCanManageVault()).rejects.toMatchObject({
|
|
106
|
+
message:
|
|
107
|
+
"Only organization owners and admins can manage the workspace vault.",
|
|
108
|
+
statusCode: 403,
|
|
109
|
+
});
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
it("rejects organization members before listing or reading grants", async () => {
|