feat(FN-3629): refine branch chip visibility in task cards

Refined branch chip visibility logic in TaskCard (suppressing the chip when the current branch is the default) and added test coverage for those suppression cases, with corresponding documentation updates. Also added chat layout regression tests and fixed a composer drag-pan issue on mobile touch de

Fusion-Task-Id: FN-3629
This commit is contained in:
Fusion
2026-05-06 22:02:27 -07:00
committed by gsxdsm
parent 7eab743eb2
commit 2b9f279d9a
3 changed files with 73 additions and 24 deletions

View File

@@ -110,7 +110,7 @@ fn task unarchive FN-001
### Branch metadata semantics
Task cards on the board surface branch metadata when present so users can scan both the working branch and merge target without opening task details.
Task cards on the board only surface branch metadata when it is non-default/user-meaningful: they hide the conventional auto-generated working branch (`fusion/<task-id>` and suffixed variants) and hide the default merge target (`main`), while still showing custom working branches and non-default merge targets.
The board header search panel now includes two **board-only** branch filters:
- **Working branch** filters by `task.branch`

View File

@@ -209,6 +209,30 @@ function formatElapsedDuration(elapsedMs: number): string {
return `${elapsedDays}d`;
}
function normalizeBranchValue(value: string | undefined): string | null {
if (!value) return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function getVisibleTaskCardBranches(task: Task): { branch: string | null; baseBranch: string | null } {
const branch = normalizeBranchValue(task.branch);
const baseBranch = normalizeBranchValue(task.baseBranch);
const defaultBranchPrefix = `fusion/${task.id.toLowerCase()}`;
const visibleBranch =
branch && (branch === defaultBranchPrefix || branch.startsWith(`${defaultBranchPrefix}-`))
? null
: branch;
const visibleBaseBranch = baseBranch?.toLowerCase() === "main" ? null : baseBranch;
return {
branch: visibleBranch,
baseBranch: visibleBaseBranch ?? null,
};
}
export function formatElapsedDurationDone(elapsedMs: number): string {
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return "";
if (elapsedMs === 0) return "";
@@ -710,7 +734,8 @@ function TaskCardComponent({
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
const hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
const isGitHubImportedTask = task.sourceType === "github_import";
const hasBranchMetadata = Boolean(task.branch || task.baseBranch);
const branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
const isAgentCreated = isAgentCreatedTask(task);
const sourceAgentName = getSourceAgentName(task);
@@ -1498,16 +1523,16 @@ function TaskCardComponent({
</div>
{hasBranchMetadata && (
<div className="card-branch-row" aria-label="Branch metadata">
{task.branch && (
<span className="card-branch-chip" title={task.branch}>
{branchMetadata.branch && (
<span className="card-branch-chip" title={branchMetadata.branch}>
<span className="card-branch-label">Branch</span>
<span className="card-branch-value">{task.branch}</span>
<span className="card-branch-value">{branchMetadata.branch}</span>
</span>
)}
{task.baseBranch && (
<span className="card-branch-chip" title={task.baseBranch}>
{branchMetadata.baseBranch && (
<span className="card-branch-chip" title={branchMetadata.baseBranch}>
<span className="card-branch-label">Base</span>
<span className="card-branch-value">{task.baseBranch}</span>
<span className="card-branch-value">{branchMetadata.baseBranch}</span>
</span>
)}
</div>

View File

@@ -171,27 +171,34 @@ describe("TaskCard", () => {
expect(screen.queryByText("paused by agent")).toBeNull();
});
it("renders branch and base branch metadata when both are present", () => {
it("hides default working branch and default base branch metadata", () => {
const { container } = render(
<TaskCard
task={makeTask({ branch: "feature/fn-3423-card-branches", baseBranch: "main" })}
task={makeTask({ branch: "fusion/fn-001", baseBranch: "main" })}
onOpenDetail={noop}
addToast={noop}
/>,
);
const branchRow = container.querySelector(".card-branch-row");
expect(branchRow).not.toBeNull();
expect(screen.getByText("Branch")).toBeDefined();
expect(screen.getByText("feature/fn-3423-card-branches")).toBeDefined();
expect(screen.getByText("Base")).toBeDefined();
expect(screen.getByText("main")).toBeDefined();
expect(container.querySelector(".card-branch-row")).toBeNull();
});
it("renders only working branch metadata when baseBranch is absent", () => {
it("hides auto-generated suffixed default working branches", () => {
const { container } = render(
<TaskCard
task={makeTask({ branch: "fusion/fn-001-2", baseBranch: "main" })}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-branch-row")).toBeNull();
});
it("shows only custom working branch metadata when base branch is default", () => {
render(
<TaskCard
task={makeTask({ branch: "feature/working-only", baseBranch: undefined })}
task={makeTask({ branch: "feature/working-only", baseBranch: "main" })}
onOpenDetail={noop}
addToast={noop}
/>,
@@ -202,10 +209,10 @@ describe("TaskCard", () => {
expect(screen.queryByText("Base")).toBeNull();
});
it("renders only base branch metadata when branch is absent", () => {
it("shows only non-default base branch metadata when working branch is default", () => {
render(
<TaskCard
task={makeTask({ branch: undefined, baseBranch: "release/2026-05" })}
task={makeTask({ branch: "fusion/fn-001", baseBranch: "release/2026-05" })}
onOpenDetail={noop}
addToast={noop}
/>,
@@ -216,19 +223,24 @@ describe("TaskCard", () => {
expect(screen.queryByText("Branch")).toBeNull();
});
it("does not render branch metadata row when both branch fields are absent", () => {
it("shows both chips when branch and base branch are both non-default", () => {
const { container } = render(
<TaskCard
task={makeTask({ branch: undefined, baseBranch: undefined })}
task={makeTask({ branch: "feature/fn-3423-card-branches", baseBranch: "develop" })}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-branch-row")).toBeNull();
const branchRow = container.querySelector(".card-branch-row");
expect(branchRow).not.toBeNull();
expect(screen.getByText("Branch")).toBeDefined();
expect(screen.getByText("feature/fn-3423-card-branches")).toBeDefined();
expect(screen.getByText("Base")).toBeDefined();
expect(screen.getByText("develop")).toBeDefined();
});
it("keeps long branch names readable via text and title semantics", () => {
it("keeps long non-default branch names readable via text and title semantics", () => {
const longBranch = "feature/fn-3423-display-very-long-working-branch-name-for-card-metadata";
const { container } = render(
<TaskCard
@@ -1420,6 +1432,18 @@ describe("TaskCard memo comparator provenance behavior", () => {
),
).toBe(false);
});
it("returns false when baseBranch changes", () => {
const previousTask = makeTask({ branch: "fusion/fn-001", baseBranch: "main" });
const nextTask = makeTask({ branch: "fusion/fn-001", baseBranch: "release/2026-05" });
expect(
__test_areTaskCardPropsEqual(
{ task: previousTask, onOpenDetail: noop, addToast: noop } as any,
{ task: nextTask, onOpenDetail: noop, addToast: noop } as any,
),
).toBe(false);
});
});
describe("TaskCard mission badge", () => {