From 49e2bfa672384bbc479fc7b5323a4be92ad36fd1 Mon Sep 17 00:00:00 2001 From: hopeatina Date: Sun, 1 Mar 2026 10:47:51 -0600 Subject: [PATCH] fix(dashboard): activity filter auto-expand, jargon cleanup, autopilot stop UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate time-window auto-expand behind user scroll + 3s cooldown after manual filter change. Prevents "Last hour" from silently expanding to "Today" when the sentinel is visible on mount with a short list. - Replace developer jargon: "requested dispatch" → "requested a run", "requester -> executor" → "handed off to", "sync replay events" → "background sync events", reword Filters menu description. - Show "Stopping Autopilot…" with disabled state while the run is gracefully stopping, instead of re-enabling "Stop Autopilot" which made it appear that the stop action didn't take effect. Co-Authored-By: Claude Opus 4.6 --- .../components/activity/ActivityTimeline.tsx | 29 +++++++++++++++---- .../mission-control/MissionControlView.tsx | 6 ++-- dashboard/src/hooks/useAutoContinue.ts | 5 +++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/dashboard/src/components/activity/ActivityTimeline.tsx b/dashboard/src/components/activity/ActivityTimeline.tsx index 3607ff83..b820de58 100644 --- a/dashboard/src/components/activity/ActivityTimeline.tsx +++ b/dashboard/src/components/activity/ActivityTimeline.tsx @@ -501,7 +501,7 @@ function resolveActivityActorFlow(item: LiveActivityItem): ActivityActorFlow { executor, mode: 'handoff', primaryLabel: executor.label, - subtitle: `${requester.label} -> ${executor.label}`, + subtitle: `${requester.label} handed off to ${executor.label}`, }; } @@ -511,7 +511,7 @@ function resolveActivityActorFlow(item: LiveActivityItem): ActivityActorFlow { executor: null, mode: 'requested', primaryLabel: requester.label, - subtitle: `${requester.label} requested dispatch`, + subtitle: `${requester.label} requested a run`, }; } @@ -3302,11 +3302,15 @@ export const ActivityTimeline = memo(function ActivityTimeline({ const [sentinelInView, setSentinelInView] = useState(false); const pendingAutoExpandRef = useRef(null); const lastKnownFilterRef = useRef(timeFilterId); + const manualFilterChangedAtRef = useRef(Date.now()); + const userHasScrolledRef = useRef(false); useEffect(() => { if (lastKnownFilterRef.current === timeFilterId) return; lastKnownFilterRef.current = timeFilterId; pendingAutoExpandRef.current = null; + manualFilterChangedAtRef.current = Date.now(); + userHasScrolledRef.current = false; }, [timeFilterId]); useEffect(() => { @@ -3326,7 +3330,14 @@ export const ActivityTimeline = memo(function ActivityTimeline({ ); observer.observe(target); - return () => observer.disconnect(); + const handleScroll = () => { + userHasScrolledRef.current = true; + }; + root.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + observer.disconnect(); + root.removeEventListener('scroll', handleScroll); + }; // Intentionally stable deps — hasMore/isLoadingMore/onLoadMore read // from refs so the observer doesn't get recreated on every poll cycle. }, []); @@ -3351,6 +3362,14 @@ export const ActivityTimeline = memo(function ActivityTimeline({ if (!sentinelInView) return; if (hasMore || isLoadingMore) return; if (!onTimeFilterChange) return; + // Don't auto-expand unless the user has actively scrolled — prevents + // the sentinel from firing on mount when the list is short (e.g. + // "Last hour" with few items) and silently overriding the chosen filter. + if (!userHasScrolledRef.current) return; + // Cooldown: don't auto-expand within 3s of a manual filter change to + // avoid overriding the user's explicit selection. + const msSinceManualChange = Date.now() - manualFilterChangedAtRef.current; + if (msSinceManualChange < 3_000) return; const nextFilter = nextActivityTimeFilter(timeFilterId); if (!nextFilter) return; if (pendingAutoExpandRef.current === nextFilter) return; @@ -4598,10 +4617,10 @@ export const ActivityTimeline = memo(function ActivityTimeline({ : 'border-white/[0.08] bg-white/[0.02] text-secondary hover:bg-white/[0.06] hover:text-primary' )} > - {showSyncEvents ? 'Hide sync replay events' : 'Show sync replay events'} + {showSyncEvents ? 'Hide background sync events' : 'Show background sync events'}

- Changeset replay and outbox sync events are low-signal operational noise for most users. + Background sync events are routine system operations that most users can safely ignore.

diff --git a/dashboard/src/components/mission-control/MissionControlView.tsx b/dashboard/src/components/mission-control/MissionControlView.tsx index f03c00af..763be2d5 100644 --- a/dashboard/src/components/mission-control/MissionControlView.tsx +++ b/dashboard/src/components/mission-control/MissionControlView.tsx @@ -1705,7 +1705,7 @@ function MissionControlInner({ disabled={ autopilotUnavailable || autopilot.isStarting || - autopilot.isStopping + autopilot.isGracefullyStopping } title={ autopilotUnavailable @@ -1773,7 +1773,9 @@ function MissionControlInner({ {autopilotNeedsUpgrade ? 'Upgrade Autopilot' - : `${autopilot.isRunning ? 'Stop' : 'Start'} Autopilot`} + : autopilot.isGracefullyStopping + ? 'Stopping Autopilot…' + : `${autopilot.isRunning ? 'Stop' : 'Start'} Autopilot`} {autopilot.isRunning && hasActiveRuntime && ( diff --git a/dashboard/src/hooks/useAutoContinue.ts b/dashboard/src/hooks/useAutoContinue.ts index a4dfe3f9..bc65b211 100644 --- a/dashboard/src/hooks/useAutoContinue.ts +++ b/dashboard/src/hooks/useAutoContinue.ts @@ -285,12 +285,15 @@ export function useAutoContinue({ }); const run = statusQuery.data?.run ?? null; - const isRunning = run?.status === 'running' || run?.status === 'stopping'; + const runStatus = run?.status ?? null; + const isRunning = runStatus === 'running' || runStatus === 'stopping'; + const isGracefullyStopping = runStatus === 'stopping' || stopMutation.isPending; return { status: statusQuery.data ?? null, run, isRunning, + isGracefullyStopping, isLoading: statusQuery.isLoading, error: statusQuery.data?.error ?? statusQuery.error?.message ?? null, start: startMutation.mutateAsync,