Workspaces layout, automation page

This commit is contained in:
2026-02-18 17:04:32 +00:00
parent 89f23f9426
commit 485ae4c3d7
89 changed files with 2168 additions and 514 deletions
@@ -10,7 +10,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
import { useClient } from 'hooks/useClient';
import { useVisibleClaudeModels } from '@/state/useModels';
import { Card } from '@/components/Card';
import type { ChatMessage } from 'widgets/Chat';
import type { ChatMessage } from 'apps/Chat';
import { useClaude } from '@/Screens/Dashboard/Chat/useClaude';
import { EmbeddableChat } from '@/Screens/Dashboard/Chat/EmbeddableChat';
type CapabilitySummary = {
@@ -20,13 +20,25 @@ type CapabilitySummary = {
scope: 'global' | 'user';
};
type CapabilityDetail = CapabilitySummary & {
export type CapabilityDetail = CapabilitySummary & {
body: string;
rawFrontmatter: string;
filePath: string;
chatSessionId: string | null;
};
type CapabilityListProps = {
kind: string;
endpoint: string;
queryKey: string;
selected: string | null;
onSelect: (dirName: string) => void;
onCreate?: (dirName: string) => void;
search?: string;
showCreate?: boolean;
onShowCreateChange?: (value: boolean) => void;
};
type CapabilityPageProps = {
kind: string;
endpoint: string;
@@ -41,10 +53,11 @@ type CapabilityChatProps = {
resourceDir: string;
chatSessionId: string | null;
isNew?: boolean;
description?: string;
onResponseEnd?: () => void;
};
const CapabilityChat = ({
export const CapabilityChat = ({
kind,
endpoint,
dirName,
@@ -52,6 +65,7 @@ const CapabilityChat = ({
resourceDir,
chatSessionId,
isNew,
description,
onResponseEnd,
}: CapabilityChatProps) => {
const client = useClient();
@@ -63,7 +77,7 @@ const CapabilityChat = ({
const defaultInput = chatSessionId
? undefined
: isNew
? `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 storage = useMemo(
@@ -110,7 +124,7 @@ const CapabilityChat = ({
);
};
const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
const [open, setOpen] = useState(false);
return (
@@ -129,18 +143,150 @@ const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
);
};
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);
const creating = showCreate ?? internalCreating;
const setCreating = onShowCreateChange ?? setInternalCreating;
const [newName, setNewName] = useState('');
const [internalSearch, setInternalSearch] = useState('');
const search = externalSearch ?? internalSearch;
const newNameRef = useRef<HTMLInputElement | null>(null);
const { data: items = [] } = useQuery<CapabilitySummary[]>({
queryKey: [queryKey],
queryFn: () => client.get<CapabilitySummary[]>(endpoint),
});
const handleCreate = async () => {
const name = newName.trim();
if (!name) return;
try {
const res = await client.post<{ name: string; dirName: string }>(endpoint, { name });
await qc.invalidateQueries({ queryKey: [queryKey] });
setCreating(false);
setNewName('');
(onCreate ?? onSelect)(res.dirName);
} catch {
toast.error(`Failed to create ${kind}`);
}
};
const filtered = items.filter(
(item) =>
!search ||
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.description?.toLowerCase().includes(search.toLowerCase()),
);
return (
<>
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 bg-white/60 flex items-center justify-between">
<span className="text-sm font-medium text-duck-dark/70">{kind}s</span>
{!creating && (
<button
onClick={() => {
setCreating(true);
setTimeout(() => newNameRef.current?.focus(), 0);
}}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
>
<Plus className="h-4 w-4 text-duck-dark/50" />
</button>
)}
</div>
{creating && (
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5">
<input
ref={newNameRef}
value={newName}
onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleCreate();
}
if (ev.key === 'Escape') {
setCreating(false);
setNewName('');
}
}}
placeholder={`${kind} name...`}
className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-white px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
/>
<button
onClick={handleCreate}
disabled={!newName.trim()}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors disabled:opacity-30"
>
<Check className="h-3.5 w-3.5 text-duck-teal" />
</button>
<button
onClick={() => {
setCreating(false);
setNewName('');
}}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
>
<X className="h-3.5 w-3.5 text-duck-dark/50" />
</button>
</div>
)}
{externalSearch === undefined && (
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10">
<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" />
<input
value={internalSearch}
onChange={(ev) => setInternalSearch(ev.target.value)}
placeholder={`Search ${kind.toLowerCase()}s...`}
className="w-full rounded border border-duck-dark/15 bg-white/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
/>
</div>
</div>
)}
<div className="overflow-y-auto flex-1">
{filtered.map((item) => (
<button
key={item.dirName}
onClick={() => onSelect(item.dirName)}
className={`w-full text-left px-4 py-3 border-b border-duck-dark/5 cursor-pointer transition-colors ${
selected === item.dirName ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
}`}
>
<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>
))}
{items.length === 0 && (
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No {kind.toLowerCase()}s found</p>
)}
{items.length > 0 && filtered.length === 0 && (
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No matches</p>
)}
</div>
</>
);
};
export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => {
const client = useClient();
const qc = useQueryClient();
const [selected, setSelected] = useState<string | null>(null);
const [editing, setEditing] = useState(false);
const [isNew, setIsNew] = useState(false);
const [creating, setCreating] = useState(false);
const [newName, setNewName] = useState('');
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [search, setSearch] = useState('');
const [showDetail, setShowDetail] = useState(false);
const newNameRef = useRef<HTMLInputElement | null>(null);
const { data: items = [] } = useQuery<CapabilitySummary[]>({
queryKey: [queryKey],
@@ -166,21 +312,11 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
setEditing(false);
};
const handleCreate = async () => {
const name = newName.trim();
if (!name) return;
try {
const res = await client.post<{ name: string; dirName: string }>(endpoint, { name });
await qc.invalidateQueries({ queryKey: [queryKey] });
setCreating(false);
setNewName('');
setSelected(res.dirName);
setShowDetail(true);
setIsNew(true);
setEditing(true);
} catch {
toast.error(`Failed to create ${kind}`);
}
const handleCreate = (dirName: string) => {
setSelected(dirName);
setShowDetail(true);
setIsNew(true);
setEditing(true);
};
const handleDelete = async () => {
@@ -197,13 +333,6 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
}
};
const filtered = items.filter(
(item) =>
!search ||
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.description?.toLowerCase().includes(search.toLowerCase()),
);
return (
<>
<div className="flex h-full p-2 md:p-4 gap-2 md:gap-4">
@@ -211,97 +340,14 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
<Card
className={`md:w-72 shrink-0 overflow-hidden flex flex-col ${showDetail ? 'hidden md:flex' : 'flex-1 md:flex-none'}`}
>
<div className="shrink-0 px-4 py-2 border-b border-duck-dark/10 bg-white/60 flex items-center justify-between">
<span className="text-sm font-medium text-duck-dark/70">{kind}s</span>
{!creating && (
<button
onClick={() => {
setCreating(true);
setTimeout(() => newNameRef.current?.focus(), 0);
}}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
>
<Plus className="h-4 w-4 text-duck-dark/50" />
</button>
)}
</div>
{creating && (
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10 bg-duck-teal/5 flex items-center gap-1.5">
<input
ref={newNameRef}
value={newName}
onChange={(ev) => setNewName(ev.target.value)}
onKeyDown={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleCreate();
}
if (ev.key === 'Escape') {
setCreating(false);
setNewName('');
}
}}
placeholder={`${kind} name...`}
className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-white px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
/>
<button
onClick={handleCreate}
disabled={!newName.trim()}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors disabled:opacity-30"
>
<Check className="h-3.5 w-3.5 text-duck-teal" />
</button>
<button
onClick={() => {
setCreating(false);
setNewName('');
}}
className="p-1 rounded hover:bg-duck-dark/10 cursor-pointer transition-colors"
>
<X className="h-3.5 w-3.5 text-duck-dark/50" />
</button>
</div>
)}
<div className="shrink-0 px-3 py-2 border-b border-duck-dark/10">
<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" />
<input
value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder={`Search ${kind.toLowerCase()}s...`}
className="w-full rounded border border-duck-dark/15 bg-white/80 pl-7 pr-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30"
/>
</div>
</div>
<div className="overflow-y-auto flex-1">
{filtered.map((item) => (
<button
key={item.dirName}
onClick={() => selectItem(item.dirName)}
className={`w-full text-left px-4 py-3 border-b border-duck-dark/5 cursor-pointer transition-colors ${
selected === item.dirName ? 'bg-duck-teal/10' : 'hover:bg-duck-dark/5'
}`}
>
<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>
))}
{items.length === 0 && (
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No {kind.toLowerCase()}s found</p>
)}
{items.length > 0 && filtered.length === 0 && (
<p className="text-sm text-duck-dark/40 px-4 py-6 text-center">No matches</p>
)}
</div>
<CapabilityList
kind={kind}
endpoint={endpoint}
queryKey={queryKey}
selected={selected}
onSelect={selectItem}
onCreate={handleCreate}
/>
</Card>
{/* Right panel — detail + chat */}