@agent-native/dispatch 0.23.2 → 0.23.5
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 +11 -11
- 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/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +3 -2
- package/dist/components/app-open-actions.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 +5 -2
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +122 -13
- 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 +3 -23
- 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-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +3 -4
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +1 -1
- 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/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 +4 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +23 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/index.js +1 -1
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/agent-integrations.d.ts +5 -0
- package/dist/routes/pages/agent-integrations.d.ts.map +1 -0
- package/dist/routes/pages/agent-integrations.js +15 -0
- package/dist/routes/pages/agent-integrations.js.map +1 -0
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +14 -2
- 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/overview.d.ts +2 -2
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +1 -1
- package/dist/routes/pages/settings.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/package.json +3 -3
- package/src/components/app-list-row.tsx +3 -2
- package/src/components/app-open-actions.tsx +3 -2
- 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 +1 -0
- package/src/components/dispatch-control-plane.tsx +8 -2
- package/src/components/layout/Layout.spec.tsx +9 -0
- package/src/components/layout/Layout.tsx +181 -19
- package/src/components/layout/workspace-apps-rail.tsx +4 -26
- package/src/components/other-apps-section.tsx +8 -1
- package/src/components/workspace-app-card.spec.tsx +6 -6
- package/src/components/workspace-app-card.tsx +4 -7
- package/src/components/workspace-template-card.spec.tsx +4 -16
- package/src/components/workspace-template-card.tsx +0 -1
- package/src/config.ts +2 -0
- package/src/lib/other-apps.spec.ts +26 -0
- package/src/lib/other-apps.ts +3 -0
- package/src/lib/workspace-apps.spec.ts +44 -0
- package/src/lib/workspace-apps.ts +27 -0
- package/src/routes/index.ts +1 -1
- package/src/routes/pages/agent-integrations.tsx +24 -0
- package/src/routes/pages/apps.$appId.tsx +19 -1
- package/src/routes/pages/apps.tsx +5 -2
- package/src/routes/pages/settings.tsx +1 -19
- package/src/server/plugins/auth.spec.ts +45 -0
- package/src/server/plugins/auth.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.5",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"tailwind-merge": "^3.5.0",
|
|
85
85
|
"vaul": "^1.1.2",
|
|
86
86
|
"zod": "^4.3.6",
|
|
87
|
-
"@agent-native/toolkit": "0.13.
|
|
87
|
+
"@agent-native/toolkit": "0.13.10"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@react-router/dev": "^8.1.0",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
99
99
|
"vite": "8.1.0",
|
|
100
100
|
"vitest": "^4.1.5",
|
|
101
|
-
"@agent-native/core": "0.
|
|
101
|
+
"@agent-native/core": "0.151.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -4,7 +4,8 @@ import { cn } from "../lib/utils";
|
|
|
4
4
|
|
|
5
5
|
export const APP_LIST_GRID_CLASS =
|
|
6
6
|
"mx-auto w-full max-w-[1000px] xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
|
|
7
|
-
export const APP_LIST_GRID_ROW_CLASS =
|
|
7
|
+
export const APP_LIST_GRID_ROW_CLASS =
|
|
8
|
+
"xl:rounded-2xl xl:border xl:border-transparent xl:bg-card xl:transition-[background-color,border-color] xl:hover:border-border xl:focus-within:border-ring/50";
|
|
8
9
|
|
|
9
10
|
export function AppList({
|
|
10
11
|
children,
|
|
@@ -30,7 +31,7 @@ export function AppListRow({
|
|
|
30
31
|
return (
|
|
31
32
|
<div
|
|
32
33
|
className={cn(
|
|
33
|
-
"group flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0 transition-[background-color] hover:bg-muted/30 focus-within:bg-muted/30",
|
|
34
|
+
"group flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0 transition-[background-color,border-color] hover:bg-muted/30 focus-within:bg-muted/30",
|
|
34
35
|
className,
|
|
35
36
|
)}
|
|
36
37
|
>
|
|
@@ -44,9 +44,10 @@ export function AppOpenActions({
|
|
|
44
44
|
showNewTabOption?: boolean;
|
|
45
45
|
}) {
|
|
46
46
|
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
47
|
+
const canOfferNewTab = showNewTabOption && target !== "_blank";
|
|
47
48
|
const hasMenu =
|
|
48
49
|
Boolean(onAddApp) ||
|
|
49
|
-
(Boolean(href) && (showInlineOption ||
|
|
50
|
+
(Boolean(href) && (showInlineOption || canOfferNewTab));
|
|
50
51
|
|
|
51
52
|
if (!href && !hasMenu) {
|
|
52
53
|
return (
|
|
@@ -98,7 +99,7 @@ export function AppOpenActions({
|
|
|
98
99
|
{labels.openInline}
|
|
99
100
|
</DropdownMenuItem>
|
|
100
101
|
) : null}
|
|
101
|
-
{
|
|
102
|
+
{canOfferNewTab && href ? (
|
|
102
103
|
<DropdownMenuItem asChild>
|
|
103
104
|
<a href={href} target="_blank" rel="noreferrer">
|
|
104
105
|
{labels.openInNewTab}
|
|
@@ -152,9 +152,12 @@ describe("CreateAppFlow", () => {
|
|
|
152
152
|
vi.unstubAllGlobals();
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
async function renderAndSubmit(
|
|
155
|
+
async function renderAndSubmit(
|
|
156
|
+
prompt: string,
|
|
157
|
+
props: { onClose?: () => void } = {},
|
|
158
|
+
) {
|
|
156
159
|
await act(async () => {
|
|
157
|
-
root.render(React.createElement(CreateAppFlow,
|
|
160
|
+
root.render(React.createElement(CreateAppFlow, props));
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
changeValue(
|
|
@@ -213,6 +216,27 @@ describe("CreateAppFlow", () => {
|
|
|
213
216
|
).toBe(false);
|
|
214
217
|
});
|
|
215
218
|
|
|
219
|
+
it("opens a fresh local chat when the server hands off app creation", async () => {
|
|
220
|
+
startWorkspaceAppCreationResponse.result = {
|
|
221
|
+
mode: "local-agent",
|
|
222
|
+
appId: "quality-dashboard",
|
|
223
|
+
prompt: "Create the quality dashboard in the new workspace app.",
|
|
224
|
+
message: "Starting the local coding chat.",
|
|
225
|
+
};
|
|
226
|
+
const onClose = vi.fn();
|
|
227
|
+
|
|
228
|
+
await renderAndSubmit("Build a quality dashboard", { onClose });
|
|
229
|
+
|
|
230
|
+
expect(sendToAgentChatMock).toHaveBeenCalledWith({
|
|
231
|
+
message: "Create the quality dashboard in the new workspace app.",
|
|
232
|
+
submit: true,
|
|
233
|
+
type: "code",
|
|
234
|
+
newTab: true,
|
|
235
|
+
reuseEmptyTab: true,
|
|
236
|
+
});
|
|
237
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
238
|
+
});
|
|
239
|
+
|
|
216
240
|
it("renders the error affordance and a Try again control for builder-error, without a Connect Builder control", async () => {
|
|
217
241
|
startWorkspaceAppCreationResponse.result = {
|
|
218
242
|
mode: "builder-unavailable",
|
|
@@ -269,6 +269,16 @@ export function CreateAppFlow({
|
|
|
269
269
|
if (result?.mode === "builder") {
|
|
270
270
|
setBranchUrl(result?.url || null);
|
|
271
271
|
setStatusMessage("Builder branch created.");
|
|
272
|
+
} else if (result?.mode === "local-agent") {
|
|
273
|
+
sendToAgentChat({
|
|
274
|
+
message: result.prompt ?? message,
|
|
275
|
+
submit: true,
|
|
276
|
+
type: "code",
|
|
277
|
+
newTab: true,
|
|
278
|
+
reuseEmptyTab: true,
|
|
279
|
+
});
|
|
280
|
+
setStatusMessage("Sent to the local agent.");
|
|
281
|
+
onClose?.();
|
|
272
282
|
} else {
|
|
273
283
|
setStatusMessage(
|
|
274
284
|
result?.message ||
|
|
@@ -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;
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
type ChatFirstAppItem,
|
|
56
56
|
type ChatFirstCopy,
|
|
57
57
|
type ChatFirstEmbedTarget,
|
|
58
|
+
type ChatFirstPrimaryTab,
|
|
58
59
|
} from "@agent-native/core/client/chat-first";
|
|
59
60
|
import { writeClipboardText } from "@agent-native/core/client/clipboard";
|
|
60
61
|
import {
|
|
@@ -103,7 +104,11 @@ import { toast } from "sonner";
|
|
|
103
104
|
|
|
104
105
|
import { cn } from "../../lib/utils";
|
|
105
106
|
import {
|
|
107
|
+
isDispatchWorkspaceAppId,
|
|
108
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
106
109
|
mergeChatFirstWorkspaceApps,
|
|
110
|
+
workspaceAppIdFromRoute,
|
|
111
|
+
workspaceAppRoute,
|
|
107
112
|
type WorkspaceAppSummary,
|
|
108
113
|
} from "../../lib/workspace-apps";
|
|
109
114
|
import { AppIcon } from "../app-icon";
|
|
@@ -296,6 +301,35 @@ function localDispatchPath(pathname: string): string {
|
|
|
296
301
|
return pathname;
|
|
297
302
|
}
|
|
298
303
|
|
|
304
|
+
export function isElectronEmbeddedSearch(search: string): boolean {
|
|
305
|
+
return new URLSearchParams(search).get("electron") === "1";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function chatFirstPrimaryTabForPath(
|
|
309
|
+
pathname: string,
|
|
310
|
+
): ChatFirstPrimaryTab | undefined {
|
|
311
|
+
if (pathname === "/chat" || pathname.startsWith("/chat/")) {
|
|
312
|
+
return "new-chat";
|
|
313
|
+
}
|
|
314
|
+
if (
|
|
315
|
+
pathname === "/integrations" ||
|
|
316
|
+
pathname.startsWith("/integrations/") ||
|
|
317
|
+
pathname === "/admin/integrations" ||
|
|
318
|
+
pathname.startsWith("/admin/integrations/")
|
|
319
|
+
) {
|
|
320
|
+
return "integrations";
|
|
321
|
+
}
|
|
322
|
+
if (
|
|
323
|
+
pathname === "/automations" ||
|
|
324
|
+
pathname.startsWith("/automations/") ||
|
|
325
|
+
pathname === "/admin/automations" ||
|
|
326
|
+
pathname.startsWith("/admin/automations/")
|
|
327
|
+
) {
|
|
328
|
+
return "scheduled";
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
299
333
|
function dispatchNavLinkTarget(path: string): string {
|
|
300
334
|
if (typeof window === "undefined") return path;
|
|
301
335
|
const basePath = appBasePath();
|
|
@@ -543,6 +577,8 @@ function DispatchChatsSection({
|
|
|
543
577
|
chatFirstMode?: boolean;
|
|
544
578
|
chatFirstEmbedded?: boolean;
|
|
545
579
|
chatFirstNavigation?: {
|
|
580
|
+
activeTab?: ChatFirstPrimaryTab;
|
|
581
|
+
onNewChat?: () => void;
|
|
546
582
|
onOpenIntegrations: () => void;
|
|
547
583
|
onOpenScheduled: () => void;
|
|
548
584
|
};
|
|
@@ -683,13 +719,17 @@ function DispatchChatsSection({
|
|
|
683
719
|
)}
|
|
684
720
|
>
|
|
685
721
|
{showNewChat && chatFirstNavigation ? (
|
|
686
|
-
<nav className="space-y-0.5 px-
|
|
722
|
+
<nav className="space-y-0.5 px-2 py-2">
|
|
687
723
|
<ChatFirstPrimaryNavigation
|
|
688
724
|
copy={chatFirstCopy}
|
|
689
|
-
onNewChat={() =>
|
|
725
|
+
onNewChat={() => {
|
|
726
|
+
chatFirstNavigation.onNewChat?.();
|
|
727
|
+
void handleNewChat();
|
|
728
|
+
}}
|
|
690
729
|
onOpenIntegrations={chatFirstNavigation.onOpenIntegrations}
|
|
691
730
|
onOpenScheduled={chatFirstNavigation.onOpenScheduled}
|
|
692
731
|
onSearch={openCommandMenu}
|
|
732
|
+
activeTab={chatFirstNavigation.activeTab}
|
|
693
733
|
/>
|
|
694
734
|
</nav>
|
|
695
735
|
) : null}
|
|
@@ -816,7 +856,7 @@ function DispatchChatsSection({
|
|
|
816
856
|
) : null}
|
|
817
857
|
</>
|
|
818
858
|
)}
|
|
819
|
-
className="min-w-0"
|
|
859
|
+
className="min-w-0 px-2"
|
|
820
860
|
/>
|
|
821
861
|
) : (
|
|
822
862
|
<ChatHistoryRail
|
|
@@ -883,7 +923,7 @@ function DispatchChatsSection({
|
|
|
883
923
|
) : null}
|
|
884
924
|
</>
|
|
885
925
|
)}
|
|
886
|
-
className="min-w-0"
|
|
926
|
+
className={cn("min-w-0", chatFirstMode && "px-2")}
|
|
887
927
|
/>
|
|
888
928
|
)}
|
|
889
929
|
</div>
|
|
@@ -904,6 +944,8 @@ export function NavContent({
|
|
|
904
944
|
chatFirstAppsLoading = false,
|
|
905
945
|
chatFirstAppsError,
|
|
906
946
|
chatFirstActiveAppId,
|
|
947
|
+
chatFirstActivePrimaryTab,
|
|
948
|
+
onChatFirstNewChat,
|
|
907
949
|
onChatFirstAppOpen,
|
|
908
950
|
onChatFirstAppsRetry,
|
|
909
951
|
}: {
|
|
@@ -920,6 +962,8 @@ export function NavContent({
|
|
|
920
962
|
chatFirstAppsLoading?: boolean;
|
|
921
963
|
chatFirstAppsError?: string | null;
|
|
922
964
|
chatFirstActiveAppId?: string;
|
|
965
|
+
chatFirstActivePrimaryTab?: ChatFirstPrimaryTab;
|
|
966
|
+
onChatFirstNewChat?: () => void;
|
|
923
967
|
onChatFirstAppOpen?: (app: ChatFirstAppItem) => void;
|
|
924
968
|
onChatFirstAppsRetry?: () => void;
|
|
925
969
|
}) {
|
|
@@ -1219,6 +1263,8 @@ export function NavContent({
|
|
|
1219
1263
|
chatFirstMode
|
|
1220
1264
|
chatFirstEmbedded={chatFirstEmbedded}
|
|
1221
1265
|
chatFirstNavigation={{
|
|
1266
|
+
activeTab: chatFirstActivePrimaryTab,
|
|
1267
|
+
onNewChat: onChatFirstNewChat,
|
|
1222
1268
|
onOpenIntegrations: () => {
|
|
1223
1269
|
navigate(dispatchNavLinkTarget("/admin/integrations"));
|
|
1224
1270
|
onNavigate?.();
|
|
@@ -1290,6 +1336,11 @@ export function Layout({
|
|
|
1290
1336
|
}
|
|
1291
1337
|
});
|
|
1292
1338
|
const localPathname = localDispatchPath(location.pathname);
|
|
1339
|
+
const [electronEmbedded] = useState(() =>
|
|
1340
|
+
typeof window !== "undefined"
|
|
1341
|
+
? isElectronEmbeddedSearch(window.location.search)
|
|
1342
|
+
: false,
|
|
1343
|
+
);
|
|
1293
1344
|
const isChatRoute =
|
|
1294
1345
|
localPathname === "/chat" || localPathname.startsWith("/chat/");
|
|
1295
1346
|
const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
|
|
@@ -1307,6 +1358,23 @@ export function Layout({
|
|
|
1307
1358
|
const chatFirstMode =
|
|
1308
1359
|
extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
|
|
1309
1360
|
const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
if (!electronEmbedded || typeof window === "undefined") return;
|
|
1363
|
+
const url = new URL(window.location.href);
|
|
1364
|
+
if (isElectronEmbeddedSearch(url.search)) return;
|
|
1365
|
+
url.searchParams.set("electron", "1");
|
|
1366
|
+
window.history.replaceState(
|
|
1367
|
+
window.history.state,
|
|
1368
|
+
"",
|
|
1369
|
+
`${url.pathname}${url.search}${url.hash}`,
|
|
1370
|
+
);
|
|
1371
|
+
}, [electronEmbedded, location.pathname, location.search]);
|
|
1372
|
+
useEffect(() => {
|
|
1373
|
+
if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
navigate(dispatchNavLinkTarget("/chat"), { replace: true });
|
|
1377
|
+
}, [chatFirstEmbedded, chatFirstMode, localPathname, navigate]);
|
|
1310
1378
|
const [chatFirstAppLayout, setChatFirstAppLayout] =
|
|
1311
1379
|
useState<ChatFirstAppLayoutPreference>(() => readChatFirstAppLayout());
|
|
1312
1380
|
const chatFirstAppLayoutHydratedRef = useRef(false);
|
|
@@ -1350,7 +1418,9 @@ export function Layout({
|
|
|
1350
1418
|
const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
|
|
1351
1419
|
() =>
|
|
1352
1420
|
chatFirstAppRegistrations
|
|
1353
|
-
.filter(
|
|
1421
|
+
.filter(
|
|
1422
|
+
(app) => app.enabled && isWorkspaceAppVisibleInDefaultLaunchers(app),
|
|
1423
|
+
)
|
|
1354
1424
|
.map((app) => ({
|
|
1355
1425
|
id: app.id,
|
|
1356
1426
|
name: app.name ?? app.id,
|
|
@@ -1371,6 +1441,7 @@ export function Layout({
|
|
|
1371
1441
|
null,
|
|
1372
1442
|
);
|
|
1373
1443
|
const [chatFirstEmbedAttempt, setChatFirstEmbedAttempt] = useState(0);
|
|
1444
|
+
const chatFirstAppFrameRef = useRef<HTMLIFrameElement>(null);
|
|
1374
1445
|
const [chatFirstNotice, setChatFirstNotice] = useState<string | null>(null);
|
|
1375
1446
|
const chatFirstSessionWatch = useChatFirstSessionWatch();
|
|
1376
1447
|
const chatFirstSurfaceTabs = useChatFirstSurfaceTabs(chatFirstSurfaceScope);
|
|
@@ -1410,6 +1481,19 @@ export function Layout({
|
|
|
1410
1481
|
isChatRoute &&
|
|
1411
1482
|
activeChatFirstSurfaceTab?.kind === "app" &&
|
|
1412
1483
|
activeChatFirstSurfaceTab.placement === "main";
|
|
1484
|
+
const chatFirstAppSelected =
|
|
1485
|
+
chatFirstMode && activeChatFirstSurfaceTab?.kind === "app";
|
|
1486
|
+
const chatFirstActivePrimaryTab = chatFirstAppSelected
|
|
1487
|
+
? activeChatFirstSurfaceTab?.kind === "app" &&
|
|
1488
|
+
activeChatFirstSurfaceTab.appId === "dispatch"
|
|
1489
|
+
? chatFirstPrimaryTabForPath(activeChatFirstSurfaceTab.path ?? "")
|
|
1490
|
+
: undefined
|
|
1491
|
+
: chatFirstPrimaryTabForPath(localPathname);
|
|
1492
|
+
const chatFirstActiveAppId = isWorkspaceAppRoute
|
|
1493
|
+
? (workspaceAppIdFromRoute(localPathname) ?? undefined)
|
|
1494
|
+
: chatFirstAppSelected && activeChatFirstSurfaceTab?.kind === "app"
|
|
1495
|
+
? activeChatFirstSurfaceTab.appId
|
|
1496
|
+
: undefined;
|
|
1413
1497
|
const activeChatFirstAppRegistration = useMemo(
|
|
1414
1498
|
() =>
|
|
1415
1499
|
activeChatFirstSurfaceTab?.kind === "app" &&
|
|
@@ -1496,6 +1580,25 @@ export function Layout({
|
|
|
1496
1580
|
createChatFirstEmbedSession.mutateAsync,
|
|
1497
1581
|
isChatRoute,
|
|
1498
1582
|
]);
|
|
1583
|
+
|
|
1584
|
+
useEffect(() => {
|
|
1585
|
+
if (!chatFirstMode || !isChatRoute || !activeChatFirstApp) return;
|
|
1586
|
+
|
|
1587
|
+
const handleEmbedSessionExpired = (event: MessageEvent) => {
|
|
1588
|
+
if (
|
|
1589
|
+
event.data?.type !== "agentNative.embedSessionExpired" ||
|
|
1590
|
+
event.source !== chatFirstAppFrameRef.current?.contentWindow
|
|
1591
|
+
) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
setChatFirstEmbedAttempt((attempt) => attempt + 1);
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
window.addEventListener("message", handleEmbedSessionExpired);
|
|
1598
|
+
return () =>
|
|
1599
|
+
window.removeEventListener("message", handleEmbedSessionExpired);
|
|
1600
|
+
}, [activeChatFirstApp, chatFirstMode, isChatRoute]);
|
|
1601
|
+
|
|
1499
1602
|
const persistChatFirstPane = useCallback(
|
|
1500
1603
|
(pane: DispatchChatFirstPane | null) => {
|
|
1501
1604
|
setChatFirstPane(pane);
|
|
@@ -1507,6 +1610,17 @@ export function Layout({
|
|
|
1507
1610
|
},
|
|
1508
1611
|
[],
|
|
1509
1612
|
);
|
|
1613
|
+
useEffect(() => {
|
|
1614
|
+
if (!chatFirstMode || isChatRoute) return;
|
|
1615
|
+
persistChatFirstPane(null);
|
|
1616
|
+
closeChatFirstSessionWatch();
|
|
1617
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
1618
|
+
}, [
|
|
1619
|
+
chatFirstMode,
|
|
1620
|
+
chatFirstSurfaceTabsStore,
|
|
1621
|
+
isChatRoute,
|
|
1622
|
+
persistChatFirstPane,
|
|
1623
|
+
]);
|
|
1510
1624
|
const openChatFirstPane = useCallback(
|
|
1511
1625
|
(
|
|
1512
1626
|
pane: DispatchChatFirstPane,
|
|
@@ -1639,6 +1753,19 @@ export function Layout({
|
|
|
1639
1753
|
setChatFirstNotice(chatFirstResolutionMessage(resolution.reason));
|
|
1640
1754
|
return;
|
|
1641
1755
|
}
|
|
1756
|
+
if (isDispatchWorkspaceAppId(resolution.target.appId)) {
|
|
1757
|
+
closeChatFirstSessionWatch();
|
|
1758
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
1759
|
+
setChatFirstSurfacePanelOpen(false);
|
|
1760
|
+
persistChatFirstPane(null);
|
|
1761
|
+
if (resolution.target.path) {
|
|
1762
|
+
navigateWithAgentChatViewTransition(
|
|
1763
|
+
navigate,
|
|
1764
|
+
dispatchNavLinkTarget(resolution.target.path),
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1642
1769
|
openChatFirstPane(resolution.target);
|
|
1643
1770
|
},
|
|
1644
1771
|
[
|
|
@@ -1646,7 +1773,12 @@ export function Layout({
|
|
|
1646
1773
|
chatFirstAppsQuery.isError,
|
|
1647
1774
|
chatFirstAppsQuery.isLoading,
|
|
1648
1775
|
chatFirstGrantedAppsQuery.isLoading,
|
|
1776
|
+
chatFirstSurfaceTabsStore,
|
|
1777
|
+
closeChatFirstSessionWatch,
|
|
1649
1778
|
openChatFirstPane,
|
|
1779
|
+
navigate,
|
|
1780
|
+
persistChatFirstPane,
|
|
1781
|
+
setChatFirstSurfacePanelOpen,
|
|
1650
1782
|
],
|
|
1651
1783
|
);
|
|
1652
1784
|
const chatHomeHandoffActive = useAgentChatHomeHandoff({
|
|
@@ -2023,6 +2155,7 @@ export function Layout({
|
|
|
2023
2155
|
data-dispatch-chat-first-app-frame
|
|
2024
2156
|
src={url}
|
|
2025
2157
|
title={title ?? chatFirstCopy("appUnavailable")}
|
|
2158
|
+
ref={chatFirstAppFrameRef}
|
|
2026
2159
|
referrerPolicy="no-referrer"
|
|
2027
2160
|
allow="clipboard-read; clipboard-write"
|
|
2028
2161
|
className="h-full w-full border-0 bg-background"
|
|
@@ -2113,6 +2246,29 @@ export function Layout({
|
|
|
2113
2246
|
return <>{children}</>;
|
|
2114
2247
|
}
|
|
2115
2248
|
|
|
2249
|
+
if (electronEmbedded) {
|
|
2250
|
+
return (
|
|
2251
|
+
<DispatchExtensionsContext.Provider value={extensions}>
|
|
2252
|
+
<HeaderActionsProvider>
|
|
2253
|
+
<div
|
|
2254
|
+
data-dispatch-electron-control-plane
|
|
2255
|
+
className="flex h-screen w-full overflow-hidden bg-background"
|
|
2256
|
+
>
|
|
2257
|
+
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
2258
|
+
<Header showAgentToggle={false} />
|
|
2259
|
+
<InvitationBanner />
|
|
2260
|
+
<main className="flex-1 overflow-y-auto">
|
|
2261
|
+
<div className="mx-auto max-w-7xl space-y-10 px-4 py-6 sm:px-6">
|
|
2262
|
+
{children}
|
|
2263
|
+
</div>
|
|
2264
|
+
</main>
|
|
2265
|
+
</div>
|
|
2266
|
+
</div>
|
|
2267
|
+
</HeaderActionsProvider>
|
|
2268
|
+
</DispatchExtensionsContext.Provider>
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2116
2272
|
if (isWorkspaceAppHostRoute) {
|
|
2117
2273
|
return (
|
|
2118
2274
|
<DispatchExtensionsContext.Provider value={extensions}>
|
|
@@ -2305,14 +2461,18 @@ export function Layout({
|
|
|
2305
2461
|
? chatFirstCopy("appsLoadError")
|
|
2306
2462
|
: null
|
|
2307
2463
|
}
|
|
2308
|
-
chatFirstActiveAppId={
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2464
|
+
chatFirstActiveAppId={chatFirstActiveAppId}
|
|
2465
|
+
chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
|
|
2466
|
+
onChatFirstNewChat={() => {
|
|
2467
|
+
closeChatFirstSessionWatch();
|
|
2468
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
2469
|
+
setChatFirstSurfacePanelOpen(false);
|
|
2470
|
+
}}
|
|
2471
|
+
onChatFirstAppOpen={(app) => {
|
|
2472
|
+
if (isDispatchWorkspaceAppId(app.id)) return;
|
|
2473
|
+
setSidebarCollapsed(true);
|
|
2474
|
+
navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
|
|
2475
|
+
}}
|
|
2316
2476
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2317
2477
|
collapsible
|
|
2318
2478
|
onCollapsedChange={setSidebarCollapsed}
|
|
@@ -2345,13 +2505,15 @@ export function Layout({
|
|
|
2345
2505
|
? chatFirstCopy("appsLoadError")
|
|
2346
2506
|
: null
|
|
2347
2507
|
}
|
|
2348
|
-
chatFirstActiveAppId={
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2508
|
+
chatFirstActiveAppId={chatFirstActiveAppId}
|
|
2509
|
+
chatFirstActivePrimaryTab={chatFirstActivePrimaryTab}
|
|
2510
|
+
onChatFirstNewChat={() => {
|
|
2511
|
+
closeChatFirstSessionWatch();
|
|
2512
|
+
chatFirstSurfaceTabsStore.closeAll();
|
|
2513
|
+
setChatFirstSurfacePanelOpen(false);
|
|
2514
|
+
}}
|
|
2353
2515
|
onChatFirstAppOpen={(app) =>
|
|
2354
|
-
|
|
2516
|
+
navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
|
|
2355
2517
|
}
|
|
2356
2518
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2357
2519
|
onNavigate={() => setMobileOpen(false)}
|
|
@@ -5,6 +5,8 @@ import { Link, useLocation } from "react-router";
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../lib/utils";
|
|
7
7
|
import {
|
|
8
|
+
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
9
|
+
workspaceAppIdFromRoute,
|
|
8
10
|
workspaceAppRoute,
|
|
9
11
|
workspaceAppHref,
|
|
10
12
|
type WorkspaceAppSummary,
|
|
@@ -12,32 +14,8 @@ import {
|
|
|
12
14
|
import { AppIcon } from "../app-icon";
|
|
13
15
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
14
16
|
|
|
15
|
-
function pathFromValue(value: string | null | undefined): string | null {
|
|
16
|
-
const trimmed = value?.trim();
|
|
17
|
-
if (!trimmed) return null;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const pathname = new URL(trimmed, "https://dispatch.local").pathname;
|
|
21
|
-
return pathname.replace(/\/+$/, "") || "/";
|
|
22
|
-
} catch {
|
|
23
|
-
// coercion-ok: malformed app URLs cannot match a workspace route.
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
17
|
function appMatchesPath(app: WorkspaceAppSummary, pathname: string): boolean {
|
|
29
|
-
|
|
30
|
-
if (pathname === appRoute || pathname.startsWith(`${appRoute}/`)) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const candidatePaths = [app.path, app.url]
|
|
35
|
-
.map(pathFromValue)
|
|
36
|
-
.filter((path): path is string => !!path);
|
|
37
|
-
|
|
38
|
-
return candidatePaths.some(
|
|
39
|
-
(path) => pathname === path || pathname.startsWith(`${path}/`),
|
|
40
|
-
);
|
|
18
|
+
return workspaceAppIdFromRoute(pathname) === app.id;
|
|
41
19
|
}
|
|
42
20
|
|
|
43
21
|
function appLabel(app: WorkspaceAppSummary): string {
|
|
@@ -64,7 +42,7 @@ export function WorkspaceAppsRail({
|
|
|
64
42
|
const apps = appsQuery.data
|
|
65
43
|
.filter(
|
|
66
44
|
(app) =>
|
|
67
|
-
|
|
45
|
+
isWorkspaceAppVisibleInDefaultLaunchers(app) &&
|
|
68
46
|
!app.archived &&
|
|
69
47
|
app.status !== "pending" &&
|
|
70
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
|
}
|
|
@@ -62,21 +62,21 @@ describe("WorkspaceAppCard", () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
const appLink = container.querySelector<HTMLAnchorElement>(
|
|
65
|
-
'a[href="/
|
|
65
|
+
'a[href="/analytics"]',
|
|
66
66
|
);
|
|
67
67
|
expect(appLink).not.toBeNull();
|
|
68
68
|
expect(appLink?.textContent).toContain("Open app");
|
|
69
|
-
expect(appLink?.
|
|
70
|
-
expect(appLink?.getAttribute("
|
|
71
|
-
expect(appLink?.getAttribute("rel")).toBeNull();
|
|
69
|
+
expect(appLink?.getAttribute("target")).toBe("_blank");
|
|
70
|
+
expect(appLink?.getAttribute("rel")).toBe("noreferrer");
|
|
72
71
|
expect(appLink?.querySelector("svg")).toBeNull();
|
|
73
|
-
expect(container.textContent).not.toContain("/apps/analytics");
|
|
74
72
|
|
|
75
73
|
expect(
|
|
76
74
|
container.querySelector(
|
|
77
75
|
'button[aria-label="Open options for Analytics"]',
|
|
78
76
|
),
|
|
79
|
-
).toBeNull();
|
|
77
|
+
).not.toBeNull();
|
|
78
|
+
expect(document.body.textContent).not.toContain("Open in new tab");
|
|
79
|
+
expect(document.body.textContent).not.toContain("Add app");
|
|
80
80
|
|
|
81
81
|
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
82
82
|
'button[aria-label="Settings for Analytics"]',
|