@agent-native/dispatch 0.24.6 → 0.25.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/README.md +12 -1
- package/dist/actions/connect-external-agent.d.ts +18 -0
- package/dist/actions/connect-external-agent.d.ts.map +1 -0
- package/dist/actions/connect-external-agent.js +68 -0
- package/dist/actions/connect-external-agent.js.map +1 -0
- package/dist/actions/create-workspace-resource.d.ts +14 -14
- package/dist/actions/create-workspace-resource.js +9 -2
- package/dist/actions/create-workspace-resource.js.map +1 -1
- package/dist/actions/create_workspace_app_embed_session.d.ts +13 -0
- package/dist/actions/create_workspace_app_embed_session.d.ts.map +1 -0
- package/dist/actions/create_workspace_app_embed_session.js +30 -0
- package/dist/actions/create_workspace_app_embed_session.js.map +1 -0
- package/dist/actions/import-agent-pack.d.ts +96 -0
- package/dist/actions/import-agent-pack.d.ts.map +1 -0
- package/dist/actions/import-agent-pack.js +116 -0
- package/dist/actions/import-agent-pack.js.map +1 -0
- package/dist/actions/import-agent.d.ts +60 -0
- package/dist/actions/import-agent.d.ts.map +1 -0
- package/dist/actions/import-agent.js +82 -0
- package/dist/actions/import-agent.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +10 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-pack.d.ts +36 -0
- package/dist/actions/list-agent-pack.d.ts.map +1 -0
- package/dist/actions/list-agent-pack.js +32 -0
- package/dist/actions/list-agent-pack.js.map +1 -0
- package/dist/actions/list-workspace-resource-options.d.ts +1 -1
- package/dist/actions/list-workspace-resource-options.js +8 -1
- package/dist/actions/list-workspace-resource-options.js.map +1 -1
- package/dist/actions/list-workspace-resources.d.ts +1 -1
- package/dist/actions/list-workspace-resources.js +8 -1
- package/dist/actions/list-workspace-resources.js.map +1 -1
- package/dist/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +10 -10
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/actions/view-screen.js +18 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -8
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +33 -106
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/simple-agents-panel.d.ts +9 -0
- package/dist/components/simple-agents-panel.d.ts.map +1 -0
- package/dist/components/simple-agents-panel.js +405 -0
- package/dist/components/simple-agents-panel.js.map +1 -0
- package/dist/components/workspace-app-host.d.ts +29 -0
- package/dist/components/workspace-app-host.d.ts.map +1 -0
- package/dist/components/workspace-app-host.js +145 -0
- package/dist/components/workspace-app-host.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -4
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +25 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/schema.js +1 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +1 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +10 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/agent-pack.d.ts +31 -0
- package/dist/lib/agent-pack.d.ts.map +1 -0
- package/dist/lib/agent-pack.js +149 -0
- package/dist/lib/agent-pack.js.map +1 -0
- package/dist/lib/simple-agent-profile.d.ts +36 -0
- package/dist/lib/simple-agent-profile.d.ts.map +1 -0
- package/dist/lib/simple-agent-profile.js +183 -0
- package/dist/lib/simple-agent-profile.js.map +1 -0
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +11 -1
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +4 -95
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +62 -10
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/agent-pack-store.d.ts +24 -0
- package/dist/server/lib/agent-pack-store.d.ts.map +1 -0
- package/dist/server/lib/agent-pack-store.js +22 -0
- package/dist/server/lib/agent-pack-store.js.map +1 -0
- package/dist/server/lib/dispatch-store.d.ts.map +1 -1
- package/dist/server/lib/dispatch-store.js +4 -0
- package/dist/server/lib/dispatch-store.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts +11 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +155 -9
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/dist/server/lib/workspace-resources-store.d.ts +15 -1
- package/dist/server/lib/workspace-resources-store.d.ts.map +1 -1
- package/dist/server/lib/workspace-resources-store.js +11 -2
- package/dist/server/lib/workspace-resources-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +13 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/shared/feature-flags.d.ts +7 -0
- package/dist/shared/feature-flags.d.ts.map +1 -0
- package/dist/shared/feature-flags.js +12 -0
- package/dist/shared/feature-flags.js.map +1 -0
- package/dist/shared/workspace-sso.d.ts +36 -0
- package/dist/shared/workspace-sso.d.ts.map +1 -0
- package/dist/shared/workspace-sso.js +99 -0
- package/dist/shared/workspace-sso.js.map +1 -0
- package/package.json +5 -3
- package/src/actions/connect-external-agent.ts +92 -0
- package/src/actions/create-workspace-resource.ts +9 -2
- package/src/actions/create_workspace_app_embed_session.ts +32 -0
- package/src/actions/import-agent-pack.ts +146 -0
- package/src/actions/import-agent.ts +102 -0
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +10 -0
- package/src/actions/list-agent-pack.ts +43 -0
- package/src/actions/list-workspace-resource-options.ts +8 -1
- package/src/actions/list-workspace-resources.ts +8 -1
- package/src/actions/navigate.ts +1 -1
- package/src/actions/view-screen.spec.ts +29 -1
- package/src/actions/view-screen.ts +19 -1
- package/src/components/app-icon.spec.tsx +10 -0
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.spec.tsx +2 -0
- package/src/components/layout/Layout.tsx +59 -171
- package/src/components/simple-agents-panel.spec.tsx +73 -0
- package/src/components/simple-agents-panel.tsx +1226 -0
- package/src/components/workspace-app-host.spec.tsx +204 -0
- package/src/components/workspace-app-host.tsx +359 -0
- package/src/db/migrations.spec.ts +5 -1
- package/src/db/migrations.ts +28 -1
- package/src/db/schema.ts +1 -1
- package/src/hooks/use-navigation-state.spec.ts +25 -0
- package/src/hooks/use-navigation-state.ts +11 -0
- package/src/lib/agent-pack.spec.ts +47 -0
- package/src/lib/agent-pack.ts +226 -0
- package/src/lib/simple-agent-profile.spec.ts +77 -0
- package/src/lib/simple-agent-profile.ts +247 -0
- package/src/routes/index.spec.ts +1 -0
- package/src/routes/pages/agents.tsx +21 -1
- package/src/routes/pages/apps.$appId.tsx +3 -240
- package/src/routes/pages/chat.spec.tsx +62 -0
- package/src/routes/pages/chat.tsx +117 -9
- package/src/server/lib/agent-pack-store.ts +35 -0
- package/src/server/lib/dispatch-store.ts +8 -0
- package/src/server/lib/mcp-gateway.spec.ts +141 -0
- package/src/server/lib/mcp-gateway.ts +227 -15
- package/src/server/lib/workspace-resources-store.ts +9 -2
- package/src/server/plugins/agent-chat.ts +13 -0
- package/src/shared/feature-flags.ts +13 -0
- package/src/shared/workspace-sso.ts +118 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
const clientState = vi.hoisted(() => {
|
|
8
|
+
const legacyMutateAsync = vi.fn().mockImplementation(async () => ({
|
|
9
|
+
startUrl: "about:blank",
|
|
10
|
+
}));
|
|
11
|
+
const workspaceSsoMutateAsync = vi.fn().mockImplementation(async () => ({
|
|
12
|
+
startUrl: "about:blank",
|
|
13
|
+
}));
|
|
14
|
+
const actionNames: string[] = [];
|
|
15
|
+
return {
|
|
16
|
+
actionNames,
|
|
17
|
+
legacyMutateAsync,
|
|
18
|
+
workspaceSsoEnabled: false,
|
|
19
|
+
workspaceSsoMutateAsync,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock("@agent-native/core/client/chat-first", () => ({
|
|
24
|
+
ChatFirstAppPane: ({
|
|
25
|
+
app,
|
|
26
|
+
embedUrl,
|
|
27
|
+
renderEmbed,
|
|
28
|
+
status,
|
|
29
|
+
}: {
|
|
30
|
+
app: { name: string } | null;
|
|
31
|
+
embedUrl?: string | null;
|
|
32
|
+
renderEmbed: (target: { url: string; title?: string }) => React.ReactNode;
|
|
33
|
+
status: string;
|
|
34
|
+
}) => (
|
|
35
|
+
<div data-chat-first-app-status={status}>
|
|
36
|
+
{status === "ready" && embedUrl
|
|
37
|
+
? renderEmbed({ url: embedUrl, title: app?.name })
|
|
38
|
+
: null}
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
defaultChatFirstCopy: (key: string) => key,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
45
|
+
useFeatureFlag: () => clientState.workspaceSsoEnabled,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
49
|
+
useActionMutation: (name: string) => {
|
|
50
|
+
clientState.actionNames.push(name);
|
|
51
|
+
return {
|
|
52
|
+
mutateAsync:
|
|
53
|
+
name === "create-workspace-app-embed-session"
|
|
54
|
+
? clientState.workspaceSsoMutateAsync
|
|
55
|
+
: clientState.legacyMutateAsync,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
useActionQuery: () => ({
|
|
59
|
+
data: [
|
|
60
|
+
{ id: "mail", name: "Mail", path: "/mail", url: null, status: "ready" },
|
|
61
|
+
{
|
|
62
|
+
id: "calendar",
|
|
63
|
+
name: "Calendar",
|
|
64
|
+
path: "/calendar",
|
|
65
|
+
url: null,
|
|
66
|
+
status: "ready",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "documents",
|
|
70
|
+
name: "Documents",
|
|
71
|
+
path: "/documents",
|
|
72
|
+
url: null,
|
|
73
|
+
status: "ready",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "settings",
|
|
77
|
+
name: "Settings",
|
|
78
|
+
path: "/settings",
|
|
79
|
+
url: null,
|
|
80
|
+
status: "ready",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
isError: false,
|
|
84
|
+
isLoading: false,
|
|
85
|
+
refetch: vi.fn(),
|
|
86
|
+
}),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
90
|
+
useT: () => (key: string) => key,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
import { WorkspaceAppFrame, WorkspaceAppKeepAlive } from "./workspace-app-host";
|
|
94
|
+
|
|
95
|
+
describe("WorkspaceAppKeepAlive", () => {
|
|
96
|
+
let container: HTMLDivElement;
|
|
97
|
+
let root: Root;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
101
|
+
container = document.createElement("div");
|
|
102
|
+
document.body.appendChild(container);
|
|
103
|
+
root = createRoot(container);
|
|
104
|
+
clientState.actionNames.length = 0;
|
|
105
|
+
clientState.legacyMutateAsync.mockClear();
|
|
106
|
+
clientState.workspaceSsoMutateAsync.mockClear();
|
|
107
|
+
clientState.workspaceSsoEnabled = false;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
act(() => root.unmount());
|
|
112
|
+
container.remove();
|
|
113
|
+
vi.unstubAllGlobals();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("keeps visited app frames in the DOM while hiding inactive apps", async () => {
|
|
117
|
+
await act(async () => {
|
|
118
|
+
root.render(<WorkspaceAppKeepAlive activeAppId="mail" />);
|
|
119
|
+
await Promise.resolve();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await act(async () => {
|
|
123
|
+
await Promise.resolve();
|
|
124
|
+
await Promise.resolve();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await act(async () => {
|
|
128
|
+
root.render(<WorkspaceAppKeepAlive activeAppId="calendar" />);
|
|
129
|
+
await Promise.resolve();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const mailEntry = container.querySelector<HTMLElement>(
|
|
133
|
+
'[data-dispatch-workspace-app-cache-entry="mail"]',
|
|
134
|
+
);
|
|
135
|
+
const calendarEntry = container.querySelector<HTMLElement>(
|
|
136
|
+
'[data-dispatch-workspace-app-cache-entry="calendar"]',
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(mailEntry).not.toBeNull();
|
|
140
|
+
expect(mailEntry?.classList.contains("hidden")).toBe(true);
|
|
141
|
+
expect(mailEntry?.querySelector("iframe")).not.toBeNull();
|
|
142
|
+
expect(calendarEntry?.classList.contains("hidden")).toBe(false);
|
|
143
|
+
expect(calendarEntry?.querySelector("iframe")).not.toBeNull();
|
|
144
|
+
expect(container.querySelectorAll("iframe")).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("uses the app-scoped workspace session action when the rollout is enabled", async () => {
|
|
148
|
+
clientState.workspaceSsoEnabled = true;
|
|
149
|
+
|
|
150
|
+
await act(async () => {
|
|
151
|
+
root.render(
|
|
152
|
+
<WorkspaceAppFrame app={{ id: "mail", name: "Mail", path: "/mail" }} />,
|
|
153
|
+
);
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
await Promise.resolve();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(clientState.workspaceSsoMutateAsync).toHaveBeenCalledWith({
|
|
159
|
+
app: "mail",
|
|
160
|
+
path: "/mail",
|
|
161
|
+
chrome: "minimal",
|
|
162
|
+
});
|
|
163
|
+
expect(clientState.legacyMutateAsync).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("evicts the oldest inactive app after reaching the keep-alive limit", async () => {
|
|
167
|
+
await act(async () => {
|
|
168
|
+
root.render(<WorkspaceAppKeepAlive activeAppId="mail" />);
|
|
169
|
+
await Promise.resolve();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
for (const activeAppId of ["calendar", "documents", "settings"]) {
|
|
173
|
+
await act(async () => {
|
|
174
|
+
root.render(<WorkspaceAppKeepAlive activeAppId={activeAppId} />);
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
expect(
|
|
180
|
+
container.querySelector(
|
|
181
|
+
'[data-dispatch-workspace-app-cache-entry="mail"]',
|
|
182
|
+
),
|
|
183
|
+
).toBeNull();
|
|
184
|
+
expect(
|
|
185
|
+
container.querySelector(
|
|
186
|
+
'[data-dispatch-workspace-app-cache-entry="calendar"]',
|
|
187
|
+
),
|
|
188
|
+
).not.toBeNull();
|
|
189
|
+
expect(
|
|
190
|
+
container.querySelector(
|
|
191
|
+
'[data-dispatch-workspace-app-cache-entry="documents"]',
|
|
192
|
+
),
|
|
193
|
+
).not.toBeNull();
|
|
194
|
+
expect(
|
|
195
|
+
container.querySelector(
|
|
196
|
+
'[data-dispatch-workspace-app-cache-entry="settings"]',
|
|
197
|
+
),
|
|
198
|
+
).not.toBeNull();
|
|
199
|
+
expect(
|
|
200
|
+
container.querySelectorAll("[data-dispatch-workspace-app-cache-entry]"),
|
|
201
|
+
).toHaveLength(3);
|
|
202
|
+
expect(container.querySelectorAll("iframe")).toHaveLength(3);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChatFirstAppPane,
|
|
3
|
+
defaultChatFirstCopy,
|
|
4
|
+
type ChatFirstCopy,
|
|
5
|
+
} from "@agent-native/core/client/chat-first";
|
|
6
|
+
import { useFeatureFlag } from "@agent-native/core/client/feature-flags";
|
|
7
|
+
import {
|
|
8
|
+
useActionMutation,
|
|
9
|
+
useActionQuery,
|
|
10
|
+
} from "@agent-native/core/client/hooks";
|
|
11
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
12
|
+
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
13
|
+
import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
|
|
14
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { Link } from "react-router";
|
|
16
|
+
|
|
17
|
+
import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
|
|
18
|
+
import {
|
|
19
|
+
workspaceAppDirectHref,
|
|
20
|
+
workspaceAppEmbedTarget,
|
|
21
|
+
workspaceAppHref,
|
|
22
|
+
type WorkspaceAppSummary,
|
|
23
|
+
} from "../lib/workspace-apps";
|
|
24
|
+
import { DISPATCH_WORKSPACE_SSO_FLAG } from "../shared/feature-flags";
|
|
25
|
+
import { ActionQueryError } from "./action-query-error";
|
|
26
|
+
import { Badge } from "./ui/badge";
|
|
27
|
+
import { Button } from "./ui/button";
|
|
28
|
+
import { Skeleton } from "./ui/skeleton";
|
|
29
|
+
|
|
30
|
+
interface EmbedSessionResult {
|
|
31
|
+
startUrl: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface EmbedSessionInput {
|
|
35
|
+
app?: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
chrome: "minimal";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildChatFirstEmbedSessionInput(
|
|
42
|
+
appId: string,
|
|
43
|
+
path: string,
|
|
44
|
+
): EmbedSessionInput {
|
|
45
|
+
return { app: appId, path, chrome: "minimal" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WorkspaceAppFrameApp {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
path?: string | null;
|
|
52
|
+
url?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface WorkspaceAppFrameProps {
|
|
56
|
+
app: WorkspaceAppFrameApp;
|
|
57
|
+
/** Chat-first app tabs use their own route while standalone hosts use app metadata. */
|
|
58
|
+
embedPath?: string;
|
|
59
|
+
copy?: ChatFirstCopy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function WorkspaceAppFrame({
|
|
63
|
+
app,
|
|
64
|
+
embedPath,
|
|
65
|
+
copy = defaultChatFirstCopy,
|
|
66
|
+
}: WorkspaceAppFrameProps) {
|
|
67
|
+
const [embedUrl, setEmbedUrl] = useState<string | null>(null);
|
|
68
|
+
const [embedError, setEmbedError] = useState<Error | null>(null);
|
|
69
|
+
const [embedAttempt, setEmbedAttempt] = useState(0);
|
|
70
|
+
const embedFrameRef = useRef<HTMLIFrameElement>(null);
|
|
71
|
+
const workspaceSsoEnabled = useFeatureFlag(DISPATCH_WORKSPACE_SSO_FLAG.key);
|
|
72
|
+
const createEmbedSession = useActionMutation<
|
|
73
|
+
EmbedSessionResult,
|
|
74
|
+
EmbedSessionInput
|
|
75
|
+
>("create_embed_session", {
|
|
76
|
+
skipActionQueryInvalidation: true,
|
|
77
|
+
});
|
|
78
|
+
const createWorkspaceSsoEmbedSession = useActionMutation<
|
|
79
|
+
EmbedSessionResult,
|
|
80
|
+
EmbedSessionInput
|
|
81
|
+
>("create-workspace-app-embed-session", {
|
|
82
|
+
skipActionQueryInvalidation: true,
|
|
83
|
+
});
|
|
84
|
+
const appHref = workspaceAppHref({
|
|
85
|
+
id: app.id,
|
|
86
|
+
name: app.name,
|
|
87
|
+
path: app.path ?? "",
|
|
88
|
+
url: app.url,
|
|
89
|
+
});
|
|
90
|
+
const embedInput = useMemo<EmbedSessionInput | null>(() => {
|
|
91
|
+
if (embedPath !== undefined) {
|
|
92
|
+
return buildChatFirstEmbedSessionInput(app.id, embedPath);
|
|
93
|
+
}
|
|
94
|
+
if (!appHref) return null;
|
|
95
|
+
return {
|
|
96
|
+
app: app.id,
|
|
97
|
+
...workspaceAppEmbedTarget({ path: app.path ?? "", url: app.url }),
|
|
98
|
+
chrome: "minimal",
|
|
99
|
+
};
|
|
100
|
+
}, [app.id, app.path, app.url, appHref, embedPath]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!embedInput) return;
|
|
104
|
+
let cancelled = false;
|
|
105
|
+
setEmbedUrl(null);
|
|
106
|
+
setEmbedError(null);
|
|
107
|
+
const createSession = workspaceSsoEnabled
|
|
108
|
+
? createWorkspaceSsoEmbedSession
|
|
109
|
+
: createEmbedSession;
|
|
110
|
+
void createSession
|
|
111
|
+
.mutateAsync(embedInput)
|
|
112
|
+
.then((result) => {
|
|
113
|
+
if (!cancelled) setEmbedUrl(result.startUrl);
|
|
114
|
+
})
|
|
115
|
+
.catch((cause: unknown) => {
|
|
116
|
+
if (cancelled) return;
|
|
117
|
+
setEmbedUrl(
|
|
118
|
+
workspaceAppDirectHref(
|
|
119
|
+
{ path: app.path ?? "", url: app.url },
|
|
120
|
+
embedPath ?? "/",
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
setEmbedError(
|
|
124
|
+
cause instanceof Error ? cause : new Error(String(cause)),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true;
|
|
129
|
+
};
|
|
130
|
+
}, [
|
|
131
|
+
app.id,
|
|
132
|
+
app.path,
|
|
133
|
+
app.url,
|
|
134
|
+
createEmbedSession.mutateAsync,
|
|
135
|
+
createWorkspaceSsoEmbedSession.mutateAsync,
|
|
136
|
+
embedInput,
|
|
137
|
+
embedPath,
|
|
138
|
+
embedAttempt,
|
|
139
|
+
workspaceSsoEnabled,
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const handleEmbedSessionExpired = (event: MessageEvent) => {
|
|
144
|
+
if (
|
|
145
|
+
!isEmbedSessionExpiredMessage(event, embedFrameRef.current, embedUrl)
|
|
146
|
+
) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
setEmbedAttempt((attempt) => attempt + 1);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
window.addEventListener("message", handleEmbedSessionExpired);
|
|
153
|
+
return () =>
|
|
154
|
+
window.removeEventListener("message", handleEmbedSessionExpired);
|
|
155
|
+
}, [embedUrl]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ChatFirstAppPane
|
|
159
|
+
app={app}
|
|
160
|
+
status={
|
|
161
|
+
embedUrl
|
|
162
|
+
? "ready"
|
|
163
|
+
: embedError
|
|
164
|
+
? "error"
|
|
165
|
+
: embedInput
|
|
166
|
+
? "loading"
|
|
167
|
+
: "unresolved"
|
|
168
|
+
}
|
|
169
|
+
embedUrl={embedUrl}
|
|
170
|
+
errorMessage={embedError?.message}
|
|
171
|
+
onRetry={
|
|
172
|
+
embedInput ? () => setEmbedAttempt((attempt) => attempt + 1) : undefined
|
|
173
|
+
}
|
|
174
|
+
renderEmbed={({ url, title }) => (
|
|
175
|
+
<iframe
|
|
176
|
+
key={url + ":" + embedAttempt}
|
|
177
|
+
data-dispatch-workspace-app-frame
|
|
178
|
+
src={url}
|
|
179
|
+
title={title ?? app.name}
|
|
180
|
+
ref={embedFrameRef}
|
|
181
|
+
referrerPolicy="no-referrer"
|
|
182
|
+
allow="clipboard-read; clipboard-write"
|
|
183
|
+
className="h-full w-full border-0 bg-background"
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
copy={copy}
|
|
187
|
+
/>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function WorkspaceAppHost({ appId }: { appId?: string }) {
|
|
192
|
+
const t = useT();
|
|
193
|
+
const appsQuery = useActionQuery("list-workspace-apps", {
|
|
194
|
+
includeAgentCards: false,
|
|
195
|
+
});
|
|
196
|
+
const { data: apps = [], isLoading } = appsQuery;
|
|
197
|
+
const app = useMemo(
|
|
198
|
+
() =>
|
|
199
|
+
(apps as WorkspaceAppSummary[]).find((item) => item.id === appId) ?? null,
|
|
200
|
+
[appId, apps],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (appsQuery.isError) {
|
|
204
|
+
return (
|
|
205
|
+
<div className="flex h-full min-h-0 items-center justify-center p-6">
|
|
206
|
+
<div className="w-full max-w-2xl">
|
|
207
|
+
<ActionQueryError
|
|
208
|
+
error={appsQuery.error}
|
|
209
|
+
onRetry={() => void appsQuery.refetch()}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isLoading && !app) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex h-full min-h-0 items-center justify-center p-6">
|
|
219
|
+
<div className="w-full max-w-2xl space-y-3 rounded-xl border bg-card p-6">
|
|
220
|
+
<Skeleton className="h-5 w-48" />
|
|
221
|
+
<Skeleton className="h-4 w-full" />
|
|
222
|
+
<Skeleton className="h-4 w-2/3" />
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!app) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="flex h-full min-h-0 items-center justify-center p-6">
|
|
231
|
+
<div className="w-full max-w-2xl rounded-xl border bg-card p-6">
|
|
232
|
+
<Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
|
|
233
|
+
<Link to="/apps">
|
|
234
|
+
<IconArrowLeft size={15} className="mr-1.5" />
|
|
235
|
+
{t("dispatch.nav.apps")}
|
|
236
|
+
</Link>
|
|
237
|
+
</Button>
|
|
238
|
+
<div className="space-y-3">
|
|
239
|
+
<h2 className="text-base font-semibold text-foreground">
|
|
240
|
+
{t("dispatch.pages.appNotFound")}
|
|
241
|
+
</h2>
|
|
242
|
+
<p className="text-sm text-muted-foreground">
|
|
243
|
+
{t("dispatch.pages.pageNotFoundDescription")}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (app.status === "pending") {
|
|
252
|
+
return (
|
|
253
|
+
<div className="flex h-full min-h-0 items-center justify-center p-6">
|
|
254
|
+
<div className="w-full max-w-2xl rounded-xl border bg-card p-6">
|
|
255
|
+
<Button asChild size="sm" variant="ghost" className="-ml-2 mb-4">
|
|
256
|
+
<Link to="/apps">
|
|
257
|
+
<IconArrowLeft size={15} className="mr-1.5" />
|
|
258
|
+
{t("dispatch.nav.apps")}
|
|
259
|
+
</Link>
|
|
260
|
+
</Button>
|
|
261
|
+
<div className="space-y-4">
|
|
262
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
263
|
+
<h2 className="text-base font-semibold text-foreground">
|
|
264
|
+
{app.name}
|
|
265
|
+
</h2>
|
|
266
|
+
<Badge
|
|
267
|
+
variant="outline"
|
|
268
|
+
className="gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
|
269
|
+
>
|
|
270
|
+
<IconClockHour4 size={12} />
|
|
271
|
+
{t("dispatch.pages.building")}
|
|
272
|
+
</Badge>
|
|
273
|
+
</div>
|
|
274
|
+
<p className="text-sm text-muted-foreground">
|
|
275
|
+
{t("dispatch.pages.appBuildingPrefix")}{" "}
|
|
276
|
+
<span className="font-mono text-foreground">{app.path}</span>{" "}
|
|
277
|
+
{t("dispatch.pages.appBuildingSuffix")}
|
|
278
|
+
</p>
|
|
279
|
+
{app.builderUrl ? (
|
|
280
|
+
<Button asChild>
|
|
281
|
+
<a
|
|
282
|
+
href={withBuilderUtmTrackingParams(app.builderUrl, {
|
|
283
|
+
campaign: "product",
|
|
284
|
+
content: "dispatch_branch",
|
|
285
|
+
})}
|
|
286
|
+
target="_blank"
|
|
287
|
+
rel="noreferrer"
|
|
288
|
+
>
|
|
289
|
+
{t("dispatch.pages.openBuilderBranch", {
|
|
290
|
+
defaultValue: "Open in Builder",
|
|
291
|
+
})}
|
|
292
|
+
</a>
|
|
293
|
+
</Button>
|
|
294
|
+
) : null}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
data-dispatch-workspace-app-host
|
|
304
|
+
className="flex h-full min-h-0 flex-col bg-background"
|
|
305
|
+
>
|
|
306
|
+
<div className="min-h-0 flex-1 bg-muted/20">
|
|
307
|
+
<WorkspaceAppFrame app={app} />
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const MAX_KEEP_ALIVE_APPS = 3;
|
|
314
|
+
|
|
315
|
+
export function WorkspaceAppKeepAlive({
|
|
316
|
+
activeAppId,
|
|
317
|
+
}: {
|
|
318
|
+
activeAppId: string | null;
|
|
319
|
+
}) {
|
|
320
|
+
const [visitedAppIds, setVisitedAppIds] = useState<string[]>(() =>
|
|
321
|
+
activeAppId ? [activeAppId] : [],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (!activeAppId) return;
|
|
326
|
+
setVisitedAppIds((current) =>
|
|
327
|
+
[activeAppId, ...current.filter((appId) => appId !== activeAppId)].slice(
|
|
328
|
+
0,
|
|
329
|
+
MAX_KEEP_ALIVE_APPS,
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}, [activeAppId]);
|
|
333
|
+
|
|
334
|
+
const renderedAppIds =
|
|
335
|
+
activeAppId && !visitedAppIds.includes(activeAppId)
|
|
336
|
+
? [activeAppId, ...visitedAppIds]
|
|
337
|
+
: visitedAppIds;
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div
|
|
341
|
+
data-dispatch-workspace-app-cache
|
|
342
|
+
className={activeAppId ? "absolute inset-0 overflow-hidden" : "hidden"}
|
|
343
|
+
>
|
|
344
|
+
{renderedAppIds.map((appId) => {
|
|
345
|
+
const active = appId === activeAppId;
|
|
346
|
+
return (
|
|
347
|
+
<div
|
|
348
|
+
key={appId}
|
|
349
|
+
data-dispatch-workspace-app-cache-entry={appId}
|
|
350
|
+
aria-hidden={!active}
|
|
351
|
+
className={active ? "h-full min-h-0" : "hidden"}
|
|
352
|
+
>
|
|
353
|
+
<WorkspaceAppHost appId={appId} />
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
@@ -75,6 +75,10 @@ describe("dispatch migrations", () => {
|
|
|
75
75
|
const { rows } = await exec.execute(
|
|
76
76
|
"SELECT MAX(version) as version FROM dispatch_migrations",
|
|
77
77
|
);
|
|
78
|
-
expect(rows[0]?.version).toBe(
|
|
78
|
+
expect(rows[0]?.version).toBe(5);
|
|
79
|
+
const { rows: identityRows } = await exec.execute(
|
|
80
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'identity_sso_authorization_code'",
|
|
81
|
+
);
|
|
82
|
+
expect(identityRows).toHaveLength(1);
|
|
79
83
|
});
|
|
80
84
|
});
|
package/src/db/migrations.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { MigrationEntry } from "@agent-native/core/db";
|
|
2
|
+
|
|
3
|
+
export const dispatchMigrations: MigrationEntry[] = [
|
|
2
4
|
{
|
|
3
5
|
version: 1,
|
|
4
6
|
sql: `
|
|
@@ -221,4 +223,29 @@ export const dispatchMigrations: Array<{ version: number; sql: string }> = [
|
|
|
221
223
|
ALTER TABLE dispatch_dreams ADD COLUMN IF NOT EXISTS source_health TEXT;
|
|
222
224
|
`,
|
|
223
225
|
},
|
|
226
|
+
{
|
|
227
|
+
version: 5,
|
|
228
|
+
name: "identity-sso-authorization-code-table",
|
|
229
|
+
sql: `
|
|
230
|
+
CREATE TABLE IF NOT EXISTS identity_sso_authorization_code (
|
|
231
|
+
code_hash TEXT PRIMARY KEY,
|
|
232
|
+
state TEXT NOT NULL,
|
|
233
|
+
app_id TEXT NOT NULL,
|
|
234
|
+
client_id TEXT NOT NULL,
|
|
235
|
+
redirect_uri TEXT NOT NULL,
|
|
236
|
+
authority TEXT NOT NULL,
|
|
237
|
+
code_challenge TEXT NOT NULL,
|
|
238
|
+
email TEXT NOT NULL,
|
|
239
|
+
name TEXT,
|
|
240
|
+
org_domain TEXT,
|
|
241
|
+
jti TEXT NOT NULL,
|
|
242
|
+
created_at INTEGER NOT NULL,
|
|
243
|
+
expires_at INTEGER NOT NULL,
|
|
244
|
+
consumed_at INTEGER
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
CREATE INDEX IF NOT EXISTS identity_sso_authorization_code_expires_idx
|
|
248
|
+
ON identity_sso_authorization_code (expires_at);
|
|
249
|
+
`,
|
|
250
|
+
},
|
|
224
251
|
];
|
package/src/db/schema.ts
CHANGED
|
@@ -180,7 +180,7 @@ export const workspaceResources = table("workspace_resources", {
|
|
|
180
180
|
id: text("id").primaryKey(),
|
|
181
181
|
ownerEmail: text("owner_email").notNull(),
|
|
182
182
|
orgId: text("org_id"),
|
|
183
|
-
kind: text("kind").notNull(), // "skill" | "instruction" | "agent" | "knowledge" | "mcp-server"
|
|
183
|
+
kind: text("kind").notNull(), // "skill" | "instruction" | "agent" | "agent-file" | "knowledge" | "mcp-server"
|
|
184
184
|
name: text("name").notNull(),
|
|
185
185
|
description: text("description"),
|
|
186
186
|
path: text("path").notNull(), // resource path, e.g. "skills/designer.md"
|
|
@@ -10,6 +10,20 @@ describe("buildDispatchNavigationState", () => {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
it("keeps the active simple agent on the chat navigation state", () => {
|
|
14
|
+
expect(
|
|
15
|
+
buildDispatchNavigationState(
|
|
16
|
+
"/chat/thread-1",
|
|
17
|
+
"?agent=agents/research-partner.md",
|
|
18
|
+
),
|
|
19
|
+
).toEqual({
|
|
20
|
+
view: "chat",
|
|
21
|
+
path: "/chat/thread-1",
|
|
22
|
+
threadId: "thread-1",
|
|
23
|
+
agentPath: "agents/research-partner.md",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
13
27
|
it("recognizes the embedded browser chat route", () => {
|
|
14
28
|
expect(buildDispatchNavigationState("/browser-chat")).toEqual({
|
|
15
29
|
view: "browser-chat",
|
|
@@ -61,6 +75,17 @@ describe("buildDispatchNavigationState", () => {
|
|
|
61
75
|
});
|
|
62
76
|
});
|
|
63
77
|
|
|
78
|
+
it("keeps simple agents and connected-agent admin routes distinct", () => {
|
|
79
|
+
expect(buildDispatchNavigationState("/agents")).toEqual({
|
|
80
|
+
view: "agents",
|
|
81
|
+
path: "/agents",
|
|
82
|
+
});
|
|
83
|
+
expect(buildDispatchNavigationState("/admin/agents")).toEqual({
|
|
84
|
+
view: "connected-agents",
|
|
85
|
+
path: "/admin/agents",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
64
89
|
it("preserves thread debug filters and selection", () => {
|
|
65
90
|
expect(
|
|
66
91
|
buildDispatchNavigationState(
|
|
@@ -32,6 +32,7 @@ export interface NavigationState {
|
|
|
32
32
|
query?: string;
|
|
33
33
|
runId?: string;
|
|
34
34
|
threadId?: string;
|
|
35
|
+
agentPath?: string;
|
|
35
36
|
usageScope?: "me" | "workspace";
|
|
36
37
|
usageUserEmail?: string;
|
|
37
38
|
}
|
|
@@ -138,6 +139,11 @@ export function buildDispatchNavigationState(
|
|
|
138
139
|
const threadId = threadIdFromPath(pathname);
|
|
139
140
|
if (threadId) state.threadId = threadId;
|
|
140
141
|
|
|
142
|
+
if (state.view === "chat") {
|
|
143
|
+
const agentPath = new URLSearchParams(search).get("agent")?.trim();
|
|
144
|
+
if (agentPath) state.agentPath = agentPath;
|
|
145
|
+
}
|
|
146
|
+
|
|
141
147
|
const extensionId = extensionIdFromPathname(pathname);
|
|
142
148
|
if (extensionId) {
|
|
143
149
|
state.view = "extensions";
|
|
@@ -250,6 +256,9 @@ function resolveView(
|
|
|
250
256
|
return "extensions";
|
|
251
257
|
}
|
|
252
258
|
if (pathname === "/admin") return "admin";
|
|
259
|
+
if (pathname === "/admin/agents" || pathname.startsWith("/admin/agents/")) {
|
|
260
|
+
return "connected-agents";
|
|
261
|
+
}
|
|
253
262
|
if (pathname.startsWith("/admin/")) {
|
|
254
263
|
const adminView = resolveView(pathname.slice("/admin".length), extensions);
|
|
255
264
|
return adminView === "overview" ? "admin" : adminView;
|
|
@@ -318,6 +327,8 @@ function resolvePath(
|
|
|
318
327
|
case "resources":
|
|
319
328
|
return "/admin/workspace";
|
|
320
329
|
case "agents":
|
|
330
|
+
return "/agents";
|
|
331
|
+
case "connected-agents":
|
|
321
332
|
return "/admin/agents";
|
|
322
333
|
case "messaging":
|
|
323
334
|
return "/admin/messaging";
|