Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 46 additions & 217 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,7 +11,6 @@ import {
peekHeldThreadTimeline,
peekRememberedThreadTimeline,
rememberReadyThreadTimeline,
resolveExplicitDiffOpenOnActivate,
resolveThreadSwitchTimeline,
timelineHasEphemeralPreviewUrls,
recallCheckoutIsRepo,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -286,7 +275,6 @@ import {
BranchRestoreConfirmHost,
DeviceSetupDialogHost,
openBranchRestoreConfirm,
openDeviceSetupDialog,
openPullRequestThreadDialog,
openRevertConfirm,
PullRequestThreadDialogHost,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading