|
1 | 1 | import { MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/20/solid"; |
2 | 2 | import { formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations"; |
3 | | -import { useState } from "react"; |
4 | 3 | import { Button } from "~/components/primitives/Buttons"; |
5 | 4 | import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog"; |
6 | 5 | import { FormButtons } from "~/components/primitives/FormButtons"; |
@@ -82,87 +81,95 @@ export function DashboardAgentHistoryMenu({ |
82 | 81 | currentChatId, |
83 | 82 | thinkingChatId, |
84 | 83 | onSelect, |
85 | | - onDelete, |
| 84 | + onRequestDelete, |
86 | 85 | }: { |
87 | 86 | chats: DashboardAgentChat[]; |
88 | 87 | currentChatId: string; |
89 | 88 | thinkingChatId?: string | null; |
90 | 89 | onSelect: (chatId: string) => void; |
91 | | - onDelete: (chatId: string) => void; |
| 90 | + onRequestDelete: (chat: DashboardAgentChat) => void; |
92 | 91 | }) { |
93 | | - const [pendingDelete, setPendingDelete] = useState<DashboardAgentChat | null>(null); |
94 | 92 | const now = Date.now(); |
95 | 93 |
|
96 | 94 | return ( |
97 | | - <> |
98 | | - <div className="max-h-80 overflow-y-auto p-1.5 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> |
99 | | - {chats.length === 0 ? ( |
100 | | - <Paragraph variant="small" className="p-1.5 text-text-dimmed"> |
101 | | - No previous chats yet. |
102 | | - </Paragraph> |
103 | | - ) : ( |
104 | | - <AgentList> |
105 | | - {unreadFirst(chats).map((chat) => { |
106 | | - const process = chatProcess(chat, chat.id === thinkingChatId); |
107 | | - const age = chat.lastMessageAt ? chatAge(chat.lastMessageAt, now) : undefined; |
108 | | - return ( |
109 | | - <AgentListRow |
110 | | - key={chat.id} |
111 | | - label={chat.title} |
112 | | - unread={chatIsUnread(chat)} |
113 | | - status={process ? <ProcessIcon process={process} /> : null} |
114 | | - meta={age} |
115 | | - variant={chat.id === currentChatId ? "selected" : "default"} |
116 | | - onSelect={() => onSelect(chat.id)} |
117 | | - action={ |
118 | | - <AgentListRowAction |
119 | | - icon={TrashIcon} |
120 | | - label={`Delete chat: ${chat.title}`} |
121 | | - onClick={() => setPendingDelete(chat)} |
122 | | - danger |
123 | | - /> |
124 | | - } |
125 | | - /> |
126 | | - ); |
127 | | - })} |
128 | | - </AgentList> |
129 | | - )} |
130 | | - </div> |
| 95 | + <div className="max-h-80 overflow-y-auto p-1.5 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"> |
| 96 | + {chats.length === 0 ? ( |
| 97 | + <Paragraph variant="small" className="p-1.5 text-text-dimmed"> |
| 98 | + No previous chats yet. |
| 99 | + </Paragraph> |
| 100 | + ) : ( |
| 101 | + <AgentList> |
| 102 | + {unreadFirst(chats).map((chat) => { |
| 103 | + const process = chatProcess(chat, chat.id === thinkingChatId); |
| 104 | + const age = chat.lastMessageAt ? chatAge(chat.lastMessageAt, now) : undefined; |
| 105 | + return ( |
| 106 | + <AgentListRow |
| 107 | + key={chat.id} |
| 108 | + label={chat.title} |
| 109 | + unread={chatIsUnread(chat)} |
| 110 | + status={process ? <ProcessIcon process={process} /> : null} |
| 111 | + meta={age} |
| 112 | + variant={chat.id === currentChatId ? "selected" : "default"} |
| 113 | + onSelect={() => onSelect(chat.id)} |
| 114 | + action={ |
| 115 | + <AgentListRowAction |
| 116 | + icon={TrashIcon} |
| 117 | + label={`Delete chat: ${chat.title}`} |
| 118 | + onClick={() => onRequestDelete(chat)} |
| 119 | + danger |
| 120 | + /> |
| 121 | + } |
| 122 | + /> |
| 123 | + ); |
| 124 | + })} |
| 125 | + </AgentList> |
| 126 | + )} |
| 127 | + </div> |
| 128 | + ); |
| 129 | +} |
131 | 130 |
|
132 | | - <Dialog |
133 | | - open={pendingDelete !== null} |
134 | | - onOpenChange={(open) => !open && setPendingDelete(null)} |
135 | | - > |
136 | | - <DialogContent> |
137 | | - <DialogHeader>Delete this chat?</DialogHeader> |
138 | | - <div className="flex flex-col gap-3 pt-3"> |
139 | | - <Paragraph> |
140 | | - "{pendingDelete?.title}" and everything in it will be deleted. This can't be undone. |
141 | | - </Paragraph> |
142 | | - <FormButtons |
143 | | - confirmButton={ |
144 | | - <Button |
145 | | - type="button" |
146 | | - variant="danger/medium" |
147 | | - LeadingIcon={TrashIcon} |
148 | | - shortcut={{ modifiers: ["mod"], key: "enter" }} |
149 | | - onClick={() => { |
150 | | - if (pendingDelete) onDelete(pendingDelete.id); |
151 | | - setPendingDelete(null); |
152 | | - }} |
153 | | - > |
154 | | - Delete chat |
155 | | - </Button> |
156 | | - } |
157 | | - cancelButton={ |
158 | | - <Button variant="tertiary/medium" onClick={() => setPendingDelete(null)}> |
159 | | - Cancel |
160 | | - </Button> |
161 | | - } |
162 | | - /> |
163 | | - </div> |
164 | | - </DialogContent> |
165 | | - </Dialog> |
166 | | - </> |
| 131 | +// Rendered outside the history popover: inside it, focus moving to the dialog dismisses the |
| 132 | +// popover, which unmounts the dialog before it can be answered. |
| 133 | +export function DashboardAgentDeleteChatDialog({ |
| 134 | + chat, |
| 135 | + onOpenChange, |
| 136 | + onConfirm, |
| 137 | +}: { |
| 138 | + chat: DashboardAgentChat | null; |
| 139 | + onOpenChange: (open: boolean) => void; |
| 140 | + onConfirm: (chatId: string) => void; |
| 141 | +}) { |
| 142 | + return ( |
| 143 | + <Dialog open={chat !== null} onOpenChange={onOpenChange}> |
| 144 | + <DialogContent> |
| 145 | + <DialogHeader>Delete this chat?</DialogHeader> |
| 146 | + <div className="flex flex-col gap-3 pt-3"> |
| 147 | + <Paragraph> |
| 148 | + "{chat?.title}" and everything in it will be deleted. This can't be undone. |
| 149 | + </Paragraph> |
| 150 | + <FormButtons |
| 151 | + confirmButton={ |
| 152 | + <Button |
| 153 | + type="button" |
| 154 | + variant="danger/medium" |
| 155 | + LeadingIcon={TrashIcon} |
| 156 | + shortcut={{ modifiers: ["mod"], key: "enter" }} |
| 157 | + onClick={() => { |
| 158 | + if (chat) onConfirm(chat.id); |
| 159 | + onOpenChange(false); |
| 160 | + }} |
| 161 | + > |
| 162 | + Delete chat |
| 163 | + </Button> |
| 164 | + } |
| 165 | + cancelButton={ |
| 166 | + <Button variant="tertiary/medium" onClick={() => onOpenChange(false)}> |
| 167 | + Cancel |
| 168 | + </Button> |
| 169 | + } |
| 170 | + /> |
| 171 | + </div> |
| 172 | + </DialogContent> |
| 173 | + </Dialog> |
167 | 174 | ); |
168 | 175 | } |
0 commit comments