chore: remove frontend related regenerate chat title code (#26867)

Co-authored-by: Tracy Johnson <tracy@coder.com>
This commit is contained in:
Jaayden Halko
2026-07-06 06:24:08 +01:00
committed by GitHub
co-authored by Tracy Johnson
parent 39da38b189
commit 14d17abae6
18 changed files with 4 additions and 294 deletions
-7
View File
@@ -3437,13 +3437,6 @@ class ExperimentalApiMethods {
await this.axios.patch(`/api/experimental/chats/${chatId}`, req);
};
regenerateChatTitle = async (chatId: string): Promise<TypesGen.Chat> => {
const response = await this.axios.post<TypesGen.Chat>(
`/api/experimental/chats/${chatId}/title/regenerate`,
);
return response.data;
};
proposeChatTitle = async (chatId: string): Promise<{ title: string }> => {
const response = await this.axios.post<{ title: string }>(
`/api/experimental/chats/${chatId}/title/propose`,
-69
View File
@@ -38,7 +38,6 @@ import {
prependToInfiniteChatsCache,
promoteChatQueuedMessage,
proposeChatTitle,
regenerateChatTitle,
removeChildFromParentInCache,
reorderPinnedChat,
setChatGroupRole,
@@ -67,7 +66,6 @@ vi.mock("#/api/api", () => ({
interruptChat: vi.fn(),
promoteChatQueuedMessage: vi.fn(),
proposeChatTitle: vi.fn(),
regenerateChatTitle: vi.fn(),
getChatAdvisorConfig: vi.fn(),
updateChatAdvisorConfig: vi.fn(),
getChatACL: vi.fn(),
@@ -844,51 +842,6 @@ describe("reorderPinnedChat", () => {
});
});
describe("regenerateChatTitle cache updates", () => {
it("preserves existing chat detail fields when the response is partial", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
const cachedChat = makeChat(chatId, {
diff_status: {
chat_id: chatId,
url: "https://example.com/pr/1",
pull_request_state: "open",
pull_request_title: "",
pull_request_draft: false,
changes_requested: false,
additions: 1,
deletions: 2,
changed_files: 3,
refreshed_at: "2025-01-01T00:00:00.000Z",
stale_at: "2025-01-01T01:00:00.000Z",
},
});
queryClient.setQueryData(chatKey(chatId), cachedChat);
seedInfiniteChats(queryClient, [cachedChat]);
const mutation = regenerateChatTitle(queryClient);
const updatedChat = {
id: chatId,
title: "New title",
} satisfies Partial<TypesGen.Chat>;
mutation.onSuccess(updatedChat as TypesGen.Chat);
const cachedDetail = queryClient.getQueryData<TypesGen.Chat>(
chatKey(chatId),
);
expect(cachedDetail).toEqual({
...cachedChat,
title: "New title",
});
expect(cachedDetail?.diff_status).toEqual(cachedChat.diff_status);
expect(readInfiniteChats(queryClient)?.[0]).toMatchObject({
id: chatId,
title: "New title",
});
});
});
describe("chat cost query factories", () => {
it("builds the summary query key and forwards snake_case params", async () => {
const user = "user-1";
@@ -1504,28 +1457,6 @@ describe("mutation invalidation scope", () => {
}
});
it("regenerateChatTitle invalidates debug runs so the title_generation run surfaces immediately", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);
const mutation = regenerateChatTitle(queryClient);
await mutation.onSettled(undefined, undefined, chatId);
expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);
for (const { label, key } of unrelatedKeys(chatId)) {
const state = queryClient.getQueryState(key);
expect(
state?.isInvalidated,
`${label} should NOT be invalidated by regenerateChatTitle`,
).not.toBe(true);
}
});
for (const { label, error } of [
{ label: "success", error: undefined },
{ label: "failure", error: new Error("proposal failed") },
-32
View File
@@ -1197,38 +1197,6 @@ export const reorderPinnedChat = (queryClient: QueryClient) => ({
},
});
export const regenerateChatTitle = (queryClient: QueryClient) => ({
mutationFn: (chatId: string) => API.experimental.regenerateChatTitle(chatId),
onSuccess: (updatedChat: TypesGen.Chat) => {
queryClient.setQueryData<TypesGen.Chat>(
chatKey(updatedChat.id),
(previousChat) =>
previousChat ? { ...previousChat, ...updatedChat } : updatedChat,
);
updateInfiniteChatsCache(queryClient, (chats) =>
chats.map((chat) =>
chat.id === updatedChat.id
? { ...chat, title: updatedChat.title }
: chat,
),
);
},
onSettled: async (
_data: TypesGen.Chat | undefined,
_error: unknown,
chatId: string,
) => {
await invalidateChatListQueries(queryClient);
await queryClient.invalidateQueries({
queryKey: chatKey(chatId),
exact: true,
});
void invalidateChatDebugRuns(queryClient, chatId);
},
});
export const proposeChatTitle = (queryClient: QueryClient) => ({
mutationFn: (chatId: string) => API.experimental.proposeChatTitle(chatId),
@@ -62,8 +62,6 @@ const AgentChatPageLayout: FC = () => {
requestUnpinAgent: () => {},
isArchiving: false,
archivingChatId: undefined,
onRegenerateTitle: () => {},
regeneratingTitleChatIds: [],
isSidebarCollapsed: false,
onToggleSidebarCollapsed: () => {},
onExpandSidebar: () => {},
@@ -710,7 +710,6 @@ const AgentChatPage: FC = () => {
isArchiving,
archivingChatId,
onOpenRenameDialog,
regeneratingTitleChatIds,
isSidebarCollapsed,
onToggleSidebarCollapsed,
onChatReady,
@@ -747,10 +746,6 @@ const AgentChatPage: FC = () => {
});
};
const isRegeneratingThisChat = agentId
? regeneratingTitleChatIds.includes(agentId)
: false;
const chatQuery = useQuery({
...chat(agentId ?? ""),
enabled: Boolean(agentId),
@@ -1702,7 +1697,6 @@ const AgentChatPage: FC = () => {
}
isPinned={(chatRecord?.pin_order ?? 0) > 0}
isChildChat={parentChatID !== undefined}
isRegeneratingTitle={isRegeneratingThisChat}
urlTransform={urlTransform}
scrollContainerRef={scrollContainerRef}
scrollToBottomRef={scrollToBottomRef}
@@ -197,7 +197,6 @@ interface AgentChatPageViewProps {
isPinned?: boolean;
isChildChat?: boolean;
isArchivingThisChat?: boolean;
isRegeneratingTitle?: boolean;
// Scroll container ref.
scrollContainerRef: RefObject<HTMLDivElement | null>;
@@ -370,7 +369,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
isPinned,
isChildChat,
isArchivingThisChat,
isRegeneratingTitle,
scrollContainerRef,
scrollToBottomRef,
hasMoreMessages,
@@ -842,7 +840,6 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
isPinned={isPinned}
isChildChat={isChildChat}
isArchiving={isArchivingThisChat}
isRegeneratingTitle={isRegeneratingTitle}
hasWorkspace={Boolean(workspace)}
isArchived={isArchived}
diffStatusData={diffStatusData}
@@ -232,8 +232,6 @@ const AgentEmbedPage: FC = () => {
requestArchiveAndDeleteWorkspace,
isArchiving: false,
archivingChatId: undefined,
// Title regeneration is not supported in embed mode.
regeneratingTitleChatIds: [],
isSidebarCollapsed,
onToggleSidebarCollapsed,
onExpandSidebar: () => {},
-58
View File
@@ -31,7 +31,6 @@ import {
prependToInfiniteChatsCache,
proposeChatTitle,
readInfiniteChatsCache,
regenerateChatTitle,
removeChildFromParentInCache,
reorderPinnedChat,
unarchiveChat,
@@ -321,12 +320,6 @@ const AgentsPage: FC = () => {
toast.error(getErrorMessage(error, "Failed to reorder pinned agents."));
},
});
const regenerateTitleMutation = useMutation({
...regenerateChatTitle(queryClient),
onError: (error: unknown) => {
toast.error(getErrorMessage(error, "Failed to generate new title."));
},
});
const proposeTitleMutation = useMutation(proposeChatTitle(queryClient));
const renameTitleMutation = useMutation({
...updateChatTitle(queryClient),
@@ -334,13 +327,6 @@ const AgentsPage: FC = () => {
toast.error(getErrorMessage(error, "Failed to rename chat."));
},
});
const regeneratingTitleChatIdsRef = useRef<ReadonlySet<string>>(new Set());
const [regeneratingTitleChatIds, setRegeneratingTitleChatIds] = useState<
readonly string[]
>([]);
const regeneratingTitlePromisesRef = useRef(
new Map<string, Promise<string>>(),
);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const catalogModelOptions = getModelOptionsFromConfigs(
chatModelConfigsQuery.data,
@@ -489,48 +475,6 @@ const AgentsPage: FC = () => {
const requestReorderPinnedAgent = (chatId: string, pinOrder: number) => {
reorderPinnedChatMutation.mutate({ chatId, pinOrder });
};
const addRegeneratingTitleChatId = (chatId: string) => {
if (!chatId || regeneratingTitleChatIdsRef.current.has(chatId)) {
return false;
}
const next = new Set(regeneratingTitleChatIdsRef.current);
next.add(chatId);
regeneratingTitleChatIdsRef.current = next;
setRegeneratingTitleChatIds(Array.from(next));
return true;
};
const removeRegeneratingTitleChatId = (chatId: string) => {
if (!regeneratingTitleChatIdsRef.current.has(chatId)) {
return;
}
const next = new Set(regeneratingTitleChatIdsRef.current);
next.delete(chatId);
regeneratingTitleChatIdsRef.current = next;
setRegeneratingTitleChatIds(Array.from(next));
};
const requestRegenerateTitle = (chatId: string): Promise<string> => {
const existing = regeneratingTitlePromisesRef.current.get(chatId);
if (existing) {
return existing;
}
addRegeneratingTitleChatId(chatId);
const clearRegenerateTitleTracking = () => {
regeneratingTitlePromisesRef.current.delete(chatId);
removeRegeneratingTitleChatId(chatId);
};
const promise = regenerateTitleMutation.mutateAsync(chatId).then(
(updated) => {
clearRegenerateTitleTracking();
return updated.title;
},
(error) => {
clearRegenerateTitleTracking();
throw error;
},
);
regeneratingTitlePromisesRef.current.set(chatId, promise);
return promise;
};
const requestProposeTitle = async (chatId: string): Promise<string> => {
const result = await proposeTitleMutation.mutateAsync(chatId);
return result.title;
@@ -743,10 +687,8 @@ const AgentsPage: FC = () => {
requestPinAgent={requestPinAgent}
requestUnpinAgent={requestUnpinAgent}
requestReorderPinnedAgent={requestReorderPinnedAgent}
onRegenerateTitle={requestRegenerateTitle}
onProposeTitle={requestProposeTitle}
onRenameTitle={requestRenameTitle}
regeneratingTitleChatIds={regeneratingTitleChatIds}
onToggleSidebarCollapsed={handleToggleSidebarCollapsed}
isPersonalModelOverridesEnabled={
personalModelOverridesQuery.data?.enabled
@@ -365,10 +365,8 @@ const defaultArgs: ComponentProps<typeof AgentsPageView> = {
requestArchiveAndDeleteWorkspace: fn(),
requestPinAgent: fn(),
requestUnpinAgent: fn(),
onRegenerateTitle: fn(async () => "Generated title"),
onProposeTitle: fn(async () => "Proposed title"),
onRenameTitle: fn(async () => {}),
regeneratingTitleChatIds: [],
onToggleSidebarCollapsed: fn(),
isAgentsAdmin: false,
sidebarFilters: defaultSidebarFilters,
+3 -11
View File
@@ -28,10 +28,9 @@ export interface AgentsOutletContext {
requestReorderPinnedAgent?: (chatId: string, pinOrder: number) => void;
isArchiving: boolean;
archivingChatId: string | undefined;
onRegenerateTitle?: (chatId: string) => void;
onRenameTitle?: (chatId: string, title: string) => Promise<void>;
/** Opens the shared rename dialog so both menus drive the same instance. */
onOpenRenameDialog?: (chat: TypesGen.Chat) => void;
regeneratingTitleChatIds: readonly string[];
isSidebarCollapsed: boolean;
onToggleSidebarCollapsed: () => void;
onExpandSidebar: () => void;
@@ -70,10 +69,8 @@ interface AgentsPageViewProps {
requestPinAgent: (chatId: string) => void;
requestUnpinAgent: (chatId: string) => void;
requestReorderPinnedAgent?: (chatId: string, pinOrder: number) => void;
onRegenerateTitle: (chatId: string) => Promise<string>;
onProposeTitle: (chatId: string) => Promise<string>;
onRenameTitle: (chatId: string, title: string) => Promise<void>;
regeneratingTitleChatIds: readonly string[];
onToggleSidebarCollapsed: () => void;
isPersonalModelOverridesEnabled?: boolean;
isAgentsAdmin: boolean;
@@ -111,10 +108,8 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
requestPinAgent,
requestUnpinAgent,
requestReorderPinnedAgent,
onRegenerateTitle,
onProposeTitle,
onRenameTitle,
regeneratingTitleChatIds,
onToggleSidebarCollapsed,
isPersonalModelOverridesEnabled,
isAgentsAdmin,
@@ -145,6 +140,8 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
// State for the shared rename-chat dialog. Lifted here so both the
// sidebar menu and the chat top bar open the same dialog instance.
const [chatPendingRename, setChatPendingRename] =
useState<TypesGen.Chat | null>(null);
@@ -160,11 +157,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
requestReorderPinnedAgent,
isArchiving,
archivingChatId,
onRegenerateTitle: (chatId: string) => {
onRegenerateTitle(chatId).catch(() => {});
},
onOpenRenameDialog: setChatPendingRename,
regeneratingTitleChatIds,
isSidebarCollapsed,
onToggleSidebarCollapsed,
onExpandSidebar,
@@ -205,7 +198,6 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
onProposeTitle={onProposeTitle}
chatPendingRename={chatPendingRename}
onChatPendingRenameChange={setChatPendingRename}
regeneratingTitleChatIds={regeneratingTitleChatIds}
onBeforeNewAgent={handleNewAgent}
isSearchDialogOpen={isSearchDialogOpen}
onSearchDialogOpenChange={onSearchDialogOpenChange}
@@ -41,13 +41,6 @@ type Story = StoryObj<typeof ChatTopBar>;
export const Default: Story = {};
export const RegeneratingTitle: Story = {
args: {
...Default.args,
isRegeneratingTitle: true,
},
};
export const SharedChat: Story = {
args: {
isSharedChat: true,
@@ -21,7 +21,6 @@ import {
DropdownMenuTrigger,
} from "#/components/DropdownMenu/DropdownMenu";
import { Popover, PopoverTrigger } from "#/components/Popover/Popover";
import { Spinner } from "#/components/Spinner/Spinner";
import { cn } from "#/utils/cn";
import { parsePullRequestUrl } from "../utils/pullRequest";
import { ChatActionsMenuItems } from "./ChatActionsMenuItems";
@@ -47,7 +46,6 @@ type ChatTopBarProps = {
onPinAgent?: () => void;
onUnpinAgent?: () => void;
onOpenRenameDialog?: () => void;
isRegeneratingTitle?: boolean;
hasWorkspace?: boolean;
isArchived?: boolean;
isArchiving?: boolean;
@@ -103,7 +101,6 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
onPinAgent,
onUnpinAgent,
onOpenRenameDialog,
isRegeneratingTitle,
hasWorkspace = false,
isArchived = false,
isArchiving = false,
@@ -163,7 +160,6 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
<div
role="status"
aria-live="polite"
aria-busy={isRegeneratingTitle}
className="flex min-w-0 items-center gap-1.5"
>
{parentChat && (
@@ -186,12 +182,7 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
<ChevronRightIcon className="size-3.5 shrink-0 text-content-secondary/70 -ml-0.5" />
</>
)}
<span
className={cn(
"truncate text-sm text-content-primary",
isRegeneratingTitle && "animate-pulse",
)}
>
<span className="truncate text-sm text-content-primary">
{chatTitle}
</span>
{isSharedChat && (
@@ -200,13 +191,6 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
aria-label="Shared chat"
/>
)}
{isRegeneratingTitle && (
<Spinner
aria-label="Regenerating title"
className="h-3.5 w-3.5 shrink-0 text-content-secondary"
loading
/>
)}
</div>
)}
{/* Actions menu sits inline with the title so it tracks the title's right edge.
@@ -104,7 +104,6 @@ const meta: Meta<typeof ChatsSidebar> = {
isSearchDialogOpen: false,
onSearchDialogOpenChange: fn(),
isCreating: false,
regeneratingTitleChatIds: [],
currentUserId: MockUserOwner.id,
sidebarFilters: defaultSidebarFilters,
isPersonalModelOverridesEnabled: true,
@@ -945,66 +944,6 @@ export const SearchDialogKeyboardShortcutHandlesRenameInput: Story = {
},
};
export const RenameChatAvailableDuringRegeneration: Story = {
args: {
chats: [
buildChat({
id: "regenerating-chat",
title: "Regenerating agent",
updated_at: recentTimestamp,
}),
buildChat({
id: "idle-chat",
title: "Idle agent",
updated_at: recentTimestamp,
}),
],
regeneratingTitleChatIds: ["regenerating-chat"],
onProposeTitle: fn(async () => "Proposed replacement"),
onRenameTitle: fn(async () => {}),
},
parameters: {
reactRouter: reactRouterParameters({
location: { path: "/agents" },
routing: agentsRouting,
}),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
await expect(canvas.getByText("Regenerating agent")).toHaveAttribute(
"aria-busy",
"true",
);
await userEvent.click(
canvas.getByRole("button", {
name: "Open actions for Regenerating agent",
}),
);
await expect(
await body.findByRole("menuitem", { name: "Rename chat" }),
).toBeInTheDocument();
await userEvent.keyboard("{Escape}");
await waitFor(() => {
expect(
body.queryByRole("menuitem", { name: "Rename chat" }),
).not.toBeInTheDocument();
});
await userEvent.click(
canvas.getByRole("button", {
name: "Open actions for Idle agent",
}),
);
await expect(
await body.findByRole("menuitem", { name: "Rename chat" }),
).toBeInTheDocument();
},
};
export const RenameChatSubmitsNewTitle: Story = {
args: {
chats: [
@@ -111,7 +111,6 @@ const defaultProps: React.ComponentProps<typeof ChatsSidebar> = {
onPinAgent: vi.fn(),
onUnpinAgent: vi.fn(),
onRenameTitle: vi.fn(async () => {}),
regeneratingTitleChatIds: [],
onBeforeNewAgent: vi.fn(),
isSearchDialogOpen: false,
onSearchDialogOpenChange: vi.fn(),
@@ -39,7 +39,6 @@ interface ChatsSidebarProps {
isCreating: boolean;
isArchiving?: boolean;
archivingChatId?: string | null;
regeneratingTitleChatIds: readonly string[];
isLoading?: boolean;
loadError?: unknown;
onRetryLoad?: () => void;
@@ -76,7 +75,6 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
isCreating,
isArchiving = false,
archivingChatId = null,
regeneratingTitleChatIds,
isLoading = false,
loadError,
onRetryLoad,
@@ -139,7 +137,6 @@ export const ChatsSidebar: FC<ChatsSidebarProps> = (props) => {
isCreating={isCreating}
isArchiving={isArchiving}
archivingChatId={archivingChatId}
regeneratingTitleChatIds={regeneratingTitleChatIds}
isLoading={isLoading}
loadError={loadError}
onRetryLoad={onRetryLoad}
@@ -85,7 +85,6 @@ interface ChatsPanelProps {
readonly isCreating: boolean;
readonly isArchiving: boolean;
readonly archivingChatId: string | null;
readonly regeneratingTitleChatIds: readonly string[];
readonly isLoading: boolean;
readonly loadError?: unknown;
readonly onRetryLoad?: () => void;
@@ -119,7 +118,6 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
isCreating,
isArchiving,
archivingChatId,
regeneratingTitleChatIds,
isLoading,
loadError,
onRetryLoad,
@@ -305,7 +303,6 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
activeChatId,
isArchiving,
archivingChatId,
regeneratingTitleChatIds,
toggleExpanded,
onArchiveAgent,
onUnarchiveAgent,
@@ -15,7 +15,6 @@ export interface ChatTreeContextValue {
readonly activeChatId: string | undefined;
readonly isArchiving: boolean;
readonly archivingChatId: string | null;
readonly regeneratingTitleChatIds: readonly string[];
readonly toggleExpanded: (chatID: string) => void;
readonly onArchiveAgent: (chatId: string) => void;
readonly onUnarchiveAgent: (chatId: string) => void;
@@ -53,7 +53,6 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
activeChatId,
isArchiving,
archivingChatId,
regeneratingTitleChatIds,
toggleExpanded,
onArchiveAgent,
onUnarchiveAgent,
@@ -137,7 +136,6 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
}`;
const workspaceId = chat.workspace_id;
const isArchivingThisChat = isArchiving && archivingChatId === chat.id;
const isRegeneratingThisChat = regeneratingTitleChatIds.includes(chat.id);
const isExpanded = normalizedSearch ? true : (expandedById[chatID] ?? false);
const sharedMenuItemProps = {
@@ -224,11 +222,9 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
<div className="min-w-0 flex-1 overflow-hidden text-left">
<div className="flex min-w-0 items-center gap-1.5 overflow-hidden">
<span
aria-busy={isRegeneratingThisChat}
className={cn(
"block flex-1 truncate text-[13px] text-content-primary",
isActive && "font-medium",
isRegeneratingThisChat && "animate-pulse",
)}
>
{chat.title}
@@ -236,11 +232,6 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
{chat.has_unread && !isActiveChat && (
<span className="sr-only">(unread)</span>
)}
{isRegeneratingThisChat && (
<span className="sr-only" role="status">
Regenerating title
</span>
)}
</div>
<div className="flex min-w-0 items-center gap-1.5">
{hasLinkedDiffStatus && hasLineStats && (