fix(site): recover malformed subagent chat links (#25532)

This commit is contained in:
Danielle Maywood
2026-05-20 14:39:49 +01:00
committed by GitHub
parent 1ba54e2ca6
commit e73292fc89
4 changed files with 83 additions and 3 deletions
@@ -12,6 +12,7 @@ import { useState } from "react";
import { Link, useLocation } from "react-router";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { cn } from "#/utils/cn";
import { safeBuildAgentChatPath } from "../../../utils/navigation";
import { Response } from "../Response";
import { Shimmer } from "../Shimmer";
import { useDesktopPanel } from "./DesktopPanelContext";
@@ -187,6 +188,7 @@ export const SubagentTool: React.FC<{
const hasReport = Boolean(report?.trim());
const hasExpandableContent = hasPrompt || hasMessage || hasReport;
const durationLabel = shortDurationMs(durationMs);
const agentChatPath = safeBuildAgentChatPath({ chatId });
return (
<div className="w-full">
@@ -217,9 +219,9 @@ export const SubagentTool: React.FC<{
title,
isTimeout,
)}
{chatId && (
{agentChatPath && (
<Link
to={{ pathname: `/agents/${chatId}`, search: location.search }}
to={{ pathname: agentChatPath, search: location.search }}
onClick={(e) => e.stopPropagation()}
className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100"
aria-label="View agent"
@@ -426,6 +426,32 @@ export const SubagentRunning: Story = {
},
};
export const SubagentMalformedChatIdLinksToRecoverableChatId: Story = {
args: {
name: "spawn_agent",
status: "completed",
args: {
title: "Workspace diagnostics",
prompt: "Collect logs and summarize why startup failed.",
},
result: {
chat_id: ["8f3a6131-1ce8-46f5-9", "b", "a8-4a36-beb2? no"].join(""),
title: "Workspace diagnostics",
status: "completed",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("button", { name: /Spawned Workspace diagnostics/ }),
).toBeInTheDocument();
expect(canvas.getByRole("link", { name: "View agent" })).toHaveAttribute(
"href",
["/agents/8f3a6131-1ce8-46f5-9", "b", "a8-4a36-beb2"].join(""),
);
},
};
export const ExploreSubagentRunning: Story = {
args: {
name: "spawn_explore_agent",
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { buildAgentChatPath, safeBuildAgentChatPath } from "./navigation";
describe("buildAgentChatPath", () => {
it("encodes chat IDs as a path segment", () => {
expect(buildAgentChatPath({ chatId: "chat/id" })).toBe("/agents/chat%2Fid");
});
});
describe("safeBuildAgentChatPath", () => {
it("returns a path for a safe chat ID", () => {
expect(safeBuildAgentChatPath({ chatId: "child-chat-id" })).toBe(
"/agents/child-chat-id",
);
});
it("recovers a leading safe segment from a malformed chat ID", () => {
expect(
safeBuildAgentChatPath({
chatId: ["8f3a6131-1ce8-46f5-9", "b", "a8-4a36-beb2? no"].join(""),
}),
).toBe(["/agents/8f3a6131-1ce8-46f5-9", "b", "a8-4a36-beb2"].join(""));
});
it("returns null when no safe chat ID is recoverable", () => {
expect(safeBuildAgentChatPath({ chatId: "? no" })).toBeNull();
expect(safeBuildAgentChatPath({ chatId: "chat/id" })).toBeNull();
});
});
+24 -1
View File
@@ -1,7 +1,30 @@
const safeChatIdPattern = /^[A-Za-z0-9._~-]+$/;
const recoverableChatIdPrefixPattern = /^([A-Za-z0-9._~-]+)[?\s]/;
export const buildAgentChatPath = ({
chatId,
}: Readonly<{
chatId: string;
}>): string => {
return `/agents/${chatId}`;
return `/agents/${encodeURIComponent(chatId)}`;
};
export const safeBuildAgentChatPath = ({
chatId,
}: Readonly<{
chatId: string;
}>): string | null => {
const trimmedChatId = chatId.trim();
if (safeChatIdPattern.test(trimmedChatId)) {
return buildAgentChatPath({ chatId: trimmedChatId });
}
const recoverableChatIdPrefix = trimmedChatId.match(
recoverableChatIdPrefixPattern,
)?.[1];
if (!recoverableChatIdPrefix) {
return null;
}
return buildAgentChatPath({ chatId: recoverableChatIdPrefix });
};