FN-049: apply Appearance settings immediately in embedded Settings

Mirror Appearance drafts into mounted dashboard surfaces immediately while keeping SettingsModal as the persistence owner.

- Add immediate setters for task routing, popup, cost badge, task-detail ordering, and chat layout settings.
- Wire embedded and overlay Settings controls through the App shell with regression coverage.
- Add the published-package fix changeset.

Files changed:
 .changeset/fn-049-live-dashboard-settings.md                  |  7 +++
 packages/dashboard/app/App.tsx                               | 17 +++++-
 packages/dashboard/app/components/AppModals.tsx              | 19 ++++++
 packages/dashboard/app/components/SettingsModal.tsx          | 31 ++++++++++
 packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx | 29 ++++++++++
 packages/dashboard/app/components/dashboard/MainContent.tsx   | 22 +++++++
 packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx | 67 +++++++++++++++++++++
 packages/dashboard/app/components/dashboard/types.ts         | 10 +++
 packages/dashboard/app/components/settings/sections/AppearanceSection.tsx | 52 +++++++++++++----
 packages/dashboard/app/components/settings/sections/__tests__/AppearanceSection.test.tsx | 41 ++++++++++++-
 packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts | 44 ++++++++++++++
 packages/dashboard/app/hooks/useAppSettings.ts                | 34 ++++++++++-
 packages/dashboard/app/task-modal-touch-resize-e2e-fixture.tsx |  2 +-
 13 files changed, 359 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-049

Fusion-Task-Lineage: 4140b47f-2f70-4c8b-ab47-2fc28672a2bb

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Fusion Agent
2026-08-19 18:31:42 +00:00
parent 67becd9fc2
commit da3c280672
13 changed files with 359 additions and 16 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Apply mounted Appearance settings immediately from either Settings view.
category: fix
dev: Mirrors chat layout, task routing, popup, cost badge, and task-detail ordering drafts into the App shell while SettingsModal remains the sole persistence writer.

View File

@@ -878,6 +878,11 @@ function AppInner() {
goalsEnabled, goalsEnabled,
setQuickChatButtonModeImmediate, setQuickChatButtonModeImmediate,
setChatMessageLayoutImmediate, setChatMessageLayoutImmediate,
setOpenTasksInRightSidebarImmediate,
setOpenMobileTasksInPopupImmediate,
setTaskPopupsBoardListOnlyImmediate,
setShowCostBadgeOnCardsImmediate,
setTaskDetailChatFirstImmediate,
setMobileNavPrimaryItemsImmediate, setMobileNavPrimaryItemsImmediate,
toggleAutoMerge, toggleAutoMerge,
togglePlanAutoApprove, togglePlanAutoApprove,
@@ -1666,6 +1671,12 @@ function AppInner() {
setShadcnCustomColors, setShadcnCustomColors,
resolvedThemeMode, resolvedThemeMode,
setQuickChatButtonModeImmediate, setQuickChatButtonModeImmediate,
setChatMessageLayoutImmediate,
setOpenTasksInRightSidebarImmediate,
setOpenMobileTasksInPopupImmediate,
setTaskPopupsBoardListOnlyImmediate,
setShowCostBadgeOnCardsImmediate,
setTaskDetailChatFirstImmediate,
setMobileNavPrimaryItemsImmediate, setMobileNavPrimaryItemsImmediate,
reopenOnboardingWithNav, reopenOnboardingWithNav,
viewMode, viewMode,
@@ -1692,8 +1703,12 @@ function AppInner() {
mergeStrategy, mergeStrategy,
planAutoApproveEnabled, planAutoApproveEnabled,
settingsLoaded, settingsLoaded,
openTasksInRightSidebar,
openMobileTasksInPopup, openMobileTasksInPopup,
taskPopupsBoardListOnly,
showCostBadgeOnCards,
taskDetailChatFirst, taskDetailChatFirst,
chatMessageLayout,
skillsEnabled, skillsEnabled,
experimentalFeatures, experimentalFeatures,
setQuickChatOpen, setQuickChatOpen,
@@ -2265,7 +2280,7 @@ function AppInner() {
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined} onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, revertTask, retryTask, pauseTask, unpauseTask, bypassReview, resetTask, duplicateTask }} taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, revertTask, retryTask, pauseTask, unpauseTask, bypassReview, resetTask, duplicateTask }}
deepLink={{ handleDetailClose }} deepLink={{ handleDetailClose }}
settings={{ prAuthAvailable, autoMerge, taskDetailChatFirst, chatMessageLayout, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate, setChatMessageLayoutImmediate, setMobileNavPrimaryItemsImmediate }} settings={{ prAuthAvailable, autoMerge, openTasksInRightSidebar, openMobileTasksInPopup, taskPopupsBoardListOnly, showCostBadgeOnCards, taskDetailChatFirst, chatMessageLayout, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate, setChatMessageLayoutImmediate, setOpenTasksInRightSidebarImmediate, setOpenMobileTasksInPopupImmediate, setTaskPopupsBoardListOnlyImmediate, setShowCostBadgeOnCardsImmediate, setTaskDetailChatFirstImmediate, setMobileNavPrimaryItemsImmediate }}
onSettingsClose={handleSettingsCloseWithNav} onSettingsClose={handleSettingsCloseWithNav}
onReopenOnboarding={reopenOnboardingWithNav} onReopenOnboarding={reopenOnboardingWithNav}
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")} onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}

View File

@@ -85,6 +85,10 @@ interface AppModalsProps {
settings: { settings: {
prAuthAvailable: boolean; prAuthAvailable: boolean;
autoMerge: boolean; autoMerge: boolean;
openTasksInRightSidebar: boolean;
openMobileTasksInPopup: boolean;
taskPopupsBoardListOnly: boolean;
showCostBadgeOnCards: boolean;
taskDetailChatFirst: boolean; taskDetailChatFirst: boolean;
chatMessageLayout: ChatMessageLayout; chatMessageLayout: ChatMessageLayout;
themeMode: ThemeMode; themeMode: ThemeMode;
@@ -98,6 +102,11 @@ interface AppModalsProps {
setShadcnCustomColors: (colors: Record<string, string>) => void; setShadcnCustomColors: (colors: Record<string, string>) => void;
setQuickChatButtonModeImmediate: (mode: "floating" | "footer" | "off") => void; setQuickChatButtonModeImmediate: (mode: "floating" | "footer" | "off") => void;
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void; setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
setMobileNavPrimaryItemsImmediate: (items: string[]) => void; setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
}; };
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */ /** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
@@ -386,6 +395,16 @@ export function AppModals({
onQuickChatButtonModeChange={settings.setQuickChatButtonModeImmediate} onQuickChatButtonModeChange={settings.setQuickChatButtonModeImmediate}
chatMessageLayout={settings.chatMessageLayout} chatMessageLayout={settings.chatMessageLayout}
onChatMessageLayoutChange={settings.setChatMessageLayoutImmediate} onChatMessageLayoutChange={settings.setChatMessageLayoutImmediate}
openTasksInRightSidebar={settings.openTasksInRightSidebar}
onOpenTasksInRightSidebarChange={settings.setOpenTasksInRightSidebarImmediate}
openMobileTasksInPopup={settings.openMobileTasksInPopup}
onOpenMobileTasksInPopupChange={settings.setOpenMobileTasksInPopupImmediate}
taskPopupsBoardListOnly={settings.taskPopupsBoardListOnly}
onTaskPopupsBoardListOnlyChange={settings.setTaskPopupsBoardListOnlyImmediate}
showCostBadgeOnCards={settings.showCostBadgeOnCards}
onShowCostBadgeOnCardsChange={settings.setShowCostBadgeOnCardsImmediate}
taskDetailChatFirst={settings.taskDetailChatFirst}
onTaskDetailChatFirstChange={settings.setTaskDetailChatFirstImmediate}
onMobileNavPrimaryItemsChange={settings.setMobileNavPrimaryItemsImmediate} onMobileNavPrimaryItemsChange={settings.setMobileNavPrimaryItemsImmediate}
onReopenOnboarding={onReopenOnboarding} onReopenOnboarding={onReopenOnboarding}
onOpenApprovals={onOpenApprovals} onOpenApprovals={onOpenApprovals}

View File

@@ -665,6 +665,17 @@ interface SettingsModalProps {
/** Mirrors the pending project conversation layout into mounted chat surfaces immediately. */ /** Mirrors the pending project conversation layout into mounted chat surfaces immediately. */
chatMessageLayout?: ChatMessageLayout; chatMessageLayout?: ChatMessageLayout;
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void; onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
/** Current App-shell values and optimistic callbacks for mounted Appearance consumers. */
openTasksInRightSidebar?: boolean;
onOpenTasksInRightSidebarChange?: (enabled: boolean) => void;
openMobileTasksInPopup?: boolean;
onOpenMobileTasksInPopupChange?: (enabled: boolean) => void;
taskPopupsBoardListOnly?: boolean;
onTaskPopupsBoardListOnlyChange?: (enabled: boolean) => void;
showCostBadgeOnCards?: boolean;
onShowCostBadgeOnCardsChange?: (enabled: boolean) => void;
taskDetailChatFirst?: boolean;
onTaskDetailChatFirstChange?: (enabled: boolean) => void;
/** Mirrors pending mobile quick-action changes into the app shell immediately. */ /** Mirrors pending mobile quick-action changes into the app shell immediately. */
onMobileNavPrimaryItemsChange?: (items: string[]) => void; onMobileNavPrimaryItemsChange?: (items: string[]) => void;
/** Optional callback when user wants to reopen the onboarding guide */ /** Optional callback when user wants to reopen the onboarding guide */
@@ -932,6 +943,16 @@ export function SettingsModal({
onQuickChatButtonModeChange, onQuickChatButtonModeChange,
chatMessageLayout = "bubbles", chatMessageLayout = "bubbles",
onChatMessageLayoutChange, onChatMessageLayoutChange,
openTasksInRightSidebar,
onOpenTasksInRightSidebarChange,
openMobileTasksInPopup,
onOpenMobileTasksInPopupChange,
taskPopupsBoardListOnly,
onTaskPopupsBoardListOnlyChange,
showCostBadgeOnCards,
onShowCostBadgeOnCardsChange,
taskDetailChatFirst,
onTaskDetailChatFirstChange,
onMobileNavPrimaryItemsChange, onMobileNavPrimaryItemsChange,
onReopenOnboarding, onReopenOnboarding,
onOpenApprovals, onOpenApprovals,
@@ -4214,6 +4235,16 @@ export function SettingsModal({
onShadcnCustomColorsChange={onShadcnCustomColorsChange} onShadcnCustomColorsChange={onShadcnCustomColorsChange}
chatMessageLayout={chatMessageLayout} chatMessageLayout={chatMessageLayout}
onChatMessageLayoutChange={onChatMessageLayoutChange} onChatMessageLayoutChange={onChatMessageLayoutChange}
openTasksInRightSidebar={openTasksInRightSidebar}
onOpenTasksInRightSidebarChange={onOpenTasksInRightSidebarChange}
openMobileTasksInPopup={openMobileTasksInPopup}
onOpenMobileTasksInPopupChange={onOpenMobileTasksInPopupChange}
taskPopupsBoardListOnly={taskPopupsBoardListOnly}
onTaskPopupsBoardListOnlyChange={onTaskPopupsBoardListOnlyChange}
showCostBadgeOnCards={showCostBadgeOnCards}
onShowCostBadgeOnCardsChange={onShowCostBadgeOnCardsChange}
taskDetailChatFirst={taskDetailChatFirst}
onTaskDetailChatFirstChange={onTaskDetailChatFirstChange}
sessionBannersHidden={sessionBannersHidden} sessionBannersHidden={sessionBannersHidden}
setSessionBannersHidden={setSessionBannersHidden} setSessionBannersHidden={setSessionBannersHidden}
/> />

View File

@@ -263,6 +263,35 @@ describe("SettingsModal", () => {
expect(mockUpdateGlobalSettings).not.toHaveBeenCalled(); expect(mockUpdateGlobalSettings).not.toHaveBeenCalled();
}); });
it("mirrors mounted Appearance controls through the modal while keeping one persistence path", async () => {
const callbacks = {
onOpenTasksInRightSidebarChange: vi.fn(),
onOpenMobileTasksInPopupChange: vi.fn(),
onTaskPopupsBoardListOnlyChange: vi.fn(),
onShowCostBadgeOnCardsChange: vi.fn(),
onTaskDetailChatFirstChange: vi.fn(),
};
renderModal({ initialSection: "appearance", ...callbacks });
await waitForSettingsModalReady();
fireEvent.click(screen.getByLabelText("Open tasks in the right sidebar"));
fireEvent.click(screen.getByLabelText("Open tasks as popups"));
fireEvent.click(screen.getByLabelText("Keep task popups on the view where they were opened"));
fireEvent.click(screen.getByLabelText("Show cost badges on task cards"));
fireEvent.click(screen.getByLabelText("Open task details with Chat first"));
expect(callbacks.onOpenTasksInRightSidebarChange).toHaveBeenCalledWith(true);
expect(callbacks.onOpenMobileTasksInPopupChange).toHaveBeenCalledWith(true);
expect(callbacks.onTaskPopupsBoardListOnlyChange).toHaveBeenCalledWith(true);
expect(callbacks.onShowCostBadgeOnCardsChange).toHaveBeenCalledWith(true);
expect(callbacks.onTaskDetailChatFirstChange).toHaveBeenCalledWith(true);
vi.useFakeTimers();
await flushSettingsAutoSave();
vi.useRealTimers();
expect(mockUpdateGlobalSettings).not.toHaveBeenCalled();
});
it("renders recommendation mailbox notices enabled by default and persists disabling it", async () => { it("renders recommendation mailbox notices enabled by default and persists disabling it", async () => {
renderModal({ initialSection: "general" }); renderModal({ initialSection: "general" });
await waitForSettingsModalReady(); await waitForSettingsModalReady();

View File

@@ -60,6 +60,12 @@ export function MainContent({
setShadcnCustomColors, setShadcnCustomColors,
resolvedThemeMode, resolvedThemeMode,
setQuickChatButtonModeImmediate, setQuickChatButtonModeImmediate,
setChatMessageLayoutImmediate,
setOpenTasksInRightSidebarImmediate,
setOpenMobileTasksInPopupImmediate,
setTaskPopupsBoardListOnlyImmediate,
setShowCostBadgeOnCardsImmediate,
setTaskDetailChatFirstImmediate,
setMobileNavPrimaryItemsImmediate, setMobileNavPrimaryItemsImmediate,
reopenOnboardingWithNav, reopenOnboardingWithNav,
viewMode, viewMode,
@@ -86,8 +92,12 @@ export function MainContent({
mergeStrategy, mergeStrategy,
planAutoApproveEnabled, planAutoApproveEnabled,
settingsLoaded, settingsLoaded,
openTasksInRightSidebar,
openMobileTasksInPopup, openMobileTasksInPopup,
taskPopupsBoardListOnly,
showCostBadgeOnCards,
taskDetailChatFirst, taskDetailChatFirst,
chatMessageLayout,
skillsEnabled, skillsEnabled,
experimentalFeatures, experimentalFeatures,
setQuickChatOpen, setQuickChatOpen,
@@ -328,6 +338,18 @@ export function MainContent({
onDashboardFontScaleChange={setDashboardFontScalePct} onDashboardFontScaleChange={setDashboardFontScalePct}
onShadcnCustomColorsChange={setShadcnCustomColors} onShadcnCustomColorsChange={setShadcnCustomColors}
onQuickChatButtonModeChange={setQuickChatButtonModeImmediate} onQuickChatButtonModeChange={setQuickChatButtonModeImmediate}
chatMessageLayout={chatMessageLayout}
onChatMessageLayoutChange={setChatMessageLayoutImmediate}
openTasksInRightSidebar={openTasksInRightSidebar}
onOpenTasksInRightSidebarChange={setOpenTasksInRightSidebarImmediate}
openMobileTasksInPopup={openMobileTasksInPopup}
onOpenMobileTasksInPopupChange={setOpenMobileTasksInPopupImmediate}
taskPopupsBoardListOnly={taskPopupsBoardListOnly}
onTaskPopupsBoardListOnlyChange={setTaskPopupsBoardListOnlyImmediate}
showCostBadgeOnCards={showCostBadgeOnCards}
onShowCostBadgeOnCardsChange={setShowCostBadgeOnCardsImmediate}
taskDetailChatFirst={taskDetailChatFirst}
onTaskDetailChatFirstChange={setTaskDetailChatFirstImmediate}
onMobileNavPrimaryItemsChange={setMobileNavPrimaryItemsImmediate} onMobileNavPrimaryItemsChange={setMobileNavPrimaryItemsImmediate}
onReopenOnboarding={reopenOnboardingWithNav} onReopenOnboarding={reopenOnboardingWithNav}
onOpenApprovals={() => handleChangeTaskView("mailbox")} onOpenApprovals={() => handleChangeTaskView("mailbox")}

View File

@@ -65,6 +65,13 @@ const LazyStub = lazy(async () => ({ default: () => null }));
const LazySettingsCloseStub = lazy(async () => ({ const LazySettingsCloseStub = lazy(async () => ({
default: ({ onClose }: { onClose: () => void }) => <button type="button" onClick={onClose}>Close settings view</button>, default: ({ onClose }: { onClose: () => void }) => <button type="button" onClick={onClose}>Close settings view</button>,
})); }));
let embeddedSettingsProps: Record<string, unknown> | undefined;
const LazySettingsBridgeStub = lazy(async () => ({
default: (props: Record<string, unknown>) => {
embeddedSettingsProps = props;
return <div>Embedded settings bridge</div>;
},
}));
function mainContentProps(overrides: Partial<MainContentProps> = {}): MainContentProps { function mainContentProps(overrides: Partial<MainContentProps> = {}): MainContentProps {
return { return {
@@ -94,6 +101,12 @@ function mainContentProps(overrides: Partial<MainContentProps> = {}): MainConten
setShadcnCustomColors: vi.fn(), setShadcnCustomColors: vi.fn(),
resolvedThemeMode: "light", resolvedThemeMode: "light",
setQuickChatButtonModeImmediate: vi.fn(), setQuickChatButtonModeImmediate: vi.fn(),
setChatMessageLayoutImmediate: vi.fn(),
setOpenTasksInRightSidebarImmediate: vi.fn(),
setOpenMobileTasksInPopupImmediate: vi.fn(),
setTaskPopupsBoardListOnlyImmediate: vi.fn(),
setShowCostBadgeOnCardsImmediate: vi.fn(),
setTaskDetailChatFirstImmediate: vi.fn(),
reopenOnboardingWithNav: vi.fn(), reopenOnboardingWithNav: vi.fn(),
viewMode: "project", viewMode: "project",
projects: [], projects: [],
@@ -136,6 +149,12 @@ function mainContentProps(overrides: Partial<MainContentProps> = {}): MainConten
autoMerge: true, autoMerge: true,
mergeStrategy: "direct", mergeStrategy: "direct",
settingsLoaded: true, settingsLoaded: true,
openTasksInRightSidebar: false,
openMobileTasksInPopup: false,
taskPopupsBoardListOnly: true,
showCostBadgeOnCards: false,
taskDetailChatFirst: false,
chatMessageLayout: "bubbles",
skillsEnabled: true, skillsEnabled: true,
experimentalFeatures: {}, experimentalFeatures: {},
setQuickChatOpen: vi.fn(), setQuickChatOpen: vi.fn(),
@@ -260,6 +279,54 @@ describe("MainContent graph task pop-out wiring", () => {
expect(refreshAppSettings).toHaveBeenCalledTimes(1); expect(refreshAppSettings).toHaveBeenCalledTimes(1);
}); });
it("forwards every live Appearance value and callback to embedded Settings", async () => {
embeddedSettingsProps = undefined;
const setters = {
setChatMessageLayoutImmediate: vi.fn(),
setOpenTasksInRightSidebarImmediate: vi.fn(),
setOpenMobileTasksInPopupImmediate: vi.fn(),
setTaskPopupsBoardListOnlyImmediate: vi.fn(),
setShowCostBadgeOnCardsImmediate: vi.fn(),
setTaskDetailChatFirstImmediate: vi.fn(),
};
render(<MainContent {...mainContentProps({
taskView: "settings",
chatMessageLayout: "full-width",
openTasksInRightSidebar: true,
openMobileTasksInPopup: true,
taskPopupsBoardListOnly: false,
showCostBadgeOnCards: true,
taskDetailChatFirst: true,
...setters,
_SettingsView: LazySettingsBridgeStub as MainContentProps["_SettingsView"],
})} />);
await screen.findByText("Embedded settings bridge");
expect(embeddedSettingsProps).toMatchObject({
chatMessageLayout: "full-width",
openTasksInRightSidebar: true,
openMobileTasksInPopup: true,
taskPopupsBoardListOnly: false,
showCostBadgeOnCards: true,
taskDetailChatFirst: true,
});
(embeddedSettingsProps?.onChatMessageLayoutChange as (value: "bubbles" | "full-width") => void)("bubbles");
(embeddedSettingsProps?.onOpenTasksInRightSidebarChange as (value: boolean) => void)(false);
(embeddedSettingsProps?.onOpenMobileTasksInPopupChange as (value: boolean) => void)(false);
(embeddedSettingsProps?.onTaskPopupsBoardListOnlyChange as (value: boolean) => void)(true);
(embeddedSettingsProps?.onShowCostBadgeOnCardsChange as (value: boolean) => void)(false);
(embeddedSettingsProps?.onTaskDetailChatFirstChange as (value: boolean) => void)(false);
expect(setters.setChatMessageLayoutImmediate).toHaveBeenCalledWith("bubbles");
expect(setters.setOpenTasksInRightSidebarImmediate).toHaveBeenCalledWith(false);
expect(setters.setOpenMobileTasksInPopupImmediate).toHaveBeenCalledWith(false);
expect(setters.setTaskPopupsBoardListOnlyImmediate).toHaveBeenCalledWith(true);
expect(setters.setShowCostBadgeOnCardsImmediate).toHaveBeenCalledWith(false);
expect(setters.setTaskDetailChatFirstImmediate).toHaveBeenCalledWith(false);
});
it("routes dependency-graph bridge and rendered task-card opens to the shared pop-out", () => { it("routes dependency-graph bridge and rendered task-card opens to the shared pop-out", () => {
hostContexts.length = 0; hostContexts.length = 0;
const openDetailTask = vi.fn(); const openDetailTask = vi.fn();

View File

@@ -134,8 +134,18 @@ export interface MainContentProps {
mergeStrategy: string; mergeStrategy: string;
planAutoApproveEnabled: boolean; planAutoApproveEnabled: boolean;
settingsLoaded: boolean; settingsLoaded: boolean;
openTasksInRightSidebar: boolean;
openMobileTasksInPopup: boolean; openMobileTasksInPopup: boolean;
taskPopupsBoardListOnly: boolean;
showCostBadgeOnCards: boolean;
taskDetailChatFirst: boolean; taskDetailChatFirst: boolean;
chatMessageLayout: "bubbles" | "full-width";
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
setChatMessageLayoutImmediate: (layout: "bubbles" | "full-width") => void;
skillsEnabled: boolean; skillsEnabled: boolean;
experimentalFeatures: Record<string, boolean>; experimentalFeatures: Record<string, boolean>;
setQuickChatOpen: Dispatch<SetStateAction<boolean>>; setQuickChatOpen: Dispatch<SetStateAction<boolean>>;

View File

@@ -18,6 +18,16 @@ export interface AppearanceSectionProps extends SectionBaseProps {
onShadcnCustomColorsChange?: (colors: Record<string, string>) => void; onShadcnCustomColorsChange?: (colors: Record<string, string>) => void;
chatMessageLayout?: ChatMessageLayout; chatMessageLayout?: ChatMessageLayout;
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void; onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
openTasksInRightSidebar?: boolean;
onOpenTasksInRightSidebarChange?: (enabled: boolean) => void;
openMobileTasksInPopup?: boolean;
onOpenMobileTasksInPopupChange?: (enabled: boolean) => void;
taskPopupsBoardListOnly?: boolean;
onTaskPopupsBoardListOnlyChange?: (enabled: boolean) => void;
showCostBadgeOnCards?: boolean;
onShowCostBadgeOnCardsChange?: (enabled: boolean) => void;
taskDetailChatFirst?: boolean;
onTaskDetailChatFirstChange?: (enabled: boolean) => void;
sessionBannersHidden: boolean; sessionBannersHidden: boolean;
setSessionBannersHidden: (hidden: boolean) => void; setSessionBannersHidden: (hidden: boolean) => void;
} }
@@ -29,7 +39,7 @@ Rows render through the shared settings primitives rather than hand-rolled `form
FNXC:SettingsScope 2026-07-15-17:35: FNXC:SettingsScope 2026-07-15-17:35:
Scope badges are per-row because this section genuinely mixes authority levels: theme, color, and font scale are global (DEFAULT_GLOBAL_SETTINGS), while every task-presentation toggle below is project-scoped (DEFAULT_PROJECT_SETTINGS). The nav labels the whole section "global", which is true only of the theme controls, so the badges are what tell an operator which of these travels between projects. Scope badges are per-row because this section genuinely mixes authority levels: theme, color, and font scale are global (DEFAULT_GLOBAL_SETTINGS), while every task-presentation toggle below is project-scoped (DEFAULT_PROJECT_SETTINGS). The nav labels the whole section "global", which is true only of the theme controls, so the badges are what tell an operator which of these travels between projects.
*/ */
export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors = {}, resolvedThemeMode, onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange, chatMessageLayout = "bubbles", onChatMessageLayoutChange, sessionBannersHidden, setSessionBannersHidden, }: AppearanceSectionProps) { export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors = {}, resolvedThemeMode, onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange, chatMessageLayout = "bubbles", onChatMessageLayoutChange, openTasksInRightSidebar, onOpenTasksInRightSidebarChange, openMobileTasksInPopup, onOpenMobileTasksInPopupChange, taskPopupsBoardListOnly, onTaskPopupsBoardListOnlyChange, showCostBadgeOnCards, onShowCostBadgeOnCardsChange, taskDetailChatFirst, onTaskDetailChatFirstChange, sessionBannersHidden, setSessionBannersHidden, }: AppearanceSectionProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
return (<> return (<>
<h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4> <h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4>
@@ -72,8 +82,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
help: t("settings.appearance.openTasksInRightSidebarHelp", "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel. Default: disabled."), help: t("settings.appearance.openTasksInRightSidebarHelp", "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel. Default: disabled."),
scope: "project", scope: "project",
}} }}
value={form.openTasksInRightSidebar === true} value={form.openTasksInRightSidebar ?? openTasksInRightSidebar === true}
onChange={(v) => setForm((f) => ({ ...f, openTasksInRightSidebar: v === true }))} onChange={(v) => {
const enabled = v === true;
setForm((f) => ({ ...f, openTasksInRightSidebar: enabled }));
onOpenTasksInRightSidebarChange?.(enabled);
}}
/> />
{/* FNXC:MobileTaskPopups 2026-07-21-00:00 (FN-8478): Keep the stored openMobileTasksInPopup key for compatibility and explain that board-card deep-tab chips now use the same popup routing, preserving the board behind Changes, Retries, or Workflow detail. */} {/* FNXC:MobileTaskPopups 2026-07-21-00:00 (FN-8478): Keep the stored openMobileTasksInPopup key for compatibility and explain that board-card deep-tab chips now use the same popup routing, preserving the board behind Changes, Retries, or Workflow detail. */}
<SettingsToggleRow <SettingsToggleRow
@@ -83,8 +97,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
help: t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, board task-card clicks including Changes, Retries, and Workflow chips, plus ordinary List row/card and right-dock Tasks-list clicks, open the existing movable task popup so the board or list remains visible. Other task opens keep their current behavior. Default: disabled."), help: t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, board task-card clicks including Changes, Retries, and Workflow chips, plus ordinary List row/card and right-dock Tasks-list clicks, open the existing movable task popup so the board or list remains visible. Other task opens keep their current behavior. Default: disabled."),
scope: "project", scope: "project",
}} }}
value={form.openMobileTasksInPopup === true} value={form.openMobileTasksInPopup ?? openMobileTasksInPopup === true}
onChange={(v) => setForm((f) => ({ ...f, openMobileTasksInPopup: v === true }))} onChange={(v) => {
const enabled = v === true;
setForm((f) => ({ ...f, openMobileTasksInPopup: enabled }));
onOpenMobileTasksInPopupChange?.(enabled);
}}
/> />
{/* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes task popups to their opening dashboard view by default. Operators may explicitly disable it for legacy globally shared popups; hidden scoped entries retain geometry and reopen on return. */} {/* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes task popups to their opening dashboard view by default. Operators may explicitly disable it for legacy globally shared popups; hidden scoped entries retain geometry and reopen on return. */}
<SettingsToggleRow <SettingsToggleRow
@@ -94,8 +112,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
help: t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled."), help: t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled."),
scope: "project", scope: "project",
}} }}
value={form.taskPopupsBoardListOnly === true} value={form.taskPopupsBoardListOnly ?? taskPopupsBoardListOnly === true}
onChange={(v) => setForm((f) => ({ ...f, taskPopupsBoardListOnly: v === true }))} onChange={(v) => {
const enabled = v === true;
setForm((f) => ({ ...f, taskPopupsBoardListOnly: enabled }));
onTaskPopupsBoardListOnlyChange?.(enabled);
}}
/> />
{/* FNXC:TaskCardCostBadge 2026-07-11-12:15: This project setting is opt-in because board cards are already dense; when enabled, only tasks with recorded positive token usage render a read-time derived spend badge. */} {/* FNXC:TaskCardCostBadge 2026-07-11-12:15: This project setting is opt-in because board cards are already dense; when enabled, only tasks with recorded positive token usage render a read-time derived spend badge. */}
<SettingsToggleRow <SettingsToggleRow
@@ -105,8 +127,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
help: t("settings.appearance.showCostBadgeOnCardsHelp", "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge."), help: t("settings.appearance.showCostBadgeOnCardsHelp", "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge."),
scope: "project", scope: "project",
}} }}
value={form.showCostBadgeOnCards === true} value={form.showCostBadgeOnCards ?? showCostBadgeOnCards === true}
onChange={(v) => setForm((f) => ({ ...f, showCostBadgeOnCards: v === true }))} onChange={(v) => {
const enabled = v === true;
setForm((f) => ({ ...f, showCostBadgeOnCards: enabled }));
onShowCostBadgeOnCardsChange?.(enabled);
}}
/> />
{/* FNXC:TaskDetailActivityFirst 2026-06-30-23:59: The project setting is opt-in because task details now default to Activity-first; explicit Activity/Chat/Logs links keep their destination regardless of this checkbox. */} {/* FNXC:TaskDetailActivityFirst 2026-06-30-23:59: The project setting is opt-in because task details now default to Activity-first; explicit Activity/Chat/Logs links keep their destination regardless of this checkbox. */}
<SettingsToggleRow <SettingsToggleRow
@@ -116,8 +142,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
help: t("settings.appearance.taskDetailChatFirstHelp", "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way."), help: t("settings.appearance.taskDetailChatFirstHelp", "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way."),
scope: "project", scope: "project",
}} }}
value={form.taskDetailChatFirst === true} value={form.taskDetailChatFirst ?? taskDetailChatFirst === true}
onChange={(v) => setForm((f) => ({ ...f, taskDetailChatFirst: v === true }))} onChange={(v) => {
const enabled = v === true;
setForm((f) => ({ ...f, taskDetailChatFirst: enabled }));
onTaskDetailChatFirstChange?.(enabled);
}}
/> />
{/* {/*
FNXC:SettingsScope 2026-07-15-17:35: FNXC:SettingsScope 2026-07-15-17:35:

View File

@@ -13,6 +13,11 @@ vi.mock("../../LanguageSelector", () => ({
})); }));
function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMessageLayoutChange = vi.fn()) { function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMessageLayoutChange = vi.fn()) {
const onOpenTasksInRightSidebarChange = vi.fn();
const onOpenMobileTasksInPopupChange = vi.fn();
const onTaskPopupsBoardListOnlyChange = vi.fn();
const onShowCostBadgeOnCardsChange = vi.fn();
const onTaskDetailChatFirstChange = vi.fn();
let form: SettingsFormState = { let form: SettingsFormState = {
maxConcurrent: 2, maxConcurrent: 2,
maxWorktrees: 4, maxWorktrees: 4,
@@ -40,12 +45,30 @@ function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMe
dashboardFontScalePct={100} dashboardFontScalePct={100}
chatMessageLayout={form.chatMessageLayout} chatMessageLayout={form.chatMessageLayout}
onChatMessageLayoutChange={onChatMessageLayoutChange} onChatMessageLayoutChange={onChatMessageLayoutChange}
openTasksInRightSidebar={form.openTasksInRightSidebar}
onOpenTasksInRightSidebarChange={onOpenTasksInRightSidebarChange}
openMobileTasksInPopup={form.openMobileTasksInPopup}
onOpenMobileTasksInPopupChange={onOpenMobileTasksInPopupChange}
taskPopupsBoardListOnly={form.taskPopupsBoardListOnly}
onTaskPopupsBoardListOnlyChange={onTaskPopupsBoardListOnlyChange}
showCostBadgeOnCards={form.showCostBadgeOnCards}
onShowCostBadgeOnCardsChange={onShowCostBadgeOnCardsChange}
taskDetailChatFirst={form.taskDetailChatFirst}
onTaskDetailChatFirstChange={onTaskDetailChatFirstChange}
sessionBannersHidden={false} sessionBannersHidden={false}
setSessionBannersHidden={vi.fn()} setSessionBannersHidden={vi.fn()}
/>, />,
); );
return { setForm, getForm: () => form }; return {
setForm,
getForm: () => form,
onOpenTasksInRightSidebarChange,
onOpenMobileTasksInPopupChange,
onTaskPopupsBoardListOnlyChange,
onShowCostBadgeOnCardsChange,
onTaskDetailChatFirstChange,
};
} }
describe("AppearanceSection", () => { describe("AppearanceSection", () => {
@@ -78,6 +101,22 @@ describe("AppearanceSection", () => {
expect(getForm().openTasksInRightSidebar).toBe(true); expect(getForm().openTasksInRightSidebar).toBe(true);
}); });
it("mirrors every mounted Appearance toggle to its matching live callback", () => {
const callbacks = renderAppearanceSection();
fireEvent.click(screen.getByLabelText("Open tasks in the right sidebar"));
fireEvent.click(screen.getByLabelText("Open tasks as popups"));
fireEvent.click(screen.getByLabelText("Keep task popups on the view where they were opened"));
fireEvent.click(screen.getByLabelText("Show cost badges on task cards"));
fireEvent.click(screen.getByLabelText("Open task details with Chat first"));
expect(callbacks.onOpenTasksInRightSidebarChange).toHaveBeenCalledWith(true);
expect(callbacks.onOpenMobileTasksInPopupChange).toHaveBeenCalledWith(true);
expect(callbacks.onTaskPopupsBoardListOnlyChange).toHaveBeenCalledWith(false);
expect(callbacks.onShowCostBadgeOnCardsChange).toHaveBeenCalledWith(true);
expect(callbacks.onTaskDetailChatFirstChange).toHaveBeenCalledWith(true);
});
it("reflects a persisted enabled value", () => { it("reflects a persisted enabled value", () => {
renderAppearanceSection({ openTasksInRightSidebar: true }); renderAppearanceSection({ openTasksInRightSidebar: true });

View File

@@ -58,6 +58,50 @@ describe("useAppSettings", () => {
expect(result.current.chatMessageLayout).toBe(expectedLayout); expect(result.current.chatMessageLayout).toBe(expectedLayout);
}); });
it("updates every mounted Appearance consumer synchronously without fetching", async () => {
const { result } = renderHook(() => useAppSettings("proj_123"));
await waitFor(() => expect(result.current.settingsLoaded).toBe(true));
const settingsFetchesBefore = mockFetchSettings.mock.calls.length;
const configFetchesBefore = mockFetchConfig.mock.calls.length;
act(() => {
result.current.setChatMessageLayoutImmediate("full-width");
result.current.setOpenTasksInRightSidebarImmediate(true);
result.current.setOpenMobileTasksInPopupImmediate(true);
result.current.setTaskPopupsBoardListOnlyImmediate(false);
result.current.setShowCostBadgeOnCardsImmediate(true);
result.current.setTaskDetailChatFirstImmediate(true);
});
expect(result.current.chatMessageLayout).toBe("full-width");
expect(result.current.openTasksInRightSidebar).toBe(true);
expect(result.current.openMobileTasksInPopup).toBe(true);
expect(result.current.taskPopupsBoardListOnly).toBe(false);
expect(result.current.showCostBadgeOnCards).toBe(true);
expect(result.current.taskDetailChatFirst).toBe(true);
expect(mockFetchSettings).toHaveBeenCalledTimes(settingsFetchesBefore);
expect(mockFetchConfig).toHaveBeenCalledTimes(configFetchesBefore);
act(() => {
result.current.setChatMessageLayoutImmediate("bubbles");
result.current.setOpenTasksInRightSidebarImmediate(false);
result.current.setOpenMobileTasksInPopupImmediate(false);
result.current.setTaskPopupsBoardListOnlyImmediate(true);
result.current.setShowCostBadgeOnCardsImmediate(false);
result.current.setTaskDetailChatFirstImmediate(false);
});
expect(result.current.chatMessageLayout).toBe("bubbles");
expect(result.current.openTasksInRightSidebar).toBe(false);
expect(result.current.openMobileTasksInPopup).toBe(false);
expect(result.current.taskPopupsBoardListOnly).toBe(true);
expect(result.current.showCostBadgeOnCards).toBe(false);
expect(result.current.taskDetailChatFirst).toBe(false);
expect(mockFetchSettings).toHaveBeenCalledTimes(settingsFetchesBefore);
expect(mockFetchConfig).toHaveBeenCalledTimes(configFetchesBefore);
});
it("resets chat message layout to bubbles while a new project hydrates", async () => { it("resets chat message layout to bubbles while a new project hydrates", async () => {
mockFetchSettings mockFetchSettings
.mockResolvedValueOnce({ chatMessageLayout: "full-width" } as never) .mockResolvedValueOnce({ chatMessageLayout: "full-width" } as never)

View File

@@ -68,6 +68,11 @@ export interface UseAppSettingsResult {
toggleShowQuickChatFAB: () => Promise<void>; toggleShowQuickChatFAB: () => Promise<void>;
setQuickChatButtonModeImmediate: (mode: QuickChatButtonMode) => void; setQuickChatButtonModeImmediate: (mode: QuickChatButtonMode) => void;
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void; setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
setMobileNavPrimaryItemsImmediate: (items: string[]) => void; setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
toggleAutoReloadOnVersionChange: () => Promise<void>; toggleAutoReloadOnVersionChange: () => Promise<void>;
/** Re-fetches settings from the backend to pick up changes made externally (e.g., by SettingsModal). */ /** Re-fetches settings from the backend to pick up changes made externally (e.g., by SettingsModal). */
@@ -338,12 +343,32 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
const setChatMessageLayoutImmediate = useCallback((layout: ChatMessageLayout) => { const setChatMessageLayoutImmediate = useCallback((layout: ChatMessageLayout) => {
/* /*
FNXC:ChatMessageLayout 2026-08-18-20:27: FNXC:LiveAppearanceSettings 2026-08-19-18:07:
The Appearance draft must update already-mounted ChatView and task-chat surfaces immediately; SettingsModal remains the sole persistence owner. Both overlay and embedded Settings must mirror every mounted Appearance control into the App shell during its input event. These setters intentionally avoid persistence: SettingsModal remains the only debounced writer and its reconciliation remains authoritative.
*/ */
setChatMessageLayout(normalizeChatMessageLayout(layout)); setChatMessageLayout(normalizeChatMessageLayout(layout));
}, []); }, []);
const setOpenTasksInRightSidebarImmediate = useCallback((enabled: boolean) => {
setOpenTasksInRightSidebar(enabled === true);
}, []);
const setOpenMobileTasksInPopupImmediate = useCallback((enabled: boolean) => {
setOpenMobileTasksInPopup(enabled === true);
}, []);
const setTaskPopupsBoardListOnlyImmediate = useCallback((enabled: boolean) => {
setTaskPopupsBoardListOnly(enabled === true);
}, []);
const setShowCostBadgeOnCardsImmediate = useCallback((enabled: boolean) => {
setShowCostBadgeOnCards(enabled === true);
}, []);
const setTaskDetailChatFirstImmediate = useCallback((enabled: boolean) => {
setTaskDetailChatFirst(enabled === true);
}, []);
/* /*
FNXC:Navigation 2026-07-17-00:00: FNXC:Navigation 2026-07-17-00:00:
The settings draft previews mobile quick-action order and membership in the app shell before Save; The settings draft previews mobile quick-action order and membership in the app shell before Save;
@@ -413,6 +438,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
toggleShowQuickChatFAB, toggleShowQuickChatFAB,
setQuickChatButtonModeImmediate, setQuickChatButtonModeImmediate,
setChatMessageLayoutImmediate, setChatMessageLayoutImmediate,
setOpenTasksInRightSidebarImmediate,
setOpenMobileTasksInPopupImmediate,
setTaskPopupsBoardListOnlyImmediate,
setShowCostBadgeOnCardsImmediate,
setTaskDetailChatFirstImmediate,
setMobileNavPrimaryItemsImmediate, setMobileNavPrimaryItemsImmediate,
toggleAutoReloadOnVersionChange, toggleAutoReloadOnVersionChange,
refresh, refresh,

View File

@@ -151,7 +151,7 @@ function TaskDetailTitleModalHarness() {
openNewTaskWithDescription: noop, openNewTaskWithDescription: noop,
openWorkflowEditor: noop, openWorkflowEditor: noop,
}; };
return <div data-testid="title-host-modal"><NavigationHistoryProvider value={{ pushNav: noop, replaceCurrent: noop, removeNav: noop }}><AppModals projectId="fixture" tasks={[fixtureTask]} projects={[]} currentProject={null} addToast={noop} toasts={[]} removeToast={noop} modalManager={modalManager as never} projectActions={{} as never} taskHandlers={{} as never} taskOperations={{ moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, archiveTask: asyncTask, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask }} deepLink={{ handleDetailClose: noop }} settings={{ prAuthAvailable: false, autoMerge: true, taskDetailChatFirst: false, chatMessageLayout: "bubbles", themeMode: "system", colorTheme: "default", dashboardFontScalePct: 100, shadcnCustomColors: {}, resolvedThemeMode: "light", setThemeMode: noop, setColorTheme: noop, setDashboardFontScalePct: noop, setShadcnCustomColors: noop, setQuickChatButtonModeImmediate: noop, setChatMessageLayoutImmediate: noop, setMobileNavPrimaryItemsImmediate: noop }} /></NavigationHistoryProvider></div>; return <div data-testid="title-host-modal"><NavigationHistoryProvider value={{ pushNav: noop, replaceCurrent: noop, removeNav: noop }}><AppModals projectId="fixture" tasks={[fixtureTask]} projects={[]} currentProject={null} addToast={noop} toasts={[]} removeToast={noop} modalManager={modalManager as never} projectActions={{} as never} taskHandlers={{} as never} taskOperations={{ moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, archiveTask: asyncTask, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask }} deepLink={{ handleDetailClose: noop }} settings={{ prAuthAvailable: false, autoMerge: true, openTasksInRightSidebar: false, openMobileTasksInPopup: false, taskPopupsBoardListOnly: true, showCostBadgeOnCards: false, taskDetailChatFirst: false, chatMessageLayout: "bubbles", themeMode: "system", colorTheme: "default", dashboardFontScalePct: 100, shadcnCustomColors: {}, resolvedThemeMode: "light", setThemeMode: noop, setColorTheme: noop, setDashboardFontScalePct: noop, setShadcnCustomColors: noop, setQuickChatButtonModeImmediate: noop, setChatMessageLayoutImmediate: noop, setOpenTasksInRightSidebarImmediate: noop, setOpenMobileTasksInPopupImmediate: noop, setTaskPopupsBoardListOnlyImmediate: noop, setShowCostBadgeOnCardsImmediate: noop, setTaskDetailChatFirstImmediate: noop, setMobileNavPrimaryItemsImmediate: noop }} /></NavigationHistoryProvider></div>;
} }
function TaskDetailTitleMainPanelHarness() { function TaskDetailTitleMainPanelHarness() {