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
6 changes: 4 additions & 2 deletions apps/web/src/browser/BrowserSurfaceSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export function BrowserSurfaceSlot(props: {

useLayoutEffect(() => {
const element = elementRef.current;
if (!element) return;
// A closing panel can remain mounted for its animation, but must hand the
// browser to the floating player as soon as it stops being visible.
if (!element || !visible) return;
let lease = acquireBrowserSurface(tabId, fitSourceContent);
const update = () => {
const rect = element.getBoundingClientRect();
Expand Down Expand Up @@ -77,7 +79,7 @@ export function BrowserSurfaceSlot(props: {
if (updateRef.current === update) updateRef.current = null;
lease.release();
};
}, [fitSourceContent, tabId]);
}, [fitSourceContent, tabId, visible]);

useLayoutEffect(() => {
presentationRef.current = { visible, cornerRadius, zIndex };
Expand Down
30 changes: 17 additions & 13 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2322,8 +2322,8 @@ export default function ChatView(props: ChatViewProps) {
: undefined,
[activeThreadRef, activePreviewServerEpoch],
);
const activePreviewMiniPlayer = usePreviewMiniPlayerStore((state) =>
selectThreadPreviewMiniPlayer(state.byThreadKey, activeThreadRef),
const activePreviewMiniPlayerSource = usePreviewMiniPlayerStore(
(state) => selectThreadPreviewMiniPlayer(state.byThreadKey, activeThreadRef)?.source ?? null,
);
const panelTerminalIds = useMemo(
() =>
Expand Down Expand Up @@ -2381,8 +2381,8 @@ export default function ChatView(props: ChatViewProps) {
const renderedRightPanelSurface = rightPanelPresence.value?.activeSurface ?? null;
const renderedRightPanelSurfaces = rightPanelPresence.value?.surfaces ?? [];
const previewMiniPlayerVisible = shouldRenderPreviewMiniPlayer(
activePreviewMiniPlayer?.source ?? null,
renderedRightPanelSurface,
activePreviewMiniPlayerSource ?? null,
rightPanelOpen ? renderedRightPanelSurface : null,
);
const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet;
const rightPanelMaximized =
Expand Down Expand Up @@ -2416,7 +2416,7 @@ export default function ChatView(props: ChatViewProps) {
}, [activePreviewState.sessions, activeThreadRef, previewSessionsReady]);

useEffect(() => {
const source = activePreviewMiniPlayer?.source;
const source = activePreviewMiniPlayerSource;
if (
activeThreadRef &&
previewSessionsReady &&
Expand All @@ -2425,7 +2425,12 @@ export default function ChatView(props: ChatViewProps) {
) {
usePreviewMiniPlayerStore.getState().close(activeThreadRef);
}
}, [activePreviewMiniPlayer, activePreviewState.sessions, activeThreadRef, previewSessionsReady]);
}, [
activePreviewMiniPlayerSource,
activePreviewState.sessions,
activeThreadRef,
previewSessionsReady,
]);

const existingOpenTerminalThreadKeys = useMemo(() => {
const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
Expand Down Expand Up @@ -5534,7 +5539,7 @@ export default function ChatView(props: ChatViewProps) {
// closes the device there is nothing left to stream.
useEffect(() => {
if (!activeThreadRef || !deviceStateLoaded) return;
const source = activePreviewMiniPlayer?.source;
const source = activePreviewMiniPlayerSource;
if (source?.kind !== "device") return;
const sessionStillExists = deviceState.sessions.some(
(session) =>
Expand All @@ -5543,7 +5548,7 @@ export default function ChatView(props: ChatViewProps) {
session.deviceId === source.deviceId,
);
if (!sessionStillExists) usePreviewMiniPlayerStore.getState().close(activeThreadRef);
}, [activePreviewMiniPlayer, activeThreadRef, deviceState.sessions, deviceStateLoaded]);
}, [activePreviewMiniPlayerSource, activeThreadRef, deviceState.sessions, deviceStateLoaded]);
const openFileSurface = useCallback(
(relativePath: string) => {
if (!activeThreadRef || !activeProject) return;
Expand Down Expand Up @@ -11220,7 +11225,6 @@ export default function ChatView(props: ChatViewProps) {
<div className="relative flex min-h-0 min-w-0 flex-1">
{/* Chat column */}
<ChatCanvas
composerOverlayElement={isDraftHeroState ? null : composerOverlayElement}
data-chat-workspace-drop-target="true"
onDragEnter={workspaceFileDropHandlers.onDragEnter}
onDragOver={workspaceFileDropHandlers.onDragOver}
Expand Down Expand Up @@ -11724,13 +11728,13 @@ export default function ChatView(props: ChatViewProps) {
<PreviewSessionSync threadRef={activeThreadRef} />
) : null}
{activeThreadRef &&
activePreviewMiniPlayer &&
activePreviewMiniPlayerSource &&
previewMiniPlayerVisible &&
(activePreviewMiniPlayer.source.kind === "device" || canOperatePreview) ? (
(activePreviewMiniPlayerSource.kind === "device" || canOperatePreview) ? (
<ThreadPreviewMiniPlayer
key={`${activeThreadKey}:${previewMiniPlayerSourceKey(activePreviewMiniPlayer.source)}`}
key={`${activeThreadKey}:${previewMiniPlayerSourceKey(activePreviewMiniPlayerSource)}`}
threadRef={activeThreadRef}
miniPlayer={activePreviewMiniPlayer}
source={activePreviewMiniPlayerSource}
/>
) : null}

Expand Down
43 changes: 7 additions & 36 deletions apps/web/src/components/chat/ChatCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,21 @@ import {
type CSSProperties,
} from "react";
import { ChatCanvasContext } from "./ChatCanvasContext";
import { resolveChatCanvasLayout, type ChatCanvasPreview } from "./chatCanvasLayout";
import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLayout";
import { resolveChatCanvasLayout, type ChatCanvasDetailsCard } from "./chatCanvasLayout";

/**
* Owns the available conversation space. Cards only report where they sit; the
* canvas decides when chat moves over to make room for them.
*/
export function ChatCanvas({
composerOverlayElement,
children,
...props
}: Omit<ComponentProps<"div">, "className" | "style" | "ref"> & {
composerOverlayElement: HTMLElement | null;
}) {
}: Omit<ComponentProps<"div">, "className" | "style" | "ref">) {
const elementRef = useRef<HTMLDivElement | null>(null);
const widthProbeRef = useRef<HTMLDivElement | null>(null);
const [timelineElement, registerTimeline] = useState<HTMLElement | null>(null);
const [preview, setPreview] = useState<ChatCanvasPreview | null>(null);
const [detailsCard, setDetailsCard] = useState<PreviewMiniPlayerObstacles["detailsCard"]>(null);
const reportDetailsCard = useCallback((next: PreviewMiniPlayerObstacles["detailsCard"]) => {
const [detailsCard, setDetailsCard] = useState<ChatCanvasDetailsCard | null>(null);
const reportDetailsCard = useCallback((next: ChatCanvasDetailsCard | null) => {
setDetailsCard((current) =>
current?.left === next?.left &&
current?.right === next?.right &&
Expand All @@ -42,26 +37,8 @@ export function ChatCanvas({
padding: 20,
maxChatWidth: 768,
minChatWidth: 640,
composerHeight: 0,
timelineGutter: 0,
});
const reportPreview = useCallback((next: ChatCanvasPreview) => {
setPreview((current) =>
current?.key === next.key &&
current.width === next.width &&
current.lastInteraction === next.lastInteraction &&
current.position?.x === next.position?.x &&
current.position?.y === next.position?.y &&
current.source.width === next.source.width &&
current.source.height === next.source.height
? current
: next,
);
}, []);
const clearPreview = useCallback(
(key: string) => setPreview((current) => (current?.key === key ? null : current)),
[],
);
useLayoutEffect(() => {
const element = elementRef.current;
const probe = widthProbeRef.current;
Expand All @@ -74,7 +51,6 @@ export function ChatCanvas({
padding: Number.parseFloat(styles.paddingLeft),
maxChatWidth: Number.parseFloat(styles.width),
minChatWidth: Number.parseFloat(styles.minWidth),
composerHeight: composerOverlayElement?.getBoundingClientRect().height ?? 0,
timelineGutter: timelineElement
? (timelineElement.offsetWidth - timelineElement.clientWidth) / 2
: 0,
Expand All @@ -91,31 +67,26 @@ export function ChatCanvas({
const observer = new ResizeObserver(measure);
observer.observe(element);
observer.observe(probe);
if (composerOverlayElement) observer.observe(composerOverlayElement);
if (timelineElement) observer.observe(timelineElement);
return () => observer.disconnect();
}, [composerOverlayElement, timelineElement]);
}, [timelineElement]);
const context = useMemo(() => {
const container = { width: measurements.width, height: measurements.height };
return {
container,
lane: { padding: measurements.padding, minChatWidth: measurements.minChatWidth },
layout: resolveChatCanvasLayout({ ...measurements, container, preview, detailsCard }),
previewKey: preview?.key ?? null,
reportPreview,
clearPreview,
layout: resolveChatCanvasLayout({ ...measurements, container, detailsCard }),
registerTimeline,
reportDetailsCard,
};
}, [measurements, preview, detailsCard, reportPreview, clearPreview, reportDetailsCard]);
}, [measurements, detailsCard, reportDetailsCard]);
const { layout } = context;
return (
<ChatCanvasContext value={context}>
<div
{...props}
ref={elementRef}
data-chat-canvas
data-preview-overlaps-chat={layout.overlapsChat || undefined}
className="relative flex min-h-0 min-w-0 flex-1 flex-col"
style={
{
Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/components/chat/ChatCanvasContext.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { createContext, useContext } from "react";
import type { ChatCanvasPreview, resolveChatCanvasLayout } from "./chatCanvasLayout";
import type { PreviewMiniPlayerObstacles } from "../preview/previewMiniPlayerLayout";
import type { ChatCanvasDetailsCard, resolveChatCanvasLayout } from "./chatCanvasLayout";

export const ChatCanvasContext = createContext<{
container: { width: number; height: number };
lane: { padding: number; minChatWidth: number };
layout: ReturnType<typeof resolveChatCanvasLayout>;
previewKey: string | null;
reportPreview: (preview: ChatCanvasPreview) => void;
clearPreview: (key: string) => void;
registerTimeline: (element: HTMLElement | null) => void;
reportDetailsCard: (card: PreviewMiniPlayerObstacles["detailsCard"]) => void;
reportDetailsCard: (card: ChatCanvasDetailsCard | null) => void;
} | null>(null);

export const useChatCanvas = () => useContext(ChatCanvasContext);
10 changes: 1 addition & 9 deletions apps/web/src/components/chat/ThreadDetailsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,9 @@ export function ThreadDetailsCard({
? resolveThreadDetailsCardLayout({
container: canvas.container,
lane: canvas.lane,
frame: null,
})
: null;
const placement = canvas
? resolveThreadDetailsCardLayout({
container: canvas.container,
lane: canvas.lane,
frame: canvas.layout.frame,
overlapsDetailsCard: canvas.layout.overlapsDetailsCard,
})
: null;
const placement = preferredPlacement;
const mode = placement ? "inline" : "popover";
const inlineOpen = useRightPanelStore((state) =>
selectThreadPanelOpen(state.threadPanelVisibilityByThreadKey, threadRef, "inline"),
Expand Down
Loading
Loading