Skip to content
Draft
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
109 changes: 97 additions & 12 deletions apps/code/src/renderer/components/permissions/PlanContent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import {
PlanListItemBlock,
PlanWrappableBlock,
type WrappableTag,
} from "@features/sessions/components/plan-annotations/PlanBlock";
import { PlanReviewSidebar } from "@features/sessions/components/plan-annotations/PlanReviewSidebar";
import { useTaskStore } from "@features/tasks/stores/taskStore";
import { ArrowsIn, ArrowsOut, ListChecks, X } from "@phosphor-icons/react";
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
import { useEffect, useRef, useState } from "react";
import { usePlanFullscreenStore } from "@stores/planFullscreenStore";
import type { Element } from "hast";
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import ReactMarkdown from "react-markdown";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";

const planScrollPosition = new Map<string, number>();
Expand All @@ -13,28 +22,40 @@ interface PlanContentProps {
}

export function PlanContent({ id, plan }: PlanContentProps) {
const toolCallId = id;
const taskId = useTaskStore((s) => s.selectedTaskId);
const scrollRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const setActiveFullscreen = usePlanFullscreenStore((s) => s.setActive);
const clearActiveFullscreen = usePlanFullscreenStore((s) => s.clear);

useEffect(() => {
const el = scrollRef.current;
if (!el) return;

const position = planScrollPosition.get(id);
const position = planScrollPosition.get(toolCallId);
if (position !== undefined) {
el.scrollTop = position;
}

const handleScroll = () => {
planScrollPosition.set(id, el.scrollTop);
planScrollPosition.set(toolCallId, el.scrollTop);
};

el.addEventListener("scroll", handleScroll, { passive: true });

return () => {
el.removeEventListener("scroll", handleScroll);
};
}, [id]);
}, [toolCallId]);

useEffect(() => {
if (!isFullscreen) return;
setActiveFullscreen(toolCallId);
return () => {
clearActiveFullscreen(toolCallId);
};
}, [isFullscreen, toolCallId, setActiveFullscreen, clearActiveFullscreen]);

useEffect(() => {
if (!isFullscreen) return;
Expand All @@ -47,8 +68,63 @@ export function PlanContent({ id, plan }: PlanContentProps) {
return () => window.removeEventListener("keydown", handler);
}, [isFullscreen]);

const annotationsEnabled = isFullscreen && !!taskId;

const components = useMemo<Components | undefined>(() => {
if (!annotationsEnabled || !taskId) return undefined;

const wrappable = (tag: WrappableTag) =>
function WrappableRenderer(props: {
node?: Element;
children?: ReactNode;
className?: string;
}) {
return (
<PlanWrappableBlock
tag={tag}
taskId={taskId}
toolCallId={toolCallId}
node={props.node}
className={props.className}
>
{props.children}
</PlanWrappableBlock>
);
};

return {
p: wrappable("p"),
blockquote: wrappable("blockquote"),
pre: wrappable("pre"),
h1: wrappable("h1"),
h2: wrappable("h2"),
h3: wrappable("h3"),
h4: wrappable("h4"),
h5: wrappable("h5"),
h6: wrappable("h6"),
li: function LiRenderer(props: {
node?: Element;
children?: ReactNode;
className?: string;
}) {
return (
<PlanListItemBlock
taskId={taskId}
toolCallId={toolCallId}
node={props.node}
className={props.className}
>
{props.children}
</PlanListItemBlock>
);
},
} as Components;
}, [annotationsEnabled, taskId, toolCallId]);

const markdown = (
<ReactMarkdown remarkPlugins={[remarkGfm]}>{plan}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
{plan}
</ReactMarkdown>
);

if (isFullscreen) {
Expand Down Expand Up @@ -90,12 +166,21 @@ export function PlanContent({ id, plan }: PlanContentProps) {
</IconButton>
</Flex>

<Box
ref={scrollRef}
className="plan-markdown flex-1 overflow-y-auto p-6"
>
{markdown}
</Box>
<Flex className="min-h-0 flex-1">
<Box
ref={scrollRef}
className="plan-markdown min-w-0 flex-1 overflow-y-auto px-12 py-6"
>
{markdown}
</Box>
{taskId && (
<PlanReviewSidebar
taskId={taskId}
toolCallId={toolCallId}
onSubmitted={() => setIsFullscreen(false)}
/>
)}
</Flex>
</Box>,
portalTarget,
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface CommentAnnotationProps {
onDismiss: () => void;
initialText?: string;
editingDraftId?: string;
forceBatch?: boolean;
placeholder?: string;
}

export function CommentAnnotation({
Expand All @@ -34,6 +36,8 @@ export function CommentAnnotation({
onDismiss,
initialText,
editingDraftId,
forceBatch,
placeholder,
}: CommentAnnotationProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const addDraft = useReviewDraftsStore((s) => s.addDraft);
Expand All @@ -44,7 +48,7 @@ export function CommentAnnotation({
);

const [batch, setBatch] = useState(
editingDraftId ? true : initialBatchEnabled,
forceBatch ? true : editingDraftId ? true : initialBatchEnabled,
);
const [isEmpty, setIsEmpty] = useState(!initialText?.trim());

Expand All @@ -64,9 +68,9 @@ export function CommentAnnotation({
);

useEffect(() => {
if (editingDraftId) return;
if (editingDraftId || forceBatch) return;
setBatch(initialBatchEnabled);
}, [initialBatchEnabled, editingDraftId]);
}, [initialBatchEnabled, editingDraftId, forceBatch]);

const handleSubmit = useCallback(() => {
const text = textareaRef.current?.value?.trim();
Expand Down Expand Up @@ -131,7 +135,7 @@ export function CommentAnnotation({
<InputGroup>
<InputGroupTextarea
ref={setTextareaRef}
placeholder="Describe the changes you'd like..."
placeholder={placeholder ?? "Describe the changes you'd like..."}
onKeyDown={handleKeyDown}
onChange={(e) => setIsEmpty(!e.currentTarget.value.trim())}
className="min-h-[48px] resize-none text-[13px]"
Expand All @@ -148,7 +152,7 @@ export function CommentAnnotation({
</InputGroupButton>
</Tooltip>
<div className="ml-auto flex items-center gap-3">
{!editingDraftId && (
{!editingDraftId && !forceBatch && (
<Text as="label" size="1" color="gray">
<span className="flex cursor-pointer items-center gap-2">
<Checkbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
isJsonRpcNotification,
isJsonRpcResponse,
} from "@shared/types/session-events";
import { usePlanFullscreenStore } from "@stores/planFullscreenStore";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { getSessionService } from "../service/service";
import { flattenSelectOptions } from "../stores/sessionStore";
Expand Down Expand Up @@ -258,6 +259,13 @@ export function SessionView({
return { ...permission, toolCallId };
}, [pendingPermissions]);

const planFullscreenToolCallId = usePlanFullscreenStore(
(s) => s.activeFullscreenToolCallId,
);
const isPlanOverlayActiveForPermission =
!!firstPendingPermission &&
firstPendingPermission.toolCallId === planFullscreenToolCallId;

const handlePermissionSelect = useCallback(
async (
optionId: string,
Expand Down Expand Up @@ -566,7 +574,8 @@ export function SessionView({
)}
</Flex>
</Flex>
) : hideInput ? null : firstPendingPermission ? (
) : hideInput ||
isPlanOverlayActiveForPermission ? null : firstPendingPermission ? (
<Box className="max-h-1/2 min-h-0 overflow-y-auto border-gray-4 border-t">
<Box
className={compact ? "p-1" : "mx-auto p-2"}
Expand Down
Loading
Loading