diff --git a/.changeset/fn-5968-changes-tab-mobile.md b/.changeset/fn-5968-changes-tab-mobile.md new file mode 100644 index 0000000000..c891eeeb51 --- /dev/null +++ b/.changeset/fn-5968-changes-tab-mobile.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Streamline the Task Changes tab header controls on mobile so diff navigation and actions use a more compact layout. diff --git a/packages/dashboard/app/components/TaskChangesTab.tsx b/packages/dashboard/app/components/TaskChangesTab.tsx index ec117c7cfb..7241aca1ca 100644 --- a/packages/dashboard/app/components/TaskChangesTab.tsx +++ b/packages/dashboard/app/components/TaskChangesTab.tsx @@ -217,6 +217,75 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai const canGoPrev = currentFileIndex !== null && currentFileIndex > 0; const canGoNext = currentFileIndex !== null && currentFileIndex < files.length - 1; + const renderChangesHeader = () => ( +
+
+

+ + {t("taskChanges.filesChangedHeading", "Files Changed ({{count}})", { count: stats.filesChanged })} +

+ + +{stats.additions}{" "} + -{stats.deletions} + +
+
+
+ {files.length > 0 && ( +
+ + + {currentFileIndex !== null ? `${currentFileIndex + 1}/${files.length}` : `—/${files.length}`} + + +
+ )} + +
+
+ + +
+
+
+ ); + if (loading) { return (
@@ -294,6 +363,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai return (
+ {renderChangesHeader()}

{t("taskChanges.noFilesModified", "No files modified.")}

@@ -335,72 +405,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
)} -
-
-

- - {t("taskChanges.filesChangedHeading", "Files Changed ({{count}})", { count: stats.filesChanged })} -

- - +{stats.additions}{" "} - -{stats.deletions} - -
-
-
- {files.length > 0 && ( -
- - - {currentFileIndex !== null ? `${currentFileIndex + 1}/${files.length}` : `—/${files.length}`} - - -
- )} - -
-
- - -
-
-
+ {renderChangesHeader()}
{files.map((file) => { diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 66fb98f4f9..85dfe64467 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -1166,6 +1166,45 @@ margin-left: calc(var(--space-lg) + var(--space-sm)); /* align with text after icon + gap */ } +@media (max-width: 768px) { + .task-changes-tab .changes-header { + align-items: stretch; + gap: var(--space-sm); + } + + .task-changes-tab .changes-header-actions-wrapper { + width: 100%; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: var(--space-sm); + } + + .task-changes-tab .changes-header-actions, + .task-changes-tab .changes-header-actions-secondary { + flex-wrap: wrap; + gap: var(--space-sm); + } + + .task-changes-tab .changes-header-actions { + flex: 1 1 auto; + min-width: 0; + } + + .task-changes-tab .changes-header-actions-secondary { + margin-left: auto; + } + + .task-changes-tab .task-changes-header-title { + gap: var(--space-xs); + } + + .task-changes-tab .task-changes-stats { + margin-left: calc(var(--space-lg) + var(--space-xs)); + } +} + /* Truncate parent dirs from the LEFT so the filename stays visible. JSX * wraps the path in `` to preserve readable left-to-right * display while `direction: rtl` flips the ellipsis to the start. */ diff --git a/packages/dashboard/app/components/__tests__/TaskChangesTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChangesTab.test.tsx index 2bce2e9760..38baf06f45 100644 --- a/packages/dashboard/app/components/__tests__/TaskChangesTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChangesTab.test.tsx @@ -1053,7 +1053,7 @@ describe("TaskChangesTab — expand button", () => { expect(screen.getByTestId("changes-diff-modal")).toBeTruthy(); }); - it("does not render expand button when no files are loaded", async () => { + it("keeps the expand button available when no files are loaded", async () => { mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 }, @@ -1072,7 +1072,7 @@ describe("TaskChangesTab — expand button", () => { expect(screen.getByText("No files modified.")).toBeTruthy(); }); - expect(screen.queryByLabelText("Expand diff view")).toBeNull(); + expect(screen.getByLabelText("Expand diff view")).toBeTruthy(); }); }); @@ -1224,6 +1224,105 @@ describe("TaskChangesTab — file path display", () => { }); }); +describe("TaskChangesTab — header toolbar structure", () => { + it("keeps nav and controls grouped inside the header actions wrapper for populated diffs", async () => { + mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF); + const { container } = render( + , + ); + + await waitFor(() => { + expect(screen.getByText("Files Changed (2)")).toBeTruthy(); + }); + + const header = container.querySelector(".task-changes-tab .changes-header"); + const actionsWrapper = header?.querySelector(".changes-header-actions-wrapper"); + const primaryActions = actionsWrapper?.querySelector(".changes-header-actions"); + const secondaryActions = actionsWrapper?.querySelector(".changes-header-actions-secondary"); + const nav = primaryActions?.querySelector(".changes-nav"); + + expect(actionsWrapper).toBeTruthy(); + expect(primaryActions).toBeTruthy(); + expect(secondaryActions).toBeTruthy(); + expect(nav).toBeTruthy(); + expect(nav?.querySelector('[aria-label="Previous file"]')).toBeTruthy(); + expect(nav?.querySelector('[aria-label="Next file"]')).toBeTruthy(); + expect(nav?.querySelector(".changes-nav-indicator")?.textContent).toBe("1/2"); + expect(screen.getByLabelText("Toggle word wrap").closest(".changes-header-actions")).toBe(primaryActions ?? null); + expect(screen.getByText("Refresh").closest(".changes-header-actions-secondary")).toBe(secondaryActions ?? null); + expect(screen.getByLabelText("Expand diff view").closest(".changes-header-actions-secondary")).toBe(secondaryActions ?? null); + }); + + it("renders the header controls without nav when the diff is empty", async () => { + mockFetchTaskDiff.mockResolvedValue({ + files: [], + stats: { filesChanged: 0, additions: 0, deletions: 0 }, + }); + + const { container } = render( + , + ); + + await waitFor(() => { + expect(screen.getByText("No files modified.")).toBeTruthy(); + }); + + const header = container.querySelector(".task-changes-tab .changes-header"); + const actionsWrapper = header?.querySelector(".changes-header-actions-wrapper"); + + expect(header).toBeTruthy(); + expect(actionsWrapper).toBeTruthy(); + expect(header?.querySelector(".changes-nav")).toBeNull(); + expect(screen.getByLabelText("Toggle word wrap")).toBeTruthy(); + expect(screen.getByText("Refresh")).toBeTruthy(); + expect(screen.getByLabelText("Expand diff view")).toBeTruthy(); + }); + + it("keeps commit metadata above the header while preserving the actions structure", async () => { + mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF); + const { container } = render( + , + ); + + await waitFor(() => { + expect(screen.getByText("Files Changed (2)")).toBeTruthy(); + }); + + const taskTab = container.querySelector(".task-changes-tab"); + const header = taskTab?.querySelector(":scope > .changes-header"); + const commitMeta = taskTab?.querySelector(":scope > .commit-diff-meta"); + + expect(commitMeta).toBeTruthy(); + expect(header).toBeTruthy(); + expect(commitMeta?.nextElementSibling).toBe(header ?? null); + expect(header?.querySelector(".changes-header-actions-wrapper .changes-header-actions-secondary")).toBeTruthy(); + }); + + it("defines task-scoped mobile toolbar overrides without changing shared desktop rules", () => { + const css = loadAllAppCss(); + const mobileToolbarRule = css.match(/@media\s*\(max-width:\s*768px\)\s*\{[\s\S]*?\.task-changes-tab\s+\.changes-header-actions-wrapper\s*\{([\s\S]*?)\}/); + + expect(mobileToolbarRule).toBeTruthy(); + expect(mobileToolbarRule![1]).toContain("flex-direction: row;"); + expect(mobileToolbarRule![1]).toContain("flex-wrap: wrap;"); + expect(mobileToolbarRule![1]).toContain("width: 100%;"); + }); +}); + describe("TaskChangesTab — action button sizing", () => { it("keeps Refresh and expand buttons on the same compact height", () => { const css = loadAllAppCss();