FN-8204: add configurable mobile footer quick actions
Add an ordered project setting for mobile footer quick-action destinations. - Render selected destinations as primary mobile tabs and retain omitted choices in More. - Provide settings controls, validation, translations, documentation, and regression coverage. - Add a minor changeset for the published CLI package. Files changed: .changeset/fn-8204-mobile-nav-primary-items.md | 7 + docs/dashboard-guide.md | 4 + docs/settings-reference.md | 4 + .../src/__tests__/mobile-nav-primary-items.test.ts | 23 +++ packages/core/src/index.ts | 9 ++ packages/core/src/mobile-nav-primary-items.ts | 55 +++++++ packages/core/src/settings-schema.ts | 1 + packages/core/src/types.ts | 7 + packages/dashboard/app/App.tsx | 2 + .../mobile-feature-access-regression.test.tsx | 19 +++ packages/dashboard/app/components/MobileNavBar.tsx | 159 +++++---------------- .../app/components/__tests__/MobileNavBar.test.tsx | 16 +++ .../app/components/settings/section-keys.ts | 1 + .../settings/sections/GeneralSection.search.ts | 9 ++ .../settings/sections/GeneralSection.tsx | 45 ++++++ .../settings-default-descriptions.test.tsx | 1 + packages/dashboard/app/hooks/useAppSettings.ts | 5 + packages/i18n/locales/en/app.json | 4 + 18 files changed, 244 insertions(+), 127 deletions(-) Fusion-Task-Id: FN-8204 Fusion-Task-Lineage: 077de4c4-d281-401d-bddd-e5ce8e91d4ab Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8204-mobile-nav-primary-items.md
Normal file
7
.changeset/fn-8204-mobile-nav-primary-items.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Choose which quick-action tabs appear in the mobile footer nav.
|
||||||
|
category: feature
|
||||||
|
dev: Adds project setting `mobileNavPrimaryItems` (ordered list of the seven selectable canonical nav-item ids: command-center, tasks, agents, missions, chat, mailbox, planning); MobileNavBar renders primary tabs from it and routes omitted selectable destinations to the More sheet. Default reproduces the prior order.
|
||||||
@@ -2034,3 +2034,7 @@ If the endpoint is unavailable on the running dashboard build, the response will
|
|||||||
In **Settings → Notifications**, enable **Agent clarification** to let Planning Mode pause when the planner needs an answer. The Planning Mode advanced settings include a per-session override, initialized from that global preference. With clarification disabled, proactive questions are redirected to a final plan summary instead of holding the session; the final summary deepening checkpoint is unchanged.
|
In **Settings → Notifications**, enable **Agent clarification** to let Planning Mode pause when the planner needs an answer. The Planning Mode advanced settings include a per-session override, initialized from that global preference. With clarification disabled, proactive questions are redirected to a final plan summary instead of holding the session; the final summary deepening checkpoint is unchanged.
|
||||||
|
|
||||||
When enabled, a proactive question holds the planner at `awaiting_input`, sends the configured `planning-awaiting-input` ntfy event, and delivers a dashboard mailbox message that links the operator back to planner chat. Mailbox delivery does not depend on ntfy configuration and is deduplicated by session/question across restarts.
|
When enabled, a proactive question holds the planner at `awaiting_input`, sends the configured `planning-awaiting-input` ntfy event, and delivers a dashboard mailbox message that links the operator back to planner chat. Mailbox delivery does not depend on ntfy configuration and is deduplicated by session/question across restarts.
|
||||||
|
|
||||||
|
### Mobile footer quick actions
|
||||||
|
|
||||||
|
In **Settings → General**, choose up to six Mobile footer quick actions with the ordered checkbox list. Planning can replace a default destination; use the adjacent earlier/later controls to set the footer order. Any unselected quick action moves to More, so it remains reachable.
|
||||||
|
|||||||
@@ -1773,3 +1773,7 @@ Values are project-scoped and finite values are floored; count/backoff must be a
|
|||||||
Escalation is enabled only when the toggle is true and either a complete provider/model pair or a node ID is configured. It is single-shot: after FN-7996 exhausts same-model retries, Fusion persists the override and tries once before the existing terminal park. The alternate model enters the [model-selection hierarchy](#model-selection-hierarchy) as a task-level override; a node target enters `resolveEffectiveNode` as a task-level routing override and is requeued so scheduler routing is recalculated. This remains opt-in by default to avoid unexpected model cost or execution behavior. Column-agent overrides still govern their sessions and can supersede a task-level model target.
|
Escalation is enabled only when the toggle is true and either a complete provider/model pair or a node ID is configured. It is single-shot: after FN-7996 exhausts same-model retries, Fusion persists the override and tries once before the existing terminal park. The alternate model enters the [model-selection hierarchy](#model-selection-hierarchy) as a task-level override; a node target enters `resolveEffectiveNode` as a task-level routing override and is requeued so scheduler routing is recalculated. This remains opt-in by default to avoid unexpected model cost or execution behavior. Column-agent overrides still govern their sessions and can supersede a task-level model target.
|
||||||
|
|
||||||
| `triageDuplicateResolution` | `"prompt" \| "keep" \| "delete"` | `"prompt"` | Controls `DUPLICATE: FN-NNNN` markers emitted during triage. **prompt** flags and system-pauses the task for an operator Keep/Delete decision; the existing decision banner links to the canonical task. **keep** dismisses the marker and replans a real task. **delete** restores legacy auto-delete behavior. |
|
| `triageDuplicateResolution` | `"prompt" \| "keep" \| "delete"` | `"prompt"` | Controls `DUPLICATE: FN-NNNN` markers emitted during triage. **prompt** flags and system-pauses the task for an operator Keep/Delete decision; the existing decision banner links to the canonical task. **keep** dismisses the marker and replans a real task. **delete** restores legacy auto-delete behavior. |
|
||||||
|
|
||||||
|
### `mobileNavPrimaryItems`
|
||||||
|
|
||||||
|
Project-scoped ordered list of mobile footer quick actions. The default is `command-center`, `tasks`, `agents`, `missions`, `chat`, `mailbox`. The only selectable ids are those six plus `planning`; unknown, `more`, and overflow-only ids are ignored. Omitted selectable destinations remain reachable in the More sheet, whose trailing footer tab is always present.
|
||||||
|
|||||||
23
packages/core/src/__tests__/mobile-nav-primary-items.test.ts
Normal file
23
packages/core/src/__tests__/mobile-nav-primary-items.test.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, resolveMobileNavPrimaryItems } from "../mobile-nav-primary-items.js";
|
||||||
|
|
||||||
|
describe("resolveMobileNavPrimaryItems", () => {
|
||||||
|
it("uses the existing six-tab order for unset or empty values", () => {
|
||||||
|
expect(resolveMobileNavPrimaryItems()).toMatchObject({ primaryItems: DEFAULT_MOBILE_NAV_PRIMARY_ITEMS });
|
||||||
|
expect(resolveMobileNavPrimaryItems({ mobileNavPrimaryItems: [] })).toMatchObject({ primaryItems: DEFAULT_MOBILE_NAV_PRIMARY_ITEMS });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts planning, preserves order, and routes omitted destinations to More", () => {
|
||||||
|
const resolved = resolveMobileNavPrimaryItems({ mobileNavPrimaryItems: ["tasks", "planning", "agents"] });
|
||||||
|
expect(resolved.primaryItems).toEqual(["tasks", "planning", "agents"]);
|
||||||
|
expect(resolved.omittedItems).toEqual(["command-center", "missions", "chat", "mailbox"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops overflow-only and unknown ids, deduplicates, and clamps footer tabs", () => {
|
||||||
|
const resolved = resolveMobileNavPrimaryItems({
|
||||||
|
mobileNavPrimaryItems: ["settings", "tasks", "more", "documents", "tasks", "agents", "missions", "chat", "mailbox", "planning", "unknown"],
|
||||||
|
});
|
||||||
|
expect(resolved.primaryItems).toEqual(["tasks", "agents", "missions", "chat", "mailbox", "planning"]);
|
||||||
|
expect(resolved.omittedItems).toEqual(["command-center"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1974,6 +1974,15 @@ export type {
|
|||||||
} from "./research-types.js";
|
} from "./research-types.js";
|
||||||
|
|
||||||
export { isExperimentalFeatureEnabled, GRAPH_NATIVE_POST_MERGE_FLAG } from "./experimental-features.js";
|
export { isExperimentalFeatureEnabled, GRAPH_NATIVE_POST_MERGE_FLAG } from "./experimental-features.js";
|
||||||
|
export {
|
||||||
|
DEFAULT_MOBILE_NAV_PRIMARY_ITEMS,
|
||||||
|
MAX_MOBILE_NAV_PRIMARY_ITEMS,
|
||||||
|
MOBILE_NAV_SELECTABLE_ITEMS,
|
||||||
|
MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS,
|
||||||
|
resolveMobileNavPrimaryItems,
|
||||||
|
type MobileNavSelectableItem,
|
||||||
|
type ResolvedMobileNavPrimaryItems,
|
||||||
|
} from "./mobile-nav-primary-items.js";
|
||||||
export {
|
export {
|
||||||
POST_MERGE_VERIFICATION_GROUP_ID,
|
POST_MERGE_VERIFICATION_GROUP_ID,
|
||||||
postMergeOptionalGroupNode,
|
postMergeOptionalGroupNode,
|
||||||
|
|||||||
55
packages/core/src/mobile-nav-primary-items.ts
Normal file
55
packages/core/src/mobile-nav-primary-items.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { ProjectSettings } from "./types.js";
|
||||||
|
|
||||||
|
/** The only destinations that may be promoted into the mobile footer. */
|
||||||
|
export const MOBILE_NAV_SELECTABLE_ITEMS = [
|
||||||
|
"command-center",
|
||||||
|
"tasks",
|
||||||
|
"agents",
|
||||||
|
"missions",
|
||||||
|
"chat",
|
||||||
|
"mailbox",
|
||||||
|
"planning",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type MobileNavSelectableItem = (typeof MOBILE_NAV_SELECTABLE_ITEMS)[number];
|
||||||
|
|
||||||
|
/** Stable i18n keys for settings controls that list selectable destinations. */
|
||||||
|
export const MOBILE_NAV_SELECTABLE_ITEM_LABEL_KEYS: Record<MobileNavSelectableItem, string> = {
|
||||||
|
"command-center": "nav.commandCenter",
|
||||||
|
tasks: "nav.tasks",
|
||||||
|
agents: "nav.agents",
|
||||||
|
missions: "nav.missions",
|
||||||
|
chat: "nav.chat",
|
||||||
|
mailbox: "nav.mailbox",
|
||||||
|
planning: "nav.planning",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MOBILE_NAV_PRIMARY_ITEMS: MobileNavSelectableItem[] = [
|
||||||
|
"command-center", "tasks", "agents", "missions", "chat", "mailbox",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MAX_MOBILE_NAV_PRIMARY_ITEMS = 6;
|
||||||
|
|
||||||
|
export interface ResolvedMobileNavPrimaryItems {
|
||||||
|
primaryItems: MobileNavSelectableItem[];
|
||||||
|
omittedItems: MobileNavSelectableItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:Navigation 2026-07-17-00:00:
|
||||||
|
Mobile footer customization is intentionally limited to these seven quick-action ids. Invalid,
|
||||||
|
overflow-only, and `more` ids cannot become footer tabs; omitted selectable destinations remain
|
||||||
|
reachable in More, and More itself is always rendered separately as the trailing tab.
|
||||||
|
*/
|
||||||
|
export function resolveMobileNavPrimaryItems(settings?: Pick<ProjectSettings, "mobileNavPrimaryItems">): ResolvedMobileNavPrimaryItems {
|
||||||
|
const selected = Array.isArray(settings?.mobileNavPrimaryItems) ? settings.mobileNavPrimaryItems : [];
|
||||||
|
const valid = new Set<string>(MOBILE_NAV_SELECTABLE_ITEMS);
|
||||||
|
const primaryItems = selected.reduce<MobileNavSelectableItem[]>((items, id) => {
|
||||||
|
if (valid.has(id) && !items.includes(id as MobileNavSelectableItem) && items.length < MAX_MOBILE_NAV_PRIMARY_ITEMS) {
|
||||||
|
items.push(id as MobileNavSelectableItem);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}, []);
|
||||||
|
const resolved = primaryItems.length > 0 ? primaryItems : [...DEFAULT_MOBILE_NAV_PRIMARY_ITEMS];
|
||||||
|
return { primaryItems: resolved, omittedItems: MOBILE_NAV_SELECTABLE_ITEMS.filter((id) => !resolved.includes(id)) };
|
||||||
|
}
|
||||||
@@ -712,6 +712,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
|||||||
reflectionAfterTask: true,
|
reflectionAfterTask: true,
|
||||||
// reviewHandoffPolicy MOVED to workflow settings (U4) — see MOVED_SETTINGS_KEYS.
|
// reviewHandoffPolicy MOVED to workflow settings (U4) — see MOVED_SETTINGS_KEYS.
|
||||||
quickChatButtonMode: "off",
|
quickChatButtonMode: "off",
|
||||||
|
mobileNavPrimaryItems: ["command-center", "tasks", "agents", "missions", "chat", "mailbox"],
|
||||||
/*
|
/*
|
||||||
FNXC:ChatModal 2026-06-28-00:00:
|
FNXC:ChatModal 2026-06-28-00:00:
|
||||||
Quick Chat outside-click dismissal remains default-on for upgrades, but it is now a project setting so operators can disable accidental board-click closes.
|
Quick Chat outside-click dismissal remains default-on for upgrades, but it is now a project setting so operators can disable accidental board-click closes.
|
||||||
|
|||||||
@@ -4250,6 +4250,13 @@ export interface ProjectSettings {
|
|||||||
reviewHandoffPolicy?: "disabled" | "comment-triggered" | "always";
|
reviewHandoffPolicy?: "disabled" | "comment-triggered" | "always";
|
||||||
/** Quick Chat launcher placement. "floating" shows the draggable FAB, "footer" shows a footer button, "off" hides both. */
|
/** Quick Chat launcher placement. "floating" shows the draggable FAB, "footer" shows a footer button, "off" hides both. */
|
||||||
quickChatButtonMode?: "floating" | "footer" | "off";
|
quickChatButtonMode?: "floating" | "footer" | "off";
|
||||||
|
/*
|
||||||
|
* FNXC:Navigation 2026-07-17-00:00:
|
||||||
|
* Ordered quick-action ids shown before the always-present mobile More tab. Only command-center,
|
||||||
|
* tasks, agents, missions, chat, mailbox, and planning are eligible; unset falls back to the
|
||||||
|
* default order, invalid/overflow-only ids (including more) are ignored, and omitted ids stay in More.
|
||||||
|
*/
|
||||||
|
mobileNavPrimaryItems?: string[];
|
||||||
/**
|
/**
|
||||||
* FNXC:ChatModal 2026-06-28-00:00:
|
* FNXC:ChatModal 2026-06-28-00:00:
|
||||||
* Outside-click dismissal of Quick Chat is now user-configurable; default true preserves the prior always-on behavior from FN-7152.
|
* Outside-click dismissal of Quick Chat is now user-configurable; default true preserves the prior always-on behavior from FN-7152.
|
||||||
|
|||||||
@@ -685,6 +685,7 @@ function AppInner() {
|
|||||||
modelPricingOverrides,
|
modelPricingOverrides,
|
||||||
taskDetailChatFirst,
|
taskDetailChatFirst,
|
||||||
quickChatButtonMode,
|
quickChatButtonMode,
|
||||||
|
mobileNavPrimaryItems,
|
||||||
quickChatCloseOnOutsideClick,
|
quickChatCloseOnOutsideClick,
|
||||||
dashboardKeyboardShortcuts,
|
dashboardKeyboardShortcuts,
|
||||||
dismissModalsOnOutsideClick,
|
dismissModalsOnOutsideClick,
|
||||||
@@ -1772,6 +1773,7 @@ function AppInner() {
|
|||||||
footerVisible={viewMode === "project" && !!currentProject}
|
footerVisible={viewMode === "project" && !!currentProject}
|
||||||
modalOpen={modalManager.anyModalOpen}
|
modalOpen={modalManager.anyModalOpen}
|
||||||
keyboardOpen={mobileNavKeyboardOpen}
|
keyboardOpen={mobileNavKeyboardOpen}
|
||||||
|
mobileNavPrimaryItems={mobileNavPrimaryItems}
|
||||||
onOpenSettings={openSettingsWithNav}
|
onOpenSettings={openSettingsWithNav}
|
||||||
onOpenActivityLog={openActivityLogWithNav}
|
onOpenActivityLog={openActivityLogWithNav}
|
||||||
onOpenMailbox={() => handleTaskViewChange("mailbox")}
|
onOpenMailbox={() => handleTaskViewChange("mailbox")}
|
||||||
|
|||||||
@@ -209,6 +209,25 @@ describe("Mobile Feature Access Regression Guard", () => {
|
|||||||
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
expect(screen.getByTestId("mobile-more-item-settings")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps every configurable destination reachable when a custom footer omits one", () => {
|
||||||
|
render(
|
||||||
|
<MobileNavBar
|
||||||
|
{...createDefaultMobileNavProps()}
|
||||||
|
mobileNavPrimaryItems={["command-center", "tasks", "agents", "planning", "chat", "mailbox", "skills"]}
|
||||||
|
showSkillsTab={false}
|
||||||
|
experimentalFeatures={{ insights: false, memoryView: false }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("mobile-nav-tab-missions")).toBeNull();
|
||||||
|
expect(screen.queryByTestId("mobile-nav-tab-skills")).toBeNull();
|
||||||
|
expect(screen.getByTestId("mobile-nav-tab-more")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||||
|
expect(screen.getByTestId("mobile-more-item-missions")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("mobile-more-item-skills")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("reliability is no longer a mobile More item and is reached via Command Center", () => {
|
it("reliability is no longer a mobile More item and is reached via Command Center", () => {
|
||||||
const props = createDefaultMobileNavProps();
|
const props = createDefaultMobileNavProps();
|
||||||
render(<MobileNavBar {...props} />);
|
render(<MobileNavBar {...props} />);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { NavigationHistoryContext } from "../hooks/useNavigationHistory";
|
|||||||
import type { TaskView } from "../hooks/useViewState";
|
import type { TaskView } from "../hooks/useViewState";
|
||||||
import { buildPluginTaskViewId, isPluginViewId } from "../plugins/pluginViewRegistry";
|
import { buildPluginTaskViewId, isPluginViewId } from "../plugins/pluginViewRegistry";
|
||||||
import { getPluginDashboardViewNavIcon } from "./pluginNavIcon";
|
import { getPluginDashboardViewNavIcon } from "./pluginNavIcon";
|
||||||
|
import { resolveMobileNavPrimaryItems, type MobileNavSelectableItem } from "../../../core/src/mobile-nav-primary-items";
|
||||||
|
|
||||||
export interface PublishedMobileNavHeightInput {
|
export interface PublishedMobileNavHeightInput {
|
||||||
navOffsetHeight: number;
|
navOffsetHeight: number;
|
||||||
@@ -116,6 +117,8 @@ export interface MobileNavBarProps {
|
|||||||
};
|
};
|
||||||
pluginDashboardViews?: PluginDashboardViewEntry[];
|
pluginDashboardViews?: PluginDashboardViewEntry[];
|
||||||
shellConnectionControl?: ReactNode;
|
shellConnectionControl?: ReactNode;
|
||||||
|
/** Ordered quick-action tabs; invalid values resolve to the safe default. */
|
||||||
|
mobileNavPrimaryItems?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function GitHubLogo({ size = 20 }: { size?: number }) {
|
function GitHubLogo({ size = 20 }: { size?: number }) {
|
||||||
@@ -167,6 +170,7 @@ export function MobileNavBar({
|
|||||||
experimentalFeatures,
|
experimentalFeatures,
|
||||||
pluginDashboardViews = [],
|
pluginDashboardViews = [],
|
||||||
shellConnectionControl,
|
shellConnectionControl,
|
||||||
|
mobileNavPrimaryItems,
|
||||||
}: MobileNavBarProps) {
|
}: MobileNavBarProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const mode = useViewportMode();
|
const mode = useViewportMode();
|
||||||
@@ -402,8 +406,10 @@ export function MobileNavBar({
|
|||||||
.filter((entry) => !topLevelPluginViewKeys.has(`${entry.pluginId}:${entry.view.viewId}`))
|
.filter((entry) => !topLevelPluginViewKeys.has(`${entry.pluginId}:${entry.view.viewId}`))
|
||||||
.sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER));
|
.sort((a, b) => (a.view.order ?? Number.MAX_SAFE_INTEGER) - (b.view.order ?? Number.MAX_SAFE_INTEGER));
|
||||||
|
|
||||||
|
const { primaryItems, omittedItems } = resolveMobileNavPrimaryItems({ mobileNavPrimaryItems });
|
||||||
const isMoreActive =
|
const isMoreActive =
|
||||||
view === "documents"
|
omittedItems.some((item) => (item === "tasks" ? view === "board" || view === "list" : view === item))
|
||||||
|
|| view === "documents"
|
||||||
|| (Boolean(experimentalFeatures?.evalsView) && view === "evals")
|
|| (Boolean(experimentalFeatures?.evalsView) && view === "evals")
|
||||||
|| (Boolean(experimentalFeatures?.goalsView) && view === "goalsView")
|
|| (Boolean(experimentalFeatures?.goalsView) && view === "goalsView")
|
||||||
|| view === "research"
|
|| view === "research"
|
||||||
@@ -417,6 +423,29 @@ export function MobileNavBar({
|
|||||||
|| view === "graph"
|
|| view === "graph"
|
||||||
|| (isPluginViewId(view) && !topLevelPrimaryPluginViews.some((entry) => buildPluginTaskViewId(entry.pluginId, entry.view.viewId) === view));
|
|| (isPluginViewId(view) && !topLevelPrimaryPluginViews.some((entry) => buildPluginTaskViewId(entry.pluginId, entry.view.viewId) === view));
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:Navigation 2026-07-17-00:00:
|
||||||
|
A configured quick-action destination renders exactly once: as a footer tab or as a More-sheet item.
|
||||||
|
This preserves its handlers and indicators while ensuring invalid settings cannot strand navigation.
|
||||||
|
*/
|
||||||
|
const renderSelectableItem = (item: MobileNavSelectableItem, surface: "primary" | "more") => {
|
||||||
|
const isPrimary = surface === "primary";
|
||||||
|
const active = item === "tasks" ? view === "board" || view === "list" : view === item;
|
||||||
|
const navigate = () => {
|
||||||
|
if (item === "tasks") onChangeView(view === "board" || view === "list" ? view : "board");
|
||||||
|
else if (item === "planning") { if (isPrimary) planningHandler?.(); else handleMoreAction(planningHandler); }
|
||||||
|
else if (isPrimary) onChangeView(item);
|
||||||
|
else handleMoreAction(() => onChangeView(item));
|
||||||
|
};
|
||||||
|
const icon = item === "command-center" ? <Gauge /> : item === "tasks" ? <LayoutGrid /> : item === "agents" ? <Bot /> : item === "missions" ? <Target /> : item === "chat" ? <MessageSquare /> : item === "mailbox" ? <Mail /> : <Lightbulb />;
|
||||||
|
const label = t(`nav.${item === "command-center" ? "commandCenter" : item}`, item === "command-center" ? "Dashboard" : item[0].toUpperCase() + item.slice(1));
|
||||||
|
const indicator = (item === "chat" && chatHasUnreadResponse && !active) || (item === "mailbox" && mailboxPendingApprovalCount > 0 && !active) || (item === "planning" && planningNeedsInput && !active);
|
||||||
|
const badge = item === "mailbox" ? mailboxUnreadCount : item === "planning" ? activePlanningSessionCount : 0;
|
||||||
|
const indicatorLabel = item === "chat" ? t("nav.chatUnreadAriaLabel", "Unread chat response") : item === "mailbox" ? t("nav.mailboxPendingAriaLabel", "Pending approvals") : t("nav.planningNeedsInputAriaLabel", "Planning needs your input");
|
||||||
|
if (isPrimary) return <button key={item} type="button" className={`mobile-nav-tab${active ? " mobile-nav-tab--active" : ""}`} data-testid={`mobile-nav-tab-${item}`} role="tab" aria-selected={active} onClick={navigate}><span className="mobile-nav-tab-icon-wrapper">{icon}{indicator && <span className="status-dot status-dot--pending mobile-nav-chat-unread-dot" aria-label={indicatorLabel} />}</span><span className="mobile-nav-tab-label">{label}</span>{badge > 0 && <span className="mobile-nav-tab-badge">{formatCount(badge)}</span>}</button>;
|
||||||
|
return <button key={item} type="button" className="mobile-more-item" data-testid={`mobile-more-item-${item}`} onClick={navigate}><span className="mobile-more-item-icon-wrapper">{icon}{indicator && <span className="status-dot status-dot--pending mobile-more-item-icon-dot" aria-label={indicatorLabel} />}</span><span>{label}</span>{badge > 0 && <span className="mobile-more-item-badge">{formatCount(badge)}</span>}</button>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
@@ -425,115 +454,7 @@ export function MobileNavBar({
|
|||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label={t("nav.primaryNavAriaLabel", "Primary navigation")}
|
aria-label={t("nav.primaryNavAriaLabel", "Primary navigation")}
|
||||||
>
|
>
|
||||||
{/*
|
{primaryItems.map((item) => renderSelectableItem(item, "primary"))}
|
||||||
FNXC:Navigation 2026-06-22-01:40:
|
|
||||||
Dashboard (Command Center) is the first mobile tab, before Tasks, matching the desktop sidebar order.
|
|
||||||
*/}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "command-center" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-command-center"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "command-center"}
|
|
||||||
onClick={() => onChangeView("command-center")}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<Gauge />
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.commandCenter", "Dashboard")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "board" || view === "list" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-tasks"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "board" || view === "list"}
|
|
||||||
onClick={() => {
|
|
||||||
// If already on a tasks view, stay there; otherwise go to board
|
|
||||||
if (view === "board" || view === "list") {
|
|
||||||
onChangeView(view);
|
|
||||||
} else {
|
|
||||||
onChangeView("board");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<LayoutGrid />
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.tasks", "Tasks")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "agents" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-agents"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "agents"}
|
|
||||||
onClick={() => onChangeView("agents")}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<Bot />
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.agents", "Agents")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "missions" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-missions"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "missions"}
|
|
||||||
onClick={() => onChangeView("missions")}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<Target />
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.missions", "Missions")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "chat" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-chat"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "chat"}
|
|
||||||
onClick={() => onChangeView("chat")}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<MessageSquare />
|
|
||||||
{chatHasUnreadResponse && view !== "chat" && (
|
|
||||||
<span className="status-dot status-dot--pending mobile-nav-chat-unread-dot" aria-label={t("nav.chatUnreadAriaLabel", "Unread chat response")} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.chat", "Chat")}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
{/*
|
|
||||||
FNXC:Navigation 2026-06-19-12:30:
|
|
||||||
Mailbox is a top-level mobile tab only and must not be duplicated in the three-dot More sheet; Todos lives only in the three-dot overflow/More menu, never the main tab list.
|
|
||||||
Keep unread and pending-approval indicators on this surviving Mailbox tab so removing the More-sheet duplicate does not hide mailbox state.
|
|
||||||
*/}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`mobile-nav-tab${view === "mailbox" ? " mobile-nav-tab--active" : ""}`}
|
|
||||||
data-testid="mobile-nav-tab-mailbox"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={view === "mailbox"}
|
|
||||||
onClick={() => onChangeView("mailbox")}
|
|
||||||
>
|
|
||||||
<span className="mobile-nav-tab-icon-wrapper">
|
|
||||||
<Mail />
|
|
||||||
{mailboxPendingApprovalCount > 0 && view !== "mailbox" && (
|
|
||||||
<span className="status-dot status-dot--pending mobile-nav-chat-unread-dot" aria-label={t("nav.mailboxPendingAriaLabel", "Pending approvals")} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="mobile-nav-tab-label">{t("nav.mailbox", "Mailbox")}</span>
|
|
||||||
{mailboxUnreadCount > 0 && (
|
|
||||||
<span className="mobile-nav-tab-badge">{formatCount(mailboxUnreadCount)}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showSkillsTopLevel && (
|
{showSkillsTopLevel && (
|
||||||
<button
|
<button
|
||||||
@@ -737,23 +658,7 @@ export function MobileNavBar({
|
|||||||
<span>{t("nav.files", "Files")}</span>
|
<span>{t("nav.files", "Files")}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
{omittedItems.map((item) => renderSelectableItem(item, "more"))}
|
||||||
type="button"
|
|
||||||
className="mobile-more-item"
|
|
||||||
data-testid="mobile-more-item-planning"
|
|
||||||
onClick={() => handleMoreAction(planningHandler)}
|
|
||||||
>
|
|
||||||
<span className="mobile-more-item-icon-wrapper">
|
|
||||||
<Lightbulb />
|
|
||||||
{planningNeedsInput && (
|
|
||||||
<span className="status-dot status-dot--pending mobile-more-item-icon-dot" aria-label={t("nav.planningNeedsInputAriaLabel", "Planning needs your input")} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span>{t("nav.planning", "Planning")}</span>
|
|
||||||
{activePlanningSessionCount > 0 && (
|
|
||||||
<span className="mobile-more-item-badge">{formatCount(activePlanningSessionCount)}</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -152,6 +152,22 @@ describe("MobileNavBar", () => {
|
|||||||
expect(screen.getByTestId("mobile-more-item-skills")).toBeDefined();
|
expect(screen.getByTestId("mobile-more-item-skills")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("promotes Planning and routes demoted Missions to More without an empty tab", () => {
|
||||||
|
const { container } = render(<MobileNavBar {...createDefaultProps()} mobileNavPrimaryItems={["command-center", "tasks", "agents", "planning", "chat", "mailbox"]} />);
|
||||||
|
expect(screen.getByTestId("mobile-nav-tab-planning")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("mobile-nav-tab-missions")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
|
||||||
|
expect(screen.getByTestId("mobile-more-item-missions")).toBeInTheDocument();
|
||||||
|
expect(container.querySelector('[data-testid="mobile-nav-tab-missions"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never promotes overflow-only ids and always keeps More", () => {
|
||||||
|
render(<MobileNavBar {...createDefaultProps()} mobileNavPrimaryItems={["settings", "planning"]} />);
|
||||||
|
expect(screen.queryByTestId("mobile-nav-tab-settings")).toBeNull();
|
||||||
|
expect(screen.getByTestId("mobile-nav-tab-planning")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("mobile-nav-tab-more")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not render legacy roadmaps tab", () => {
|
it("does not render legacy roadmaps tab", () => {
|
||||||
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{}} />);
|
render(<MobileNavBar {...createDefaultProps()} experimentalFeatures={{}} />);
|
||||||
expect(screen.queryByTestId("mobile-nav-tab-roadmaps")).toBeNull();
|
expect(screen.queryByTestId("mobile-nav-tab-roadmaps")).toBeNull();
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const PROJECT_SECTION_KEYS: Record<string, readonly string[]> = {
|
|||||||
"ephemeralAgentsEnabled",
|
"ephemeralAgentsEnabled",
|
||||||
"sessionAdvisorEnabledByDefault",
|
"sessionAdvisorEnabledByDefault",
|
||||||
"mailAutoCleanupDays",
|
"mailAutoCleanupDays",
|
||||||
|
"mobileNavPrimaryItems",
|
||||||
"operationalLogRetentionDays",
|
"operationalLogRetentionDays",
|
||||||
"quickChatButtonMode",
|
"quickChatButtonMode",
|
||||||
"quickChatCloseOnOutsideClick",
|
"quickChatCloseOnOutsideClick",
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ export const generalSearchEntries: SettingsSearchEntry[] = [
|
|||||||
"When enabled, slash-prefixed paths such as /tmp can be opened in the workspace file browser. Windows drive-letter paths remain blocked, and other path validators are unchanged. Default: disabled.",
|
"When enabled, slash-prefixed paths such as /tmp can be opened in the workspace file browser. Windows drive-letter paths remain blocked, and other path validators are unchanged. Default: disabled.",
|
||||||
keywords: ["outside workspace", "root paths"],
|
keywords: ["outside workspace", "root paths"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sectionId: "general",
|
||||||
|
key: "mobileNavPrimaryItems",
|
||||||
|
labelKey: "settings.general.mobileNavPrimaryItems",
|
||||||
|
labelFallback: "Mobile footer quick actions",
|
||||||
|
helpKey: "settings.general.mobileNavPrimaryItemsHint",
|
||||||
|
helpFallback: "Default: Dashboard, Tasks, Agents, Missions, Chat, Mailbox. Unselected destinations remain in More.",
|
||||||
|
keywords: ["mobile", "footer", "navigation", "planning", "more"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
sectionId: "general",
|
sectionId: "general",
|
||||||
key: "quickChatButtonMode",
|
key: "quickChatButtonMode",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { DEPRECATED_BUILTIN_WORKFLOW_IDS, isLocale, SUPPORTED_LOCALES, type WorkflowDefinition } from "@fusion/core";
|
import { DEPRECATED_BUILTIN_WORKFLOW_IDS, isLocale, SUPPORTED_LOCALES, type WorkflowDefinition } from "@fusion/core";
|
||||||
|
import { DEFAULT_MOBILE_NAV_PRIMARY_ITEMS, MAX_MOBILE_NAV_PRIMARY_ITEMS, MOBILE_NAV_SELECTABLE_ITEMS } from "../../../../../core/src/mobile-nav-primary-items";
|
||||||
|
import { SettingsFieldRow } from "../SettingsFieldRow";
|
||||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||||
import { SettingsNumberRow } from "../SettingsNumberRow";
|
import { SettingsNumberRow } from "../SettingsNumberRow";
|
||||||
@@ -289,6 +291,49 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
|
|||||||
return { ...f, quickChatButtonMode: mode, showQuickChatFAB: mode === "floating" };
|
return { ...f, quickChatButtonMode: mode, showQuickChatFAB: mode === "floating" };
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
{/*
|
||||||
|
FNXC:Navigation 2026-07-17-00:00:
|
||||||
|
The Settings control exposes only the fixed seven-item quick-action universe. Checkboxes choose
|
||||||
|
destinations and the adjacent controls preserve their selected order; core still sanitizes persisted
|
||||||
|
values so unselected destinations remain reachable through More and its trailing tab cannot be removed.
|
||||||
|
*/}
|
||||||
|
<SettingsFieldRow
|
||||||
|
htmlFor="mobileNavPrimaryItems"
|
||||||
|
label={t("settings.general.mobileNavPrimaryItems", "Mobile footer quick actions")}
|
||||||
|
help={t("settings.general.mobileNavPrimaryItemsHint", "Default: Dashboard, Tasks, Agents, Missions, Chat, Mailbox. Unselected destinations remain in the More menu.")}
|
||||||
|
scope="project"
|
||||||
|
>
|
||||||
|
<div role="group" aria-label={t("settings.general.mobileNavPrimaryItems", "Mobile footer quick actions")}>
|
||||||
|
{MOBILE_NAV_SELECTABLE_ITEMS.map((item) => {
|
||||||
|
const selectedItems = Array.isArray(form.mobileNavPrimaryItems)
|
||||||
|
? form.mobileNavPrimaryItems
|
||||||
|
: DEFAULT_MOBILE_NAV_PRIMARY_ITEMS;
|
||||||
|
const selectedIndex = selectedItems.indexOf(item);
|
||||||
|
const selected = selectedIndex >= 0;
|
||||||
|
const label = t(`nav.${item === "command-center" ? "commandCenter" : item}`, item);
|
||||||
|
const updateItems = (nextItems: string[]) => setForm((current) => ({ ...current, mobileNavPrimaryItems: nextItems }));
|
||||||
|
return (
|
||||||
|
<div key={item}>
|
||||||
|
<label className="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected}
|
||||||
|
disabled={!selected && selectedItems.length >= MAX_MOBILE_NAV_PRIMARY_ITEMS}
|
||||||
|
onChange={(event) => updateItems(event.target.checked ? [...selectedItems, item] : selectedItems.filter((selectedItem) => selectedItem !== item))}
|
||||||
|
/>
|
||||||
|
<span>{label}</span>
|
||||||
|
</label>
|
||||||
|
{selected && (
|
||||||
|
<>
|
||||||
|
<button type="button" className="btn btn-icon" disabled={selectedIndex === 0} aria-label={t("settings.general.moveNavItemEarlier", "Move {{item}} earlier", { item: label })} onClick={() => updateItems(selectedItems.map((selectedItem, index) => index === selectedIndex - 1 ? item : index === selectedIndex ? selectedItems[index - 1] : selectedItem))}>↑</button>
|
||||||
|
<button type="button" className="btn btn-icon" disabled={selectedIndex === selectedItems.length - 1} aria-label={t("settings.general.moveNavItemLater", "Move {{item}} later", { item: label })} onClick={() => updateItems(selectedItems.map((selectedItem, index) => index === selectedIndex + 1 ? item : index === selectedIndex ? selectedItems[index + 1] : selectedItem))}>↓</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SettingsFieldRow>
|
||||||
{/*
|
{/*
|
||||||
FNXC:ChatModal 2026-06-28-00:00:
|
FNXC:ChatModal 2026-06-28-00:00:
|
||||||
Operators need a Settings > General toggle for Quick Chat outside-click dismissal because accidental board clicks can otherwise close active chat context. Default checked preserves the shipped FN-7152 interaction.
|
Operators need a Settings > General toggle for Quick Chat outside-click dismissal because accidental board clicks can otherwise close active chat context. Default checked preserves the shipped FN-7152 interaction.
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
|||||||
mailAutoCleanupDays: "general.deleteInboxOutboxMessagesOlderThanThisMany",
|
mailAutoCleanupDays: "general.deleteInboxOutboxMessagesOlderThanThisMany",
|
||||||
operationalLogRetentionDays: "general.loweringThisWindowMeansReliabilityMetricsChartsAnd",
|
operationalLogRetentionDays: "general.loweringThisWindowMeansReliabilityMetricsChartsAnd",
|
||||||
quickChatButtonMode: "general.quickChatLauncherHint",
|
quickChatButtonMode: "general.quickChatLauncherHint",
|
||||||
|
mobileNavPrimaryItems: "general.mobileNavPrimaryItemsHint",
|
||||||
quickChatCloseOnOutsideClick: "general.quickChatCloseOnOutsideClickHint",
|
quickChatCloseOnOutsideClick: "general.quickChatCloseOnOutsideClickHint",
|
||||||
showTaskChatsInCommonFeed: "general.showTaskChatsInCommonFeedHint",
|
showTaskChatsInCommonFeed: "general.showTaskChatsInCommonFeedHint",
|
||||||
taskPrefix: "general.prefixForNewTaskIDsEGKB",
|
taskPrefix: "general.prefixForNewTaskIDsEGKB",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { fetchConfig, fetchSettings, updateSettings, updateGlobalSettings } from "../api";
|
import { fetchConfig, fetchSettings, updateSettings, updateGlobalSettings } from "../api";
|
||||||
import type { GlobalSettings, ProjectSettings } from "@fusion/core";
|
import type { GlobalSettings, ProjectSettings } from "@fusion/core";
|
||||||
|
import { resolveMobileNavPrimaryItems } from "../../../core/src/mobile-nav-primary-items";
|
||||||
import type { ModelPricingOverrides } from "../../../core/src/model-pricing";
|
import type { ModelPricingOverrides } from "../../../core/src/model-pricing";
|
||||||
import { setAutoReloadEnabled } from "../versionCheck";
|
import { setAutoReloadEnabled } from "../versionCheck";
|
||||||
import { DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS, resolveDashboardKeyboardShortcuts, type DashboardKeyboardShortcutMap } from "../utils/keyboardShortcuts";
|
import { DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS, resolveDashboardKeyboardShortcuts, type DashboardKeyboardShortcutMap } from "../utils/keyboardShortcuts";
|
||||||
@@ -34,6 +35,7 @@ export interface UseAppSettingsResult {
|
|||||||
modelPricingOverrides?: ModelPricingOverrides;
|
modelPricingOverrides?: ModelPricingOverrides;
|
||||||
taskDetailChatFirst: boolean;
|
taskDetailChatFirst: boolean;
|
||||||
quickChatButtonMode: QuickChatButtonMode;
|
quickChatButtonMode: QuickChatButtonMode;
|
||||||
|
mobileNavPrimaryItems: string[];
|
||||||
quickChatCloseOnOutsideClick: boolean;
|
quickChatCloseOnOutsideClick: boolean;
|
||||||
dashboardKeyboardShortcuts: Required<DashboardKeyboardShortcutMap>;
|
dashboardKeyboardShortcuts: Required<DashboardKeyboardShortcutMap>;
|
||||||
dismissModalsOnOutsideClick: boolean;
|
dismissModalsOnOutsideClick: boolean;
|
||||||
@@ -93,6 +95,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
|||||||
const [modelPricingOverrides, setModelPricingOverrides] = useState<ModelPricingOverrides | undefined>(undefined);
|
const [modelPricingOverrides, setModelPricingOverrides] = useState<ModelPricingOverrides | undefined>(undefined);
|
||||||
const [taskDetailChatFirst, setTaskDetailChatFirst] = useState(false);
|
const [taskDetailChatFirst, setTaskDetailChatFirst] = useState(false);
|
||||||
const [quickChatButtonMode, setQuickChatButtonMode] = useState<QuickChatButtonMode>("off");
|
const [quickChatButtonMode, setQuickChatButtonMode] = useState<QuickChatButtonMode>("off");
|
||||||
|
const [mobileNavPrimaryItems, setMobileNavPrimaryItems] = useState<string[]>(() => resolveMobileNavPrimaryItems().primaryItems);
|
||||||
const [quickChatCloseOnOutsideClick, setQuickChatCloseOnOutsideClick] = useState(true);
|
const [quickChatCloseOnOutsideClick, setQuickChatCloseOnOutsideClick] = useState(true);
|
||||||
const [dashboardKeyboardShortcuts, setDashboardKeyboardShortcuts] = useState<Required<DashboardKeyboardShortcutMap>>(DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS);
|
const [dashboardKeyboardShortcuts, setDashboardKeyboardShortcuts] = useState<Required<DashboardKeyboardShortcutMap>>(DEFAULT_DASHBOARD_KEYBOARD_SHORTCUTS);
|
||||||
const [dismissModalsOnOutsideClick, setDismissModalsOnOutsideClick] = useState(false);
|
const [dismissModalsOnOutsideClick, setDismissModalsOnOutsideClick] = useState(false);
|
||||||
@@ -161,6 +164,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
|||||||
? "floating"
|
? "floating"
|
||||||
: "off";
|
: "off";
|
||||||
setQuickChatButtonMode(nextQuickChatButtonMode);
|
setQuickChatButtonMode(nextQuickChatButtonMode);
|
||||||
|
setMobileNavPrimaryItems(resolveMobileNavPrimaryItems(settings).primaryItems);
|
||||||
setQuickChatCloseOnOutsideClick(settings.quickChatCloseOnOutsideClick !== false);
|
setQuickChatCloseOnOutsideClick(settings.quickChatCloseOnOutsideClick !== false);
|
||||||
setDashboardKeyboardShortcuts(resolveDashboardKeyboardShortcuts((settings as GlobalSettings).dashboardKeyboardShortcuts));
|
setDashboardKeyboardShortcuts(resolveDashboardKeyboardShortcuts((settings as GlobalSettings).dashboardKeyboardShortcuts));
|
||||||
setDismissModalsOnOutsideClick(settings.dismissModalsOnOutsideClick === true);
|
setDismissModalsOnOutsideClick(settings.dismissModalsOnOutsideClick === true);
|
||||||
@@ -349,6 +353,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
|||||||
modelPricingOverrides,
|
modelPricingOverrides,
|
||||||
taskDetailChatFirst,
|
taskDetailChatFirst,
|
||||||
quickChatButtonMode,
|
quickChatButtonMode,
|
||||||
|
mobileNavPrimaryItems,
|
||||||
quickChatCloseOnOutsideClick,
|
quickChatCloseOnOutsideClick,
|
||||||
dashboardKeyboardShortcuts,
|
dashboardKeyboardShortcuts,
|
||||||
dismissModalsOnOutsideClick,
|
dismissModalsOnOutsideClick,
|
||||||
|
|||||||
@@ -5868,6 +5868,10 @@
|
|||||||
"hardCapOnTheSynthesizedEarlierRoomContext": "Hard cap on the synthesized \"Earlier room context\" summary block. Default: 3000.",
|
"hardCapOnTheSynthesizedEarlierRoomContext": "Hard cap on the synthesized \"Earlier room context\" summary block. Default: 3000.",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
"loweringThisWindowMeansReliabilityMetricsChartsAnd": " Lowering this window means Reliability metrics/charts and the Activity feed will not show history older than the selected range. Per-task task detail history is unaffected. Default: 30 days. ",
|
||||||
|
"mobileNavPrimaryItems": "Mobile footer quick actions",
|
||||||
|
"mobileNavPrimaryItemsHint": "Default: Dashboard, Tasks, Agents, Missions, Chat, Mailbox. Unselected destinations remain in the More menu.",
|
||||||
|
"moveNavItemEarlier": "Move {{item}} earlier",
|
||||||
|
"moveNavItemLater": "Move {{item}} later",
|
||||||
"newTasksInheritThisCustomWorkflowsStepsOverridable": "New tasks inherit this custom workflow's steps (overridable per task). No default — unset (built-in default workflow).",
|
"newTasksInheritThisCustomWorkflowsStepsOverridable": "New tasks inherit this custom workflow's steps (overridable per task). No default — unset (built-in default workflow).",
|
||||||
"numberOfMostRecentChatRoomMessagesKept": "Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.",
|
"numberOfMostRecentChatRoomMessagesKept": "Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
|
|||||||
Reference in New Issue
Block a user