mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site): recover malformed subagent chat links (#25532)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user