unify agent items into a flat file-based store, drop the marketplace
Replace the marketplace service dependency and the native/global/user scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding skills, tools, tasks, processes and extensions as plain files. - tasks move from Postgres to TASK.md files (new file-backed task layer); task editing now works, which the DB path never supported - skills/tools/processes collapse into one shared file router (single dir) - remove the marketplace client (sync-marketplace/sync-version) and the boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store - drop the dead tasks + vestigial skills/tools/processes/extensions + item_chats tables (migration 0004) - one-time migration script exports DB tasks and consolidates disk items Migration verified: all 6 tasks round-trip through the runtime parser identically to their DB rows (pipeline steps, triggers, script impls and agentic bodies all intact). NOTE: not yet functionally tested end-to-end — every item (each task mode, tool, skill, extension) still needs to be run/exercised in the app before this is trusted. To be done manually. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,25 +8,21 @@ 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 { useAuth } from 'hooks/useAuth';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { Card } from '@/components/Card';
|
||||
import { TaskRunnerModal } from 'officerdev';
|
||||
import type { TaskSummary } from 'officerdev';
|
||||
|
||||
type TaskDetail = {
|
||||
id: number;
|
||||
dirName: string;
|
||||
name: string;
|
||||
description: string;
|
||||
scope: 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;
|
||||
userId: number | null;
|
||||
};
|
||||
|
||||
const modeLabels: Record<string, { label: string; icon: typeof Terminal; color: string }> = {
|
||||
@@ -38,13 +34,14 @@ const modeLabels: Record<string, { label: string; icon: typeof Terminal; color:
|
||||
export const AutomationDetail = () => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
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]);
|
||||
useEffect(() => {
|
||||
setDeleteConfirm(false);
|
||||
}, [selected?.dirName]);
|
||||
|
||||
const { data: detail } = useQuery<TaskDetail>({
|
||||
queryKey: ['tasks', selected?.dirName],
|
||||
@@ -73,7 +70,6 @@ export const AutomationDetail = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const canModify = selected.scope === 'user' || user?.role === 'Super Admin';
|
||||
const mode = modeLabels[selected.mode] ?? modeLabels.agentic!;
|
||||
const ModeIcon = mode.icon;
|
||||
|
||||
@@ -88,7 +84,9 @@ export const AutomationDetail = () => {
|
||||
<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}`}>
|
||||
<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>
|
||||
@@ -99,15 +97,13 @@ export const AutomationDetail = () => {
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
{canModify && (
|
||||
<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>
|
||||
)}
|
||||
<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 */}
|
||||
@@ -119,11 +115,6 @@ export const AutomationDetail = () => {
|
||||
|
||||
{/* Meta badges */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{detail?.scope && detail.scope !== 'user' && (
|
||||
<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.scope}
|
||||
</span>
|
||||
)}
|
||||
{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}
|
||||
@@ -139,13 +130,24 @@ export const AutomationDetail = () => {
|
||||
{/* 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>
|
||||
<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>
|
||||
<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>}
|
||||
{step.foreach && (
|
||||
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">
|
||||
(foreach: {step.foreach})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -155,15 +157,26 @@ export const AutomationDetail = () => {
|
||||
{/* 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>
|
||||
<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">
|
||||
<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>}
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
@@ -208,7 +221,9 @@ export const AutomationDetail = () => {
|
||||
{runTask && (
|
||||
<TaskRunnerModal
|
||||
open
|
||||
onOpenChange={(open) => { if (!open) setRunTask(null); }}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setRunTask(null);
|
||||
}}
|
||||
task={runTask}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,14 +8,12 @@ import { ArrowLeft, Pencil, Plus, Check, X, Trash2, Search, ChevronRight } from
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { Card } from '@/components/Card';
|
||||
import { usePiChat, EmbeddableChat } from 'officerdev';
|
||||
type CapabilitySummary = {
|
||||
dirName: string;
|
||||
name: string;
|
||||
description: string;
|
||||
scope: 'native' | 'global' | 'user';
|
||||
};
|
||||
|
||||
export type CapabilityDetail = CapabilitySummary & {
|
||||
@@ -276,9 +274,9 @@ export const CapabilityChat = ({
|
||||
}: CapabilityChatProps) => {
|
||||
const seedFile = `${kind.toUpperCase()}.md`;
|
||||
const genericPrefix = `<frontmatter>\ninput file: ${filePath}\n${seedFile}: ${filePath}\ndir: ${resourceDir}\n\nBe aware of any extra files alongside the same dir as the ${kind} file we're handling, for possible extra context. You can also, if pertinent, create scripts or other files that will help you in the future.\n</frontmatter>`;
|
||||
const promptFrontmatter = isNew ? buildCreationPrefix(kind, filePath, resourceDir) ?? genericPrefix : genericPrefix;
|
||||
const promptFrontmatter = isNew ? (buildCreationPrefix(kind, filePath, resourceDir) ?? genericPrefix) : genericPrefix;
|
||||
const defaultInput = isNew
|
||||
? description ?? `Help me create the content for this new ${kind} file`
|
||||
? (description ?? `Help me create the content for this new ${kind} file`)
|
||||
: `Help me understand and improve this ${kind} file`;
|
||||
|
||||
const pi = usePiChat(undefined, undefined, { replaceUrl: false });
|
||||
@@ -294,14 +292,7 @@ export const CapabilityChat = ({
|
||||
wasGenerating.current = pi.isGenerating;
|
||||
}, [pi.isGenerating]);
|
||||
|
||||
return (
|
||||
<EmbeddableChat
|
||||
chat={pi}
|
||||
defaultInput={defaultInput}
|
||||
promptPrefix={promptFrontmatter}
|
||||
className="h-full"
|
||||
/>
|
||||
);
|
||||
return <EmbeddableChat chat={pi} defaultInput={defaultInput} promptPrefix={promptFrontmatter} className="h-full" />;
|
||||
};
|
||||
|
||||
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||
@@ -323,7 +314,17 @@ export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, onCreate, search: externalSearch, showCreate, onShowCreateChange }: CapabilityListProps) => {
|
||||
export const CapabilityList = ({
|
||||
kind,
|
||||
endpoint,
|
||||
queryKey,
|
||||
selected,
|
||||
onSelect,
|
||||
onCreate,
|
||||
search: externalSearch,
|
||||
showCreate,
|
||||
onShowCreateChange,
|
||||
}: CapabilityListProps) => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const [internalCreating, setInternalCreating] = useState(false);
|
||||
@@ -437,13 +438,6 @@ export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, o
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark truncate">{item.name}</span>
|
||||
<span
|
||||
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium ${
|
||||
item.scope === 'user' ? 'bg-duck-teal/20 text-duck-teal' : 'bg-duck-dark/10 text-duck-dark/60'
|
||||
}`}
|
||||
>
|
||||
{item.scope}
|
||||
</span>
|
||||
</div>
|
||||
{item.description && <p className="text-xs text-duck-dark/50 mt-1 line-clamp-2">{item.description}</p>}
|
||||
</button>
|
||||
@@ -462,7 +456,6 @@ export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, o
|
||||
export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => {
|
||||
const client = useClient();
|
||||
const qc = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
@@ -544,7 +537,7 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
|
||||
<span className="text-sm font-medium text-duck-dark/70 flex-1">
|
||||
{detail?.name ?? `Select a ${kind.toLowerCase()}`}
|
||||
</span>
|
||||
{detail && (detail.scope === 'user' || user?.role === 'Super Admin') && (
|
||||
{detail && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setEditing((e) => !e)}
|
||||
|
||||
Reference in New Issue
Block a user