Skip to content

Commit fe403d6

Browse files
root-daemonclaude
andcommitted
fix(web): sidebar search results can be dragged onto the composer
Search results sat outside the sidebar's DndContext and had no drag sensor, so a result could not be dropped into the composer as thread context. Wrap them in a DndContext that reuses the sidebar pointer sensor, make each row a draggable, and measure whichever list is mounted when deciding a context drag. Fixes #28015 Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent b823dcf commit fe403d6

2 files changed

Lines changed: 73 additions & 52 deletions

File tree

‎apps/web/src/components/Sidebar.tsx‎

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
10231023
const [renamingTitle, setRenamingTitle] = useState("");
10241024
const settlingThreadKeysRef = useRef(new Set<string>());
10251025
const threadListRef = useRef<HTMLUListElement | null>(null);
1026+
const searchResultsListRef = useRef<HTMLUListElement | null>(null);
10261027
const dragLabelOffsetRef = useRef(0);
10271028
const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null);
10281029
const [dragState, setDragState] = useState<{
@@ -1685,7 +1686,8 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
16851686
});
16861687
};
16871688
const pointerOutsideThreadList = (point: { x: number; y: number }) => {
1688-
const bounds = threadListRef.current?.getBoundingClientRect();
1689+
// Search results replace the thread list, so only one of them is mounted.
1690+
const bounds = (threadListRef.current ?? searchResultsListRef.current)?.getBoundingClientRect();
16891691
return bounds !== undefined && (point.x < bounds.left || point.x > bounds.right);
16901692
};
16911693
const moveThreadContextDrag = (point: { x: number; y: number }) => {
@@ -3101,57 +3103,66 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
31013103
closeDelay={0}
31023104
timeout={400}
31033105
>
3104-
<ul
3105-
id="sidebar-thread-search-results"
3106-
role="listbox"
3107-
aria-label="Thread search results"
3108-
className="flex flex-col gap-px"
3106+
<DndContext
3107+
sensors={dndSensors}
3108+
autoScroll={false}
3109+
onDragStart={(event) => {
3110+
contextDragKeyRef.current = String(event.active.id);
3111+
}}
31093112
>
3110-
{threadSearchResults.map((thread, index) => {
3111-
const threadKey = scopedThreadKey(
3112-
scopeThreadRef(thread.environmentId, thread.id),
3113-
);
3114-
return (
3115-
<SidebarSearchResultRow
3116-
isSettled={settledThreadKeys.has(threadKey)}
3117-
key={threadKey}
3118-
thread={thread}
3119-
project={
3120-
projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null
3121-
}
3122-
projectDisplayName={
3123-
projectDisplayNameByKey.get(
3124-
`${thread.environmentId}:${thread.projectId}`,
3125-
) ?? null
3126-
}
3127-
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
3128-
environmentMachine={
3129-
environmentMachineById.get(thread.environmentId) ?? "server"
3130-
}
3131-
providerEntryByInstanceId={
3132-
providerEntriesByEnvironment.get(thread.environmentId) ??
3133-
EMPTY_PROVIDER_ENTRIES
3134-
}
3135-
isHighlighted={activeSearchResultIndex === index}
3136-
isRouteActive={routeRowThreadKey === threadKey}
3137-
resultId={threadSearchResultId(index)}
3138-
searchMatch={
3139-
threadSearchMatchByKey.get(
3140-
threadSearchMatchKey({
3141-
environmentId: thread.environmentId,
3142-
threadId: thread.id,
3143-
}),
3144-
) ?? null
3145-
}
3146-
searchQuery={threadSearchQuery}
3147-
searchMatchQuery={threadSearch.query}
3148-
onHighlight={() => setActiveSearchResultIndex(index)}
3149-
onSelect={() => selectThreadSearchResult(thread)}
3150-
onFileDropThreads={handleThreadFileDrop}
3151-
/>
3152-
);
3153-
})}
3154-
</ul>
3113+
<ul
3114+
ref={searchResultsListRef}
3115+
id="sidebar-thread-search-results"
3116+
role="listbox"
3117+
aria-label="Thread search results"
3118+
className="flex flex-col gap-px"
3119+
>
3120+
{threadSearchResults.map((thread, index) => {
3121+
const threadKey = scopedThreadKey(
3122+
scopeThreadRef(thread.environmentId, thread.id),
3123+
);
3124+
return (
3125+
<SidebarSearchResultRow
3126+
isSettled={settledThreadKeys.has(threadKey)}
3127+
key={threadKey}
3128+
thread={thread}
3129+
project={
3130+
projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null
3131+
}
3132+
projectDisplayName={
3133+
projectDisplayNameByKey.get(
3134+
`${thread.environmentId}:${thread.projectId}`,
3135+
) ?? null
3136+
}
3137+
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
3138+
environmentMachine={
3139+
environmentMachineById.get(thread.environmentId) ?? "server"
3140+
}
3141+
providerEntryByInstanceId={
3142+
providerEntriesByEnvironment.get(thread.environmentId) ??
3143+
EMPTY_PROVIDER_ENTRIES
3144+
}
3145+
isHighlighted={activeSearchResultIndex === index}
3146+
isRouteActive={routeRowThreadKey === threadKey}
3147+
resultId={threadSearchResultId(index)}
3148+
searchMatch={
3149+
threadSearchMatchByKey.get(
3150+
threadSearchMatchKey({
3151+
environmentId: thread.environmentId,
3152+
threadId: thread.id,
3153+
}),
3154+
) ?? null
3155+
}
3156+
searchQuery={threadSearchQuery}
3157+
searchMatchQuery={threadSearch.query}
3158+
onHighlight={() => setActiveSearchResultIndex(index)}
3159+
onSelect={() => selectThreadSearchResult(thread)}
3160+
onFileDropThreads={handleThreadFileDrop}
3161+
/>
3162+
);
3163+
})}
3164+
</ul>
3165+
</DndContext>
31553166
</TooltipProvider>
31563167
) : (
31573168
<p

‎apps/web/src/components/sidebar/SidebarThreadRow.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { requestCustomSnooze } from "../CustomSnoozeDialog";
55
import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";
66
import { useDeadlinePassed } from "~/hooks/useDeadline";
77
import { resolveThreadCurrentPullRequestLink } from "@t3tools/client-runtime/thread-pull-requests";
8+
import { useDraggable } from "@dnd-kit/core";
89
import { useSortable } from "@dnd-kit/sortable";
910
import { CSS } from "@dnd-kit/utilities";
1011
import {
@@ -1628,9 +1629,17 @@ export const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props
16281629
isActive: props.isRouteActive,
16291630
});
16301631
const threadRef = scopeThreadRef(thread.environmentId, thread.id);
1631-
const { leaseLiveStatus, rowRef } = useSidebarRowSubscriptionLease(
1632+
// Results only drag out as chat context; the row keeps its own listbox attributes.
1633+
const { listeners: dragListeners, setNodeRef: setDragNodeRef } = useDraggable({
1634+
id: scopedThreadKey(threadRef),
1635+
});
1636+
const { leaseLiveStatus, rowRef: leaseRowRef } = useSidebarRowSubscriptionLease(
16321637
props.isHighlighted || props.isRouteActive,
16331638
);
1639+
const rowRef = (row: HTMLElement | null) => {
1640+
setDragNodeRef(row);
1641+
return leaseRowRef(row);
1642+
};
16341643
const gitCwd = thread.worktreePath ?? props.project?.workspaceRoot ?? null;
16351644
const gitStatus = useEnvironmentQuery(
16361645
leaseLiveStatus && (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null
@@ -1695,6 +1704,7 @@ export const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props
16951704
aria-current={accessibility.current}
16961705
aria-describedby={`${props.resultId}-pr`}
16971706
aria-label={accessibility.label}
1707+
{...dragListeners}
16981708
onMouseMove={props.onHighlight}
16991709
onClick={props.onSelect}
17001710
className={cn(

0 commit comments

Comments
 (0)