From b5e366da6293aff2bcd80968d68172ccc8210c73 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Fri, 21 Aug 2026 01:32:48 +0000 Subject: [PATCH] FN-099: match task Chat model menu width to Direct Chat Match the task Chat model selector to Direct Chat sizing so provider and model names remain readable across desktop and mobile. - Apply the shared readable, viewport-clamped dropdown width to Task Chat. - Add regression coverage and desktop/mobile screenshots. - Document the behavior and publish a patch changeset. Files changed: .changeset/fn-099-task-chat-model-menu-width.md | 7 ++ docs/dashboard-guide.md | 2 +- .../app/components/TaskPlannerChatTab.tsx | 5 ++ .../ChatThinkingLevelControl.portal.test.tsx | 3 +- .../__tests__/TaskPlannerChatTab.test.tsx | 92 ++++++++++++++++++++- .../fn-099-task-chat-model-menu-desktop.png | Bin 0 -> 9534 bytes screenshots/fn-099-task-chat-model-menu-mobile.png | Bin 0 -> 7240 bytes 7 files changed, 105 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-099 Fusion-Task-Lineage: fec67d58-9ca9-4878-8a0a-ac83984a3766 Co-authored-by: Fusion --- .../fn-099-task-chat-model-menu-width.md | 7 ++ docs/dashboard-guide.md | 2 +- .../app/components/TaskPlannerChatTab.tsx | 5 + .../ChatThinkingLevelControl.portal.test.tsx | 3 +- .../__tests__/TaskPlannerChatTab.test.tsx | 92 +++++++++++++++++- .../fn-099-task-chat-model-menu-desktop.png | Bin 0 -> 9534 bytes .../fn-099-task-chat-model-menu-mobile.png | Bin 0 -> 7240 bytes 7 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 .changeset/fn-099-task-chat-model-menu-width.md create mode 100644 screenshots/fn-099-task-chat-model-menu-desktop.png create mode 100644 screenshots/fn-099-task-chat-model-menu-mobile.png diff --git a/.changeset/fn-099-task-chat-model-menu-width.md b/.changeset/fn-099-task-chat-model-menu-width.md new file mode 100644 index 0000000000..d96184799d --- /dev/null +++ b/.changeset/fn-099-task-chat-model-menu-width.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep task Chat model names readable by widening its selector menu to match Direct Chat. +category: fix +dev: Reuses the shared readable, viewport-clamped CustomModelDropdown width mode. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index b8a5d199a6..4052c61453 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -248,7 +248,7 @@ On mobile viewports, the Right Dock never renders. The compact Header actions an ## Task-detail Chat -Task-detail **Chat** uses the project’s configured Direct Chat default model and thinking level rather than the task’s planning model. It remains task-aware: the server builds the task definition, dependencies, activity, metrics, steering, and refinement context, and the existing `task-planner:` session keeps one transcript per task. Task context always comes from the selected project’s authoritative store, including while that project’s engine has not started or is unavailable; another project’s task with the same ID cannot supply its context. The composer exposes the same model and thinking controls as Direct Chat; model choices remain model-targeted and do not replace the synthetic task-scoped permission contract. Changing the project default does not hide history, and the next explicit send applies the current target to the existing idle session. There is no separate planner-model lane for this conversation. +Task-detail **Chat** uses the project’s configured Direct Chat default model and thinking level rather than the task’s planning model. It remains task-aware: the server builds the task definition, dependencies, activity, metrics, steering, and refinement context, and the existing `task-planner:` session keeps one transcript per task. Task context always comes from the selected project’s authoritative store, including while that project’s engine has not started or is unavailable; another project’s task with the same ID cannot supply its context. The composer exposes the same model and thinking controls as Direct Chat; its compact model trigger opens the same readable, viewport-clamped menu on desktop and mobile. Model choices remain model-targeted and do not replace the synthetic task-scoped permission contract. Changing the project default does not hide history, and the next explicit send applies the current target to the existing idle session. There is no separate planner-model lane for this conversation. ## Chat message editing and rewind diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.tsx b/packages/dashboard/app/components/TaskPlannerChatTab.tsx index 088cea1149..bb3d504037 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.tsx +++ b/packages/dashboard/app/components/TaskPlannerChatTab.tsx @@ -1592,6 +1592,11 @@ export function TaskPlannerChatTab({ task, columnFlags, projectId, active, expan onChange={(value) => void handleTaskChatModelChange(value)} placeholder={t("model.selectPlaceholder", "Select a model…")} defaultOptionLabel={t("models.useDefault", "Use project default")} + /* + FNXC:TaskChatModelMenu 2026-08-21-01:12: + Task Chat keeps its compact composer trigger, but long provider/model names need Direct Chat's readable, viewport-clamped portaled menu on desktop and mobile. + */ + menuWidth="readable" favoriteProviders={favoriteProviders} favoriteModels={favoriteModels} disabled={queueActionPending || composerState === "sending"} diff --git a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.portal.test.tsx b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.portal.test.tsx index 9230a6e15c..091b01898c 100644 --- a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.portal.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.portal.test.tsx @@ -25,10 +25,11 @@ const openModelPortal = async () => { }; describe("ChatThinkingLevelControl with the real CustomModelDropdown portal", () => { - it("keeps the brain popup open for pointerdown inside the portaled model menu, then selects the model normally", async () => { + it("keeps the brain popup open for pointerdown inside the readable portaled model menu, then selects the model normally", async () => { const onChangeModel = vi.fn(); const portal = await openModelPortalWithRender({ onChangeModel }); + expect(portal).toHaveAttribute("data-menu-width", "readable"); fireEvent.pointerDown(portal); expect(screen.getByTestId("chat-thinking-popover")).toBeInTheDocument(); diff --git a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx index 9692c56b01..405428d854 100644 --- a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx @@ -13,7 +13,14 @@ const originalScrollTopDescriptor = Object.getOwnPropertyDescriptor(HTMLElement. const originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight"); const originalClientHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "clientHeight"); -const { mockEnsureTaskPlannerChatSession, mockFetchTaskPlannerChatSession, mockFetchChatSession, mockFetchChatMessages, mockFetchTaskDetail, mockStreamChatResponse, mockAttachChatStream, mockCancelChatResponse, mockAddSteeringComment, mockTranslations, mockT } = vi.hoisted(() => { +const mockModelCatalog = vi.hoisted(() => ({ + models: [ + { provider: "anthropic", id: "claude-plan", name: "Claude Plan", reasoning: true, contextWindow: 200000 }, + { provider: "enterprise-provider", id: "very-long-production-model", name: "Enterprise Production Model With A Readable Long Name", reasoning: true, contextWindow: 200000 }, + ], +})); + +const { mockEnsureTaskPlannerChatSession, mockFetchTaskPlannerChatSession, mockFetchChatSession, mockFetchChatMessages, mockFetchTaskDetail, mockUpdateChatSession, mockStreamChatResponse, mockAttachChatStream, mockCancelChatResponse, mockAddSteeringComment, mockTranslations, mockT } = vi.hoisted(() => { const translations = new Map(); return { mockEnsureTaskPlannerChatSession: vi.fn(), @@ -21,15 +28,28 @@ const { mockEnsureTaskPlannerChatSession, mockFetchTaskPlannerChatSession, mockF mockFetchChatSession: vi.fn(), mockFetchChatMessages: vi.fn(), mockFetchTaskDetail: vi.fn(), + mockUpdateChatSession: vi.fn(), mockStreamChatResponse: vi.fn(), mockAttachChatStream: vi.fn(), mockCancelChatResponse: vi.fn(), mockAddSteeringComment: vi.fn(), mockTranslations: translations, - mockT: (key: string, fallback: string) => translations.get(key) ?? fallback, + mockT: (key: string, fallback: string | { defaultValue?: string; defaultValue_one?: string; defaultValue_other?: string; count?: number }) => { + if (translations.has(key)) return translations.get(key)!; + if (typeof fallback === "string") return fallback; + return (fallback.count === 1 ? fallback.defaultValue_one : fallback.defaultValue_other) ?? fallback.defaultValue ?? key; + }, }; }); +vi.mock("../../hooks/useModelsCache", () => ({ + useModelsCache: () => ({ + models: mockModelCatalog.models, + favoriteProviders: [], + favoriteModels: [], + }), +})); + vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: mockT, @@ -45,6 +65,7 @@ vi.mock("../../api", async (importOriginal) => { fetchChatSession: mockFetchChatSession, fetchChatMessages: mockFetchChatMessages, fetchTaskDetail: mockFetchTaskDetail, + updateChatSession: mockUpdateChatSession, streamChatResponse: mockStreamChatResponse, attachChatStream: mockAttachChatStream, cancelChatResponse: mockCancelChatResponse, @@ -179,10 +200,15 @@ describe("TaskPlannerChatTab", () => { mockEnsureTaskPlannerChatSession.mockResolvedValue({ session: plannerSession }); mockFetchChatMessages.mockResolvedValue({ messages: [] }); mockFetchTaskDetail.mockResolvedValue(makeTask("FN-7310")); + mockUpdateChatSession.mockResolvedValue({ session: makePlannerSession() }); mockStreamChatResponse.mockReturnValue({ close: vi.fn(), isConnected: () => true }); mockAttachChatStream.mockReturnValue({ close: vi.fn(), isConnected: () => true }); mockCancelChatResponse.mockResolvedValue({ success: true, interrupted: false }); mockAddSteeringComment.mockResolvedValue(makeTask("FN-7310")); + mockModelCatalog.models = [ + { provider: "anthropic", id: "claude-plan", name: "Claude Plan", reasoning: true, contextWindow: 200000 }, + { provider: "enterprise-provider", id: "very-long-production-model", name: "Enterprise Production Model With A Readable Long Name", reasoning: true, contextWindow: 200000 }, + ]; }); afterEach(() => { @@ -248,6 +274,68 @@ describe("TaskPlannerChatTab", () => { ); }); + it("uses Direct Chat's readable portal for compact task-chat triggers and keeps long models searchable", async () => { + const user = userEvent.setup(); + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + vi.spyOn(window, "innerWidth", "get").mockReturnValue(1000); + Element.prototype.getBoundingClientRect = vi.fn(() => ({ + top: 100, left: 50, bottom: 136, width: 200, height: 36, right: 250, x: 50, y: 100, toJSON: () => ({}), + } as DOMRect)); + + try { + renderPlannerChat(); + await screen.findByTestId("task-planner-chat-empty"); + await user.click(screen.getByRole("button", { name: "Chat model" })); + const portal = await screen.findByTestId("model-combobox-portal"); + + expect(portal).toHaveAttribute("data-menu-width", "readable"); + expect(Number.parseFloat(portal.style.width)).toBeGreaterThan(200); + await user.type(within(portal).getByPlaceholderText("Filter models…"), "readable long"); + expect(within(portal).getByText("Enterprise Production Model With A Readable Long Name")).toBeInTheDocument(); + await user.click(within(portal).getByText("Enterprise Production Model With A Readable Long Name")); + await waitFor(() => expect(mockUpdateChatSession).toHaveBeenCalledWith( + "chat-planner", + expect.objectContaining({ modelProvider: "enterprise-provider", modelId: "very-long-production-model" }), + undefined, + )); + } finally { + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + } + }); + + it("keeps readable task-chat menus viewport-clamped for undefined selections and duplicate mobile catalogues", async () => { + const user = userEvent.setup(); + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + const originalVisualViewport = window.visualViewport; + Object.defineProperty(window, "visualViewport", { + configurable: true, + value: { width: 320, height: 640, offsetTop: 0, offsetLeft: 20, addEventListener: vi.fn(), removeEventListener: vi.fn() }, + }); + mockModelCatalog.models = [ + { provider: "anthropic", id: "claude-plan", name: "Claude Plan", reasoning: true, contextWindow: 200000 }, + { provider: "anthropic", id: "claude-plan-copy", name: "Claude Plan", reasoning: true, contextWindow: 200000 }, + ]; + Element.prototype.getBoundingClientRect = vi.fn(() => ({ + top: 100, left: 250, bottom: 136, width: 160, height: 36, right: 410, x: 250, y: 100, toJSON: () => ({}), + } as DOMRect)); + + try { + renderPlannerChat({ taskChatModel: {} }); + await screen.findByTestId("task-planner-chat-empty"); + await user.click(screen.getByRole("button", { name: "Chat model" })); + const portal = await screen.findByTestId("model-combobox-portal"); + const left = Number.parseFloat(portal.style.left); + const width = Number.parseFloat(portal.style.width); + + expect(portal).toHaveAttribute("data-menu-width", "readable"); + expect(left - 20).toBeGreaterThanOrEqual(16); + expect(left - 20 + width).toBeLessThanOrEqual(320 - 16); + } finally { + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; + Object.defineProperty(window, "visualViewport", { configurable: true, value: originalVisualViewport }); + } + }); + /* FNXC:ChatStreaming 2026-08-19-13:52: Task-detail Planner Chat must use the same shared Markdown anchor contract for both loaded history and an in-flight reattached response; this catches a renderer fork that would regress only task-bound Chat. diff --git a/screenshots/fn-099-task-chat-model-menu-desktop.png b/screenshots/fn-099-task-chat-model-menu-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..490bed53610439261c48ccf2e2a869803667330a GIT binary patch literal 9534 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

SiINi|3r+J|Io~z2Q>7EW0DNnEHVgV&Sjhib%m3 zzdy)v&Zw<=uziwXxY^6|m!S90c*&r3KD#erGRU&Oo->eFiagvV3Enl&+e%qJ7Ci?I zx(<|4Y9RSIW1<@<7=ma7VyX;T@wXlj4*dGXvp8uI*h}T*i1)%zXU|8Gw7$hdN2uoE zl;$}&0s$9pv?L+lVL4bZ`<@b(rd;5{PBgh>s-*a;>+IaZfRGDh|TExQ5ZvT_} ze}~UZEqfP^ZhxZ}bVX`uwXW_q)Qv_I;_OJZ%Zm&}F8>uqmHQhqNQuOb`WtlCqeCRe z%rG!AU*1I-ik+5q_}6cADY6oZdx_q0g(@8G>797)lsQv94;D=}gZep_{o@sDzs5%) z^n_x4X)iZh2F~~09?cbWOS{C7OOHHg!%Pb4+8)SFnK&o5*0TpPM*O|thbix?A&C}r z#VAf-K~Tmi8V>jLIR=AGXRRO~Li0@8Fh5LWq)uJEpVZgV=NJNh>Q?rin>;VVSZi4h zASlZyj>rykJH*uv_{+~N1NXR!cEM!Dcw!$}jkx}KAn1@a)e!|K$X7s$B7;UiEb8yt zhpj6iiTArr@`>}0u~`F0Y40&K&1HgfQ_g3MBM-=WbtR%ur_sR+XLX)PN_GUo1H<-+ zPgwN$P-XSvg-tIvGB}C478-f2TC@n1JDc+r-LANDwes%xq_oo6BIfL3;-8JZ>^O;P zD+UoS&J$&Yfj#CsfXNV7mjpQ%g&;S~p0Mhft#0toc!lzqqOhb0QTo%P!AYT8O=vof``Q3{95ktAWo&-&~o*d zr%xd5h$^a*W*echJX~_HvL~i~vryZ4TpiP51SkxiLC7S#JccJoIrKGw& zv0HNOm;$GiZMu*S*^dYkZY8EM8TOxMo<~;I0PZ-W#ok2Ej4J*SykpK+CZ|bGcFI3o z$UoBZ%>w?x9PAa%*5@)kA;0MXl%vPV)%)QRT;9GQA3H`10jIorup_#6_`=G2xNEFZ zi9d-QVPTblh2B$w1x4Goo?G~Gr7#aXEOx}_8pVacR5dt>?c9OeRTSNWeM&P$fXH1iKXT-Cib=Lko{|#Dd}oA&dc?al^kx!r zc`~GeLBQ2D2nhAh=0jWr{0H{vSGPzXtep47xE`^d_d(cSWlO}@1y7BJ+zai{AB&&dhtXzAdU8N;;y0i zFFiW|`@)=XaS_NAb@)=|I0RCl=*1;a{9;|xR9I+^mkY$EfLXW74Auo=b6Ctdik`WhR5*?j66CJGT66427mCb&{)Uldc;_fAK)}YtwG;@>O1I39 zz_MfvVw>4*-H$%x5Z0&w!0@&HTY$!_92(Em$|7L5-Y&v=(95*Dpqh!ap3IwqL{r~^ zs^8avdYW!v^SnMxM-$LHP)}%v&{BM0gT|9E?WN#Di~&v7f@&FHs1r{=oXoAPtUs3_ z&A^x;LDL23Mg8jGr|G7`7-fG#WKf%(3L3E6*E|Tb0&PkSYnhd9K{2bJSUbQL5|@$_qcXbF)2oX`O#{q zGvM_v6N^X!(D<>B1KrD3*4}`B+YAXQX-PUJa))5l1 zwq~Zy32CVJ2)keuy0|l>`c0eDuUAtkIB!eM&hQsn@jBtOTZ^phs>w}fL)HkpfbdiY zo<@8fDL0;Nsj9@g1nR484C!9)qH^Z`bS#7gklEkD)9Wi}9L3}z*N&>vp zrE>LgTsq9skqzBf+fILNV`0XhK1Z(?u-I2xSXLQxon6@V4c2#k6GV=p7)Oe`6&L4+ zPv}4R7Su%(rGO#w2IC5K~7 zPup9q@_G%GXKXus?=lVc;lj;|@&oP4d)v>nq(2y4DSE8Jtl;LFFJo4oa`=i6sjCsl z`wRrI@h)h#h9MIKAYsL`syw*kRLD#Z9+oOF^56L#d^)0!AmG}6^5c~m34ZTi*!=cF cJ|-g?zRSNceJ$byJOQxxL*L!SJHx;H2acqQaR2}S literal 0 HcmV?d00001 diff --git a/screenshots/fn-099-task-chat-model-menu-mobile.png b/screenshots/fn-099-task-chat-model-menu-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..95cdfae257b4c59c7581ba3af8831f4c6c5a09db GIT binary patch literal 7240 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 literal 0 HcmV?d00001