- add reusable `ui/toggle` and `ui/toggle-group` components built on Base UI - switch diff view mode controls and chat header diff toggle to use the new toggle primitives
Unified · Split
| @@ -112,6 +112,7 @@ import { · 111 unmodified lines | ||
| 112 | 112 | projectScriptIdFromCommand, |
| 113 | 113 | setupProjectScript, |
| 114 | 114 | } from "~/projectScripts"; |
| 115 | +import { Toggle } from "./ui/toggle"; | |
| 115 | 116 | |
| 116 | 117 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 117 | 118 | if (!duration) return formatTimestamp(createdAt); |
| @@ -377,7 +378,8 @@ export default function ChatView() { · 259 unmodified lines | ||
| 377 | 378 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 378 | 379 | const activeThreadId = activeThread?.id ?? null; |
| 379 | 380 | const activeSessionId = activeThread?.session?.sessionId; |
| 380 | − const activeThreadRuntimeId = activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; | |
| 381 | + const activeThreadRuntimeId = | |
| 382 | + activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; | |
| 381 | 383 | const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); |
| 382 | 384 | const selectedModel = resolveModelSlug( |
| 383 | 385 | activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, |
| @@ -499,7 +501,8 @@ export default function ChatView() { · 115 unmodified lines | ||
| 499 | 501 | } |
| 500 | 502 | |
| 501 | 503 | const turnSummariesNeedingDiff = turnDiffSummaries.filter( |
| 502 | − (summary) => !summary.checkpointDiffLoaded && inferredCheckpointTurnCountByTurnId[summary.turnId], | |
| 504 | + (summary) => | |
| 505 | + !summary.checkpointDiffLoaded && inferredCheckpointTurnCountByTurnId[summary.turnId], | |
| 503 | 506 | ); |
| 504 | 507 | const requestedSummaries = turnSummariesNeedingDiff.filter((summary) => { |
| 505 | 508 | const requestKey = `${activeThreadId}:${summary.turnId}`; |
| @@ -1463,7 +1466,15 @@ export default function ChatView() { · 957 unmodified lines | ||
| 1463 | 1466 | setIsConnecting(false); |
| 1464 | 1467 | } |
| 1465 | 1468 | }, |
| 1466 | − [activeProject, activeThread, api, dispatch, runtimeApprovalPolicy, runtimeSandboxMode, selectedModel], | |
| 1469 | + [ | |
| 1470 | + activeProject, | |
| 1471 | + activeThread, | |
| 1472 | + api, | |
| 1473 | + dispatch, | |
| 1474 | + runtimeApprovalPolicy, | |
| 1475 | + runtimeSandboxMode, | |
| 1476 | + selectedModel, | |
| 1477 | + ], | |
| 1467 | 1478 | ); |
| 1468 | 1479 | |
| 1469 | 1480 | useEffect(() => { |
| @@ -1474,14 +1485,16 @@ export default function ChatView() { · 4 unmodified lines | ||
| 1474 | 1485 | const selectedDiffTurnId = |
| 1475 | 1486 | state.diffOpen && state.diffThreadId === activeThreadId ? state.diffTurnId : null; |
| 1476 | 1487 | const selectedDiffTurn = |
| 1477 | − turnDiffSummaries.find((summary) => summary.turnId === selectedDiffTurnId) ?? turnDiffSummaries[0]; | |
| 1488 | + turnDiffSummaries.find((summary) => summary.turnId === selectedDiffTurnId) ?? | |
| 1489 | + turnDiffSummaries[0]; | |
| 1478 | 1490 | const selectedTurnMissingPatchBody = Boolean( |
| 1479 | 1491 | selectedDiffTurn && |
| 1480 | − !selectedDiffTurn.unifiedDiff && | |
| 1481 | − selectedDiffTurn.files.every((file) => !file.diff), | |
| 1492 | + !selectedDiffTurn.unifiedDiff && | |
| 1493 | + selectedDiffTurn.files.every((file) => !file.diff), | |
| 1482 | 1494 | ); |
| 1483 | 1495 | const hasPendingDiffHydration = |
| 1484 | − turnDiffSummaries.some((summary) => !summary.checkpointDiffLoaded) || selectedTurnMissingPatchBody; | |
| 1496 | + turnDiffSummaries.some((summary) => !summary.checkpointDiffLoaded) || | |
| 1497 | + selectedTurnMissingPatchBody; | |
| 1485 | 1498 | if (!hasPendingDiffHydration) return; |
| 1486 | 1499 | |
| 1487 | 1500 | const requestKey = `${activeThreadId}:${activeThreadRuntimeId ?? "none"}`; |
| @@ -2310,15 +2323,15 @@ const ChatHeader = memo(function ChatHeader({ · 822 unmodified lines | ||
| 2310 | 2323 | )} |
| 2311 | 2324 | {activeProjectName && <OpenInPicker keybindings={keybindings} />} |
| 2312 | 2325 | {activeProjectName && <GitActionsControl api={api} gitCwd={gitCwd} />} |
| 2313 | − <Button | |
| 2326 | + <Toggle | |
| 2327 | + pressed={diffOpen} | |
| 2328 | + onPressedChange={onToggleDiff} | |
| 2314 | 2329 | aria-label="Toggle diff panel" |
| 2315 | − size="icon-xs" | |
| 2316 | 2330 | variant="outline" |
| 2317 | − className={cn(diffOpen && "bg-accent text-accent-foreground")} | |
| 2318 | − onClick={onToggleDiff} | |
| 2331 | + size="xs" | |
| 2319 | 2332 | > |
| 2320 | − <DiffIcon /> | |
| 2321 | − </Button> | |
| 2333 | + <DiffIcon className="size-3" /> | |
| 2334 | + </Toggle> | |
| 2322 | 2335 | </div> |
| 2323 | 2336 | </> |
| 2324 | 2337 | ); |
| @@ -2620,7 +2633,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ · 295 unmodified lines | ||
| 2620 | 2633 | } |
| 2621 | 2634 | /> |
| 2622 | 2635 | {(() => { |
| 2623 | − const turnSummary = turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id); | |
| 2636 | + const turnSummary = turnDiffSummaryByAssistantMessageId.get( | |
| 2637 | + timelineEntry.message.id, | |
| 2638 | + ); | |
| 2624 | 2639 | if (!turnSummary) return null; |
| 2625 | 2640 | const isCheckpointDiffLoading = |
| 2626 | 2641 | !turnSummary.checkpointDiffLoaded && turnSummary.files.length === 0; |
| @@ -2666,7 +2681,9 @@ const MessagesTimeline = memo(function MessagesTimeline({ · 39 unmodified lines | ||
| 2666 | 2681 | type="button" |
| 2667 | 2682 | size="xs" |
| 2668 | 2683 | variant="outline" |
| 2669 | − onClick={() => onOpenTurnDiff(turnSummary.turnId, turnSummary.files[0]?.path)} | |
| 2684 | + onClick={() => | |
| 2685 | + onOpenTurnDiff(turnSummary.turnId, turnSummary.files[0]?.path) | |
| 2686 | + } | |
| 2670 | 2687 | > |
| 2671 | 2688 | View diff |
| 2672 | 2689 | </Button> |
| @@ -14,6 +14,7 @@ import { useStore } from "../store"; · 13 unmodified lines | ||
| 14 | 14 | import { Button } from "./ui/button"; |
| 15 | 15 | import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover"; |
| 16 | 16 | import { cn } from "~/lib/utils"; |
| 17 | +import { ToggleGroup, Toggle } from "./ui/toggle-group"; | |
| 17 | 18 | |
| 18 | 19 | type DiffRenderMode = "stacked" | "split"; |
| 19 | 20 | |
| @@ -406,68 +407,19 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 386 unmodified lines | ||
| 406 | 407 | </button> |
| 407 | 408 | ))} |
| 408 | 409 | </div> |
| 409 | − <div className="flex items-center gap-1"> | |
| 410 | − <Popover> | |
| 411 | − <PopoverTrigger | |
| 412 | − openOnHover | |
| 413 | − render={ | |
| 414 | − <Button | |
| 415 | − aria-label="Stacked diff view" | |
| 416 | − aria-pressed={diffRenderMode === "stacked"} | |
| 417 | − size="icon-xs" | |
| 418 | − variant="outline" | |
| 419 | − className={cn(diffRenderMode === "stacked" && "bg-accent text-accent-foreground")} | |
| 420 | − onClick={() => setDiffRenderMode("stacked")} | |
| 421 | − /> | |
| 422 | − } | |
| 423 | − > | |
| 424 | − <Rows3Icon /> | |
| 425 | − </PopoverTrigger> | |
| 426 | − <PopoverPopup | |
| 427 | − tooltipStyle | |
| 428 | − side="bottom" | |
| 429 | − sideOffset={6} | |
| 430 | − align="center" | |
| 431 | − className="pointer-events-none select-none" | |
| 432 | − > | |
| 433 | − Stacked | |
| 434 | − </PopoverPopup> | |
| 435 | − </Popover> | |
| 436 | − <Popover> | |
| 437 | − <PopoverTrigger | |
| 438 | − openOnHover | |
| 439 | − render={ | |
| 440 | − <Button | |
| 441 | − aria-label="Split diff view" | |
| 442 | − aria-pressed={diffRenderMode === "split"} | |
| 443 | − size="icon-xs" | |
| 444 | − variant="outline" | |
| 445 | − className={cn(diffRenderMode === "split" && "bg-accent text-accent-foreground")} | |
| 446 | − onClick={() => setDiffRenderMode("split")} | |
| 447 | − /> | |
| 448 | − } | |
| 449 | − > | |
| 450 | − <Columns2Icon /> | |
| 451 | − </PopoverTrigger> | |
| 452 | − <PopoverPopup | |
| 453 | − tooltipStyle | |
| 454 | − side="bottom" | |
| 455 | − sideOffset={6} | |
| 456 | − align="center" | |
| 457 | − className="pointer-events-none select-none" | |
| 458 | − > | |
| 459 | − Split | |
| 460 | − </PopoverPopup> | |
| 461 | − </Popover> | |
| 462 | − <Button | |
| 463 | − type="button" | |
| 464 | − size="icon-xs" | |
| 465 | − variant="ghost" | |
| 466 | − onClick={() => dispatch({ type: "CLOSE_DIFF" })} | |
| 467 | − > | |
| 468 | − <XIcon /> | |
| 469 | − </Button> | |
| 470 | − </div> | |
| 410 | + <ToggleGroup | |
| 411 | + variant="outline" | |
| 412 | + size="xs" | |
| 413 | + value={[diffRenderMode]} | |
| 414 | + onValueChange={(value) => setDiffRenderMode(value[0] as DiffRenderMode)} | |
| 415 | + > | |
| 416 | + <Toggle aria-label="Stacked diff view" value="stacked"> | |
| 417 | + <Rows3Icon className="size-3" /> | |
| 418 | + </Toggle> | |
| 419 | + <Toggle aria-label="Split diff view" value="split"> | |
| 420 | + <Columns2Icon className="size-3" /> | |
| 421 | + </Toggle> | |
| 422 | + </ToggleGroup> | |
| 471 | 423 | </div> |
| 472 | 424 | |
| 473 | 425 | {!activeThread ? ( |
| @@ -0,0 +1,97 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import type { Toggle as TogglePrimitive } from "@base-ui/react/toggle"; | |
| 4 | +import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"; | |
| 5 | +import type { VariantProps } from "class-variance-authority"; | |
| 6 | +import * as React from "react"; | |
| 7 | + | |
| 8 | +import { cn } from "~/lib/utils"; | |
| 9 | +import { Separator } from "~/components/ui/separator"; | |
| 10 | +import { Toggle as ToggleComponent, type toggleVariants } from "~/components/ui/toggle"; | |
| 11 | + | |
| 12 | +const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({ | |
| 13 | + size: "default", | |
| 14 | + variant: "default", | |
| 15 | +}); | |
| 16 | + | |
| 17 | +function ToggleGroup({ | |
| 18 | + className, | |
| 19 | + variant = "default", | |
| 20 | + size = "default", | |
| 21 | + orientation = "horizontal", | |
| 22 | + children, | |
| 23 | + ...props | |
| 24 | +}: ToggleGroupPrimitive.Props & VariantProps<typeof toggleVariants>) { | |
| 25 | + return ( | |
| 26 | + <ToggleGroupPrimitive | |
| 27 | + className={cn( | |
| 28 | + "flex w-fit *:focus-visible:z-10 dark:*:[[data-slot=separator]:has(+[data-slot=toggle]:hover)]:before:bg-input/64 dark:*:[[data-slot=separator]:has(+[data-slot=toggle][data-pressed])]:before:bg-input dark:*:[[data-slot=toggle]:hover+[data-slot=separator]]:before:bg-input/64 dark:*:[[data-slot=toggle][data-pressed]+[data-slot=separator]]:before:bg-input", | |
| 29 | + orientation === "horizontal" | |
| 30 | + ? "*:pointer-coarse:after:min-w-auto" | |
| 31 | + : "*:pointer-coarse:after:min-h-auto", | |
| 32 | + variant === "default" | |
| 33 | + ? "gap-0.5" | |
| 34 | + : orientation === "horizontal" | |
| 35 | + ? "*:not-first:not-data-[slot=separator]:before:-start-[0.5px] *:not-last:not-data-[slot=separator]:before:-end-[0.5px] *:not-first:rounded-s-none *:not-last:rounded-e-none *:not-first:border-s-0 *:not-last:border-e-0 *:not-first:before:rounded-s-none *:not-last:before:rounded-e-none" | |
| 36 | + : "*:not-first:not-data-[slot=separator]:before:-top-[0.5px] *:not-last:not-data-[slot=separator]:before:-bottom-[0.5px] flex-col *:not-first:rounded-t-none *:not-last:rounded-b-none *:not-first:border-t-0 *:not-last:border-b-0 *:not-first:before:rounded-t-none *:not-last:before:rounded-b-none *:data-[slot=toggle]:not-last:before:hidden dark:*:last:before:hidden dark:*:first:before:block", | |
| 37 | + className, | |
| 38 | + )} | |
| 39 | + data-size={size} | |
| 40 | + data-slot="toggle-group" | |
| 41 | + data-variant={variant} | |
| 42 | + orientation={orientation} | |
| 43 | + {...props} | |
| 44 | + > | |
| 45 | + <ToggleGroupContext.Provider value={{ size, variant }}> | |
| 46 | + {children} | |
| 47 | + </ToggleGroupContext.Provider> | |
| 48 | + </ToggleGroupPrimitive> | |
| 49 | + ); | |
| 50 | +} | |
| 51 | + | |
| 52 | +function Toggle({ | |
| 53 | + className, | |
| 54 | + children, | |
| 55 | + variant, | |
| 56 | + size, | |
| 57 | + ...props | |
| 58 | +}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) { | |
| 59 | + const context = React.useContext(ToggleGroupContext); | |
| 60 | + | |
| 61 | + const resolvedVariant = context.variant || variant; | |
| 62 | + const resolvedSize = context.size || size; | |
| 63 | + | |
| 64 | + return ( | |
| 65 | + <ToggleComponent | |
| 66 | + className={className} | |
| 67 | + data-size={resolvedSize} | |
| 68 | + data-variant={resolvedVariant} | |
| 69 | + size={resolvedSize} | |
| 70 | + variant={resolvedVariant} | |
| 71 | + {...props} | |
| 72 | + > | |
| 73 | + {children} | |
| 74 | + </ToggleComponent> | |
| 75 | + ); | |
| 76 | +} | |
| 77 | + | |
| 78 | +function ToggleGroupSeparator({ | |
| 79 | + className, | |
| 80 | + orientation = "vertical", | |
| 81 | + ...props | |
| 82 | +}: { | |
| 83 | + className?: string; | |
| 84 | +} & React.ComponentProps<typeof Separator>) { | |
| 85 | + return ( | |
| 86 | + <Separator | |
| 87 | + className={cn( | |
| 88 | + "pointer-events-none relative bg-input before:absolute before:inset-0 dark:before:bg-input/32", | |
| 89 | + className, | |
| 90 | + )} | |
| 91 | + orientation={orientation} | |
| 92 | + {...props} | |
| 93 | + /> | |
| 94 | + ); | |
| 95 | +} | |
| 96 | + | |
| 97 | +export { ToggleGroup, Toggle, Toggle as ToggleGroupItem, ToggleGroupSeparator }; | |
| @@ -0,0 +1,46 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"; | |
| 4 | +import { cva, type VariantProps } from "class-variance-authority"; | |
| 5 | + | |
| 6 | +import { cn } from "~/lib/utils"; | |
| 7 | + | |
| 8 | +const toggleVariants = cva( | |
| 9 | + "[&_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", | |
| 10 | + { | |
| 11 | + defaultVariants: { | |
| 12 | + size: "default", | |
| 13 | + variant: "default", | |
| 14 | + }, | |
| 15 | + variants: { | |
| 16 | + size: { | |
| 17 | + default: "h-9 min-w-9 px-[calc(--spacing(2)-1px)] sm:h-8 sm:min-w-8", | |
| 18 | + lg: "h-10 min-w-10 px-[calc(--spacing(2.5)-1px)] sm:h-9 sm:min-w-9", | |
| 19 | + sm: "h-8 min-w-8 px-[calc(--spacing(1.5)-1px)] sm:h-7 sm:min-w-7", | |
| 20 | + xs: "h-7 min-w-7 px-[calc(--spacing(1)-1px)] sm:h-6 sm:min-w-6 rounded-md", | |
| 21 | + }, | |
| 22 | + variant: { | |
| 23 | + default: "border-transparent", | |
| 24 | + outline: | |
| 25 | + "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", | |
| 26 | + }, | |
| 27 | + }, | |
| 28 | + }, | |
| 29 | +); | |
| 30 | + | |
| 31 | +function Toggle({ | |
| 32 | + className, | |
| 33 | + variant, | |
| 34 | + size, | |
| 35 | + ...props | |
| 36 | +}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) { | |
| 37 | + return ( | |
| 38 | + <TogglePrimitive | |
| 39 | + className={cn(toggleVariants({ className, size, variant }))} | |
| 40 | + data-slot="toggle" | |
| 41 | + {...props} | |
| 42 | + /> | |
| 43 | + ); | |
| 44 | +} | |
| 45 | + | |
| 46 | +export { Toggle, toggleVariants }; | |