FN-8348: relocate report actions to settings and command center

Relocate guided reporting from the header into durable project and overview surfaces.

- Add Report entry points to Project General settings and Command Center Overview.
- Preserve guided report modal context through a shared route helper.
- Cover relocated controls and update dashboard guidance.

Files changed:
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/Header.tsx       | 33 ++++------------------
 .../dashboard/app/components/SettingsModal.tsx     |  2 +-
 .../app/components/__tests__/Header.test.tsx       |  7 +++++
 .../components/command-center/CommandCenter.css    | 32 +++++++++++++++++++++
 .../components/command-center/CommandCenter.tsx    | 30 +++++++++++++++++++-
 .../__tests__/CommandCenter.test.tsx               | 11 ++++++++
 .../settings/sections/GeneralSection.tsx           | 21 ++++++++++++++
 .../GeneralSection.aiUndoWorkflow.test.tsx         | 20 +++++++++++--
 packages/dashboard/app/utils/reportContextRefs.ts  | 13 +++++++++
 10 files changed, 139 insertions(+), 32 deletions(-)

Fusion-Task-Id: FN-8348

Fusion-Task-Lineage: add1b302-deab-4e17-be2f-832971c1c83d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-19 15:12:23 -07:00
parent f85acce548
commit bea51f7bdb
10 changed files with 139 additions and 32 deletions

View File

@@ -2117,7 +2117,7 @@ In **Settings → General**, choose up to six Mobile footer quick actions from t
## In-app reports
The Header **Report** menu is available on desktop and mobile and offers **Bug**, **Feedback**, **Idea**, and **Help**. Each action begins with a short, guided prompt rather than a raw GitHub issue form.
The **Report** menu is available in **Settings → General · Project** and the Command Center **Overview** on desktop and mobile. It offers **Bug**, **Feedback**, **Idea**, and **Help**; each action begins with a short, guided prompt rather than a raw GitHub issue form.
Fusion gathers available task/agent context, structures the prompt into a report, scrubs secrets, local paths, project names, home-directory identities, email addresses, and likely personal names, then checks **open** GitHub issues or Discussions for duplicates. Scrubbing is mandatory for every route and is repeated on the server when a reviewed draft is edited before filing. A strong duplicate receives a visible 👍 reaction and one scrubbed data-point comment instead of a new issue or Discussion. Bug and Idea reports use issues; Feedback and unresolved Help reports use repository Discussions. If preparation or filing cannot reach GitHub, Fusion preserves the draft and shows a retryable error.

View File

@@ -17,28 +17,12 @@ import type { PluginDashboardViewEntry } from "../api";
import { buildPluginTaskViewId, isPluginViewId } from "../plugins/pluginViewRegistry";
import { getPluginNavIcon } from "./pluginNavIcon";
import type { ShellHostContext } from "../shell-host";
import type { ReportActionType } from "@fusion/core";
import { ReportActionMenu } from "./ReportActionMenu";
import { ReportModal } from "./ReportModal";
export { resolveReportContextRefs } from "../utils/reportContextRefs";
export { useViewportMode };
const NO_BRANCH_FILTER_VALUE = "__fusion:no-branch__";
/**
* FNXC:ReportPipeline 2026-07-16-19:30:
* Reports opened from task detail routes must gather the active task's logs,
* agent, and errors. Task navigation uses hash routes, while legacy links use
* query parameters, so resolve both at modal-open render time.
*/
export function resolveReportContextRefs(location: Pick<Location, "hash" | "search">): { taskId?: string; agentId?: string } | undefined {
const params = new URLSearchParams(location.search);
const hashTaskMatch = location.hash.match(/^#\/tasks\/([^/?#]+)/);
const taskId = hashTaskMatch?.[1] ? decodeURIComponent(hashTaskMatch[1]) : params.get("taskId") ?? undefined;
const agentId = params.get("agentId") ?? undefined;
return taskId || agentId ? { taskId, agentId } : undefined;
}
// Status icon config for project selector dropdown
const PROJECT_STATUS_CONFIG: Record<ProjectStatus, { color: string }> = {
active: { color: "var(--success)" },
@@ -189,10 +173,6 @@ export function Header({
const isTablet = mode === "tablet";
const isCompact = isMobile || isTablet;
const hideFullNav = isMobile && mobileNavEnabled;
const [reportAction, setReportAction] = useState<ReportActionType | null>(null);
// Resolve on every render so opening a report after in-app hash navigation
// uses the current task rather than the Header's initial route.
const reportContextRefs = typeof window === "undefined" ? undefined : resolveReportContextRefs(window.location);
/*
FNXC:Navigation 2026-06-19-00:00:
When experimental left sidebar navigation is active on tablet/desktop, Header must suppress its view-toggle and More-views trigger so there is one canonical non-mobile navigation surface and no orphaned chevron remains.
@@ -1000,12 +980,12 @@ export function Header({
When the left sidebar is active it owns Workflows as a main-content destination, so the Header drops its duplicate desktop Workflow button. The flag-off desktop layout keeps the Header button; mobile/tablet keep the overflow entry.
*/}
{/*
FNXC:ReportPipeline 2026-07-16-21:30:
Bug, Feedback, Idea, and Help must remain reachable at every responsive
breakpoint. Compact navigation can hide the overflow trigger entirely,
so the report menu lives directly in the header action row instead.
FNXC:ReportPipeline 2026-07-18-19:25:
FN-8348 relocates guided Bug, Feedback, Idea, and Help reporting from
Header to Settings General and Command Center. Those always-reachable
destinations replace this responsive action-row slot without leaving a
compact-navigation dependency or an empty header control.
*/}
<ReportActionMenu onSelect={setReportAction} />
{!isCompact && !leftSidebarNavActive && onOpenWorkflowEditor && (
<button
@@ -1338,7 +1318,6 @@ export function Header({
)}
</div>
)}
{reportAction && <ReportModal actionType={reportAction} contextRefs={reportContextRefs} onClose={() => setReportAction(null)} />}
</div>
);
}

View File

@@ -491,7 +491,7 @@ export const SETTINGS_SECTIONS: SettingsSection[] = [
Import auto-translation lives in Project General beside the other import-scoped GitHub settings, but operators look for it by what it DOES ("translate", "language", "auto translate issues"), not by the section it happens to live in.
FNXC:SettingsSearch 2026-07-15-19:10: the per-setting index now matches these controls on their own label and help text, so the terms that merely restate the copy are no longer load-bearing. The list is kept for the genuine vocabulary gaps — "localize", "localization", "foreign language issues" — which appear nowhere in the copy, and because unmigrated siblings in this section still rely on section-level keywords.
*/
{ id: "general", label: "General · Project", labelKey: "settings.nav.projectGeneral", scope: "project", searchableText: ["project general", "Completion Documentation Automation", "Quick Chat launcher", "ephemeral task-worker agents", "chat rooms", "auto-cleanup old chats", "translate", "translation", "auto translate", "auto-translate", "autotranslate", "auto translate issues", "translate issues", "translate imported issues", "githubImportAutoTranslate", "importTranslateTargetLocale", "target language", "translation target language", "translation language", "language", "foreign language issues", "import language", "localize", "localization"], searchableKeys: ["settings.general.autoTranslateImportedIssues", "settings.general.autoTranslateImportedIssuesHelp", "settings.general.translationTargetLanguage", "settings.general.translationTargetLanguageHelp", "settings.general.followDashboardLanguage"] },
{ id: "general", label: "General · Project", labelKey: "settings.nav.projectGeneral", scope: "project", searchableText: ["project general", "Completion Documentation Automation", "Quick Chat launcher", "ephemeral task-worker agents", "chat rooms", "auto-cleanup old chats", "translate", "translation", "auto translate", "auto-translate", "autotranslate", "auto translate issues", "translate issues", "translate imported issues", "githubImportAutoTranslate", "importTranslateTargetLocale", "target language", "translation target language", "translation language", "language", "foreign language issues", "import language", "localize", "localization", "report", "report bug", "send feedback", "share idea", "get help"], searchableKeys: ["settings.general.autoTranslateImportedIssues", "settings.general.autoTranslateImportedIssuesHelp", "settings.general.translationTargetLanguage", "settings.general.translationTargetLanguageHelp", "settings.general.followDashboardLanguage"] },
{ id: "commands", label: "Commands & Scripts", labelKey: "settings.nav.commands", scope: "project", searchableText: ["test command", "build command", "verification command", "workflow scripts", "commands"] },
{ id: "worktrees", label: "Worktrees", labelKey: "settings.nav.worktrees", scope: "project", searchableText: ["worktree directory", "copy files", "recycle worktrees", "branch naming", "sibling branch rename"] },
{ id: "merge", label: "Merge", labelKey: "settings.nav.merge", scope: "project", searchableText: ["auto merge", "AI merge", "merge strategy", "plan approval", "direct merge", "integration branch", "push after merge"] },

View File

@@ -52,6 +52,7 @@ describe("Header", () => {
it("derives report context from task hash routes and legacy query parameters", () => {
expect(resolveReportContextRefs({ hash: "#/tasks/FN-8277", search: "?agentId=agent-1" })).toEqual({ taskId: "FN-8277", agentId: "agent-1" });
expect(resolveReportContextRefs({ hash: "", search: "?taskId=FN-8277" })).toEqual({ taskId: "FN-8277", agentId: undefined });
expect(resolveReportContextRefs({ hash: "#/command-center", search: "" })).toBeUndefined();
});
beforeEach(() => {
@@ -64,6 +65,12 @@ describe("Header", () => {
expect(screen.getByText("Fusion")).toBeDefined();
});
it.each(["desktop", "tablet", "mobile"] as const)("does not render the relocated Report affordance in the %s header", (tier) => {
renderHeader({}, tier);
expect(screen.queryByRole("button", { name: "Report" })).toBeNull();
expect(screen.queryByText("Report bug")).toBeNull();
});
it("applies shell host metadata on the header root", () => {
const { container } = renderHeader({ shellHost: { kind: "desktop-shell", mode: "remote", canOpenConnectionManager: true } });
expect(container.querySelector("header.header")?.getAttribute("data-shell-kind")).toBe("desktop-shell");

View File

@@ -711,3 +711,35 @@ Add a deliberate vertical gap between the Start/Stop AI Engine action and the Vi
flex: 1 1 auto;
justify-content: center;
}
/*
FNXC:ReportPipeline 2026-07-18-19:35:
FN-8348 keeps the Command Center report home in the Overview card rhythm. The
row wraps at the shared mobile breakpoint so its guided action remains usable
without creating a second scroll owner or a one-off button treatment.
*/
.cc-report-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
padding: var(--space-md);
}
.cc-report-actions__title {
margin: 0;
color: var(--text);
font-size: 1rem;
}
.cc-report-actions p {
margin: var(--space-xs) 0 0;
color: var(--text-muted);
}
@media (max-width: 768px) {
.cc-report-actions {
align-items: flex-start;
flex-direction: column;
}
}

View File

@@ -1,12 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertCircle, Gauge } from "lucide-react";
import type { ActivityAnalytics, ColorTheme, LiveSnapshot, SignalsAnalytics, ThemeMode, TokenAnalytics, ToolAnalytics, TaskVerificationRequest } from "@fusion/core";
import type { ActivityAnalytics, ColorTheme, LiveSnapshot, ReportActionType, SignalsAnalytics, ThemeMode, TokenAnalytics, ToolAnalytics, TaskVerificationRequest } from "@fusion/core";
import { api, fetchCodebaseMetrics, withProjectId, type CodebaseMetrics } from "../../api/legacy";
import { formatBytes } from "../../utils/formatBytes";
import { DateRangePicker, defaultPresets, rangeFromPreset, type DateRange } from "./DateRangePicker";
import { LoadingSpinner } from "../LoadingSpinner";
import { TaskVerificationStatus } from "../TaskVerificationStatus";
import { ReportActionMenu } from "../ReportActionMenu";
import { ReportModal } from "../ReportModal";
import { resolveReportContextRefs } from "../../utils/reportContextRefs";
import { TokensArea } from "./areas/TokensArea";
import { ToolsArea } from "./areas/ToolsArea";
import { ActivityArea } from "./areas/ActivityArea";
@@ -149,6 +152,30 @@ interface CommandCenterProps {
onChangeView?: (view: TaskView) => void;
}
/**
* FNXC:ReportPipeline 2026-07-18-19:35:
* FN-8348 makes the Command Center Overview a second canonical report home.
* Keeping the trigger with its modal in this always-rendered controls fragment
* preserves the guided pipeline and task/agent deep-link context in loading,
* empty, error, and populated dashboard states.
*/
function ReportActionCard() {
const { t } = useTranslation("app");
const [reportAction, setReportAction] = useState<ReportActionType | null>(null);
const contextRefs = typeof window === "undefined" ? undefined : resolveReportContextRefs(window.location);
return (
<section className="card cc-report-actions" data-testid="command-center-report-actions">
<div>
<h3 className="cc-report-actions__title">{t("commandCenter.report.title", "Report")}</h3>
<p>{t("commandCenter.report.description", "Report a bug, send feedback, share an idea, or get help.")}</p>
</div>
<ReportActionMenu onSelect={setReportAction} />
{reportAction && <ReportModal actionType={reportAction} contextRefs={contextRefs} onClose={() => setReportAction(null)} />}
</section>
);
}
function OverviewTab({
range,
projectId,
@@ -381,6 +408,7 @@ function OverviewTab({
onShadcnCustomColorsChange={onShadcnCustomColorsChange}
onChangeView={onChangeView}
/>
<ReportActionCard />
</>
);
const throughputSection = (

View File

@@ -483,6 +483,17 @@ describe("CommandCenter shell", () => {
expect(screen.queryByTestId("cc-controls-heartbeat")).toBeNull();
});
it("renders the report menu and opens its guided modal in every Overview state", async () => {
mockEmptyOverviewApi();
render(<CommandCenter />);
expect(screen.getByTestId("command-center-report-actions")).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Report" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Report bug" }));
expect(await screen.findByRole("dialog", { name: "bug report" })).toBeTruthy();
});
it("renders throughput last while the Overview branch is loading", () => {
mockEmptyOverviewApi();
render(<CommandCenter />);

View File

@@ -7,6 +7,9 @@ import { SettingsSelectRow } from "../SettingsSelectRow";
import { SettingsNumberRow } from "../SettingsNumberRow";
import { SettingsTextRow } from "../SettingsTextRow";
import { SettingsHelpTip } from "../SettingsHelpTip";
import { ReportActionMenu } from "../../ReportActionMenu";
import { ReportModal } from "../../ReportModal";
import { resolveReportContextRefs } from "../../../utils/reportContextRefs";
/*
FNXC:GitHubImportTranslate 2026-07-15-09:30:
Locale labels come from core's shared `localeDisplayName` (endonyms), NOT from the LanguageSelector component: importing a component module for a constant drags its i18n/react-i18next initialization into every consumer of this section, which breaks tests that mock react-i18next narrowly.
@@ -44,6 +47,8 @@ GitHub/GitLab settings are NOT in this section. The tracking block, the tracking
export function GeneralSection({ form, setForm, projectId, addToast, prefixError, setPrefixError, onQuickChatButtonModeChange, onMobileNavPrimaryItemsChange, }: GeneralSectionProps) {
const { t } = useTranslation("app");
const [builtinWorkflows, setBuiltinWorkflows] = useState<WorkflowDefinition[]>([]);
const [reportAction, setReportAction] = useState<ReportActionType | null>(null);
const reportContextRefs = typeof window === "undefined" ? undefined : resolveReportContextRefs(window.location);
useEffect(() => {
let cancelled = false;
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
@@ -135,6 +140,21 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
};
return (<>
<h4 className="settings-section-heading">{t("settings.general.general", "General")}</h4>
{/*
FNXC:ReportPipeline 2026-07-18-19:30:
FN-8348 makes Settings General a canonical report entry point so Bug,
Feedback, Idea, and Help stay available when compact Header navigation
hides actions. Reuse the shared menu and modal to preserve guided-report
capture and deep-link task/agent context.
*/}
<SettingsFieldRow
htmlFor="report-action-menu"
label={t("settings.general.report", "Report")}
help={t("settings.general.reportHelp", "Report a bug, send feedback, share an idea, or get help from Fusion.")}
scope="project"
>
<ReportActionMenu onSelect={setReportAction} />
</SettingsFieldRow>
{/*
FNXC:SettingsGeneral 2026-07-15-17:35:
A blank prefix stores `undefined`, not "": empty means "no prefix configured" and must delete the
@@ -703,6 +723,7 @@ export function GeneralSection({ form, setForm, projectId, addToast, prefixError
<button type="button" className="btn btn-sm" onClick={handleClearLocalData}>{t("settings.general.clearLocalDataButton", "Clear local data")}</button>
</div>
</div>
{reportAction && <ReportModal actionType={reportAction} contextRefs={reportContextRefs} onClose={() => setReportAction(null)} />}
</>);
}
export default GeneralSection;

View File

@@ -12,10 +12,12 @@ import { render, screen, fireEvent, cleanup, waitFor } from "@testing-library/re
import * as jestDomMatchers from "@testing-library/jest-dom/matchers";
import { GeneralSection } from "../GeneralSection";
import { SETTINGS_SECTIONS } from "../../../SettingsModal";
import type { SettingsFormState } from "../context";
import { fetchWorkflows } from "../../../../api";
vi.mock("react-i18next", () => ({
vi.mock("react-i18next", async (importOriginal) => ({
...await importOriginal<typeof import("react-i18next")>(),
useTranslation: () => ({
t: (_key: string, fallback?: string) => fallback ?? _key,
}),
@@ -69,7 +71,21 @@ function GeneralHost({ initialForm, onSetForm }: {
);
}
describe("GeneralSection - AI-undo task workflow picker", () => {
describe("GeneralSection - project controls", () => {
it("renders the report menu and opens its guided modal", async () => {
render(<GeneralHost initialForm={{}} />);
fireEvent.click(screen.getByRole("button", { name: "Report" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Report bug" }));
expect(await screen.findByRole("dialog", { name: "bug report" })).toBeInTheDocument();
});
it("advertises report actions through General settings search metadata", () => {
const general = SETTINGS_SECTIONS.find((section) => section.id === "general");
expect(general?.searchableText).toEqual(expect.arrayContaining(["report", "report bug", "send feedback", "share idea", "get help"]));
});
it("shows builtin:review-heavy as the effective default when unset", async () => {
render(<GeneralHost initialForm={{}} />);

View File

@@ -0,0 +1,13 @@
/**
* FNXC:ReportPipeline 2026-08-02-00:00:
* FN-8348 moves report entry points from Header into Settings General and Command
* Center. Resolve task and agent context in this shared helper so each home keeps
* the same deep-link behavior without duplicating route parsing.
*/
export function resolveReportContextRefs(location: Pick<Location, "hash" | "search">): { taskId?: string; agentId?: string } | undefined {
const params = new URLSearchParams(location.search);
const hashTaskMatch = location.hash.match(/^#\/tasks\/([^/?#]+)/);
const taskId = hashTaskMatch?.[1] ? decodeURIComponent(hashTaskMatch[1]) : params.get("taskId") ?? undefined;
const agentId = params.get("agentId") ?? undefined;
return taskId || agentId ? { taskId, agentId } : undefined;
}