Skip to content
Merged
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
92 changes: 50 additions & 42 deletions src/features/chat/ui/widgets/ChangesWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ExternalLink } from "lucide-react";
import { cn } from "@/shared/lib/cn";
Expand All @@ -7,11 +7,6 @@ import { Skeleton } from "@/shared/ui/skeleton";
import type { ChangedFile } from "@/shared/types/git";
import type { WorkspaceChangedFilesRuntime } from "../hooks/useWorkspaceGitRuntimes";

const CHANGES_SCROLL_CONTAINER_CLASS =
"scrollbar-none max-h-[300px] overflow-y-auto rounded-sm bg-muted/60 py-1";
const CHANGES_SCROLL_FADE_CLASS =
"pointer-events-none absolute inset-x-0 bottom-0 h-10 rounded-b-sm bg-gradient-to-t from-muted/90 to-transparent";

/** Placeholder rows shared by the widgets and the tab-level loading branch.
* Padding is the caller's job: the widgets already sit in a padded
* `<section>`, while the tab renders these directly. */
Expand Down Expand Up @@ -104,11 +99,10 @@ function ChangedFileRow({
type="button"
disabled={isDeleted}
className={cn(
"group relative flex min-h-12 w-full select-none items-center gap-2 py-3 pl-3 pr-2 text-left",
"transition-colors duration-100 before:absolute before:inset-x-1 before:inset-y-0 before:rounded-[8px] before:transition-colors after:absolute after:bottom-0 after:left-3 after:right-3 after:h-px after:bg-border/60 last:after:hidden [&:has(+_:focus-visible)]:after:bg-transparent [&:has(+_:hover)]:after:bg-transparent [&>*]:relative [&>*]:z-[1]",
"group flex min-h-9 w-full select-none items-center gap-2 rounded-sm px-2.5 py-2 text-left transition-colors duration-100",
isDeleted
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:before:bg-background/45 hover:after:bg-transparent focus-visible:before:bg-background/45 focus-visible:after:bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
: "cursor-pointer hover:bg-muted focus-visible:bg-muted focus-visible:outline-none",
)}
onClick={isDeleted ? undefined : () => onOpen(file.path)}
>
Expand All @@ -134,6 +128,41 @@ function ChangedFileRow({
return <FileContextMenu path={fullPath}>{row}</FileContextMenu>;
}

function ChangedFilesList({
files,
getFullPath,
onOpen,
}: {
files: ChangedFile[];
getFullPath: (file: ChangedFile) => string;
onOpen: (path: string) => void;
}) {
const [scrolled, setScrolled] = useState(false);

return (
<div
// Scrolled rows fade under a top mask like the picker dropdown lists
// (`chat-context-dropdown-results-scroll`); the -webkit- duplicate is
// for WKWebView.
className={cn(
"scrollbar-none max-h-[300px] space-y-0.5 overflow-y-auto",
scrolled &&
"[-webkit-mask-image:linear-gradient(to_bottom,transparent,black_1.25rem)] [mask-image:linear-gradient(to_bottom,transparent,black_1.25rem)]",
)}
onScroll={(event) => setScrolled(event.currentTarget.scrollTop > 0)}
>
{files.map((file) => (
<ChangedFileRow
key={file.path}
file={file}
fullPath={getFullPath(file)}
onOpen={onOpen}
/>
))}
</div>
);
}

interface ChangesWidgetProps {
files: ChangedFile[] | undefined;
isLoading: boolean;
Expand Down Expand Up @@ -215,19 +244,11 @@ export function ChangesWidget({
)}
</div>
{files?.length ? (
<div className="relative">
<div className={CHANGES_SCROLL_CONTAINER_CLASS}>
{files.map((file) => (
<ChangedFileRow
key={file.path}
file={file}
fullPath={`${repoPath}/${file.path}`}
onOpen={onOpenFile}
/>
))}
</div>
<div className={CHANGES_SCROLL_FADE_CLASS} aria-hidden="true" />
</div>
<ChangedFilesList
files={files}
getFullPath={(file) => `${repoPath}/${file.path}`}
onOpen={onOpenFile}
/>
) : null}
</div>
) : (
Expand Down Expand Up @@ -291,12 +312,12 @@ export function WorkspaceChangesWidget({
) : changedGroups.length > 0 ? (
<div className="space-y-4">
{probeErrorMessage ? (
<p className="px-2 text-sm text-destructive">{probeErrorMessage}</p>
<p className="text-sm text-destructive">{probeErrorMessage}</p>
) : null}
{changedGroups.map((group) => (
<div key={group.id} className="space-y-2">
{hasMultipleGroups ? (
<p className="truncate px-2 text-xs text-muted-foreground">
<p className="truncate text-xs text-muted-foreground">
{group.workspaceTitle}
</p>
) : null}
Expand Down Expand Up @@ -338,24 +359,11 @@ export function WorkspaceChangesWidget({
)}
</div>
{group.files.length > 0 ? (
<div className="relative">
<div className={CHANGES_SCROLL_CONTAINER_CLASS}>
{group.files.map((file) => (
<ChangedFileRow
key={`${group.id}:${file.path}`}
file={file}
fullPath={`${group.repoPath}/${file.path}`}
onOpen={() =>
onOpenFile(`${group.repoPath}/${file.path}`)
}
/>
))}
</div>
<div
className={CHANGES_SCROLL_FADE_CLASS}
aria-hidden="true"
/>
</div>
<ChangedFilesList
files={group.files}
getFullPath={(file) => `${group.repoPath}/${file.path}`}
onOpen={(path) => onOpenFile(`${group.repoPath}/${path}`)}
/>
) : null}
</div>
))}
Expand Down
25 changes: 15 additions & 10 deletions src/features/chat/ui/widgets/WorkingContextPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,20 @@ export function WorkingContextPicker({
const hasWorktrees = worktrees.length > 0;
const hasVisibleWorktrees = visibleWorktrees.length > 0;
const hasVisibleBranches = visibleBranches.length > 0 && Boolean(currentPath);
// Hover/selected fills use `muted`: in dark it equals the old
// `sidebar-accent` value (gray-700), while in light it stays visible on
// white surfaces where `sidebar-accent` (gray-50) disappears. rounded-sm
// rows sit concentric inside the rounded-md popover's p-1.5 padding
// (12px + 6px = 18px); the active row carries the selected fill.
const pickerRowClassName = cn(
"group flex w-full items-start gap-3 rounded-xs px-2 py-2.5 text-left",
"group flex w-full items-start gap-3 rounded-sm px-2 py-2.5 text-left",
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-foreground focus-visible:outline-none aria-[current=true]:bg-sidebar-accent aria-[current=true]:text-sidebar-foreground",
"enabled:hover:bg-muted focus-visible:bg-muted focus-visible:outline-none aria-[current=true]:bg-muted",
);
const branchRowClassName = cn(
"group flex w-full items-center gap-3 rounded-xs px-2 py-2.5 text-left",
"group flex w-full items-center gap-3 rounded-sm px-2 py-2.5 text-left",
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-foreground focus-visible:outline-none aria-[current=true]:bg-sidebar-accent aria-[current=true]:text-sidebar-foreground",
"enabled:hover:bg-muted focus-visible:bg-muted focus-visible:outline-none aria-[current=true]:bg-muted",
);

return (
Expand All @@ -334,7 +339,7 @@ export function WorkingContextPicker({
"flex w-full items-start gap-3 rounded-sm bg-muted/60 px-3.5 py-2.5",
"text-sm text-foreground",
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
)}
aria-label={t("contextPanel.picker.selectWorktree")}
>
Expand All @@ -359,9 +364,9 @@ export function WorkingContextPicker({
<PopoverContent
align="start"
sideOffset={8}
className="flex max-h-[min(28rem,var(--radix-popover-content-available-height))] w-[var(--radix-popover-trigger-width)] min-w-72 flex-col overflow-hidden rounded-sm p-2 text-sm font-normal"
className="flex max-h-[min(28rem,var(--radix-popover-content-available-height))] w-[var(--radix-popover-trigger-width)] min-w-72 flex-col overflow-hidden rounded-md bg-card p-1.5 text-sm font-normal"
>
<div className="mb-2 flex h-10 items-center gap-2 rounded-xs border border-transparent bg-muted/60 px-3 text-muted-foreground focus-within:border-transparent focus-within:ring-0">
<div className="mb-2 flex h-10 items-center gap-2 rounded-sm border border-transparent px-3 text-muted-foreground transition-colors hover:bg-muted/60 focus-within:border-transparent focus-within:ring-0">
<IconSearch className="size-4 shrink-0" />
<input
type="search"
Expand Down Expand Up @@ -456,7 +461,7 @@ export function WorkingContextPicker({
"flex w-full items-start gap-3 rounded-sm bg-muted/60 px-3.5 py-2.5",
"text-sm text-foreground",
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
)}
aria-label={t("contextPanel.picker.selectBranch")}
>
Expand All @@ -473,9 +478,9 @@ export function WorkingContextPicker({
<PopoverContent
align="start"
sideOffset={8}
className="flex max-h-[min(28rem,var(--radix-popover-content-available-height))] w-[var(--radix-popover-trigger-width)] min-w-72 flex-col overflow-hidden rounded-sm p-2 text-sm font-normal"
className="flex max-h-[min(28rem,var(--radix-popover-content-available-height))] w-[var(--radix-popover-trigger-width)] min-w-72 flex-col overflow-hidden rounded-md bg-card p-1.5 text-sm font-normal"
>
<div className="mb-2 flex h-10 items-center gap-2 rounded-xs border border-transparent bg-muted/60 px-3 text-muted-foreground focus-within:border-transparent focus-within:ring-0">
<div className="mb-2 flex h-10 items-center gap-2 rounded-sm border border-transparent px-3 text-muted-foreground transition-colors hover:bg-muted/60 focus-within:border-transparent focus-within:ring-0">
<IconSearch className="size-4 shrink-0" />
<input
type="search"
Expand Down
4 changes: 2 additions & 2 deletions src/features/chat/ui/widgets/WorkspaceActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function WorkspaceActionsMenu({
SIDEBAR_ROW_VERTICAL_PADDING_CLASS,
SIDEBAR_NAV_TEXT_CLASS,
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"rounded-xs pl-2 pr-3 text-foreground hover:bg-sidebar-accent hover:text-sidebar-foreground focus:bg-sidebar-accent focus:text-sidebar-foreground data-[highlighted]:bg-sidebar-accent data-[highlighted]:text-sidebar-foreground",
"rounded-sm pl-2 pr-3 text-foreground hover:bg-muted focus:bg-muted data-[highlighted]:bg-muted",
);
const menuLabelClassName =
"px-2 pb-1 text-sm font-normal text-muted-foreground";
Expand All @@ -137,7 +137,7 @@ export function WorkspaceActionsMenu({
<DropdownMenuContent
align="end"
sideOffset={8}
className="w-64 rounded-sm px-1 pb-[6px] pt-3"
className="w-64 rounded-md bg-card px-1.5 pb-[6px] pt-3"
>
{onToggleTerminal ? (
<DropdownMenuItem
Expand Down
27 changes: 19 additions & 8 deletions src/features/chat/ui/widgets/WorkspaceContextPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,22 @@ export function WorkspaceContextPicker({
void carrySwitch(branch);
};

// Hover/selected fills use `muted`: in dark it equals the old
// `sidebar-accent` value (gray-700), while in light it stays visible on
// white surfaces where `sidebar-accent` (gray-50) disappears. The resting
// fill is `muted/60` to match the legacy picker cards — `background/45`
// reads darker than the rail surface in dark mode.
const pickerClassName = cn(
"group flex min-h-9 w-full items-center gap-2 rounded-sm bg-background/45 px-2.5 py-2 text-left text-sm",
"transition-colors hover:bg-background/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"group flex min-h-9 w-full items-center gap-2 rounded-sm bg-muted/60 px-2.5 py-2 text-left text-sm",
"transition-colors enabled:hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-60",
);
// rounded-sm rows sit concentric inside the rounded-md popover's p-1.5
// padding (12px + 6px = 18px). The active row carries the selected fill.
const optionClassName = cn(
"flex w-full items-start gap-2 rounded-xs px-2 py-2 text-left text-sm",
"hover:bg-sidebar-accent focus-visible:bg-sidebar-accent focus-visible:outline-none",
"flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left text-sm",
"enabled:hover:bg-muted focus-visible:bg-muted focus-visible:outline-none",
"aria-[current=true]:bg-muted",
"disabled:cursor-default disabled:text-muted-foreground",
);

Expand Down Expand Up @@ -228,9 +236,12 @@ export function WorkspaceContextPicker({
<PopoverContent
align="start"
sideOffset={6}
className="w-[var(--radix-popover-trigger-width)] rounded-sm p-2"
// Rail dropdowns live on the card surface (the rail glass
// derives from card), not the darker overflow-popover surface,
// and share the rail container's rounded-md radius.
className="w-[var(--radix-popover-trigger-width)] rounded-md bg-card p-1.5"
>
<div className="mb-2 flex h-9 items-center gap-2 rounded-xs bg-muted/60 px-2.5 text-muted-foreground">
<div className="mb-2 flex h-9 items-center gap-2 rounded-sm px-2.5 text-muted-foreground transition-colors hover:bg-muted/60">
<Search className="size-3.5" />
<input
type="search"
Expand Down Expand Up @@ -327,9 +338,9 @@ export function WorkspaceContextPicker({
<PopoverContent
align="start"
sideOffset={6}
className="w-[var(--radix-popover-trigger-width)] rounded-sm p-2"
className="w-[var(--radix-popover-trigger-width)] rounded-md bg-card p-1.5"
>
<div className="mb-2 flex h-9 items-center gap-2 rounded-xs bg-muted/60 px-2.5 text-muted-foreground">
<div className="mb-2 flex h-9 items-center gap-2 rounded-sm px-2.5 text-muted-foreground transition-colors hover:bg-muted/60">
<Search className="size-3.5" />
<input
type="search"
Expand Down
7 changes: 5 additions & 2 deletions src/features/chat/ui/widgets/WorkspaceRowActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export function WorkspaceRowActionsMenu({
SIDEBAR_ROW_VERTICAL_PADDING_CLASS,
SIDEBAR_NAV_TEXT_CLASS,
SIDEBAR_MENU_HOVER_TRANSITION_CLASS,
"rounded-xs text-foreground hover:bg-sidebar-accent hover:text-sidebar-foreground focus:bg-sidebar-accent focus:text-sidebar-foreground data-[highlighted]:bg-sidebar-accent data-[highlighted]:text-sidebar-foreground",
// `muted`, not `sidebar-accent`: identical in dark, visible in light.
"rounded-xs text-foreground hover:bg-muted focus:bg-muted data-[highlighted]:bg-muted",
);
const menuLabelClassName =
"px-3 pb-1 text-sm font-normal text-muted-foreground";
Expand Down Expand Up @@ -290,7 +291,9 @@ export function WorkspaceRowActionsMenu({
<DropdownMenuContent
align="end"
sideOffset={8}
className="w-64 rounded-sm px-3 pb-[6px] pt-3"
// Card surface + the rail's rounded-md; rounded-xs items stay
// concentric inside the px-3 gutter (6px + 12px = 18px).
className="w-64 rounded-md bg-card px-3 pb-[6px] pt-3"
>
{onOpenTerminalAtPath ? (
<DropdownMenuItem
Expand Down
10 changes: 7 additions & 3 deletions src/features/chat/ui/widgets/WorkspaceWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ function AddWorkspaceRow({
disabled={disabled}
className={cn(
"gap-2 bg-transparent px-2 py-1 leading-[15px] text-foreground duration-150",
"hover:bg-sidebar-accent hover:text-foreground focus-visible:bg-sidebar-accent focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
// `muted`, not `sidebar-accent`: identical in dark, visible in light.
"hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-foreground",
)}
iconClassName="size-3.5 text-current"
Expand Down Expand Up @@ -326,10 +327,13 @@ function WorkspaceSectionActionsMenu({
<DropdownMenuContent
align="end"
sideOffset={8}
className="w-56 rounded-sm px-1 pb-[6px] pt-1"
// Card surface + the rail's rounded-md, with the primitive's p-1.5 so
// the rounded-sm items stay concentric.
className="w-56 rounded-md bg-card p-1.5"
>
<DropdownMenuItem
disabled={!onAddWorkspace}
className="focus:bg-muted data-[highlighted]:bg-muted"
onSelect={() => void onAddWorkspace?.()}
>
<FolderPlus className="size-4" />
Expand Down Expand Up @@ -537,7 +541,7 @@ export function WorkspaceWidget({
className={cn(
"flex w-full items-center gap-2 rounded-sm px-2 py-1",
"text-sm text-foreground transition-colors",
"hover:bg-sidebar-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
"disabled:cursor-not-allowed disabled:opacity-70 disabled:hover:bg-transparent",
)}
aria-label={t("contextPanel.folder.change")}
Expand Down
Loading