open-swe/ui/src/features/agents/components/messages/TurnChangedFilesCard.tsx
Adam Moussa 4d51410dc8
refactor: move ui/src/{components,lib}/agents into features/ layout
Part of the domain-reorg adoption (build plan step C6): fork content,
upstream layout. git mv's all 80 pure-rename files from the move-map
(agent-scoped components/lib -> ui/src/features/{agents,automations,
reviews,settings}/...), including ported/ splitting into
features/agents/experiments/ (still-unintegrated desktop-host ports)
and features/agents/components/chat/ (CloudPromptBar, CodeBlock,
DiffView, Logo, Markdown, ReplyCard, ShellCommand, ToolExecution --
files actually wired into the live dashboard). Fork-diverged files
(PlanReview.tsx, WorkflowApprovalCard.tsx, AgentsSidebar.tsx,
SidebarFilterMenu.tsx, AgentGitPanel.tsx, AutomationEditor.tsx,
DiffView.tsx, CloudPromptBar.tsx) keep fork content -- verified via
diff that only import lines changed.

Rewrote @/components/agents and @/lib/agents imports across all 54
importer files plus the moved files' own internal imports (grep-driven,
85-entry alias map covering every old->new path pair). Two hazards
caught only by the build gate (not tsc, since both files sit under the
new experiments/** tsconfig exclude): ui/src/lib/notifications.ts had
a relative `./agents/types` import (no `@/` alias) that the grep missed;
and features/agents/experiments/index.ts's barrel re-export of the
messages module needed to switch from an `@/` alias to a relative
import (`../components/messages`) after landing inside the newly
tsconfig-excluded experiments/ directory -- vite-tsconfig-paths failed
to resolve it at build time even though tsc stayed silent.

AgentPromptBar.tsx (the 2-line CloudPromptBar re-export shim) moves to
features/agents/components/ with its export path retargeted at
CloudPromptBar's new chat/ location -- a D/A pair, not a content loss,
since the import-path edit drops it below git's rename-similarity
threshold. ported/index.ts is the same D/A story for the same reason.

Swapped the 8-file ported/ exclude lists in tsconfig.json and
eslint.config.js for the single `src/features/agents/experiments/**`
glob (upstream's simplification, no behavior change). Deleted
ui/pnpm-workspace.yaml (lockfile hygiene -- bun stays the toolchain).

Gates: bunx tsc --noEmit (clean), bun run build (clean after the two
notifications.ts / experiments-index.ts fixes above), bun run test
(33/33), and the full Playwright E2E suite against the real langgraph
dev server + built dashboard (9/9).
2026-07-17 14:51:12 -04:00

128 lines
4.4 KiB
TypeScript

import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { MultiFileDiff } from "@pierre/diffs/react";
import type { ChangedFileSummaryItem } from "./types";
import { useDiffOptions, warmDiffHighlighter } from "@/features/agents/utils/diffUtils";
function stripProjectPathForDisplay(path: string, projectPath?: string): string {
if (!projectPath) return path;
const normalizedPath = path.replace(/\\/g, "/");
const normalizedProjectPath = projectPath.replace(/\\/g, "/").replace(/\/+$/, "");
if (!normalizedPath.startsWith(`${normalizedProjectPath}/`)) return path;
return normalizedPath.slice(normalizedProjectPath.length + 1);
}
const ChangedFileRow = memo(function ChangedFileRow({
file,
projectPath,
open,
diffReady,
onToggle,
}: {
file: ChangedFileSummaryItem;
projectPath?: string;
open: boolean;
diffReady: boolean;
onToggle: (filePath: string) => void;
}) {
const diffOptions = useDiffOptions();
const displayPath = useMemo(
() => stripProjectPathForDisplay(file.filePath, projectPath),
[file.filePath, projectPath],
);
const oldFile = useMemo(
() => ({ name: displayPath, contents: file.originalContent }),
[displayPath, file.originalContent],
);
const newFile = useMemo(
() => ({ name: displayPath, contents: file.modifiedContent }),
[displayPath, file.modifiedContent],
);
return (
<div className="border-b last:border-b-0 border-[var(--ui-border)]">
<button
type="button"
onClick={() => onToggle(file.filePath)}
className="w-full px-3 py-2 text-left hover:bg-[var(--ui-panel-2)] transition-colors flex items-center justify-between gap-3"
aria-expanded={open}
>
<span className="text-[13px] text-[color:var(--ui-accent)] truncate min-w-0">{displayPath}</span>
<span className="shrink-0 flex items-center gap-2">
<span className="text-xs flex items-center gap-2">
<span className="text-green-400">+{file.additions}</span>
<span className="text-red-400">-{file.deletions}</span>
</span>
{open ? (
<ChevronUp className="h-3.5 w-3.5 text-[color:var(--ui-text-dim)] shrink-0" aria-hidden />
) : (
<ChevronDown className="h-3.5 w-3.5 text-[color:var(--ui-text-dim)] shrink-0" aria-hidden />
)}
</span>
</button>
{open && (
<div className="px-2 pb-2">
<div className="rounded-lg bg-[var(--ui-panel)] overflow-hidden border border-[var(--ui-border-subtle)]">
{diffReady ? (
<div className="max-h-[250px] overflow-auto">
<MultiFileDiff oldFile={oldFile} newFile={newFile} options={diffOptions} />
</div>
) : (
<div className="px-3 py-4 text-xs text-[color:var(--ui-text-dim)]">Loading diff…</div>
)}
</div>
</div>
)}
</div>
);
});
export const TurnChangedFilesCard = memo(function TurnChangedFilesCard({
files,
totals,
projectPath,
}: {
files: Array<ChangedFileSummaryItem>;
totals: { additions: number; deletions: number };
projectPath?: string;
}) {
const [expandedByPath, setExpandedByPath] = useState<Record<string, boolean>>({});
const [diffReady, setDiffReady] = useState(false);
useEffect(() => {
let active = true;
warmDiffHighlighter().finally(() => {
if (active) setDiffReady(true);
});
return () => {
active = false;
};
}, []);
const toggleFile = useCallback((filePath: string) => {
setExpandedByPath((prev) => ({ ...prev, [filePath]: !prev[filePath] }));
}, []);
return (
<div className="mt-3 rounded-xl bg-[var(--ui-code-bubble)] overflow-hidden">
<div className="px-3 py-2 text-xs text-[color:var(--ui-text-muted)] border-b border-[var(--ui-border)] flex items-center gap-2">
<span>{files.length} file{files.length === 1 ? "" : "s"} changed</span>
<span className="text-green-400">+{totals.additions}</span>
<span className="text-red-400">-{totals.deletions}</span>
</div>
<div>
{files.map((file) => (
<ChangedFileRow
key={file.filePath}
file={file}
projectPath={projectPath}
open={!!expandedByPath[file.filePath]}
diffReady={diffReady}
onToggle={toggleFile}
/>
))}
</div>
</div>
);
});