FN-8679: contain mobile Summary table overflow

Keep Summary token and cost tables horizontally scrollable within mobile task details.

- Constrain Summary flex ancestors so wide tables cannot widen the detail body
- Preserve real token/cost table rendering across populated and empty usage states
- Add a patch changeset for the mobile overflow fix

Files changed:
 .changeset/fn-8679-mobile-token-scroll.md          |  7 +++
 .../dashboard/app/components/TaskDetailModal.css   |  9 ++++
 .../__tests__/TaskDetailModal.summary-tab.test.tsx | 60 +++++++++++++++++++++-
 3 files changed, 75 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8679

Fusion-Task-Lineage: 33562f89-053f-4681-b1b1-98aa2f2704a2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-01 01:28:38 -07:00
parent 4c2369ed99
commit 5786c87eff
3 changed files with 75 additions and 1 deletions

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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(<TaskSummaryTab task={doneTask({ tokenUsage: twoModelUsage })} />);
const section = screen.getByTestId("task-summary-token-cost-section");
const wrapper = section.querySelector<HTMLElement>(".task-summary-token-table-wrap");
const table = wrapper?.querySelector<HTMLTableElement>("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(<TaskSummaryTab task={doneTask({ tokenUsage: { ...twoModelUsage, perModel: [twoModelUsage.perModel![0]] } })} />);
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(<TaskSummaryTab task={doneTask({ tokenUsage: undefined })} />);
expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull();
});
it("renders multi-model token counts with priced, unpriced, and unavailable total cost states", () => {
render(
<TaskSummaryTab
@@ -450,7 +508,7 @@ describe("TaskDetailModal Summary tab", () => {
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}/);
});