@agent-native/dispatch 0.23.3 → 0.24.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/dist/actions/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +5 -1
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
- package/dist/actions/list-dispatch-usage-metrics.js +11 -1
- package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/actions/remix-workspace-template.js +4 -3
- package/dist/actions/remix-workspace-template.js.map +1 -1
- package/dist/actions/scaffold-workspace-app.js +3 -2
- package/dist/actions/scaffold-workspace-app.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +3 -2
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/view-screen.js +11 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/app-icon.js +1 -1
- package/dist/components/app-icon.js.map +1 -1
- package/dist/components/app-list-row.d.ts +1 -1
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +2 -2
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +2 -1
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +11 -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 +3 -2
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +47 -10
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +2 -2
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/other-apps-section.d.ts.map +1 -1
- package/dist/components/other-apps-section.js +5 -1
- package/dist/components/other-apps-section.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +8 -14
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +2 -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/embed-session-recovery.d.ts +3 -0
- package/dist/lib/embed-session-recovery.d.ts.map +1 -0
- package/dist/lib/embed-session-recovery.js +18 -0
- package/dist/lib/embed-session-recovery.js.map +1 -0
- package/dist/lib/other-apps.d.ts +2 -0
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +2 -0
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +7 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +17 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.d.ts +2 -0
- package/dist/lib/workspace-apps.test.d.ts.map +1 -0
- package/dist/lib/workspace-apps.test.js +16 -0
- package/dist/lib/workspace-apps.test.js.map +1 -0
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +10 -11
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +2 -1
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +7 -2
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +143 -17
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +13 -1
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.d.ts +17 -0
- package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +187 -30
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/auth.d.ts.map +1 -1
- package/dist/server/plugins/auth.js +1 -0
- package/dist/server/plugins/auth.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +1 -0
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/list-connected-agents.spec.ts +73 -0
- package/src/actions/list-connected-agents.ts +6 -2
- package/src/actions/list-dispatch-usage-metrics.ts +11 -1
- package/src/actions/remix-workspace-template.spec.ts +29 -0
- package/src/actions/remix-workspace-template.ts +13 -6
- package/src/actions/scaffold-workspace-app.ts +12 -5
- package/src/actions/start-workspace-app-creation.ts +8 -2
- package/src/actions/view-screen.ts +13 -1
- package/src/components/app-icon.spec.tsx +34 -0
- package/src/components/app-icon.tsx +1 -1
- package/src/components/app-list-row.tsx +3 -2
- package/src/components/connected-app-card.tsx +3 -1
- package/src/components/create-app-popover.spec.tsx +26 -2
- package/src/components/create-app-popover.tsx +10 -0
- package/src/components/dispatch-control-plane.spec.tsx +9 -1
- package/src/components/dispatch-control-plane.tsx +8 -2
- package/src/components/layout/Layout.spec.tsx +12 -0
- package/src/components/layout/Layout.tsx +84 -13
- package/src/components/layout/workspace-apps-rail.tsx +2 -1
- package/src/components/other-apps-section.tsx +8 -1
- package/src/components/workspace-template-card.spec.tsx +1 -1
- package/src/components/workspace-template-card.tsx +9 -16
- package/src/config.ts +2 -0
- package/src/hooks/use-navigation-state.ts +12 -0
- package/src/lib/embed-session-recovery.spec.ts +63 -0
- package/src/lib/embed-session-recovery.ts +26 -0
- package/src/lib/other-apps.spec.ts +26 -0
- package/src/lib/other-apps.ts +5 -0
- package/src/lib/workspace-apps.spec.ts +24 -1
- package/src/lib/workspace-apps.test.ts +20 -0
- package/src/lib/workspace-apps.ts +25 -0
- package/src/routes/pages/apps.$appId.tsx +10 -11
- package/src/routes/pages/apps.tsx +5 -2
- package/src/routes/pages/chat.spec.tsx +10 -0
- package/src/routes/pages/chat.tsx +8 -0
- package/src/routes/pages/metrics.tsx +452 -102
- package/src/routes/pages/settings.tsx +13 -1
- package/src/server/lib/usage-metrics-store.spec.ts +117 -0
- package/src/server/lib/usage-metrics-store.ts +249 -30
- package/src/server/plugins/agent-chat.spec.ts +4 -0
- package/src/server/plugins/agent-chat.ts +2 -0
- package/src/server/plugins/auth.spec.ts +45 -0
- package/src/server/plugins/auth.ts +1 -0
- package/src/server/plugins/integrations.ts +1 -0
- package/src/styles/dispatch.css +9 -0
|
@@ -12,7 +12,10 @@ import { Link, useNavigate } from "react-router";
|
|
|
12
12
|
|
|
13
13
|
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
14
14
|
import { cn } from "../lib/utils";
|
|
15
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
17
|
+
type WorkspaceAppSummary,
|
|
18
|
+
} from "../lib/workspace-apps";
|
|
16
19
|
import { ActionQueryError } from "./action-query-error";
|
|
17
20
|
import {
|
|
18
21
|
APP_LIST_GRID_CLASS,
|
|
@@ -115,6 +118,7 @@ function CommandPanel() {
|
|
|
115
118
|
selectedEffort={selectedEffort}
|
|
116
119
|
selectedEngine={selectedEngine}
|
|
117
120
|
selectedModel={selectedModel}
|
|
121
|
+
rootClassName="bg-card"
|
|
118
122
|
onEffortChange={onEffortChange}
|
|
119
123
|
onModelChange={onModelChange}
|
|
120
124
|
onSubmit={(text) => send(text)}
|
|
@@ -161,7 +165,9 @@ function AppsPanel({
|
|
|
161
165
|
}) {
|
|
162
166
|
const t = useT();
|
|
163
167
|
const [showPending, setShowPending] = useState(false);
|
|
164
|
-
const visibleApps = apps.filter(
|
|
168
|
+
const visibleApps = apps.filter(
|
|
169
|
+
(app) => isWorkspaceAppVisibleInDefaultLaunchers(app) && !app.archived,
|
|
170
|
+
);
|
|
165
171
|
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
166
172
|
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
167
173
|
const otherAppEntries = mergeOtherAppEntries({
|
|
@@ -9,6 +9,7 @@ import { TooltipProvider } from "../ui/tooltip";
|
|
|
9
9
|
import {
|
|
10
10
|
buildChatFirstEmbedSessionInput,
|
|
11
11
|
formatThreadAge,
|
|
12
|
+
isElectronEmbeddedSearch,
|
|
12
13
|
NavContent,
|
|
13
14
|
} from "./Layout";
|
|
14
15
|
|
|
@@ -119,6 +120,14 @@ describe("chat-first embed sessions", () => {
|
|
|
119
120
|
});
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
describe("Electron control-plane mode", () => {
|
|
124
|
+
it("recognizes only the explicit Electron query flag", () => {
|
|
125
|
+
expect(isElectronEmbeddedSearch("?electron=1")).toBe(true);
|
|
126
|
+
expect(isElectronEmbeddedSearch("?electron=0")).toBe(false);
|
|
127
|
+
expect(isElectronEmbeddedSearch("?chatFirst=1")).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
122
131
|
describe("Dispatch NavContent", () => {
|
|
123
132
|
let container: HTMLDivElement;
|
|
124
133
|
let root: Root;
|
|
@@ -293,6 +302,9 @@ describe("Dispatch NavContent", () => {
|
|
|
293
302
|
);
|
|
294
303
|
expect(sidebarLabel?.textContent?.trim()).toBe("Dispatch");
|
|
295
304
|
expect(container.textContent).not.toContain("Agent-Native Dispatch");
|
|
305
|
+
expect(
|
|
306
|
+
sidebarLabel?.closest('a[data-dispatch-logo][href="/overview"]'),
|
|
307
|
+
).not.toBeNull();
|
|
296
308
|
|
|
297
309
|
const settingsLink = container.querySelector('a[href="/settings"]');
|
|
298
310
|
const adminLink = container.querySelector('a[href="/admin"]');
|
|
@@ -102,8 +102,11 @@ import {
|
|
|
102
102
|
import { Link, useLocation, useNavigate } from "react-router";
|
|
103
103
|
import { toast } from "sonner";
|
|
104
104
|
|
|
105
|
+
import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
|
|
105
106
|
import { cn } from "../../lib/utils";
|
|
106
107
|
import {
|
|
108
|
+
isDispatchWorkspaceAppId,
|
|
109
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
107
110
|
mergeChatFirstWorkspaceApps,
|
|
108
111
|
workspaceAppIdFromRoute,
|
|
109
112
|
workspaceAppRoute,
|
|
@@ -299,6 +302,10 @@ function localDispatchPath(pathname: string): string {
|
|
|
299
302
|
return pathname;
|
|
300
303
|
}
|
|
301
304
|
|
|
305
|
+
export function isElectronEmbeddedSearch(search: string): boolean {
|
|
306
|
+
return new URLSearchParams(search).get("electron") === "1";
|
|
307
|
+
}
|
|
308
|
+
|
|
302
309
|
function chatFirstPrimaryTabForPath(
|
|
303
310
|
pathname: string,
|
|
304
311
|
): ChatFirstPrimaryTab | undefined {
|
|
@@ -1187,7 +1194,7 @@ export function NavContent({
|
|
|
1187
1194
|
<div
|
|
1188
1195
|
className={cn("py-2", collapsed ? "flex justify-center px-1" : "px-3")}
|
|
1189
1196
|
>
|
|
1190
|
-
<OrgSwitcher compact={collapsed} reserveSpace />
|
|
1197
|
+
<OrgSwitcher compact={collapsed} reserveSpace currentAppId="dispatch" />
|
|
1191
1198
|
</div>
|
|
1192
1199
|
);
|
|
1193
1200
|
const sidebarFooterActions = (
|
|
@@ -1207,9 +1214,12 @@ export function NavContent({
|
|
|
1207
1214
|
collapsed ? "justify-center px-0" : "px-4",
|
|
1208
1215
|
)}
|
|
1209
1216
|
>
|
|
1210
|
-
<
|
|
1217
|
+
<Link
|
|
1218
|
+
to={dispatchNavLinkTarget("/overview")}
|
|
1219
|
+
aria-label={`${DISPATCH_SIDEBAR_LABEL} overview`}
|
|
1220
|
+
data-dispatch-logo
|
|
1211
1221
|
className={cn(
|
|
1212
|
-
"flex items-center",
|
|
1222
|
+
"flex items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
1213
1223
|
collapsed ? "justify-center" : "gap-2",
|
|
1214
1224
|
)}
|
|
1215
1225
|
>
|
|
@@ -1245,7 +1255,7 @@ export function NavContent({
|
|
|
1245
1255
|
</div>
|
|
1246
1256
|
</div>
|
|
1247
1257
|
)}
|
|
1248
|
-
</
|
|
1258
|
+
</Link>
|
|
1249
1259
|
</div>
|
|
1250
1260
|
|
|
1251
1261
|
{chatFirstMode ? (
|
|
@@ -1330,6 +1340,11 @@ export function Layout({
|
|
|
1330
1340
|
}
|
|
1331
1341
|
});
|
|
1332
1342
|
const localPathname = localDispatchPath(location.pathname);
|
|
1343
|
+
const [electronEmbedded] = useState(() =>
|
|
1344
|
+
typeof window !== "undefined"
|
|
1345
|
+
? isElectronEmbeddedSearch(window.location.search)
|
|
1346
|
+
: false,
|
|
1347
|
+
);
|
|
1333
1348
|
const isChatRoute =
|
|
1334
1349
|
localPathname === "/chat" || localPathname.startsWith("/chat/");
|
|
1335
1350
|
const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
|
|
@@ -1347,6 +1362,17 @@ export function Layout({
|
|
|
1347
1362
|
const chatFirstMode =
|
|
1348
1363
|
extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
|
|
1349
1364
|
const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
|
|
1365
|
+
useEffect(() => {
|
|
1366
|
+
if (!electronEmbedded || typeof window === "undefined") return;
|
|
1367
|
+
const url = new URL(window.location.href);
|
|
1368
|
+
if (isElectronEmbeddedSearch(url.search)) return;
|
|
1369
|
+
url.searchParams.set("electron", "1");
|
|
1370
|
+
window.history.replaceState(
|
|
1371
|
+
window.history.state,
|
|
1372
|
+
"",
|
|
1373
|
+
`${url.pathname}${url.search}${url.hash}`,
|
|
1374
|
+
);
|
|
1375
|
+
}, [electronEmbedded, location.pathname, location.search]);
|
|
1350
1376
|
useEffect(() => {
|
|
1351
1377
|
if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
|
|
1352
1378
|
return;
|
|
@@ -1396,7 +1422,9 @@ export function Layout({
|
|
|
1396
1422
|
const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
|
|
1397
1423
|
() =>
|
|
1398
1424
|
chatFirstAppRegistrations
|
|
1399
|
-
.filter(
|
|
1425
|
+
.filter(
|
|
1426
|
+
(app) => app.enabled && isWorkspaceAppVisibleInDefaultLaunchers(app),
|
|
1427
|
+
)
|
|
1400
1428
|
.map((app) => ({
|
|
1401
1429
|
id: app.id,
|
|
1402
1430
|
name: app.name ?? app.id,
|
|
@@ -1562,11 +1590,9 @@ export function Layout({
|
|
|
1562
1590
|
|
|
1563
1591
|
const handleEmbedSessionExpired = (event: MessageEvent) => {
|
|
1564
1592
|
if (
|
|
1565
|
-
event
|
|
1566
|
-
|
|
1567
|
-
) {
|
|
1593
|
+
!isEmbedSessionExpiredMessage(event, chatFirstAppFrameRef.current, null)
|
|
1594
|
+
)
|
|
1568
1595
|
return;
|
|
1569
|
-
}
|
|
1570
1596
|
setChatFirstEmbedAttempt((attempt) => attempt + 1);
|
|
1571
1597
|
};
|
|
1572
1598
|
|
|
@@ -1729,6 +1755,19 @@ export function Layout({
|
|
|
1729
1755
|
setChatFirstNotice(chatFirstResolutionMessage(resolution.reason));
|
|
1730
1756
|
return;
|
|
1731
1757
|
}
|
|
1758
|
+
if (isDispatchWorkspaceAppId(resolution.target.appId)) {
|
|
1759
|
+
closeChatFirstSessionWatch();
|
|
1760
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
1761
|
+
setChatFirstSurfacePanelOpen(false);
|
|
1762
|
+
persistChatFirstPane(null);
|
|
1763
|
+
if (resolution.target.path) {
|
|
1764
|
+
navigateWithAgentChatViewTransition(
|
|
1765
|
+
navigate,
|
|
1766
|
+
dispatchNavLinkTarget(resolution.target.path),
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1732
1771
|
openChatFirstPane(resolution.target);
|
|
1733
1772
|
},
|
|
1734
1773
|
[
|
|
@@ -1736,7 +1775,12 @@ export function Layout({
|
|
|
1736
1775
|
chatFirstAppsQuery.isError,
|
|
1737
1776
|
chatFirstAppsQuery.isLoading,
|
|
1738
1777
|
chatFirstGrantedAppsQuery.isLoading,
|
|
1778
|
+
chatFirstSurfaceTabsStore,
|
|
1779
|
+
closeChatFirstSessionWatch,
|
|
1739
1780
|
openChatFirstPane,
|
|
1781
|
+
navigate,
|
|
1782
|
+
persistChatFirstPane,
|
|
1783
|
+
setChatFirstSurfacePanelOpen,
|
|
1740
1784
|
],
|
|
1741
1785
|
);
|
|
1742
1786
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
@@ -2067,13 +2111,14 @@ export function Layout({
|
|
|
2067
2111
|
chatOnly
|
|
2068
2112
|
dynamicSuggestions={false}
|
|
2069
2113
|
suggestions={[]}
|
|
2114
|
+
suppressInlineOpenApp={chatFirstMode}
|
|
2070
2115
|
emptyStateDisplay="hidden"
|
|
2071
2116
|
composerPlaceholder={t(
|
|
2072
2117
|
"dispatch.pages.chatFirstSessionMessagePlaceholder",
|
|
2073
2118
|
)}
|
|
2074
2119
|
/>
|
|
2075
2120
|
),
|
|
2076
|
-
[t],
|
|
2121
|
+
[chatFirstMode, t],
|
|
2077
2122
|
);
|
|
2078
2123
|
|
|
2079
2124
|
const renderChatFirstSurfaceTab = useCallback(
|
|
@@ -2110,6 +2155,7 @@ export function Layout({
|
|
|
2110
2155
|
onRetry={() => setChatFirstEmbedAttempt((value) => value + 1)}
|
|
2111
2156
|
renderEmbed={({ url, title }: ChatFirstEmbedTarget) => (
|
|
2112
2157
|
<iframe
|
|
2158
|
+
key={`${url}:${chatFirstEmbedAttempt}`}
|
|
2113
2159
|
data-dispatch-chat-first-app-frame
|
|
2114
2160
|
src={url}
|
|
2115
2161
|
title={title ?? chatFirstCopy("appUnavailable")}
|
|
@@ -2204,6 +2250,29 @@ export function Layout({
|
|
|
2204
2250
|
return <>{children}</>;
|
|
2205
2251
|
}
|
|
2206
2252
|
|
|
2253
|
+
if (electronEmbedded) {
|
|
2254
|
+
return (
|
|
2255
|
+
<DispatchExtensionsContext.Provider value={extensions}>
|
|
2256
|
+
<HeaderActionsProvider>
|
|
2257
|
+
<div
|
|
2258
|
+
data-dispatch-electron-control-plane
|
|
2259
|
+
className="flex h-screen w-full overflow-hidden bg-background"
|
|
2260
|
+
>
|
|
2261
|
+
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
2262
|
+
<Header showAgentToggle={false} />
|
|
2263
|
+
<InvitationBanner />
|
|
2264
|
+
<main className="flex-1 overflow-y-auto">
|
|
2265
|
+
<div className="mx-auto max-w-7xl space-y-10 px-4 py-6 sm:px-6">
|
|
2266
|
+
{children}
|
|
2267
|
+
</div>
|
|
2268
|
+
</main>
|
|
2269
|
+
</div>
|
|
2270
|
+
</div>
|
|
2271
|
+
</HeaderActionsProvider>
|
|
2272
|
+
</DispatchExtensionsContext.Provider>
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2207
2276
|
if (isWorkspaceAppHostRoute) {
|
|
2208
2277
|
return (
|
|
2209
2278
|
<DispatchExtensionsContext.Provider value={extensions}>
|
|
@@ -2403,9 +2472,11 @@ export function Layout({
|
|
|
2403
2472
|
chatFirstSurfaceTabsStore.closeAll();
|
|
2404
2473
|
setChatFirstSurfacePanelOpen(false);
|
|
2405
2474
|
}}
|
|
2406
|
-
onChatFirstAppOpen={(app) =>
|
|
2407
|
-
|
|
2408
|
-
|
|
2475
|
+
onChatFirstAppOpen={(app) => {
|
|
2476
|
+
if (isDispatchWorkspaceAppId(app.id)) return;
|
|
2477
|
+
setSidebarCollapsed(true);
|
|
2478
|
+
navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
|
|
2479
|
+
}}
|
|
2409
2480
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2410
2481
|
collapsible
|
|
2411
2482
|
onCollapsedChange={setSidebarCollapsed}
|
|
@@ -5,6 +5,7 @@ import { Link, useLocation } from "react-router";
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../lib/utils";
|
|
7
7
|
import {
|
|
8
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
8
9
|
workspaceAppIdFromRoute,
|
|
9
10
|
workspaceAppRoute,
|
|
10
11
|
workspaceAppHref,
|
|
@@ -41,7 +42,7 @@ export function WorkspaceAppsRail({
|
|
|
41
42
|
const apps = appsQuery.data
|
|
42
43
|
.filter(
|
|
43
44
|
(app) =>
|
|
44
|
-
|
|
45
|
+
isWorkspaceAppVisibleInDefaultLaunchers(app) &&
|
|
45
46
|
!app.archived &&
|
|
46
47
|
app.status !== "pending" &&
|
|
47
48
|
!!workspaceAppHref(app),
|
|
@@ -3,6 +3,7 @@ import { useT } from "@agent-native/core/client/i18n";
|
|
|
3
3
|
import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
|
|
4
4
|
import type { WorkspaceAppId } from "../lib/other-apps";
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { isDefaultWorkspaceAppHiddenId } from "../lib/workspace-apps";
|
|
6
7
|
import { ActionQueryError } from "./action-query-error";
|
|
7
8
|
import {
|
|
8
9
|
APP_LIST_GRID_CLASS,
|
|
@@ -52,7 +53,13 @@ export function mergeOtherAppEntries({
|
|
|
52
53
|
|
|
53
54
|
for (const template of getTemplateItems(templates)) {
|
|
54
55
|
const id = templateKey(template);
|
|
55
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
!id ||
|
|
58
|
+
isDefaultWorkspaceAppHiddenId(id) ||
|
|
59
|
+
template.installed ||
|
|
60
|
+
workspaceAppIds.has(id)
|
|
61
|
+
)
|
|
62
|
+
continue;
|
|
56
63
|
seen.add(id);
|
|
57
64
|
entries.push({ kind: "template", template });
|
|
58
65
|
}
|
|
@@ -130,7 +130,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
130
130
|
Object.getOwnPropertyDescriptor(
|
|
131
131
|
HTMLInputElement.prototype,
|
|
132
132
|
"value",
|
|
133
|
-
)?.set?.call(input, "
|
|
133
|
+
)?.set?.call(input, "Sales Ops");
|
|
134
134
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { normalizeWorkspaceAppId } from "@agent-native/core/shared";
|
|
2
3
|
import {
|
|
3
4
|
IconArrowUpRight,
|
|
4
5
|
IconCircleCheck,
|
|
@@ -94,17 +95,6 @@ export interface WorkspaceTemplateCardProps {
|
|
|
94
95
|
) => void;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function slugifyAppId(value: string): string {
|
|
98
|
-
return (
|
|
99
|
-
value
|
|
100
|
-
.trim()
|
|
101
|
-
.toLowerCase()
|
|
102
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
103
|
-
.replace(/^-+|-+$/g, "")
|
|
104
|
-
.slice(0, 48) || "new-app"
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
98
|
function templateIdFor(template: CuratedWorkspaceTemplate): string {
|
|
109
99
|
return template.templateId || template.id || template.appId || template.name;
|
|
110
100
|
}
|
|
@@ -113,8 +103,9 @@ function defaultAppIdFor(
|
|
|
113
103
|
template: CuratedWorkspaceTemplate,
|
|
114
104
|
defaultAppId?: string,
|
|
115
105
|
): string {
|
|
116
|
-
if (defaultAppId) return
|
|
117
|
-
const sourceId =
|
|
106
|
+
if (defaultAppId) return normalizeWorkspaceAppId(defaultAppId) || "new-app";
|
|
107
|
+
const sourceId =
|
|
108
|
+
normalizeWorkspaceAppId(template.appId || template.name) || "new-app";
|
|
118
109
|
return `${sourceId}-app`;
|
|
119
110
|
}
|
|
120
111
|
|
|
@@ -173,15 +164,16 @@ export function WorkspaceTemplateCard({
|
|
|
173
164
|
}, [defaultAppId, open, template]);
|
|
174
165
|
|
|
175
166
|
function submitRemix() {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
167
|
+
const normalizedAppId = normalizeWorkspaceAppId(appId);
|
|
168
|
+
if (!normalizedAppId) {
|
|
178
169
|
toast.error(labels.appIdRequired);
|
|
179
170
|
return;
|
|
180
171
|
}
|
|
172
|
+
setAppId(normalizedAppId);
|
|
181
173
|
|
|
182
174
|
remix.mutate({
|
|
183
175
|
templateId: templateIdFor(template),
|
|
184
|
-
appId:
|
|
176
|
+
appId: normalizedAppId,
|
|
185
177
|
});
|
|
186
178
|
}
|
|
187
179
|
|
|
@@ -280,6 +272,7 @@ export function WorkspaceTemplateCard({
|
|
|
280
272
|
value={appId}
|
|
281
273
|
autoComplete="off"
|
|
282
274
|
onChange={(event) => setAppId(event.target.value)}
|
|
275
|
+
onBlur={() => setAppId(normalizeWorkspaceAppId(appId))}
|
|
283
276
|
disabled={remix.isPending}
|
|
284
277
|
/>
|
|
285
278
|
</div>
|
package/src/config.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface DispatchAuthConfig {
|
|
|
13
13
|
googleOnly?: boolean;
|
|
14
14
|
/** Marketing/branding copy passed straight through to `createAuthPlugin`. */
|
|
15
15
|
marketing?: Record<string, unknown>;
|
|
16
|
+
/** Exact framework routes that perform their own authentication checks. */
|
|
17
|
+
publicPaths?: string[];
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface DispatchIntegrationsConfig {
|
|
@@ -32,6 +32,8 @@ export interface NavigationState {
|
|
|
32
32
|
query?: string;
|
|
33
33
|
runId?: string;
|
|
34
34
|
threadId?: string;
|
|
35
|
+
usageScope?: "me" | "workspace";
|
|
36
|
+
usageUserEmail?: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export function useNavigationState(extensions?: DispatchExtensionConfig) {
|
|
@@ -177,6 +179,16 @@ export function buildDispatchNavigationState(
|
|
|
177
179
|
if (selectedThreadId) state.threadId = selectedThreadId;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
if (state.view === "metrics") {
|
|
183
|
+
const params = new URLSearchParams(search);
|
|
184
|
+
const usageScope = params.get("scope");
|
|
185
|
+
const usageUserEmail = params.get("user");
|
|
186
|
+
if (usageScope === "me" || usageScope === "workspace") {
|
|
187
|
+
state.usageScope = usageScope;
|
|
188
|
+
}
|
|
189
|
+
if (usageUserEmail) state.usageUserEmail = usageUserEmail;
|
|
190
|
+
}
|
|
191
|
+
|
|
180
192
|
return state;
|
|
181
193
|
}
|
|
182
194
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EMBED_SESSION_EXPIRED_MESSAGE,
|
|
5
|
+
isEmbedSessionExpiredMessage,
|
|
6
|
+
} from "./embed-session-recovery";
|
|
7
|
+
|
|
8
|
+
const frameWindow = {} as Window;
|
|
9
|
+
const frame = {
|
|
10
|
+
contentWindow: frameWindow,
|
|
11
|
+
src: "https://mail.example.test/_agent-native/embed/start?ticket=one",
|
|
12
|
+
} as HTMLIFrameElement;
|
|
13
|
+
|
|
14
|
+
describe("isEmbedSessionExpiredMessage", () => {
|
|
15
|
+
it("accepts a message from the active app frame", () => {
|
|
16
|
+
expect(
|
|
17
|
+
isEmbedSessionExpiredMessage(
|
|
18
|
+
{ data: { type: EMBED_SESSION_EXPIRED_MESSAGE }, source: frameWindow },
|
|
19
|
+
frame,
|
|
20
|
+
),
|
|
21
|
+
).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects messages from another frame", () => {
|
|
25
|
+
expect(
|
|
26
|
+
isEmbedSessionExpiredMessage(
|
|
27
|
+
{ data: { type: EMBED_SESSION_EXPIRED_MESSAGE }, source: {} as Window },
|
|
28
|
+
frame,
|
|
29
|
+
),
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("accepts a matching start URL when the browser omits event.source", () => {
|
|
34
|
+
expect(
|
|
35
|
+
isEmbedSessionExpiredMessage(
|
|
36
|
+
{
|
|
37
|
+
data: {
|
|
38
|
+
type: EMBED_SESSION_EXPIRED_MESSAGE,
|
|
39
|
+
embedStartUrl: frame.src,
|
|
40
|
+
},
|
|
41
|
+
source: null,
|
|
42
|
+
},
|
|
43
|
+
frame,
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("can match the expected URL before the iframe ref is available", () => {
|
|
49
|
+
expect(
|
|
50
|
+
isEmbedSessionExpiredMessage(
|
|
51
|
+
{
|
|
52
|
+
data: {
|
|
53
|
+
type: EMBED_SESSION_EXPIRED_MESSAGE,
|
|
54
|
+
embedStartUrl: frame.src,
|
|
55
|
+
},
|
|
56
|
+
source: null,
|
|
57
|
+
},
|
|
58
|
+
null,
|
|
59
|
+
frame.src,
|
|
60
|
+
),
|
|
61
|
+
).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const EMBED_SESSION_EXPIRED_MESSAGE = "agentNative.embedSessionExpired";
|
|
2
|
+
|
|
3
|
+
interface EmbedSessionExpiredMessage {
|
|
4
|
+
type?: unknown;
|
|
5
|
+
embedStartUrl?: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isEmbedSessionExpiredMessage(
|
|
9
|
+
event: Pick<MessageEvent, "data" | "source">,
|
|
10
|
+
frame: Pick<HTMLIFrameElement, "contentWindow" | "src"> | null,
|
|
11
|
+
expectedStartUrl?: string | null,
|
|
12
|
+
): boolean {
|
|
13
|
+
const data =
|
|
14
|
+
event.data && typeof event.data === "object"
|
|
15
|
+
? (event.data as EmbedSessionExpiredMessage)
|
|
16
|
+
: null;
|
|
17
|
+
if (data?.type !== EMBED_SESSION_EXPIRED_MESSAGE) return false;
|
|
18
|
+
|
|
19
|
+
const frameWindow = frame?.contentWindow;
|
|
20
|
+
if (frameWindow && event.source === frameWindow) return true;
|
|
21
|
+
if (event.source !== null) return false;
|
|
22
|
+
if (typeof data.embedStartUrl !== "string") return false;
|
|
23
|
+
|
|
24
|
+
const frameUrl = frame?.src || expectedStartUrl;
|
|
25
|
+
return Boolean(frameUrl && data.embedStartUrl === frameUrl);
|
|
26
|
+
}
|
|
@@ -104,4 +104,30 @@ describe("filterOtherApps", () => {
|
|
|
104
104
|
},
|
|
105
105
|
]);
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it("hides the generic chat starter from connected app launchers", () => {
|
|
109
|
+
expect(
|
|
110
|
+
filterOtherApps(
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
id: "chat",
|
|
114
|
+
name: "Chat",
|
|
115
|
+
url: "https://chat.agent-native.com",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "mail",
|
|
119
|
+
name: "Mail",
|
|
120
|
+
url: "https://mail.agent-native.com",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[],
|
|
124
|
+
),
|
|
125
|
+
).toEqual([
|
|
126
|
+
{
|
|
127
|
+
id: "mail",
|
|
128
|
+
name: "Mail",
|
|
129
|
+
url: "https://mail.agent-native.com",
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
107
133
|
});
|
package/src/lib/other-apps.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { isDefaultWorkspaceAppHiddenId } from "./workspace-apps";
|
|
2
|
+
|
|
1
3
|
export interface ConnectedAppSummary {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
4
6
|
description?: string;
|
|
5
7
|
url: string;
|
|
8
|
+
/** Canonical app-home URL used for launchers; `url` remains the A2A endpoint. */
|
|
9
|
+
homeUrl?: string;
|
|
6
10
|
color?: string;
|
|
7
11
|
source?: "builtin" | "custom" | "workspace";
|
|
8
12
|
}
|
|
@@ -38,6 +42,7 @@ export function filterOtherApps(
|
|
|
38
42
|
const id = app.id.trim().toLowerCase();
|
|
39
43
|
if (
|
|
40
44
|
!id ||
|
|
45
|
+
isDefaultWorkspaceAppHiddenId(id) ||
|
|
41
46
|
HIDDEN_OTHER_APP_IDS.has(id) ||
|
|
42
47
|
workspaceAppIds.has(id) ||
|
|
43
48
|
seen.has(id)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
isDefaultWorkspaceAppHiddenId,
|
|
5
|
+
isDispatchWorkspaceAppId,
|
|
6
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
7
|
+
workspaceAppIdFromRoute,
|
|
8
|
+
workspaceAppRoute,
|
|
9
|
+
} from "./workspace-apps";
|
|
4
10
|
|
|
5
11
|
describe("workspace app routes", () => {
|
|
6
12
|
it("round-trips encoded app ids", () => {
|
|
@@ -18,4 +24,21 @@ describe("workspace app routes", () => {
|
|
|
18
24
|
it("accepts nested app routes while preserving the app id", () => {
|
|
19
25
|
expect(workspaceAppIdFromRoute("/apps/mail/inbox")).toBe("mail");
|
|
20
26
|
});
|
|
27
|
+
|
|
28
|
+
it("identifies Dispatch regardless of casing or surrounding whitespace", () => {
|
|
29
|
+
expect(isDispatchWorkspaceAppId(" Dispatch ")).toBe(true);
|
|
30
|
+
expect(isDispatchWorkspaceAppId("dispatch-tools")).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("hides the generic chat starter and Dispatch from default launchers", () => {
|
|
34
|
+
expect(isDefaultWorkspaceAppHiddenId(" Chat ")).toBe(true);
|
|
35
|
+
expect(isWorkspaceAppVisibleInDefaultLaunchers({ id: "chat" })).toBe(false);
|
|
36
|
+
expect(
|
|
37
|
+
isWorkspaceAppVisibleInDefaultLaunchers({
|
|
38
|
+
id: "dispatch",
|
|
39
|
+
isDispatch: true,
|
|
40
|
+
}),
|
|
41
|
+
).toBe(false);
|
|
42
|
+
expect(isWorkspaceAppVisibleInDefaultLaunchers({ id: "mail" })).toBe(true);
|
|
43
|
+
});
|
|
21
44
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { workspaceAppEmbedTarget } from "./workspace-apps";
|
|
4
|
+
|
|
5
|
+
describe("workspaceAppEmbedTarget", () => {
|
|
6
|
+
it("uses the app URL as the embed root when a mount path is also present", () => {
|
|
7
|
+
expect(
|
|
8
|
+
workspaceAppEmbedTarget({
|
|
9
|
+
path: "/content",
|
|
10
|
+
url: "https://workspace.example.test/content",
|
|
11
|
+
}),
|
|
12
|
+
).toEqual({ url: "https://workspace.example.test/content" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("falls back to the mounted path when no app URL is available", () => {
|
|
16
|
+
expect(workspaceAppEmbedTarget({ path: "/content", url: null })).toEqual({
|
|
17
|
+
path: "/content",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -27,6 +27,21 @@ export interface WorkspaceAppSummary {
|
|
|
27
27
|
archived?: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function isDispatchWorkspaceAppId(appId: string): boolean {
|
|
31
|
+
return appId.trim().toLowerCase() === "dispatch";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isDefaultWorkspaceAppHiddenId(appId: string): boolean {
|
|
35
|
+
const normalized = appId.trim().toLowerCase();
|
|
36
|
+
return normalized === "chat" || isDispatchWorkspaceAppId(normalized);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isWorkspaceAppVisibleInDefaultLaunchers(
|
|
40
|
+
app: Pick<WorkspaceAppSummary, "id" | "isDispatch">,
|
|
41
|
+
): boolean {
|
|
42
|
+
return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export function workspaceAppRoute(appId: string): string {
|
|
31
46
|
return `/apps/${encodeURIComponent(appId)}`;
|
|
32
47
|
}
|
|
@@ -55,6 +70,16 @@ export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
|
|
|
55
70
|
return app.path || app.url || null;
|
|
56
71
|
}
|
|
57
72
|
|
|
73
|
+
export function workspaceAppEmbedTarget(
|
|
74
|
+
app: Pick<WorkspaceAppSummary, "path" | "url">,
|
|
75
|
+
): { path?: string; url?: string } {
|
|
76
|
+
const url = app.url?.trim();
|
|
77
|
+
if (url) return { url };
|
|
78
|
+
|
|
79
|
+
const path = app.path.trim();
|
|
80
|
+
return path.startsWith("/") ? { path } : path ? { url: path } : {};
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
|
|
59
84
|
return app.status === "pending" && !!app.builderUrl;
|
|
60
85
|
}
|