diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 75438f6e7828..cc416448e351 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1,7 +1,6 @@ +import { useRightPanelActions } from "./chat/useRightPanelActions"; import { type TerminalLaunchContext, useChatTerminalActions } from "./chat/useChatTerminalActions"; import { useComposerSend } from "./chat/useComposerSend"; -import { formatFilePathForClipboard } from "@t3tools/client-runtime/media-actions"; -import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { ThreadFind, ThreadFindCanvas, type ThreadFindControls } from "./chat/ThreadFindProvider"; import { THREAD_FIND_BAR_RESERVED_HEIGHT } from "./chat/ThreadFindBar"; import { usageLimitRecoveryBannerItem } from "./chat/UsageLimitRecoveryBanner"; @@ -12,7 +11,6 @@ import { peekHeldThreadTimeline, peekRememberedThreadTimeline, rememberReadyThreadTimeline, - resolveExplicitDiffOpenOnActivate, resolveThreadSwitchTimeline, timelineHasEphemeralPreviewUrls, recallCheckoutIsRepo, @@ -159,24 +157,15 @@ import { selectActiveRightPanelSurface, selectThreadPanelOpen, selectThreadRightPanelState, - type RightPanelSurface, useRightPanelStore, } from "../rightPanelStore"; -import { - isPreviewSupportedInRuntime, - setActivePreviewTab, - useThreadPreviewState, -} from "../previewStateStore"; -import { BrowserSettingsReadError } from "../browser/openFileInPreview"; +import { isPreviewSupportedInRuntime, useThreadPreviewState } from "../previewStateStore"; import { previewRuntimeTabId } from "../browser/previewRuntimeTabId"; -import { addBrowserSurface } from "./preview/addBrowserSurface"; import { ThreadPreviewMiniPlayer } from "./preview/ThreadPreviewMiniPlayer"; import { usePreviewSession } from "./preview/usePreviewSession"; -import { subscribePreviewAction } from "./preview/previewActionBus"; import { getConfiguredPreviewUrls } from "./preview/previewEmptyStateLogic"; import { - browserMiniPlayerSource, previewMiniPlayerSourceKey, selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore, @@ -286,7 +275,6 @@ import { BranchRestoreConfirmHost, DeviceSetupDialogHost, openBranchRestoreConfirm, - openDeviceSetupDialog, openPullRequestThreadDialog, openRevertConfirm, PullRequestThreadDialogHost, @@ -380,7 +368,6 @@ import { } from "./ChatView.logic"; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { useComposerHandleContext } from "../composerHandleContext"; -import { previewEnvironment } from "../state/preview"; import { clampFileAttachmentUploadBytes } from "@t3tools/client-runtime/state/attachments"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { assetEnvironment } from "../state/assets"; @@ -509,7 +496,6 @@ export default function ChatView(props: ChatViewProps) { const forkThreadFromRun = useAtomCommand(threadEnvironment.forkFromRun, { reportFailure: false, }); - const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false }); const { environments } = useEnvironments(); const providersByEnvironment = useEnvironmentProviders(); const primaryEnvironment = usePrimaryEnvironment(); @@ -2305,18 +2291,6 @@ export default function ChatView(props: ChatViewProps) { "terminal.close", terminalShortcutLabelOptions, ); - const onToggleDiff = () => { - if (!isServerThread) { - return; - } - if (!diffOpen) { - onDiffPanelOpen?.(); - } - if (activeThreadRef) { - useRightPanelStore.getState().toggle(activeThreadRef, "diff"); - } - }; - const onToggleDiffStable = useStableCallback(onToggleDiff); const { needsLoadBalancing, @@ -2498,43 +2472,6 @@ export default function ChatView(props: ChatViewProps) { activeThreadRef?.environmentId ?? null, AuthPreviewOperateScope, ); - const createBrowserSurface = (profileId?: string) => { - if (!activeThreadRef || !canOperatePreview) return; - void addBrowserSurface({ - threadRef: activeThreadRef, - openPreview, - ...(profileId === undefined ? {} : { profileId }), - }).then((result) => { - if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; - const error = squashAtomCommandFailure(result); - if (error instanceof BrowserSettingsReadError) { - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Unable to open browser", - description: error.message, - }), - ); - } - }); - }; - const createBrowserSurfaceStable = useStableCallback(createBrowserSurface); - const addDiffSurface = () => { - if (!activeThreadRef || !isServerThread || !isGitRepo) return; - useDiffPanelStore.getState().selectGitScope(activeThreadRef, "branch"); - useRightPanelStore.getState().open(activeThreadRef, "diff"); - onDiffPanelOpen?.(); - }; - const addDiffSurfaceStable = useStableCallback(addDiffSurface); - const openChangesFromThreadPanel = () => { - addDiffSurface(); - }; - const openChangesFromThreadPanelStable = useStableCallback(openChangesFromThreadPanel); - const addFilesSurface = () => { - if (!activeThreadRef || !activeProject) return; - useRightPanelStore.getState().open(activeThreadRef, "files"); - }; - const addFilesSurfaceStable = useStableCallback(addFilesSurface); const supportsThreadPullRequests = serverConfig?.environment.capabilities.threadPullRequests === true; const visiblePullRequests = visibleThreadPullRequests( @@ -2543,23 +2480,9 @@ export default function ChatView(props: ChatViewProps) { const visiblePullRequestCount = visiblePullRequests.length; const pullRequestsSurfaceAvailable = isServerThread && supportsThreadPullRequests && visiblePullRequestCount > 0; - const addPullRequestsSurface = () => { - if (!activeThreadRef || !pullRequestsSurfaceAvailable) return; - useRightPanelStore.getState().open(activeThreadRef, "pull-requests"); - }; - const addPullRequestsSurfaceStable = useStableCallback(addPullRequestsSurface); const { state: deviceState, loaded: deviceStateLoaded } = useDeviceState( activeThreadRef?.environmentId ?? null, ); - const addDeviceSurface = () => { - if (!activeThreadRef) return; - if (!deviceState.onboardingCompleted || deviceState.hostStatus === "disabled") { - openDeviceSetupDialog(scopedThreadKey(activeThreadRef)); - return; - } - useRightPanelStore.getState().open(activeThreadRef, "device"); - }; - const addDeviceSurfaceStable = useStableCallback(addDeviceSurface); useAgentOpenedPreviews({ threadRef: activeThreadRef, deviceState, @@ -2570,11 +2493,6 @@ export default function ChatView(props: ChatViewProps) { miniPlayerSource: activePreviewMiniPlayer?.source ?? null, sheetLayout: shouldUsePlanSidebarSheet, }); - const openFileSurface = (relativePath: string) => { - if (!activeThreadRef || !activeProject) return; - useRightPanelStore.getState().openFile(activeThreadRef, relativePath); - }; - const openFileSurfaceStable = useStableCallback(openFileSurface); // The thread's own change request, placed against the project it belongs to. Without a // project there is nothing to resolve it against, so the caller falls back to the browser. const activeProjectRepository = sourceControlRepositorySelector( @@ -2620,81 +2538,12 @@ export default function ChatView(props: ChatViewProps) { visiblePullRequestCount, onDiffPanelOpen, }); - const closePreviewPanel = () => { - if (activeThreadRef) { - if (activeRightPanelSurface?.kind === "preview" && activeRightPanelSurface.resourceId) { - usePreviewMiniPlayerStore - .getState() - .open(activeThreadRef, browserMiniPlayerSource(activeRightPanelSurface.resourceId)); - } else if (activeRightPanelSurface?.kind === "device" && activeRightPanelSurface.target) { - usePreviewMiniPlayerStore - .getState() - .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target }); - } - useRightPanelStore.getState().setMaximized(activeThreadRef, false); - useRightPanelStore.getState().close(activeThreadRef); - } - }; - const closePreviewPanelStable = useStableCallback(closePreviewPanel); - const togglePreviewPanel = () => { - if (!activeThreadRef || !browserAvailable) return; - if (previewPanelOpen) { - closePreviewPanel(); - return; - } - if (!canOperatePreview) return; - const activeTabId = activePreviewState.activeTabId; - if (activeTabId) { - useRightPanelStore.getState().openBrowser(activeThreadRef, activeTabId); - } else { - createBrowserSurface(); - } - }; - const togglePreviewPanelStable = useStableCallback(togglePreviewPanel); - const activateRightPanelSurface = (surface: RightPanelSurface) => { - if (!activeThreadRef) return; - explicitDiffOpenRef.current = resolveExplicitDiffOpenOnActivate({ - surfaceKind: surface.kind, - diffOpen, - activeThreadRef, - }); - useRightPanelStore.getState().activateSurface(activeThreadRef, surface.id); - if (surface.kind === "preview" && surface.resourceId) { - setActivePreviewTab(activeThreadRef, surface.resourceId); - } - if (surface.kind === "terminal") { - setTerminalFocusRequestId((value) => value + 1); - } - if (surface.kind === "diff" && !diffOpen) { - onDiffPanelOpen?.(); - } - }; - const activateRightPanelSurfaceStable = useStableCallback(activateRightPanelSurface); - const toggleRightPanel = () => { - if (!activeThreadRef) return; - if (rightPanelOpen) { - closePreviewPanel(); - return; - } - useRightPanelStore.getState().toggleVisibility(activeThreadRef); - }; - const toggleRightPanelStable = useStableCallback(toggleRightPanel); - const toggleThreadPanel = () => { - if (!activeThreadRef) return; - useRightPanelStore.getState().toggleThreadPanel(activeThreadRef, threadPanelPresentation); - }; - const toggleThreadPanelStable = useStableCallback(toggleThreadPanel); useEffect(() => { if (!canMaximizeRightPanel) return; if (useRightPanelStore.getState().consumeMaximizeRequest(routeThreadRef)) { useRightPanelStore.getState().setMaximized(routeThreadRef, true); } }, [canMaximizeRightPanel, routeThreadRef]); - const toggleRightPanelMaximized = () => { - if (!canMaximizeRightPanel || !activeThreadRef) return; - useRightPanelStore.getState().setMaximized(activeThreadRef, !rightPanelMaximized); - }; - const toggleRightPanelMaximizedStable = useStableCallback(toggleRightPanelMaximized); const { closeSurface: closeRightPanelSurface, closeOtherSurfaces: closeOtherRightPanelSurfacesStable, @@ -2705,54 +2554,6 @@ export default function ChatView(props: ChatViewProps) { terminalLabelsById: activeTerminalLabelsById, }); const closeRightPanelSurfaceStable = closeRightPanelSurface; - const moveRightPanelSurface = (surfaceId: string, toIndex: number) => { - if (activeThreadRef) - useRightPanelStore.getState().moveSurface(activeThreadRef, surfaceId, toIndex); - }; - const moveRightPanelSurfaceStable = useStableCallback(moveRightPanelSurface); - const copyRightPanelFilePath = (relativePath: string) => { - if (typeof window === "undefined" || !navigator.clipboard?.writeText) { - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Failed to copy path", - description: "Clipboard API unavailable.", - }), - ); - return; - } - - const clipboardPath = formatFilePathForClipboard( - relativePath, - resolvePathLinkTarget(relativePath, gitCwd ?? activeProjectCwd ?? ""), - ); - void navigator.clipboard.writeText(clipboardPath).then( - () => { - toastManager.add({ - type: "success", - title: "Path copied", - description: clipboardPath, - }); - }, - (error) => { - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Failed to copy path", - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); - }, - ); - }; - const copyRightPanelFilePathStable = useStableCallback(copyRightPanelFilePath); - useEffect( - () => - subscribePreviewAction((action) => { - if (action === "toggle-panel") togglePreviewPanelStable(); - }), - [togglePreviewPanelStable], - ); const persistThreadSettingsForNextTurn = async (input: { threadId: ThreadId; createdAt: string; @@ -3171,12 +2972,50 @@ export default function ChatView(props: ChatViewProps) { branchPullRequest: activeThreadShell?.branchPullRequest ?? activeThread.branchPullRequest, }) : null; - const addPullRequestSurface = () => { - if (!supportsPullRequests || activeThreadRef === null || pullRequestPanelTarget === null) - return; - useRightPanelStore.getState().openPullRequest(activeThreadRef, pullRequestPanelTarget); - }; - const addPullRequestSurfaceStable = useStableCallback(addPullRequestSurface); + const { + activateRightPanelSurfaceStable, + addDeviceSurfaceStable, + addDiffSurfaceStable, + addFilesSurfaceStable, + addPullRequestSurfaceStable, + addPullRequestsSurfaceStable, + closePreviewPanelStable, + copyRightPanelFilePathStable, + createBrowserSurface, + createBrowserSurfaceStable, + moveRightPanelSurfaceStable, + onOpenTurnDiffStable, + onToggleDiffStable, + openChangesFromThreadPanelStable, + openFileSurfaceStable, + toggleRightPanelMaximizedStable, + toggleRightPanelStable, + toggleThreadPanelStable, + } = useRightPanelActions({ + activeThreadRef, + activeProject, + activeRightPanelSurface, + activePreviewState, + deviceState, + pullRequestPanelTarget, + threadPanelPresentation, + isServerThread, + isGitRepo, + browserAvailable, + canOperatePreview, + diffOpen, + previewPanelOpen, + rightPanelOpen, + canMaximizeRightPanel, + rightPanelMaximized, + supportsPullRequests, + pullRequestsSurfaceAvailable, + gitCwd, + activeProjectCwd, + explicitDiffOpenRef, + setTerminalFocusRequestId, + onDiffPanelOpen, + }); const pullRequestSurfaceAvailable = supportsPullRequests && pullRequestPanelTarget !== null; const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true; const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true; @@ -4462,16 +4301,6 @@ export default function ChatView(props: ChatViewProps) { openExpandedImagePreview(routeThreadKey, preview); }; const onExpandTimelineImageStable = useStableCallback(onExpandTimelineImage); - const onOpenTurnDiff = (runId: RunId, filePath?: string) => { - if (!isServerThread || !activeThreadRef) return; - const diffOpen = - selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) === "diff"; - explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef; - useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath); - useRightPanelStore.getState().open(activeThreadRef, "diff"); - onDiffPanelOpen?.(); - }; - const onOpenTurnDiffStable = useStableCallback(onOpenTurnDiff); const onTimelineRollbackCheckpoint = (input: { readonly checkpointId: string; readonly scopeId: string; diff --git a/apps/web/src/components/chat/useRightPanelActions.ts b/apps/web/src/components/chat/useRightPanelActions.ts new file mode 100644 index 000000000000..d57a87e382e6 --- /dev/null +++ b/apps/web/src/components/chat/useRightPanelActions.ts @@ -0,0 +1,313 @@ +import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; +import type { Dispatch } from "react"; +import type { ThreadPreviewState } from "../../previewStateStore"; +import { formatFilePathForClipboard } from "@t3tools/client-runtime/media-actions"; +import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; +import { resolveExplicitDiffOpenOnActivate } from "../ChatView.logic"; +import { useStableCallback } from "../../hooks/useStableCallback"; +import { type ScopedThreadRef, type RunId } from "@t3tools/contracts"; +import { scopedThreadKey } from "@t3tools/client-runtime/environment"; +import { type SetStateAction, useEffect } from "react"; +import { + isAtomCommandInterrupted, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; +import { useDiffPanelStore } from "../../diffPanelStore"; +import { type ThreadPanelPresentation } from "../../rightPanelLayout"; +import { + selectActiveRightPanel, + type RightPanelSurface, + useRightPanelStore, +} from "../../rightPanelStore"; +import { setActivePreviewTab } from "../../previewStateStore"; +import { BrowserSettingsReadError } from "../../browser/openFileInPreview"; +import { addBrowserSurface } from "../preview/addBrowserSurface"; +import { subscribePreviewAction } from "../preview/previewActionBus"; +import { browserMiniPlayerSource, usePreviewMiniPlayerStore } from "../../previewMiniPlayerStore"; +import type { threadPullRequestPanelTarget } from "../pullRequest/pullRequestDetail.logic"; +import type { useDeviceState } from "../../state/device"; +import { stackedThreadToast, toastManager } from "../ui/toast"; +import { openDeviceSetupDialog } from "./ChatOverlayHosts"; +import { previewEnvironment } from "../../state/preview"; +import { useAtomCommand } from "../../state/use-atom-command"; +interface RightPanelActionsInput { + readonly activeThreadRef: ScopedThreadRef | null; + readonly activeProject: EnvironmentProject | null; + readonly activeRightPanelSurface: RightPanelSurface | null; + readonly activePreviewState: ThreadPreviewState; + readonly deviceState: ReturnType["state"]; + readonly pullRequestPanelTarget: ReturnType | null; + readonly threadPanelPresentation: ThreadPanelPresentation; + readonly isServerThread: boolean; + readonly isGitRepo: boolean; + readonly browserAvailable: boolean; + readonly canOperatePreview: boolean; + readonly diffOpen: boolean; + readonly previewPanelOpen: boolean; + readonly rightPanelOpen: boolean; + readonly canMaximizeRightPanel: boolean; + readonly rightPanelMaximized: boolean; + readonly supportsPullRequests: boolean; + readonly pullRequestsSurfaceAvailable: boolean; + readonly gitCwd: string | null; + readonly activeProjectCwd: string | null; + readonly explicitDiffOpenRef: { current: ScopedThreadRef | null }; + readonly setTerminalFocusRequestId: Dispatch>; + readonly onDiffPanelOpen: (() => void) | undefined; +} + +/** + * Opening, closing, and arranging the right panel's surfaces: browser, diff, files, pull + * requests, device, and terminal tabs. + */ +export function useRightPanelActions(input: RightPanelActionsInput) { + const { + activeThreadRef, + activeProject, + activeRightPanelSurface, + activePreviewState, + deviceState, + pullRequestPanelTarget, + threadPanelPresentation, + isServerThread, + isGitRepo, + browserAvailable, + canOperatePreview, + diffOpen, + previewPanelOpen, + rightPanelOpen, + canMaximizeRightPanel, + rightPanelMaximized, + supportsPullRequests, + pullRequestsSurfaceAvailable, + gitCwd, + activeProjectCwd, + explicitDiffOpenRef, + setTerminalFocusRequestId, + onDiffPanelOpen, + } = input; + + const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false }); + const onToggleDiff = () => { + if (!isServerThread) { + return; + } + if (!diffOpen) { + onDiffPanelOpen?.(); + } + if (activeThreadRef) { + useRightPanelStore.getState().toggle(activeThreadRef, "diff"); + } + }; + const onToggleDiffStable = useStableCallback(onToggleDiff); + const createBrowserSurface = (profileId?: string) => { + if (!activeThreadRef || !canOperatePreview) return; + void addBrowserSurface({ + threadRef: activeThreadRef, + openPreview, + ...(profileId === undefined ? {} : { profileId }), + }).then((result) => { + if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; + const error = squashAtomCommandFailure(result); + if (error instanceof BrowserSettingsReadError) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open browser", + description: error.message, + }), + ); + } + }); + }; + const createBrowserSurfaceStable = useStableCallback(createBrowserSurface); + const addDiffSurface = () => { + if (!activeThreadRef || !isServerThread || !isGitRepo) return; + useDiffPanelStore.getState().selectGitScope(activeThreadRef, "branch"); + useRightPanelStore.getState().open(activeThreadRef, "diff"); + onDiffPanelOpen?.(); + }; + const addDiffSurfaceStable = useStableCallback(addDiffSurface); + const openChangesFromThreadPanel = () => { + addDiffSurface(); + }; + const openChangesFromThreadPanelStable = useStableCallback(openChangesFromThreadPanel); + const addFilesSurface = () => { + if (!activeThreadRef || !activeProject) return; + useRightPanelStore.getState().open(activeThreadRef, "files"); + }; + const addFilesSurfaceStable = useStableCallback(addFilesSurface); + const addPullRequestsSurface = () => { + if (!activeThreadRef || !pullRequestsSurfaceAvailable) return; + useRightPanelStore.getState().open(activeThreadRef, "pull-requests"); + }; + const addPullRequestsSurfaceStable = useStableCallback(addPullRequestsSurface); + const addDeviceSurface = () => { + if (!activeThreadRef) return; + if (!deviceState.onboardingCompleted || deviceState.hostStatus === "disabled") { + openDeviceSetupDialog(scopedThreadKey(activeThreadRef)); + return; + } + useRightPanelStore.getState().open(activeThreadRef, "device"); + }; + const addDeviceSurfaceStable = useStableCallback(addDeviceSurface); + const openFileSurface = (relativePath: string) => { + if (!activeThreadRef || !activeProject) return; + useRightPanelStore.getState().openFile(activeThreadRef, relativePath); + }; + const openFileSurfaceStable = useStableCallback(openFileSurface); + const closePreviewPanel = () => { + if (activeThreadRef) { + if (activeRightPanelSurface?.kind === "preview" && activeRightPanelSurface.resourceId) { + usePreviewMiniPlayerStore + .getState() + .open(activeThreadRef, browserMiniPlayerSource(activeRightPanelSurface.resourceId)); + } else if (activeRightPanelSurface?.kind === "device" && activeRightPanelSurface.target) { + usePreviewMiniPlayerStore + .getState() + .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target }); + } + useRightPanelStore.getState().setMaximized(activeThreadRef, false); + useRightPanelStore.getState().close(activeThreadRef); + } + }; + const closePreviewPanelStable = useStableCallback(closePreviewPanel); + const togglePreviewPanel = () => { + if (!activeThreadRef || !browserAvailable) return; + if (previewPanelOpen) { + closePreviewPanel(); + return; + } + if (!canOperatePreview) return; + const activeTabId = activePreviewState.activeTabId; + if (activeTabId) { + useRightPanelStore.getState().openBrowser(activeThreadRef, activeTabId); + } else { + createBrowserSurface(); + } + }; + const togglePreviewPanelStable = useStableCallback(togglePreviewPanel); + const activateRightPanelSurface = (surface: RightPanelSurface) => { + if (!activeThreadRef) return; + explicitDiffOpenRef.current = resolveExplicitDiffOpenOnActivate({ + surfaceKind: surface.kind, + diffOpen, + activeThreadRef, + }); + useRightPanelStore.getState().activateSurface(activeThreadRef, surface.id); + if (surface.kind === "preview" && surface.resourceId) { + setActivePreviewTab(activeThreadRef, surface.resourceId); + } + if (surface.kind === "terminal") { + setTerminalFocusRequestId((value) => value + 1); + } + if (surface.kind === "diff" && !diffOpen) { + onDiffPanelOpen?.(); + } + }; + const activateRightPanelSurfaceStable = useStableCallback(activateRightPanelSurface); + const toggleRightPanel = () => { + if (!activeThreadRef) return; + if (rightPanelOpen) { + closePreviewPanel(); + return; + } + useRightPanelStore.getState().toggleVisibility(activeThreadRef); + }; + const toggleRightPanelStable = useStableCallback(toggleRightPanel); + const toggleThreadPanel = () => { + if (!activeThreadRef) return; + useRightPanelStore.getState().toggleThreadPanel(activeThreadRef, threadPanelPresentation); + }; + const toggleThreadPanelStable = useStableCallback(toggleThreadPanel); + const toggleRightPanelMaximized = () => { + if (!canMaximizeRightPanel || !activeThreadRef) return; + useRightPanelStore.getState().setMaximized(activeThreadRef, !rightPanelMaximized); + }; + const toggleRightPanelMaximizedStable = useStableCallback(toggleRightPanelMaximized); + const moveRightPanelSurface = (surfaceId: string, toIndex: number) => { + if (activeThreadRef) + useRightPanelStore.getState().moveSurface(activeThreadRef, surfaceId, toIndex); + }; + const moveRightPanelSurfaceStable = useStableCallback(moveRightPanelSurface); + const copyRightPanelFilePath = (relativePath: string) => { + if (typeof window === "undefined" || !navigator.clipboard?.writeText) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to copy path", + description: "Clipboard API unavailable.", + }), + ); + return; + } + + const clipboardPath = formatFilePathForClipboard( + relativePath, + resolvePathLinkTarget(relativePath, gitCwd ?? activeProjectCwd ?? ""), + ); + void navigator.clipboard.writeText(clipboardPath).then( + () => { + toastManager.add({ + type: "success", + title: "Path copied", + description: clipboardPath, + }); + }, + (error) => { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to copy path", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + }, + ); + }; + const copyRightPanelFilePathStable = useStableCallback(copyRightPanelFilePath); + const addPullRequestSurface = () => { + if (!supportsPullRequests || activeThreadRef === null || pullRequestPanelTarget === null) + return; + useRightPanelStore.getState().openPullRequest(activeThreadRef, pullRequestPanelTarget); + }; + const addPullRequestSurfaceStable = useStableCallback(addPullRequestSurface); + const onOpenTurnDiff = (runId: RunId, filePath?: string) => { + if (!isServerThread || !activeThreadRef) return; + const diffOpen = + selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) === "diff"; + explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef; + useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath); + useRightPanelStore.getState().open(activeThreadRef, "diff"); + onDiffPanelOpen?.(); + }; + const onOpenTurnDiffStable = useStableCallback(onOpenTurnDiff); + useEffect( + () => + subscribePreviewAction((action) => { + if (action === "toggle-panel") togglePreviewPanelStable(); + }), + [togglePreviewPanelStable], + ); + + return { + activateRightPanelSurfaceStable, + addDeviceSurfaceStable, + addDiffSurfaceStable, + addFilesSurfaceStable, + addPullRequestSurfaceStable, + addPullRequestsSurfaceStable, + closePreviewPanelStable, + copyRightPanelFilePathStable, + createBrowserSurface, + createBrowserSurfaceStable, + moveRightPanelSurfaceStable, + onOpenTurnDiffStable, + onToggleDiffStable, + openChangesFromThreadPanelStable, + openFileSurfaceStable, + toggleRightPanelMaximizedStable, + toggleRightPanelStable, + toggleThreadPanelStable, + }; +}