feat(lanes): name the files behind a lane's uncommitted counts

GET /api/lanes/:id/git now returns `files`, the raw `git status --porcelain`
lines the dirty/untracked counts were already parsed from, capped at 20 so a
lane mid-`npm install` cannot ship thousands of paths through a polled
endpoint. The lane card hovers them as the tooltip on its
"N modified · N untracked" text, appending "… +N" when the cap bit.

No new subprocess and no new endpoint — statusCounts was counting those lines
and throwing them away.
This commit is contained in:
2026-09-17 13:19:07 +07:00
parent 2fe1dcd46f
commit af2ad63a18
8 changed files with 46 additions and 8 deletions
+14 -1
View File
@@ -539,7 +539,20 @@ export default function LaneCard({
<span className="shrink-0 text-fg-muted">{git.head}</span>
<span className="truncate text-fg-muted">{git.subject}</span>
{(git.dirty > 0 || git.untracked > 0) && (
<span className="shrink-0 text-status-warning/80">
<span
className="shrink-0 cursor-help text-status-warning/80"
/* Native title: the counts' own file list, capped server-side.
A hover card would need a portal to escape this truncating
row — the browser's tooltip already escapes it for free. */
title={
git.files?.length
? git.files.join("\n") +
(git.dirty + git.untracked > git.files.length
? `\n… +${git.dirty + git.untracked - git.files.length}`
: "")
: undefined
}
>
{t("git.uncommitted", { dirty: git.dirty, untracked: git.untracked })}
</span>
)}
@@ -207,6 +207,7 @@ describe("LaneCard git block", () => {
subject: "free-text rule mode in the form",
dirty: 2,
untracked: 1,
files: [" M server/lib/alerts.js", " M client/src/lib/api.ts", "?? scratch.txt"],
};
it("renders the live branch, head, subject and uncommitted counts", async () => {
@@ -218,6 +219,10 @@ describe("LaneCard git block", () => {
expect(block.textContent).toContain("9b3e74a");
expect(block.textContent).toContain("free-text rule mode in the form");
expect(block.textContent).toContain("2");
// Hovering the counts names the files behind them.
expect(screen.getByText(/modified/).title).toBe(
" M server/lib/alerts.js\n M client/src/lib/api.ts\n?? scratch.txt"
);
// The live branch replaces the lane's recorded one rather than doubling it.
expect(screen.queryByText(/stale\/recorded/)).toBeNull();
});