diff --git a/.changeset/mobile-chat-modal-chrome.md b/.changeset/mobile-chat-modal-chrome.md new file mode 100644 index 0000000000..6500c8dcde --- /dev/null +++ b/.changeset/mobile-chat-modal-chrome.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Polish mobile workflow header alignment, task chat provider icons, modal overlay chrome, and shadcn font consistency. diff --git a/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts b/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts index bbec23ce5b..3b8a6a06b8 100644 --- a/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts +++ b/packages/dashboard/app/__tests__/shadcn-gray-theme.test.ts @@ -43,6 +43,30 @@ describe("Shadcn Gray color theme", () => { expect(dividerBlock).not.toContain("border-left-color"); }); + it("pins shadcn-family UI controls to one font family while preserving mono content", () => { + const uiFontBlock = extractGroupedRuleBlock(themeData, '[data-color-theme^="shadcn"],'); + const monoFontBlock = extractGroupedRuleBlock(themeData, '[data-color-theme^="shadcn"] code'); + + expect(uiFontBlock).toContain('[data-color-theme^="shadcn"] button'); + expect(uiFontBlock).toContain('[data-color-theme^="shadcn"] input'); + expect(uiFontBlock).toContain('[data-color-theme^="shadcn"] select'); + expect(uiFontBlock).toContain('[data-color-theme^="shadcn"] textarea'); + expect(uiFontBlock).toContain('[data-color-theme^="shadcn"] .modal'); + expect(uiFontBlock).toContain("font-family: var(--font-primary);"); + expect(monoFontBlock).toContain('[data-color-theme^="shadcn"] pre'); + expect(monoFontBlock).toContain('[data-color-theme^="shadcn"] .font-mono'); + expect(monoFontBlock).toContain("font-family: var(--font-mono);"); + }); + + it("keeps glass theme modal overlays transparent and non-blurring", () => { + const glassModalOverlayBlock = extractSelectorBlock(themeData, '[data-color-theme="glass"] .modal-overlay'); + + expect(glassModalOverlayBlock).toContain("background: transparent;"); + expect(glassModalOverlayBlock).toContain("backdrop-filter: none;"); + expect(glassModalOverlayBlock).toContain("-webkit-backdrop-filter: none;"); + expect(glassModalOverlayBlock).not.toContain("blur("); + }); + it("registers Shadcn Gray in core, dashboard options, and the dashboard bootstrap validator", () => { expect(CORE_COLOR_THEMES).toContain("shadcn-gray"); expect(DASHBOARD_COLOR_THEMES).toContainEqual({ diff --git a/packages/dashboard/app/__tests__/styles-css-rgba-tokenization.test.ts b/packages/dashboard/app/__tests__/styles-css-rgba-tokenization.test.ts index e3a4c7a78b..24f25c7150 100644 --- a/packages/dashboard/app/__tests__/styles-css-rgba-tokenization.test.ts +++ b/packages/dashboard/app/__tests__/styles-css-rgba-tokenization.test.ts @@ -7,21 +7,17 @@ interface SelectorExpectation { } const convertedSelectors: SelectorExpectation[] = [ - { - selector: ".modal-overlay", - expectedColorMix: "color-mix(in srgb, var(--text) 60%, transparent)", - }, { selector: ".modal-header", - expectedColorMix: "color-mix(in srgb, var(--text) 10%, transparent)", + expectedColorMix: "color-mix(in srgb, var(--surface) 80%, transparent)", }, { selector: ".modal-actions", - expectedColorMix: "color-mix(in srgb, var(--text) 5%, transparent)", + expectedColorMix: "color-mix(in srgb, var(--surface) 60%, transparent)", }, { selector: ".settings-sidebar", - expectedColorMix: "color-mix(in srgb, var(--text) 10%, transparent)", + expectedColorMix: "color-mix(in srgb, var(--surface) 60%, transparent)", }, { selector: ".step-progress-segment[data-tooltip]:hover::after", @@ -106,6 +102,18 @@ describe("styles.css rgba tokenization", () => { expect(nonTokenRgbaLines(loadStylesCss())).toEqual([]); }); + it("keeps shared modal overlays visually transparent while panels provide shadow depth", () => { + const css = loadStylesCss(); + const overlayBlock = extractSelectorBlocks(css, ".modal-overlay").at(0) ?? ""; + const modalBlock = extractSelectorBlocks(css, ".modal").at(0) ?? ""; + + expect(overlayBlock).toContain("background: transparent;"); + expect(overlayBlock).toContain("backdrop-filter: none;"); + expect(overlayBlock).toContain("-webkit-backdrop-filter: none;"); + expect(overlayBlock).not.toContain("color-mix(in srgb, var(--text)"); + expect(modalBlock).toContain("box-shadow: var(--shadow-lg);"); + }); + it("keeps converted selectors on tokenized color-mix() values", () => { const css = loadStylesCss(); diff --git a/packages/dashboard/app/components/Header.css b/packages/dashboard/app/components/Header.css index e34178828c..b7e9ad85b8 100644 --- a/packages/dashboard/app/components/Header.css +++ b/packages/dashboard/app/components/Header.css @@ -134,12 +134,20 @@ non-notched devices, so this is a no-op there. Pair with viewport-fit=cover (ind /* FNXC:WorkflowControls 2026-06-23-20:05: Mobile places the workflow dropdown in the top header beside the logo/project switch. Match the 32px board/list toggle height, reduce row gaps, hide the text label/counts, and cap width aggressively so resizing does not crowd adjacent header controls. + + FNXC:WorkflowControls 2026-06-23-20:58: + In narrow mobile board/list views the workflow selector should sit centered in the available space between the project dropdown and the board/list toggle, while the trigger height lines up exactly with the 32px view toggle. Let the slot flex across the remaining header row and clamp the trigger to a wider 104px-128px range so short names such as "Coding" render fully when the screen can support it. + + FNXC:WorkflowControls 2026-06-23-21:12: + The mobile workflow dropdown must visually match the board/list segmented toggle height exactly. Pin the toolbar wrapper, switcher, and trigger to the same 32px border-box and remove inherited vertical padding/line-height drift so the top and bottom edges align pixel-for-pixel. */ .header { - gap: var(--space-xs); + gap: var(--space-sm); + align-items: center; } .header-left { + flex: 1 1 auto; min-width: 0; gap: var(--space-xs); } @@ -150,24 +158,35 @@ non-notched devices, so this is a no-op there. Pair with viewport-fit=cover (ind } .header-workflow-slot { - flex: 0 1 min(30vw, calc(var(--space-2xl) * 2.875)); - justify-content: flex-start; - min-width: calc(var(--space-2xl) * 2); - max-width: min(30vw, calc(var(--space-2xl) * 2.875)); + flex: 1 1 auto; + justify-content: center; + min-width: 0; + max-width: none; } .header-actions { - gap: var(--space-xs); + flex: 0 0 auto; + align-items: center; + gap: var(--space-sm); } .header-workflow-slot:empty { display: none; } - .header-workflow-slot .workflow-switcher { - width: 100%; + .header-workflow-slot .board-workflow-toolbar, + .header-workflow-slot .list-workflow-control { height: 32px; + align-items: center; + } + + .header-workflow-slot .workflow-switcher { + width: clamp(calc(var(--space-2xl) * 3.25), 36vw, calc(var(--space-2xl) * 4)); + height: 32px; + max-height: 32px; min-width: 0; + flex: 0 1 auto; + align-items: center; } .header-workflow-slot .workflow-switcher-label { @@ -175,13 +194,17 @@ non-notched devices, so this is a no-op there. Pair with viewport-fit=cover (ind } .header-workflow-slot .workflow-switcher-trigger { + appearance: none; box-sizing: border-box; width: 100%; height: 32px; max-width: 100%; min-height: 32px; + max-height: 32px; padding: 0 var(--space-xs); font-size: 12px; + line-height: 1; + overflow: hidden; } .header-workflow-slot .workflow-switcher-trigger-main { diff --git a/packages/dashboard/app/components/ListView.css b/packages/dashboard/app/components/ListView.css index f9847582cf..c8d4a01a55 100644 --- a/packages/dashboard/app/components/ListView.css +++ b/packages/dashboard/app/components/ListView.css @@ -19,7 +19,8 @@ /* FNXC:ListView 2026-06-23-23:42: No divider between the controls and quick-add. The controls row now carries action groups only; the aggregate top task count was removed while contextual section counts remain lower in the list. FNXC:ListView 2026-06-23-23:55: Bulk Edit, View, and New Task are one no-wrap action cluster so the primary list actions stay visually together instead of splitting across opposite edges or separate wrapped lines. -FNXC:ListView 2026-06-23-00:25: The primary action cluster must stay on one physical row even when the list pane narrows; preserve max-content width and let the cluster scroll horizontally instead of wrapping individual actions onto separate lines. */ +FNXC:ListView 2026-06-23-00:25: The primary action cluster must stay on one physical row even when the list pane narrows; preserve max-content width and let the cluster scroll horizontally instead of wrapping individual actions onto separate lines. +FNXC:ListView 2026-06-23-21:42: Center the Bulk Edit, View, and New Task cluster in both desktop sidebar controls and the mobile list toolbar so the primary actions read as a single balanced control group. */ .list-sidebar-controls { display: flex; flex-direction: column; @@ -32,7 +33,7 @@ FNXC:ListView 2026-06-23-00:25: The primary action cluster must stay on one phys .list-sidebar-controls__toolbar { display: flex; align-items: center; - justify-content: flex-start; + justify-content: center; gap: var(--space-sm); flex-wrap: wrap; } @@ -41,7 +42,7 @@ FNXC:ListView 2026-06-23-00:25: The primary action cluster must stay on one phys .list-sidebar-controls__actions { display: flex; align-items: center; - justify-content: flex-start; + justify-content: center; flex: 0 1 auto; flex-wrap: nowrap; gap: var(--space-xs); @@ -945,6 +946,7 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h .list-toolbar { padding: var(--space-md); flex-wrap: wrap; + justify-content: center; } .list-create-area { @@ -1153,6 +1155,11 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h padding: var(--space-sm) var(--space-md); flex-wrap: wrap; gap: var(--space-sm); + justify-content: center; + } + + .list-toolbar .list-action-cluster { + justify-content: center; } .list-selection-stats { diff --git a/packages/dashboard/app/components/ScriptsModal.css b/packages/dashboard/app/components/ScriptsModal.css index abb8380888..2d10aa92cb 100644 --- a/packages/dashboard/app/components/ScriptsModal.css +++ b/packages/dashboard/app/components/ScriptsModal.css @@ -1965,10 +1965,14 @@ queries already apply (the container-query rule above also fires and produces th /* FNXC:GitManager 2026-06-23-23:52: Sidebar-launched floating modals should not dim, blur, or block the app behind them. Match the Files/RightDock floating-window model: the overlay is transparent and click-through, while the Git Manager panel remains interactive. Dismissal stays on Escape/close button instead of backdrop click. + +FNXC:GitManager 2026-06-23-21:28: +Theme-level modal backdrop rules can load after component CSS and reapply dim/blur, especially in glass themes. Use a higher-specificity modal overlay selector and clear both standard and WebKit backdrop filters so Git Manager never darkens the app behind it. */ -.modal-overlay.git-manager-modal-overlay { +.modal-overlay.git-manager-modal-overlay.git-manager-modal-overlay { background: transparent; backdrop-filter: none; + -webkit-backdrop-filter: none; pointer-events: none; } diff --git a/packages/dashboard/app/components/TaskChatTab.css b/packages/dashboard/app/components/TaskChatTab.css index 7ca5a27319..5a049e14db 100644 --- a/packages/dashboard/app/components/TaskChatTab.css +++ b/packages/dashboard/app/components/TaskChatTab.css @@ -128,6 +128,24 @@ FN-6425 requires the chat expand control to stay inside the chat view as an icon flex: 0 0 auto; } +.task-chat-provider-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 36px; + width: 36px; + height: 36px; + border-radius: var(--radius); + background: color-mix(in srgb, var(--provider-icon-color, currentColor) 8%, transparent); + color: var(--text-muted); +} + +.task-chat-provider-icon .provider-icon { + display: inline-flex; + align-items: center; + justify-content: center; +} + .task-chat-role-label { color: var(--text); font-weight: 600; diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx index ec659f81d0..7d6ad1c702 100644 --- a/packages/dashboard/app/components/TaskChatTab.tsx +++ b/packages/dashboard/app/components/TaskChatTab.tsx @@ -12,6 +12,7 @@ import { getErrorMessage } from "@fusion/core"; import { linkifyFilePaths } from "../utils/filePathLinkify"; import { formatRelativeTimeAgo } from "../utils/relativeTimeAgo"; import { AgentAvatar } from "./AgentAvatar"; +import { ProviderIcon } from "./ProviderIcon"; import { clampChatInputHeight, resolveChatInputOverflowY } from "../utils/chatInputAutosize"; import { markdownComponents } from "./AgentLogViewer"; import "./TaskChatTab.css"; @@ -29,6 +30,11 @@ interface TaskChatTabProps { type AgentLogRole = AgentRole | undefined; +type TaskChatModelInfo = { + provider: string; + modelId?: string; +}; + type UserChatMessage = Pick & { optimistic?: boolean }; type TaskChatTranscriptItem = @@ -97,6 +103,67 @@ function getRoleIcon(role: AgentLogRole): string | undefined { } } +function parseModelMarker(entry: AgentLogEntry): TaskChatModelInfo | null { + if (entry.type !== "text") return null; + const match = entry.text.match(/^(?:Triage|Executor|Reviewer) using model: (.+?)\/(.+)$/); + if (!match) return null; + return { provider: match[1], modelId: match[2] }; +} + +function makeModelInfo(provider: string | undefined, modelId: string | undefined): TaskChatModelInfo | null { + if (!provider) return null; + return modelId ? { provider, modelId } : { provider }; +} + +function getExplicitModelForRole(task: Task | TaskDetail, role: AgentLogRole): TaskChatModelInfo | null { + if (role === "triage" && task.planningModelProvider) { + return makeModelInfo(task.planningModelProvider, task.planningModelId); + } + if (role === "executor" && task.modelProvider) { + return makeModelInfo(task.modelProvider, task.modelId); + } + if ((role === "reviewer" || role === "merger") && task.validatorModelProvider) { + return makeModelInfo(task.validatorModelProvider, task.validatorModelId); + } + return null; +} + +function getRuntimeModelForRole(entries: readonly AgentLogEntry[], role: AgentLogRole): TaskChatModelInfo | null { + for (let index = entries.length - 1; index >= 0; index -= 1) { + const entry = entries[index]; + if (entry.agent !== role) continue; + const parsed = parseModelMarker(entry); + if (parsed) return parsed; + } + return null; +} + +function getModelForRole(task: Task | TaskDetail, role: AgentLogRole, entries: readonly AgentLogEntry[]): TaskChatModelInfo | null { + /* + FNXC:TaskDetailChat 2026-06-23-21:18: + Task-detail chat agent headers should identify the AI provider actually backing each role, not a generic/role avatar. Prefer explicit task model overrides because they are stable before logs stream, then fall back to the runtime "using model" log marker emitted by active planner/executor/reviewer sessions. Merger output uses the validator/reviewer lane provider because merge-fix/review flows share that model family in the UI. + */ + return getExplicitModelForRole(task, role) ?? getRuntimeModelForRole(entries, role); +} + +function TaskChatAgentIcon({ label, modelInfo, role }: { label: string; modelInfo: TaskChatModelInfo | null; role: AgentLogRole }) { + if (modelInfo?.provider) { + const title = modelInfo.modelId ? `${label}: ${modelInfo.provider}/${modelInfo.modelId}` : `${label}: ${modelInfo.provider}`; + return ( + + + + ); + } + + const avatarAgent = { + id: role ?? "agent", + name: label, + icon: getRoleIcon(role), + }; + return ; +} + function getEntryKey(entry: AgentLogEntry, index: number): string { return [entry.taskId, entry.timestamp, entry.agent ?? "agent", entry.type, index].join(":"); } @@ -810,18 +877,14 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on return ; } - const avatarAgent = { - id: item.role ?? "agent", - name: item.label, - icon: getRoleIcon(item.role), - }; const segments = segmentGroupEntries(item.entries); const latestEntryTimestamp = item.entries[item.entries.length - 1]?.timestamp ?? ""; const relativeTime = formatRelativeTimeAgo(latestEntryTimestamp); + const modelInfo = getModelForRole(task, item.role, item.entries); return (
- +
{item.label}
diff --git a/packages/dashboard/app/components/TerminalModal.css b/packages/dashboard/app/components/TerminalModal.css index 72219fbfd2..8b92177305 100644 --- a/packages/dashboard/app/components/TerminalModal.css +++ b/packages/dashboard/app/components/TerminalModal.css @@ -37,19 +37,24 @@ The override MUST out-specify the base `.modal-overlay` (which sets a dimmed bac /* FNXC:Terminal 2026-06-23-04:30: The terminal must NEVER dim or blur the page behind it, in ANY mode. The base .modal-overlay applies a dimmed background + blur; the docked/floating overrides killed it, but a terminal that is NEITHER (the mobile/default sheet) still fell back to the dim. This base terminal-overlay rule removes the dim+blur for every terminal state. + +FNXC:Terminal 2026-06-23-21:28: +Theme-level modal backdrop rules can load after component CSS and reapply dim/blur, especially in glass themes. Use a higher-specificity terminal overlay selector and clear both standard and WebKit backdrop filters so docked and modal terminal surfaces never darken the app behind them. */ -.modal-overlay.terminal-modal-overlay { +.modal-overlay.terminal-modal-overlay.terminal-modal-overlay { background: transparent; backdrop-filter: none; + -webkit-backdrop-filter: none; } -.modal-overlay.terminal-modal-overlay--docked, -.modal-overlay.terminal-modal-overlay--floating { +.modal-overlay.terminal-modal-overlay--docked.terminal-modal-overlay--docked, +.modal-overlay.terminal-modal-overlay--floating.terminal-modal-overlay--floating { align-items: stretch; justify-content: flex-end; padding: 0; background: transparent; backdrop-filter: none; + -webkit-backdrop-filter: none; pointer-events: none; } diff --git a/packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx b/packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx index c2a62a17f0..9028f8049b 100644 --- a/packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx @@ -315,11 +315,12 @@ describe("GitManagerModal", () => { it("keeps the sidebar-launched Git Manager overlay transparent and click-through like Files", () => { const css = loadAllAppCss(); - const overlayRule = css.match(/\.modal-overlay\.git-manager-modal-overlay\s*\{([^}]*)\}/)?.[1] ?? ""; + const overlayRule = css.match(/\.modal-overlay\.git-manager-modal-overlay\.git-manager-modal-overlay\s*\{([^}]*)\}/)?.[1] ?? ""; const panelRule = css.match(/\.modal\.gm-modal\s*\{([^}]*)\}/)?.[1] ?? ""; expect(overlayRule).toContain("background: transparent"); expect(overlayRule).toContain("backdrop-filter: none"); + expect(overlayRule).toContain("-webkit-backdrop-filter: none"); expect(overlayRule).toContain("pointer-events: none"); expect(panelRule).toContain("pointer-events: auto"); }); diff --git a/packages/dashboard/app/components/__tests__/Header.css.test.ts b/packages/dashboard/app/components/__tests__/Header.css.test.ts index da52f16ff7..4bec5e3d16 100644 --- a/packages/dashboard/app/components/__tests__/Header.css.test.ts +++ b/packages/dashboard/app/components/__tests__/Header.css.test.ts @@ -32,7 +32,11 @@ describe("Header CSS", () => { }); it("compacts the workflow portal in the mobile top header", () => { - expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot\s*\{[^}]*max-width:\s*min\(42vw,\s*calc\(var\(--space-2xl\) \* 4\)\);/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot\s*\{[^}]*flex:\s*1 1 auto;[^}]*justify-content:\s*center;[^}]*max-width:\s*none;/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-actions\s*\{[^}]*flex:\s*0 0 auto;[^}]*align-items:\s*center;[^}]*gap:\s*var\(--space-sm\);/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot \.board-workflow-toolbar,\s*\n\s*\.header-workflow-slot \.list-workflow-control\s*\{[^}]*height:\s*32px;[^}]*align-items:\s*center;/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot \.workflow-switcher\s*\{[^}]*width:\s*clamp\(calc\(var\(--space-2xl\) \* 3\.25\),\s*36vw,\s*calc\(var\(--space-2xl\) \* 4\)\);[^}]*height:\s*32px;[^}]*max-height:\s*32px;[^}]*align-items:\s*center;/); + expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot \.workflow-switcher-trigger\s*\{[^}]*appearance:\s*none;[^}]*height:\s*32px;[^}]*min-height:\s*32px;[^}]*max-height:\s*32px;[^}]*line-height:\s*1;[^}]*overflow:\s*hidden;/); expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot \.workflow-switcher-label\s*\{[^}]*display:\s*none;/); expect(css).toMatch(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.header-workflow-slot \.workflow-switcher-counts\s*\{[^}]*display:\s*none;/); }); diff --git a/packages/dashboard/app/components/__tests__/ListView.test.tsx b/packages/dashboard/app/components/__tests__/ListView.test.tsx index d8145d2cd6..9cd7f039e5 100644 --- a/packages/dashboard/app/components/__tests__/ListView.test.tsx +++ b/packages/dashboard/app/components/__tests__/ListView.test.tsx @@ -1597,11 +1597,16 @@ describe("ListView", () => { it("keeps the primary list action cluster on one physical row when the pane narrows", () => { const css = readFileSync("app/components/ListView.css", "utf8"); const actionClusterRule = css.match(/\.list-action-cluster,\s*\n\.list-sidebar-controls__actions\s*\{[^}]*\}/)?.[0] ?? ""; + const toolbarRule = css.match(/\.list-sidebar-controls__toolbar\s*\{[^}]*\}/)?.[0] ?? ""; + const mobileToolbarRule = css.match(/@media\s*\(max-width:\s*768px\)[\s\S]*?\.list-toolbar\s*\{[^}]*padding:\s*var\(--space-sm\) var\(--space-md\);[^}]*\}/)?.[0] ?? ""; expect(actionClusterRule).toContain("flex-wrap: nowrap"); + expect(actionClusterRule).toContain("justify-content: center"); expect(actionClusterRule).toContain("inline-size: max-content"); expect(actionClusterRule).toContain("min-width: max-content"); expect(actionClusterRule).toContain("overflow-x: auto"); + expect(toolbarRule).toContain("justify-content: center"); + expect(mobileToolbarRule).toContain("justify-content: center"); }); it("keeps Bulk Edit, View, and + New Task together in the mobile toolbar controls", () => { diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx index 15443690bd..c266e4aea5 100644 --- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx @@ -425,6 +425,48 @@ describe("TaskChatTab", () => { expect(screen.getByText("legacy output")).toBeTruthy(); }); + it("renders provider icons for task chat roles from task model overrides", () => { + mockLogs([ + makeEntry({ agent: "triage", text: "planning output" }), + makeEntry({ agent: "executor", text: "executor output" }), + makeEntry({ agent: "reviewer", text: "reviewer output" }), + makeEntry({ agent: "merger", text: "merger output" }), + ]); + + render( + , + ); + + expect(document.querySelector(".task-chat-provider-icon [data-provider='google']")).toBeTruthy(); + expect(document.querySelector(".task-chat-provider-icon [data-provider='openai']")).toBeTruthy(); + expect(document.querySelectorAll(".task-chat-provider-icon [data-provider='anthropic']")).toHaveLength(2); + }); + + it("renders provider icons for task chat roles from runtime model markers", () => { + mockLogs([ + makeEntry({ agent: "triage", text: "Triage using model: google/gemini-pro" }), + makeEntry({ agent: "executor", text: "Executor using model: openai/gpt-4o" }), + makeEntry({ agent: "reviewer", text: "Reviewer using model: anthropic/claude-sonnet-4-5" }), + ]); + + render(); + + expect(document.querySelector(".task-chat-provider-icon [data-provider='google']")).toBeTruthy(); + expect(document.querySelector(".task-chat-provider-icon [data-provider='openai']")).toBeTruthy(); + expect(document.querySelector(".task-chat-provider-icon [data-provider='anthropic']")).toBeTruthy(); + }); + it("groups consecutive entries by agent role", () => { mockLogs([ makeEntry({ agent: "executor", text: "first" }), diff --git a/packages/dashboard/app/public/theme-data.css b/packages/dashboard/app/public/theme-data.css index e733b3b64a..1ebaecb934 100644 --- a/packages/dashboard/app/public/theme-data.css +++ b/packages/dashboard/app/public/theme-data.css @@ -477,6 +477,33 @@ Shadcn-family themes should read as seamless app shells: header and modal-title border-bottom-color: transparent; } +/* +FNXC:DashboardTheming 2026-06-23-00:07: +All shadcn-family themes should use one UI font family across app chrome, forms, popovers, and buttons. Native controls do not reliably inherit the document font in every browser, so pin shadcn UI controls to --font-primary while preserving --font-mono for code-oriented text. +*/ +[data-color-theme^="shadcn"], +[data-color-theme^="shadcn"] body, +[data-color-theme^="shadcn"] button, +[data-color-theme^="shadcn"] input, +[data-color-theme^="shadcn"] select, +[data-color-theme^="shadcn"] textarea, +[data-color-theme^="shadcn"] optgroup, +[data-color-theme^="shadcn"] .btn, +[data-color-theme^="shadcn"] .input, +[data-color-theme^="shadcn"] .select, +[data-color-theme^="shadcn"] .modal, +[data-color-theme^="shadcn"] .card { + font-family: var(--font-primary); +} + +[data-color-theme^="shadcn"] code, +[data-color-theme^="shadcn"] pre, +[data-color-theme^="shadcn"] kbd, +[data-color-theme^="shadcn"] samp, +[data-color-theme^="shadcn"] .font-mono { + font-family: var(--font-mono); +} + /* GRAPHITE - Dark graphite blacks */ [data-color-theme="graphite"] { --surface-hover: color-mix(in srgb, var(--surface) 90%, var(--text) 10%); @@ -5619,8 +5646,13 @@ body[data-color-theme="terminal"][data-theme="light"]::before { } [data-color-theme="glass"] .modal-overlay { - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); + /* + FNXC:ModalChrome 2026-06-23-21:36: + Even glass-style themes should keep modal overlays non-dimming/non-blurring; modal panels provide depth with shadows and their own translucent surfaces. + */ + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; } /* HORIZON - Warm sunset-inspired palette */ diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index f28395b9de..73eb2da2e8 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -639,11 +639,10 @@ html .column.drag-over * { transition: none !important; } -[data-theme="light"] .modal-overlay, [data-theme="light"] .agent-detail-overlay, [data-theme="light"] .agent-dialog-overlay, [data-theme="light"] .chat-new-dialog-backdrop { - background: color-mix(in srgb, var(--text) 50%, transparent); + background: transparent; } [data-theme="light"] .modal-header { @@ -1205,12 +1204,17 @@ body { /* === Modals === */ +/* +FNXC:ModalChrome 2026-06-23-21:36: +Modal and dock pop-out surfaces should not darken or blur the app behind them. Keep the overlay for positioning, stacking, and click/escape dismissal, but make it visually transparent; panel depth comes from each `.modal` box-shadow instead of a dimmed backdrop. This shared contract covers Git Manager, Terminal, Dev Server, Secrets, and future sidebar/tool modals without per-modal overrides. +*/ .modal-overlay { display: none; position: fixed; inset: 0; - background: color-mix(in srgb, var(--text) 60%, transparent); - backdrop-filter: blur(4px); + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; /* Must remain above sticky top banners (e.g. onboarding/session resume cards). */ z-index: 100; justify-content: center;