[aidan] ux/workflows: surface paused state on card, sidebar, and calendar; tidy run history

This commit is contained in:
abccodes
2026-06-18 23:30:43 -07:00
parent c31748ca53
commit d401e11ebe
4 changed files with 31 additions and 17 deletions
@@ -34,6 +34,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const workflows = useAppSelector((s) => Object.values(s.workflows.items));
const allPaused = useAppSelector((s) => s.workflows.paused);
// Right-click menu: pinned position + the workflow whose pill was
// clicked. Same anchor pattern as MUI's menu examples.
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; workflow: Workflow } | null>(null);
@@ -236,6 +237,7 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
sx={{ height: SLOT_H, borderLeft: `1px solid ${c.border.subtle}`, borderTop: hourIdx === 0 ? 'none' : `1px solid ${c.border.subtle}`, position: 'relative' }}>
<EventStack
events={evs}
paused={allPaused}
onSelectWorkflow={onSelectWorkflow}
eventFontSize={EVENT_FS}
onContextWorkflow={(wf, ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, workflow: wf }); }}
@@ -374,8 +376,9 @@ export default function ScheduleCalendar({ view, density, onSelectWorkflow, refD
// Apple Calendar style event chip: 3px colored left-bar + faintly-tinted
// background + readable text. One chip per cell with a "+N" badge for
// overflow; clicking it opens a popover listing all events that hour.
function EventStack({ events, onSelectWorkflow, eventFontSize, onContextWorkflow }: {
function EventStack({ events, paused, onSelectWorkflow, eventFontSize, onContextWorkflow }: {
events: { workflow: Workflow; date: Date }[];
paused?: boolean;
onSelectWorkflow?: (id: string) => void;
eventFontSize: string;
onContextWorkflow?: (workflow: Workflow, e: React.MouseEvent) => void;
@@ -414,6 +417,7 @@ function EventStack({ events, onSelectWorkflow, eventFontSize, onContextWorkflow
fontSize: eventFontSize, fontWeight: 500,
overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis',
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 0.5,
opacity: paused ? 0.45 : 1,
'&:hover': { bgcolor: accent + '22' },
}}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>{first.workflow.title}</span>
@@ -47,7 +47,7 @@ import { Typewriter } from '@/app/components/feedback/Animated';
import StopRounded from '@mui/icons-material/StopRounded';
import PauseRounded from '@mui/icons-material/PauseRounded';
import { StatusDot, RunSparkline, LastFiredHint, isStaleSinceLastRun, isRealTitle } from './workflowVisuals';
import { stepsSignature } from './scheduleUtils';
import { stepsSignature, isWorkflowSchedulable } from './scheduleUtils';
import { store } from '@/shared/state/store';
import { getAgentWorkTime, fmtSeconds } from '@/shared/agentWorkTime';
@@ -118,6 +118,7 @@ const WorkflowCard: React.FC<Props> = ({
const expandedSessionIds = useAppSelector((s) => s.agents.expandedSessionIds);
const defaultModel = useAppSelector((s) => s.settings.data.default_model);
const defaultMode = useAppSelector((s) => s.settings.data.default_mode);
const allPaused = useAppSelector((s) => s.workflows.paused);
const cardBoxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -591,6 +592,15 @@ const WorkflowCard: React.FC<Props> = ({
</Typography>
)}
<StatusPill view={card.view} workflow={workflow} runs={runs} />
{workflow && isWorkflowSchedulable(workflow) && (!workflow.schedule.enabled || allPaused) && (
<Box sx={{
display: 'inline-flex', alignItems: 'center',
fontSize: '0.74rem', fontWeight: 600,
px: 0.8, py: 0.18, borderRadius: `${c.radius.md}px`,
color: c.text.muted, bgcolor: c.bg.elevated,
ml: 0.25,
}}>Paused</Box>
)}
<Box sx={{ flex: 1 }} />
</>
)}
@@ -694,20 +694,17 @@ export function HistoryList({ runs, onOpen, showWorkflow = false, workflowTitleF
}
return out;
}, [filtered]);
// Header sparkline summarising recent successes/failures so users can
// see "lately broken" before scrolling.
const recent = (runs || []).slice(0, 30);
if (!runs || runs.length === 0) {
return <Typography sx={{ fontSize: '0.88rem', color: c.text.muted, py: 1.5, textAlign: 'center' }}>No runs yet</Typography>;
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.75 }}>
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.25 }}>
{recent.map((r) => (
<Box key={r.id} sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: statusColor(r.status, c) }} />
))}
</Box>
{groups.length > 0 && (
<Typography sx={{ fontSize: '0.7rem', fontWeight: 700, color: c.text.muted, letterSpacing: '0.06em' }}>
{groups[0].key.toUpperCase()}
</Typography>
)}
<Box sx={{ flex: 1 }} />
{(['all', 'failure', 'ran_late'] as const).map((k) => (
<Box key={k} onClick={() => setFilter(k)} role="button" sx={{
@@ -722,11 +719,13 @@ export function HistoryList({ runs, onOpen, showWorkflow = false, workflowTitleF
</Box>
))}
</Box>
{groups.map(({ key, runs: gRuns }) => (
{groups.map(({ key, runs: gRuns }, gi) => (
<Box key={key} sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ fontSize: '0.7rem', fontWeight: 700, color: c.text.muted, letterSpacing: '0.06em', mt: 0.5, mb: 0.25 }}>
{key.toUpperCase()}
</Typography>
{gi > 0 && (
<Typography sx={{ fontSize: '0.7rem', fontWeight: 700, color: c.text.muted, letterSpacing: '0.06em', mt: 0.5, mb: 0.25 }}>
{key.toUpperCase()}
</Typography>
)}
{gRuns.map((r) => {
const expanded = expandedId === r.id;
const dur = runDuration(r);
@@ -759,7 +758,7 @@ export function HistoryList({ runs, onOpen, showWorkflow = false, workflowTitleF
<Typography sx={{ fontSize: '0.78rem', color: c.status.error, lineHeight: 1.4 }}>{r.error}</Typography>
) : (
<Typography sx={{ fontSize: '0.78rem', color: c.text.secondary, lineHeight: 1.4 }}>
{r.session_id ? `Saved as session ${r.session_id.slice(0, 8)}.` : 'No session was recorded for this run.'} Click below to see the full conversation.
{r.session_id ? 'Click below to see the full conversation.' : 'No session was recorded for this run. Click below to see the full conversation.'}
</Typography>
)}
<Box sx={{ mt: 0.5, display: 'flex', justifyContent: 'flex-end' }}>
@@ -641,6 +641,7 @@ function SidebarSection({ title, items, onPick, scheduled, onContext, onSchedule
}) {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const allPaused = useAppSelector((s) => s.workflows.paused);
const [open, setOpen] = useState(true);
const toggleEnabled = useCallback((wf: Workflow, e: React.MouseEvent) => {
@@ -707,10 +708,10 @@ function SidebarSection({ title, items, onPick, scheduled, onContext, onSchedule
never animates on first appearance or for already-named rows. */}
<Typewriter value={w.title} enabled={isRealTitle(w.title)}>
{(t) => (
<Typography sx={{ flex: 1, fontSize: '0.82rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: scheduled && !w.schedule.enabled ? 0.7 : 1 }}>{t}</Typography>
<Typography sx={{ flex: 1, fontSize: '0.82rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: scheduled && (!w.schedule.enabled || allPaused) ? 0.7 : 1 }}>{t}</Typography>
)}
</Typewriter>
{scheduled && !w.schedule.enabled && (
{scheduled && (!w.schedule.enabled || allPaused) && (
<Box sx={{ flexShrink: 0, px: 0.6, py: 0.1, borderRadius: '3px', bgcolor: c.bg.elevated, color: c.text.muted, fontSize: '0.62rem', fontWeight: 600, lineHeight: 1.5, letterSpacing: '0.02em' }}>Paused</Box>
)}
</Box>