remove the Automation page
Deletes the Automation and NewAutomation screens, their /automation and /new-automation routes, the Dashboard barrel exports, the dock item (+ default dock path), and the page-title rule. TaskRunnerModal is kept — the file browser still uses it to run tasks on files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,8 +50,6 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
|
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
|
||||||
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
|
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
|
||||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
|
||||||
<Route path="/new-automation" element={<Dashboard.NewAutomation />} />
|
|
||||||
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
||||||
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
||||||
<Route path="/chat/saved/:id" element={<Dashboard.SessionListPage />} />
|
<Route path="/chat/saved/:id" element={<Dashboard.SessionListPage />} />
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import { Play, Trash2, Terminal, Bot, Workflow } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
|
||||||
import { useClient } from 'hooks/useClient';
|
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
|
||||||
import { Card } from '@/components/Card';
|
|
||||||
import { TaskRunnerModal } from 'officerdev';
|
|
||||||
import type { TaskSummary } from 'officerdev';
|
|
||||||
|
|
||||||
type TaskDetail = {
|
|
||||||
dirName: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
mode: string;
|
|
||||||
language: string | null;
|
|
||||||
body: string | null;
|
|
||||||
inputs: Record<string, unknown> | null;
|
|
||||||
config: { steps?: Array<{ task: string; foreach?: string }> } | null;
|
|
||||||
version: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const modeLabels: Record<string, { label: string; icon: typeof Terminal; color: string }> = {
|
|
||||||
script: { label: 'Script', icon: Terminal, color: 'bg-emerald-500/10 text-emerald-600' },
|
|
||||||
agentic: { label: 'Agentic', icon: Bot, color: 'bg-violet-500/10 text-violet-600' },
|
|
||||||
pipeline: { label: 'Pipeline', icon: Workflow, color: 'bg-amber-500/10 text-amber-600' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AutomationDetail = () => {
|
|
||||||
const client = useClient();
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [selected, setSelected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
|
||||||
const [runTask, setRunTask] = useState<TaskSummary | null>(null);
|
|
||||||
|
|
||||||
// Reset delete confirm when selection changes
|
|
||||||
useEffect(() => {
|
|
||||||
setDeleteConfirm(false);
|
|
||||||
}, [selected?.dirName]);
|
|
||||||
|
|
||||||
const { data: detail } = useQuery<TaskDetail>({
|
|
||||||
queryKey: ['tasks', selected?.dirName],
|
|
||||||
queryFn: () => client.get<TaskDetail>(`/tasks/${selected!.dirName}`),
|
|
||||||
enabled: !!selected?.dirName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!selected) return;
|
|
||||||
try {
|
|
||||||
await client.delete(`/tasks/${selected.dirName}`);
|
|
||||||
await qc.invalidateQueries({ queryKey: ['tasks'] });
|
|
||||||
setDeleteConfirm(false);
|
|
||||||
setSelected(null);
|
|
||||||
toast.success('Automation deleted');
|
|
||||||
} catch {
|
|
||||||
toast.error('Failed to delete automation');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!selected) {
|
|
||||||
return (
|
|
||||||
<div className="h-full flex items-center justify-center">
|
|
||||||
<p className="text-sm text-duck-dark/40 dark:text-foreground/40">Select an automation to view details</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = modeLabels[selected.mode] ?? modeLabels.agentic!;
|
|
||||||
const ModeIcon = mode.icon;
|
|
||||||
|
|
||||||
const hasSteps = detail?.config?.steps && detail.config.steps.length > 0;
|
|
||||||
const hasInputs = detail?.inputs && Object.keys(detail.inputs).length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className="h-full overflow-hidden flex flex-col min-h-0">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60 flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70 flex-1 truncate">
|
|
||||||
{detail?.name ?? selected.name}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${mode.color}`}
|
|
||||||
>
|
|
||||||
<ModeIcon className="h-2.5 w-2.5" />
|
|
||||||
{mode.label}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setRunTask(selected)}
|
|
||||||
className="p-1 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors"
|
|
||||||
title="Run"
|
|
||||||
>
|
|
||||||
<Play className="h-3.5 w-3.5 text-duck-teal" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setDeleteConfirm(true)}
|
|
||||||
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-500/10 cursor-pointer transition-colors"
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3.5 w-3.5 text-duck-dark/50 dark:text-foreground/50 hover:text-red-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="overflow-y-auto flex-1 p-6">
|
|
||||||
{/* Description */}
|
|
||||||
{detail?.description && (
|
|
||||||
<p className="text-sm text-duck-dark/60 dark:text-foreground/60 mb-4">{detail.description}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Meta badges */}
|
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
|
||||||
{detail?.language && (
|
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-duck-dark/8 dark:bg-foreground/8 text-duck-dark/50 dark:text-foreground/50 font-medium">
|
|
||||||
{detail.language}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{detail?.version && (
|
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-duck-dark/8 dark:bg-foreground/8 text-duck-dark/50 dark:text-foreground/50 font-medium">
|
|
||||||
v{detail.version}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pipeline steps */}
|
|
||||||
{hasSteps && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-xs font-medium text-duck-dark/50 dark:text-foreground/50 uppercase tracking-wider mb-2">
|
|
||||||
Pipeline Steps
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{detail!.config!.steps!.map((step, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex items-center gap-2 text-sm text-duck-dark/70 dark:text-foreground/70 px-2 py-1 rounded bg-duck-dark/3 dark:bg-foreground/3"
|
|
||||||
>
|
|
||||||
<span className="w-5 text-center font-mono text-xs text-duck-dark/40 dark:text-foreground/40">
|
|
||||||
{i + 1}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">{step.task}</span>
|
|
||||||
{step.foreach && (
|
|
||||||
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
|
||||||
(foreach: {step.foreach})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Inputs */}
|
|
||||||
{hasInputs && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-xs font-medium text-duck-dark/50 dark:text-foreground/50 uppercase tracking-wider mb-2">
|
|
||||||
Inputs
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{Object.entries(detail!.inputs!).map(([key, def]) => {
|
|
||||||
const d = def as { type?: string; description?: string; default?: string };
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
className="flex items-baseline gap-2 text-sm px-2 py-1 rounded bg-duck-dark/3 dark:bg-foreground/3"
|
|
||||||
>
|
|
||||||
<span className="font-mono text-xs text-duck-teal">{key}</span>
|
|
||||||
{d.type && (
|
|
||||||
<span className="text-[10px] text-duck-dark/40 dark:text-foreground/40">{d.type}</span>
|
|
||||||
)}
|
|
||||||
{d.description && (
|
|
||||||
<span className="text-xs text-duck-dark/50 dark:text-foreground/50 flex-1">
|
|
||||||
{d.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Body (agent instructions) */}
|
|
||||||
{detail?.body ? (
|
|
||||||
<article className="skill-md">
|
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
|
||||||
{detail.body}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</article>
|
|
||||||
) : detail && !hasSteps ? (
|
|
||||||
<p className="text-sm text-duck-dark/40 dark:text-foreground/40 text-center mt-12">No instructions</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Delete confirmation */}
|
|
||||||
<Dialog open={deleteConfirm} onOpenChange={setDeleteConfirm}>
|
|
||||||
<DialogContent className="sm:max-w-md z-[700]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Delete Automation</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Are you sure you want to delete "{selected.name}"? This cannot be undone.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex justify-end gap-2 mt-2">
|
|
||||||
<Button variant="outline" onClick={() => setDeleteConfirm(false)} className="cursor-pointer">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" onClick={handleDelete} className="cursor-pointer">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Run modal */}
|
|
||||||
{runTask && (
|
|
||||||
<TaskRunnerModal
|
|
||||||
open
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) setRunTask(null);
|
|
||||||
}}
|
|
||||||
task={runTask}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { Plus, Search, Play, Terminal, Bot, Workflow } from 'lucide-react';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { useClient } from 'hooks/useClient';
|
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
|
||||||
import { TaskRunnerModal } from 'officerdev';
|
|
||||||
import type { TaskSummary } from 'officerdev';
|
|
||||||
|
|
||||||
const modeIcons = {
|
|
||||||
script: Terminal,
|
|
||||||
agentic: Bot,
|
|
||||||
pipeline: Workflow,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const modeColors = {
|
|
||||||
script: 'bg-emerald-500/10 text-emerald-600',
|
|
||||||
agentic: 'bg-violet-500/10 text-violet-600',
|
|
||||||
pipeline: 'bg-amber-500/10 text-amber-600',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const AutomationList = () => {
|
|
||||||
const client = useClient();
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [selected, setSelected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
|
||||||
const [createName, setCreateName] = useState('');
|
|
||||||
const [createDesc, setCreateDesc] = useState('');
|
|
||||||
const [creating, setCreating] = useState(false);
|
|
||||||
const [runTask, setRunTask] = useState<TaskSummary | null>(null);
|
|
||||||
|
|
||||||
const { data: tasks = [] } = useQuery<TaskSummary[]>({
|
|
||||||
queryKey: ['tasks'],
|
|
||||||
queryFn: () => client.get('/tasks'),
|
|
||||||
staleTime: 60_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
const filtered = search
|
|
||||||
? tasks.filter((t) => t.name.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q))
|
|
||||||
: tasks;
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
const name = createName.trim();
|
|
||||||
if (!name) return;
|
|
||||||
setCreating(true);
|
|
||||||
try {
|
|
||||||
await client.post('/tasks', { name, description: createDesc.trim() || undefined });
|
|
||||||
await qc.invalidateQueries({ queryKey: ['tasks'] });
|
|
||||||
setShowCreate(false);
|
|
||||||
setCreateName('');
|
|
||||||
setCreateDesc('');
|
|
||||||
toast.success('Automation created');
|
|
||||||
} catch {
|
|
||||||
toast.error('Failed to create automation');
|
|
||||||
} finally {
|
|
||||||
setCreating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60 flex items-center gap-2">
|
|
||||||
<span className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70 flex-1">Automations</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreate(true)}
|
|
||||||
className="p-1 rounded hover:bg-duck-dark/10 dark:hover:bg-foreground/10 cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
<Plus className="h-3.5 w-3.5 text-duck-dark/50 dark:text-foreground/50" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="px-3 pt-3">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-duck-dark/30 dark:text-foreground/30" />
|
|
||||||
<input
|
|
||||||
value={search}
|
|
||||||
onChange={(ev) => setSearch(ev.target.value)}
|
|
||||||
placeholder="Search..."
|
|
||||||
className="w-full rounded border border-duck-dark/15 dark:border-foreground/15 bg-background/80 pl-7 pr-2 py-1.5 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task list */}
|
|
||||||
<div className="flex-1 overflow-y-auto px-3 pt-2 pb-3">
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
{filtered.map((task) => {
|
|
||||||
const ModeIcon = modeIcons[task.mode] ?? Bot;
|
|
||||||
const modeColor = modeColors[task.mode] ?? modeColors.agentic;
|
|
||||||
const isSelected = selected?.dirName === task.dirName;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={task.dirName}
|
|
||||||
className={`group flex items-center gap-2 w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-duck-teal/10 text-duck-dark dark:text-foreground'
|
|
||||||
: 'text-duck-dark/60 dark:text-foreground/60 hover:bg-duck-dark/5 dark:hover:bg-foreground/5 hover:text-duck-dark dark:hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelected(task)}
|
|
||||||
className="flex-1 min-w-0 text-left cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className="font-medium truncate">{task.name}</span>
|
|
||||||
<span className={`shrink-0 inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-[9px] font-medium ${modeColor}`}>
|
|
||||||
<ModeIcon className="h-2.5 w-2.5" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{task.description && (
|
|
||||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 mt-0.5 line-clamp-1">{task.description}</p>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={(ev) => { ev.stopPropagation(); setRunTask(task); }}
|
|
||||||
className="p-1 rounded hover:bg-duck-teal/10 transition-colors cursor-pointer opacity-0 group-hover:opacity-100 shrink-0"
|
|
||||||
title="Run"
|
|
||||||
>
|
|
||||||
<Play className="h-3 w-3 text-duck-teal" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{tasks.length === 0 && (
|
|
||||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 px-3 py-4 text-center">
|
|
||||||
No automations yet
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{tasks.length > 0 && filtered.length === 0 && (
|
|
||||||
<p className="text-xs text-duck-dark/40 dark:text-foreground/40 px-3 py-4 text-center">
|
|
||||||
No matches
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create dialog */}
|
|
||||||
<Dialog open={showCreate} onOpenChange={setShowCreate}>
|
|
||||||
<DialogContent className="sm:max-w-md z-[700]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create Automation</DialogTitle>
|
|
||||||
<DialogDescription>Give your automation a name and optional description.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form
|
|
||||||
onSubmit={(ev) => { ev.preventDefault(); handleCreate(); }}
|
|
||||||
className="flex flex-col gap-3 mt-1"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Name</label>
|
|
||||||
<input
|
|
||||||
value={createName}
|
|
||||||
onChange={(ev) => setCreateName(ev.target.value)}
|
|
||||||
placeholder="e.g. Batch Resize Images"
|
|
||||||
autoFocus
|
|
||||||
className="rounded border border-duck-dark/20 dark:border-foreground/20 bg-background px-3 py-2 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
<label className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Description</label>
|
|
||||||
<textarea
|
|
||||||
value={createDesc}
|
|
||||||
onChange={(ev) => setCreateDesc(ev.target.value)}
|
|
||||||
placeholder="What does this automation do?"
|
|
||||||
rows={3}
|
|
||||||
className="rounded border border-duck-dark/20 dark:border-foreground/20 bg-background px-3 py-2 text-sm text-duck-dark dark:text-foreground placeholder:text-duck-dark/30 dark:placeholder:text-foreground/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30 resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end gap-2 mt-1">
|
|
||||||
<Button type="button" variant="outline" onClick={() => setShowCreate(false)} className="cursor-pointer">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={!createName.trim() || creating}
|
|
||||||
className="bg-duck-teal text-white hover:bg-duck-teal/90 cursor-pointer"
|
|
||||||
>
|
|
||||||
{creating ? 'Creating...' : 'Create'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Run modal */}
|
|
||||||
{runTask && (
|
|
||||||
<TaskRunnerModal
|
|
||||||
open
|
|
||||||
onOpenChange={(open) => { if (!open) setRunTask(null); }}
|
|
||||||
task={runTask}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Automation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The `/automation` route shows all user automations (tasks) in a standard `WorkspaceView` layout: left panel for the list, right panel for task details.
|
|
||||||
|
|
||||||
## Route
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
+------------------+------------------------------------------+
|
|
||||||
| List (30%) | Detail (70%) |
|
|
||||||
| | |
|
|
||||||
| Search | Task name, mode, description |
|
|
||||||
| Task items | Pipeline steps / Inputs / Body |
|
|
||||||
| + Create | Run / Delete actions |
|
|
||||||
+------------------+------------------------------------------+
|
|
||||||
```
|
|
||||||
|
|
||||||
Uses `useDashboardState('screens/automation')` for persistent layout and `usePanelChannel<TaskSummary>('automation:selected-task')` for cross-panel selection.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### `index.tsx`
|
|
||||||
Entry point. Sets up `WorkspaceView` with two panels, manages mobile panel switching.
|
|
||||||
|
|
||||||
### `AutomationList.tsx`
|
|
||||||
Left panel. Fetches tasks from `GET /tasks`, shows searchable list with mode badges (Script/Agentic/Pipeline). Has create dialog and run button per task.
|
|
||||||
|
|
||||||
### `AutomationDetail.tsx`
|
|
||||||
Right panel. Fetches task detail from `GET /tasks/:name`. Shows description, pipeline steps, input definitions, and markdown body. Run and delete actions.
|
|
||||||
|
|
||||||
## Task Data
|
|
||||||
|
|
||||||
Tasks come from `officerdb` database. Modes:
|
|
||||||
- **script** (green) — deterministic bash/python/ts
|
|
||||||
- **agentic** (purple) — AI-powered with MCP tools
|
|
||||||
- **pipeline** (amber) — multi-step orchestration
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- `TaskRunnerModal` in `officerdev` workspace handles execution for all modes
|
|
||||||
- `CapabilityPage.tsx` — legacy standalone pages for `/skills`, `/tasks`, `/processes`
|
|
||||||
- File browser context menu triggers via `useTasks` hook
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
|
||||||
import { WorkspaceView } from 'officerdev';
|
|
||||||
import { useIsMobile } from 'hooks/useIsMobile';
|
|
||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
|
||||||
import { useDashboardState } from 'state/useDashboardState';
|
|
||||||
import type { TaskSummary } from 'officerdev';
|
|
||||||
|
|
||||||
import { AutomationList } from './AutomationList';
|
|
||||||
import { AutomationDetail } from './AutomationDetail';
|
|
||||||
|
|
||||||
export type { TaskSummary as SelectedTask };
|
|
||||||
|
|
||||||
const defaultLayout: LayoutNode = {
|
|
||||||
type: 'group',
|
|
||||||
id: 'automation-root',
|
|
||||||
direction: 'horizontal',
|
|
||||||
children: [
|
|
||||||
{ node: { type: 'panel', id: 'automation-list', appType: null }, size: 30 },
|
|
||||||
{ node: { type: 'panel', id: 'automation-detail', appType: null }, size: 70 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Automation = () => {
|
|
||||||
const workspace = useDashboardState<LayoutNode>('screens/automation', defaultLayout);
|
|
||||||
const [selected] = usePanelChannel<TaskSummary | null>('automation:selected-task', null);
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const panelComponents: PanelComponents = useMemo(
|
|
||||||
() => ({
|
|
||||||
'automation-list': AutomationList,
|
|
||||||
'automation-detail': AutomationDetail,
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const mobilePanelId = isMobile && selected ? 'automation-detail' : undefined;
|
|
||||||
const onMobileBack = useCallback(() => {}, []);
|
|
||||||
|
|
||||||
if (!workspace.isLoaded) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full pt-2">
|
|
||||||
<WorkspaceView
|
|
||||||
workspace={workspace}
|
|
||||||
locked
|
|
||||||
components={panelComponents}
|
|
||||||
mobilePanelId={mobilePanelId}
|
|
||||||
onMobilePanelChange={mobilePanelId ? () => onMobileBack() : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -110,7 +110,7 @@ export const Dock = ({ items, className }: DockProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
import { Home, MessageCircle, FileText, FolderOpen, Code, LayoutGrid, Bot, ScrollText, FolderKanban, Monitor, Mail, Globe, MonitorSmartphone, Workflow } from 'lucide-react';
|
import { Home, MessageCircle, FileText, FolderOpen, Code, LayoutGrid, ScrollText, FolderKanban, Monitor, Mail, Globe, MonitorSmartphone, Workflow } from 'lucide-react';
|
||||||
|
|
||||||
export const ALL_DOCK_ITEMS: DockItem[] = [
|
export const ALL_DOCK_ITEMS: DockItem[] = [
|
||||||
{ label: 'Home', to: '/', icon: Home, color: '#f59e0b' },
|
{ label: 'Home', to: '/', icon: Home, color: '#f59e0b' },
|
||||||
@@ -119,7 +119,6 @@ export const ALL_DOCK_ITEMS: DockItem[] = [
|
|||||||
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
|
{ label: 'Chat', to: '/chat', icon: MessageCircle, color: '#60a5fa' },
|
||||||
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
{ label: 'Editor', to: '/code-editor', icon: Code, color: '#a78bfa' },
|
||||||
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
|
{ label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
|
||||||
{ label: 'Automation', to: '/automation', icon: Bot, color: '#2dd4bf' },
|
|
||||||
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
{ label: 'Jobs', to: '/jobs', icon: Workflow, color: '#14b8a6' },
|
||||||
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
||||||
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
{ label: 'Terminal', to: '/terminal', icon: Monitor, color: '#f97316' },
|
||||||
@@ -129,4 +128,4 @@ export const ALL_DOCK_ITEMS: DockItem[] = [
|
|||||||
{ label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' },
|
{ label: 'Dashboards', to: '/dashboards', icon: LayoutGrid, color: '#8b5cf6' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/automation', '/projects', '/dashboards', '/chat'];
|
export const DEFAULT_DOCK_PATHS = ['/', '/files', '/projects', '/dashboards', '/chat'];
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
|
||||||
import { WorkspaceLayout } from 'officerdev';
|
|
||||||
|
|
||||||
const LeftPanel = () => {
|
|
||||||
return <div className="h-full w-full p-4" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RightPanel = () => {
|
|
||||||
return <div className="h-full w-full p-4" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const layout: LayoutNode = {
|
|
||||||
type: 'group',
|
|
||||||
id: 'new-automation-root',
|
|
||||||
direction: 'horizontal',
|
|
||||||
children: [
|
|
||||||
{ node: { type: 'panel', id: 'new-automation-left', appType: null }, size: 30 },
|
|
||||||
{ node: { type: 'panel', id: 'new-automation-right', appType: null }, size: 70 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NewAutomation = () => {
|
|
||||||
const panelComponents: PanelComponents = useMemo(
|
|
||||||
() => ({
|
|
||||||
'new-automation-left': LeftPanel,
|
|
||||||
'new-automation-right': RightPanel,
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full pt-2">
|
|
||||||
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,8 +6,6 @@ export * from './Plans';
|
|||||||
export * from './Processes';
|
export * from './Processes';
|
||||||
export * from './CapabilityPage';
|
export * from './CapabilityPage';
|
||||||
export * from './Settings';
|
export * from './Settings';
|
||||||
export * from './Automation';
|
|
||||||
export * from './NewAutomation';
|
|
||||||
export * from './Skills';
|
export * from './Skills';
|
||||||
export * from './TaskLogs';
|
export * from './TaskLogs';
|
||||||
export * from './Tasks';
|
export * from './Tasks';
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const RULES: TitleRule[] = [
|
|||||||
{ match: (p) => p.startsWith('/skills'), title: 'Skills' },
|
{ match: (p) => p.startsWith('/skills'), title: 'Skills' },
|
||||||
{ match: (p) => p.startsWith('/processes'), title: 'Processes' },
|
{ match: (p) => p.startsWith('/processes'), title: 'Processes' },
|
||||||
{ match: (p) => p.startsWith('/dashboards'), title: 'Dashboards' },
|
{ match: (p) => p.startsWith('/dashboards'), title: 'Dashboards' },
|
||||||
{ match: (p) => p.startsWith('/automation') || p.startsWith('/new-automation'), title: 'Automation' },
|
|
||||||
{ match: (p) => p.startsWith('/terminal'), title: 'Terminal' },
|
{ match: (p) => p.startsWith('/terminal'), title: 'Terminal' },
|
||||||
{ match: (p) => p.startsWith('/browser'), title: 'Browser' },
|
{ match: (p) => p.startsWith('/browser'), title: 'Browser' },
|
||||||
{ match: (p) => p.startsWith('/desktop'), title: 'Desktop' },
|
{ match: (p) => p.startsWith('/desktop'), title: 'Desktop' },
|
||||||
|
|||||||
Reference in New Issue
Block a user