diff --git a/.changeset/fn-7351-activity-tab-dropdown.md b/.changeset/fn-7351-activity-tab-dropdown.md new file mode 100644 index 0000000000..a5794ec595 --- /dev/null +++ b/.changeset/fn-7351-activity-tab-dropdown.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make the task Activity tab switch Live, Feed, and Raw views directly. +category: fix +dev: Removes the duplicate in-panel Activity view select from TaskDetailModal. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 6e4f989eb1..f4f70663ea 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -836,7 +836,8 @@ The same expanded-mode chrome collapse now applies to all Activity segments: Liv } .task-detail-content--chat-expanded .detail-tabs { - display: none; + /* FNXC:TaskDetailActivity 2026-06-30-23:59: Expanded Activity still needs the top-level Activity tab dropdown because the in-panel view selector was removed. Keep the tab strip visible so Live, Feed, and Raw remain switchable while the footer and metadata chrome stay collapsed. */ + display: flex; } .task-detail-content--chat-expanded .modal-actions { @@ -1204,7 +1205,7 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a } .task-detail-content--chat-expanded .detail-tabs { - display: none; + display: flex; } .task-detail-content--chat-expanded .modal-actions { @@ -2500,26 +2501,76 @@ FNXC:TaskDetailTabs 2026-06-26-00:35: /* === Activity View Selector === */ /* -FNXC:TaskDetailActivity 2026-06-30-15:50: -The Activity top-level tab owns Live (internal `current`), Feed, and Raw as one dropdown rather than in-content subtabs. Keep the selector compact and token-styled on desktop/mobile, with the Activity-wide expand control beside it on every view. +FNXC:TaskDetailActivity 2026-06-30-23:59: +The Activity tab itself is the Live/Feed/Raw dropdown trigger. Keep it in the `.detail-tabs` scroller with the same tab sizing and active underline, and keep the Activity panel toolbar only for the expand affordance so no duplicate in-content selector or empty mobile shell remains. */ +.detail-tab-dropdown { + position: relative; + flex-shrink: 0; +} + +.detail-tab--activity { + display: inline-flex; + align-items: center; + gap: var(--space-xs); +} + +.detail-tab-chevron { + inline-size: var(--icon-size-sm); + block-size: var(--icon-size-sm); + flex: 0 0 auto; +} + +.activity-view-menu { + position: absolute; + inset-block-start: 100%; + inset-inline-start: 0; + z-index: 20; + min-inline-size: 100%; + margin-block-start: var(--space-xs); + padding: var(--space-xs); + display: flex; + flex-direction: column; + gap: calc(var(--space-xs) / 2); + background: var(--card); + border: var(--btn-border-width) solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); +} + +.activity-view-menu-item { + appearance: none; + border: 0; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text); + cursor: pointer; + font: inherit; + text-align: start; + padding: var(--space-xs) var(--space-sm); + white-space: nowrap; +} + +.activity-view-menu-item:hover, +.activity-view-menu-item:focus-visible, +.activity-view-menu-item[aria-current="true"] { + background: var(--surface-hover); + outline: none; +} + +.activity-view-menu-item[aria-current="true"] { + color: var(--text); + font-weight: 600; +} + .activity-toolbar { display: flex; align-items: flex-start; - justify-content: space-between; + justify-content: flex-end; gap: var(--space-sm); margin-bottom: var(--space-md); } -.activity-view-select { - min-inline-size: calc(var(--space-2xl) + var(--space-xl) + var(--space-lg)); - min-block-size: var(--space-2xl); - padding-block: calc(var(--space-xs) / 2); - padding-inline: var(--space-sm) var(--space-xl); - font-size: var(--font-size-sm); - line-height: var(--line-height-tight); -} - .activity-expand-toggle { flex: 0 0 auto; min-inline-size: var(--space-2xl); @@ -2544,9 +2595,8 @@ The Activity top-level tab owns Live (internal `current`), Feed, and Raw as one align-items: stretch; } - .activity-view-select { - flex: 1 1 auto; - min-inline-size: 0; + .activity-view-menu { + min-inline-size: calc(100% + var(--space-xl)); } .activity-expand-toggle { diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index b16752a917..dc5fd30df4 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -1,5 +1,5 @@ import "./TaskDetailModal.css"; -import React, { Suspense, lazy, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2 } from "lucide-react"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; @@ -215,8 +215,8 @@ The first Activity segment keeps the stable internal `current` id for legacy seg FNXC:TaskDetailActivity 2026-06-30-23:55: The first Activity segment is user-facing Live while legacy internals remain `current` and explicit `initialTab="chat"` continues landing there for compatibility. -FNXC:TaskDetailActivity 2026-06-30-15:50: -Activity view switching uses one dropdown labeled for Live, Feed, and Raw while retaining the internal `current`, `feed`, and `raw-logs` segment ids. Legacy `chat` and `logs` initial-tab routing remains compatible so older links still open Activity → Live or Activity → Feed. +FNXC:TaskDetailActivity 2026-06-30-23:59: +Activity view switching lives in the top-level Activity tab dropdown for Live, Feed, and Raw while retaining the internal `current`, `feed`, and `raw-logs` segment ids. Legacy `chat` and `logs` initial-tab routing remains compatible so older links still open Activity → Live or Activity → Feed. */ function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId, taskDetailChatFirst = false): TabId { if (initialTab === "retries") { @@ -555,7 +555,6 @@ export function TaskDetailContent({ workflowFieldDefs: workflowFieldDefsProp, }: TaskDetailContentProps) { const { t } = useTranslation("app"); - const activitySelectorId = useId(); const columnLabel = useColumnLabel(); const fileBrowser = useFileBrowser(); const [activeTab, setActiveTab] = useState(() => resolveDefaultTab(initialTab, task.column, taskDetailChatFirst)); @@ -922,6 +921,7 @@ export function TaskDetailContent({ // Split-menu dropdown state for footer actions const [showMoveMenu, setShowMoveMenu] = useState(false); const [showActionsMenu, setShowActionsMenu] = useState(false); + const [showActivityViewMenu, setShowActivityViewMenu] = useState(false); const [sourceIssueExpanded, setSourceIssueExpanded] = useState(false); const [retriesExpanded, setRetriesExpanded] = useState(initialTab === "retries"); const [githubTrackingExpanded, setGithubTrackingExpanded] = useState(false); @@ -934,6 +934,8 @@ export function TaskDetailContent({ const activityListRef = useRef(null); const moveButtonRef = useRef(null); const actionsMenuRef = useRef(null); + const activityViewMenuRef = useRef(null); + const activityViewButtonRef = useRef(null); // Plugin UI slots for task-detail-tab const { getSlotsForId: getPluginSlots } = usePluginUiSlots(projectId); @@ -1267,15 +1269,16 @@ export function TaskDetailContent({ setShowAgentPicker(false); }, [task.id]); - // Close footer dropdown menus on outside click + // Close task-detail dropdown menus on outside click useEffect(() => { - const hasOpenMenu = showMoveMenu || showActionsMenu; + const hasOpenMenu = showMoveMenu || showActionsMenu || showActivityViewMenu; if (!hasOpenMenu) return; const handleClick = (e: MouseEvent) => { const target = e.target as Node; const inMoveMenu = moveMenuRef.current?.contains(target); const inActionsMenu = actionsMenuRef.current?.contains(target); + const inActivityViewMenu = activityViewMenuRef.current?.contains(target); if (!inMoveMenu && showMoveMenu) { setShowMoveMenu(false); @@ -1283,15 +1286,18 @@ export function TaskDetailContent({ if (!inActionsMenu && showActionsMenu) { setShowActionsMenu(false); } + if (!inActivityViewMenu && showActivityViewMenu) { + setShowActivityViewMenu(false); + } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); - }, [showMoveMenu, showActionsMenu]); + }, [showMoveMenu, showActionsMenu, showActivityViewMenu]); - // Close footer dropdown menus on Escape key (before modal Escape handler) + // Close task-detail dropdown menus on Escape key (before modal Escape handler) useEffect(() => { - const hasOpenMenu = showMoveMenu || showActionsMenu; + const hasOpenMenu = showMoveMenu || showActionsMenu || showActivityViewMenu; if (!hasOpenMenu) return; const handleKeyDown = (e: KeyboardEvent) => { @@ -1299,12 +1305,13 @@ export function TaskDetailContent({ e.stopPropagation(); // Prevent modal from closing if (showMoveMenu) setShowMoveMenu(false); if (showActionsMenu) setShowActionsMenu(false); + if (showActivityViewMenu) setShowActivityViewMenu(false); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [showMoveMenu, showActionsMenu]); + }, [showMoveMenu, showActionsMenu, showActivityViewMenu]); // Reset spec edit state when task changes useEffect(() => { @@ -2812,6 +2819,41 @@ export function TaskDetailContent({ closeMoveMenuAndFocusTrigger(); }, [closeMoveMenuAndFocusTrigger]); + const activityViewOptions = useMemo>(() => [ + { value: "current", label: t("taskDetail.activity.current", "Live") }, + { value: "feed", label: t("taskDetail.activity.feed", "Feed") }, + { value: "raw-logs", label: t("taskDetail.activity.raw", "Raw") }, + ], [t]); + const selectedActivityViewLabel = activityViewOptions.find((option) => option.value === activitySegment)?.label ?? activityViewOptions[0]?.label ?? "Live"; + + const selectActivityView = useCallback((value: ActivitySegment) => { + setActiveTab("chat"); + setActivitySegment(value); + setShowActivityViewMenu(false); + }, []); + + const handleActivityTabKeyDown = useCallback((event: React.KeyboardEvent) => { + const shouldOpenMenu = event.key === "ArrowDown" || (event.altKey && event.key === "ArrowDown"); + if (!shouldOpenMenu) { + return; + } + + event.preventDefault(); + setActiveTab("chat"); + setShowActivityViewMenu(true); + }, []); + + const handleActivityViewMenuKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key !== "Escape") { + return; + } + + event.preventDefault(); + event.stopPropagation(); + setShowActivityViewMenu(false); + activityViewButtonRef.current?.focus(); + }, []); + useEffect(() => { if (!showMoveMenu) { return; @@ -2821,6 +2863,58 @@ export function TaskDetailContent({ firstMenuItem?.focus(); }, [showMoveMenu]); + useEffect(() => { + if (!showActivityViewMenu) { + return; + } + + const selectedMenuItem = activityViewMenuRef.current?.querySelector(".activity-view-menu-item[aria-current='true']"); + const firstMenuItem = activityViewMenuRef.current?.querySelector(".activity-view-menu-item"); + (selectedMenuItem ?? firstMenuItem)?.focus(); + }, [showActivityViewMenu]); + + const renderActivityTab = () => ( +
+ {/* + FNXC:TaskDetailActivity 2026-06-30-23:59: + The top-level Activity tab is the only Activity view dropdown trigger. Keep the stable internal `chat` tab id and `current`/`feed`/`raw-logs` segment ids, but remove the in-panel Activity view select so desktop, embedded, and mobile tab strips have one canonical view switcher. + */} + + {showActivityViewMenu && ( +
+ {activityViewOptions.map((option) => ( + + ))} +
+ )} +
+ ); + return (
{t("taskDetail.tabs.chat", "Chat")} - + {renderActivityTab()} ) : ( <> - + {renderActivityTab()}