feat(FN-2921): merge fusion/fn-2921

- feat(FN-2921): complete Step 2 — apply filename-first truncation in change lists
- feat(FN-2921): complete Step 1 — prioritize filename-first path truncation

Fusion-Task-Id: FN-2921
This commit is contained in:
Fusion
2026-04-28 22:43:51 -07:00
committed by gsxdsm
parent 44480c0d88
commit 81cf7dcbab
7 changed files with 33 additions and 19 deletions

View File

@@ -469,6 +469,7 @@ describe("Settings view", () => {
controller.setInteractiveView("settings");
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
await waitForFrameContains(lastFrame, "──── Remote ────");
stdin.write("\t");
await new Promise((r) => setTimeout(r, 20));
@@ -529,6 +530,7 @@ describe("Settings view", () => {
controller.setInteractiveView("settings");
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
await waitForFrameContains(lastFrame, "──── Remote ────");
stdin.write("\t");
await new Promise((r) => setTimeout(r, 20));

View File

@@ -96,7 +96,6 @@
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-family: var(--font-mono);
font-size: 12px;
}

View File

@@ -12,6 +12,7 @@ import {
} from "lucide-react";
import type { MergeDetails, Column } from "@fusion/core";
import { highlightDiff } from "../utils/highlightDiff";
import { truncateMiddle } from "../utils/truncatePath";
import "./TaskDiffShared.css";
import "./ChangesDiffModal.css";
@@ -222,7 +223,7 @@ export function ChangesDiffModal({
>
{getStatusLabel(file.status)}
</span>
<span className="changes-diff-file-path">{file.path}</span>
<span className="changes-diff-file-path">{truncateMiddle(file.path)}</span>
<span className="changes-diff-file-stat">
+{file.additions} -{file.deletions}
</span>

View File

@@ -4,6 +4,7 @@ import type { Task } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { useConfirm } from "../hooks/useConfirm";
import { truncateMiddle } from "../utils/truncatePath";
import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import type {
@@ -1114,7 +1115,7 @@ function ChangesPanel({
/>
</label>
<FileStatusIcon status={f.status} />
<span className="gm-file-name" title={f.file}>{f.file}</span>
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file)}</span>
<FileStatusBadge status={f.status} />
<button
className="gm-icon-btn"
@@ -1186,7 +1187,7 @@ function ChangesPanel({
/>
</label>
<FileStatusIcon status={f.status} />
<span className="gm-file-name" title={f.file}>{f.file}</span>
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file)}</span>
<FileStatusBadge status={f.status} />
<button
className="gm-icon-btn"

View File

@@ -2122,7 +2122,6 @@
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 12px;

View File

@@ -20,13 +20,11 @@ describe("truncateMiddle", () => {
expect(truncateMiddle(path, 60)).toBe(path);
});
it("truncates a long path from the middle", () => {
it("truncates a long path by prioritizing the filename suffix", () => {
const path = "packages/dashboard/app/components/TaskChangesTab.tsx";
const result = truncateMiddle(path, 30);
expect(result).toContain("...");
expect(result).toBe(".../TaskChangesTab.tsx");
expect(result.length).toBeLessThanOrEqual(30);
// Filename should be preserved
expect(result.endsWith("TaskChangesTab.tsx")).toBe(true);
});
it("preserves the full path when under maxLength", () => {
@@ -84,12 +82,11 @@ describe("truncateMiddle", () => {
expect(result).toContain("...");
});
it("preserves start portion when truncating", () => {
it("does not require preserving the start when filename suffix fits", () => {
const path = "packages/dashboard/app/components/TaskChangesTab.tsx";
const result = truncateMiddle(path, 35);
expect(result.startsWith("packages")).toBe(true);
expect(result).toContain("...");
expect(result.endsWith("TaskChangesTab.tsx")).toBe(true);
expect(result).toBe(".../TaskChangesTab.tsx");
expect(result.endsWith("/TaskChangesTab.tsx")).toBe(true);
});
it("works with paths that have dots but no slashes", () => {
@@ -103,4 +100,18 @@ describe("truncateMiddle", () => {
const result = truncateMiddle(path, 60);
expect(result.length).toBeLessThanOrEqual(60);
});
it("shows full filename for deeply nested paths when filename fits", () => {
const path = "a/b/c/d/e/f/Component.tsx";
const result = truncateMiddle(path, 20);
expect(result).toBe(".../Component.tsx");
expect(result.endsWith("/Component.tsx")).toBe(true);
});
it("preserves extension when filename fits", () => {
const path = "deeply/nested/another/path/verylongname.test.tsx";
const result = truncateMiddle(path, 24);
expect(result).toBe("...verylongname.test.tsx");
expect(result.endsWith(".test.tsx")).toBe(true);
});
});

View File

@@ -24,6 +24,13 @@ export function truncateMiddle(path: string, maxLength: number = 60): string {
return path.slice(0, headLen) + ellipsis;
}
const suffixWithSlash = path.slice(lastSep);
// Primary behavior: always prefer showing full filename (and extension)
// when it can fit with an ellipsis prefix.
if (ellipsis.length + suffixWithSlash.length <= maxLength) {
return ellipsis + suffixWithSlash;
}
// Find all separator positions to use as candidate split points
const seps: number[] = [];
for (let i = 1; i < path.length; i++) {
@@ -63,12 +70,6 @@ export function truncateMiddle(path: string, maxLength: number = 60): string {
if (bestResult) return bestResult;
// Fallback: just ellipsis + the filename portion (with leading "/")
const suffixWithSlash = path.slice(lastSep);
if (ellipsis.length + suffixWithSlash.length <= maxLength) {
return ellipsis + suffixWithSlash;
}
// Filename itself is too long — truncate from the end
const endLen = maxLength - ellipsis.length;
return ellipsis + path.slice(path.length - endLen);