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
42 changes: 32 additions & 10 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2304,12 +2304,23 @@ function useChatMarkdownState({
githubMedia = false,
}: ChatMarkdownProps) {
const { resolvedTheme } = useTheme();
const [localMediaPreview, setLocalMediaPreview] = useState<ExpandedImagePreview | null>(null);
const [localMediaPreviewDialog, setLocalMediaPreviewDialog] = useState<{
open: boolean;
preview: ExpandedImagePreview;
generation: number;
} | null>(null);
const markdownRef = useRef<HTMLDivElement>(null);
const expandMedia = onImageExpand ?? setLocalMediaPreview;
const openLocalMediaPreview = useCallback((preview: ExpandedImagePreview) => {
setLocalMediaPreviewDialog((current) => ({
open: true,
preview,
generation: (current?.generation ?? 0) + 1,
}));
}, []);
const expandMedia = onImageExpand ?? openLocalMediaPreview;
const mediaRequestId = useRef(0);
useEffect(() => {
setLocalMediaPreview(null);
setLocalMediaPreviewDialog(null);
return () => {
mediaRequestId.current += 1;
};
Expand Down Expand Up @@ -2755,8 +2766,8 @@ function useChatMarkdownState({
handleCopy,
markdownRef,
markdownUrlTransform,
localMediaPreview,
setLocalMediaPreview,
localMediaPreviewDialog,
setLocalMediaPreviewDialog,
};
}

Expand Down Expand Up @@ -3328,8 +3339,8 @@ function ChatMarkdown({
handleCopy,
markdownRef,
markdownUrlTransform,
localMediaPreview,
setLocalMediaPreview,
localMediaPreviewDialog,
setLocalMediaPreviewDialog,
} = useChatMarkdownState({ text, ...props });
const incrementalParsing =
props.isStreaming === true &&
Expand Down Expand Up @@ -3369,10 +3380,21 @@ function ChatMarkdown({
{text}
</ReactMarkdown>
</ChatMarkdownRendererContext>
{localMediaPreview ? (
{localMediaPreviewDialog ? (
<ExpandedImageDialog
preview={localMediaPreview}
onClose={() => setLocalMediaPreview(null)}
key={localMediaPreviewDialog.generation}
preview={localMediaPreviewDialog.preview}
open={localMediaPreviewDialog.open}
onOpenChange={(open) => {
if (!open) {
setLocalMediaPreviewDialog((current) =>
current ? { ...current, open: false } : current,
);
}
}}
onOpenChangeComplete={(open) => {
if (!open) setLocalMediaPreviewDialog(null);
}}
/>
) : null}
</div>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ export function collectUserMessageBlobPreviewUrls(message: ChatMessage): string[
}

export interface PullRequestDialogState {
open: boolean;
initialReference: string | null;
key: number;
}
Expand Down
49 changes: 34 additions & 15 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ import { resolveTimelineIsAtEnd, worktreeSetupAgentStarted } from "./chat/Messag
import { resolveComposerTimelineInset, resolveScrollToEndClearance } from "./composerFooterLayout";
import { ChatHeader } from "./chat/ChatHeader";
import { PanelLayoutControls, RightPanelMaximizeControl } from "./chat/PanelLayoutControls";
import { expandedImageKey, type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
import type { ExpandedImagePreview } from "./chat/ExpandedImagePreview";
import { NoActiveThreadState } from "./NoActiveThreadState";
import { WorkspacePageHeader } from "./WorkspacePageHeader";
import {
Expand Down Expand Up @@ -1688,13 +1688,17 @@ export default function ChatView(props: ChatViewProps) {
);
const [isWorkspaceFileDragActive, setIsWorkspaceFileDragActive] = useState(false);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
const [expandedImageDialog, setExpandedImageDialog] = useState<{
readonly open: boolean;
readonly preview: ExpandedImagePreview;
readonly generation: number;
} | null>(null);
useEffect(() => {
const item = expandedImage?.images[expandedImage.index];
const item = expandedImageDialog?.preview.images[expandedImageDialog.preview.index];
if (item?.type !== "video" || item.src === null || !item.src.startsWith("blob:")) return;
const src = item.src;
return () => revokeBlobPreviewUrl(src);
}, [expandedImage]);
}, [expandedImageDialog?.preview]);
const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
// Last live snapshot from the setup stream. The server drops a finished
// snapshot after a grace period and emits null; holding it here bridges the
Expand Down Expand Up @@ -2072,7 +2076,7 @@ export default function ChatView(props: ChatViewProps) {
[activeRightPanelSurface, rightPanelState.surfaces],
);
const rightPanelPresence = usePanelPresence(
rightPanelOpen && activeThreadRef !== null,
(rightPanelOpen || shouldUseRightPanelSheet) && activeThreadRef !== null,
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
rightPanelPresenceValue,
panelAnimationsActive,
activeThreadKey,
Expand Down Expand Up @@ -2492,6 +2496,7 @@ export default function ChatView(props: ChatViewProps) {
return;
}
setPullRequestDialogState({
open: true,
initialReference: reference ?? null,
key: Date.now(),
});
Expand All @@ -2500,7 +2505,7 @@ export default function ChatView(props: ChatViewProps) {
);

const closePullRequestDialog = useCallback(() => {
setPullRequestDialogState(null);
setPullRequestDialogState((current) => (current ? { ...current, open: false } : current));
}, []);

const openOrReuseProjectDraftThread = useCallback(
Expand Down Expand Up @@ -5814,11 +5819,11 @@ export default function ChatView(props: ChatViewProps) {
return [];
});
resetLocalDispatch();
setExpandedImage(null);
setExpandedImageDialog(null);
}, [draftId, resetLocalDispatch, threadId]);

const closeExpandedImage = useCallback(() => {
setExpandedImage(null);
setExpandedImageDialog((current) => (current ? { ...current, open: false } : current));
}, []);

const activeWorktreePath = activeThread?.worktreePath ?? null;
Expand Down Expand Up @@ -9411,7 +9416,11 @@ export default function ChatView(props: ChatViewProps) {
};

const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => {
setExpandedImage(preview);
setExpandedImageDialog((current) => ({
open: true,
preview,
generation: (current?.generation ?? 0) + 1,
}));
}, []);
const onOpenTurnDiff = useCallback(
(turnId: TurnId, filePath?: string) => {
Expand Down Expand Up @@ -10217,7 +10226,7 @@ export default function ChatView(props: ChatViewProps) {
{pullRequestDialogState ? (
<PullRequestThreadDialog
key={pullRequestDialogState.key}
open
open={pullRequestDialogState.open}
environmentId={activeThread.environmentId}
threadId={activeThread.id}
cwd={activeProject?.workspaceRoot ?? null}
Expand All @@ -10227,6 +10236,9 @@ export default function ChatView(props: ChatViewProps) {
closePullRequestDialog();
}
}}
onOpenChangeComplete={(open) => {
if (!open) setPullRequestDialogState(null);
}}
onPrepared={handlePreparedPullRequestThread}
/>
) : null}
Expand Down Expand Up @@ -10309,6 +10321,7 @@ export default function ChatView(props: ChatViewProps) {
>
<RightPanelTabs
mode="sheet"
launcherShortcutsEnabled={rightPanelOpen}
// Same effective inset as the closed-state titlebar controls
// (pr-3 in the tab bar plus this pixel equals the absolute
// right inset plus mr-px), so the cluster does not creep when
Expand Down Expand Up @@ -10404,13 +10417,19 @@ export default function ChatView(props: ChatViewProps) {
</AlertDialogPopup>
</AlertDialog>
<LinkPullRequestDialogHost />
{expandedImage && (
{expandedImageDialog ? (
<ExpandedImageDialog
key={expandedImageKey(expandedImage)}
preview={expandedImage}
onClose={closeExpandedImage}
key={expandedImageDialog.generation}
open={expandedImageDialog.open}
preview={expandedImageDialog.preview}
onOpenChange={(open) => {
if (!open) closeExpandedImage();
}}
onOpenChangeComplete={(open) => {
if (!open) setExpandedImageDialog(null);
}}
/>
)}
) : null}
</div>
);
}
45 changes: 31 additions & 14 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -489,13 +489,23 @@ export function CommandPalette({ children }: { children: ReactNode }) {
mode: "command",
openIntent: null,
});
const setOpen = useCallback((open: boolean) => dispatch({ _tag: "SetOpen", open }), []);
const toggleMode = useCallback(
(mode: SearchOverlayMode) => dispatch({ _tag: "ToggleMode", mode }),
[],
);
const openAddProject = useCallback(() => dispatch({ _tag: "OpenAddProject" }), []);
const openNewThreadIn = useCallback(() => dispatch({ _tag: "OpenNewThreadIn" }), []);
const [dialogContentMounted, setDialogContentMounted] = useState(false);
const setOpen = useCallback((open: boolean) => {
if (open) setDialogContentMounted(true);
dispatch({ _tag: "SetOpen", open });
}, []);
const toggleMode = useCallback((mode: SearchOverlayMode) => {
setDialogContentMounted(true);
dispatch({ _tag: "ToggleMode", mode });
}, []);
const openAddProject = useCallback(() => {
setDialogContentMounted(true);
dispatch({ _tag: "OpenAddProject" });
}, []);
const openNewThreadIn = useCallback(() => {
setDialogContentMounted(true);
dispatch({ _tag: "OpenNewThreadIn" });
}, []);
const clearOpenIntent = useCallback(() => dispatch({ _tag: "ClearOpenIntent" }), []);
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const { theme, themeHalves, resolvedTheme, appearanceMode, setAppearanceMode } = useTheme();
Expand Down Expand Up @@ -563,6 +573,7 @@ export function CommandPalette({ children }: { children: ReactNode }) {
event.preventDefault();
event.stopPropagation();
if (event.repeat) return;
setDialogContentMounted(true);
dispatch({ _tag: "OpenChangeTheme" });
return;
}
Expand Down Expand Up @@ -615,6 +626,7 @@ export function CommandPalette({ children }: { children: ReactNode }) {
} else if (detail.open === "add-project") {
openAddProject();
} else if (detail.query !== undefined) {
setDialogContentMounted(true);
dispatch({
_tag: "OpenSearch",
query: detail.query,
Expand All @@ -639,18 +651,23 @@ export function CommandPalette({ children }: { children: ReactNode }) {
}
setOpen(open);
}}
onOpenChangeComplete={(open) => {
if (!open) setDialogContentMounted(false);
}}
>
{/* Block background focus calls for the entire time the palette is open. */}
<div className="contents" inert={state.open}>
{children}
</div>
<CommandPaletteDialog
mode={state.mode}
openIntent={state.openIntent}
setOpen={setOpen}
openOverlayMode={toggleMode}
clearOpenIntent={clearOpenIntent}
/>
{dialogContentMounted ? (
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
<CommandPaletteDialog
mode={state.mode}
openIntent={state.openIntent}
setOpen={setOpen}
openOverlayMode={toggleMode}
clearOpenIntent={clearOpenIntent}
/>
) : null}
</CommandDialog>
</ComposerHandleContext>
);
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,8 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
const handleOpenChange = useCallback(
(open: boolean) => {
props.onOpenChange(open);
if (!open) {
resetState();
}
},
[props, resetState],
[props],
);

const openSourceControlSettings = useCallback(() => {
Expand All @@ -595,7 +592,13 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
}, [handleOpenChange, navigate]);

return (
<Dialog open={props.open} onOpenChange={handleOpenChange}>
<Dialog
open={props.open}
onOpenChange={handleOpenChange}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
onOpenChangeComplete={(open) => {
if (!open) resetState();
}}
>
<WizardPopup>
<WizardHeader
title="Publish repository"
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/PullRequestThreadDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface PullRequestThreadDialogProps {
cwd: string | null;
initialReference: string | null;
onOpenChange: (open: boolean) => void;
onOpenChangeComplete: (open: boolean) => void;
onPrepared: (input: { branch: string; worktreePath: string | null }) => Promise<void> | void;
}

Expand All @@ -43,6 +44,7 @@ export function PullRequestThreadDialog({
cwd,
initialReference,
onOpenChange,
onOpenChangeComplete,
onPrepared,
}: PullRequestThreadDialogProps) {
const referenceInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -193,6 +195,7 @@ export function PullRequestThreadDialog({
onOpenChange(nextOpen);
}
}}
onOpenChangeComplete={onOpenChangeComplete}
>
<DialogPopup className="max-w-xl">
<DialogHeader>
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ interface RightPanelTabsProps {
mode: PreviewPanelMode;
maximized?: boolean;
open?: boolean;
/** False while a kept-mounted panel is closed, so its launcher letters stay inert. */
launcherShortcutsEnabled?: boolean;
/** Forwarded to PreviewPanelShell so this surface persists its own width. */
widthStorageKey?: string;
/** Forwarded to PreviewPanelShell as the initial width before a user resize. */
Expand Down Expand Up @@ -335,6 +337,7 @@ function RightPanelEmptyState(props: {
agentsAvailable: boolean;
deviceAvailable: boolean;
liveAgentCount: number;
shortcutsEnabled: boolean;
}) {
// -1 means no highlight: it only appears on hover or arrow use.
const [highlight, setHighlight] = useState(-1);
Expand Down Expand Up @@ -426,11 +429,14 @@ function RightPanelEmptyState(props: {
// Capture phase so app-level key handlers cannot swallow the event first;
// typing contexts and already-handled events are left alone.
const shortcutActionsRef = useRef(availableActions);
const shortcutsEnabledRef = useRef(props.shortcutsEnabled);
useEffect(() => {
shortcutActionsRef.current = availableActions;
shortcutsEnabledRef.current = props.shortcutsEnabled;
});
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (!shortcutsEnabledRef.current) return;
const action = surfaceShortcutActionForKey(shortcutActionsRef.current, event);
if (!action) return;
if (document.querySelector(LAUNCHER_SHORTCUT_BLOCKING_LAYERS)) return;
Expand Down Expand Up @@ -1425,6 +1431,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
agentsAvailable={props.agentsAvailable}
deviceAvailable={props.deviceAvailable}
liveAgentCount={props.liveAgentCount}
shortcutsEnabled={props.launcherShortcutsEnabled ?? true}
/>
) : (
props.children
Expand Down
Loading
Loading