mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add confirmation dialog when archiving chat with workspace (#22524)
When archiving a chat that has an attached workspace, a dialog now pops up asking whether to also delete the associated workspace. ## Changes ### New file: `ArchiveAgentDialog.tsx` A Radix-based dialog component that appears when archiving a chat that has a `workspace_id`. It provides: - A checkbox to opt into deleting the associated workspace - **Cancel** — closes without archiving - **Archive only** — archives the chat, leaves the workspace intact - **Archive & Delete Workspace** — archives the chat and triggers workspace deletion (enabled only when checkbox is checked) ### Modified: `AgentsPage.tsx` - Extracted archive logic into a `performArchive` helper - `requestArchiveAgent` now checks if the chat has a `workspace_id`: - If yes, opens the `ArchiveAgentDialog` - If no, proceeds with archiving directly (existing behavior) - Added `handleArchiveOnly`, `handleArchiveAndDeleteWorkspace`, and `handleCloseArchiveDialog` handlers - Renders the `<ArchiveAgentDialog>` at the page level Chats without a workspace are archived immediately as before — no UX change for those.
This commit is contained in:
@@ -43,6 +43,7 @@ const AgentDetailLayout: FC = () => {
|
||||
setChatErrorReason: () => {},
|
||||
clearChatErrorReason: () => {},
|
||||
requestArchiveAgent: () => {},
|
||||
requestArchiveAndDeleteWorkspace: () => {},
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: () => {},
|
||||
} satisfies AgentsOutletContext
|
||||
|
||||
@@ -74,6 +74,8 @@ const noopClearChatErrorReason: AgentsOutletContext["clearChatErrorReason"] =
|
||||
() => {};
|
||||
const noopRequestArchiveAgent: AgentsOutletContext["requestArchiveAgent"] =
|
||||
() => {};
|
||||
const noopRequestArchiveAndDeleteWorkspace: AgentsOutletContext["requestArchiveAndDeleteWorkspace"] =
|
||||
() => {};
|
||||
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
|
||||
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
||||
|
||||
@@ -489,6 +491,9 @@ const AgentDetail: FC = () => {
|
||||
outletContext?.clearChatErrorReason ?? noopClearChatErrorReason;
|
||||
const requestArchiveAgent =
|
||||
outletContext?.requestArchiveAgent ?? noopRequestArchiveAgent;
|
||||
const requestArchiveAndDeleteWorkspace =
|
||||
outletContext?.requestArchiveAndDeleteWorkspace ??
|
||||
noopRequestArchiveAndDeleteWorkspace;
|
||||
const isSidebarCollapsed = outletContext?.isSidebarCollapsed ?? false;
|
||||
const onToggleSidebarCollapsed =
|
||||
outletContext?.onToggleSidebarCollapsed ?? (() => {});
|
||||
@@ -870,6 +875,13 @@ const AgentDetail: FC = () => {
|
||||
requestArchiveAgent(agentId);
|
||||
};
|
||||
|
||||
const handleArchiveAndDeleteWorkspaceAction = () => {
|
||||
if (!agentId || isArchived) {
|
||||
return;
|
||||
}
|
||||
requestArchiveAndDeleteWorkspace(agentId);
|
||||
};
|
||||
|
||||
if (chatQuery.isLoading) {
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
||||
@@ -888,6 +900,8 @@ const AgentDetail: FC = () => {
|
||||
}}
|
||||
onOpenParentChat={() => {}}
|
||||
onArchiveAgent={() => {}}
|
||||
onArchiveAndDeleteWorkspace={() => {}}
|
||||
hasWorkspace={false}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
/>
|
||||
@@ -957,6 +971,8 @@ const AgentDetail: FC = () => {
|
||||
}}
|
||||
onOpenParentChat={() => {}}
|
||||
onArchiveAgent={() => {}}
|
||||
onArchiveAndDeleteWorkspace={() => {}}
|
||||
hasWorkspace={false}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
/>
|
||||
@@ -995,6 +1011,8 @@ const AgentDetail: FC = () => {
|
||||
onViewWorkspace: handleViewWorkspace,
|
||||
}}
|
||||
onArchiveAgent={handleArchiveAgentAction}
|
||||
onArchiveAndDeleteWorkspace={handleArchiveAndDeleteWorkspaceAction}
|
||||
hasWorkspace={Boolean(workspaceId)}
|
||||
isArchived={isArchived}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
|
||||
|
||||
@@ -28,6 +28,7 @@ const defaultProps = {
|
||||
onViewWorkspace: () => {},
|
||||
},
|
||||
onArchiveAgent: () => {},
|
||||
onArchiveAndDeleteWorkspace: () => {},
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: () => {},
|
||||
} satisfies React.ComponentProps<typeof AgentDetailTopBar>;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
PanelLeftIcon,
|
||||
PanelRightCloseIcon,
|
||||
PanelRightOpenIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
@@ -81,6 +82,8 @@ type AgentDetailTopBarProps = {
|
||||
diff: DiffPanelState;
|
||||
workspace: WorkspaceActions;
|
||||
onArchiveAgent: () => void;
|
||||
onArchiveAndDeleteWorkspace: () => void;
|
||||
hasWorkspace?: boolean;
|
||||
isArchived?: boolean;
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
@@ -93,6 +96,8 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
|
||||
diff,
|
||||
workspace,
|
||||
onArchiveAgent,
|
||||
onArchiveAndDeleteWorkspace,
|
||||
hasWorkspace,
|
||||
isArchived,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
@@ -208,7 +213,16 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
|
||||
Archive Agent
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
{!isArchived && hasWorkspace && (
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
onSelect={onArchiveAndDeleteWorkspace}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
Archive & Delete Workspace
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>{" "}
|
||||
</DropdownMenu>
|
||||
<WebPushButton />
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { watchChats } from "api/api";
|
||||
import { API, watchChats } from "api/api";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import {
|
||||
archiveChat,
|
||||
@@ -88,6 +88,7 @@ export interface AgentsOutletContext {
|
||||
setChatErrorReason: (chatId: string, reason: string) => void;
|
||||
clearChatErrorReason: (chatId: string) => void;
|
||||
requestArchiveAgent: (chatId: string) => void;
|
||||
requestArchiveAndDeleteWorkspace: (chatId: string) => void;
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
}
|
||||
@@ -151,7 +152,11 @@ const AgentsPage: FC = () => {
|
||||
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||
const createMutation = useMutation(createChat(queryClient));
|
||||
const archiveMutation = useMutation(archiveChat(queryClient));
|
||||
const deleteWorkspaceMutation = useMutation({
|
||||
mutationFn: (workspaceId: string) => API.deleteWorkspace(workspaceId),
|
||||
});
|
||||
const [archivingChatId, setArchivingChatId] = useState<string | null>(null);
|
||||
|
||||
const [isConfigureAgentsDialogOpen, setConfigureAgentsDialogOpen] =
|
||||
useState(false);
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
@@ -225,8 +230,6 @@ const AgentsPage: FC = () => {
|
||||
try {
|
||||
await archiveMutation.mutateAsync(chatId);
|
||||
clearChatErrorReason(chatId);
|
||||
// Invalidate the individual chat query so the detail view
|
||||
// picks up the archived flag without a redirect.
|
||||
await queryClient.invalidateQueries({ queryKey: chatKey(chatId) });
|
||||
toast.success("Agent archived.");
|
||||
} catch (error) {
|
||||
@@ -237,6 +240,44 @@ const AgentsPage: FC = () => {
|
||||
},
|
||||
[archiveMutation, queryClient, clearChatErrorReason],
|
||||
);
|
||||
const requestArchiveAndDeleteWorkspace = useCallback(
|
||||
async (chatId: string) => {
|
||||
if (archiveMutation.isPending || deleteWorkspaceMutation.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
setArchivingChatId(chatId);
|
||||
|
||||
try {
|
||||
await archiveMutation.mutateAsync(chatId);
|
||||
clearChatErrorReason(chatId);
|
||||
await queryClient.invalidateQueries({ queryKey: chatKey(chatId) });
|
||||
toast.success("Agent archived.");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "Failed to archive agent."));
|
||||
return;
|
||||
} finally {
|
||||
setArchivingChatId(null);
|
||||
}
|
||||
|
||||
const chat = chatList.find((c) => c.id === chatId);
|
||||
if (chat?.workspace_id) {
|
||||
try {
|
||||
await deleteWorkspaceMutation.mutateAsync(chat.workspace_id);
|
||||
toast.success("Workspace deletion initiated.");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, "Failed to delete workspace."));
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
archiveMutation,
|
||||
deleteWorkspaceMutation,
|
||||
queryClient,
|
||||
clearChatErrorReason,
|
||||
chatList,
|
||||
],
|
||||
);
|
||||
const handleToggleSidebarCollapsed = useCallback(
|
||||
() => setIsSidebarCollapsed((prev) => !prev),
|
||||
[],
|
||||
@@ -247,6 +288,7 @@ const AgentsPage: FC = () => {
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed: handleToggleSidebarCollapsed,
|
||||
}),
|
||||
@@ -255,6 +297,7 @@ const AgentsPage: FC = () => {
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
handleToggleSidebarCollapsed,
|
||||
],
|
||||
@@ -411,6 +454,7 @@ const AgentsPage: FC = () => {
|
||||
modelConfigs={chatModelConfigsQuery.data ?? []}
|
||||
logoUrl={appearance.logo_url}
|
||||
onArchiveAgent={requestArchiveAgent}
|
||||
onArchiveAndDeleteWorkspace={requestArchiveAndDeleteWorkspace}
|
||||
onNewAgent={handleNewAgent}
|
||||
isCreating={createMutation.isPending}
|
||||
isArchiving={archiveMutation.isPending}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
PanelLeftCloseIcon,
|
||||
PauseIcon,
|
||||
SearchIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
createContext,
|
||||
@@ -51,6 +52,7 @@ interface AgentsSidebarProps {
|
||||
modelConfigs: readonly ChatModelConfig[];
|
||||
logoUrl?: string;
|
||||
onArchiveAgent: (chatId: string) => void;
|
||||
onArchiveAndDeleteWorkspace: (chatId: string) => void;
|
||||
onNewAgent: () => void;
|
||||
isCreating: boolean;
|
||||
isArchiving?: boolean;
|
||||
@@ -264,6 +266,7 @@ interface ChatTreeContextValue {
|
||||
readonly archivingChatId: string | null;
|
||||
readonly toggleExpanded: (chatID: string) => void;
|
||||
readonly onArchiveAgent: (chatId: string) => void;
|
||||
readonly onArchiveAndDeleteWorkspace: (chatId: string) => void;
|
||||
}
|
||||
|
||||
const ChatTreeContext = createContext<ChatTreeContextValue | null>(null);
|
||||
@@ -295,6 +298,7 @@ const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
archivingChatId,
|
||||
toggleExpanded,
|
||||
onArchiveAgent,
|
||||
onArchiveAndDeleteWorkspace,
|
||||
} = useChatTree();
|
||||
const chatID = chat.id;
|
||||
const childIDs = (chatTree.childrenById.get(chatID) ?? []).filter((childID) =>
|
||||
@@ -469,7 +473,17 @@ const ChatTreeNode = memo<ChatTreeNodeProps>(({ chat, isChildNode }) => {
|
||||
<ArchiveIcon className="h-3.5 w-3.5" />
|
||||
Archive agent
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
{chat.workspace_id && (
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
disabled={isArchiving}
|
||||
onSelect={() => onArchiveAndDeleteWorkspace(chat.id)}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
Archive & delete workspace
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>{" "}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
@@ -498,6 +512,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
modelConfigs,
|
||||
logoUrl,
|
||||
onArchiveAgent,
|
||||
onArchiveAndDeleteWorkspace,
|
||||
onNewAgent,
|
||||
isCreating,
|
||||
isArchiving = false,
|
||||
@@ -576,6 +591,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
archivingChatId,
|
||||
toggleExpanded,
|
||||
onArchiveAgent,
|
||||
onArchiveAndDeleteWorkspace,
|
||||
}),
|
||||
[
|
||||
chatTree,
|
||||
@@ -590,6 +606,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
|
||||
archivingChatId,
|
||||
toggleExpanded,
|
||||
onArchiveAgent,
|
||||
onArchiveAndDeleteWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user