54ddb0aa1e
* feat(web): track active file path for diff panels
Extend resolveFilePath so activeFilePath updates when a file-diff panel
(preview:file-diff or diff:file:<path>) becomes the active dockview
panel. Also re-subscribe onDidParametersChange for the preview file-diff
slot so swapping the preview to a different file refreshes the path.
* feat(web): highlight active-tab file row in Changes panel
When a file's diff tab is the active dockview panel, mark the matching
row in unstaged, staged, and PR Changes lists with data-active="true"
and reuse the existing bg-accent/60 highlight (same as multi-select).
PRFileRow also gains data-changes-file so it shares the existing row
selector with FileRow.
* test(web): cover active-tab highlight unit + E2E
Unit test in FileRow guards the data-active attribute and bg-accent/60
class when isActive (with and without isSelected). New E2E spec drives
the full flow: open file-a diff, switch to file-b, close the preview,
and verify highlight follows when a file moves from unstaged to staged.
* fix(web): use distinct e2e task titles for active-tab highlight spec
clickTab("Changes") fails strict-mode when the task title also contains
the word "Changes" — Playwright matches both the session tab and the
dockview Changes tab. Renames to "Active Highlight (Staged) Test".
* test(web): close pinned diff:file panels in closeFileDiffPreview
The helper previously closed only the preview slot, leaving pinned
diff:file:<path> panels (created when a preview is promoted) active.
Sweeps every diff:file: panel so the row settles to data-active=false
regardless of how the diff was opened.
* test(web): unit-cover resolveFilePath panel-id branches
Exercises every branch of the dockview-store resolveFilePath closure:
file:, diff:file:, preview:file-editor, and preview:file-diff (the
last two via params.path), plus the null-out paths. Catches regressions
in the panel-id mapping without paying for an E2E round-trip.
* fix(web): strip "changes" from active-tab spec profile names
The agent profile name becomes the dockview session tab title, so a
profile named "changes-active-highlight" makes clickTab("Changes")
match two .dv-default-tab elements (session tab + Changes tab) and
trip strict-mode. Rename to "active-highlight" / "active-highlight-
staged" to keep the locator unambiguous.
* docs(web): note multi-repo path collision in PRFilesRepoGroup
Mirrors the FileListBody comment so future authors see the same caveat
in both file-list components: activeFilePath carries the path but not
the repo, so identical paths across repos light up both rows.
* fix(web): snapshot panels before closing in closeFileDiffPreview
dockview's api.panels is a live reference; calling panel.api.close()
removes the panel in place, so iterating the live array skipped every
other diff:file:* panel when two or more were open. Filter into a
local array before closing.
153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { IconChevronDown, IconChevronRight, IconGitPullRequest } from "@tabler/icons-react";
|
|
import { LineStat } from "@/components/diff-stat";
|
|
import { cn } from "@/lib/utils";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
import { FileStatusIcon } from "./file-status-icon";
|
|
import { groupByRepositoryName } from "@/lib/group-by-repo";
|
|
import type { PRChangedFile } from "./changes-panel-timeline";
|
|
import type { OpenDiffOptions } from "./changes-diff-target";
|
|
|
|
type PRFilesSectionContentProps = {
|
|
files: PRChangedFile[];
|
|
onOpenDiff: (path: string, options?: OpenDiffOptions) => void;
|
|
/** Maps a repository_name to a human-readable label for the per-repo header. */
|
|
repoDisplayName?: (repositoryName: string) => string | undefined;
|
|
};
|
|
|
|
/**
|
|
* Body of the PR Changes section: groups files by `repository_name` so
|
|
* multi-repo tasks render one sub-header per repo, mirroring the Commits
|
|
* and Changes sections. Single-repo tasks (one group with empty name)
|
|
* render the file list flat — the section header is enough.
|
|
*/
|
|
export function PRFilesGroupedList({
|
|
files,
|
|
onOpenDiff,
|
|
repoDisplayName,
|
|
}: PRFilesSectionContentProps) {
|
|
const activeFilePath = useDockviewStore((s) => s.activeFilePath);
|
|
const groups = groupByRepositoryName(files, (f) => f.repository_name);
|
|
const showRepoHeaders = groups.length > 1 || (groups[0]?.repositoryName ?? "") !== "";
|
|
return (
|
|
<ul className="space-y-0.5" data-testid="pr-files-list">
|
|
{groups.map((group) => (
|
|
<PRFilesRepoGroup
|
|
key={group.repositoryName || "__no_repo__"}
|
|
repositoryName={group.repositoryName}
|
|
displayName={repoDisplayName?.(group.repositoryName)}
|
|
files={group.items}
|
|
showHeader={showRepoHeaders}
|
|
onOpenDiff={onOpenDiff}
|
|
activeFilePath={activeFilePath}
|
|
/>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function PRFilesRepoGroup({
|
|
repositoryName,
|
|
displayName,
|
|
files,
|
|
showHeader,
|
|
onOpenDiff,
|
|
activeFilePath,
|
|
}: {
|
|
repositoryName: string;
|
|
displayName?: string;
|
|
files: PRChangedFile[];
|
|
showHeader: boolean;
|
|
onOpenDiff: (path: string, options?: OpenDiffOptions) => void;
|
|
activeFilePath: string | null;
|
|
}) {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const label = displayName ?? repositoryName ?? "";
|
|
return (
|
|
<li data-testid="pr-files-repo-group" data-repository-name={repositoryName || ""}>
|
|
{showHeader && (
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-1.5 px-1 py-0.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wide cursor-pointer hover:text-foreground/80 min-w-0"
|
|
data-testid="pr-files-repo-header"
|
|
aria-expanded={!collapsed}
|
|
onClick={() => setCollapsed((c) => !c)}
|
|
>
|
|
{collapsed ? (
|
|
<IconChevronRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
|
|
) : (
|
|
<IconChevronDown className="h-3 w-3 text-muted-foreground/50 shrink-0" />
|
|
)}
|
|
<span className="truncate">{label}</span>
|
|
<span className="text-muted-foreground/50 normal-case tracking-normal">
|
|
{files.length}
|
|
</span>
|
|
</button>
|
|
)}
|
|
{!collapsed && (
|
|
<ul className="space-y-0.5">
|
|
{/* Multi-repo: activeFilePath carries no repo context, so identical
|
|
paths across repos light up both rows. Matches FileListBody. */}
|
|
{files.map((file) => (
|
|
<PRFileRow
|
|
key={file.path}
|
|
file={file}
|
|
onOpenDiff={onOpenDiff}
|
|
isActive={file.path === activeFilePath}
|
|
/>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function PRFileRow({
|
|
file,
|
|
onOpenDiff,
|
|
isActive,
|
|
}: {
|
|
file: PRChangedFile;
|
|
onOpenDiff: (path: string, options?: OpenDiffOptions) => void;
|
|
isActive?: boolean;
|
|
}) {
|
|
const lastSlash = file.path.lastIndexOf("/");
|
|
const folder = lastSlash === -1 ? "" : file.path.slice(0, lastSlash);
|
|
const name = lastSlash === -1 ? file.path : file.path.slice(lastSlash + 1);
|
|
|
|
return (
|
|
<li
|
|
data-changes-file={file.path}
|
|
data-active={isActive ? "true" : "false"}
|
|
className={cn(
|
|
"group flex items-center justify-between gap-2 text-sm rounded-md px-2 py-1.5 -mx-1 cursor-pointer md:px-1 md:py-0.5",
|
|
isActive ? "bg-accent/60 text-accent-foreground hover:bg-accent/50" : "hover:bg-muted/60",
|
|
)}
|
|
onClick={() =>
|
|
onOpenDiff(file.path, {
|
|
source: "pr",
|
|
repositoryName: file.repository_name,
|
|
})
|
|
}
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<div className="flex-shrink-0 flex items-center justify-center size-4">
|
|
<IconGitPullRequest className="h-3 w-3 text-purple-500" />
|
|
</div>
|
|
<button type="button" className="min-w-0 text-left cursor-pointer" title={file.path}>
|
|
<p className="flex text-foreground text-xs min-w-0">
|
|
{folder && <span className="text-foreground/60 truncate shrink">{folder}/</span>}
|
|
<span className="font-medium text-foreground whitespace-nowrap shrink-0">{name}</span>
|
|
</p>
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<LineStat added={file.plus} removed={file.minus} />
|
|
<FileStatusIcon status={file.status} />
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|