@agent-native/dispatch 0.23.5 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/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 +12 -12
- 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/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +21 -5
- package/dist/components/admin-navigation.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 +1 -1
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +1 -17
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +1 -20
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/app-row-settings.d.ts +6 -0
- package/dist/components/app-row-settings.d.ts.map +1 -0
- package/dist/components/app-row-settings.js +11 -0
- package/dist/components/app-row-settings.js.map +1 -0
- package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
- package/dist/components/automation-run-history-dialog.js +3 -2
- package/dist/components/automation-run-history-dialog.js.map +1 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +3 -2
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/create-app-popover.js +2 -2
- 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 +23 -7
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/dispatch-shell.d.ts.map +1 -1
- package/dist/components/dispatch-shell.js +5 -1
- package/dist/components/dispatch-shell.js.map +1 -1
- package/dist/components/docs-link.d.ts +16 -0
- package/dist/components/docs-link.d.ts.map +1 -0
- package/dist/components/docs-link.js +34 -0
- package/dist/components/docs-link.js.map +1 -0
- 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 +43 -35
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/messaging-setup-panel.d.ts.map +1 -1
- package/dist/components/messaging-setup-panel.js +3 -2
- package/dist/components/messaging-setup-panel.js.map +1 -1
- package/dist/components/usage-alerts-panel.d.ts +11 -0
- package/dist/components/usage-alerts-panel.d.ts.map +1 -0
- package/dist/components/usage-alerts-panel.js +125 -0
- package/dist/components/usage-alerts-panel.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts +3 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +70 -29
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-app-search.d.ts +10 -0
- package/dist/components/workspace-app-search.d.ts.map +1 -0
- package/dist/components/workspace-app-search.js +27 -0
- package/dist/components/workspace-app-search.js.map +1 -0
- package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
- package/dist/components/workspace-resource-effective-stack.js +3 -2
- package/dist/components/workspace-resource-effective-stack.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +10 -15
- package/dist/components/workspace-template-card.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.map +1 -1
- package/dist/lib/workspace-app-layout.d.ts +19 -0
- package/dist/lib/workspace-app-layout.d.ts.map +1 -0
- package/dist/lib/workspace-app-layout.js +75 -0
- package/dist/lib/workspace-app-layout.js.map +1 -0
- package/dist/lib/workspace-apps.d.ts +14 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +66 -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 +33 -0
- package/dist/lib/workspace-apps.test.js.map +1 -0
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +4 -2
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +2 -3
- 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 +17 -13
- 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 +31 -16
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/audit.d.ts.map +1 -1
- package/dist/routes/pages/audit.js +2 -1
- package/dist/routes/pages/audit.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/dreams.d.ts.map +1 -1
- package/dist/routes/pages/dreams.js +14 -18
- package/dist/routes/pages/dreams.js.map +1 -1
- package/dist/routes/pages/integrations.d.ts.map +1 -1
- package/dist/routes/pages/integrations.js +51 -14
- package/dist/routes/pages/integrations.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +146 -17
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/operations.js +1 -1
- package/dist/routes/pages/operations.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +19 -2
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +5 -8
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/routes/pages/vault.d.ts.map +1 -1
- package/dist/routes/pages/vault.js +1 -1
- package/dist/routes/pages/vault.js.map +1 -1
- package/dist/routes/pages/workspace.d.ts.map +1 -1
- package/dist/routes/pages/workspace.js +13 -10
- package/dist/routes/pages/workspace.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +12 -52
- package/dist/server/lib/app-creation-store.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/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +1 -0
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/app-roles.d.ts +9 -0
- package/dist/shared/app-roles.d.ts.map +1 -0
- package/dist/shared/app-roles.js +7 -0
- package/dist/shared/app-roles.js.map +1 -0
- package/package.json +4 -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/admin-navigation.tsx +57 -11
- package/src/components/app-icon.spec.tsx +34 -0
- package/src/components/app-icon.tsx +1 -1
- package/src/components/app-list-row.tsx +1 -2
- package/src/components/app-open-actions.tsx +5 -114
- package/src/components/app-row-settings.tsx +54 -0
- package/src/components/automation-run-history-dialog.tsx +8 -4
- package/src/components/connected-app-card.tsx +3 -2
- package/src/components/create-app-popover.spec.tsx +1 -1
- package/src/components/create-app-popover.tsx +2 -2
- package/src/components/dispatch-control-plane.spec.tsx +44 -3
- package/src/components/dispatch-control-plane.tsx +83 -27
- package/src/components/dispatch-shell.tsx +7 -1
- package/src/components/docs-link.tsx +52 -0
- package/src/components/layout/Layout.spec.tsx +13 -0
- package/src/components/layout/Layout.tsx +63 -32
- package/src/components/messaging-setup-panel.tsx +10 -3
- package/src/components/usage-alerts-panel.tsx +445 -0
- package/src/components/workspace-app-card.spec.tsx +130 -9
- package/src/components/workspace-app-card.tsx +128 -71
- package/src/components/workspace-app-search.tsx +96 -0
- package/src/components/workspace-resource-effective-stack.tsx +10 -5
- package/src/components/workspace-template-card.spec.tsx +53 -24
- package/src/components/workspace-template-card.tsx +16 -26
- 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.ts +2 -0
- package/src/lib/workspace-app-layout.spec.ts +47 -0
- package/src/lib/workspace-app-layout.ts +121 -0
- package/src/lib/workspace-apps.test.ts +64 -0
- package/src/lib/workspace-apps.ts +82 -0
- package/src/routes/pages/$appId.tsx +3 -6
- package/src/routes/pages/agents.tsx +6 -3
- package/src/routes/pages/apps.$appId.tsx +16 -17
- package/src/routes/pages/apps.tsx +116 -62
- package/src/routes/pages/audit.tsx +28 -8
- package/src/routes/pages/chat.spec.tsx +10 -0
- package/src/routes/pages/chat.tsx +8 -0
- package/src/routes/pages/dreams.tsx +92 -142
- package/src/routes/pages/integrations.tsx +147 -68
- package/src/routes/pages/metrics.tsx +460 -102
- package/src/routes/pages/operations.tsx +2 -2
- package/src/routes/pages/settings.tsx +27 -10
- package/src/routes/pages/thread-debug.tsx +18 -36
- package/src/routes/pages/vault.tsx +7 -2
- package/src/routes/pages/workspace.tsx +92 -56
- package/src/server/index.ts +4 -0
- package/src/server/lib/app-creation-store.spec.ts +90 -43
- package/src/server/lib/app-creation-store.ts +15 -61
- 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/integrations.ts +1 -0
- package/src/shared/app-roles.ts +8 -0
- package/src/styles/dispatch.css +9 -0
|
@@ -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,
|
|
@@ -12,6 +13,7 @@ import { cn } from "../lib/utils";
|
|
|
12
13
|
import { AppIcon } from "./app-icon";
|
|
13
14
|
import { AppListRow } from "./app-list-row";
|
|
14
15
|
import { AppOpenActions } from "./app-open-actions";
|
|
16
|
+
import { AppRowSettings } from "./app-row-settings";
|
|
15
17
|
import { Button } from "./ui/button";
|
|
16
18
|
import {
|
|
17
19
|
Dialog,
|
|
@@ -94,17 +96,6 @@ export interface WorkspaceTemplateCardProps {
|
|
|
94
96
|
) => void;
|
|
95
97
|
}
|
|
96
98
|
|
|
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
99
|
function templateIdFor(template: CuratedWorkspaceTemplate): string {
|
|
109
100
|
return template.templateId || template.id || template.appId || template.name;
|
|
110
101
|
}
|
|
@@ -113,8 +104,9 @@ function defaultAppIdFor(
|
|
|
113
104
|
template: CuratedWorkspaceTemplate,
|
|
114
105
|
defaultAppId?: string,
|
|
115
106
|
): string {
|
|
116
|
-
if (defaultAppId) return
|
|
117
|
-
const sourceId =
|
|
107
|
+
if (defaultAppId) return normalizeWorkspaceAppId(defaultAppId) || "new-app";
|
|
108
|
+
const sourceId =
|
|
109
|
+
normalizeWorkspaceAppId(template.appId || template.name) || "new-app";
|
|
118
110
|
return `${sourceId}-app`;
|
|
119
111
|
}
|
|
120
112
|
|
|
@@ -173,15 +165,16 @@ export function WorkspaceTemplateCard({
|
|
|
173
165
|
}, [defaultAppId, open, template]);
|
|
174
166
|
|
|
175
167
|
function submitRemix() {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
168
|
+
const normalizedAppId = normalizeWorkspaceAppId(appId);
|
|
169
|
+
if (!normalizedAppId) {
|
|
178
170
|
toast.error(labels.appIdRequired);
|
|
179
171
|
return;
|
|
180
172
|
}
|
|
173
|
+
setAppId(normalizedAppId);
|
|
181
174
|
|
|
182
175
|
remix.mutate({
|
|
183
176
|
templateId: templateIdFor(template),
|
|
184
|
-
appId:
|
|
177
|
+
appId: normalizedAppId,
|
|
185
178
|
});
|
|
186
179
|
}
|
|
187
180
|
|
|
@@ -219,20 +212,16 @@ export function WorkspaceTemplateCard({
|
|
|
219
212
|
<AppOpenActions
|
|
220
213
|
name={template.name}
|
|
221
214
|
href={liveUrl}
|
|
222
|
-
target="_blank"
|
|
223
215
|
rel="noreferrer"
|
|
224
216
|
labels={{ openApp: labels.openApp }}
|
|
217
|
+
showNewTabOption
|
|
225
218
|
/>
|
|
226
219
|
) : null}
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
>
|
|
233
|
-
<IconPlus />
|
|
234
|
-
{labels.remix}
|
|
235
|
-
</Button>
|
|
220
|
+
<AppRowSettings
|
|
221
|
+
name={template.name}
|
|
222
|
+
addAppLabel={labels.remix}
|
|
223
|
+
onAddApp={() => setOpen(true)}
|
|
224
|
+
/>
|
|
236
225
|
</>
|
|
237
226
|
) : liveUrl ? (
|
|
238
227
|
<Button variant="ghost" size="sm" className="shrink-0" asChild>
|
|
@@ -280,6 +269,7 @@ export function WorkspaceTemplateCard({
|
|
|
280
269
|
value={appId}
|
|
281
270
|
autoComplete="off"
|
|
282
271
|
onChange={(event) => setAppId(event.target.value)}
|
|
272
|
+
onBlur={() => setAppId(normalizeWorkspaceAppId(appId))}
|
|
283
273
|
disabled={remix.isPending}
|
|
284
274
|
/>
|
|
285
275
|
</div>
|
|
@@ -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
|
+
}
|
package/src/lib/other-apps.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface ConnectedAppSummary {
|
|
|
5
5
|
name: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
url: string;
|
|
8
|
+
/** Canonical app-home URL used for launchers; `url` remains the A2A endpoint. */
|
|
9
|
+
homeUrl?: string;
|
|
8
10
|
color?: string;
|
|
9
11
|
source?: "builtin" | "custom" | "workspace";
|
|
10
12
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeWorkspaceAppLayout,
|
|
5
|
+
orderWorkspaceApps,
|
|
6
|
+
toggleWorkspaceAppPinned,
|
|
7
|
+
workspaceAppMatchesQuery,
|
|
8
|
+
} from "./workspace-app-layout";
|
|
9
|
+
|
|
10
|
+
describe("workspace app layout", () => {
|
|
11
|
+
it("keeps pinned apps first without changing the remaining catalog order", () => {
|
|
12
|
+
const apps = [
|
|
13
|
+
{ id: "mail", name: "Mail" },
|
|
14
|
+
{ id: "analytics", name: "Analytics" },
|
|
15
|
+
{ id: "calendar", name: "Calendar" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
expect(
|
|
19
|
+
orderWorkspaceApps(apps, {
|
|
20
|
+
pinnedIds: ["calendar", "calendar"],
|
|
21
|
+
orderedIds: ["mail", "analytics", "calendar"],
|
|
22
|
+
}).map((app) => app.id),
|
|
23
|
+
).toEqual(["calendar", "mail", "analytics"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("matches app names and descriptions case-insensitively", () => {
|
|
27
|
+
expect(
|
|
28
|
+
workspaceAppMatchesQuery(
|
|
29
|
+
{ name: "Analytics", description: "Explore product health" },
|
|
30
|
+
"PRODUCT",
|
|
31
|
+
),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
expect(workspaceAppMatchesQuery({ name: "Mail" }, "calendar")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("normalizes persisted layout ids and toggles pin state", () => {
|
|
37
|
+
const layout = normalizeWorkspaceAppLayout({
|
|
38
|
+
pinnedIds: ["mail", "mail", 42],
|
|
39
|
+
orderedIds: ["calendar", "", "calendar"],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(toggleWorkspaceAppPinned(layout, "calendar")).toEqual({
|
|
43
|
+
pinnedIds: ["calendar", "mail"],
|
|
44
|
+
orderedIds: ["calendar"],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
orderChatFirstAppIds,
|
|
3
|
+
readChatFirstAppLayout,
|
|
4
|
+
writeChatFirstAppLayout,
|
|
5
|
+
type ChatFirstAppLayoutPreference,
|
|
6
|
+
} from "@agent-native/core/client/agent-chat";
|
|
7
|
+
import {
|
|
8
|
+
readClientAppState,
|
|
9
|
+
writeClientAppState,
|
|
10
|
+
} from "@agent-native/core/client/application-state";
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
|
|
13
|
+
export const WORKSPACE_APP_LAYOUT_STATE_KEY = "chat-first-app-layout";
|
|
14
|
+
|
|
15
|
+
export type WorkspaceAppLayoutPersistenceError =
|
|
16
|
+
| "device"
|
|
17
|
+
| "workspace"
|
|
18
|
+
| "both";
|
|
19
|
+
|
|
20
|
+
export function normalizeWorkspaceAppLayout(
|
|
21
|
+
value: unknown,
|
|
22
|
+
): ChatFirstAppLayoutPreference {
|
|
23
|
+
if (!value || typeof value !== "object") {
|
|
24
|
+
return { pinnedIds: [], orderedIds: [] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const candidate = value as Partial<ChatFirstAppLayoutPreference>;
|
|
28
|
+
const ids = (input: unknown): string[] =>
|
|
29
|
+
Array.isArray(input)
|
|
30
|
+
? input.filter(
|
|
31
|
+
(id): id is string => typeof id === "string" && id.trim().length > 0,
|
|
32
|
+
)
|
|
33
|
+
: [];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
pinnedIds: [...new Set(ids(candidate.pinnedIds))],
|
|
37
|
+
orderedIds: [...new Set(ids(candidate.orderedIds))],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function orderWorkspaceApps<T extends { id: string }>(
|
|
42
|
+
apps: readonly T[],
|
|
43
|
+
layout: ChatFirstAppLayoutPreference,
|
|
44
|
+
): T[] {
|
|
45
|
+
const appsById = new Map(apps.map((app) => [app.id, app]));
|
|
46
|
+
return orderChatFirstAppIds(
|
|
47
|
+
apps.map((app) => app.id),
|
|
48
|
+
layout,
|
|
49
|
+
)
|
|
50
|
+
.map((id) => appsById.get(id))
|
|
51
|
+
.filter((app): app is T => Boolean(app));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function workspaceAppMatchesQuery(
|
|
55
|
+
app: { name: string; description?: string },
|
|
56
|
+
query: string,
|
|
57
|
+
): boolean {
|
|
58
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
59
|
+
if (!normalizedQuery) return true;
|
|
60
|
+
return `${app.name} ${app.description ?? ""}`
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.includes(normalizedQuery);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toggleWorkspaceAppPinned(
|
|
66
|
+
layout: ChatFirstAppLayoutPreference,
|
|
67
|
+
appId: string,
|
|
68
|
+
): ChatFirstAppLayoutPreference {
|
|
69
|
+
const pinnedIds = layout.pinnedIds.includes(appId)
|
|
70
|
+
? layout.pinnedIds.filter((id) => id !== appId)
|
|
71
|
+
: [appId, ...layout.pinnedIds];
|
|
72
|
+
return { ...layout, pinnedIds };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function useWorkspaceAppLayout() {
|
|
76
|
+
const [layout, setLayout] = useState<ChatFirstAppLayoutPreference>(() =>
|
|
77
|
+
readChatFirstAppLayout(),
|
|
78
|
+
);
|
|
79
|
+
const [persistenceError, setPersistenceError] =
|
|
80
|
+
useState<WorkspaceAppLayoutPersistenceError | null>(null);
|
|
81
|
+
const hydratedRef = useRef(false);
|
|
82
|
+
const localChangeRef = useRef(false);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (hydratedRef.current) return;
|
|
86
|
+
hydratedRef.current = true;
|
|
87
|
+
void readClientAppState<unknown>(WORKSPACE_APP_LAYOUT_STATE_KEY)
|
|
88
|
+
.then((value) => {
|
|
89
|
+
if (value !== null && !localChangeRef.current) {
|
|
90
|
+
setLayout(normalizeWorkspaceAppLayout(value));
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.catch(() => {
|
|
94
|
+
// Device-local preferences remain usable when workspace state is unavailable.
|
|
95
|
+
});
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const persistLayout = useCallback((next: ChatFirstAppLayoutPreference) => {
|
|
99
|
+
localChangeRef.current = true;
|
|
100
|
+
setLayout(next);
|
|
101
|
+
const deviceResult = writeChatFirstAppLayout(next);
|
|
102
|
+
const deviceFailed = !deviceResult.ok;
|
|
103
|
+
|
|
104
|
+
void writeClientAppState(WORKSPACE_APP_LAYOUT_STATE_KEY, next)
|
|
105
|
+
.then(() => {
|
|
106
|
+
setPersistenceError(deviceFailed ? "device" : null);
|
|
107
|
+
})
|
|
108
|
+
.catch(() => {
|
|
109
|
+
setPersistenceError(deviceFailed ? "both" : "workspace");
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const togglePinned = useCallback(
|
|
114
|
+
(appId: string) => {
|
|
115
|
+
persistLayout(toggleWorkspaceAppPinned(layout, appId));
|
|
116
|
+
},
|
|
117
|
+
[layout, persistLayout],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return { layout, persistenceError, persistLayout, togglePinned };
|
|
121
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
workspaceAppDirectHref,
|
|
5
|
+
workspaceAppEmbedTarget,
|
|
6
|
+
} from "./workspace-apps";
|
|
7
|
+
|
|
8
|
+
describe("workspaceAppEmbedTarget", () => {
|
|
9
|
+
it("uses the app URL as the embed root when a mount path is also present", () => {
|
|
10
|
+
expect(
|
|
11
|
+
workspaceAppEmbedTarget({
|
|
12
|
+
path: "/content",
|
|
13
|
+
url: "https://workspace.example.test/content",
|
|
14
|
+
}),
|
|
15
|
+
).toEqual({ url: "https://workspace.example.test/content" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("falls back to the mounted path when no app URL is available", () => {
|
|
19
|
+
expect(workspaceAppEmbedTarget({ path: "/content", url: null })).toEqual({
|
|
20
|
+
path: "/content",
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("workspaceAppDirectHref", () => {
|
|
26
|
+
it("joins an app-relative route onto an absolute app URL", () => {
|
|
27
|
+
expect(
|
|
28
|
+
workspaceAppDirectHref(
|
|
29
|
+
{ path: "/atlas", url: "https://workspace.example.test/atlas" },
|
|
30
|
+
"/emails?status=failed#latest",
|
|
31
|
+
),
|
|
32
|
+
).toBe("https://workspace.example.test/atlas/emails?status=failed#latest");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("does not duplicate a mount path already present in the target", () => {
|
|
36
|
+
expect(
|
|
37
|
+
workspaceAppDirectHref(
|
|
38
|
+
{ path: "/atlas", url: "https://workspace.example.test/atlas" },
|
|
39
|
+
"/atlas",
|
|
40
|
+
),
|
|
41
|
+
).toBe("https://workspace.example.test/atlas");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("resolves a mounted relative app path", () => {
|
|
45
|
+
expect(workspaceAppDirectHref({ path: "/atlas" }, "/emails")).toBe(
|
|
46
|
+
"/atlas/emails",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("falls back to a safe mount path when the app URL is invalid", () => {
|
|
51
|
+
expect(
|
|
52
|
+
workspaceAppDirectHref(
|
|
53
|
+
{ path: "/atlas", url: "javascript:alert(1)" },
|
|
54
|
+
"/",
|
|
55
|
+
),
|
|
56
|
+
).toBe("/atlas");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects an unsafe target path", () => {
|
|
60
|
+
expect(workspaceAppDirectHref({ path: "/atlas" }, "//evil.example")).toBe(
|
|
61
|
+
null,
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -27,6 +27,11 @@ export interface WorkspaceAppSummary {
|
|
|
27
27
|
archived?: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
interface WorkspaceAppHrefSource {
|
|
31
|
+
path?: string | null;
|
|
32
|
+
url?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export function isDispatchWorkspaceAppId(appId: string): boolean {
|
|
31
36
|
return appId.trim().toLowerCase() === "dispatch";
|
|
32
37
|
}
|
|
@@ -70,6 +75,83 @@ export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
|
|
|
70
75
|
return app.path || app.url || null;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
export function workspaceAppEmbedTarget(
|
|
79
|
+
app: Pick<WorkspaceAppSummary, "path" | "url">,
|
|
80
|
+
): { path?: string; url?: string } {
|
|
81
|
+
const url = app.url?.trim();
|
|
82
|
+
if (url) return { url };
|
|
83
|
+
|
|
84
|
+
const path = app.path.trim();
|
|
85
|
+
return path.startsWith("/") ? { path } : path ? { url: path } : {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve an app route without an embed ticket so the target can render its
|
|
90
|
+
* own error document when session setup fails.
|
|
91
|
+
*/
|
|
92
|
+
export function workspaceAppDirectHref(
|
|
93
|
+
app: WorkspaceAppHrefSource,
|
|
94
|
+
targetPath: string,
|
|
95
|
+
): string | null {
|
|
96
|
+
const target = targetPath.trim();
|
|
97
|
+
if (!target || !target.startsWith("/") || target.startsWith("//")) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let targetUrl: URL;
|
|
102
|
+
try {
|
|
103
|
+
targetUrl = new URL(target, "https://agent-native.invalid");
|
|
104
|
+
} catch {
|
|
105
|
+
// coercion-ok: invalid relative target input has no safe href.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const targetPathname = targetUrl.pathname || "/";
|
|
109
|
+
|
|
110
|
+
let absoluteBase: URL | null = null;
|
|
111
|
+
const rawUrl = app.url?.trim();
|
|
112
|
+
if (rawUrl) {
|
|
113
|
+
try {
|
|
114
|
+
const parsedBase = new URL(rawUrl);
|
|
115
|
+
if (parsedBase.protocol === "http:" || parsedBase.protocol === "https:") {
|
|
116
|
+
absoluteBase = parsedBase;
|
|
117
|
+
}
|
|
118
|
+
// coercion-ok: invalid app URLs use the mounted path fallback below.
|
|
119
|
+
} catch {
|
|
120
|
+
absoluteBase = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const mountedPath = app.path?.trim();
|
|
125
|
+
const basePath = absoluteBase
|
|
126
|
+
? absoluteBase.pathname
|
|
127
|
+
: mountedPath
|
|
128
|
+
? `/${mountedPath.replace(/^[/\\]+/, "").split(/[?#]/, 1)[0]}`
|
|
129
|
+
: null;
|
|
130
|
+
if (!basePath) return null;
|
|
131
|
+
|
|
132
|
+
const normalizedBasePath = basePath.replace(/\/+$/, "") || "/";
|
|
133
|
+
const targetIsMountedPath =
|
|
134
|
+
normalizedBasePath !== "/" &&
|
|
135
|
+
(targetPathname === normalizedBasePath ||
|
|
136
|
+
targetPathname.startsWith(`${normalizedBasePath}/`));
|
|
137
|
+
const resolvedPath = targetIsMountedPath
|
|
138
|
+
? targetPathname
|
|
139
|
+
: normalizedBasePath === "/"
|
|
140
|
+
? targetPathname
|
|
141
|
+
: targetPathname === "/"
|
|
142
|
+
? normalizedBasePath
|
|
143
|
+
: `${normalizedBasePath}/${targetPathname.replace(/^\/+/, "")}`;
|
|
144
|
+
|
|
145
|
+
if (absoluteBase) {
|
|
146
|
+
absoluteBase.pathname = resolvedPath;
|
|
147
|
+
absoluteBase.search = targetUrl.search;
|
|
148
|
+
absoluteBase.hash = targetUrl.hash;
|
|
149
|
+
return absoluteBase.toString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return `${resolvedPath}${targetUrl.search}${targetUrl.hash}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
|
|
74
156
|
return app.status === "pending" && !!app.builderUrl;
|
|
75
157
|
}
|
|
@@ -176,11 +176,6 @@ export default function WorkspaceAppCatchAllRoute() {
|
|
|
176
176
|
<span className="font-mono text-foreground">{app.path}</span>{" "}
|
|
177
177
|
{t("dispatch.pages.appBuildingSuffix")}
|
|
178
178
|
</p>
|
|
179
|
-
{app.branchName ? (
|
|
180
|
-
<p className="text-xs text-muted-foreground">
|
|
181
|
-
{t("dispatch.pages.branch", { branch: app.branchName })}
|
|
182
|
-
</p>
|
|
183
|
-
) : null}
|
|
184
179
|
{app.builderUrl ? (
|
|
185
180
|
<Button asChild>
|
|
186
181
|
<a
|
|
@@ -191,7 +186,9 @@ export default function WorkspaceAppCatchAllRoute() {
|
|
|
191
186
|
target="_blank"
|
|
192
187
|
rel="noreferrer"
|
|
193
188
|
>
|
|
194
|
-
{t("dispatch.pages.openBuilderBranch"
|
|
189
|
+
{t("dispatch.pages.openBuilderBranch", {
|
|
190
|
+
defaultValue: "Open in Builder",
|
|
191
|
+
})}
|
|
195
192
|
<IconArrowUpRight size={15} className="ml-1.5" />
|
|
196
193
|
</a>
|
|
197
194
|
</Button>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DispatchShell } from "../../components/dispatch-shell";
|
|
16
16
|
import { Button } from "../../components/ui/button";
|
|
17
17
|
import { Input } from "../../components/ui/input";
|
|
18
|
+
import { Skeleton } from "../../components/ui/skeleton";
|
|
18
19
|
import { Switch } from "../../components/ui/switch";
|
|
19
20
|
|
|
20
21
|
export function meta() {
|
|
@@ -123,9 +124,11 @@ function DispatchMcpAccessPanel() {
|
|
|
123
124
|
: t("dispatch.pages.selectedApps")}
|
|
124
125
|
</div>
|
|
125
126
|
<div className="text-xs text-muted-foreground">
|
|
126
|
-
{isLoading
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
{isLoading ? (
|
|
128
|
+
<Skeleton className="h-3 w-16" />
|
|
129
|
+
) : (
|
|
130
|
+
t("dispatch.pages.grantedCount", { count: grantedCount })
|
|
131
|
+
)}
|
|
129
132
|
</div>
|
|
130
133
|
</div>
|
|
131
134
|
<Switch
|
|
@@ -13,7 +13,10 @@ import { Badge } from "../../components/ui/badge";
|
|
|
13
13
|
import { Button } from "../../components/ui/button";
|
|
14
14
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
15
15
|
import { Spinner } from "../../components/ui/spinner";
|
|
16
|
+
import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
|
|
16
17
|
import {
|
|
18
|
+
workspaceAppDirectHref,
|
|
19
|
+
workspaceAppEmbedTarget,
|
|
17
20
|
workspaceAppHref,
|
|
18
21
|
type WorkspaceAppSummary,
|
|
19
22
|
} from "../../lib/workspace-apps";
|
|
@@ -61,12 +64,12 @@ export default function WorkspaceAppRoute() {
|
|
|
61
64
|
});
|
|
62
65
|
const embedInput = useMemo<EmbedSessionInput | null>(() => {
|
|
63
66
|
if (!app || !href) return null;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}, [app?.id, app?.path, href]);
|
|
67
|
+
return {
|
|
68
|
+
app: app.id,
|
|
69
|
+
...workspaceAppEmbedTarget(app),
|
|
70
|
+
chrome: "minimal",
|
|
71
|
+
};
|
|
72
|
+
}, [app?.id, app?.path, app?.url, href]);
|
|
70
73
|
|
|
71
74
|
useEffect(() => {
|
|
72
75
|
if (!app || app.status === "pending" || !embedInput) return;
|
|
@@ -80,6 +83,8 @@ export default function WorkspaceAppRoute() {
|
|
|
80
83
|
})
|
|
81
84
|
.catch((cause: unknown) => {
|
|
82
85
|
if (!cancelled) {
|
|
86
|
+
const directAppUrl = workspaceAppDirectHref(app, "/");
|
|
87
|
+
if (directAppUrl) setEmbedUrl(directAppUrl);
|
|
83
88
|
setEmbedError(
|
|
84
89
|
cause instanceof Error ? cause : new Error(String(cause)),
|
|
85
90
|
);
|
|
@@ -99,12 +104,8 @@ export default function WorkspaceAppRoute() {
|
|
|
99
104
|
|
|
100
105
|
useEffect(() => {
|
|
101
106
|
const handleEmbedSessionExpired = (event: MessageEvent) => {
|
|
102
|
-
if (
|
|
103
|
-
event.data?.type !== "agentNative.embedSessionExpired" ||
|
|
104
|
-
event.source !== embedFrameRef.current?.contentWindow
|
|
105
|
-
) {
|
|
107
|
+
if (!isEmbedSessionExpiredMessage(event, embedFrameRef.current, null))
|
|
106
108
|
return;
|
|
107
|
-
}
|
|
108
109
|
setEmbedAttempt((attempt) => attempt + 1);
|
|
109
110
|
};
|
|
110
111
|
|
|
@@ -189,11 +190,6 @@ export default function WorkspaceAppRoute() {
|
|
|
189
190
|
<span className="font-mono text-foreground">{app.path}</span>{" "}
|
|
190
191
|
{t("dispatch.pages.appBuildingSuffix")}
|
|
191
192
|
</p>
|
|
192
|
-
{app.branchName ? (
|
|
193
|
-
<p className="text-xs text-muted-foreground">
|
|
194
|
-
{t("dispatch.pages.branch", { branch: app.branchName })}
|
|
195
|
-
</p>
|
|
196
|
-
) : null}
|
|
197
193
|
{app.builderUrl ? (
|
|
198
194
|
<Button asChild>
|
|
199
195
|
<a
|
|
@@ -204,7 +200,9 @@ export default function WorkspaceAppRoute() {
|
|
|
204
200
|
target="_blank"
|
|
205
201
|
rel="noreferrer"
|
|
206
202
|
>
|
|
207
|
-
{t("dispatch.pages.openBuilderBranch"
|
|
203
|
+
{t("dispatch.pages.openBuilderBranch", {
|
|
204
|
+
defaultValue: "Open in Builder",
|
|
205
|
+
})}
|
|
208
206
|
</a>
|
|
209
207
|
</Button>
|
|
210
208
|
) : null}
|
|
@@ -222,6 +220,7 @@ export default function WorkspaceAppRoute() {
|
|
|
222
220
|
<div className="min-h-0 flex-1 bg-muted/20">
|
|
223
221
|
{embedUrl ? (
|
|
224
222
|
<iframe
|
|
223
|
+
key={`${embedUrl}:${embedAttempt}`}
|
|
225
224
|
data-dispatch-workspace-app-frame
|
|
226
225
|
src={embedUrl}
|
|
227
226
|
title={app.name}
|