diff --git a/.changeset/fn-8679-mobile-token-scroll.md b/.changeset/fn-8679-mobile-token-scroll.md new file mode 100644 index 0000000000..45377a0352 --- /dev/null +++ b/.changeset/fn-8679-mobile-token-scroll.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Summary token and cost tables scrollable on mobile. +category: fix +dev: Contains mobile table overflow within the task Summary surface. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 0115856b14..9616aa137b 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -2383,12 +2383,16 @@ The done-task Summary tab is a scrollable overview composed from existing detail display: flex; flex-direction: column; gap: var(--space-lg); + min-width: 0; + max-width: 100%; } .task-summary-section { display: flex; flex-direction: column; gap: var(--space-sm); + min-width: 0; + max-width: 100%; padding: var(--space-lg); background: var(--card); border: var(--btn-border-width) solid var(--border); @@ -2467,9 +2471,14 @@ The desktop table needs a token-scale min-width and non-anywhere model-name wrap FNXC:TaskDetailSummaryTokenCost 2026-07-11-00:00: Mobile per-model breakdowns must stay real tables and scroll horizontally inside .task-summary-token-table-wrap, matching the Command Center .cc-table-wrap pattern. Do not stack rows into cards; side-by-side model token/cost columns are the user-facing scan path. + +FNXC:TaskDetailSummaryTab 2026-08-01-07:15: +Mobile token/cost scrolling requires min-width: 0 containment through the Summary flex ancestors. Overflow-x alone cannot shrink an intrinsically wide table's flex/grid child, so the wrapper must own horizontal overflow rather than widening the detail body. */ .task-summary-token-table-wrap { width: 100%; + min-width: 0; + max-width: 100%; overflow-x: auto; } diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx index 1bb86579ed..cd6243be17 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.summary-tab.test.tsx @@ -190,6 +190,64 @@ describe("TaskDetailModal Summary tab", () => { expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull(); }); + it("keeps populated token costs in a locally scrollable real table across data states", () => { + const twoModelUsage = tokenUsage({ + inputTokens: 300, + outputTokens: 150, + totalTokens: 450, + perModel: [ + { + modelProvider: "anthropic", + modelId: "claude-sonnet-4-6", + inputTokens: 200, + outputTokens: 100, + cachedTokens: 0, + cacheWriteTokens: 0, + totalTokens: 300, + firstUsedAt: "2026-01-01T00:00:00Z", + lastUsedAt: "2026-01-01T00:00:00Z", + }, + { + modelProvider: "openai", + modelId: "gpt-4o-mini", + inputTokens: 100, + outputTokens: 50, + cachedTokens: 0, + cacheWriteTokens: 0, + totalTokens: 150, + firstUsedAt: "2026-01-01T00:00:00Z", + lastUsedAt: "2026-01-01T00:00:00Z", + }, + ], + }); + const view = render(); + const section = screen.getByTestId("task-summary-token-cost-section"); + const wrapper = section.querySelector(".task-summary-token-table-wrap"); + const table = wrapper?.querySelector("table.task-summary-token-table"); + + expect(wrapper).toBeTruthy(); + expect(table).toBeTruthy(); + expect(table?.tagName).toBe("TABLE"); + expect(wrapper?.contains(table ?? null)).toBe(true); + for (const heading of ["Model", "Input", "Output", "Cached", "Total", "Cost"]) { + expect(within(table!).getByRole("columnheader", { name: heading })).toBeTruthy(); + } + expect(table?.querySelector("tfoot")).toBeTruthy(); + expect(screen.getAllByTestId("task-summary-token-row")).toHaveLength(2); + + const css = readDashboardStylesSource(); + expect(css).toMatch(/\.task-summary-tab\s*\{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/s); + expect(css).toMatch(/\.task-summary-section\s*\{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;/s); + expect(css).toMatch(/\.task-summary-token-table-wrap\s*\{[^}]*width:\s*100%;[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow-x:\s*auto;/s); + + view.rerender(); + expect(screen.getAllByTestId("task-summary-token-row")).toHaveLength(1); + expect(screen.getByTestId("task-summary-token-cost-section").querySelector("table.task-summary-token-table")).toBeTruthy(); + + view.rerender(); + expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull(); + }); + it("renders multi-model token counts with priced, unpriced, and unavailable total cost states", () => { render( { expect(mobileTokenBlock).not.toContain(".task-summary-token-table-wrap"); expect(mobileTokenBlock).not.toContain("overflow-x: visible"); expect(mobileTokenBlock).not.toContain(".task-summary-token-table td::before"); - expect(mobileTokenBlock).not.toContain("min-width: 0"); + expect(tokenTableRule).not.toContain("min-width: 0"); expect(css).toContain("var(--color-warning)"); expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/); });