diff --git a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx index fe44d4cd..a3df6f96 100644 --- a/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardToolbar.tsx @@ -190,7 +190,7 @@ const DashboardToolbar = React.forwardRef( ); }, [outputList, viewSearch]); - const shortcutLabel = shortcut + const shortcutLabel = (shortcut || '') .split('+') .map((p) => { if (p === 'Meta') return '⌘'; diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts index bcc25d4b0..ddaf5223 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardInteractions.ts @@ -72,7 +72,16 @@ export function useDashboardInteractions({ setTimeout(() => { const rect = getCardRect(id, type); if (rect) canvas.actions.fitToCards([rect], 1.15, true, type === 'browser' ? 0.8 : undefined); - setTimeout(() => (document.activeElement as HTMLElement)?.blur?.(), 150); + setTimeout(() => { + // Don't blur an input/textarea/contentEditable the user is typing in + // (e.g. a workflow card's embedded chat); the click that selected the + // card also focused the field, and blurring it kills the cursor. + const active = document.activeElement as HTMLElement | null; + if (!active) return; + const tag = active.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return; + active.blur?.(); + }, 150); }, 100); }, [selection, getCardRect, canvas.actions, dispatch, expandedSessionIds]); @@ -152,7 +161,16 @@ export function useDashboardInteractions({ setTimeout(() => { const rect = getCardRect(id, type); if (rect) canvas.actions.fitToCards([rect], 1.15, true); - setTimeout(() => (document.activeElement as HTMLElement)?.blur?.(), 150); + setTimeout(() => { + // Don't blur an input/textarea/contentEditable the user is typing in + // (e.g. a workflow card's embedded chat); the click that selected the + // card also focused the field, and blurring it kills the cursor. + const active = document.activeElement as HTMLElement | null; + if (!active) return; + const tag = active.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || active.isContentEditable) return; + active.blur?.(); + }, 150); }, 100); }, [getCardRect, canvas.actions, dispatch]); diff --git a/frontend/src/app/pages/Workflows/SchedulingView.tsx b/frontend/src/app/pages/Workflows/SchedulingView.tsx index ae1bb74e..1473421c 100644 --- a/frontend/src/app/pages/Workflows/SchedulingView.tsx +++ b/frontend/src/app/pages/Workflows/SchedulingView.tsx @@ -6,7 +6,7 @@ // ScheduleConfig in a confirmation modal (the "always ask permission" // stand-in for the schedule_workflow tool call), and PATCH on confirm. -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Dialog from '@mui/material/Dialog'; @@ -63,6 +63,23 @@ export default function SchedulingView({ workflow, steps }: Props) { const [error, setError] = useState(null); const [pending, setPending] = useState(null); + // The composer behaves like any normal chat: it defaults to the user's + // configured default model/mode (e.g. their subscription model), not the + // workflow's stored run model, and its pickers actually work. + const defaultModel = _useAppSelector((s) => s.settings.data.default_model); + const defaultMode = _useAppSelector((s) => s.settings.data.default_mode); + const settingsLoaded = _useAppSelector((s) => s.settings.loaded); + const [chatModel, setChatModel] = useState(defaultModel || 'sonnet'); + const [chatMode, setChatMode] = useState(defaultMode || 'agent'); + const settingsApplied = useRef(false); + useEffect(() => { + if (settingsLoaded && !settingsApplied.current) { + setChatModel(defaultModel || 'sonnet'); + setChatMode(defaultMode || 'agent'); + settingsApplied.current = true; + } + }, [settingsLoaded, defaultModel, defaultMode]); + const onCancel = useCallback(() => { dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'saved' } })); }, [dispatch, workflow.id]); @@ -136,21 +153,10 @@ export default function SchedulingView({ workflow, steps }: Props) { Cancel task scheduling - - - - - - When should this workflow run (e.g. every Wednesday at 1pm) - - + + + When should this workflow run (e.g. every Wednesday at 1pm) + {error && ( {error} )} @@ -164,10 +170,10 @@ export default function SchedulingView({ workflow, steps }: Props) { { void onSubmit(msg); }} - mode={workflow.mode || 'agent'} - onModeChange={() => { /* schedule chat doesn't persist mode */ }} - model={workflow.model || 'sonnet'} - onModelChange={() => { /* schedule chat doesn't persist model */ }} + mode={chatMode} + onModeChange={setChatMode} + model={chatModel} + onModelChange={setChatModel} embedded autoFocus sessionId={`schedule-${workflow.id}`} diff --git a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx index 3f480431..457d3dfb 100644 --- a/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx +++ b/frontend/src/app/pages/Workflows/WorkflowCardSubviews.tsx @@ -10,7 +10,6 @@ import EditOutlined from '@mui/icons-material/EditOutlined'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { - closeWorkflowCard, createWorkflow, toggleExpandedStep, updateWorkflow, @@ -18,7 +17,6 @@ import { type Workflow, type WorkflowRun, } from '@/shared/state/workflowsSlice'; -import { removeWorkflowCard } from '@/shared/state/dashboardLayoutSlice'; import { CostChip, humanDuration, routingFor, StreakBadge } from './workflowVisuals'; import StepList from './StepList'; @@ -114,6 +112,12 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, const liveDraft = (card?.draft ?? initialDraft ?? {}) as Partial; const title = (liveDraft.title as string) || 'New workflow'; const description = (liveDraft.description as string) || ''; + // The new workflow runs with the user's configured default model/mode (their + // subscription, etc.), falling back to whatever the source chat used. Without + // this the backend picks its own default, which surprised users who'd set a + // subscription default but saw the workflow created on an API-key model. + const defaultModel = useAppSelector((s) => s.settings.data.default_model); + const defaultMode = useAppSelector((s) => s.settings.data.default_mode); // Steps render compact (label + chevron, capped + "... N more"), same as // the saved card. The raw prompt drills down on click. Keeping them short // is what leaves room for the schedule prompt + buttons to stay on-card. @@ -122,58 +126,64 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, dispatch(toggleExpandedStep({ workflowId, stepId })); }, [dispatch, workflowId]); - const onDiscard = useCallback(() => { - dispatch(closeWorkflowCard(workflowId)); - dispatch(removeWorkflowCard(workflowId)); - }, [dispatch, workflowId]); - const onChangeDescription = useCallback((value: string) => { dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...liveDraft, description: value } } })); }, [dispatch, workflowId, liveDraft]); - // The Save flow auto-creates the workflow, then prompts the user to schedule it - // (Image #7). Ignore = save without schedule. Schedule = open the scheduling - // composer (slice 3 wires this to the natural-language input). + // Both buttons persist the workflow; the only difference is where they land. + // Ignore = save and show the saved card. Schedule = save then open the + // natural-language scheduling composer. (Ignore used to delete the card, + // which surprised people; the schedule prompt is optional, the workflow isn't.) + const saveWorkflow = useCallback(async (): Promise => { + const result = await dispatch(createWorkflow({ + title, + description, + steps: steps.map((s) => ({ id: s.id, text: s.text })), + source_session_id: sourceSessionId, + use_synced_prompt: true, + // The user's configured default wins over whatever model the source chat + // happened to run on, so a converted workflow behaves like a fresh chat. + model: defaultModel || (liveDraft.model as string), + mode: defaultMode || (liveDraft.mode as string), + } as Partial)); + const wf = (result as unknown as { payload: Workflow }).payload; + if (wf?.id) { onSaved(wf); return wf; } + return null; + }, [dispatch, title, description, steps, sourceSessionId, onSaved, liveDraft, defaultModel, defaultMode]); + + const onIgnore = useCallback(async () => { + if (busy) return; + setBusy(true); + try { await saveWorkflow(); } finally { setBusy(false); } + }, [busy, saveWorkflow]); + const onSaveThenSchedule = useCallback(async () => { if (busy) return; setBusy(true); try { - const result = await dispatch(createWorkflow({ - title, - description, - steps: steps.map((s) => ({ id: s.id, text: s.text })), - source_session_id: sourceSessionId, - use_synced_prompt: true, - } as Partial)); - const wf = (result as unknown as { payload: Workflow }).payload; - if (wf?.id) { - onSaved(wf); - // Route to the new SchedulingView (Image #49), not the legacy - // facet editor. The old `view: 'edit', editFacet: 'Schedule'` - // path opens a different design entirely. - dispatch(updateWorkflowCard({ workflowId: wf.id, patch: { view: 'scheduling' } })); - } + const wf = await saveWorkflow(); + if (wf?.id) dispatch(updateWorkflowCard({ workflowId: wf.id, patch: { view: 'scheduling' } })); } finally { setBusy(false); } - }, [busy, dispatch, title, description, steps, sourceSessionId, onSaved]); + }, [busy, saveWorkflow, dispatch]); void onChangeDescription; return ( - {/* Schedule prompt card. Soft warning-gold tint + calendar icon, matching - Image #35. Gold is the same token the HITL/human-intervention UI uses. */} + {/* Schedule prompt card. Soft accent tint + calendar icon. Accent is the + same color the human-intervention (AskUserQuestion) popup uses. */} @@ -189,11 +199,12 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, Ignore @@ -205,10 +216,10 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft, display: 'inline-flex', alignItems: 'center', gap: 0.5, fontSize: '0.88rem', fontWeight: 700, px: 1.75, py: 0.6, borderRadius: 999, - color: '#fff', bgcolor: c.status.warning, + color: '#fff', bgcolor: c.accent.primary, cursor: busy ? 'wait' : 'pointer', opacity: busy ? 0.6 : 1, - '&:hover': { bgcolor: c.status.warning, filter: 'brightness(1.06)' }, + '&:hover': { bgcolor: c.accent.primary, filter: 'brightness(1.06)' }, }}> Schedule Workflow