[eric] workflows: subtitle row + status pill + image-audit polish across all states

This commit is contained in:
ciregenz
2026-05-22 02:11:47 -07:00
parent 19a5cd8131
commit 4f30080e62
7 changed files with 264 additions and 97 deletions
@@ -971,18 +971,18 @@ const AgentCard: React.FC<Props> = ({
onMouseDown={(e) => e.stopPropagation()}
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.5,
color: c.accent.primary,
bgcolor: c.accent.primary + '12',
border: `1px solid ${c.accent.primary}40`,
fontSize: '0.78rem', fontWeight: 600,
px: 1, py: 0.45,
color: '#fff',
bgcolor: c.accent.primary,
border: `1px solid ${c.accent.primary}`,
fontSize: '0.78rem', fontWeight: 700,
px: 1.1, py: 0.5,
borderRadius: `${c.radius.md}px`,
cursor: 'pointer',
'&:hover': { bgcolor: c.accent.primary + '22' },
'&:hover': { filter: 'brightness(1.05)' },
}}
>
<AutoAwesomeIcon sx={{ fontSize: 14 }} />
Make workflow
Convert to workflow
</Box>
</Tooltip>
)}
@@ -21,6 +21,8 @@ import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import PlayArrowRounded from '@mui/icons-material/PlayArrowRounded';
import BuildRounded from '@mui/icons-material/BuildRounded';
import TuneRounded from '@mui/icons-material/TuneRounded';
import KeyboardArrowDownRounded from '@mui/icons-material/KeyboardArrowDownRounded';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { clearFixSeed, setCardSidecar, updateWorkflow, updateWorkflowCard, type Workflow } from '@/shared/state/workflowsSlice';
@@ -37,11 +39,43 @@ interface Props {
isFixMode?: boolean;
}
function InlineSubtitle({ workflow }: { workflow: Workflow }) {
const c = useClaudeTokens();
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
const runs = useAppSelector((s) => s.workflows.runs[workflow.id]);
const modelLabel = React.useMemo(() => {
if (!workflow?.model) return '';
for (const list of Object.values(modelsByProvider || {})) {
for (const m of (list as Array<{ value: string; label?: string }>) || []) {
if (m.value === workflow.model) return m.label || workflow.model;
}
}
return workflow.model;
}, [workflow?.model, modelsByProvider]);
const duration = React.useMemo(() => {
if (!runs || runs.length === 0) return '';
const last = runs.find((r) => r.finished_at);
if (!last || !last.finished_at) return '';
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
if (ms <= 0) return '';
if (ms < 1000) return `${ms}ms`;
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
return `${Math.floor(ms / 60_000)}m`;
}, [runs]);
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
{workflow.mode && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{workflow.mode}</Box>}
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
</Box>
);
}
type Turn =
| { kind: 'user'; text: string }
| { kind: 'assistant'; text: string }
| { kind: 'proposal'; stepIdx: number; before: string; after: string; explanation: string; applied: boolean }
| { kind: 'fix-prefix'; stepIdx: number; stepLabel: string; error: string };
| { kind: 'fix-prefix'; stepIdx: number; stepLabel: string; error: string; expanded: boolean };
export default function EditAgentView({ workflow, steps, isFixMode = false }: Props) {
const c = useClaudeTokens();
@@ -54,7 +88,7 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
const [draftSteps, setDraftSteps] = useState<Workflow['steps']>(steps);
const [turns, setTurns] = useState<Turn[]>(() => isFixMode && fixSeed ? [
{ kind: 'fix-prefix', stepIdx: fixSeed.stepIdx, stepLabel: fixSeed.stepLabel, error: fixSeed.error },
{ kind: 'fix-prefix', stepIdx: fixSeed.stepIdx, stepLabel: fixSeed.stepLabel, error: fixSeed.error, expanded: false },
] : [
{ kind: 'assistant', text: 'How would you like to modify the workflow (e.g. filter out spam emails before summarizing)' },
]);
@@ -221,10 +255,25 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
{/* Inline header replacement. The card's default action bar is
hidden for edit_agent / fix_agent views. */}
{/* Inline header replacement. Matches Image #38: subtitle on the
LEFT, Settings + Discard + Save flush right. The card's default
action bar is hidden for edit_agent / fix_agent views. */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<InlineSubtitle workflow={workflow} />
<Box sx={{ flex: 1 }} />
<Tooltip title="Permissions, actions, cost cap">
<Box
onClick={() => dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit', editFacet: 'Actions' } }))}
role="button"
sx={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 28, height: 28, borderRadius: 999,
color: c.text.secondary, cursor: 'pointer',
'&:hover': { color: c.text.primary, bgcolor: c.bg.elevated },
}}>
<TuneRounded sx={{ fontSize: 16 }} />
</Box>
</Tooltip>
<HeaderBtn
label="Discard"
icon={<DeleteOutlineRounded sx={{ fontSize: 16 }} />}
@@ -249,13 +298,23 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{turns.map((t, idx) => {
if (t.kind === 'fix-prefix') {
const PREVIEW_MAX = 110;
const needsExpand = (t.error || '').length > PREVIEW_MAX;
const shown = !needsExpand || t.expanded
? t.error
: (t.error || '').slice(0, PREVIEW_MAX).trimEnd() + '…';
return (
<Box key={idx} sx={{
display: 'flex', alignItems: 'flex-start', gap: 1.25,
p: 1.25, borderRadius: `${c.radius.lg}px`,
bgcolor: c.status.errorBg,
border: `1px solid ${c.status.error}30`,
}}>
<Box
key={idx}
onClick={needsExpand ? () => setTurns((all) => all.map((tt, i) => i === idx && tt.kind === 'fix-prefix' ? { ...tt, expanded: !tt.expanded } : tt)) : undefined}
sx={{
display: 'flex', alignItems: 'flex-start', gap: 1.25,
p: 1.25, borderRadius: `${c.radius.lg}px`,
bgcolor: c.status.errorBg,
border: `1px solid ${c.status.error}30`,
cursor: needsExpand ? 'pointer' : 'default',
'&:hover': needsExpand ? { bgcolor: c.status.error + '14' } : {},
}}>
<Box sx={{
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
bgcolor: c.status.error + '22', color: c.status.error,
@@ -264,11 +323,22 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr
<BuildRounded sx={{ fontSize: 16 }} />
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography sx={{ fontSize: '0.92rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
Fixing Step {t.stepIdx + 1}: {t.stepLabel}
</Typography>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
{t.error}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography sx={{ flex: 1, fontSize: '0.92rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
Fixing Step {t.stepIdx + 1}: {t.stepLabel}
</Typography>
{needsExpand && (
<KeyboardArrowDownRounded sx={{
fontSize: 18,
color: c.text.muted,
transform: t.expanded ? 'rotate(180deg)' : 'none',
transition: 'transform 0.18s ease',
flexShrink: 0,
}} />
)}
</Box>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45, whiteSpace: 'pre-wrap' }}>
{shown}
</Typography>
</Box>
</Box>
@@ -17,12 +17,45 @@ import { useAppDispatch } from '@/shared/hooks';
import { updateWorkflow, updateWorkflowCard, type ScheduleConfig, type Workflow } from '@/shared/state/workflowsSlice';
import StepList from './StepList';
import { API_BASE, getAuthToken } from '@/shared/config';
import { useAppSelector as _useAppSelector } from '@/shared/hooks';
interface Props {
workflow: Workflow;
steps: Workflow['steps'];
}
function InlineSubtitle({ workflow }: { workflow: Workflow }) {
const c = useClaudeTokens();
const modelsByProvider = _useAppSelector((s) => s.models.byProvider);
const runs = _useAppSelector((s) => s.workflows.runs[workflow.id]);
const modelLabel = React.useMemo(() => {
if (!workflow?.model) return '';
for (const list of Object.values(modelsByProvider || {})) {
for (const m of (list as Array<{ value: string; label?: string }>) || []) {
if (m.value === workflow.model) return m.label || workflow.model;
}
}
return workflow.model;
}, [workflow?.model, modelsByProvider]);
const duration = React.useMemo(() => {
if (!runs || runs.length === 0) return '';
const last = runs.find((r) => r.finished_at);
if (!last || !last.finished_at) return '';
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
if (ms <= 0) return '';
if (ms < 1000) return `${ms}ms`;
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
return `${Math.floor(ms / 60_000)}m`;
}, [runs]);
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
{workflow.mode && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{workflow.mode}</Box>}
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
</Box>
);
}
export default function SchedulingView({ workflow, steps }: Props) {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
@@ -82,9 +115,11 @@ export default function SchedulingView({ workflow, steps }: Props) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
{/* Inline header replacement. The default History/Run row is hidden
for 'scheduling' view; this Cancel button sits in its place. */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
{/* Inline header replacement. Image #49: subtitle on LEFT, Cancel
on RIGHT. The default History/Run row is hidden for 'scheduling'. */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<InlineSubtitle workflow={workflow} />
<Box sx={{ flex: 1 }} />
<Box
onClick={onCancel}
role="button"
@@ -94,10 +94,10 @@ export default function StepList(props: Props) {
position: 'relative',
cursor: hasExpandableBody && !isActive ? 'pointer' : 'default',
borderRadius: `${c.radius.md}px`,
px: isActive ? 0.5 : 0,
py: isActive ? 0.5 : 0,
mx: isActive ? -0.5 : 0,
bgcolor: isActive ? c.bg.elevated : 'transparent',
px: (isActive || (isExpanded && hasExpandableBody)) ? 0.5 : 0,
py: (isActive || (isExpanded && hasExpandableBody)) ? 0.5 : 0,
mx: (isActive || (isExpanded && hasExpandableBody)) ? -0.5 : 0,
bgcolor: (isActive || (isExpanded && hasExpandableBody)) ? c.bg.elevated : 'transparent',
transition: 'background 0.18s ease',
'&:hover': hasExpandableBody && !isActive ? { bgcolor: c.bg.elevated } : {},
}}>
@@ -451,9 +451,13 @@ const WorkflowCard: React.FC<Props> = ({
}}
/>
) : (
<Typography sx={{ flex: 1, fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
{title}
</Typography>
<>
<Typography sx={{ fontWeight: 700, fontSize: '1rem', color: c.text.primary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: '-0.005em' }}>
{title}
</Typography>
<StatusPill view={card.view} workflow={workflow} runs={runs} />
<Box sx={{ flex: 1 }} />
</>
)}
{runs && runs.length > 0 && <RunSparkline runs={runs} />}
<IconButton
@@ -471,7 +475,8 @@ const WorkflowCard: React.FC<Props> = ({
The flex spacer is the empty left side; History is a quiet text link, Run is the
accent pill. */}
{isDraft && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0, opacity: 0.45, pointerEvents: 'none' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
<SubtitleRow workflow={null} runs={null} />
<Box sx={{ flex: 1 }} />
<TabBtn label="History" icon={<HistoryIcon sx={{ fontSize: 16 }} />} active={false} onClick={() => {}} />
<TabBtn label="Run" icon={<PlayArrowIcon sx={{ fontSize: 16 }} />} active={false} accent onClick={() => {}} />
@@ -479,6 +484,7 @@ const WorkflowCard: React.FC<Props> = ({
)}
{!isDraft && workflow && !isHeaderlessView(card.view) && card.view !== 'running' && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
<SubtitleRow workflow={workflow} runs={runs} />
<Box sx={{ flex: 1 }} />
<TabBtn
label="History"
@@ -677,6 +683,77 @@ function isHeaderlessView(view: string): boolean {
return view === 'edit_agent' || view === 'fix_agent' || view === 'scheduling';
}
function StatusPill({ view, workflow, runs }: { view: string; workflow: Workflow | undefined; runs: import('@/shared/state/workflowsSlice').WorkflowRun[] | undefined }) {
const c = useClaudeTokens();
// Pills appear on the title row to mirror Image #34 (completed source
// chat), #40 (running), #42 (completed workflow), #41 (running while
// watching). Saved / Preview / Edit / Scheduling have no pill.
let label = '';
let color = c.text.muted;
let bg = c.bg.elevated;
if (view === 'running') {
label = 'running';
color = c.status.success;
bg = c.status.successBg;
} else if (view === 'completed') {
label = 'completed';
color = c.text.secondary;
bg = c.bg.elevated;
} else {
// Image #46: failed view has NO pill in the title row; the red X
// on the failed step row carries the signal. Preview/Saved/Edit/
// Scheduling are likewise pill-less.
void workflow; void runs;
return null;
}
return (
<Box sx={{
display: 'inline-flex', alignItems: 'center',
fontSize: '0.74rem', fontWeight: 600,
px: 0.8, py: 0.18, borderRadius: `${c.radius.md}px`,
color, bgcolor: bg,
ml: 0.25,
}}>{label}</Box>
);
}
function SubtitleRow({ workflow, runs }: { workflow: Workflow | null; runs: import('@/shared/state/workflowsSlice').WorkflowRun[] | null }) {
const c = useClaudeTokens();
const modelsByProvider = useAppSelector((s) => s.models.byProvider);
// Match Image #34/#35/#36/#38/#40: "Claude Opus 4.6 agent 28s".
// Spaces between fields, all in muted text. Duration is the most
// recent finished run's elapsed time; falls back to nothing when no
// run has completed yet (PreviewView).
const modelLabel = React.useMemo(() => {
if (!workflow?.model) return '';
for (const list of Object.values(modelsByProvider || {})) {
for (const m of (list as any[]) || []) {
if (m.value === workflow.model) return m.label || workflow.model;
}
}
return workflow.model;
}, [workflow?.model, modelsByProvider]);
const modeLabel = workflow?.mode || '';
const duration = React.useMemo(() => {
if (!runs || runs.length === 0) return '';
const last = runs.find((r) => r.finished_at);
if (!last || !last.finished_at) return '';
const ms = new Date(last.finished_at).getTime() - new Date(last.started_at).getTime();
if (ms <= 0) return '';
if (ms < 1000) return `${ms}ms`;
if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
const m = Math.floor(ms / 60_000);
return `${m}m`;
}, [runs]);
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1.25, fontSize: '0.82rem', color: c.text.muted, minWidth: 0, overflow: 'hidden' }}>
{modelLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modelLabel}</Box>}
{modeLabel && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{modeLabel}</Box>}
{duration && <Box component="span" sx={{ whiteSpace: 'nowrap' }}>{duration}</Box>}
</Box>
);
}
function RunningHeader({ workflowId }: { workflowId: string }) {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
@@ -714,6 +791,7 @@ function RunningHeader({ workflowId }: { workflowId: string }) {
}, [dispatch, workflow, isPaused]);
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, px: 2, pb: 1.25, pt: 0, flexShrink: 0 }}>
<SubtitleRow workflow={workflow || null} runs={runs || null} />
<Box sx={{ flex: 1 }} />
<Box
onClick={onStop}
@@ -305,30 +305,32 @@ export function CompletedView({ workflow, steps, runs, mode = 'card' }: {
stepStatuses={statuses}
/>
<Box sx={{ flex: 1 }} />
{/* Success card. Soft green tint + rocket icon. Per Image #42 the
subtitle copy ends with "...see exactly what the agent did." */}
<Box sx={{
display: 'flex', alignItems: 'flex-start', gap: 1.25,
p: 1.5, borderRadius: `${c.radius.lg}px`,
bgcolor: c.status.successBg,
border: `1px solid ${c.status.success}30`,
}}>
{/* Success card. Hidden in sidecar-linked mode (Image #43) so the
compacted card stays tight. Soft green tint + rocket icon. */}
{!isLinked && (
<Box sx={{
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
bgcolor: c.status.success + '22', color: c.status.success,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
display: 'flex', alignItems: 'flex-start', gap: 1.25,
p: 1.5, borderRadius: `${c.radius.lg}px`,
bgcolor: c.status.successBg,
border: `1px solid ${c.status.success}30`,
}}>
<RocketLaunchRounded sx={{ fontSize: 16 }} />
<Box sx={{
width: 32, height: 32, borderRadius: `${c.radius.md}px`,
bgcolor: c.status.success + '22', color: c.status.success,
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<RocketLaunchRounded sx={{ fontSize: 16 }} />
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography sx={{ fontSize: '0.95rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
Workflow Success!
</Typography>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
If you&apos;re curious, you can click the green button below to see exactly what the agent did.
</Typography>
</Box>
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography sx={{ fontSize: '0.95rem', fontWeight: 700, color: c.text.primary, lineHeight: 1.3 }}>
Workflow Success!
</Typography>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, mt: 0.25, lineHeight: 1.45 }}>
If you&apos;re curious, you can click the green button below to see exactly what the agent did.
</Typography>
</Box>
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25 }}>
<PillButton
label="Edit"
@@ -338,7 +340,9 @@ export function CompletedView({ workflow, steps, runs, mode = 'card' }: {
onClick={onEdit}
/>
<Box sx={{ flex: 1 }} />
<GhostTextBtn label="Done" onClick={onDone} />
{/* Image #43: Done is hidden in sidecar mode; Stop Viewing alone
fills the right slot. Default mode keeps Done + View Agent. */}
{!isLinked && <GhostTextBtn label="Done" onClick={onDone} />}
{isLinked ? (
<PillButton
label="Stop Viewing"
@@ -7,7 +7,6 @@ import InputBase from '@mui/material/InputBase';
import HistoryIcon from '@mui/icons-material/HistoryToggleOffRounded';
import CalendarTodayRounded from '@mui/icons-material/CalendarTodayRounded';
import EditOutlined from '@mui/icons-material/EditOutlined';
import TuneRounded from '@mui/icons-material/TuneRounded';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import {
@@ -239,13 +238,15 @@ function describeSchedule(workflow: Workflow): string {
const h12 = ((s.hour + 11) % 12) + 1;
const ampm = s.hour < 12 ? 'am' : 'pm';
const time = s.minute === 0 ? `${h12}${ampm}` : `${h12}:${String(s.minute).padStart(2, '0')}${ampm}`;
if (s.repeat_unit === 'day') return s.repeat_every === 1 ? `Every day at ${time}` : `Every ${s.repeat_every} days at ${time}`;
if (s.repeat_unit === 'month') return s.repeat_every === 1 ? `Every month at ${time}` : `Every ${s.repeat_every} months at ${time}`;
if (s.repeat_unit === 'day') return s.repeat_every === 1 ? `Daily at ${time}` : `Every ${s.repeat_every} days at ${time}`;
if (s.repeat_unit === 'month') return s.repeat_every === 1 ? `Monthly at ${time}` : `Every ${s.repeat_every} months at ${time}`;
if (s.on_days.length === 5 && [1,2,3,4,5].every((d) => s.on_days.includes(d))) return `Weekdays at ${time}`;
if (s.on_days.length === 2 && [0,6].every((d) => s.on_days.includes(d))) return `Weekends at ${time}`;
if (s.on_days.length === 1) {
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return `Every ${labels[s.on_days[0]]} at ${time}`;
// Image #50: "Mondays at 3pm" (plural day, no "Every" prefix). Reads
// more naturally than "Every Mon at 3pm".
const plurals = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
return `${plurals[s.on_days[0]]} at ${time}`;
}
return `Weekly at ${time}`;
}
@@ -262,12 +263,6 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
const openScheduling = useCallback(() => {
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'scheduling' } }));
}, [dispatch, workflow.id]);
const openFacetEditor = useCallback(() => {
// Legacy General/Actions/Schedule facet picker. The new chat-based
// EditAgentView replaces it for step iteration; this is the escape
// hatch for permissions, cost cap, action allowlists, etc.
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit', editFacet: 'Actions' } }));
}, [dispatch, workflow.id]);
const onToggleStep = useCallback((stepId: string) => {
dispatch(toggleExpandedStep({ workflowId: workflow.id, stepId }));
}, [dispatch, workflow.id]);
@@ -298,37 +293,22 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
<CalendarTodayRounded sx={{ fontSize: 15, color: c.text.muted, flexShrink: 0 }} />
<Box component="span" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{scheduleLine}</Box>
</Box>
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5 }}>
<Tooltip title="Permissions, actions, cost cap">
<Box
onClick={openFacetEditor}
role="button"
sx={{
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: 28, height: 28, borderRadius: 999,
color: c.text.secondary, cursor: 'pointer',
'&:hover': { color: c.text.primary, bgcolor: c.bg.elevated },
}}>
<TuneRounded sx={{ fontSize: 16 }} />
</Box>
</Tooltip>
<Box
onClick={openEditAgent}
role="button"
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.45,
fontSize: '0.82rem', fontWeight: 600,
px: 1.25, py: 0.5,
borderRadius: 999,
cursor: 'pointer',
color: c.text.secondary,
bgcolor: 'transparent',
border: `1px solid ${c.border.medium}`,
'&:hover': { bgcolor: c.bg.elevated, borderColor: c.border.strong || c.border.medium, color: c.text.primary },
}}>
<EditOutlined sx={{ fontSize: 15 }} />
Edit
</Box>
<Box
onClick={openEditAgent}
role="button"
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.45,
fontSize: '0.82rem', fontWeight: 600,
px: 1.25, py: 0.5,
borderRadius: 999,
cursor: 'pointer',
color: c.text.secondary,
bgcolor: 'transparent',
border: `1px solid ${c.border.medium}`,
'&:hover': { bgcolor: c.bg.elevated, borderColor: c.border.strong || c.border.medium, color: c.text.primary },
}}>
<EditOutlined sx={{ fontSize: 15 }} />
Edit
</Box>
</Box>
</Box>