authorgravatar for 130890337+eimexdev@users.noreply.github.comeimexdev <130890337+eimexdev@users.noreply.github.com> 2026-10-09 22:38:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 02:38:39-03:00
log4107930420cffe14776ad80783f7060236b5868b
tree29c34fc77120473cb5d1d8eb7a3f89ba37b9d378
parent1f093d119b91263deb83f0d18e1504b791fdbbd4
signature Signed by PGP key B5690EEEBB952194

feat(web): reorder right panel tabs by dragging (#17730)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

5 files changed, 260 insertions(+), 134 deletions(-)

apps/web/src/components/ChatView.tsx+9
...@@ -6233,6 +6233,13 @@ export default function ChatView(props: ChatViewProps) {...@@ -6233,6 +6233,13 @@ export default function ChatView(props: ChatViewProps) {
6233 finishRightPanelSurfaceClose,6233 finishRightPanelSurfaceClose,
6234 rightPanelState.surfaces,6234 rightPanelState.surfaces,
6235 ]);6235 ]);
6236 const moveRightPanelSurface = useCallback(
6237 (surfaceId: string, toIndex: number) => {
6238 if (activeThreadRef)
6239 useRightPanelStore.getState().moveSurface(activeThreadRef, surfaceId, toIndex);
6240 },
6241 [activeThreadRef],
6242 );
6236 const copyRightPanelFilePath = useCallback((relativePath: string) => {6243 const copyRightPanelFilePath = useCallback((relativePath: string) => {
6237 if (typeof window === "undefined" || !navigator.clipboard?.writeText) {6244 if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
6238 toastManager.add(6245 toastManager.add(
...@@ -11991,6 +11998,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -11991,6 +11998,7 @@ export default function ChatView(props: ChatViewProps) {
11991 onCloseOtherSurfaces={closeOtherRightPanelSurfaces}11998 onCloseOtherSurfaces={closeOtherRightPanelSurfaces}
11992 onCloseSurfacesToRight={closeRightPanelSurfacesToRight}11999 onCloseSurfacesToRight={closeRightPanelSurfacesToRight}
11993 onCloseAllSurfaces={closeAllRightPanelSurfaces}12000 onCloseAllSurfaces={closeAllRightPanelSurfaces}
12001 onMoveSurface={moveRightPanelSurface}
11994 onCopyFilePath={copyRightPanelFilePath}12002 onCopyFilePath={copyRightPanelFilePath}
11995 onAddBrowser={() => createBrowserSurface()}12003 onAddBrowser={() => createBrowserSurface()}
11996 onAddBrowserInProfile={createBrowserSurface}12004 onAddBrowserInProfile={createBrowserSurface}
...@@ -12049,6 +12057,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -12049,6 +12057,7 @@ export default function ChatView(props: ChatViewProps) {
12049 onCloseOtherSurfaces={closeOtherRightPanelSurfaces}12057 onCloseOtherSurfaces={closeOtherRightPanelSurfaces}
12050 onCloseSurfacesToRight={closeRightPanelSurfacesToRight}12058 onCloseSurfacesToRight={closeRightPanelSurfacesToRight}
12051 onCloseAllSurfaces={closeAllRightPanelSurfaces}12059 onCloseAllSurfaces={closeAllRightPanelSurfaces}
12060 onMoveSurface={moveRightPanelSurface}
12052 onCopyFilePath={copyRightPanelFilePath}12061 onCopyFilePath={copyRightPanelFilePath}
12053 onAddBrowser={() => createBrowserSurface()}12062 onAddBrowser={() => createBrowserSurface()}
12054 onAddBrowserInProfile={createBrowserSurface}12063 onAddBrowserInProfile={createBrowserSurface}
apps/web/src/components/RightPanelTabs.tsx+204-126
...@@ -14,6 +14,17 @@ import type {...@@ -14,6 +14,17 @@ import type {
14 ResolvedKeybindingsConfig,14 ResolvedKeybindingsConfig,
15} from "@t3tools/contracts";15} from "@t3tools/contracts";
16import { getTerminalLabel } from "@t3tools/shared/terminalLabels";16import { getTerminalLabel } from "@t3tools/shared/terminalLabels";
17import {
18 closestCenter,
19 DndContext,
20 type DragEndEvent,
21 PointerSensor,
22 useSensor,
23 useSensors,
24} from "@dnd-kit/core";
25import { restrictToFirstScrollableAncestor, restrictToHorizontalAxis } from "@dnd-kit/modifiers";
26import { horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable";
27import { CSS } from "@dnd-kit/utilities";
17import {28import {
18 Smartphone,29 Smartphone,
19 ChevronDown,30 ChevronDown,
...@@ -27,6 +38,7 @@ import {...@@ -27,6 +38,7 @@ import {
27} from "lucide-react";38} from "lucide-react";
28import { Volume2, VolumeOff } from "lucide";39import { Volume2, VolumeOff } from "lucide";
29import {40import {
41 type ComponentProps,
30 type KeyboardEvent as ReactKeyboardEvent,42 type KeyboardEvent as ReactKeyboardEvent,
31 type MouseEvent as ReactMouseEvent,43 type MouseEvent as ReactMouseEvent,
32 type ReactElement,44 type ReactElement,
...@@ -115,6 +127,8 @@ interface RightPanelTabsProps {...@@ -115,6 +127,8 @@ interface RightPanelTabsProps {
115 onCloseOtherSurfaces: (surface: RightPanelSurface) => void;127 onCloseOtherSurfaces: (surface: RightPanelSurface) => void;
116 onCloseSurfacesToRight: (surface: RightPanelSurface) => void;128 onCloseSurfacesToRight: (surface: RightPanelSurface) => void;
117 onCloseAllSurfaces: () => void;129 onCloseAllSurfaces: () => void;
130 /** Tabs are draggable only when the owner can persist the new order. */
131 onMoveSurface?: (surfaceId: string, toIndex: number) => void;
118 onCopyFilePath: (relativePath: string) => void;132 onCopyFilePath: (relativePath: string) => void;
119 onAddBrowser: () => void;133 onAddBrowser: () => void;
120 /**134 /**
...@@ -199,6 +213,7 @@ type TabContextMenuAction =...@@ -199,6 +213,7 @@ type TabContextMenuAction =
199 | "close-all";213 | "close-all";
200214
201const TAB_SCROLL_EDGE_TOLERANCE = 1;215const TAB_SCROLL_EDGE_TOLERANCE = 1;
216const TAB_DRAG_MODIFIERS = [restrictToHorizontalAxis, restrictToFirstScrollableAncestor];
202217
203function tabScrollViewport(root: HTMLDivElement | null): HTMLDivElement | null {218function tabScrollViewport(root: HTMLDivElement | null): HTMLDivElement | null {
204 return root?.querySelector<HTMLDivElement>('[data-slot="scroll-area-viewport"]') ?? null;219 return root?.querySelector<HTMLDivElement>('[data-slot="scroll-area-viewport"]') ?? null;
...@@ -795,6 +810,61 @@ function PullRequestSurfaceIcon({...@@ -795,6 +810,61 @@ function PullRequestSurfaceIcon({
795 return <presentation.Icon className={cn("size-3 shrink-0", presentation.toneClassName)} />;810 return <presentation.Icon className={cn("size-3 shrink-0", presentation.toneClassName)} />;
796}811}
797812
813/**
814 * Lets its tabs be dragged into a new order. Neighbours slide aside while dragging,
815 * and the order commits on drop.
816 */
817function SortableTabList(props: {
818 surfaces: readonly RightPanelSurface[];
819 onMove: ((surfaceId: string, toIndex: number) => void) | undefined;
820 children: ReactNode;
821}) {
822 const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
823 const surfaceIds = useMemo(() => props.surfaces.map((surface) => surface.id), [props.surfaces]);
824 const { onMove, surfaces } = props;
825 const handleDragEnd = useCallback(
826 ({ active, over }: DragEndEvent) => {
827 if (!over || active.id === over.id) return;
828 const toIndex = surfaces.findIndex((surface) => surface.id === over.id);
829 if (toIndex >= 0) onMove?.(String(active.id), toIndex);
830 },
831 [onMove, surfaces],
832 );
833 return (
834 <DndContext
835 sensors={sensors}
836 collisionDetection={closestCenter}
837 modifiers={TAB_DRAG_MODIFIERS}
838 onDragEnd={handleDragEnd}
839 >
840 <SortableContext items={surfaceIds} strategy={horizontalListSortingStrategy}>
841 {props.children}
842 </SortableContext>
843 </DndContext>
844 );
845}
846
847function SortableTab({
848 surfaceId,
849 disabled,
850 className,
851 ...props
852}: ComponentProps<"div"> & { surfaceId: string; disabled: boolean }) {
853 const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({
854 id: surfaceId,
855 disabled,
856 });
857 return (
858 <div
859 {...props}
860 {...listeners}
861 ref={setNodeRef}
862 style={{ transform: CSS.Translate.toString(transform), transition }}
863 className={cn(className, isDragging && "relative z-10")}
864 />
865 );
866}
867
798export function RightPanelTabs(props: RightPanelTabsProps) {868export function RightPanelTabs(props: RightPanelTabsProps) {
799 const ownsDesktopTitleBar = isElectron && props.mode === "inline";869 const ownsDesktopTitleBar = isElectron && props.mode === "inline";
800 const browserProfiles = useBrowserDefaults().profiles;870 const browserProfiles = useBrowserDefaults().profiles;
...@@ -1118,135 +1188,143 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -1118,135 +1188,143 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
1118 data-right-panel-tab-list1188 data-right-panel-tab-list
1119 >1189 >
1120 <div className="flex h-full w-max min-w-full items-center gap-1">1190 <div className="flex h-full w-max min-w-full items-center gap-1">
1121 {props.surfaces.map((surface) => {1191 <SortableTabList surfaces={props.surfaces} onMove={props.onMoveSurface}>
1122 const active = surface.id === props.activeSurfaceId;1192 {props.surfaces.map((surface) => {
1123 const pending = props.pendingSurfaceIds.has(surface.id);1193 const active = surface.id === props.activeSurfaceId;
1124 const title = surfaceTitle(surface, props.previewSessions, props.terminalLabelsById);1194 const pending = props.pendingSurfaceIds.has(surface.id);
1125 const previewTabId = previewTabIdOf(surface, props.previewSessions);1195 const title = surfaceTitle(
1126 // Desktop state is keyed by the session id, but desktop actions1196 surface,
1127 // must be addressed with the runtime id.1197 props.previewSessions,
1128 const audio = tabAudioState(1198 props.terminalLabelsById,
1129 previewTabId ? (props.desktopByTabId[previewTabId] ?? null) : null,1199 );
1130 );1200 const previewTabId = previewTabIdOf(surface, props.previewSessions);
1131 const audioRuntimeTabId = previewTabId1201 // Desktop state is keyed by the session id, but desktop actions
1132 ? (props.previewRuntimeTabId?.(previewTabId) ?? null)1202 // must be addressed with the runtime id.
1133 : null;1203 const audio = tabAudioState(
1134 return (1204 previewTabId ? (props.desktopByTabId[previewTabId] ?? null) : null,
1135 <div1205 );
1136 key={surface.id}1206 const audioRuntimeTabId = previewTabId
1137 data-active-tab={active}1207 ? (props.previewRuntimeTabId?.(previewTabId) ?? null)
1138 onMouseDown={handleTabMouseDown}1208 : null;
1139 onAuxClick={(event) => handleTabAuxClick(event, surface)}1209 return (
1140 onContextMenu={(event) => void handleTabContextMenu(event, surface)}1210 <SortableTab
1141 className={cn(1211 key={surface.id}
1142 "cursor-pointer group/tab flex h-6 max-w-36 shrink-0 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",1212 surfaceId={surface.id}
1143 ownsDesktopTitleBar && "[-webkit-app-region:no-drag]",1213 disabled={!props.onMoveSurface || renamingDevice === surface.id}
1144 active1214 data-active-tab={active}
1145 ? "bg-accent text-foreground"1215 onMouseDown={handleTabMouseDown}
1146 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",1216 onAuxClick={(event) => handleTabAuxClick(event, surface)}
1147 )}1217 onContextMenu={(event) => void handleTabContextMenu(event, surface)}
1148 >1218 className={cn(
1149 <PanelTabCloseButton1219 "cursor-pointer group/tab flex h-6 max-w-36 shrink-0 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
1150 label={`Close ${title}`}1220 ownsDesktopTitleBar && "[-webkit-app-region:no-drag]",
1151 onClick={() => props.onCloseSurface(surface)}1221 active
1222 ? "bg-accent text-foreground"
1223 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
1224 )}
1152 >1225 >
1153 <SurfaceIcon1226 <PanelTabCloseButton
1154 surface={surface}1227 label={`Close ${title}`}
1155 sessions={props.previewSessions}1228 onClick={() => props.onCloseSurface(surface)}
1156 desktopByTabId={props.desktopByTabId}1229 >
1157 theme={resolvedTheme}1230 <SurfaceIcon
1158 environmentId={props.environmentId}1231 surface={surface}
1159 pullRequestStatusSeeds={props.pullRequestStatusSeeds}1232 sessions={props.previewSessions}
1160 />1233 desktopByTabId={props.desktopByTabId}
1161 {pending ? (1234 theme={resolvedTheme}
1162 <span1235 environmentId={props.environmentId}
1163 className="absolute -right-0.5 -bottom-0.5 size-1.5 rounded-full bg-current"1236 pullRequestStatusSeeds={props.pullRequestStatusSeeds}
1164 aria-hidden
1165 />
1166 ) : null}
1167 </PanelTabCloseButton>
1168 {audio === "none" || !audioRuntimeTabId ? null : (
1169 <Tooltip>
1170 <TooltipTrigger
1171 render={
1172 <button
1173 type="button"
1174 className="cursor-pointer flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted"
1175 aria-label={audio === "muted" ? `Unmute ${title}` : `Mute ${title}`}
1176 onClick={(event) => {
1177 // Sibling of the close button, inside a tab that
1178 // activates on click: keep this to the toggle.
1179 event.stopPropagation();
1180 void previewBridge
1181 ?.setAudioMuted(audioRuntimeTabId, audio !== "muted")
1182 .catch(() => undefined);
1183 }}
1184 >
1185 <MorphIcon
1186 className="size-3"
1187 icon={audio === "muted" ? VolumeOff : Volume2}
1188 />
1189 </button>
1190 }
1191 />1237 />
1192 <TooltipPopup>{audio === "muted" ? "Unmute tab" : "Mute tab"}</TooltipPopup>1238 {pending ? (
1193 </Tooltip>1239 <span
1194 )}1240 className="absolute -right-0.5 -bottom-0.5 size-1.5 rounded-full bg-current"
1195 {renamingDevice === surface.id ? (1241 aria-hidden
1196 <input1242 />
1197 aria-label="Device tab name"1243 ) : null}
1198 className="w-24 min-w-0 rounded-sm bg-background px-1 outline-none ring-1 ring-inset ring-ring"1244 </PanelTabCloseButton>
1199 defaultValue={title}1245 {audio === "none" || !audioRuntimeTabId ? null : (
1200 ref={(element) => {1246 <Tooltip>
1201 element?.focus();1247 <TooltipTrigger
1202 element?.select();1248 render={
1203 }}1249 <button
1204 onBlur={(event) => {1250 type="button"
1205 props.onRenameDevice?.(surface.id, event.currentTarget.value);1251 className="cursor-pointer flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted"
1206 setRenamingDevice(null);1252 aria-label={audio === "muted" ? `Unmute ${title}` : `Mute ${title}`}
1207 }}1253 onClick={(event) => {
1208 onKeyDown={(event) => {1254 // Sibling of the close button, inside a tab that
1209 event.stopPropagation();1255 // activates on click: keep this to the toggle.
1210 if (event.key === "Enter") event.currentTarget.blur();1256 event.stopPropagation();
1211 if (event.key === "Escape") {1257 void previewBridge
1212 event.currentTarget.value = title;1258 ?.setAudioMuted(audioRuntimeTabId, audio !== "muted")
1213 event.currentTarget.blur();1259 .catch(() => undefined);
1214 }1260 }}
1215 }}1261 >
1216 />1262 <MorphIcon
1217 ) : (1263 className="size-3"
1218 <Tooltip>1264 icon={audio === "muted" ? VolumeOff : Volume2}
1219 <TooltipTrigger1265 />
1220 render={1266 </button>
1221 <button1267 }
1222 type="button"1268 />
1223 onDoubleClick={() => {1269 <TooltipPopup>{audio === "muted" ? "Unmute tab" : "Mute tab"}</TooltipPopup>
1224 if (surface.kind === "device" && props.onRenameDevice)1270 </Tooltip>
1225 setRenamingDevice(surface.id);1271 )}
1226 }}1272 {renamingDevice === surface.id ? (
1227 className="cursor-pointer flex min-w-0 items-center"1273 <input
1228 onClick={() => props.onActivate(surface)}1274 aria-label="Device tab name"
1229 >1275 className="w-24 min-w-0 rounded-sm bg-background px-1 outline-none ring-1 ring-inset ring-ring"
1230 <span className="truncate">{title}</span>1276 defaultValue={title}
1231 </button>1277 ref={(element) => {
1232 }1278 element?.focus();
1279 element?.select();
1280 }}
1281 onBlur={(event) => {
1282 props.onRenameDevice?.(surface.id, event.currentTarget.value);
1283 setRenamingDevice(null);
1284 }}
1285 onKeyDown={(event) => {
1286 event.stopPropagation();
1287 if (event.key === "Enter") event.currentTarget.blur();
1288 if (event.key === "Escape") {
1289 event.currentTarget.value = title;
1290 event.currentTarget.blur();
1291 }
1292 }}
1233 />1293 />
1234 <TooltipPopup>1294 ) : (
1235 {surface.kind === "device" ? (1295 <Tooltip>
1236 <DeviceTabTooltip1296 <TooltipTrigger
1237 surface={surface}1297 render={
1238 environmentId={props.environmentId}1298 <button
1239 title={title}1299 type="button"
1240 />1300 onDoubleClick={() => {
1241 ) : (1301 if (surface.kind === "device" && props.onRenameDevice)
1242 title1302 setRenamingDevice(surface.id);
1243 )}1303 }}
1244 </TooltipPopup>1304 className="cursor-pointer flex min-w-0 items-center"
1245 </Tooltip>1305 onClick={() => props.onActivate(surface)}
1246 )}1306 >
1247 </div>1307 <span className="truncate">{title}</span>
1248 );1308 </button>
1249 })}1309 }
1310 />
1311 <TooltipPopup>
1312 {surface.kind === "device" ? (
1313 <DeviceTabTooltip
1314 surface={surface}
1315 environmentId={props.environmentId}
1316 title={title}
1317 />
1318 ) : (
1319 title
1320 )}
1321 </TooltipPopup>
1322 </Tooltip>
1323 )}
1324 </SortableTab>
1325 );
1326 })}
1327 </SortableTabList>
1250 {props.open !== false ? (1328 {props.open !== false ? (
1251 <Menu open={addSurfaceMenuOpen} onOpenChange={setAddSurfaceMenuOpen}>1329 <Menu open={addSurfaceMenuOpen} onOpenChange={setAddSurfaceMenuOpen}>
1252 <MenuTrigger1330 <MenuTrigger
apps/web/src/rightPanelStore.test.ts+23
...@@ -1187,4 +1187,27 @@ describe("rightPanelStore", () => {...@@ -1187,4 +1187,27 @@ describe("rightPanelStore", () => {
1187 ),1187 ),
1188 ).toEqual(["terminal:term-1", "browser:tab-b", "browser:tab-c"]);1188 ).toEqual(["terminal:term-1", "browser:tab-b", "browser:tab-c"]);
1189 });1189 });
1190
1191 it("moves a surface to a new index and keeps it there through browser reconciliation", () => {
1192 const store = useRightPanelStore.getState();
1193 store.openTerminal(refA, "term-1");
1194 store.openBrowser(refA, "tab-a");
1195 store.open(refA, "diff");
1196 const revision = store.getUserActionRevision(refA);
1197 const surfaceIds = () =>
1198 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.map(
1199 (surface) => surface.id,
1200 );
1201
1202 store.moveSurface(refA, "browser:tab-a", 0);
1203 expect(surfaceIds()).toEqual(["browser:tab-a", "terminal:term-1", "diff"]);
1204 store.moveSurface(refA, "browser:tab-a", 2);
1205 expect(surfaceIds()).toEqual(["terminal:term-1", "diff", "browser:tab-a"]);
1206 store.moveSurface(refA, "browser:tab-a", 0);
1207
1208 store.reconcileBrowserSurfaces(refA, ["tab-a", "tab-b"]);
1209 expect(surfaceIds()).toEqual(["browser:tab-a", "terminal:term-1", "diff", "browser:tab-b"]);
1210 // Reordering is not a choice about what the panel shows.
1211 expect(store.getUserActionRevision(refA)).toBe(revision);
1212 });
1190});1213});
apps/web/src/rightPanelStore.ts+20-8
...@@ -176,6 +176,7 @@ interface RightPanelStoreState {...@@ -176,6 +176,7 @@ interface RightPanelStoreState {
176 closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void;176 closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void;
177 closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void;177 closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void;
178 closeAllSurfaces: (ref: ScopedThreadRef) => void;178 closeAllSurfaces: (ref: ScopedThreadRef) => void;
179 moveSurface: (ref: ScopedThreadRef, surfaceId: string, toIndex: number) => void;
179 reconcileBrowserSurfaces: (180 reconcileBrowserSurfaces: (
180 ref: ScopedThreadRef,181 ref: ScopedThreadRef,
181 tabIds: readonly string[],182 tabIds: readonly string[],
...@@ -893,22 +894,33 @@ export const useRightPanelStore = create<RightPanelStoreState>()(...@@ -893,22 +894,33 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
893 : { ...current, isOpen: false, surfaces: [], activeSurfaceId: null },894 : { ...current, isOpen: false, surfaces: [], activeSurfaceId: null },
894 ),895 ),
895 ),896 ),
897 // Reordering is not a choice about what the panel shows, so it leaves the
898 // user-action revision alone and proactive opens still apply.
899 moveSurface: (ref, surfaceId, toIndex) =>
900 set((state) =>
901 updateThread(state, scopedThreadKey(ref), (current) => {
902 const fromIndex = current.surfaces.findIndex((surface) => surface.id === surfaceId);
903 if (fromIndex < 0 || fromIndex === toIndex) return current;
904 const surfaces = [...current.surfaces];
905 surfaces.splice(toIndex, 0, ...surfaces.splice(fromIndex, 1));
906 return { ...current, surfaces };
907 }),
908 ),
896 reconcileBrowserSurfaces: (ref, tabIds, hiddenTabIds) =>909 reconcileBrowserSurfaces: (ref, tabIds, hiddenTabIds) =>
897 set((state) =>910 set((state) =>
898 automaticUpdate(state, scopedThreadKey(ref), (current) => {911 automaticUpdate(state, scopedThreadKey(ref), (current) => {
899 const validIds = new Set(tabIds.map((tabId) => `browser:${tabId}`));912 const validIds = new Set(tabIds.map((tabId) => `browser:${tabId}`));
900 const nonBrowser = current.surfaces.filter((surface) => surface.kind !== "preview");913 // Filtered in place so browser tabs keep the order the user dragged them into.
901 const existingBrowser = current.surfaces.filter(914 const kept = current.surfaces.filter(
902 (surface): surface is Extract<RightPanelSurface, { kind: "preview" }> =>915 (surface) =>
903 surface.kind === "preview" &&916 surface.kind !== "preview" ||
904 surface.id !== "browser:new" &&917 (surface.id !== "browser:new" && validIds.has(surface.id)),
905 validIds.has(surface.id),
906 );918 );
907 const knownIds = new Set(existingBrowser.map((surface) => surface.id));919 const knownIds = new Set(kept.map((surface) => surface.id));
908 const added = tabIds920 const added = tabIds
909 .filter((tabId) => !knownIds.has(`browser:${tabId}`) && !hiddenTabIds?.has(tabId))921 .filter((tabId) => !knownIds.has(`browser:${tabId}`) && !hiddenTabIds?.has(tabId))
910 .map((tabId) => browserSurface(tabId));922 .map((tabId) => browserSurface(tabId));
911 const surfaces = [...nonBrowser, ...existingBrowser, ...added];923 const surfaces = [...kept, ...added];
912 const activeStillExists = surfaces.some(924 const activeStillExists = surfaces.some(
913 (surface) => surface.id === current.activeSurfaceId,925 (surface) => surface.id === current.activeSurfaceId,
914 );926 );
apps/web/src/routes/_chat.pull-requests.tsx+4
...@@ -2241,6 +2241,10 @@ function PullRequestsRouteView() {...@@ -2241,6 +2241,10 @@ function PullRequestsRouteView() {
2241 if (surface.kind === "pull-request") closeSurfacesToRight(surface);2241 if (surface.kind === "pull-request") closeSurfacesToRight(surface);
2242 }}2242 }}
2243 onCloseAllSurfaces={closeAllSurfaces}2243 onCloseAllSurfaces={closeAllSurfaces}
2244 onMoveSurface={(surfaceId, toIndex) => {
2245 if (rightPanelRef !== null)
2246 useRightPanelStore.getState().moveSurface(rightPanelRef, surfaceId, toIndex);
2247 }}
2244 onCopyFilePath={() => undefined}2248 onCopyFilePath={() => undefined}
2245 onAddBrowser={() => undefined}2249 onAddBrowser={() => undefined}
2246 onAddBrowserInProfile={() => undefined}2250 onAddBrowserInProfile={() => undefined}