diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index a053dc732452c498e0251857f810235274513065..b386430568bef8073872ec9a13a2f4f78051c403 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -112,6 +112,7 @@ import { projectScriptIdFromCommand, setupProjectScript, } from "~/projectScripts"; +import { Toggle } from "./ui/toggle"; function formatMessageMeta(createdAt: string, duration: string | null): string { if (!duration) return formatTimestamp(createdAt); @@ -377,7 +378,8 @@ export default function ChatView() { const activeThread = state.threads.find((t) => t.id === state.activeThreadId); const activeThreadId = activeThread?.id ?? null; const activeSessionId = activeThread?.session?.sessionId; - const activeThreadRuntimeId = activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; + const activeThreadRuntimeId = + activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); const selectedModel = resolveModelSlug( activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, @@ -499,7 +501,8 @@ export default function ChatView() { } const turnSummariesNeedingDiff = turnDiffSummaries.filter( - (summary) => !summary.checkpointDiffLoaded && inferredCheckpointTurnCountByTurnId[summary.turnId], + (summary) => + !summary.checkpointDiffLoaded && inferredCheckpointTurnCountByTurnId[summary.turnId], ); const requestedSummaries = turnSummariesNeedingDiff.filter((summary) => { const requestKey = `${activeThreadId}:${summary.turnId}`; @@ -1463,7 +1466,15 @@ export default function ChatView() { setIsConnecting(false); } }, - [activeProject, activeThread, api, dispatch, runtimeApprovalPolicy, runtimeSandboxMode, selectedModel], + [ + activeProject, + activeThread, + api, + dispatch, + runtimeApprovalPolicy, + runtimeSandboxMode, + selectedModel, + ], ); useEffect(() => { @@ -1474,14 +1485,16 @@ export default function ChatView() { const selectedDiffTurnId = state.diffOpen && state.diffThreadId === activeThreadId ? state.diffTurnId : null; const selectedDiffTurn = - turnDiffSummaries.find((summary) => summary.turnId === selectedDiffTurnId) ?? turnDiffSummaries[0]; + turnDiffSummaries.find((summary) => summary.turnId === selectedDiffTurnId) ?? + turnDiffSummaries[0]; const selectedTurnMissingPatchBody = Boolean( selectedDiffTurn && - !selectedDiffTurn.unifiedDiff && - selectedDiffTurn.files.every((file) => !file.diff), + !selectedDiffTurn.unifiedDiff && + selectedDiffTurn.files.every((file) => !file.diff), ); const hasPendingDiffHydration = - turnDiffSummaries.some((summary) => !summary.checkpointDiffLoaded) || selectedTurnMissingPatchBody; + turnDiffSummaries.some((summary) => !summary.checkpointDiffLoaded) || + selectedTurnMissingPatchBody; if (!hasPendingDiffHydration) return; const requestKey = `${activeThreadId}:${activeThreadRuntimeId ?? "none"}`; @@ -2310,15 +2323,15 @@ const ChatHeader = memo(function ChatHeader({ )} {activeProjectName && } {activeProjectName && } - + + ); @@ -2620,7 +2633,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ } /> {(() => { - const turnSummary = turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id); + const turnSummary = turnDiffSummaryByAssistantMessageId.get( + timelineEntry.message.id, + ); if (!turnSummary) return null; const isCheckpointDiffLoading = !turnSummary.checkpointDiffLoaded && turnSummary.files.length === 0; @@ -2666,7 +2681,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ type="button" size="xs" variant="outline" - onClick={() => onOpenTurnDiff(turnSummary.turnId, turnSummary.files[0]?.path)} + onClick={() => + onOpenTurnDiff(turnSummary.turnId, turnSummary.files[0]?.path) + } > View diff diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 07014c6a3017657187ab43542b434180906c1e9a..60e23d724f1acae6beaa7e0e42ccbbf7c25caa06 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -14,6 +14,7 @@ import { useStore } from "../store"; import { Button } from "./ui/button"; import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; import { cn } from "~/lib/utils"; +import { ToggleGroup, Toggle } from "./ui/toggle-group"; type DiffRenderMode = "stacked" | "split"; @@ -406,68 +407,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { ))} -
- - setDiffRenderMode("stacked")} - /> - } - > - - - - Stacked - - - - setDiffRenderMode("split")} - /> - } - > - - - - Split - - - -
+ setDiffRenderMode(value[0] as DiffRenderMode)} + > + + + + + + + {!activeThread ? ( diff --git a/apps/web/src/components/ui/toggle-group.tsx b/apps/web/src/components/ui/toggle-group.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d633c0010fd6e48ad6ffeb3274bb017fab3d7323 --- /dev/null +++ b/apps/web/src/components/ui/toggle-group.tsx @@ -0,0 +1,97 @@ +"use client"; + +import type { Toggle as TogglePrimitive } from "@base-ui/react/toggle"; +import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"; +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "~/lib/utils"; +import { Separator } from "~/components/ui/separator"; +import { Toggle as ToggleComponent, type toggleVariants } from "~/components/ui/toggle"; + +const ToggleGroupContext = React.createContext>({ + size: "default", + variant: "default", +}); + +function ToggleGroup({ + className, + variant = "default", + size = "default", + orientation = "horizontal", + children, + ...props +}: ToggleGroupPrimitive.Props & VariantProps) { + return ( + + + {children} + + + ); +} + +function Toggle({ + className, + children, + variant, + size, + ...props +}: TogglePrimitive.Props & VariantProps) { + const context = React.useContext(ToggleGroupContext); + + const resolvedVariant = context.variant || variant; + const resolvedSize = context.size || size; + + return ( + + {children} + + ); +} + +function ToggleGroupSeparator({ + className, + orientation = "vertical", + ...props +}: { + className?: string; +} & React.ComponentProps) { + return ( + + ); +} + +export { ToggleGroup, Toggle, Toggle as ToggleGroupItem, ToggleGroupSeparator }; diff --git a/apps/web/src/components/ui/toggle.tsx b/apps/web/src/components/ui/toggle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5bbfdc7b54728929e2a7b6213858ba011377c576 --- /dev/null +++ b/apps/web/src/components/ui/toggle.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "~/lib/utils"; + +const toggleVariants = cva( + "[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base text-foreground outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-pressed:bg-input/64 data-pressed:text-accent-foreground sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + defaultVariants: { + size: "default", + variant: "default", + }, + variants: { + size: { + default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8", + lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9", + sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7", + xs: "h-7 min-w-7 px-[calc(--spacing(1)-1px)] sm:h-6 sm:min-w-6 rounded-md", + }, + variant: { + default: "border-transparent", + outline: + "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:data-pressed:bg-input dark:hover:bg-input/64 dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] dark:not-disabled:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/2%)] [:disabled,:active,[data-pressed]]:shadow-none", + }, + }, + }, +); + +function Toggle({ + className, + variant, + size, + ...props +}: TogglePrimitive.Props & VariantProps) { + return ( + + ); +} + +export { Toggle, toggleVariants };