app: hard-cut workspace routing to workspace-only URLs
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostNewAgentRoute,
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
buildHostWorkspaceFileTabRoute,
|
||||
buildHostWorkspaceAgentRoute,
|
||||
buildHostWorkspaceFileRoute,
|
||||
buildHostWorkspaceRoute,
|
||||
buildHostWorkspaceTerminalTabRoute,
|
||||
buildHostWorkspaceRouteWithOpenIntent,
|
||||
buildHostWorkspaceTerminalRoute,
|
||||
decodeFilePathFromPathSegment,
|
||||
decodeWorkspaceIdFromPathSegment,
|
||||
encodeFilePathForPathSegment,
|
||||
encodeWorkspaceIdForPathSegment,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceTabRouteFromPathname,
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
parseWorkspaceOpenIntent,
|
||||
} from "./host-routes";
|
||||
|
||||
describe("parseHostAgentRouteFromPathname", () => {
|
||||
@@ -49,35 +52,10 @@ describe("workspace route parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace route for /tab targets", () => {
|
||||
it("does not treat /tab routes as valid workspace routes", () => {
|
||||
expect(
|
||||
parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace tab route", () => {
|
||||
expect(
|
||||
parseHostWorkspaceTabRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv/tab/draft_abc123")
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
tabId: "draft_abc123",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers path tab segment and ignores query params for workspace tab routes", () => {
|
||||
expect(
|
||||
parseHostWorkspaceTabRouteFromPathname(
|
||||
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_from_path?tabId=draft_from_query&workspaceId=wrong"
|
||||
)
|
||||
).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
tabId: "draft_from_path",
|
||||
});
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds base64url workspace routes", () => {
|
||||
@@ -92,21 +70,50 @@ describe("workspace route parsing", () => {
|
||||
expect(buildHostNewAgentRoute("local")).toBe("/h/local/new-agent");
|
||||
});
|
||||
|
||||
it("builds workspace agent tab routes", () => {
|
||||
expect(buildHostWorkspaceAgentTabRoute("local", "/tmp/repo", "agent-1")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv/tab/agent_agent-1"
|
||||
it("builds workspace routes with open intent query", () => {
|
||||
expect(buildHostWorkspaceAgentRoute("local", "/tmp/repo", "agent-1")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
|
||||
);
|
||||
expect(buildHostWorkspaceTerminalRoute("local", "/tmp/repo", "term-1")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=terminal%3Aterm-1"
|
||||
);
|
||||
expect(buildHostWorkspaceFileRoute("local", "/tmp/repo", "src/index.ts")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=file%3Ac3JjL2luZGV4LnRz"
|
||||
);
|
||||
expect(
|
||||
buildHostWorkspaceRouteWithOpenIntent("local", "/tmp/repo", {
|
||||
kind: "draft",
|
||||
draftId: "new",
|
||||
})
|
||||
).toBe("/h/local/workspace/L3RtcC9yZXBv?open=draft%3Anew");
|
||||
});
|
||||
|
||||
it("builds workspace terminal tab routes", () => {
|
||||
expect(buildHostWorkspaceTerminalTabRoute("local", "/tmp/repo", "term-1")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv/tab/terminal_term-1"
|
||||
);
|
||||
it("parses workspace open intent from pathname query", () => {
|
||||
expect(
|
||||
parseHostWorkspaceOpenIntentFromPathname(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
|
||||
)
|
||||
).toEqual({
|
||||
kind: "agent",
|
||||
agentId: "agent-1",
|
||||
});
|
||||
expect(parseWorkspaceOpenIntent("terminal:term-1")).toEqual({
|
||||
kind: "terminal",
|
||||
terminalId: "term-1",
|
||||
});
|
||||
expect(parseWorkspaceOpenIntent("draft:new")).toEqual({
|
||||
kind: "draft",
|
||||
draftId: "new",
|
||||
});
|
||||
expect(parseWorkspaceOpenIntent("file:c3JjL2luZGV4LnRz")).toEqual({
|
||||
kind: "file",
|
||||
path: "src/index.ts",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds workspace file tab routes", () => {
|
||||
expect(buildHostWorkspaceFileTabRoute("local", "/tmp/repo", "src/index.ts")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv/tab/file_src%2Findex.ts"
|
||||
it("keeps agent detail workspace routing on workspace path with open intent", () => {
|
||||
expect(buildHostAgentDetailRoute("local", "agent-1", "/tmp/repo")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,17 @@ function stripSearchAndHash(pathname: string): string {
|
||||
return pathname.slice(0, end);
|
||||
}
|
||||
|
||||
function extractSearch(pathname: string): string {
|
||||
const queryIndex = pathname.indexOf("?");
|
||||
if (queryIndex < 0) {
|
||||
return "";
|
||||
}
|
||||
const hashIndex = pathname.indexOf("#", queryIndex);
|
||||
return hashIndex >= 0
|
||||
? pathname.slice(queryIndex + 1, hashIndex)
|
||||
: pathname.slice(queryIndex + 1);
|
||||
}
|
||||
|
||||
function trimNonEmpty(value: NullableString): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -88,19 +99,82 @@ function normalizeWorkspaceId(value: string): string {
|
||||
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function buildWorkspaceAgentTabId(agentId: string): string | null {
|
||||
const normalized = trimNonEmpty(agentId);
|
||||
return normalized ? `agent_${normalized}` : null;
|
||||
export type WorkspaceOpenIntent =
|
||||
| { kind: "agent"; agentId: string }
|
||||
| { kind: "terminal"; terminalId: string }
|
||||
| { kind: "file"; path: string }
|
||||
| { kind: "draft"; draftId: string };
|
||||
|
||||
export function parseWorkspaceOpenIntent(
|
||||
value: string | null | undefined
|
||||
): WorkspaceOpenIntent | null {
|
||||
const normalized = trimNonEmpty(value);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const separator = normalized.indexOf(":");
|
||||
if (separator <= 0 || separator >= normalized.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const kind = normalized.slice(0, separator);
|
||||
const payload = trimNonEmpty(normalized.slice(separator + 1));
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kind === "agent") {
|
||||
return { kind: "agent", agentId: payload };
|
||||
}
|
||||
if (kind === "terminal") {
|
||||
return { kind: "terminal", terminalId: payload };
|
||||
}
|
||||
if (kind === "draft") {
|
||||
return { kind: "draft", draftId: payload };
|
||||
}
|
||||
if (kind === "file") {
|
||||
const decodedPath = decodeFilePathFromPathSegment(payload);
|
||||
if (!decodedPath) {
|
||||
return null;
|
||||
}
|
||||
return { kind: "file", path: decodedPath };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildWorkspaceTerminalTabId(terminalId: string): string | null {
|
||||
const normalized = trimNonEmpty(terminalId);
|
||||
return normalized ? `terminal_${normalized}` : null;
|
||||
export function buildWorkspaceOpenIntentParam(
|
||||
intent: WorkspaceOpenIntent
|
||||
): string | null {
|
||||
if (intent.kind === "agent") {
|
||||
const agentId = trimNonEmpty(intent.agentId);
|
||||
return agentId ? `agent:${agentId}` : null;
|
||||
}
|
||||
if (intent.kind === "terminal") {
|
||||
const terminalId = trimNonEmpty(intent.terminalId);
|
||||
return terminalId ? `terminal:${terminalId}` : null;
|
||||
}
|
||||
if (intent.kind === "draft") {
|
||||
const draftId = trimNonEmpty(intent.draftId);
|
||||
return draftId ? `draft:${draftId}` : null;
|
||||
}
|
||||
const normalizedPath = trimNonEmpty(intent.path);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
const encodedPath = encodeFilePathForPathSegment(normalizedPath);
|
||||
return encodedPath ? `file:${encodedPath}` : null;
|
||||
}
|
||||
|
||||
function buildWorkspaceFileTabId(filePath: string): string | null {
|
||||
const normalized = trimNonEmpty(filePath);
|
||||
return normalized ? `file_${normalized.replace(/\\/g, "/")}` : null;
|
||||
export function parseHostWorkspaceOpenIntentFromPathname(
|
||||
pathname: string
|
||||
): WorkspaceOpenIntent | null {
|
||||
const search = extractSearch(pathname);
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
return parseWorkspaceOpenIntent(new URLSearchParams(search).get("open"));
|
||||
}
|
||||
|
||||
export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
||||
@@ -203,35 +277,17 @@ export function parseHostWorkspaceRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string } | null {
|
||||
const pathOnly = stripSearchAndHash(pathname);
|
||||
const prefix = "/h/";
|
||||
if (!pathOnly.startsWith(prefix)) {
|
||||
const match = pathOnly.match(/^\/h\/([^/]+)\/workspace\/([^/]+)\/?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverIdStart = prefix.length;
|
||||
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
|
||||
if (serverIdEnd < 0) {
|
||||
return null;
|
||||
}
|
||||
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
|
||||
const serverId = trimNonEmpty(decodeSegment(rawServerId));
|
||||
const serverId = trimNonEmpty(decodeSegment(match[1]));
|
||||
if (!serverId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspacePrefix = "/workspace/";
|
||||
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
|
||||
let workspaceIdEnd = pathOnly.length;
|
||||
const tabIdx = pathOnly.lastIndexOf("/tab/");
|
||||
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
|
||||
workspaceIdEnd = tabIdx;
|
||||
}
|
||||
|
||||
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
|
||||
const rawWorkspaceId = match[2];
|
||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
@@ -239,56 +295,6 @@ export function parseHostWorkspaceRouteFromPathname(
|
||||
return { serverId, workspaceId };
|
||||
}
|
||||
|
||||
export function parseHostWorkspaceTabRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; workspaceId: string; tabId: string } | null {
|
||||
const pathOnly = stripSearchAndHash(pathname);
|
||||
const prefix = "/h/";
|
||||
if (!pathOnly.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverIdStart = prefix.length;
|
||||
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
|
||||
if (serverIdEnd < 0) {
|
||||
return null;
|
||||
}
|
||||
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
|
||||
const serverId = trimNonEmpty(decodeSegment(rawServerId));
|
||||
if (!serverId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspacePrefix = "/workspace/";
|
||||
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
|
||||
const tabMarker = "/tab/";
|
||||
const tabIdx = pathOnly.lastIndexOf(tabMarker);
|
||||
if (tabIdx < 0 || tabIdx <= workspaceIdStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, tabIdx).replace(/\/+$/, "");
|
||||
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
|
||||
if (!workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabIdStart = tabIdx + tabMarker.length;
|
||||
const tabIdEnd = pathOnly.indexOf("/", tabIdStart);
|
||||
const rawTabId =
|
||||
tabIdEnd < 0 ? pathOnly.slice(tabIdStart) : pathOnly.slice(tabIdStart, tabIdEnd);
|
||||
const tabId = trimNonEmpty(decodeSegment(rawTabId));
|
||||
if (!tabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { serverId, workspaceId, tabId };
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceRoute(
|
||||
serverId: string,
|
||||
workspaceId: string
|
||||
@@ -305,53 +311,53 @@ export function buildHostWorkspaceRoute(
|
||||
return `/h/${encodeSegment(normalizedServerId)}/workspace/${encodeSegment(encodedWorkspaceId)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceAgentTabRoute(
|
||||
export function buildHostWorkspaceRouteWithOpenIntent(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
intent: WorkspaceOpenIntent
|
||||
): string {
|
||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||
if (base === "/") {
|
||||
return "/";
|
||||
}
|
||||
const open = buildWorkspaceOpenIntentParam(intent);
|
||||
if (!open) {
|
||||
return "/";
|
||||
}
|
||||
return `${base}?open=${encodeURIComponent(open)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceAgentRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
agentId: string
|
||||
): string {
|
||||
const tabId = buildWorkspaceAgentTabId(agentId);
|
||||
if (!tabId) {
|
||||
return "/";
|
||||
}
|
||||
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
|
||||
kind: "agent",
|
||||
agentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceTerminalTabRoute(
|
||||
export function buildHostWorkspaceTerminalRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
terminalId: string
|
||||
): string {
|
||||
const tabId = buildWorkspaceTerminalTabId(terminalId);
|
||||
if (!tabId) {
|
||||
return "/";
|
||||
}
|
||||
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
|
||||
kind: "terminal",
|
||||
terminalId,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceTabRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
tabId: string
|
||||
): string {
|
||||
const base = buildHostWorkspaceRoute(serverId, workspaceId);
|
||||
const normalizedTabId = trimNonEmpty(tabId);
|
||||
if (base === "/" || !normalizedTabId) {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/tab/${encodeSegment(normalizedTabId)}`;
|
||||
}
|
||||
|
||||
export function buildHostWorkspaceFileTabRoute(
|
||||
export function buildHostWorkspaceFileRoute(
|
||||
serverId: string,
|
||||
workspaceId: string,
|
||||
filePath: string
|
||||
): string {
|
||||
const tabId = buildWorkspaceFileTabId(filePath);
|
||||
if (!tabId) {
|
||||
return "/";
|
||||
}
|
||||
return buildHostWorkspaceTabRoute(serverId, workspaceId, tabId);
|
||||
return buildHostWorkspaceRouteWithOpenIntent(serverId, workspaceId, {
|
||||
kind: "file",
|
||||
path: filePath,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildHostAgentDetailRoute(
|
||||
@@ -361,7 +367,7 @@ export function buildHostAgentDetailRoute(
|
||||
): string {
|
||||
const normalizedWorkspaceId = trimNonEmpty(workspaceId);
|
||||
if (normalizedWorkspaceId) {
|
||||
return buildHostWorkspaceAgentTabRoute(
|
||||
return buildHostWorkspaceAgentRoute(
|
||||
serverId,
|
||||
normalizedWorkspaceId,
|
||||
agentId
|
||||
@@ -429,8 +435,21 @@ export function mapPathnameToServer(
|
||||
if (suffix.startsWith("new-agent")) {
|
||||
return `${base}/new-agent`;
|
||||
}
|
||||
if (suffix.startsWith("workspace/")) {
|
||||
return `${base}/${suffix}`;
|
||||
const workspaceRoute = parseHostWorkspaceRouteFromPathname(pathname);
|
||||
if (workspaceRoute) {
|
||||
const workspacePath = buildHostWorkspaceRoute(
|
||||
normalized,
|
||||
workspaceRoute.workspaceId
|
||||
);
|
||||
const openIntent = parseHostWorkspaceOpenIntentFromPathname(pathname);
|
||||
if (!openIntent) {
|
||||
return workspacePath;
|
||||
}
|
||||
return buildHostWorkspaceRouteWithOpenIntent(
|
||||
normalized,
|
||||
workspaceRoute.workspaceId,
|
||||
openIntent
|
||||
);
|
||||
}
|
||||
if (suffix.startsWith("agent/")) {
|
||||
return `${base}/${suffix}`;
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("resolveSelectedAgentForNewAgent", () => {
|
||||
it("prefers the agent in the current route", () => {
|
||||
expect(
|
||||
resolveSelectedAgentForNewAgent({
|
||||
pathname: "/h/srv-1/workspace/%2Frepo/tab/agent_agent-2",
|
||||
pathname: "/h/srv-1/workspace/L3JlcG8?open=agent%3Aagent-2",
|
||||
selectedAgentId: "srv-9:agent-9",
|
||||
})
|
||||
).toEqual({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
|
||||
import {
|
||||
parseHostWorkspaceOpenIntentFromPathname,
|
||||
buildHostWorkspaceRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceTabRouteFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
export function parseAgentKey(
|
||||
@@ -27,11 +28,12 @@ export function resolveSelectedAgentForNewAgent(input: {
|
||||
pathname: string;
|
||||
selectedAgentId?: string;
|
||||
}): { serverId: string; agentId: string } | null {
|
||||
const workspaceTabRoute = parseHostWorkspaceTabRouteFromPathname(input.pathname);
|
||||
if (workspaceTabRoute?.tabId?.startsWith("agent_")) {
|
||||
const agentId = workspaceTabRoute.tabId.slice("agent_".length).trim();
|
||||
const workspaceRoute = parseHostWorkspaceRouteFromPathname(input.pathname);
|
||||
const openIntent = parseHostWorkspaceOpenIntentFromPathname(input.pathname);
|
||||
if (workspaceRoute && openIntent?.kind === "agent") {
|
||||
const agentId = openIntent.agentId.trim();
|
||||
if (agentId) {
|
||||
return { serverId: workspaceTabRoute.serverId, agentId };
|
||||
return { serverId: workspaceRoute.serverId, agentId };
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -34,14 +34,14 @@ describe("resolveNotificationTarget", () => {
|
||||
});
|
||||
|
||||
describe("buildNotificationRoute", () => {
|
||||
it("routes directly to workspace tab when workspace id is present", () => {
|
||||
it("routes to workspace with one-shot open intent when workspace id is present", () => {
|
||||
expect(
|
||||
buildNotificationRoute({
|
||||
serverId: "srv-1",
|
||||
agentId: "agent-1",
|
||||
workspaceId: "/tmp/repo",
|
||||
})
|
||||
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv/tab/agent_agent-1");
|
||||
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1");
|
||||
});
|
||||
|
||||
it("routes directly to server-scoped agent path when both ids are present", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
buildHostWorkspaceAgentRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
type NotificationData = Record<string, unknown> | null | undefined;
|
||||
@@ -36,7 +36,7 @@ export function buildNotificationRoute(data: NotificationData): string {
|
||||
const { serverId, agentId, workspaceId } = resolveNotificationTarget(data);
|
||||
if (serverId && agentId) {
|
||||
if (workspaceId) {
|
||||
return buildHostWorkspaceAgentTabRoute(serverId, workspaceId, agentId);
|
||||
return buildHostWorkspaceAgentRoute(serverId, workspaceId, agentId);
|
||||
}
|
||||
return buildHostAgentDetailRoute(serverId, agentId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user