Skip to content
Open
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
33 changes: 24 additions & 9 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
useRightPanelStore,
} from "../rightPanelStore";
import {
type PreviewMiniPlayerSource,
selectThreadPreviewMiniPlayer,
usePreviewMiniPlayerStore,
} from "../previewMiniPlayerStore";
Expand Down Expand Up @@ -130,6 +131,17 @@ describe("agent browser close confirmation", () => {
});

describe("floating browser preview", () => {
const isMiniPlayerVisible = (
source: PreviewMiniPlayerSource | null,
renderedRightPanelSurface: RightPanelSurface | null,
rightPanelSurfaceVisible = true,
) =>
shouldRenderPreviewMiniPlayer({
source,
renderedRightPanelSurface,
rightPanelSurfaceVisible,
});

it("keeps agent preview intent when a user selects its browser tab and then switches away", () => {
useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });
usePreviewMiniPlayerStore.setState({ byThreadKey: {} });
Expand All @@ -143,7 +155,7 @@ describe("floating browser preview", () => {
ref,
);
const isFloating = () =>
shouldRenderPreviewMiniPlayer(
isMiniPlayerVisible(
selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, ref)
?.source ?? null,
selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, ref),
Expand All @@ -163,22 +175,25 @@ describe("floating browser preview", () => {

it("only hides the duplicate while the same browser is rendered in the panel", () => {
const tab = { kind: "browser", tabId: "tab-1" } as const;
expect(shouldRenderPreviewMiniPlayer(null, null)).toBe(false);
expect(isMiniPlayerVisible(null, null, false)).toBe(false);
expect(
shouldRenderPreviewMiniPlayer(tab, {
isMiniPlayerVisible(tab, {
id: "browser:one",
kind: "preview",
resourceId: "tab-1",
}),
).toBe(false);
expect(
shouldRenderPreviewMiniPlayer(tab, {
isMiniPlayerVisible(tab, {
id: "browser:two",
kind: "preview",
resourceId: "tab-2",
}),
).toBe(true);
expect(shouldRenderPreviewMiniPlayer(tab, { id: "diff", kind: "diff" })).toBe(true);
expect(isMiniPlayerVisible(tab, { id: "diff", kind: "diff" })).toBe(true);
expect(
isMiniPlayerVisible(tab, { id: "browser:one", kind: "preview", resourceId: "tab-1" }, false),
).toBe(true);
});

it("only hides a floating device while that device is rendered in the panel", () => {
Expand All @@ -196,22 +211,22 @@ describe("floating browser preview", () => {
name: "Pixel",
} as const;
expect(
shouldRenderPreviewMiniPlayer(pixel, {
isMiniPlayerVisible(pixel, {
id: "device:nucbox:emulator-5580",
kind: "device",
target,
}),
).toBe(false);
expect(
shouldRenderPreviewMiniPlayer(pixel, {
isMiniPlayerVisible(pixel, {
id: "device:nucbox:emulator-5554",
kind: "device",
target: { ...target, deviceId: "emulator-5554" },
}),
).toBe(true);
expect(shouldRenderPreviewMiniPlayer(pixel, { id: "device", kind: "device" })).toBe(true);
expect(isMiniPlayerVisible(pixel, { id: "device", kind: "device" })).toBe(true);
expect(
shouldRenderPreviewMiniPlayer(pixel, {
isMiniPlayerVisible(pixel, {
id: "browser:one",
kind: "preview",
resourceId: "emulator-5580",
Expand Down
11 changes: 7 additions & 4 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,14 @@ export function agentControlledBrowserCloseConfirmation(
}

/** The floating player hides only while the same source is rendered in the panel. */
export function shouldRenderPreviewMiniPlayer(
source: PreviewMiniPlayerSource | null,
renderedRightPanelSurface: RightPanelSurface | null,
): boolean {
export function shouldRenderPreviewMiniPlayer(input: {
readonly source: PreviewMiniPlayerSource | null;
readonly renderedRightPanelSurface: RightPanelSurface | null;
readonly rightPanelSurfaceVisible: boolean;
}): boolean {
const { renderedRightPanelSurface, rightPanelSurfaceVisible, source } = input;
if (source === null) return false;
if (!rightPanelSurfaceVisible) return true;
if (source.kind === "browser") {
return !(
renderedRightPanelSurface?.kind === "preview" &&
Expand Down
92 changes: 68 additions & 24 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1960,10 +1960,12 @@ export default function ChatView(props: ChatViewProps) {
const activeRightPanelKind = useRightPanelStore((state) =>
selectActiveRightPanel(state.byThreadKey, activeThreadRef),
);
const diffOpen = activeRightPanelKind === "diff";
const rightPanelState = useRightPanelStore((state) =>
selectThreadRightPanelState(state.byThreadKey, activeThreadRef),
);
const threadTabSelected = useRightPanelStore((state) =>
activeThreadRef ? state.selectedThreadTabKey === scopedThreadKey(activeThreadRef) : false,
);
const activeRightPanelSurface = useRightPanelStore((state) =>
selectActiveRightPanelSurface(state.byThreadKey, activeThreadRef),
);
Expand Down Expand Up @@ -1992,7 +1994,6 @@ export default function ChatView(props: ChatViewProps) {
() => [...new Set([...activeKnownTerminalIds, ...panelTerminalIds])],
[activeKnownTerminalIds, panelTerminalIds],
);
const previewPanelOpen = activeRightPanelKind === "preview" && isPreviewSupportedInRuntime();
const rightPanelOpen = rightPanelState.isOpen;
const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
usePanelAnimationSettings();
Expand Down Expand Up @@ -2022,13 +2023,19 @@ export default function ChatView(props: ChatViewProps) {
const rightPanelControlsAtRoot = rightPanelPresent && !shouldUseRightPanelSheet;
const renderedRightPanelSurface = rightPanelPresence.value?.activeSurface ?? null;
const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];
const previewMiniPlayerVisible = shouldRenderPreviewMiniPlayer(
activePreviewMiniPlayer?.source ?? null,
renderedRightPanelSurface,
);
const canMaximizeRightPanel = rightPanelOpen && !shouldUseRightPanelSheet;
const rightPanelMaximized =
canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
const maximizedThreadTabActive = rightPanelMaximized && threadTabSelected;
const rightPanelSurfaceVisible = rightPanelOpen && !maximizedThreadTabActive;
const diffOpen = activeRightPanelKind === "diff" && rightPanelSurfaceVisible;
const previewPanelOpen =
activeRightPanelKind === "preview" && rightPanelSurfaceVisible && isPreviewSupportedInRuntime();
const previewMiniPlayerVisible = shouldRenderPreviewMiniPlayer({
source: activePreviewMiniPlayer?.source ?? null,
renderedRightPanelSurface,
rightPanelSurfaceVisible,
});
const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUseRightPanelSheet;

useEffect(() => {
Expand Down Expand Up @@ -3747,9 +3754,14 @@ export default function ChatView(props: ChatViewProps) {
onDiffPanelOpen?.();
}
if (activeThreadRef) {
useRightPanelStore.getState().toggle(activeThreadRef, "diff");
const panels = useRightPanelStore.getState();
if (maximizedThreadTabActive) {
panels.open(activeThreadRef, "diff");
} else {
panels.toggle(activeThreadRef, "diff");
}
}
}, [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen]);
}, [activeThreadRef, diffOpen, isServerThread, maximizedThreadTabActive, onDiffPanelOpen]);

const needsLoadBalancing = automaticEnvironment && !draftThread?.loadBalancedEnvironmentId;
const loadBalancingCandidates = useMemo(
Expand Down Expand Up @@ -4765,19 +4777,27 @@ export default function ChatView(props: ChatViewProps) {
const closePreviewPanel = useCallback(() => {
if (activeThreadRef) {
// Closing the panel on a live browser or device floats it instead of dropping it.
if (activeRightPanelSurface?.kind === "preview" && activeRightPanelSurface.resourceId) {
if (
rightPanelSurfaceVisible &&
activeRightPanelSurface?.kind === "preview" &&
activeRightPanelSurface.resourceId
) {
usePreviewMiniPlayerStore
.getState()
.open(activeThreadRef, browserMiniPlayerSource(activeRightPanelSurface.resourceId));
} else if (activeRightPanelSurface?.kind === "device" && activeRightPanelSurface.target) {
} else if (
rightPanelSurfaceVisible &&
activeRightPanelSurface?.kind === "device" &&
activeRightPanelSurface.target
) {
usePreviewMiniPlayerStore
.getState()
.open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target });
}
setMaximizedRightPanelThreadKey(null);
useRightPanelStore.getState().close(activeThreadRef);
}
}, [activeRightPanelSurface, activeThreadRef]);
}, [activeRightPanelSurface, activeThreadRef, rightPanelSurfaceVisible]);
const togglePreviewPanel = useCallback(() => {
if (!activeThreadRef || !isPreviewSupportedInRuntime()) return;
if (previewPanelOpen) {
Expand Down Expand Up @@ -4939,10 +4959,13 @@ export default function ChatView(props: ChatViewProps) {
}, [activeThreadRef, closePreviewPanel, rightPanelOpen]);
const toggleRightPanelMaximized = useCallback(() => {
if (!canMaximizeRightPanel) return;
setMaximizedRightPanelThreadKey((threadKey) =>
threadKey === routeThreadKey ? null : routeThreadKey,
);
}, [canMaximizeRightPanel, routeThreadKey]);
if (activeThreadRef) useRightPanelStore.getState().selectPanelTab(activeThreadRef);
if (rightPanelMaximized) {
setMaximizedRightPanelThreadKey(null);
return;
}
setMaximizedRightPanelThreadKey(routeThreadKey);
}, [activeThreadRef, canMaximizeRightPanel, rightPanelMaximized, routeThreadKey]);
const cleanupRightPanelSurfaces = useCallback(
(surfaces: readonly RightPanelSurface[]) => {
if (!activeThreadRef) return;
Expand Down Expand Up @@ -6680,7 +6703,7 @@ export default function ChatView(props: ChatViewProps) {
if (command === "rightPanel.close") {
// Nothing open: leave the event alone so the shortcut keeps its
// native meaning (close window on desktop, close tab in a browser).
if (!activeRightPanelSurface) return;
if (!activeRightPanelSurface || maximizedThreadTabActive) return;
event.preventDefault();
event.stopPropagation();
if (!event.repeat) closeRightPanelSurface(activeRightPanelSurface);
Expand Down Expand Up @@ -6833,6 +6856,7 @@ export default function ChatView(props: ChatViewProps) {
confirmAndUnpinThread,
copyActiveThreadReference,
getShortcutContext,
maximizedThreadTabActive,
toggleRightPanel,
toggleRightPanelMaximized,
toggleTerminalVisibility,
Expand Down Expand Up @@ -9092,7 +9116,9 @@ export default function ChatView(props: ChatViewProps) {
// Suppressed while the Agents surface is visible: the roster itself is
// on screen, so the toggle badge would be pointing at nothing.
liveAgentCount={
rightPanelOpen && activeRightPanelSurface?.kind === "agents" ? 0 : agentPanelModel.liveCount
rightPanelSurfaceVisible && activeRightPanelSurface?.kind === "agents"
? 0
: agentPanelModel.liveCount
}
onToggleTerminal={toggleTerminalVisibility}
onToggleRightPanel={toggleRightPanel}
Expand Down Expand Up @@ -9135,15 +9161,15 @@ export default function ChatView(props: ChatViewProps) {
threadRef={activeThreadRef}
tabId={renderedRightPanelSurface.resourceId}
configuredUrls={configuredPreviewUrls}
visible={rightPanelOpen}
visible={rightPanelSurfaceVisible}
onSendAnnotation={(annotation, image) => {
void onSend(undefined, "foreground", { annotation, image });
}}
/>
</Suspense>
) : renderedRightPanelSurface?.kind === "terminal" ? (
<PersistentThreadTerminalPanel
visible={rightPanelOpen}
visible={rightPanelSurfaceVisible}
threadRef={activeThreadRef}
surface={renderedRightPanelSurface}
launchContext={activeTerminalLaunchContext ?? null}
Expand Down Expand Up @@ -9186,7 +9212,7 @@ export default function ChatView(props: ChatViewProps) {
<PullRequestDetailPanel
getShortcutContext={getShortcutContext}
shortcutsEnabled={
rightPanelOpen && activeRightPanelSurface?.id === renderedRightPanelSurface.id
rightPanelSurfaceVisible && activeRightPanelSurface?.id === renderedRightPanelSurface.id
}
key={`${renderedRightPanelSurface.host ?? ""}:${renderedRightPanelSurface.repository}#${renderedRightPanelSurface.number}`}
environmentId={activeThread.environmentId}
Expand Down Expand Up @@ -9244,7 +9270,7 @@ export default function ChatView(props: ChatViewProps) {
threadRef={activeThreadRef}
key={renderedRightPanelSurface.id}
surface={renderedRightPanelSurface}
visible={rightPanelOpen}
visible={rightPanelSurfaceVisible}
onDismissSetup={() => {
closeRightPanelSurface(renderedRightPanelSurface);
useRightPanelStore.getState().show(activeThreadRef);
Expand Down Expand Up @@ -9333,16 +9359,23 @@ export default function ChatView(props: ChatViewProps) {
<div
className={cn(
"flex min-h-0 min-w-0 flex-col overflow-x-hidden",
rightPanelMaximized ? "w-0 flex-none" : "flex-1",
maximizedThreadTabActive
? "absolute inset-0 z-10 bg-background pt-[var(--workspace-topbar-height)]"
: rightPanelMaximized
? "w-0 flex-none"
: "flex-1",
)}
data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"}
data-chat-column-maximized-away={
rightPanelMaximized && !maximizedThreadTabActive ? "true" : "false"
}
inert={(rightPanelMaximized && !maximizedThreadTabActive) || undefined}
>
{/* Top bar */}
<WorkspacePageHeader
data-chat-header
electron={isElectron}
reserveNativeControls={reserveTitleBarControlInset && !inlineRightPanelOwnsTitleBar}
className="relative bg-background"
className={cn("relative bg-background", maximizedThreadTabActive && "hidden")}
>
{isElectron && rightPanelControlsAtRoot ? (
<span
Expand Down Expand Up @@ -9853,6 +9886,17 @@ export default function ChatView(props: ChatViewProps) {
widthStorageKey={`t3code:preview-panel-width:${activeThreadKey}`}
open={rightPanelOpen}
maximized={rightPanelMaximized}
{...(rightPanelMaximized
? {
threadTab: {
label: activeThread.title,
active: maximizedThreadTabActive,
onActivate: () => {
useRightPanelStore.getState().selectThreadTab(activeThreadRef);
},
},
}
: {})}
surfaces={renderedRightPanelSurfaces}
environmentId={activeThreadRef.environmentId}
activeSurfaceId={renderedRightPanelSurface?.id ?? null}
Expand Down
Loading
Loading