| author | |
| committer | |
| log | ce98d4107758ed7a492f94a93b3685a828caeba1 |
| tree | af4b0ca5130b373cd40e1cd68497d87f84c60476 |
| parent | 1a81ea8c153214ddc74b7046062a8957c2612e8d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>4 files changed, 129 insertions(+), 111 deletions(-)
apps/web/src/components/DiffPanel.tsx+55-53| ... | ... | @@ -72,7 +72,8 @@ import { |
| 72 | 72 | import { |
| 73 | 73 | DropdownMenu, |
| 74 | 74 | DropdownMenuContent, |
| 75 | DropdownMenuItem, | |
| 75 | DropdownMenuRadioGroup, | |
| 76 | DropdownMenuRadioItem, | |
| 76 | 77 | DropdownMenuSub, |
| 77 | 78 | DropdownMenuSubContent, |
| 78 | 79 | DropdownMenuSubTrigger, |
| ... | ... | @@ -645,6 +646,26 @@ export default function DiffPanel({ |
| 645 | 646 | if (!routeThreadRef) return; |
| 646 | 647 | useDiffPanelStore.getState().selectBranchBaseRef(routeThreadRef, baseRef); |
| 647 | 648 | }; |
| 649 | // The scope menu has two radio groups: the top-level one treats the latest | |
| 650 | // turn as "latest", while the turn sub-menu keys every turn by id so the | |
| 651 | // latest turn is also marked there. | |
| 652 | const selectedTurnValue = selectedTurn ? `turn:${selectedTurn.turnId}` : ""; | |
| 653 | const selectedScopeValue = | |
| 654 | selectedTurnId === null | |
| 655 | ? selectedGitScope | |
| 656 | : selectedTurn?.turnId === latestTurn?.turnId | |
| 657 | ? "latest" | |
| 658 | : selectedTurnValue; | |
| 659 | const selectScopeValue = (value: string) => { | |
| 660 | if (value === "unstaged" || value === "branch") { | |
| 661 | selectGitScope(value); | |
| 662 | } else if (value === "latest") { | |
| 663 | if (latestTurn) selectTurn(latestTurn.turnId); | |
| 664 | } else { | |
| 665 | const turn = orderedTurnDiffSummaries.find((summary) => `turn:${summary.turnId}` === value); | |
| 666 | if (turn) selectTurn(turn.turnId); | |
| 667 | } | |
| 668 | }; | |
| 648 | 669 | |
| 649 | 670 | const headerRow = ( |
| 650 | 671 | <> |
| ... | ... | @@ -658,61 +679,42 @@ export default function DiffPanel({ |
| 658 | 679 | <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" /> |
| 659 | 680 | </DropdownMenuTrigger> |
| 660 | 681 | <DropdownMenuContent align="start" className="w-60"> |
| 661 | <DropdownMenuItem | |
| 662 | className={ | |
| 663 | selectedTurnId === null && selectedGitScope === "unstaged" | |
| 664 | ? "bg-foreground/[0.08]" | |
| 665 | : undefined | |
| 666 | } | |
| 667 | onClick={() => selectGitScope("unstaged")} | |
| 668 | > | |
| 669 | <span>Working tree</span> | |
| 670 | </DropdownMenuItem> | |
| 671 | <DropdownMenuItem | |
| 672 | className={ | |
| 673 | selectedTurnId === null && selectedGitScope === "branch" | |
| 674 | ? "bg-foreground/[0.08]" | |
| 675 | : undefined | |
| 676 | } | |
| 677 | onClick={() => selectGitScope("branch")} | |
| 678 | > | |
| 679 | <span>Branch changes</span> | |
| 680 | </DropdownMenuItem> | |
| 681 | <DropdownMenuItem | |
| 682 | className={ | |
| 683 | selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId | |
| 684 | ? "bg-foreground/[0.08]" | |
| 685 | : undefined | |
| 686 | } | |
| 687 | onClick={() => { | |
| 688 | if (latestTurn) selectTurn(latestTurn.turnId); | |
| 689 | }} | |
| 690 | > | |
| 691 | <span>Latest turn</span> | |
| 692 | </DropdownMenuItem> | |
| 682 | <DropdownMenuRadioGroup value={selectedScopeValue} onValueChange={selectScopeValue}> | |
| 683 | <DropdownMenuRadioItem value="unstaged" closeOnClick> | |
| 684 | <span>Working tree</span> | |
| 685 | </DropdownMenuRadioItem> | |
| 686 | <DropdownMenuRadioItem value="branch" closeOnClick> | |
| 687 | <span>Branch changes</span> | |
| 688 | </DropdownMenuRadioItem> | |
| 689 | <DropdownMenuRadioItem value="latest" closeOnClick> | |
| 690 | <span>Latest turn</span> | |
| 691 | </DropdownMenuRadioItem> | |
| 692 | </DropdownMenuRadioGroup> | |
| 693 | 693 | <DropdownMenuSub> |
| 694 | 694 | <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger> |
| 695 | 695 | <DropdownMenuSubContent className="w-64"> |
| 696 | {orderedTurnDiffSummaries.map((summary) => { | |
| 697 | const turnCount = | |
| 698 | summary.checkpointTurnCount ?? | |
| 699 | inferredCheckpointTurnCountByTurnId[summary.turnId] ?? | |
| 700 | "?"; | |
| 701 | return ( | |
| 702 | <DropdownMenuItem | |
| 703 | key={summary.turnId} | |
| 704 | className={ | |
| 705 | summary.turnId === selectedTurn?.turnId ? "bg-foreground/[0.08]" : undefined | |
| 706 | } | |
| 707 | onClick={() => selectTurn(summary.turnId)} | |
| 708 | > | |
| 709 | <span>Turn {turnCount}</span> | |
| 710 | <span className="ml-auto text-xs tabular-nums text-muted-foreground"> | |
| 711 | {formatShortTimestamp(summary.completedAt, settings.timestampFormat)} | |
| 712 | </span> | |
| 713 | </DropdownMenuItem> | |
| 714 | ); | |
| 715 | })} | |
| 696 | <DropdownMenuRadioGroup value={selectedTurnValue} onValueChange={selectScopeValue}> | |
| 697 | {orderedTurnDiffSummaries.map((summary) => { | |
| 698 | const turnCount = | |
| 699 | summary.checkpointTurnCount ?? | |
| 700 | inferredCheckpointTurnCountByTurnId[summary.turnId] ?? | |
| 701 | "?"; | |
| 702 | return ( | |
| 703 | <DropdownMenuRadioItem | |
| 704 | key={summary.turnId} | |
| 705 | value={`turn:${summary.turnId}`} | |
| 706 | closeOnClick | |
| 707 | > | |
| 708 | <span className="flex items-center gap-2"> | |
| 709 | <span>Turn {turnCount}</span> | |
| 710 | <span className="ml-auto text-xs tabular-nums text-muted-foreground"> | |
| 711 | {formatShortTimestamp(summary.completedAt, settings.timestampFormat)} | |
| 712 | </span> | |
| 713 | </span> | |
| 714 | </DropdownMenuRadioItem> | |
| 715 | ); | |
| 716 | })} | |
| 717 | </DropdownMenuRadioGroup> | |
| 716 | 718 | </DropdownMenuSubContent> |
| 717 | 719 | </DropdownMenuSub> |
| 718 | 720 | </DropdownMenuContent> |
apps/web/src/components/files/FileBreadcrumbs.tsx+47-33| ... | ... | @@ -10,6 +10,8 @@ import { |
| 10 | 10 | MenuGroup, |
| 11 | 11 | MenuItem, |
| 12 | 12 | MenuPopup, |
| 13 | MenuRadioGroup, | |
| 14 | MenuRadioItem, | |
| 13 | 15 | MenuSeparator, |
| 14 | 16 | MenuTrigger, |
| 15 | 17 | } from "~/components/ui/menu"; |
| ... | ... | @@ -141,39 +143,51 @@ function BreadcrumbMenuContent(props: { |
| 141 | 143 | : "This folder is empty."} |
| 142 | 144 | </MenuItem> |
| 143 | 145 | ) : ( |
| 144 | children.map((entry) => { | |
| 145 | const isCurrentFile = entry.kind === "file" && entry.path === props.currentFilePath; | |
| 146 | return ( | |
| 147 | <MenuItem | |
| 148 | key={entry.path} | |
| 149 | closeOnClick={entry.kind === "file"} | |
| 150 | aria-current={isCurrentFile ? "page" : undefined} | |
| 151 | className={cn( | |
| 152 | isCurrentFile && "bg-foreground/[0.08]", | |
| 153 | entry.ignored && "text-muted-foreground", | |
| 154 | )} | |
| 155 | onClick={() => { | |
| 156 | if (entry.kind === "directory") { | |
| 157 | props.onDirectoryChange(entry.path); | |
| 158 | return; | |
| 159 | } | |
| 160 | props.onOpenChange(false); | |
| 161 | props.onOpenFile(entry.path); | |
| 162 | }} | |
| 163 | > | |
| 164 | <PierreEntryIcon pathValue={entry.path} kind={entry.kind} theme={resolvedTheme} /> | |
| 165 | <Tooltip> | |
| 166 | <TooltipTrigger render={<span className="min-w-0 flex-1 truncate" />}> | |
| 167 | {entry.label} | |
| 168 | </TooltipTrigger> | |
| 169 | <TooltipPopup side="right" className="max-w-80"> | |
| 170 | {entry.path} | |
| 171 | </TooltipPopup> | |
| 172 | </Tooltip> | |
| 173 | {entry.kind === "directory" ? <ChevronRightIcon /> : null} | |
| 174 | </MenuItem> | |
| 175 | ); | |
| 176 | }) | |
| 146 | // Files form a radio group keyed by path so the open file is marked as checked; | |
| 147 | // directories only navigate the menu, so they stay plain items. | |
| 148 | <MenuRadioGroup | |
| 149 | value={props.currentFilePath} | |
| 150 | onValueChange={(path) => { | |
| 151 | props.onOpenChange(false); | |
| 152 | props.onOpenFile(path); | |
| 153 | }} | |
| 154 | > | |
| 155 | {children.map((entry) => { | |
| 156 | const isCurrentFile = entry.kind === "file" && entry.path === props.currentFilePath; | |
| 157 | const row = ( | |
| 158 | <> | |
| 159 | <PierreEntryIcon pathValue={entry.path} kind={entry.kind} theme={resolvedTheme} /> | |
| 160 | <Tooltip> | |
| 161 | <TooltipTrigger render={<span className="min-w-0 flex-1 truncate" />}> | |
| 162 | {entry.label} | |
| 163 | </TooltipTrigger> | |
| 164 | <TooltipPopup side="right">{entry.path}</TooltipPopup> | |
| 165 | </Tooltip> | |
| 166 | </> | |
| 167 | ); | |
| 168 | return entry.kind === "directory" ? ( | |
| 169 | <MenuItem | |
| 170 | key={entry.path} | |
| 171 | closeOnClick={false} | |
| 172 | className={entry.ignored ? "text-muted-foreground" : undefined} | |
| 173 | onClick={() => props.onDirectoryChange(entry.path)} | |
| 174 | > | |
| 175 | {row} | |
| 176 | <ChevronRightIcon /> | |
| 177 | </MenuItem> | |
| 178 | ) : ( | |
| 179 | <MenuRadioItem | |
| 180 | key={entry.path} | |
| 181 | value={entry.path} | |
| 182 | closeOnClick | |
| 183 | aria-current={isCurrentFile ? "page" : undefined} | |
| 184 | className={entry.ignored ? "text-muted-foreground" : undefined} | |
| 185 | > | |
| 186 | <span className="flex min-w-0 items-center gap-2">{row}</span> | |
| 187 | </MenuRadioItem> | |
| 188 | ); | |
| 189 | })} | |
| 190 | </MenuRadioGroup> | |
| 177 | 191 | )} |
| 178 | 192 | </MenuGroup> |
| 179 | 193 | {entriesQuery.error && entriesQuery.data !== null ? ( |
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+26-24| ... | ... | @@ -70,6 +70,8 @@ import { |
| 70 | 70 | DropdownMenu, |
| 71 | 71 | DropdownMenuContent, |
| 72 | 72 | DropdownMenuItem, |
| 73 | DropdownMenuRadioGroup, | |
| 74 | DropdownMenuRadioItem, | |
| 73 | 75 | DropdownMenuTrigger, |
| 74 | 76 | } from "../ui/menu"; |
| 75 | 77 | import { toastManager } from "../ui/toast"; |
| ... | ... | @@ -1076,31 +1078,31 @@ function PullRequestCodeTab({ |
| 1076 | 1078 | <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" /> |
| 1077 | 1079 | </DropdownMenuTrigger> |
| 1078 | 1080 | <DropdownMenuContent align="start" className="w-80"> |
| 1079 | <DropdownMenuItem | |
| 1080 | className={commit === null ? "bg-foreground/[0.08]" : undefined} | |
| 1081 | onClick={() => onSelectedCommitChange(null)} | |
| 1081 | <DropdownMenuRadioGroup | |
| 1082 | value={commit ?? "all"} | |
| 1083 | onValueChange={(value) => onSelectedCommitChange(value === "all" ? null : value)} | |
| 1082 | 1084 | > |
| 1083 | <span>All commits</span> | |
| 1084 | </DropdownMenuItem> | |
| 1085 | {orderedCommits.slice(0, visibleCommitCount).map((entry) => ( | |
| 1086 | <DropdownMenuItem | |
| 1087 | key={entry.oid} | |
| 1088 | className={entry.oid === commit ? "bg-foreground/[0.08]" : undefined} | |
| 1089 | onClick={() => onSelectedCommitChange(entry.oid)} | |
| 1090 | > | |
| 1091 | {/* Headlines run long, and the abbreviated oid after one is what a reader | |
| 1092 | matches against the commit list on the host. */} | |
| 1093 | <Tooltip> | |
| 1094 | <TooltipTrigger | |
| 1095 | render={<span className="min-w-0 truncate">{entry.messageHeadline}</span>} | |
| 1096 | /> | |
| 1097 | <TooltipPopup side="top">{entry.messageHeadline}</TooltipPopup> | |
| 1098 | </Tooltip> | |
| 1099 | <span className="ml-auto shrink-0 font-mono text-xs text-muted-foreground"> | |
| 1100 | {entry.oid.slice(0, 7)} | |
| 1101 | </span> | |
| 1102 | </DropdownMenuItem> | |
| 1103 | ))} | |
| 1085 | <DropdownMenuRadioItem value="all" closeOnClick> | |
| 1086 | <span>All commits</span> | |
| 1087 | </DropdownMenuRadioItem> | |
| 1088 | {orderedCommits.slice(0, visibleCommitCount).map((entry) => ( | |
| 1089 | <DropdownMenuRadioItem key={entry.oid} value={entry.oid} closeOnClick> | |
| 1090 | {/* Headlines run long, and the abbreviated oid after one is what a reader | |
| 1091 | matches against the commit list on the host. */} | |
| 1092 | <span className="flex items-center gap-2"> | |
| 1093 | <Tooltip> | |
| 1094 | <TooltipTrigger | |
| 1095 | render={<span className="min-w-0 truncate">{entry.messageHeadline}</span>} | |
| 1096 | /> | |
| 1097 | <TooltipPopup side="top">{entry.messageHeadline}</TooltipPopup> | |
| 1098 | </Tooltip> | |
| 1099 | <span className="ml-auto shrink-0 font-mono text-xs text-muted-foreground"> | |
| 1100 | {entry.oid.slice(0, 7)} | |
| 1101 | </span> | |
| 1102 | </span> | |
| 1103 | </DropdownMenuRadioItem> | |
| 1104 | ))} | |
| 1105 | </DropdownMenuRadioGroup> | |
| 1104 | 1106 | {orderedCommits.length > visibleCommitCount ? ( |
| 1105 | 1107 | // Kept out of the radio group: it changes how much of the list is on screen |
| 1106 | 1108 | // rather than what the diff is scoped to. |
scripts/lint-restyle-ceiling.ts+1-1| ... | ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; |
| 8 | 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the |
| 9 | 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you |
| 10 | 10 | // migrate a file, and delete this script when the rule becomes an error. |
| 11 | export const RESTYLE_CEILING = 1165; | |
| 11 | export const RESTYLE_CEILING = 1159; | |
| 12 | 12 | |
| 13 | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |