onSelect(r.wfId)} style={{ display: 'flex', alignItems: 'center', background: r.color, color: '#fff', borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, cursor: 'pointer', lineHeight: 1.2, overflow: 'hidden' }}>
+
onSelect(r.wfId)} onContextMenu={(e) => onContextWorkflow(r.wfId, e)} style={{ display: 'flex', alignItems: 'center', background: r.color, color: '#fff', borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600, cursor: 'pointer', lineHeight: 1.2, overflow: 'hidden' }}>
{r.title}
{miniTime(r.at)}
diff --git a/frontend/src/app/pages/Workflows/app/HistoryCard.tsx b/frontend/src/app/pages/Workflows/app/HistoryCard.tsx
index 5cc60bfd..52d1d8fd 100644
--- a/frontend/src/app/pages/Workflows/app/HistoryCard.tsx
+++ b/frontend/src/app/pages/Workflows/app/HistoryCard.tsx
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { fetchRuns } from '@/shared/state/workflowsSlice';
import { openWorkflowMonitor } from '@/shared/state/dashboardLayoutSlice';
+import { openCardContextMenu } from '@/app/pages/Dashboard/desktop/openCardContextMenu';
import { useWC, FONT_SERIF, statusChip, statusDot, statusLabel } from './uiKit';
import type { WCPalette } from './uiKit';
import { toRunRow, whenText } from './model';
@@ -18,6 +19,12 @@ const Row: React.FC<{ entry: Entry; wc: WCPalette; now: Date }> = ({ entry, wc,
openCardContextMenu(e, {
+ items: [
+ { label: 'Open run', disabled: !entry.open, onClick: () => entry.open?.() },
+ { label: 'Copy summary', onClick: () => { navigator.clipboard.writeText(entry.summary); } },
+ ],
+ })}
style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '9px 0', borderBottom: `1px solid rgba(${wc.inkRGB},0.05)`, cursor: entry.open ? 'pointer' : 'default' }}
>
diff --git a/frontend/src/app/pages/Workflows/app/HomeView.tsx b/frontend/src/app/pages/Workflows/app/HomeView.tsx
index 0dad456b..cd7f0cf3 100644
--- a/frontend/src/app/pages/Workflows/app/HomeView.tsx
+++ b/frontend/src/app/pages/Workflows/app/HomeView.tsx
@@ -6,6 +6,7 @@ import { useCalendarOccurrences } from './useCalendarOccurrences';
import { colorForWorkflow, useWC, statusChip, statusDot } from './uiKit';
import { clockOf, whenText } from './model';
import WorkflowTitle from './WorkflowTitle';
+import { useWorkflowMenu } from './useWorkflowMenu';
import type { AppNav } from './types';
interface ComingRun { wfId: string; title: string; time: string; sortKey: number; steps: number; color: string; }
@@ -21,6 +22,11 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
const missed = useAppSelector((s) => s.missedRuns.items);
const [missedExpanded, setMissedExpanded] = useState(false);
const [expandedDays, setExpandedDays] = useState
>(() => new Set());
+ const openWorkflowMenu = useWorkflowMenu();
+ const menuFor = (wfId: string) => (e: React.MouseEvent) => {
+ const wf = items[wfId];
+ if (wf) openWorkflowMenu(e, wf, { onEdit: () => nav.selectWorkflow(wfId) });
+ };
const now = new Date();
const todayLabel = now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
@@ -103,7 +109,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{ongoing.map((o) => (
-
dispatch(openWorkflowMonitor({ workflowId: o.wfId }))} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.10)`, borderRadius: WC.radius.md, padding: '11px 15px', cursor: 'pointer' }}>
+
dispatch(openWorkflowMonitor({ workflowId: o.wfId }))} onContextMenu={menuFor(o.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.10)`, borderRadius: WC.radius.md, padding: '11px 15px', cursor: 'pointer' }}>
@@ -178,7 +184,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{(expandedDays.has(g.key) ? g.runs : g.runs.slice(0, COMING_CAP)).map((r, i) => (
-
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '12px 15px', cursor: 'pointer' }}>
+
nav.selectWorkflow(r.wfId)} onContextMenu={menuFor(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '12px 15px', cursor: 'pointer' }}>
{(t) => {t}}
@@ -205,7 +211,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{recents.length === 0 && No runs yet.
}
{recents.map((r) => (
-
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}>
+
nav.selectWorkflow(r.wfId)} onContextMenu={menuFor(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}>
diff --git a/frontend/src/app/pages/Workflows/app/LeftRail.tsx b/frontend/src/app/pages/Workflows/app/LeftRail.tsx
index d78f0536..8c4179d3 100644
--- a/frontend/src/app/pages/Workflows/app/LeftRail.tsx
+++ b/frontend/src/app/pages/Workflows/app/LeftRail.tsx
@@ -6,6 +6,7 @@ import { isScheduleActive, describeSchedule } from '@/app/pages/Workflows/schedu
import ShareButton from '@/app/components/share/ShareButton';
import { colorForWorkflow, useWC } from './uiKit';
import WorkflowTitle from './WorkflowTitle';
+import { useWorkflowMenu } from './useWorkflowMenu';
import type { AppNav } from './types';
const navBase: CSSProperties = {
@@ -20,6 +21,7 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
const trashCount = useAppSelector((s) => s.workflows.deleted.length);
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(null);
+ const openWorkflowMenu = useWorkflowMenu();
const workflows = useMemo(() => Object.values(items)
.filter((w) => !w.unsaved)
@@ -94,6 +96,10 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
nav.selectWorkflow(w.id)}
+ onContextMenu={(e) => openWorkflowMenu(e, w, {
+ onEdit: () => nav.selectWorkflow(w.id),
+ afterTrash: () => { if (nav.selectedId === w.id) nav.goHome(); },
+ })}
onMouseEnter={() => setHovered(w.id)}
onMouseLeave={() => setHovered((h) => (h === w.id ? null : h))}
style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '5px 9px', borderRadius: 8, cursor: 'pointer', background: isSel ? WC.selBg : 'transparent' }}
diff --git a/frontend/src/app/pages/Workflows/app/StepsCard.tsx b/frontend/src/app/pages/Workflows/app/StepsCard.tsx
index e5aaec16..e4b2fedc 100644
--- a/frontend/src/app/pages/Workflows/app/StepsCard.tsx
+++ b/frontend/src/app/pages/Workflows/app/StepsCard.tsx
@@ -4,6 +4,7 @@ import { useAppDispatch } from '@/shared/hooks';
import { commitDraft } from '@/shared/state/workflowsSlice';
import type { Workflow, WorkflowStep } from '@/shared/state/workflowsSlice';
import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils';
+import { openCardContextMenu, isNativeMenuTarget } from '@/app/pages/Dashboard/desktop/openCardContextMenu';
import { useWC, FONT_SERIF, FONT_SANS, track, knob } from './uiKit';
import { useWorkflowPatch } from './useWorkflowPatch';
@@ -100,7 +101,21 @@ const StepsCard: React.FC<{ workflow: Workflow }> = ({ workflow }) => {
{local.map((s, i) => (
-
+
{
+ if (isNativeMenuTarget(e)) return;
+ openCardContextMenu(e, {
+ items: [
+ { label: s.open ? 'Collapse' : 'Edit prompt', onClick: () => update(s.id, { open: !s.open }) },
+ { label: s.enabled ? 'Disable step' : 'Enable step', onClick: () => toggleEnabled(s.id) },
+ { kind: 'separator' },
+ { label: 'Remove step', danger: true, onClick: () => onDelete(s.id) },
+ ],
+ });
+ }}
+ style={{ border: `1px solid ${s.open ? `rgba(${WC.inkRGB},0.16)` : `rgba(${WC.inkRGB},0.10)`}`, borderRadius: WC.radius.md, background: s.open ? WC.raised : WC.paper, overflow: 'hidden' }}
+ >
{i + 1}
{
{deleted.length > 0 ? (
{deleted.map((w) => (
-
+
openCardContextMenu(e, {
+ items: [
+ { label: 'Restore', onClick: () => dispatch(restoreWorkflow(w.id)) },
+ { kind: 'separator' },
+ { label: 'Delete forever', danger: true, onClick: () => onPurge(w.id, w.title || 'this workflow') },
+ ],
+ })}
+ style={{ display: 'flex', alignItems: 'center', gap: 14, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '13px 16px' }}
+ >
{w.title || 'Untitled workflow'}
diff --git a/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts b/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts
new file mode 100644
index 00000000..ca2524db
--- /dev/null
+++ b/frontend/src/app/pages/Workflows/app/useWorkflowMenu.ts
@@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+import type React from 'react';
+import { useAppDispatch, useAppSelector } from '@/shared/hooks';
+import { deleteWorkflow, runWorkflowNow } from '@/shared/state/workflowsSlice';
+import type { Workflow } from '@/shared/state/workflowsSlice';
+import { openWorkflowMonitor } from '@/shared/state/dashboardLayoutSlice';
+import { stepsSignature } from '@/app/pages/Workflows/scheduleUtils';
+import { openCardContextMenu, isNativeMenuTarget } from '@/app/pages/Dashboard/desktop/openCardContextMenu';
+import { isRunning } from './model';
+import { useWorkflowPatch } from './useWorkflowPatch';
+
+interface WorkflowMenuOptions {
+ onEdit: () => void;
+ afterTrash?: () => void;
+}
+
+/** The one workflow right-click row set, so every surface (rail, home, calendars, popover) offers the same verbs. */
+export function useWorkflowMenu(): (e: React.MouseEvent, wf: Workflow, opts: WorkflowMenuOptions) => void {
+ const dispatch = useAppDispatch();
+ const active = useAppSelector((s) => s.workflows.active);
+ const patch = useWorkflowPatch();
+ return useCallback((e: React.MouseEvent, wf: Workflow, opts: WorkflowMenuOptions) => {
+ if (isNativeMenuTarget(e)) return;
+ const running = isRunning(wf, active);
+ openCardContextMenu(e, {
+ items: [
+ {
+ label: 'Run now',
+ disabled: running,
+ onClick: () => {
+ dispatch(runWorkflowNow({ id: wf.id, signature: stepsSignature(wf.steps) }));
+ dispatch(openWorkflowMonitor({ workflowId: wf.id }));
+ },
+ },
+ {
+ label: wf.schedule.enabled ? 'Pause schedule' : 'Resume schedule',
+ onClick: () => patch(wf, { schedule: { ...wf.schedule, enabled: !wf.schedule.enabled } }),
+ },
+ { label: 'Edit', onClick: opts.onEdit },
+ { kind: 'separator' },
+ {
+ label: 'Move to Trash',
+ danger: true,
+ onClick: () => {
+ dispatch(deleteWorkflow(wf.id));
+ opts.afterTrash?.();
+ },
+ },
+ ],
+ });
+ }, [dispatch, active, patch]);
+}