diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index a4cc68e2b0..0b8fc90292 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -123,6 +123,13 @@ const DevServerView = lazy(() => import("./components/DevServerView").then((m) = const TodoView = lazy(() => import("./components/TodoView").then((m) => ({ default: m.TodoView }))); const GoalsView = lazy(() => import("./components/GoalsView").then((m) => ({ default: m.GoalsView }))); const PullRequestView = lazy(() => import("./components/PullRequestView").then((m) => ({ default: m.PullRequestView }))); +/* +FNXC:Navigation 2026-06-22-00:00: +Workflows, Import Tasks (GitHub import), and Automations render as embedded main-content views (presentation="embedded") via these lazy chunks; the same components still mount as modals in AppModals for the mobile overflow path. +*/ +const WorkflowEditorView = lazy(() => import("./components/WorkflowNodeEditor").then((m) => ({ default: m.WorkflowNodeEditor }))); +const ImportTasksView = lazy(() => import("./components/GitHubImportModal").then((m) => ({ default: m.GitHubImportModal }))); +const AutomationsView = lazy(() => import("./components/ScheduledTasksModal").then((m) => ({ default: m.ScheduledTasksModal }))); // Warm lazy chunks during browser idle so first navigation to each view is // instant. Each chunk is ~10–80 kB; total prefetch finishes well under a @@ -1832,6 +1839,58 @@ function AppInner() { ); } + /* + FNXC:Navigation 2026-06-22-00:00: + Workflows, Import Tasks (GitHub import), and Automations are left-sidebar destinations that render embedded in the main content area instead of as modal overlays. Closing returns to the board. The same components still mount as modals in AppModals for the mobile overflow path. + */ + if (taskView === "workflows") { + return ( + + + handleChangeTaskView("board")} + addToast={addToast} + projectId={currentProject?.id} + presentation="embedded" + /> + + + ); + } + + if (taskView === "import-tasks") { + return ( + + + handleChangeTaskView("board")} + onImport={handleGitHubImport} + tasks={tasks} + projectId={currentProject?.id} + presentation="embedded" + /> + + + ); + } + + if (taskView === "automations") { + return ( + + + handleChangeTaskView("board")} + addToast={addToast} + projectId={currentProject?.id} + presentation="embedded" + /> + + + ); + } + if (taskView === "devserver" || taskView === "dev-server") { if (!settingsLoaded || !devServerEnabled) { return null; diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index f7cb4da344..c2661d53c4 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -937,4 +937,29 @@ } } +/* +FNXC:RightDockEmbedding 2026-06-22-00:00: +Right-dock redesign renders the GitHub import surface inline in the main content area instead of as a fixed popup overlay. +The embedded root is a plain flow box that fills the host; the inner shell sheds overlay-only chrome (fixed sizing, box-shadow, rounded corners, resize) and fills 100% so the main panel owns the frame. No close button is rendered in embedded mode. +*/ +.github-import-embedded.right-dock-embedded-view { + display: flex; + width: 100%; + height: 100%; + min-height: 0; +} + +.github-import-modal.github-import-modal--embedded { + width: 100%; + height: 100%; + max-width: none; + min-width: 0; + max-height: none; + min-height: 0; + position: static; + box-shadow: none; + border-radius: 0; + resize: none; +} + diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 52c57f8d2d..2163fe9c85 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -24,6 +24,12 @@ interface GitHubImportModalProps { onImport: (task: Task) => void; tasks: Task[]; projectId?: string; + /* + FNXC:RightDockEmbedding 2026-06-22-00:00: + Right-dock redesign renders the GitHub import surface inline inside the main content area instead of as a fixed popup overlay. + "embedded" drops the modal overlay/close button and disables modal-only chrome (scroll lock, resize persistence, escape/overlay dismiss); "modal" (default) keeps the original byte-identical overlay behavior. + */ + presentation?: "modal" | "embedded"; } // Mobile and two-pane breakpoints in pixels @@ -50,8 +56,9 @@ function formatPreviewBody(body: string | null | undefined, isMobile: boolean) { return body.slice(0, 200) + (body.length > 200 ? "…" : ""); } -export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId }: GitHubImportModalProps) { - useMobileScrollLock(isOpen); +export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) { + const isEmbedded = presentation === "embedded"; + useMobileScrollLock(isOpen && !isEmbedded); const { t } = useTranslation("app"); const [owner, setOwner] = useState(""); const [repo, setRepo] = useState(""); @@ -80,7 +87,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId const [selectedRemoteName, setSelectedRemoteName] = useState(""); const mountedRef = useRef(false); const modalRef = useRef(null); - useModalResizePersist(modalRef, isOpen, "fusion:github-modal-size"); + useModalResizePersist(modalRef, isOpen && !isEmbedded, "fusion:github-modal-size"); const overlayDismissProps = useOverlayDismiss(onClose); // Responsive view state @@ -281,14 +288,15 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId }, [owner, repo, labels, activeTab, isOpen, loading, importing, handleLoad, handleLoadPulls]); // Handle escape key + // FNXC:RightDockEmbedding 2026-06-22-00:00: Escape-to-close is a modal-only affordance; embedded mode has no dismiss. useEffect(() => { - if (!isOpen) return; + if (!isOpen || isEmbedded) return; const handleKey = (e: globalThis.KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); - }, [isOpen, onClose]); + }, [isOpen, isEmbedded, onClose]); // Detect responsive viewport bands useEffect(() => { @@ -480,9 +488,18 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId const showPullsError = Boolean(error) && pulls.length > 0 && !isPullsEmpty; const showInlineErrorBanner = activeTab === "issues" ? showIssuesError : showPullsError; - return ( -
-
+ /* + FNXC:RightDockEmbedding 2026-06-22-00:00: + Embedded mode renders the import surface as a main-content-area view (no fixed .modal-overlay, no close button, no overlay-dismiss). + Modal mode is kept byte-identical: same overlay wrapper, header with subtitle + close button, and overlay-dismiss props. + */ + const inner = ( +
+ {isEmbedded ? ( +
+

{t("git.importTasksHeading", "Import Tasks")}

+
+ ) : (

{t("git.importFromGitHub", "Import from GitHub")}

@@ -494,6 +511,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId ×
+ )}
{/* Tab Navigation */} @@ -861,7 +879,16 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId {importing ? : t("git.import", "Import")}
-
+
+ ); + + if (isEmbedded) { + return
{inner}
; + } + + return ( +
+ {inner}
); } diff --git a/packages/dashboard/app/components/Header.tsx b/packages/dashboard/app/components/Header.tsx index 9a7d7e4b6d..103056365b 100644 --- a/packages/dashboard/app/components/Header.tsx +++ b/packages/dashboard/app/components/Header.tsx @@ -945,8 +945,11 @@ export function Header({ FN-6886 removes the header Lightbulb affordances because Planning Mode is now a primary left-sidebar destination after Command Center and a single canonical MobileNavBar More item on compact breakpoints. */} - {/* Workflows - desktop only (moved to overflow on mobile/tablet) */} - {!isCompact && onOpenWorkflowEditor && ( + {/* + FNXC:Navigation 2026-06-22-00:00: + 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. + */} + {!isCompact && !leftSidebarNavActive && onOpenWorkflowEditor && ( + +
+ + + {t("schedule.automationCount", "{{count}} automation{{plural}}", { count: routines.length, plural: routines.length === 1 ? "" : "s" })} + +
+
+ {isShowingList && ( + + )} +
+ + ); + + // ── Embedded (main-content-area) presentation ─────────────────────────── + // FNXC:AutomationsEmbedded 2026-06-22-00:00: + // Renders inline like Command Center: no overlay/close, a plain .cc-header title row, --space-lg view padding, + // no card chrome. The body is a responsive two-pane layout: a left list pane and a right detail pane that + // collapse to a single column below ~900px (see .automations-embedded CSS). In list view the left pane shows a + // compact selectable rail; selecting a routine renders its full RoutineCard on the right. In create/edit view the + // editor spans the full width. + if (isEmbedded) { + const isListView = routineView === "list"; + return ( +
+
+
+

+ + {t("schedule.title", "Automations")} +

+
+ + {toolbar} + + {isListView && routines.length > 0 ? ( +
+ {/* Left pane: compact selectable list of automations */} +
+ {routines.map((r) => ( + + ))} +
+ + {/* Right pane: detail for the selected automation, or an empty prompt */} +
+ {selectedRoutine ? ( +
+ +
+ ) : ( +
+ +

{t("schedule.selectAutomation", "Select an automation")}

+

{t("schedule.selectAutomationHint", "Choose an automation from the list to view its details.")}

+
+ )} +
+
+ ) : ( + // Empty state, create, and edit views span the full width (single column). +
+ {renderContent()} +
+ )} +
+
+ ); + } + + // ── Modal (fixed overlay) presentation ────────────────────────────────── return (
@@ -299,48 +453,7 @@ export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledT
-
-
-
- - -
- - - {t("schedule.automationCount", "{{count}} automation{{plural}}", { count: routines.length, plural: routines.length === 1 ? "" : "s" })} - -
-
- {isShowingList && ( - - )} -
-
+ {toolbar}
{renderContent()} diff --git a/packages/dashboard/app/components/ScriptsModal.css b/packages/dashboard/app/components/ScriptsModal.css index 5136f6f8bd..f715cc0b4d 100644 --- a/packages/dashboard/app/components/ScriptsModal.css +++ b/packages/dashboard/app/components/ScriptsModal.css @@ -1215,6 +1215,132 @@ The embedded root is a plain flow box that fills the dock; the inner panel sheds resize: none; } +/* +FNXC:AutomationsEmbedded 2026-06-22-00:00: +Automations can render inline in the main content area (presentation="embedded") instead of as a fixed modal overlay. +The embedded root fills its host and sheds all modal chrome — no overlay, no card/shadow/border/radius — so the view +blends into the main panel like Command Center. The view container carries --space-lg padding and a plain .cc-header +title row (reused from Command Center). The body is a responsive two-pane layout (list + detail) via container query +when supported, falling back to a min-width media breakpoint, that collapses to a single column below ~900px. +*/ +.automations-embedded.right-dock-embedded-view { + display: flex; + width: 100%; + height: 100%; + min-height: 0; + background: none; + box-shadow: none; + border: none; + border-radius: 0; +} + +.automations-embedded-view { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--space-lg); + min-height: 0; + inline-size: 100%; + padding: var(--space-lg); + /* Enable container-query-driven two-pane breakpoint scoped to the view's own width, not the viewport. */ + container-type: inline-size; + overflow-y: auto; +} + +/* Two-pane body: single column by default (narrow); two columns when the container is wide enough. */ +.automations-two-pane { + display: grid; + grid-template-columns: 1fr; + gap: var(--space-lg); + min-height: 0; + flex: 1; +} + +.automations-single-pane { + min-height: 0; + flex: 1; +} + +/* Left list rail */ +.automations-list-pane { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 0; +} + +.automation-list-row { + display: flex; + align-items: center; + gap: var(--space-sm); + width: 100%; + padding: var(--space-sm) var(--space-md); + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text); + font-size: 0.875rem; + text-align: left; + cursor: pointer; + transition: border-color var(--transition-fast), background var(--transition-fast); +} + +.automation-list-row:hover { + border-color: var(--accent); +} + +.automation-list-row.active { + border-color: var(--accent); + background: var(--accent-subtle, var(--card)); +} + +.automation-list-row-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.automation-list-row-badge { + flex-shrink: 0; + padding: 0 var(--space-sm); + border-radius: var(--radius-sm); + background: var(--bg); + border: 1px solid var(--border); + color: var(--text-muted); + font-size: 0.6875rem; +} + +/* Right detail pane */ +.automations-detail-pane { + min-width: 0; + min-height: 0; +} + +.automations-detail-empty { + height: 100%; +} + +/* Two columns once the embedded container is wide enough (~900px). */ +@container (min-width: 900px) { + .automations-two-pane { + grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); + align-items: start; + } +} + +/* +Fallback for browsers without container-query support: use a viewport media query. Harmless where container +queries already apply (the container-query rule above also fires and produces the same two-column layout). +*/ +@media (min-width: 900px) { + .automations-two-pane { + grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); + align-items: start; + } +} + .activity-log-header { /* Extends shared .modal-header with activity-log-specific overrides */ gap: var(--space-sm); diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index 76291dcc97..689e155c6d 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -14,6 +14,35 @@ border-radius: var(--radius-md); } +/* +FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: +Embedded presentation renders the editor inline as a main-content-area view +filling the right-dock panel instead of as a centered fixed modal. The wrapper +takes the full panel box and the modal element drops its modal chrome +(fixed sizing, box-shadow, border-radius, resize grip) so it reads as a flush +embedded view. +*/ +.workflow-editor-embedded { + display: flex; + width: 100%; + height: 100%; + min-height: 0; +} + +.wf-editor-modal--embedded { + width: 100%; + height: 100%; + max-width: none; + max-height: none; + min-width: 0; + min-height: 0; + position: static; + resize: none; + box-shadow: none; + border: none; + border-radius: 0; +} + .wf-create-modal { --wf-editor-touch-target: calc(var(--space-xl) + var(--space-lg) + var(--space-xs)); } diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 96bb377571..ea87164ea7 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -197,6 +197,17 @@ interface WorkflowNodeEditorProps { initialAction?: "create"; /** Workflow id to preselect when the editor opens from workflow-aware surfaces. */ initialWorkflowId?: string; + /* + FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: + The workflow editor can render either as a fixed modal overlay ("modal", the + default and historical behavior) or inline as a main-content-area view + ("embedded") that fills the right-dock panel like a Command Center view. + In embedded mode the editor drops the .modal-overlay shell, the X close + button, native resize, and all modal-only dismiss paths (Escape, overlay + click) so it reads as a persistent view rather than a dismissible dialog. + The modal path stays byte-identical when presentation is "modal"/undefined. + */ + presentation?: "modal" | "embedded"; } let nodeSeq = 0; @@ -697,7 +708,11 @@ function InnerEditor({ initialAction, initialWorkflowId, modalRef, -}: Omit & { modalRef: React.RefObject }) { + isEmbedded = false, +}: Omit & { + modalRef: React.RefObject; + isEmbedded?: boolean; +}) { const [workflows, setWorkflows] = useState([]); const [activeId, setActiveId] = useState(null); const viewportMode = useViewportMode(); @@ -2435,11 +2450,15 @@ function InnerEditor({ ) : null; - return ( - <> -
+ // FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: + // Embedded mode renders the editor inline inside the right-dock panel: no + // fixed .modal-overlay shell, no overlay-click dismiss, no Escape-to-close, + // and a --embedded sized variant of the modal element. The modal path stays + // byte-identical (same overlay + overlayProps + Escape handler) when not + // embedded. + const modalElement = (
e.stopPropagation()} onKeyDown={(e) => { @@ -2447,6 +2466,8 @@ function InnerEditor({ // Ignore Escape originating from inputs/textareas/selects so inline // editors (name/description) keep their own Escape-to-cancel behavior. if (e.key !== "Escape") return; + // Embedded views are persistent; Escape must not dismiss them. + if (isEmbedded) return; // The create dialog (rendered as a child) owns its own Escape; if it's // open, let it handle the event (it stops propagation already). if (createOpen) return; @@ -2459,9 +2480,13 @@ function InnerEditor({ >

{t("workflows.title", "Workflows")}

- + {/* FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: embedded views keep a + Command Center-style header title but drop the modal X close button. */} + {!isEmbedded ? ( + + ) : null}
{showMigrationNotice ? ( @@ -4598,7 +4623,20 @@ function InnerEditor({ /> )}
-
+ ); + return ( + <> + {isEmbedded ? ( + // FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: inline main-content + // wrapper (no fixed overlay, no overlayProps overlay-click dismiss). +
+ {modalElement} +
+ ) : ( +
+ {modalElement} +
+ )} {promptFullscreenOverlay} ); @@ -4612,9 +4650,14 @@ export function WorkflowNodeEditor({ initialPanel, initialAction, initialWorkflowId, + presentation = "modal", }: WorkflowNodeEditorProps) { const modalRef = useRef(null); - useModalResizePersist(modalRef, isOpen, "fusion:workflow-node-editor-size"); + const isEmbedded = presentation === "embedded"; + // FNXC:WorkflowEditorEmbedding 2026-06-22-00:00: + // Size persistence + native resize are modal-only; an embedded view fills its + // host panel (width/height:100%) so persisting a saved pixel size is wrong. + useModalResizePersist(modalRef, isOpen && !isEmbedded, "fusion:workflow-node-editor-size"); if (!isOpen) return null; return ( @@ -4626,6 +4669,7 @@ export function WorkflowNodeEditor({ initialAction={initialAction} initialWorkflowId={initialWorkflowId} modalRef={modalRef} + isEmbedded={isEmbedded} /> ); diff --git a/packages/dashboard/app/components/overflowViewRegistry.tsx b/packages/dashboard/app/components/overflowViewRegistry.tsx index a07b2107a5..b389ceec30 100644 --- a/packages/dashboard/app/components/overflowViewRegistry.tsx +++ b/packages/dashboard/app/components/overflowViewRegistry.tsx @@ -1,10 +1,8 @@ import { Suspense, type ComponentType, type ReactNode } from "react"; import { Activity, - Clock, Folder, GitBranch, - GitPullRequestArrow, History, type LucideProps, } from "lucide-react"; @@ -25,10 +23,8 @@ import { GitManagerModal } from "./GitManagerModal"; export type OverflowViewKey = | "usage" | "activity-log" - | "github-import" | "git-manager" | "files" - | "automation" | `plugin:${string}:${string}`; export interface OverflowViewFeatureState { @@ -149,13 +145,6 @@ export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [ />, ), }, - { - key: "github-import", - label: "Import from GitHub", - icon: GitPullRequestArrow, - testId: "right-dock-tab-github-import", - onActivate: (props) => props.onOpenGitHubImport?.(), - }, { key: "git-manager", label: "Git Manager", @@ -179,13 +168,6 @@ export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [ testId: "right-dock-tab-files", render: (props) => wrapOverflowView(), }, - { - key: "automation", - label: "Automation", - icon: Clock, - testId: "right-dock-tab-automation", - onActivate: (props) => props.onOpenSchedules?.(), - }, ]; function buildPluginOverflowViewEntries(pluginDashboardViews: PluginDashboardViewEntry[] = []): OverflowViewEntry[] { diff --git a/packages/dashboard/app/hooks/useViewState.ts b/packages/dashboard/app/hooks/useViewState.ts index 51d10a23e4..d38d8185a9 100644 --- a/packages/dashboard/app/hooks/useViewState.ts +++ b/packages/dashboard/app/hooks/useViewState.ts @@ -5,7 +5,11 @@ import { getScopedItem, setScopedItem } from "../utils/projectStorage"; import { getPluginViewId, isPluginViewId, isPluginViewRegistered } from "../plugins/pluginViewRegistry"; export type ViewMode = "overview" | "project"; -export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "evals" | "goalsView" | "todos" | "planning" | "skills" | "mailbox" | "insights" | "memory" | "command-center" | "secrets" | "devserver" | "dev-server" | "pull-requests"; +/* +FNXC:ViewState 2026-06-22-00:00: +Workflows, Import Tasks, and Automations are promoted to top-level main-content task views (left-sidebar destinations) instead of modal-only overlays, so they render in the main panel like Command Center. +*/ +export type BuiltInTaskView = "board" | "list" | "graph" | "agents" | "missions" | "chat" | "documents" | "research" | "evals" | "goalsView" | "todos" | "planning" | "skills" | "mailbox" | "insights" | "memory" | "command-center" | "secrets" | "devserver" | "dev-server" | "pull-requests" | "workflows" | "import-tasks" | "automations"; export type PluginTaskView = `plugin:${string}:${string}`; export type TaskView = BuiltInTaskView | PluginTaskView; @@ -40,6 +44,9 @@ const BUILT_IN_TASK_VIEWS: readonly BuiltInTaskView[] = [ "devserver", "dev-server", "pull-requests", + "workflows", + "import-tasks", + "automations", ]; function isBuiltInTaskView(value: string | null): value is BuiltInTaskView {