From e630e78075fc96752be8d45f914ff7b9455eea5a Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:58:35 +0800 Subject: [PATCH] fix(webui): preserve range selection and turn timing --- webui/src/components/ChatList.tsx | 70 ++++++++++++++++--- .../src/components/thread/ThreadMessages.tsx | 24 ++++++- webui/src/components/thread/ThreadShell.tsx | 12 +++- webui/src/lib/activity-timeline.ts | 3 + webui/src/lib/nanobot-client.ts | 5 ++ webui/src/tests/app-layout.test.tsx | 1 + webui/src/tests/chat-list.test.tsx | 36 ++++++++++ webui/src/tests/thread-messages.test.tsx | 60 ++++++++++++++++ webui/src/tests/thread-shell.test.tsx | 67 ++++++++++++++++++ 9 files changed, 262 insertions(+), 16 deletions(-) diff --git a/webui/src/components/ChatList.tsx b/webui/src/components/ChatList.tsx index a91a074e3..e45f53e94 100644 --- a/webui/src/components/ChatList.tsx +++ b/webui/src/components/ChatList.tsx @@ -298,6 +298,7 @@ export const ChatList = memo(function ChatList({ const [selectedDeleteKeys, setSelectedDeleteKeys] = useState>( () => new Set(), ); + const deleteSelectionAnchorRef = useRef(null); useEffect(() => { const clearPaneDropTarget = () => setPaneDropTarget(null); @@ -421,6 +422,7 @@ export const ChatList = memo(function ChatList({ if (event.key !== "Escape") return; setDeleteSelectionMode(false); setSelectedDeleteKeys(new Set()); + deleteSelectionAnchorRef.current = null; }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); @@ -530,15 +532,39 @@ export const ChatList = memo(function ChatList({ const updated = new Set(updatedChatIds); const compact = density === "compact"; const firstProjectGroupIndex = limitedGroups.findIndex((group) => group.kind === "project"); + const selectableDeleteKeys = Array.from(new Set(limitedGroups.flatMap((group) => ( + group.sessions.flatMap((session) => { + const paneGroup = paneGroups[session.key]; + const isWorkbenchTab = paneGroup?.visible + ?? ((paneGroup?.panes.length ?? 0) > 1); + return isWorkbenchTab + ? paneGroup?.panes.map((pane) => pane.key) ?? [session.key] + : [session.key]; + }) + )))); const beginDeleteSelection = (keys: string[]) => { + const validKeys = keys.filter((key) => deleteItemsByKey.has(key)); setDeleteSelectionMode(true); - setSelectedDeleteKeys(new Set(keys.filter((key) => deleteItemsByKey.has(key)))); + setSelectedDeleteKeys(new Set(validKeys)); + deleteSelectionAnchorRef.current = validKeys[0] ?? null; }; - const toggleDeleteSelection = (keys: string[]) => { + const toggleDeleteSelection = ( + keys: string[], + shiftKey = false, + targetKey = keys[0], + ) => { + const validKeys = keys.filter((key) => deleteItemsByKey.has(key)); + const anchorKey = deleteSelectionAnchorRef.current; + const range = shiftKey && anchorKey && targetKey + ? selectionRange(selectableDeleteKeys, anchorKey, targetKey) + : null; setSelectedDeleteKeys((current) => { const next = new Set(current); - const validKeys = keys.filter((key) => deleteItemsByKey.has(key)); + if (range) { + for (const key of range) next.add(key); + return next; + } const remove = validKeys.length > 0 && validKeys.every((key) => next.has(key)); for (const key of validKeys) { if (remove) next.delete(key); @@ -546,10 +572,12 @@ export const ChatList = memo(function ChatList({ } return next; }); + if (!range) deleteSelectionAnchorRef.current = targetKey ?? null; }; const closeDeleteSelection = () => { setDeleteSelectionMode(false); setSelectedDeleteKeys(new Set()); + deleteSelectionAnchorRef.current = null; }; const requestDeleteItems = (items: SidebarDeleteItem[]) => { if (items.length === 0) return; @@ -801,7 +829,11 @@ export const ChatList = memo(function ChatList({ selected={tabSelected} partiallySelected={tabPartiallySelected} onToggle={() => togglePaneGroup(s.key)} - onToggleSelection={() => toggleDeleteSelection(tabDeleteKeys)} + onToggleSelection={(shiftKey) => toggleDeleteSelection( + tabDeleteKeys, + shiftKey, + tabDeleteKeys[0], + )} onRequestRename={onRequestRenameTab ? () => onRequestRenameTab(s.key, title) : undefined} @@ -901,9 +933,9 @@ export const ChatList = memo(function ChatList({