* fix(app): open notifications on the right host Agent notifications previously omitted workspace ownership, so a cold target host was treated as missing and fell back to its empty home route. Carry the authoritative workspace and keep older notifications on a target-host resolver until lookup is conclusive. * fix(app): separate notification and agent URL routing Notifications use their authoritative workspace target directly. Stable agent URLs remain server-and-agent targets whose workspace is resolved by the agent route. * fix(protocol): preserve notification workspace targets Both attention message variants retain workspaceId through outbound validation so notification clicks receive the authoritative route target. * test(app): use workspace-scoped notification target * test(server): give notification agents workspace targets * test(app): target notification workspace in navigation e2e
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
export interface AgentDeepLinkTarget {
|
|
serverId: string;
|
|
agentId: string;
|
|
}
|
|
|
|
function normalizeSegment(value: string): string {
|
|
return value.trim();
|
|
}
|
|
|
|
function normalizeAgentDeepLinkTarget(target: AgentDeepLinkTarget): AgentDeepLinkTarget {
|
|
const serverId = normalizeSegment(target.serverId);
|
|
const agentId = normalizeSegment(target.agentId);
|
|
if (!serverId || !agentId) {
|
|
throw new Error("Agent deep links require a server ID and agent ID.");
|
|
}
|
|
return { serverId, agentId };
|
|
}
|
|
|
|
export function buildAgentDeepLinkRoute(
|
|
target: AgentDeepLinkTarget,
|
|
): `/h/${string}/agent/${string}` {
|
|
const { serverId, agentId } = normalizeAgentDeepLinkTarget(target);
|
|
return `/h/${encodeURIComponent(serverId)}/agent/${encodeURIComponent(agentId)}`;
|
|
}
|
|
|
|
export function buildAgentDeepLink(target: AgentDeepLinkTarget): string {
|
|
return `paseo:/${buildAgentDeepLinkRoute(target)}`;
|
|
}
|
|
|
|
export function parseAgentDeepLink(input: string): AgentDeepLinkTarget | null {
|
|
let url: URL;
|
|
try {
|
|
url = new URL(input);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
if (
|
|
url.protocol !== "paseo:" ||
|
|
url.hostname !== "h" ||
|
|
url.username ||
|
|
url.password ||
|
|
url.port ||
|
|
url.search ||
|
|
url.hash
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
if (segments.length !== 3 || segments[1] !== "agent") {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const serverId = normalizeSegment(decodeURIComponent(segments[0] ?? ""));
|
|
const agentId = normalizeSegment(decodeURIComponent(segments[2] ?? ""));
|
|
return serverId && agentId ? { serverId, agentId } : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|