| author | |
| committer | |
| log | e56bb200fa977d016c751af1a65946f0273614e8 |
| tree | dd2ea0b49f3d0c2f3bcfce03f83dec3f92f39902 |
| parent | c2ca9de33ab88d3a5f15530d41b9fe866e5c812f |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 266 insertions(+), 22 deletions(-)
apps/web/src/components/ChatView.tsx+109-20| ... | @@ -3100,26 +3100,19 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -3100,26 +3100,19 @@ function ChatViewContent(props: ChatViewProps) { |
| 3100 | threadKey === routeThreadKey ? null : routeThreadKey, | 3100 | threadKey === routeThreadKey ? null : routeThreadKey, |
| 3101 | ); | 3101 | ); |
| 3102 | }; | 3102 | }; |
| 3103 | const closeRightPanelSurface = useCallback( | 3103 | const cleanupRightPanelSurfaces = useCallback( |
| 3104 | (surface: RightPanelSurface) => { | 3104 | (surfaces: readonly RightPanelSurface[]) => { |
| 3105 | if (!activeThreadRef) return; | 3105 | if (!activeThreadRef) return; |
| 3106 | if (surface.kind === "preview" && surface.resourceId) { | 3106 | |
| 3107 | usePreviewStateStore.getState().removeSession(activeThreadRef, surface.resourceId); | 3107 | const api = readEnvironmentApi(activeThreadRef.environmentId); |
| 3108 | const api = readEnvironmentApi(activeThreadRef.environmentId); | 3108 | for (const surface of surfaces) { |
| 3109 | void api?.preview | 3109 | if (surface.kind === "preview" && surface.resourceId) { |
| 3110 | .close({ threadId: activeThreadRef.threadId, tabId: surface.resourceId }) | 3110 | usePreviewStateStore.getState().removeSession(activeThreadRef, surface.resourceId); |
| 3111 | .catch(() => undefined); | 3111 | void api?.preview |
| 3112 | } | 3112 | .close({ threadId: activeThreadRef.threadId, tabId: surface.resourceId }) |
| 3113 | useRightPanelStore.getState().closeSurface(activeThreadRef, surface.id); | 3113 | .catch(() => undefined); |
| 3114 | const nextActiveSurface = selectActiveRightPanelSurface( | 3114 | } |
| 3115 | useRightPanelStore.getState().byThreadKey, | 3115 | if (surface.kind !== "terminal") continue; |
| 3116 | activeThreadRef, | ||
| 3117 | ); | ||
| 3118 | if (nextActiveSurface?.kind === "preview" && nextActiveSurface.resourceId) { | ||
| 3119 | usePreviewStateStore.getState().setActiveTab(activeThreadRef, nextActiveSurface.resourceId); | ||
| 3120 | } | ||
| 3121 | if (surface.kind === "terminal") { | ||
| 3122 | const api = readEnvironmentApi(activeThreadRef.environmentId); | ||
| 3123 | for (const terminalId of surface.terminalIds) { | 3116 | for (const terminalId of surface.terminalIds) { |
| 3124 | void api?.terminal | 3117 | void api?.terminal |
| 3125 | .close({ | 3118 | .close({ |
| ... | @@ -3130,7 +3123,8 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -3130,7 +3123,8 @@ function ChatViewContent(props: ChatViewProps) { |
| 3130 | .catch(() => undefined); | 3123 | .catch(() => undefined); |
| 3131 | } | 3124 | } |
| 3132 | } | 3125 | } |
| 3133 | if (surface.kind === "diff" && diffOpen) { | 3126 | |
| 3127 | if (diffOpen && surfaces.some((surface) => surface.kind === "diff")) { | ||
| 3134 | void navigate({ | 3128 | void navigate({ |
| 3135 | to: "/$environmentId/$threadId", | 3129 | to: "/$environmentId/$threadId", |
| 3136 | params: { environmentId, threadId }, | 3130 | params: { environmentId, threadId }, |
| ... | @@ -3141,6 +3135,93 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -3141,6 +3135,93 @@ function ChatViewContent(props: ChatViewProps) { |
| 3141 | }, | 3135 | }, |
| 3142 | [activeThreadRef, diffOpen, environmentId, navigate, threadId], | 3136 | [activeThreadRef, diffOpen, environmentId, navigate, threadId], |
| 3143 | ); | 3137 | ); |
| 3138 | const syncActivePreviewSurface = useCallback(() => { | ||
| 3139 | if (!activeThreadRef) return; | ||
| 3140 | const nextActiveSurface = selectActiveRightPanelSurface( | ||
| 3141 | useRightPanelStore.getState().byThreadKey, | ||
| 3142 | activeThreadRef, | ||
| 3143 | ); | ||
| 3144 | if (nextActiveSurface?.kind === "preview" && nextActiveSurface.resourceId) { | ||
| 3145 | usePreviewStateStore.getState().setActiveTab(activeThreadRef, nextActiveSurface.resourceId); | ||
| 3146 | } | ||
| 3147 | }, [activeThreadRef]); | ||
| 3148 | const closeRightPanelSurface = useCallback( | ||
| 3149 | (surface: RightPanelSurface) => { | ||
| 3150 | if (!activeThreadRef) return; | ||
| 3151 | cleanupRightPanelSurfaces([surface]); | ||
| 3152 | useRightPanelStore.getState().closeSurface(activeThreadRef, surface.id); | ||
| 3153 | syncActivePreviewSurface(); | ||
| 3154 | }, | ||
| 3155 | [activeThreadRef, cleanupRightPanelSurfaces, syncActivePreviewSurface], | ||
| 3156 | ); | ||
| 3157 | const closeOtherRightPanelSurfaces = useCallback( | ||
| 3158 | (surface: RightPanelSurface) => { | ||
| 3159 | if (!activeThreadRef) return; | ||
| 3160 | const surfaces = rightPanelState.surfaces.filter((entry) => entry.id !== surface.id); | ||
| 3161 | cleanupRightPanelSurfaces(surfaces); | ||
| 3162 | useRightPanelStore.getState().closeOtherSurfaces(activeThreadRef, surface.id); | ||
| 3163 | syncActivePreviewSurface(); | ||
| 3164 | }, | ||
| 3165 | [ | ||
| 3166 | activeThreadRef, | ||
| 3167 | cleanupRightPanelSurfaces, | ||
| 3168 | rightPanelState.surfaces, | ||
| 3169 | syncActivePreviewSurface, | ||
| 3170 | ], | ||
| 3171 | ); | ||
| 3172 | const closeRightPanelSurfacesToRight = useCallback( | ||
| 3173 | (surface: RightPanelSurface) => { | ||
| 3174 | if (!activeThreadRef) return; | ||
| 3175 | const surfaceIndex = rightPanelState.surfaces.findIndex((entry) => entry.id === surface.id); | ||
| 3176 | if (surfaceIndex < 0) return; | ||
| 3177 | const surfaces = rightPanelState.surfaces.slice(surfaceIndex + 1); | ||
| 3178 | cleanupRightPanelSurfaces(surfaces); | ||
| 3179 | useRightPanelStore.getState().closeSurfacesToRight(activeThreadRef, surface.id); | ||
| 3180 | syncActivePreviewSurface(); | ||
| 3181 | }, | ||
| 3182 | [ | ||
| 3183 | activeThreadRef, | ||
| 3184 | cleanupRightPanelSurfaces, | ||
| 3185 | rightPanelState.surfaces, | ||
| 3186 | syncActivePreviewSurface, | ||
| 3187 | ], | ||
| 3188 | ); | ||
| 3189 | const closeAllRightPanelSurfaces = useCallback(() => { | ||
| 3190 | if (!activeThreadRef) return; | ||
| 3191 | cleanupRightPanelSurfaces(rightPanelState.surfaces); | ||
| 3192 | useRightPanelStore.getState().closeAllSurfaces(activeThreadRef); | ||
| 3193 | }, [activeThreadRef, cleanupRightPanelSurfaces, rightPanelState.surfaces]); | ||
| 3194 | const copyRightPanelFilePath = useCallback((relativePath: string) => { | ||
| 3195 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) { | ||
| 3196 | toastManager.add( | ||
| 3197 | stackedThreadToast({ | ||
| 3198 | type: "error", | ||
| 3199 | title: "Failed to copy path", | ||
| 3200 | description: "Clipboard API unavailable.", | ||
| 3201 | }), | ||
| 3202 | ); | ||
| 3203 | return; | ||
| 3204 | } | ||
| 3205 | |||
| 3206 | void navigator.clipboard.writeText(relativePath).then( | ||
| 3207 | () => { | ||
| 3208 | toastManager.add({ | ||
| 3209 | type: "success", | ||
| 3210 | title: "Path copied", | ||
| 3211 | description: relativePath, | ||
| 3212 | }); | ||
| 3213 | }, | ||
| 3214 | (error) => { | ||
| 3215 | toastManager.add( | ||
| 3216 | stackedThreadToast({ | ||
| 3217 | type: "error", | ||
| 3218 | title: "Failed to copy path", | ||
| 3219 | description: error instanceof Error ? error.message : "An error occurred.", | ||
| 3220 | }), | ||
| 3221 | ); | ||
| 3222 | }, | ||
| 3223 | ); | ||
| 3224 | }, []); | ||
| 3144 | const persistThreadSettingsForNextTurn = useCallback( | 3225 | const persistThreadSettingsForNextTurn = useCallback( |
| 3145 | async (input: { | 3226 | async (input: { |
| 3146 | threadId: ThreadId; | 3227 | threadId: ThreadId; |
| ... | @@ -4851,6 +4932,10 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4851,6 +4932,10 @@ function ChatViewContent(props: ChatViewProps) { |
| 4851 | terminalLabelsById={activeTerminalLabelsById} | 4932 | terminalLabelsById={activeTerminalLabelsById} |
| 4852 | onActivate={activateRightPanelSurface} | 4933 | onActivate={activateRightPanelSurface} |
| 4853 | onCloseSurface={closeRightPanelSurface} | 4934 | onCloseSurface={closeRightPanelSurface} |
| 4935 | onCloseOtherSurfaces={closeOtherRightPanelSurfaces} | ||
| 4936 | onCloseSurfacesToRight={closeRightPanelSurfacesToRight} | ||
| 4937 | onCloseAllSurfaces={closeAllRightPanelSurfaces} | ||
| 4938 | onCopyFilePath={copyRightPanelFilePath} | ||
| 4854 | onAddBrowser={createBrowserSurface} | 4939 | onAddBrowser={createBrowserSurface} |
| 4855 | onAddTerminal={addTerminalSurface} | 4940 | onAddTerminal={addTerminalSurface} |
| 4856 | onAddDiff={addDiffSurface} | 4941 | onAddDiff={addDiffSurface} |
| ... | @@ -4929,6 +5014,10 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4929,6 +5014,10 @@ function ChatViewContent(props: ChatViewProps) { |
| 4929 | terminalLabelsById={activeTerminalLabelsById} | 5014 | terminalLabelsById={activeTerminalLabelsById} |
| 4930 | onActivate={activateRightPanelSurface} | 5015 | onActivate={activateRightPanelSurface} |
| 4931 | onCloseSurface={closeRightPanelSurface} | 5016 | onCloseSurface={closeRightPanelSurface} |
| 5017 | onCloseOtherSurfaces={closeOtherRightPanelSurfaces} | ||
| 5018 | onCloseSurfacesToRight={closeRightPanelSurfacesToRight} | ||
| 5019 | onCloseAllSurfaces={closeAllRightPanelSurfaces} | ||
| 5020 | onCopyFilePath={copyRightPanelFilePath} | ||
| 4932 | onAddBrowser={createBrowserSurface} | 5021 | onAddBrowser={createBrowserSurface} |
| 4933 | onAddTerminal={addTerminalSurface} | 5022 | onAddTerminal={addTerminalSurface} |
| 4934 | onAddDiff={addDiffSurface} | 5023 | onAddDiff={addDiffSurface} |
apps/web/src/components/RightPanelTabs.tsx+76-2| ... | @@ -1,11 +1,20 @@ | ... | @@ -1,11 +1,20 @@ |
| 1 | import type { PreviewSessionSnapshot } from "@t3tools/contracts"; | 1 | import type { ContextMenuItem, PreviewSessionSnapshot } from "@t3tools/contracts"; |
| 2 | import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; | 2 | import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; |
| 3 | import { ClipboardList, FileDiff, Files, Globe2, Plus, TerminalSquare, X } from "lucide-react"; | 3 | import { ClipboardList, FileDiff, Files, Globe2, Plus, TerminalSquare, X } from "lucide-react"; |
| 4 | import { type ReactElement, type ReactNode, useEffect, useRef, useState } from "react"; | 4 | import { |
| 5 | type MouseEvent as ReactMouseEvent, | ||
| 6 | type ReactElement, | ||
| 7 | type ReactNode, | ||
| 8 | useCallback, | ||
| 9 | useEffect, | ||
| 10 | useRef, | ||
| 11 | useState, | ||
| 12 | } from "react"; | ||
| 5 | 13 | ||
| 6 | import { isElectron } from "~/env"; | 14 | import { isElectron } from "~/env"; |
| 7 | import type { RightPanelSurface } from "~/rightPanelStore"; | 15 | import type { RightPanelSurface } from "~/rightPanelStore"; |
| 8 | import { cn } from "~/lib/utils"; | 16 | import { cn } from "~/lib/utils"; |
| 17 | import { readLocalApi } from "~/localApi"; | ||
| 9 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; | 18 | import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; |
| 10 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; | 19 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; |
| 11 | import { ScrollArea } from "~/components/ui/scroll-area"; | 20 | import { ScrollArea } from "~/components/ui/scroll-area"; |
| ... | @@ -25,6 +34,10 @@ interface RightPanelTabsProps { | ... | @@ -25,6 +34,10 @@ interface RightPanelTabsProps { |
| 25 | terminalLabelsById: ReadonlyMap<string, string>; | 34 | terminalLabelsById: ReadonlyMap<string, string>; |
| 26 | onActivate: (surface: RightPanelSurface) => void; | 35 | onActivate: (surface: RightPanelSurface) => void; |
| 27 | onCloseSurface: (surface: RightPanelSurface) => void; | 36 | onCloseSurface: (surface: RightPanelSurface) => void; |
| 37 | onCloseOtherSurfaces: (surface: RightPanelSurface) => void; | ||
| 38 | onCloseSurfacesToRight: (surface: RightPanelSurface) => void; | ||
| 39 | onCloseAllSurfaces: () => void; | ||
| 40 | onCopyFilePath: (relativePath: string) => void; | ||
| 28 | onAddBrowser: () => void; | 41 | onAddBrowser: () => void; |
| 29 | onAddTerminal: () => void; | 42 | onAddTerminal: () => void; |
| 30 | onAddDiff: () => void; | 43 | onAddDiff: () => void; |
| ... | @@ -41,6 +54,8 @@ const SURFACE_DISABLED_REASONS = { | ... | @@ -41,6 +54,8 @@ const SURFACE_DISABLED_REASONS = { |
| 41 | diff: "Diff is only available for server threads in Git repositories.", | 54 | diff: "Diff is only available for server threads in Git repositories.", |
| 42 | } as const; | 55 | } as const; |
| 43 | 56 | ||
| 57 | type TabContextMenuAction = "copy-path" | "close" | "close-others" | "close-to-right" | "close-all"; | ||
| 58 | |||
| 44 | function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) { | 59 | function DisabledReasonTooltip(props: { reason: string; trigger: ReactElement }) { |
| 45 | return ( | 60 | return ( |
| 46 | <Tooltip> | 61 | <Tooltip> |
| ... | @@ -257,6 +272,64 @@ export function RightPanelTabs(props: RightPanelTabsProps) { | ... | @@ -257,6 +272,64 @@ export function RightPanelTabs(props: RightPanelTabsProps) { |
| 257 | const { resolvedTheme } = useTheme(); | 272 | const { resolvedTheme } = useTheme(); |
| 258 | const tabListRef = useRef<HTMLDivElement>(null); | 273 | const tabListRef = useRef<HTMLDivElement>(null); |
| 259 | 274 | ||
| 275 | const handleTabContextMenu = useCallback( | ||
| 276 | async (event: ReactMouseEvent, surface: RightPanelSurface) => { | ||
| 277 | event.preventDefault(); | ||
| 278 | event.stopPropagation(); | ||
| 279 | |||
| 280 | const api = readLocalApi(); | ||
| 281 | if (!api) return; | ||
| 282 | |||
| 283 | const surfaceIndex = props.surfaces.findIndex((entry) => entry.id === surface.id); | ||
| 284 | if (surfaceIndex < 0) return; | ||
| 285 | |||
| 286 | const items: ContextMenuItem<TabContextMenuAction>[] = []; | ||
| 287 | if (surface.kind === "file") { | ||
| 288 | items.push({ id: "copy-path", label: "Copy path" }); | ||
| 289 | } | ||
| 290 | items.push( | ||
| 291 | { id: "close", label: "Close" }, | ||
| 292 | { | ||
| 293 | id: "close-others", | ||
| 294 | label: "Close others", | ||
| 295 | disabled: props.surfaces.length <= 1, | ||
| 296 | }, | ||
| 297 | { | ||
| 298 | id: "close-to-right", | ||
| 299 | label: "Close to the right", | ||
| 300 | disabled: surfaceIndex >= props.surfaces.length - 1, | ||
| 301 | }, | ||
| 302 | { | ||
| 303 | id: "close-all", | ||
| 304 | label: "Close all", | ||
| 305 | disabled: props.surfaces.length === 0, | ||
| 306 | }, | ||
| 307 | ); | ||
| 308 | |||
| 309 | const action = await api.contextMenu.show(items, { x: event.clientX, y: event.clientY }); | ||
| 310 | switch (action) { | ||
| 311 | case "copy-path": | ||
| 312 | if (surface.kind === "file") props.onCopyFilePath(surface.relativePath); | ||
| 313 | break; | ||
| 314 | case "close": | ||
| 315 | props.onCloseSurface(surface); | ||
| 316 | break; | ||
| 317 | case "close-others": | ||
| 318 | props.onCloseOtherSurfaces(surface); | ||
| 319 | break; | ||
| 320 | case "close-to-right": | ||
| 321 | props.onCloseSurfacesToRight(surface); | ||
| 322 | break; | ||
| 323 | case "close-all": | ||
| 324 | props.onCloseAllSurfaces(); | ||
| 325 | break; | ||
| 326 | case null: | ||
| 327 | break; | ||
| 328 | } | ||
| 329 | }, | ||
| 330 | [props], | ||
| 331 | ); | ||
| 332 | |||
| 260 | useEffect(() => { | 333 | useEffect(() => { |
| 261 | const activeTab = tabListRef.current?.querySelector<HTMLElement>("[data-active-tab='true']"); | 334 | const activeTab = tabListRef.current?.querySelector<HTMLElement>("[data-active-tab='true']"); |
| 262 | activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" }); | 335 | activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" }); |
| ... | @@ -291,6 +364,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { | ... | @@ -291,6 +364,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { |
| 291 | <div | 364 | <div |
| 292 | key={surface.id} | 365 | key={surface.id} |
| 293 | data-active-tab={active} | 366 | data-active-tab={active} |
| 367 | onContextMenu={(event) => void handleTabContextMenu(event, surface)} | ||
| 294 | className={cn( | 368 | className={cn( |
| 295 | "group flex h-7 min-w-25 max-w-44 shrink-0 items-center gap-1.5 rounded-md px-2 text-sm", | 369 | "group flex h-7 min-w-25 max-w-44 shrink-0 items-center gap-1.5 rounded-md px-2 text-sm", |
| 296 | active | 370 | active |
apps/web/src/rightPanelStore.test.ts+41| ... | @@ -304,6 +304,47 @@ describe("rightPanelStore", () => { | ... | @@ -304,6 +304,47 @@ describe("rightPanelStore", () => { |
| 304 | }); | 304 | }); |
| 305 | }); | 305 | }); |
| 306 | 306 | ||
| 307 | it("closing other surfaces keeps the selected surface active", () => { | ||
| 308 | useRightPanelStore.getState().openBrowser(refA, "tab-a"); | ||
| 309 | useRightPanelStore.getState().openFile(refA, "src/index.ts"); | ||
| 310 | useRightPanelStore.getState().openTerminal(refA, "term-1"); | ||
| 311 | |||
| 312 | useRightPanelStore.getState().closeOtherSurfaces(refA, "file:src/index.ts"); | ||
| 313 | |||
| 314 | expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ | ||
| 315 | isOpen: true, | ||
| 316 | activeSurfaceId: "file:src/index.ts", | ||
| 317 | surfaces: [{ id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }], | ||
| 318 | }); | ||
| 319 | }); | ||
| 320 | |||
| 321 | it("closing surfaces to the right activates the selected surface when active was removed", () => { | ||
| 322 | useRightPanelStore.getState().openBrowser(refA, "tab-a"); | ||
| 323 | useRightPanelStore.getState().openFile(refA, "src/index.ts"); | ||
| 324 | useRightPanelStore.getState().openTerminal(refA, "term-1"); | ||
| 325 | |||
| 326 | useRightPanelStore.getState().closeSurfacesToRight(refA, "browser:tab-a"); | ||
| 327 | |||
| 328 | expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ | ||
| 329 | isOpen: true, | ||
| 330 | activeSurfaceId: "browser:tab-a", | ||
| 331 | surfaces: [{ id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }], | ||
| 332 | }); | ||
| 333 | }); | ||
| 334 | |||
| 335 | it("closing all surfaces leaves the panel open and empty", () => { | ||
| 336 | useRightPanelStore.getState().openBrowser(refA, "tab-a"); | ||
| 337 | useRightPanelStore.getState().openFile(refA, "src/index.ts"); | ||
| 338 | |||
| 339 | useRightPanelStore.getState().closeAllSurfaces(refA); | ||
| 340 | |||
| 341 | expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ | ||
| 342 | isOpen: true, | ||
| 343 | activeSurfaceId: null, | ||
| 344 | surfaces: [], | ||
| 345 | }); | ||
| 346 | }); | ||
| 347 | |||
| 307 | it("reconciles browser surfaces without deleting other surface kinds", () => { | 348 | it("reconciles browser surfaces without deleting other surface kinds", () => { |
| 308 | useRightPanelStore.getState().openTerminal(refA, "term-1"); | 349 | useRightPanelStore.getState().openTerminal(refA, "term-1"); |
| 309 | useRightPanelStore.getState().openBrowser(refA, "tab-a"); | 350 | useRightPanelStore.getState().openBrowser(refA, "tab-a"); |
apps/web/src/rightPanelStore.ts+40| ... | @@ -58,6 +58,9 @@ interface RightPanelStoreState { | ... | @@ -58,6 +58,9 @@ interface RightPanelStoreState { |
| 58 | closeTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; | 58 | closeTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; |
| 59 | activateSurface: (ref: ScopedThreadRef, surfaceId: string) => void; | 59 | activateSurface: (ref: ScopedThreadRef, surfaceId: string) => void; |
| 60 | closeSurface: (ref: ScopedThreadRef, surfaceId: string) => void; | 60 | closeSurface: (ref: ScopedThreadRef, surfaceId: string) => void; |
| 61 | closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void; | ||
| 62 | closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void; | ||
| 63 | closeAllSurfaces: (ref: ScopedThreadRef) => void; | ||
| 61 | reconcileBrowserSurfaces: (ref: ScopedThreadRef, tabIds: readonly string[]) => void; | 64 | reconcileBrowserSurfaces: (ref: ScopedThreadRef, tabIds: readonly string[]) => void; |
| 62 | reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; | 65 | reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; |
| 63 | show: (ref: ScopedThreadRef) => void; | 66 | show: (ref: ScopedThreadRef) => void; |
| ... | @@ -334,6 +337,43 @@ export const useRightPanelStore = create<RightPanelStoreState>()( | ... | @@ -334,6 +337,43 @@ export const useRightPanelStore = create<RightPanelStoreState>()( |
| 334 | return { ...current, surfaces, activeSurfaceId: fallback?.id ?? null }; | 337 | return { ...current, surfaces, activeSurfaceId: fallback?.id ?? null }; |
| 335 | }), | 338 | }), |
| 336 | })), | 339 | })), |
| 340 | closeOtherSurfaces: (ref, surfaceId) => | ||
| 341 | set((state) => ({ | ||
| 342 | byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { | ||
| 343 | const surface = current.surfaces.find((entry) => entry.id === surfaceId); | ||
| 344 | if (!surface || current.surfaces.length === 1) return current; | ||
| 345 | return { | ||
| 346 | ...current, | ||
| 347 | isOpen: true, | ||
| 348 | surfaces: [surface], | ||
| 349 | activeSurfaceId: surface.id, | ||
| 350 | }; | ||
| 351 | }), | ||
| 352 | })), | ||
| 353 | closeSurfacesToRight: (ref, surfaceId) => | ||
| 354 | set((state) => ({ | ||
| 355 | byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { | ||
| 356 | const index = current.surfaces.findIndex((surface) => surface.id === surfaceId); | ||
| 357 | if (index < 0 || index === current.surfaces.length - 1) return current; | ||
| 358 | const surfaces = current.surfaces.slice(0, index + 1); | ||
| 359 | const activeStillExists = surfaces.some( | ||
| 360 | (surface) => surface.id === current.activeSurfaceId, | ||
| 361 | ); | ||
| 362 | return { | ||
| 363 | ...current, | ||
| 364 | surfaces, | ||
| 365 | activeSurfaceId: activeStillExists ? current.activeSurfaceId : surfaceId, | ||
| 366 | }; | ||
| 367 | }), | ||
| 368 | })), | ||
| 369 | closeAllSurfaces: (ref) => | ||
| 370 | set((state) => ({ | ||
| 371 | byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => | ||
| 372 | current.surfaces.length === 0 | ||
| 373 | ? current | ||
| 374 | : { ...current, isOpen: true, surfaces: [], activeSurfaceId: null }, | ||
| 375 | ), | ||
| 376 | })), | ||
| 337 | reconcileBrowserSurfaces: (ref, tabIds) => | 377 | reconcileBrowserSurfaces: (ref, tabIds) => |
| 338 | set((state) => ({ | 378 | set((state) => ({ |
| 339 | byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { | 379 | byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { |