fix(FN-1488): add showQuickChatFAB setting to control Quick Chat FAB visibility
- Add showQuickChatFAB boolean setting to ProjectSettings type - Add useAppSettings hook function for accessing the setting - Wire QuickChatFAB visibility in App.tsx based on setting - Hide QuickChatFAB in MobileNavBar when setting is false - Add Settings UI toggle for the new setting - Add CSS for hiding QuickChatFAB on mobile when disabled - Add comprehensive tests for all affected components - Fix QuickChatFAB onOpenChange test to use controlled mode - Update settings reference documentation
This commit is contained in:
5
.changeset/fix-quick-chat-mobile-ux.md
Normal file
5
.changeset/fix-quick-chat-mobile-ux.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gsxdsm/fusion": patch
|
||||
---
|
||||
|
||||
Fix mobile quick-chat UX: add showQuickChatFAB project setting to hide the floating button, add Chat access to mobile More menu, and fix FAB/nav bar overlap on small screens.
|
||||
@@ -96,6 +96,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `buildRetryCount` | `number` | `0` | Build retry attempts during merge. |
|
||||
| `buildTimeoutMs` | `number` | `300000` | Build timeout in ms (5 minutes). |
|
||||
| `requirePlanApproval` | `boolean` | `false` | Require manual approval before triage → todo. |
|
||||
| `showQuickChatFAB` | `boolean` | `true` | Show floating quick-chat button. Chat accessible from More menu when hidden. |
|
||||
| `taskStuckTimeoutMs` | `number` | `undefined` | Inactivity timeout for stuck-task recovery. |
|
||||
| `autoUnpauseEnabled` | `boolean` | `true` | Auto-unpause after rate-limit-triggered pauses. |
|
||||
| `autoUnpauseBaseDelayMs` | `number` | `300000` | Base unpause retry delay in ms (5 min). |
|
||||
|
||||
@@ -99,6 +99,7 @@ const PROJECT_KEYS: (keyof ProjectSettings)[] = [
|
||||
"reflectionIntervalMs",
|
||||
"reflectionAfterTask",
|
||||
"reviewHandoffPolicy",
|
||||
"showQuickChatFAB",
|
||||
];
|
||||
|
||||
function assertExactKeyCoverage(scopeName: string, actual: readonly string[], expected: readonly string[]): void {
|
||||
|
||||
@@ -1144,6 +1144,10 @@ export interface ProjectSettings {
|
||||
* - "always": Always handoff after completion (not implemented, reserved for future)
|
||||
*/
|
||||
reviewHandoffPolicy?: "disabled" | "comment-triggered" | "always";
|
||||
/** When true, show the quick-chat floating action button (FAB) in the dashboard.
|
||||
* When false, the FAB is hidden but chat remains accessible via the More menu.
|
||||
* Default: true. */
|
||||
showQuickChatFAB?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1255,6 +1259,7 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
|
||||
reflectionIntervalMs: 3_600_000,
|
||||
reflectionAfterTask: true,
|
||||
reviewHandoffPolicy: "disabled",
|
||||
showQuickChatFAB: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1361,6 +1366,7 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
|
||||
"reflectionIntervalMs",
|
||||
"reflectionAfterTask",
|
||||
"reviewHandoffPolicy",
|
||||
"showQuickChatFAB",
|
||||
] as const;
|
||||
|
||||
// ── Compile-time parity: ensures every interface key is listed exactly once ──
|
||||
|
||||
@@ -121,6 +121,7 @@ function AppInner() {
|
||||
const [nodesOpen, setNodesOpen] = useState(false);
|
||||
const [missionResumeSessionId, setMissionResumeSessionId] = useState<string | undefined>(undefined);
|
||||
const [missionTargetId, setMissionTargetId] = useState<string | undefined>(undefined);
|
||||
const [quickChatOpen, setQuickChatOpen] = useState(false);
|
||||
|
||||
// Settings state
|
||||
const {
|
||||
@@ -130,6 +131,7 @@ function AppInner() {
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
taskStuckTimeoutMs,
|
||||
showQuickChatFAB,
|
||||
githubTokenConfigured,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
@@ -505,10 +507,17 @@ function AppInner() {
|
||||
onOpenUsage={modalManager.openUsage}
|
||||
onViewAllProjects={handleViewAllProjects}
|
||||
onRunScript={modalManager.runScript}
|
||||
onOpenQuickChat={() => setQuickChatOpen(true)}
|
||||
projectId={currentProject?.id}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && taskView !== "chat" && (
|
||||
<QuickChatFAB projectId={currentProject.id} addToast={addToast} />
|
||||
<QuickChatFAB
|
||||
projectId={currentProject.id}
|
||||
addToast={addToast}
|
||||
showFAB={showQuickChatFAB}
|
||||
open={quickChatOpen}
|
||||
onOpenChange={setQuickChatOpen}
|
||||
/>
|
||||
)}
|
||||
<AppModals
|
||||
projectId={currentProject?.id}
|
||||
|
||||
@@ -59,6 +59,7 @@ const createDefaultMobileNavProps = () => ({
|
||||
activePlanningSessionCount: 0,
|
||||
onOpenUsage: vi.fn(),
|
||||
onRunScript: vi.fn(),
|
||||
onOpenQuickChat: vi.fn(),
|
||||
projectId: "proj_1",
|
||||
});
|
||||
|
||||
@@ -161,9 +162,20 @@ describe("Mobile Feature Access Regression Guard", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-schedules")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-github")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-usage")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-chat")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||
});
|
||||
|
||||
it("chat is accessible via more sheet even when FAB is hidden", () => {
|
||||
const props = createDefaultMobileNavProps();
|
||||
render(<MobileNavBar {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-chat"));
|
||||
|
||||
expect(props.onOpenQuickChat).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("mobile nav bar renders only on mobile viewport and hides for modal or desktop", () => {
|
||||
const mobileRender = render(<MobileNavBar {...createDefaultMobileNavProps()} />);
|
||||
expect(mobileRender.container.querySelector(".mobile-nav-bar")).not.toBeNull();
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface MobileNavBarProps {
|
||||
activePlanningSessionCount?: number;
|
||||
onOpenUsage?: () => void;
|
||||
onRunScript?: (name: string, command: string) => void;
|
||||
onOpenQuickChat?: () => void;
|
||||
projectId?: string;
|
||||
onViewAllProjects?: () => void;
|
||||
}
|
||||
@@ -93,6 +94,7 @@ export function MobileNavBar({
|
||||
activePlanningSessionCount = 0,
|
||||
onOpenUsage,
|
||||
onRunScript,
|
||||
onOpenQuickChat,
|
||||
projectId,
|
||||
onViewAllProjects,
|
||||
}: MobileNavBarProps) {
|
||||
@@ -440,6 +442,16 @@ export function MobileNavBar({
|
||||
<span>Projects</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mobile-more-item"
|
||||
data-testid="mobile-more-item-chat"
|
||||
onClick={() => handleMoreAction(onOpenQuickChat)}
|
||||
>
|
||||
<MessageSquare />
|
||||
<span>Chat</span>
|
||||
</button>
|
||||
|
||||
<div className="mobile-more-separator" />
|
||||
|
||||
<button
|
||||
|
||||
@@ -8,6 +8,12 @@ import { useAgents } from "../hooks/useAgents";
|
||||
interface QuickChatFABProps {
|
||||
projectId?: string;
|
||||
addToast: (msg: string, type?: "success" | "error") => void;
|
||||
/** When false, the FAB button is hidden but the panel can still be opened programmatically via the open prop */
|
||||
showFAB?: boolean;
|
||||
/** When true, the chat panel is open */
|
||||
open?: boolean;
|
||||
/** Callback when the panel should be opened/closed */
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function getAgentLabel(agent: Agent): string {
|
||||
@@ -15,9 +21,21 @@ function getAgentLabel(agent: Agent): string {
|
||||
return `${base} (${agent.role})`;
|
||||
}
|
||||
|
||||
export function QuickChatFAB({ projectId, addToast }: QuickChatFABProps) {
|
||||
export function QuickChatFAB({ projectId, addToast, showFAB = true, open, onOpenChange }: QuickChatFABProps) {
|
||||
const { agents } = useAgents(projectId);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// Internal state for uncontrolled mode, controlled state when open prop is provided
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const isControlled = open !== undefined;
|
||||
const isOpen = isControlled ? open : internalOpen;
|
||||
const setIsOpen = isControlled
|
||||
? (value: boolean | ((prev: boolean) => boolean)) => {
|
||||
if (typeof value === "function") {
|
||||
onOpenChange?.(value(isOpen));
|
||||
} else {
|
||||
onOpenChange?.(value);
|
||||
}
|
||||
}
|
||||
: setInternalOpen;
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isConversationLoading, setIsConversationLoading] = useState(false);
|
||||
@@ -137,16 +155,18 @@ export function QuickChatFAB({ projectId, addToast }: QuickChatFABProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={fabRef}
|
||||
type="button"
|
||||
className="quick-chat-fab"
|
||||
aria-label="Open quick chat"
|
||||
data-testid="quick-chat-fab"
|
||||
onClick={() => setIsOpen((open) => !open)}
|
||||
>
|
||||
<MessageSquare size={24} />
|
||||
</button>
|
||||
{showFAB && (
|
||||
<button
|
||||
ref={fabRef}
|
||||
type="button"
|
||||
className="quick-chat-fab"
|
||||
aria-label="Open quick chat"
|
||||
data-testid="quick-chat-fab"
|
||||
onClick={() => setIsOpen((open) => !open)}
|
||||
>
|
||||
<MessageSquare size={24} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
<div className="quick-chat-panel" ref={panelRef} data-testid="quick-chat-panel">
|
||||
|
||||
@@ -654,6 +654,20 @@ export function SettingsModal({
|
||||
</label>
|
||||
<small>When enabled, AI-generated task specifications require manual approval before moving to Todo</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="showQuickChatFAB" className="checkbox-label">
|
||||
<input
|
||||
id="showQuickChatFAB"
|
||||
type="checkbox"
|
||||
checked={form.showQuickChatFAB !== false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, showQuickChatFAB: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Show quick chat button
|
||||
</label>
|
||||
<small>Show the floating chat button in the dashboard. Chat is still accessible from the More menu.</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "models": {
|
||||
|
||||
@@ -47,6 +47,7 @@ const createDefaultProps = () => ({
|
||||
onOpenUsage: vi.fn(),
|
||||
onViewAllProjects: vi.fn(),
|
||||
onRunScript: vi.fn(),
|
||||
onOpenQuickChat: vi.fn(),
|
||||
projectId: "proj_1",
|
||||
});
|
||||
|
||||
@@ -149,6 +150,7 @@ describe("MobileNavBar", () => {
|
||||
expect(screen.getByTestId("mobile-more-item-github")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-usage")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-projects")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-chat")).toBeDefined();
|
||||
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -185,6 +187,17 @@ describe("MobileNavBar", () => {
|
||||
expect(props.onViewAllProjects).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("calls onOpenQuickChat from the Chat more-sheet item", () => {
|
||||
const props = createDefaultProps();
|
||||
const { container } = render(<MobileNavBar {...props} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||
fireEvent.click(screen.getByTestId("mobile-more-item-chat"));
|
||||
|
||||
expect(container.querySelector(".mobile-more-sheet")).toBeNull();
|
||||
expect(props.onOpenQuickChat).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("closes sheet on backdrop click", () => {
|
||||
const { container } = render(<MobileNavBar {...createDefaultProps()} />);
|
||||
|
||||
|
||||
@@ -227,4 +227,54 @@ describe("QuickChatFAB", () => {
|
||||
expect(screen.queryByTestId("quick-chat-panel")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("hides FAB button when showFAB is false", () => {
|
||||
render(<QuickChatFAB addToast={addToast} showFAB={false} />);
|
||||
|
||||
expect(screen.queryByTestId("quick-chat-fab")).toBeNull();
|
||||
});
|
||||
|
||||
it("still opens panel programmatically when showFAB is false with controlled open prop", async () => {
|
||||
render(<QuickChatFAB addToast={addToast} showFAB={false} open={true} />);
|
||||
|
||||
expect(screen.queryByTestId("quick-chat-fab")).toBeNull();
|
||||
expect(screen.getByTestId("quick-chat-panel")).toBeDefined();
|
||||
});
|
||||
|
||||
it("controlled open prop opens panel without clicking FAB", () => {
|
||||
render(<QuickChatFAB addToast={addToast} open={true} />);
|
||||
|
||||
expect(screen.getByTestId("quick-chat-panel")).toBeDefined();
|
||||
});
|
||||
|
||||
it("controlled open prop defaults to closed when not set", () => {
|
||||
render(<QuickChatFAB addToast={addToast} />);
|
||||
|
||||
expect(screen.queryByTestId("quick-chat-panel")).toBeNull();
|
||||
});
|
||||
|
||||
it("onOpenChange callback is called when panel is opened via FAB (controlled mode)", async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
render(<QuickChatFAB addToast={addToast} open={false} onOpenChange={onOpenChange} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("onOpenChange callback is called when panel is closed via FAB", async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
render(<QuickChatFAB addToast={addToast} open={true} onOpenChange={onOpenChange} />);
|
||||
|
||||
// Panel should be open initially
|
||||
expect(screen.getByTestId("quick-chat-panel")).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ const defaultSettings: Settings = {
|
||||
maxStuckKills: 6,
|
||||
runStepsInNewSessions: false,
|
||||
maxParallelSteps: 2,
|
||||
showQuickChatFAB: true,
|
||||
};
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
@@ -400,6 +401,90 @@ describe("SettingsModal", () => {
|
||||
expect(updateSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows Show quick chat button checkbox in General section", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button");
|
||||
expect(checkbox).toBeTruthy();
|
||||
expect(checkbox.getAttribute("type")).toBe("checkbox");
|
||||
});
|
||||
|
||||
it("showQuickChatFAB defaults to checked (true) when not set", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("showQuickChatFAB defaults to checked when setting is true", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
showQuickChatFAB: true,
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("showQuickChatFAB is unchecked when setting is false", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
showQuickChatFAB: false,
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button") as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it("toggling showQuickChatFAB checkbox sends false in save payload when unchecked", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button");
|
||||
// Default is checked (true), click to uncheck
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.showQuickChatFAB).toBe(false);
|
||||
});
|
||||
|
||||
it("toggling showQuickChatFAB checkbox sends true in save payload when checked", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
showQuickChatFAB: false,
|
||||
});
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(screen.getAllByText("General")[0]);
|
||||
const checkbox = screen.getByLabelText("Show quick chat button");
|
||||
// Default is unchecked (false), click to check
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1));
|
||||
|
||||
const payload = (updateSettings as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(payload.showQuickChatFAB).toBe(true);
|
||||
});
|
||||
|
||||
it("shows Auto-completion mode select in Merge section", async () => {
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
@@ -28,6 +28,7 @@ describe("useAppSettings", () => {
|
||||
enginePaused: false,
|
||||
githubTokenConfigured: true,
|
||||
taskStuckTimeoutMs: 600000,
|
||||
showQuickChatFAB: false,
|
||||
} as never);
|
||||
|
||||
mockUpdateSettings.mockResolvedValue({} as never);
|
||||
@@ -44,6 +45,7 @@ describe("useAppSettings", () => {
|
||||
expect(result.current.enginePaused).toBe(false);
|
||||
expect(result.current.githubTokenConfigured).toBe(true);
|
||||
expect(result.current.taskStuckTimeoutMs).toBe(600000);
|
||||
expect(result.current.showQuickChatFAB).toBe(false);
|
||||
});
|
||||
|
||||
expect(mockFetchConfig).toHaveBeenCalledWith("proj_123");
|
||||
|
||||
@@ -11,10 +11,12 @@ export interface UseAppSettingsResult {
|
||||
globalPaused: boolean;
|
||||
enginePaused: boolean;
|
||||
taskStuckTimeoutMs: number | undefined;
|
||||
showQuickChatFAB: boolean;
|
||||
githubTokenConfigured: boolean;
|
||||
toggleAutoMerge: () => Promise<void>;
|
||||
toggleGlobalPause: () => Promise<void>;
|
||||
toggleEnginePause: () => Promise<void>;
|
||||
toggleShowQuickChatFAB: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,6 +29,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [globalPaused, setGlobalPaused] = useState(false);
|
||||
const [enginePaused, setEnginePaused] = useState(false);
|
||||
const [taskStuckTimeoutMs, setTaskStuckTimeoutMs] = useState<number | undefined>(undefined);
|
||||
const [showQuickChatFAB, setShowQuickChatFAB] = useState(true);
|
||||
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,6 +49,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
setEnginePaused(Boolean(settings.enginePaused));
|
||||
setGithubTokenConfigured(Boolean(settings.githubTokenConfigured));
|
||||
setTaskStuckTimeoutMs(settings.taskStuckTimeoutMs);
|
||||
setShowQuickChatFAB(settings.showQuickChatFAB !== false);
|
||||
})
|
||||
.catch(() => {
|
||||
// Keep defaults on fetch failure.
|
||||
@@ -85,6 +89,17 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
}
|
||||
}, [enginePaused, projectId]);
|
||||
|
||||
const toggleShowQuickChatFAB = useCallback(async () => {
|
||||
const next = !showQuickChatFAB;
|
||||
setShowQuickChatFAB(next);
|
||||
|
||||
try {
|
||||
await updateSettings({ showQuickChatFAB: next }, projectId);
|
||||
} catch {
|
||||
setShowQuickChatFAB(!next);
|
||||
}
|
||||
}, [showQuickChatFAB, projectId]);
|
||||
|
||||
return {
|
||||
maxConcurrent,
|
||||
rootDir,
|
||||
@@ -92,9 +107,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
taskStuckTimeoutMs,
|
||||
showQuickChatFAB,
|
||||
githubTokenConfigured,
|
||||
toggleAutoMerge,
|
||||
toggleGlobalPause,
|
||||
toggleEnginePause,
|
||||
toggleShowQuickChatFAB,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25225,6 +25225,17 @@ html .column.drag-over * {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Mobile: position FAB and panel above the bottom nav bar */
|
||||
@media (max-width: 768px) {
|
||||
.quick-chat-fab {
|
||||
bottom: calc(24px + var(--executor-footer-height, 0px) + var(--mobile-nav-height, 44px) + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.quick-chat-panel {
|
||||
bottom: calc(84px + var(--executor-footer-height, 0px) + var(--mobile-nav-height, 44px) + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Chat View ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
.chat-view {
|
||||
|
||||
Reference in New Issue
Block a user