@agent-native/dispatch 0.24.0 → 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/provider-api-register.d.ts +12 -12
- 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-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 +1 -1
- 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 +37 -28
- 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 +2 -1
- package/dist/components/workspace-template-card.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 +10 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +59 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +18 -1
- package/dist/lib/workspace-apps.test.js.map +1 -1
- 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 +8 -3
- 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/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 +4 -1
- 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 +7 -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/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/components/admin-navigation.tsx +57 -11
- 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 +0 -1
- 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 +36 -2
- 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 +10 -0
- package/src/components/layout/Layout.tsx +50 -23
- 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 +52 -23
- package/src/components/workspace-template-card.tsx +7 -10
- 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 +45 -1
- package/src/lib/workspace-apps.ts +72 -0
- package/src/routes/pages/$appId.tsx +3 -6
- package/src/routes/pages/agents.tsx +6 -3
- package/src/routes/pages/apps.$appId.tsx +6 -6
- package/src/routes/pages/apps.tsx +116 -62
- package/src/routes/pages/audit.tsx +28 -8
- package/src/routes/pages/dreams.tsx +92 -142
- package/src/routes/pages/integrations.tsx +147 -68
- package/src/routes/pages/metrics.tsx +8 -0
- package/src/routes/pages/operations.tsx +2 -2
- package/src/routes/pages/settings.tsx +14 -9
- 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/shared/app-roles.ts +8 -0
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
workspaceAppDirectHref,
|
|
5
|
+
workspaceAppEmbedTarget,
|
|
6
|
+
} from "./workspace-apps";
|
|
4
7
|
|
|
5
8
|
describe("workspaceAppEmbedTarget", () => {
|
|
6
9
|
it("uses the app URL as the embed root when a mount path is also present", () => {
|
|
@@ -18,3 +21,44 @@ describe("workspaceAppEmbedTarget", () => {
|
|
|
18
21
|
});
|
|
19
22
|
});
|
|
20
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
|
}
|
|
@@ -80,6 +85,73 @@ export function workspaceAppEmbedTarget(
|
|
|
80
85
|
return path.startsWith("/") ? { path } : path ? { url: path } : {};
|
|
81
86
|
}
|
|
82
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
|
+
|
|
83
155
|
export function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {
|
|
84
156
|
return app.status === "pending" && !!app.builderUrl;
|
|
85
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
|
|
@@ -15,6 +15,7 @@ import { Skeleton } from "../../components/ui/skeleton";
|
|
|
15
15
|
import { Spinner } from "../../components/ui/spinner";
|
|
16
16
|
import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
|
|
17
17
|
import {
|
|
18
|
+
workspaceAppDirectHref,
|
|
18
19
|
workspaceAppEmbedTarget,
|
|
19
20
|
workspaceAppHref,
|
|
20
21
|
type WorkspaceAppSummary,
|
|
@@ -82,6 +83,8 @@ export default function WorkspaceAppRoute() {
|
|
|
82
83
|
})
|
|
83
84
|
.catch((cause: unknown) => {
|
|
84
85
|
if (!cancelled) {
|
|
86
|
+
const directAppUrl = workspaceAppDirectHref(app, "/");
|
|
87
|
+
if (directAppUrl) setEmbedUrl(directAppUrl);
|
|
85
88
|
setEmbedError(
|
|
86
89
|
cause instanceof Error ? cause : new Error(String(cause)),
|
|
87
90
|
);
|
|
@@ -187,11 +190,6 @@ export default function WorkspaceAppRoute() {
|
|
|
187
190
|
<span className="font-mono text-foreground">{app.path}</span>{" "}
|
|
188
191
|
{t("dispatch.pages.appBuildingSuffix")}
|
|
189
192
|
</p>
|
|
190
|
-
{app.branchName ? (
|
|
191
|
-
<p className="text-xs text-muted-foreground">
|
|
192
|
-
{t("dispatch.pages.branch", { branch: app.branchName })}
|
|
193
|
-
</p>
|
|
194
|
-
) : null}
|
|
195
193
|
{app.builderUrl ? (
|
|
196
194
|
<Button asChild>
|
|
197
195
|
<a
|
|
@@ -202,7 +200,9 @@ export default function WorkspaceAppRoute() {
|
|
|
202
200
|
target="_blank"
|
|
203
201
|
rel="noreferrer"
|
|
204
202
|
>
|
|
205
|
-
{t("dispatch.pages.openBuilderBranch"
|
|
203
|
+
{t("dispatch.pages.openBuilderBranch", {
|
|
204
|
+
defaultValue: "Open in Builder",
|
|
205
|
+
})}
|
|
206
206
|
</a>
|
|
207
207
|
</Button>
|
|
208
208
|
) : null}
|
|
@@ -27,12 +27,21 @@ import {
|
|
|
27
27
|
} from "../../components/ui/collapsible";
|
|
28
28
|
import { Skeleton } from "../../components/ui/skeleton";
|
|
29
29
|
import { WorkspaceAppCard } from "../../components/workspace-app-card";
|
|
30
|
+
import {
|
|
31
|
+
WorkspaceAppSearch,
|
|
32
|
+
WorkspaceAppSearchEmpty,
|
|
33
|
+
} from "../../components/workspace-app-search";
|
|
30
34
|
import type {
|
|
31
35
|
CuratedWorkspaceTemplatesResult,
|
|
32
36
|
WorkspaceTemplateLabels,
|
|
33
37
|
} from "../../components/workspace-template-card";
|
|
34
38
|
import type { ConnectedAppSummary } from "../../lib/other-apps";
|
|
35
39
|
import { cn } from "../../lib/utils";
|
|
40
|
+
import {
|
|
41
|
+
orderWorkspaceApps,
|
|
42
|
+
useWorkspaceAppLayout,
|
|
43
|
+
workspaceAppMatchesQuery,
|
|
44
|
+
} from "../../lib/workspace-app-layout";
|
|
36
45
|
import {
|
|
37
46
|
isWorkspaceAppVisibleInDefaultLaunchers,
|
|
38
47
|
type WorkspaceAppSummary,
|
|
@@ -57,6 +66,8 @@ function AppsRoute() {
|
|
|
57
66
|
const t = useT();
|
|
58
67
|
const [showPending, setShowPending] = useState(false);
|
|
59
68
|
const [showHidden, setShowHidden] = useState(false);
|
|
69
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
70
|
+
const { layout, persistenceError, togglePinned } = useWorkspaceAppLayout();
|
|
60
71
|
const appsQuery = useActionQuery("list-workspace-apps", {
|
|
61
72
|
includeAgentCards: false,
|
|
62
73
|
includeArchived: true,
|
|
@@ -81,6 +92,22 @@ function AppsRoute() {
|
|
|
81
92
|
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
82
93
|
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
83
94
|
const archivedApps = allApps.filter((app) => app.archived);
|
|
95
|
+
const orderedActiveApps = orderWorkspaceApps(activeApps, layout);
|
|
96
|
+
const orderedPendingApps = orderWorkspaceApps(pendingApps, layout);
|
|
97
|
+
const orderedArchivedApps = orderWorkspaceApps(archivedApps, layout);
|
|
98
|
+
const filteredActiveApps = orderedActiveApps.filter((app) =>
|
|
99
|
+
workspaceAppMatchesQuery(app, searchQuery),
|
|
100
|
+
);
|
|
101
|
+
const filteredPendingApps = orderedPendingApps.filter((app) =>
|
|
102
|
+
workspaceAppMatchesQuery(app, searchQuery),
|
|
103
|
+
);
|
|
104
|
+
const filteredArchivedApps = orderedArchivedApps.filter((app) =>
|
|
105
|
+
workspaceAppMatchesQuery(app, searchQuery),
|
|
106
|
+
);
|
|
107
|
+
const hasSearchResults =
|
|
108
|
+
filteredActiveApps.length > 0 ||
|
|
109
|
+
filteredPendingApps.length > 0 ||
|
|
110
|
+
filteredArchivedApps.length > 0;
|
|
84
111
|
const showAppSkeletons = appsLoading && allApps.length === 0;
|
|
85
112
|
const templateLabels: WorkspaceTemplateLabels = {
|
|
86
113
|
appId: t("dispatch.pages.remixAppIdLabel"),
|
|
@@ -111,7 +138,7 @@ function AppsRoute() {
|
|
|
111
138
|
>
|
|
112
139
|
<div className="space-y-8">
|
|
113
140
|
<section className="space-y-3">
|
|
114
|
-
<div className="flex flex-wrap items-
|
|
141
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
115
142
|
<div className="flex min-w-0 items-start gap-2">
|
|
116
143
|
<IconApps
|
|
117
144
|
size={16}
|
|
@@ -121,36 +148,44 @@ function AppsRoute() {
|
|
|
121
148
|
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
122
149
|
{t("dispatch.pages.yourApps", { defaultValue: "Your apps" })}
|
|
123
150
|
</h2>
|
|
124
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
125
|
-
{t("dispatch.pages.activeCount", {
|
|
126
|
-
count: activeApps.length,
|
|
127
|
-
})}
|
|
128
|
-
{pendingApps.length > 0
|
|
129
|
-
? ` · ${t("dispatch.pages.pendingCount", {
|
|
130
|
-
count: pendingApps.length,
|
|
131
|
-
})}`
|
|
132
|
-
: ""}
|
|
133
|
-
{archivedApps.length > 0
|
|
134
|
-
? ` · ${t("dispatch.pages.hiddenCount", {
|
|
135
|
-
count: archivedApps.length,
|
|
136
|
-
})}`
|
|
137
|
-
: ""}
|
|
138
|
-
</p>
|
|
139
151
|
</div>
|
|
140
152
|
</div>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
<div className="flex min-w-0 flex-wrap items-center justify-end gap-2">
|
|
154
|
+
{!showAppSkeletons && allApps.length > 0 ? (
|
|
155
|
+
<WorkspaceAppSearch
|
|
156
|
+
className="w-full sm:w-[250px]"
|
|
157
|
+
query={searchQuery}
|
|
158
|
+
onQueryChange={setSearchQuery}
|
|
159
|
+
/>
|
|
160
|
+
) : null}
|
|
161
|
+
{activeApps.length > 0 || pendingApps.length > 0 ? (
|
|
162
|
+
<CreateAppPopover
|
|
163
|
+
align="end"
|
|
164
|
+
trigger={
|
|
165
|
+
<Button size="sm" variant="outline">
|
|
166
|
+
<IconPlus size={15} className="mr-1.5" />
|
|
167
|
+
{t("dispatch.pages.addApp", {
|
|
168
|
+
defaultValue: "Add app",
|
|
169
|
+
})}
|
|
170
|
+
</Button>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
152
175
|
</div>
|
|
153
176
|
|
|
177
|
+
{!showAppSkeletons && allApps.length > 0 && persistenceError ? (
|
|
178
|
+
<p className="text-xs text-muted-foreground" role="status">
|
|
179
|
+
{persistenceError === "both"
|
|
180
|
+
? t("dispatch.pages.appPinSaveFailed", {
|
|
181
|
+
defaultValue: "App pins could not be saved.",
|
|
182
|
+
})
|
|
183
|
+
: t("dispatch.pages.appPinSavedLocally", {
|
|
184
|
+
defaultValue: "App pins are saved on this device only.",
|
|
185
|
+
})}
|
|
186
|
+
</p>
|
|
187
|
+
) : null}
|
|
188
|
+
|
|
154
189
|
{appsQuery.isError ? (
|
|
155
190
|
<ActionQueryError
|
|
156
191
|
error={appsQuery.error}
|
|
@@ -158,25 +193,36 @@ function AppsRoute() {
|
|
|
158
193
|
/>
|
|
159
194
|
) : showAppSkeletons ? (
|
|
160
195
|
<AppsSkeletonGrid />
|
|
161
|
-
) :
|
|
196
|
+
) : !hasSearchResults && searchQuery.trim() ? (
|
|
197
|
+
<WorkspaceAppSearchEmpty
|
|
198
|
+
query={searchQuery}
|
|
199
|
+
onClear={() => setSearchQuery("")}
|
|
200
|
+
/>
|
|
201
|
+
) : filteredActiveApps.length > 0 ? (
|
|
162
202
|
<AppList className={APP_LIST_GRID_CLASS}>
|
|
163
|
-
{
|
|
203
|
+
{filteredActiveApps.map((app) => (
|
|
164
204
|
<WorkspaceAppCard
|
|
165
205
|
key={app.id}
|
|
166
206
|
app={app}
|
|
167
207
|
className={APP_LIST_GRID_ROW_CLASS}
|
|
208
|
+
isPinned={layout.pinnedIds.includes(app.id)}
|
|
209
|
+
onTogglePinned={() => togglePinned(app.id)}
|
|
168
210
|
/>
|
|
169
211
|
))}
|
|
170
212
|
</AppList>
|
|
171
|
-
) : pendingApps.length > 0 ? (
|
|
213
|
+
) : searchQuery.trim() ? null : pendingApps.length > 0 ? (
|
|
172
214
|
<EmptyActiveAppsState />
|
|
173
215
|
) : (
|
|
174
216
|
<EmptyAppsState />
|
|
175
217
|
)}
|
|
176
218
|
</section>
|
|
177
219
|
|
|
178
|
-
{pendingApps.length > 0
|
|
179
|
-
|
|
220
|
+
{pendingApps.length > 0 &&
|
|
221
|
+
(!searchQuery.trim() || filteredPendingApps.length > 0) ? (
|
|
222
|
+
<Collapsible
|
|
223
|
+
open={showPending || Boolean(searchQuery.trim())}
|
|
224
|
+
onOpenChange={setShowPending}
|
|
225
|
+
>
|
|
180
226
|
<section className="space-y-3 border-t pt-6">
|
|
181
227
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
182
228
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -202,14 +248,15 @@ function AppsRoute() {
|
|
|
202
248
|
size="sm"
|
|
203
249
|
className="gap-1.5"
|
|
204
250
|
>
|
|
205
|
-
{showPending
|
|
251
|
+
{showPending || Boolean(searchQuery.trim())
|
|
206
252
|
? t("dispatch.pages.hidePendingApps")
|
|
207
253
|
: t("dispatch.pages.showPendingApps")}
|
|
208
254
|
<IconChevronDown
|
|
209
255
|
size={14}
|
|
210
256
|
className={cn(
|
|
211
257
|
"transition-transform",
|
|
212
|
-
showPending &&
|
|
258
|
+
(showPending || Boolean(searchQuery.trim())) &&
|
|
259
|
+
"rotate-180",
|
|
213
260
|
)}
|
|
214
261
|
/>
|
|
215
262
|
</Button>
|
|
@@ -217,7 +264,7 @@ function AppsRoute() {
|
|
|
217
264
|
</div>
|
|
218
265
|
<CollapsibleContent>
|
|
219
266
|
<AppList className={APP_LIST_GRID_CLASS}>
|
|
220
|
-
{
|
|
267
|
+
{filteredPendingApps.map((app) => (
|
|
221
268
|
<WorkspaceAppCard
|
|
222
269
|
key={app.id}
|
|
223
270
|
app={app}
|
|
@@ -230,31 +277,37 @@ function AppsRoute() {
|
|
|
230
277
|
</Collapsible>
|
|
231
278
|
) : null}
|
|
232
279
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
280
|
+
{!searchQuery.trim() ? (
|
|
281
|
+
<OtherAppsSection
|
|
282
|
+
templates={
|
|
283
|
+
curatedTemplatesQuery.data as
|
|
284
|
+
| CuratedWorkspaceTemplatesResult
|
|
285
|
+
| undefined
|
|
286
|
+
}
|
|
287
|
+
connectedApps={
|
|
288
|
+
connectedAppsQuery.data as ConnectedAppSummary[] | undefined
|
|
289
|
+
}
|
|
290
|
+
workspaceApps={allApps}
|
|
291
|
+
templatesLoading={curatedTemplatesQuery.isLoading}
|
|
292
|
+
connectedAppsLoading={connectedAppsQuery.isLoading}
|
|
293
|
+
templatesError={curatedTemplatesQuery.error}
|
|
294
|
+
connectedAppsError={connectedAppsQuery.error}
|
|
295
|
+
onRetryTemplates={() => void curatedTemplatesQuery.refetch()}
|
|
296
|
+
onRetryConnectedApps={() => void connectedAppsQuery.refetch()}
|
|
297
|
+
templateLabels={templateLabels}
|
|
298
|
+
onRemixSuccess={() => {
|
|
299
|
+
void appsQuery.refetch();
|
|
300
|
+
void curatedTemplatesQuery.refetch();
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
) : null}
|
|
255
304
|
|
|
256
|
-
{archivedApps.length > 0
|
|
257
|
-
|
|
305
|
+
{archivedApps.length > 0 &&
|
|
306
|
+
(!searchQuery.trim() || filteredArchivedApps.length > 0) ? (
|
|
307
|
+
<Collapsible
|
|
308
|
+
open={showHidden || Boolean(searchQuery.trim())}
|
|
309
|
+
onOpenChange={setShowHidden}
|
|
310
|
+
>
|
|
258
311
|
<section className="space-y-3">
|
|
259
312
|
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
|
|
260
313
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -280,14 +333,15 @@ function AppsRoute() {
|
|
|
280
333
|
size="sm"
|
|
281
334
|
className="gap-1.5"
|
|
282
335
|
>
|
|
283
|
-
{showHidden
|
|
336
|
+
{showHidden || Boolean(searchQuery.trim())
|
|
284
337
|
? t("dispatch.pages.hide")
|
|
285
338
|
: t("dispatch.pages.show")}
|
|
286
339
|
<IconChevronDown
|
|
287
340
|
size={14}
|
|
288
341
|
className={cn(
|
|
289
342
|
"transition-transform",
|
|
290
|
-
showHidden &&
|
|
343
|
+
(showHidden || Boolean(searchQuery.trim())) &&
|
|
344
|
+
"rotate-180",
|
|
291
345
|
)}
|
|
292
346
|
/>
|
|
293
347
|
</Button>
|
|
@@ -295,7 +349,7 @@ function AppsRoute() {
|
|
|
295
349
|
</div>
|
|
296
350
|
<CollapsibleContent>
|
|
297
351
|
<AppList className={APP_LIST_GRID_CLASS}>
|
|
298
|
-
{
|
|
352
|
+
{filteredArchivedApps.map((app) => (
|
|
299
353
|
<WorkspaceAppCard
|
|
300
354
|
key={app.id}
|
|
301
355
|
app={app}
|