Skip to content

Commit be21f99

Browse files
committed
feat: add multi-select functionality for resuming sessions in ChatView
- Introduced a select mode in ChatView that allows users to select multiple previous sessions for bulk actions. - Implemented "Select all" and "Delete" buttons to manage selected sessions, enhancing user experience in session management. - Updated the UI to reflect selected states and improved interaction for resuming sessions, ensuring a more efficient workflow.
1 parent 07532cd commit be21f99

1 file changed

Lines changed: 103 additions & 22 deletions

File tree

src/components/ChatView.tsx

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export const ChatView: React.FC<ChatViewProps> = ({ selectedConversation, showCh
3939
const [resumeCandidates, setResumeCandidates] = useState<Conversation[]>([]);
4040
const [resumeLoading, setResumeLoading] = useState(false);
4141
const [resumeOnlyProject, setResumeOnlyProject] = useState(true);
42+
const [resumeSelectMode, setResumeSelectMode] = useState(false);
43+
const [resumeSelected, setResumeSelected] = useState<Record<string, boolean>>({});
4244
const [categoryOpen, setCategoryOpen] = useState(false);
4345
const [newCategoryName, setNewCategoryName] = useState("");
4446

@@ -251,14 +253,69 @@ export const ChatView: React.FC<ChatViewProps> = ({ selectedConversation, showCh
251253
<DialogHeader>
252254
<div className="flex items-center justify-between gap-2">
253255
<DialogTitle>Resume a previous session</DialogTitle>
254-
<label className="flex items-center gap-2 text-xs text-muted-foreground">
255-
<input
256-
type="checkbox"
257-
checked={resumeOnlyProject}
258-
onChange={(e) => setResumeOnlyProject(e.target.checked)}
259-
/>
260-
Only current project
261-
</label>
256+
<div className="flex items-center gap-2">
257+
<label className="flex items-center gap-2 text-xs text-muted-foreground">
258+
<input
259+
type="checkbox"
260+
checked={resumeSelectMode}
261+
onChange={(e) => {
262+
const enabled = e.target.checked;
263+
setResumeSelectMode(enabled);
264+
if (!enabled) setResumeSelected({});
265+
}}
266+
/>
267+
Select mode
268+
</label>
269+
{resumeSelectMode && (
270+
<>
271+
<Button
272+
size="sm"
273+
variant="outline"
274+
onClick={() => {
275+
if (resumeCandidates.length === 0) return;
276+
const allSelected: Record<string, boolean> = {};
277+
for (const c of resumeCandidates) {
278+
const fp = (c as any).filePath as string | undefined;
279+
if (fp) allSelected[fp] = true;
280+
}
281+
setResumeSelected(allSelected);
282+
}}
283+
>
284+
Select all
285+
</Button>
286+
<Button
287+
size="sm"
288+
variant="destructive"
289+
disabled={Object.values(resumeSelected).filter(Boolean).length === 0}
290+
onClick={async () => {
291+
const selectedPaths = resumeCandidates
292+
.map((c) => (c as any).filePath as string | undefined)
293+
.filter((fp): fp is string => !!fp && resumeSelected[fp]);
294+
if (selectedPaths.length === 0) return;
295+
try {
296+
// Delete files in parallel
297+
await Promise.all(
298+
selectedPaths.map((filePath) =>
299+
invoke("delete_session_file", { filePath })
300+
.catch((err) => {
301+
console.error("Failed to delete session file:", filePath, err);
302+
})
303+
)
304+
);
305+
const selectedSet = new Set(selectedPaths);
306+
setResumeCandidates((prev) => prev.filter((c) => !selectedSet.has((c as any).filePath)));
307+
setResumeSelected({});
308+
setResumeSelectMode(false);
309+
} catch (err) {
310+
console.error("Bulk delete failed:", err);
311+
}
312+
}}
313+
>
314+
Delete ({Object.values(resumeSelected).filter(Boolean).length})
315+
</Button>
316+
</>
317+
)}
318+
</div>
262319
</div>
263320
<div className="text-xs text-muted-foreground">
264321
Current project: {useFolderStore.getState().currentFolder || '(none)'}
@@ -278,20 +335,44 @@ export const ChatView: React.FC<ChatViewProps> = ({ selectedConversation, showCh
278335
)}
279336
</div>
280337
) : (
281-
resumeCandidates.map((c) => (
282-
<button
283-
key={c.id + (c.filePath || "")}
284-
className="w-full text-left p-3 hover:bg-accent"
285-
onClick={() => {
286-
// Route through existing handler which also sets resumePath via store
287-
handleConversationSelect(c);
288-
setResumeOpen(false);
289-
}}
290-
>
291-
<div className="text-sm font-medium truncate">{c.title}</div>
292-
<div className="text-xs text-muted-foreground truncate">{c.projectRealpath || "(unknown project)"}</div>
293-
</button>
294-
))
338+
resumeCandidates.map((c) => {
339+
const fp = (c as any).filePath as string | undefined;
340+
const selected = fp ? !!resumeSelected[fp] : false;
341+
return (
342+
<div
343+
key={c.id + (fp || "")}
344+
className={`w-full text-left p-3 hover:bg-accent flex items-center gap-3 ${selected ? 'bg-accent/50' : ''}`}
345+
onClick={() => {
346+
if (resumeSelectMode) {
347+
if (!fp) return;
348+
setResumeSelected((prev) => ({ ...prev, [fp]: !prev[fp] }));
349+
return;
350+
}
351+
// Route through existing handler which also sets resumePath via store
352+
handleConversationSelect(c);
353+
setResumeOpen(false);
354+
}}
355+
>
356+
{resumeSelectMode && (
357+
<input
358+
type="checkbox"
359+
checked={selected}
360+
onChange={(e) => {
361+
e.stopPropagation();
362+
if (!fp) return;
363+
const checked = e.target.checked;
364+
setResumeSelected((prev) => ({ ...prev, [fp]: checked }));
365+
}}
366+
onClick={(e) => e.stopPropagation()}
367+
/>
368+
)}
369+
<div className="min-w-0 flex-1">
370+
<div className="text-sm font-medium truncate">{c.title}</div>
371+
<div className="text-xs text-muted-foreground truncate">{c.projectRealpath || "(unknown project)"}</div>
372+
</div>
373+
</div>
374+
);
375+
})
295376
)}
296377
</div>
297378
</DialogContent>

0 commit comments

Comments
 (0)