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 };