diff --git a/frontend/src/app/pages/Dashboard/AgentCard.tsx b/frontend/src/app/pages/Dashboard/AgentCard.tsx index 5eb77001..fc30697a 100644 --- a/frontend/src/app/pages/Dashboard/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/AgentCard.tsx @@ -971,18 +971,18 @@ const AgentCard: React.FC = ({ 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)' }, }} > - Make workflow + Convert to workflow )} diff --git a/frontend/src/app/pages/Workflows/EditAgentView.tsx b/frontend/src/app/pages/Workflows/EditAgentView.tsx index 9f997c7c..35ed65c2 100644 --- a/frontend/src/app/pages/Workflows/EditAgentView.tsx +++ b/frontend/src/app/pages/Workflows/EditAgentView.tsx @@ -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 ( + + {modelLabel && {modelLabel}} + {workflow.mode && {workflow.mode}} + {duration && {duration}} + + ); +} + 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(steps); const [turns, setTurns] = useState(() => 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 ( - {/* 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. */} + + + 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 }, + }}> + + + } @@ -249,13 +298,23 @@ export default function EditAgentView({ workflow, steps, isFixMode = false }: Pr {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 ( - + 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' } : {}, + }}> - - Fixing Step {t.stepIdx + 1}: {t.stepLabel} - - - {t.error} + + + Fixing Step {t.stepIdx + 1}: {t.stepLabel} + + {needsExpand && ( + + )} + + + {shown} diff --git a/frontend/src/app/pages/Workflows/SchedulingView.tsx b/frontend/src/app/pages/Workflows/SchedulingView.tsx index 7ad009f8..bd9a08a6 100644 --- a/frontend/src/app/pages/Workflows/SchedulingView.tsx +++ b/frontend/src/app/pages/Workflows/SchedulingView.tsx @@ -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 ( + + {modelLabel && {modelLabel}} + {workflow.mode && {workflow.mode}} + {duration && {duration}} + + ); +} + 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 ( - {/* Inline header replacement. The default History/Run row is hidden - for 'scheduling' view; this Cancel button sits in its place. */} - + {/* Inline header replacement. Image #49: subtitle on LEFT, Cancel + on RIGHT. The default History/Run row is hidden for 'scheduling'. */} + + + diff --git a/frontend/src/app/pages/Workflows/WorkflowCard.tsx b/frontend/src/app/pages/Workflows/WorkflowCard.tsx index adf46e94..2b883478 100644 --- a/frontend/src/app/pages/Workflows/WorkflowCard.tsx +++ b/frontend/src/app/pages/Workflows/WorkflowCard.tsx @@ -451,9 +451,13 @@ const WorkflowCard: React.FC = ({ }} /> ) : ( - - {title} - + <> + + {title} + + + + )} {runs && runs.length > 0 && } = ({ The flex spacer is the empty left side; History is a quiet text link, Run is the accent pill. */} {isDraft && ( - + + } active={false} onClick={() => {}} /> } active={false} accent onClick={() => {}} /> @@ -479,6 +484,7 @@ const WorkflowCard: React.FC = ({ )} {!isDraft && workflow && !isHeaderlessView(card.view) && card.view !== 'running' && ( + {label} + ); +} + +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 ( + + {modelLabel && {modelLabel}} + {modeLabel && {modeLabel}} + {duration && {duration}} + + ); +} + function RunningHeader({ workflowId }: { workflowId: string }) { const c = useClaudeTokens(); const dispatch = useAppDispatch(); @@ -714,6 +791,7 @@ function RunningHeader({ workflowId }: { workflowId: string }) { }, [dispatch, workflow, isPaused]); return ( + - {/* Success card. Soft green tint + rocket icon. Per Image #42 the - subtitle copy ends with "...see exactly what the agent did." */} - + {/* Success card. Hidden in sidecar-linked mode (Image #43) so the + compacted card stays tight. Soft green tint + rocket icon. */} + {!isLinked && ( - + + + + + + Workflow Success! + + + If you're curious, you can click the green button below to see exactly what the agent did. + + - - - Workflow Success! - - - If you're curious, you can click the green button below to see exactly what the agent did. - - - + )} - + {/* Image #43: Done is hidden in sidecar mode; Stop Viewing alone + fills the right slot. Default mode keeps Done + View Agent. */} + {!isLinked && } {isLinked ? ( 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 {scheduleLine} - - - - - - - - - Edit - + + + Edit