sparkecoder 0.1.120 → 0.1.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.js +182 -21
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +800 -220
- package/dist/cli.js.map +1 -1
- package/dist/index.js +748 -168
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +748 -168
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/web/.next/server/app/(main)/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/settings/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/settings/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_lucide-react_dist_esm_icons_6ab1f7b7._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__f3e6443f._.js → [root-of-the-server]__4de426bd._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_62ca4286._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_settings_page_tsx_eb320e07._.js +3 -1
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/74ae1f17d607b2fc.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/883ea0d08f88e366.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
- package/web/.next/standalone/web/.next/static/chunks/9b88f148788e4504.js +3 -0
- package/web/.next/standalone/web/.next/static/chunks/acb0fc66f5414af6.css +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/74ae1f17d607b2fc.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/883ea0d08f88e366.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
- package/web/.next/standalone/web/.next/static/static/chunks/9b88f148788e4504.js +3 -0
- package/web/.next/standalone/web/.next/static/static/chunks/acb0fc66f5414af6.css +1 -0
- package/web/.next/standalone/web/src/app/(main)/settings/page.tsx +464 -1
- package/web/.next/static/chunks/74ae1f17d607b2fc.js +7 -0
- package/web/.next/static/chunks/883ea0d08f88e366.js +1 -0
- package/web/.next/static/chunks/{44c575e006ddb992.js → 91988e253d5fa420.js} +4 -4
- package/web/.next/static/chunks/9b88f148788e4504.js +3 -0
- package/web/.next/static/chunks/acb0fc66f5414af6.css +1 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_lucide-react_dist_esm_icons_7340c8b3._.js +0 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_41927ef5._.js +0 -3
- package/web/.next/standalone/web/.next/static/chunks/2c3c1d478808e4e4.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/3b0501ec3249235f.js +0 -7
- package/web/.next/standalone/web/.next/static/chunks/9a3afb48c245fb2a.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/b3bc7244f3477729.css +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/2c3c1d478808e4e4.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/3b0501ec3249235f.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/9a3afb48c245fb2a.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/b3bc7244f3477729.css +0 -1
- package/web/.next/static/chunks/2c3c1d478808e4e4.js +0 -1
- package/web/.next/static/chunks/3b0501ec3249235f.js +0 -7
- package/web/.next/static/chunks/9a3afb48c245fb2a.js +0 -1
- package/web/.next/static/chunks/b3bc7244f3477729.css +0 -1
- /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → static/BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
- /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_buildManifest.js +0 -0
- /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{uy1OnyxIm3QeGGgKEmxAj → BEIBC9-dP0_AWGmRy97hJ}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Copy, RotateCw, Trash2, Plus, Check, Eye, EyeOff, Bot, Cpu, Key,
|
|
7
7
|
BookOpen, X as XIcon, ShieldCheck, Plug, List as ListIcon,
|
|
8
8
|
ChevronLeft, ChevronRight, RefreshCw, Search,
|
|
9
|
+
Sparkles, FolderPlus, FileText, FolderOpen, Save, Pencil, Upload,
|
|
9
10
|
} from 'lucide-react';
|
|
10
11
|
import { Button } from '@/components/ui/button';
|
|
11
12
|
import { Input } from '@/components/ui/input';
|
|
@@ -62,12 +63,13 @@ async function jsonFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
62
63
|
return res.json();
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
type TabId = 'general' | 'integrations' | 'mcp' | 'schedules' | 'webhooks' | 'audit' | 'models' | 'apikeys';
|
|
66
|
+
type TabId = 'general' | 'integrations' | 'mcp' | 'schedules' | 'webhooks' | 'audit' | 'models' | 'apikeys' | 'skills';
|
|
66
67
|
|
|
67
68
|
const TABS: Array<{ id: TabId; label: string; icon: React.ComponentType<{ className?: string }> }> = [
|
|
68
69
|
{ id: 'general', label: 'General', icon: Bot },
|
|
69
70
|
{ id: 'integrations', label: 'Integrations', icon: Slack },
|
|
70
71
|
{ id: 'mcp', label: 'MCP', icon: Plug },
|
|
72
|
+
{ id: 'skills', label: 'Skills', icon: Sparkles },
|
|
71
73
|
{ id: 'schedules', label: 'Schedules', icon: CalendarIcon },
|
|
72
74
|
{ id: 'webhooks', label: 'Webhooks', icon: WebhookIcon },
|
|
73
75
|
{ id: 'audit', label: 'Audit log', icon: ListIcon },
|
|
@@ -149,6 +151,7 @@ export default function SettingsPage() {
|
|
|
149
151
|
{tab === 'audit' && <AuditSection />}
|
|
150
152
|
{tab === 'models' && <ModelsSection />}
|
|
151
153
|
{tab === 'apikeys' && <ApiKeysSection />}
|
|
154
|
+
{tab === 'skills' && <SkillsSection />}
|
|
152
155
|
</div>
|
|
153
156
|
</div>
|
|
154
157
|
</div>
|
|
@@ -1421,6 +1424,466 @@ function formatEventTime(iso: string): string {
|
|
|
1421
1424
|
return `${date} ${time}`;
|
|
1422
1425
|
}
|
|
1423
1426
|
|
|
1427
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1428
|
+
// Skills — markdown skill files from default + global directories
|
|
1429
|
+
// ────────────────────────────────────────────────────────────────────
|
|
1430
|
+
|
|
1431
|
+
interface SkillRow {
|
|
1432
|
+
id: string;
|
|
1433
|
+
name: string;
|
|
1434
|
+
description: string;
|
|
1435
|
+
filePath: string;
|
|
1436
|
+
fileName: string;
|
|
1437
|
+
sourceDir: string;
|
|
1438
|
+
sourceLabel: string;
|
|
1439
|
+
sourceType: 'builtin' | 'project' | 'additional';
|
|
1440
|
+
alwaysApply: boolean;
|
|
1441
|
+
globs: string[];
|
|
1442
|
+
sizeBytes: number;
|
|
1443
|
+
}
|
|
1444
|
+
interface SkillDirectory {
|
|
1445
|
+
path: string;
|
|
1446
|
+
label: string;
|
|
1447
|
+
source: 'builtin' | 'project' | 'additional';
|
|
1448
|
+
alwaysApply: boolean;
|
|
1449
|
+
exists: boolean;
|
|
1450
|
+
writable: boolean;
|
|
1451
|
+
}
|
|
1452
|
+
interface SkillsResponse {
|
|
1453
|
+
directories: SkillDirectory[];
|
|
1454
|
+
skills: SkillRow[];
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function SkillsSection() {
|
|
1458
|
+
const [data, setData] = useState<SkillsResponse | null>(null);
|
|
1459
|
+
const [loading, setLoading] = useState(true);
|
|
1460
|
+
const [filter, setFilter] = useState('');
|
|
1461
|
+
const [editing, setEditing] = useState<SkillRow | null>(null);
|
|
1462
|
+
const [creatingInDir, setCreatingInDir] = useState<string | null>(null);
|
|
1463
|
+
const [addDirOpen, setAddDirOpen] = useState(false);
|
|
1464
|
+
const [dragTargetDir, setDragTargetDir] = useState<string | null>(null);
|
|
1465
|
+
|
|
1466
|
+
const refresh = useCallback(async () => {
|
|
1467
|
+
setLoading(true);
|
|
1468
|
+
try { setData(await jsonFetch<SkillsResponse>('/api/skills')); }
|
|
1469
|
+
finally { setLoading(false); }
|
|
1470
|
+
}, []);
|
|
1471
|
+
|
|
1472
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
1473
|
+
|
|
1474
|
+
if (loading || !data) {
|
|
1475
|
+
return <div className="flex items-center justify-center py-12"><Loader2 className="size-5 animate-spin text-muted-foreground" /></div>;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const grouped = data.directories.map((dir) => ({
|
|
1479
|
+
dir,
|
|
1480
|
+
items: data.skills.filter((s) => s.sourceDir === dir.path)
|
|
1481
|
+
.filter((s) => !filter || s.name.toLowerCase().includes(filter.toLowerCase()) || s.description.toLowerCase().includes(filter.toLowerCase())),
|
|
1482
|
+
}));
|
|
1483
|
+
|
|
1484
|
+
const handleDropFiles = async (files: FileList | null, targetDir: string) => {
|
|
1485
|
+
if (!files || files.length === 0) return;
|
|
1486
|
+
for (const file of Array.from(files)) {
|
|
1487
|
+
if (!/\.(md|mdc|markdown|txt)$/i.test(file.name)) continue;
|
|
1488
|
+
const text = await file.text();
|
|
1489
|
+
try {
|
|
1490
|
+
await jsonFetch('/api/skills', {
|
|
1491
|
+
method: 'POST',
|
|
1492
|
+
body: JSON.stringify({ directory: targetDir, fileName: file.name, content: text }),
|
|
1493
|
+
});
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
console.error('skill upload failed', err);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
refresh();
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
return (
|
|
1502
|
+
<section>
|
|
1503
|
+
<div className="flex items-center justify-between mb-3">
|
|
1504
|
+
<SectionHeader
|
|
1505
|
+
title="Skills"
|
|
1506
|
+
description="Markdown skill files the orchestrator loads on demand. Pulled from built-in defaults plus any directories below — drop .md files in or create new ones from here."
|
|
1507
|
+
/>
|
|
1508
|
+
<div className="flex gap-2">
|
|
1509
|
+
<Button size="sm" variant="outline" onClick={() => setAddDirOpen(true)}>
|
|
1510
|
+
<FolderPlus className="size-3.5 mr-1" />Add folder
|
|
1511
|
+
</Button>
|
|
1512
|
+
<Button size="sm" variant="ghost" onClick={refresh}>
|
|
1513
|
+
<RefreshCw className="size-3.5" />
|
|
1514
|
+
</Button>
|
|
1515
|
+
</div>
|
|
1516
|
+
</div>
|
|
1517
|
+
|
|
1518
|
+
<div className="mb-3">
|
|
1519
|
+
<div className="relative max-w-sm">
|
|
1520
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
|
1521
|
+
<Input
|
|
1522
|
+
value={filter}
|
|
1523
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
1524
|
+
placeholder="Search skills…"
|
|
1525
|
+
className="pl-7 h-8 text-xs"
|
|
1526
|
+
/>
|
|
1527
|
+
</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
|
|
1530
|
+
{addDirOpen && (
|
|
1531
|
+
<AddSkillDirectoryForm
|
|
1532
|
+
onCancel={() => setAddDirOpen(false)}
|
|
1533
|
+
onSaved={() => { setAddDirOpen(false); refresh(); }}
|
|
1534
|
+
/>
|
|
1535
|
+
)}
|
|
1536
|
+
|
|
1537
|
+
<div className="space-y-3">
|
|
1538
|
+
{grouped.map(({ dir, items }) => (
|
|
1539
|
+
<div
|
|
1540
|
+
key={dir.path}
|
|
1541
|
+
onDragOver={(e) => {
|
|
1542
|
+
if (!dir.writable) return;
|
|
1543
|
+
e.preventDefault();
|
|
1544
|
+
setDragTargetDir(dir.path);
|
|
1545
|
+
}}
|
|
1546
|
+
onDragLeave={() => setDragTargetDir((d) => (d === dir.path ? null : d))}
|
|
1547
|
+
onDrop={(e) => {
|
|
1548
|
+
e.preventDefault();
|
|
1549
|
+
setDragTargetDir(null);
|
|
1550
|
+
if (!dir.writable) return;
|
|
1551
|
+
void handleDropFiles(e.dataTransfer.files, dir.path);
|
|
1552
|
+
}}
|
|
1553
|
+
className={cn(
|
|
1554
|
+
'rounded-lg border bg-card/40 p-3 transition-colors',
|
|
1555
|
+
dragTargetDir === dir.path ? 'border-primary bg-primary/5' : 'border-border/60',
|
|
1556
|
+
)}
|
|
1557
|
+
>
|
|
1558
|
+
<div className="flex items-center gap-2 mb-2">
|
|
1559
|
+
<FolderOpen className={cn('size-4', dir.alwaysApply ? 'text-amber-500' : 'text-muted-foreground')} />
|
|
1560
|
+
<span className="text-sm font-medium">{dir.label}</span>
|
|
1561
|
+
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">{dir.source}</span>
|
|
1562
|
+
{dir.alwaysApply && <span className="text-[10px] uppercase tracking-wide text-amber-500">always-on</span>}
|
|
1563
|
+
{!dir.exists && <span className="text-[10px] text-rose-500">missing</span>}
|
|
1564
|
+
<code className="ml-auto text-[10px] text-muted-foreground/70 font-mono truncate max-w-[280px]">{dir.path}</code>
|
|
1565
|
+
{dir.writable && (
|
|
1566
|
+
<Button size="sm" variant="ghost" className="h-6 px-2"
|
|
1567
|
+
onClick={() => setCreatingInDir(dir.path)}>
|
|
1568
|
+
<Plus className="size-3 mr-1" />New
|
|
1569
|
+
</Button>
|
|
1570
|
+
)}
|
|
1571
|
+
{dir.source === 'additional' && (
|
|
1572
|
+
<Button size="sm" variant="ghost" className="h-6 px-1 text-rose-500"
|
|
1573
|
+
onClick={async () => {
|
|
1574
|
+
if (!confirm(`Stop loading skills from ${dir.path}?`)) return;
|
|
1575
|
+
await jsonFetch(`/api/skills/directories?path=${encodeURIComponent(dir.path)}`, { method: 'DELETE' });
|
|
1576
|
+
refresh();
|
|
1577
|
+
}}>
|
|
1578
|
+
<Trash2 className="size-3" />
|
|
1579
|
+
</Button>
|
|
1580
|
+
)}
|
|
1581
|
+
</div>
|
|
1582
|
+
|
|
1583
|
+
{creatingInDir === dir.path && (
|
|
1584
|
+
<NewSkillForm
|
|
1585
|
+
directory={dir.path}
|
|
1586
|
+
onCancel={() => setCreatingInDir(null)}
|
|
1587
|
+
onSaved={() => { setCreatingInDir(null); refresh(); }}
|
|
1588
|
+
/>
|
|
1589
|
+
)}
|
|
1590
|
+
|
|
1591
|
+
{items.length === 0 ? (
|
|
1592
|
+
<p className="text-[11px] text-muted-foreground italic px-1">
|
|
1593
|
+
{dir.writable ? 'No skills here yet — drop .md files or click New.' : 'No skills here.'}
|
|
1594
|
+
</p>
|
|
1595
|
+
) : (
|
|
1596
|
+
<div className="divide-y divide-border/30">
|
|
1597
|
+
{items.map((s) => (
|
|
1598
|
+
<button
|
|
1599
|
+
key={s.id}
|
|
1600
|
+
type="button"
|
|
1601
|
+
onClick={() => setEditing(s)}
|
|
1602
|
+
className="w-full flex items-start gap-2 px-1 py-2 text-left hover:bg-accent/40 rounded transition-colors"
|
|
1603
|
+
>
|
|
1604
|
+
<FileText className="size-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
|
1605
|
+
<div className="flex-1 min-w-0">
|
|
1606
|
+
<div className="flex items-center gap-2">
|
|
1607
|
+
<span className="text-xs font-medium truncate">{s.name}</span>
|
|
1608
|
+
<code className="text-[10px] text-muted-foreground font-mono truncate">{s.fileName}</code>
|
|
1609
|
+
{s.alwaysApply && <span className="text-[10px] text-amber-500 uppercase tracking-wide">always</span>}
|
|
1610
|
+
{s.globs.length > 0 && (
|
|
1611
|
+
<span className="text-[10px] text-muted-foreground">{s.globs.join(', ')}</span>
|
|
1612
|
+
)}
|
|
1613
|
+
</div>
|
|
1614
|
+
{s.description && (
|
|
1615
|
+
<p className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">{s.description}</p>
|
|
1616
|
+
)}
|
|
1617
|
+
</div>
|
|
1618
|
+
<Pencil className="size-3 text-muted-foreground/60 mt-1" />
|
|
1619
|
+
</button>
|
|
1620
|
+
))}
|
|
1621
|
+
</div>
|
|
1622
|
+
)}
|
|
1623
|
+
</div>
|
|
1624
|
+
))}
|
|
1625
|
+
</div>
|
|
1626
|
+
|
|
1627
|
+
{editing && (
|
|
1628
|
+
<SkillEditorModal
|
|
1629
|
+
skill={editing}
|
|
1630
|
+
onClose={() => setEditing(null)}
|
|
1631
|
+
onSaved={() => { setEditing(null); refresh(); }}
|
|
1632
|
+
/>
|
|
1633
|
+
)}
|
|
1634
|
+
</section>
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function AddSkillDirectoryForm({ onCancel, onSaved }: { onCancel: () => void; onSaved: () => void }) {
|
|
1639
|
+
const [path, setPath] = useState('');
|
|
1640
|
+
const [saving, setSaving] = useState(false);
|
|
1641
|
+
const [error, setError] = useState<string | null>(null);
|
|
1642
|
+
// Browsers can't expose absolute filesystem paths even when the user
|
|
1643
|
+
// picks a directory. We let them pick a folder so we can suggest the
|
|
1644
|
+
// folder NAME, but they still have to paste/confirm the absolute path
|
|
1645
|
+
// (or a path relative to the working directory) so the orchestrator
|
|
1646
|
+
// can read files server-side.
|
|
1647
|
+
const folderInputRef = useRef<HTMLInputElement | null>(null);
|
|
1648
|
+
|
|
1649
|
+
const onPickFolder = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1650
|
+
const files = e.target.files;
|
|
1651
|
+
if (!files || files.length === 0) return;
|
|
1652
|
+
const first = files[0];
|
|
1653
|
+
// webkitRelativePath is "<folder>/file.md"; first segment = folder.
|
|
1654
|
+
const rel = (first as any).webkitRelativePath || first.name;
|
|
1655
|
+
const folderName = rel.split('/')[0];
|
|
1656
|
+
if (!path) setPath(folderName);
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const save = async () => {
|
|
1660
|
+
setError(null);
|
|
1661
|
+
if (!path.trim()) return;
|
|
1662
|
+
setSaving(true);
|
|
1663
|
+
try {
|
|
1664
|
+
const r = await jsonFetch<{ ok?: boolean; error?: string }>('/api/skills/directories', {
|
|
1665
|
+
method: 'POST',
|
|
1666
|
+
body: JSON.stringify({ path: path.trim() }),
|
|
1667
|
+
});
|
|
1668
|
+
if ((r as any).error) { setError((r as any).error); return; }
|
|
1669
|
+
onSaved();
|
|
1670
|
+
} finally { setSaving(false); }
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
return (
|
|
1674
|
+
<div className="rounded-lg border border-border/60 bg-card/40 p-3 mb-3 space-y-2">
|
|
1675
|
+
<Label className="text-xs">Directory path</Label>
|
|
1676
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1677
|
+
Absolute path (recommended) or a path relative to the orchestrator's working directory. All <code>.md</code> / <code>.mdc</code> files inside are picked up automatically.
|
|
1678
|
+
</p>
|
|
1679
|
+
<div className="flex gap-2">
|
|
1680
|
+
<Input
|
|
1681
|
+
value={path}
|
|
1682
|
+
onChange={(e) => setPath(e.target.value)}
|
|
1683
|
+
placeholder="/Users/you/skills or ./my-skills"
|
|
1684
|
+
className="text-xs font-mono"
|
|
1685
|
+
/>
|
|
1686
|
+
<Button
|
|
1687
|
+
size="sm"
|
|
1688
|
+
variant="outline"
|
|
1689
|
+
onClick={() => folderInputRef.current?.click()}
|
|
1690
|
+
title="Pick a folder to suggest its name (browser security: you'll still need to paste the absolute path)"
|
|
1691
|
+
>
|
|
1692
|
+
<FolderOpen className="size-3.5 mr-1" />Browse
|
|
1693
|
+
</Button>
|
|
1694
|
+
<input
|
|
1695
|
+
ref={folderInputRef}
|
|
1696
|
+
type="file"
|
|
1697
|
+
// @ts-expect-error non-standard but widely supported
|
|
1698
|
+
webkitdirectory=""
|
|
1699
|
+
directory=""
|
|
1700
|
+
multiple
|
|
1701
|
+
hidden
|
|
1702
|
+
onChange={onPickFolder}
|
|
1703
|
+
/>
|
|
1704
|
+
</div>
|
|
1705
|
+
{error && <p className="text-[11px] text-rose-500">{error}</p>}
|
|
1706
|
+
<div className="flex gap-2 pt-1">
|
|
1707
|
+
<Button size="sm" onClick={save} disabled={saving || !path.trim()}>
|
|
1708
|
+
{saving ? <Loader2 className="size-3.5 animate-spin" /> : 'Add folder'}
|
|
1709
|
+
</Button>
|
|
1710
|
+
<Button size="sm" variant="ghost" onClick={onCancel}>Cancel</Button>
|
|
1711
|
+
</div>
|
|
1712
|
+
</div>
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function NewSkillForm({ directory, onCancel, onSaved }: { directory: string; onCancel: () => void; onSaved: () => void }) {
|
|
1717
|
+
const [fileName, setFileName] = useState('');
|
|
1718
|
+
const [name, setName] = useState('');
|
|
1719
|
+
const [description, setDescription] = useState('');
|
|
1720
|
+
const [body, setBody] = useState('');
|
|
1721
|
+
const [saving, setSaving] = useState(false);
|
|
1722
|
+
const [error, setError] = useState<string | null>(null);
|
|
1723
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
1724
|
+
|
|
1725
|
+
const buildContent = () => {
|
|
1726
|
+
const fm: string[] = ['---'];
|
|
1727
|
+
if (name) fm.push(`name: ${name}`);
|
|
1728
|
+
if (description) fm.push(`description: ${description.replace(/\n/g, ' ')}`);
|
|
1729
|
+
fm.push('---', '');
|
|
1730
|
+
return fm.join('\n') + (body || `# ${name || 'New skill'}\n\nDescribe what this skill does and when to use it.`);
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
const save = async () => {
|
|
1734
|
+
setError(null);
|
|
1735
|
+
const finalFileName = fileName.trim() || (name ? name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.md' : 'untitled.md');
|
|
1736
|
+
setSaving(true);
|
|
1737
|
+
try {
|
|
1738
|
+
const r = await jsonFetch<{ error?: string }>('/api/skills', {
|
|
1739
|
+
method: 'POST',
|
|
1740
|
+
body: JSON.stringify({ directory, fileName: finalFileName, content: buildContent() }),
|
|
1741
|
+
});
|
|
1742
|
+
if (r.error) { setError(r.error); return; }
|
|
1743
|
+
onSaved();
|
|
1744
|
+
} finally { setSaving(false); }
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
const onPickFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1748
|
+
const files = e.target.files;
|
|
1749
|
+
if (!files || !files[0]) return;
|
|
1750
|
+
const file = files[0];
|
|
1751
|
+
setFileName(file.name);
|
|
1752
|
+
setBody(await file.text());
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
return (
|
|
1756
|
+
<div className="rounded-md border border-border/60 bg-background/50 p-3 mb-2 space-y-2">
|
|
1757
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1758
|
+
<div>
|
|
1759
|
+
<Label className="text-xs">Skill name</Label>
|
|
1760
|
+
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="React Performance" className="text-xs" />
|
|
1761
|
+
</div>
|
|
1762
|
+
<div>
|
|
1763
|
+
<Label className="text-xs">File name</Label>
|
|
1764
|
+
<Input value={fileName} onChange={(e) => setFileName(e.target.value)} placeholder="react-performance.md" className="text-xs font-mono" />
|
|
1765
|
+
</div>
|
|
1766
|
+
</div>
|
|
1767
|
+
<div>
|
|
1768
|
+
<Label className="text-xs">Description</Label>
|
|
1769
|
+
<Input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="When to use this skill (one sentence)." className="text-xs" />
|
|
1770
|
+
</div>
|
|
1771
|
+
<div>
|
|
1772
|
+
<Label className="text-xs">Body (markdown)</Label>
|
|
1773
|
+
<textarea
|
|
1774
|
+
value={body}
|
|
1775
|
+
onChange={(e) => setBody(e.target.value)}
|
|
1776
|
+
rows={6}
|
|
1777
|
+
placeholder="# How to do X Step 1…"
|
|
1778
|
+
className="w-full rounded-md border border-input bg-background px-2 py-1.5 text-xs font-mono"
|
|
1779
|
+
/>
|
|
1780
|
+
</div>
|
|
1781
|
+
{error && <p className="text-[11px] text-rose-500">{error}</p>}
|
|
1782
|
+
<div className="flex gap-2 pt-1">
|
|
1783
|
+
<Button size="sm" onClick={save} disabled={saving}>
|
|
1784
|
+
{saving ? <Loader2 className="size-3.5 animate-spin" /> : <><Save className="size-3.5 mr-1" />Save</>}
|
|
1785
|
+
</Button>
|
|
1786
|
+
<Button size="sm" variant="outline" onClick={() => fileInputRef.current?.click()}>
|
|
1787
|
+
<Upload className="size-3.5 mr-1" />Import .md
|
|
1788
|
+
</Button>
|
|
1789
|
+
<input ref={fileInputRef} type="file" hidden accept=".md,.mdc,.markdown,.txt" onChange={onPickFile} />
|
|
1790
|
+
<Button size="sm" variant="ghost" onClick={onCancel}>Cancel</Button>
|
|
1791
|
+
</div>
|
|
1792
|
+
</div>
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function SkillEditorModal({ skill, onClose, onSaved }: { skill: SkillRow; onClose: () => void; onSaved: () => void }) {
|
|
1797
|
+
const [content, setContent] = useState<string | null>(null);
|
|
1798
|
+
const [writable, setWritable] = useState(false);
|
|
1799
|
+
const [saving, setSaving] = useState(false);
|
|
1800
|
+
const [savedFlash, setSavedFlash] = useState(false);
|
|
1801
|
+
const [original, setOriginal] = useState('');
|
|
1802
|
+
|
|
1803
|
+
useEffect(() => {
|
|
1804
|
+
let cancelled = false;
|
|
1805
|
+
(async () => {
|
|
1806
|
+
const r = await jsonFetch<{ content: string; writable: boolean }>(`/api/skills/${skill.id}`);
|
|
1807
|
+
if (cancelled) return;
|
|
1808
|
+
setContent(r.content);
|
|
1809
|
+
setOriginal(r.content);
|
|
1810
|
+
setWritable(r.writable);
|
|
1811
|
+
})();
|
|
1812
|
+
return () => { cancelled = true; };
|
|
1813
|
+
}, [skill.id]);
|
|
1814
|
+
|
|
1815
|
+
const dirty = content !== null && content !== original;
|
|
1816
|
+
|
|
1817
|
+
const save = async () => {
|
|
1818
|
+
if (content === null || !writable) return;
|
|
1819
|
+
setSaving(true);
|
|
1820
|
+
try {
|
|
1821
|
+
await jsonFetch(`/api/skills/${skill.id}`, { method: 'PUT', body: JSON.stringify({ content }) });
|
|
1822
|
+
setOriginal(content);
|
|
1823
|
+
setSavedFlash(true);
|
|
1824
|
+
setTimeout(() => setSavedFlash(false), 1200);
|
|
1825
|
+
} finally { setSaving(false); }
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
const remove = async () => {
|
|
1829
|
+
if (!confirm(`Delete skill "${skill.name}"? This removes the file from disk.`)) return;
|
|
1830
|
+
await jsonFetch(`/api/skills/${skill.id}`, { method: 'DELETE' });
|
|
1831
|
+
onSaved();
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
return (
|
|
1835
|
+
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4" onClick={onClose}>
|
|
1836
|
+
<div className="bg-card rounded-lg border border-border max-w-3xl w-full max-h-[85vh] flex flex-col shadow-2xl" onClick={(e) => e.stopPropagation()}>
|
|
1837
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-border/60">
|
|
1838
|
+
<div className="min-w-0">
|
|
1839
|
+
<h2 className="text-base font-semibold flex items-center gap-2 truncate">
|
|
1840
|
+
<FileText className="size-4 text-muted-foreground" />
|
|
1841
|
+
{skill.name}
|
|
1842
|
+
{!writable && <span className="text-[10px] uppercase tracking-wide text-muted-foreground">read-only</span>}
|
|
1843
|
+
</h2>
|
|
1844
|
+
<code className="text-[11px] text-muted-foreground font-mono truncate block">{skill.filePath}</code>
|
|
1845
|
+
</div>
|
|
1846
|
+
<Button size="icon" variant="ghost" onClick={onClose}><XIcon className="size-4" /></Button>
|
|
1847
|
+
</div>
|
|
1848
|
+
|
|
1849
|
+
<div className="flex-1 min-h-0 overflow-hidden flex flex-col p-4">
|
|
1850
|
+
{content === null ? (
|
|
1851
|
+
<div className="flex items-center justify-center flex-1"><Loader2 className="size-5 animate-spin text-muted-foreground" /></div>
|
|
1852
|
+
) : (
|
|
1853
|
+
<textarea
|
|
1854
|
+
value={content}
|
|
1855
|
+
onChange={(e) => setContent(e.target.value)}
|
|
1856
|
+
readOnly={!writable}
|
|
1857
|
+
spellCheck={false}
|
|
1858
|
+
className="flex-1 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-mono resize-none focus:outline-none focus:ring-2 focus:ring-primary/40"
|
|
1859
|
+
/>
|
|
1860
|
+
)}
|
|
1861
|
+
</div>
|
|
1862
|
+
|
|
1863
|
+
<div className="flex items-center gap-2 px-5 py-3 border-t border-border/60">
|
|
1864
|
+
{writable && (
|
|
1865
|
+
<>
|
|
1866
|
+
<Button size="sm" onClick={save} disabled={!dirty || saving}>
|
|
1867
|
+
{saving ? <Loader2 className="size-3.5 animate-spin mr-1" /> : <Save className="size-3.5 mr-1" />}
|
|
1868
|
+
Save
|
|
1869
|
+
</Button>
|
|
1870
|
+
{savedFlash && <span className="text-xs text-emerald-500">Saved ✓</span>}
|
|
1871
|
+
<Button size="sm" variant="ghost" onClick={remove} className="text-rose-500 hover:text-rose-600 ml-auto">
|
|
1872
|
+
<Trash2 className="size-3.5 mr-1" />Delete
|
|
1873
|
+
</Button>
|
|
1874
|
+
</>
|
|
1875
|
+
)}
|
|
1876
|
+
{!writable && (
|
|
1877
|
+
<p className="text-[11px] text-muted-foreground">
|
|
1878
|
+
Built-in skills can't be edited from the UI. Copy the contents into a custom folder to override.
|
|
1879
|
+
</p>
|
|
1880
|
+
)}
|
|
1881
|
+
</div>
|
|
1882
|
+
</div>
|
|
1883
|
+
</div>
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1424
1887
|
function StatusPill({ status }: { status: WebhookEventRow['status'] }) {
|
|
1425
1888
|
const colors: Record<WebhookEventRow['status'], string> = {
|
|
1426
1889
|
routed: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30',
|