@nastechai/agent 0.16.0 → 0.17.0
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/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState, useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Package,
|
|
4
|
+
Search,
|
|
5
|
+
Wrench,
|
|
6
|
+
X,
|
|
7
|
+
Cpu,
|
|
8
|
+
Globe,
|
|
9
|
+
Shield,
|
|
10
|
+
Eye,
|
|
11
|
+
Paintbrush,
|
|
12
|
+
Brain,
|
|
13
|
+
Blocks,
|
|
14
|
+
Code,
|
|
15
|
+
Zap,
|
|
16
|
+
Filter,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { api } from "@/lib/api";
|
|
19
|
+
import type { SkillInfo, ToolsetInfo } from "@/lib/api";
|
|
20
|
+
import { useToast } from "@nastechai/ui/hooks/use-toast";
|
|
21
|
+
import { Toast } from "@nastechai/ui/ui/components/toast";
|
|
22
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@nastechai/ui/ui/components/card";
|
|
23
|
+
import { Badge } from "@nastechai/ui/ui/components/badge";
|
|
24
|
+
import { Button } from "@nastechai/ui/ui/components/button";
|
|
25
|
+
import { ListItem } from "@nastechai/ui/ui/components/list-item";
|
|
26
|
+
import { Spinner } from "@nastechai/ui/ui/components/spinner";
|
|
27
|
+
import { Switch } from "@nastechai/ui/ui/components/switch";
|
|
28
|
+
import { cn } from "@/lib/utils";
|
|
29
|
+
import { Input } from "@nastechai/ui/ui/components/input";
|
|
30
|
+
import { useI18n } from "@/i18n";
|
|
31
|
+
import { usePageHeader } from "@/contexts/usePageHeader";
|
|
32
|
+
import { PluginSlot } from "@/plugins";
|
|
33
|
+
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
/* Types & helpers */
|
|
36
|
+
/* ------------------------------------------------------------------ */
|
|
37
|
+
|
|
38
|
+
const CATEGORY_LABELS: Record<string, string> = {
|
|
39
|
+
mlops: "MLOps",
|
|
40
|
+
"mlops/cloud": "MLOps / Cloud",
|
|
41
|
+
"mlops/evaluation": "MLOps / Evaluation",
|
|
42
|
+
"mlops/inference": "MLOps / Inference",
|
|
43
|
+
"mlops/models": "MLOps / Models",
|
|
44
|
+
"mlops/training": "MLOps / Training",
|
|
45
|
+
"mlops/vector-databases": "MLOps / Vector DBs",
|
|
46
|
+
mcp: "MCP",
|
|
47
|
+
"red-teaming": "Red Teaming",
|
|
48
|
+
ocr: "OCR",
|
|
49
|
+
p5js: "p5.js",
|
|
50
|
+
ai: "AI",
|
|
51
|
+
ux: "UX",
|
|
52
|
+
ui: "UI",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function prettyCategory(
|
|
56
|
+
raw: string | null | undefined,
|
|
57
|
+
generalLabel: string,
|
|
58
|
+
): string {
|
|
59
|
+
if (!raw) return generalLabel;
|
|
60
|
+
if (CATEGORY_LABELS[raw]) return CATEGORY_LABELS[raw];
|
|
61
|
+
return raw
|
|
62
|
+
.split(/[-_/]/)
|
|
63
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
64
|
+
.join(" ");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const TOOLSET_ICONS: Record<
|
|
68
|
+
string,
|
|
69
|
+
React.ComponentType<{ className?: string }>
|
|
70
|
+
> = {
|
|
71
|
+
computer: Cpu,
|
|
72
|
+
web: Globe,
|
|
73
|
+
security: Shield,
|
|
74
|
+
vision: Eye,
|
|
75
|
+
design: Paintbrush,
|
|
76
|
+
ai: Brain,
|
|
77
|
+
integration: Blocks,
|
|
78
|
+
code: Code,
|
|
79
|
+
automation: Zap,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function toolsetIcon(
|
|
83
|
+
name: string,
|
|
84
|
+
): React.ComponentType<{ className?: string }> {
|
|
85
|
+
const lower = name.toLowerCase();
|
|
86
|
+
for (const [key, icon] of Object.entries(TOOLSET_ICONS)) {
|
|
87
|
+
if (lower.includes(key)) return icon;
|
|
88
|
+
}
|
|
89
|
+
return Wrench;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ------------------------------------------------------------------ */
|
|
93
|
+
/* Component */
|
|
94
|
+
/* ------------------------------------------------------------------ */
|
|
95
|
+
|
|
96
|
+
export default function SkillsPage() {
|
|
97
|
+
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
98
|
+
const [toolsets, setToolsets] = useState<ToolsetInfo[]>([]);
|
|
99
|
+
const [loading, setLoading] = useState(true);
|
|
100
|
+
const [search, setSearch] = useState("");
|
|
101
|
+
const [view, setView] = useState<"skills" | "toolsets">("skills");
|
|
102
|
+
const [activeCategory, setActiveCategory] = useState<string | null>(null);
|
|
103
|
+
const [togglingSkills, setTogglingSkills] = useState<Set<string>>(new Set());
|
|
104
|
+
const { toast, showToast } = useToast();
|
|
105
|
+
const { t } = useI18n();
|
|
106
|
+
const { setAfterTitle, setEnd } = usePageHeader();
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
Promise.all([api.getSkills(), api.getToolsets()])
|
|
110
|
+
.then(([s, tsets]) => {
|
|
111
|
+
setSkills(s);
|
|
112
|
+
setToolsets(tsets);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => showToast(t.common.loading, "error"))
|
|
115
|
+
.finally(() => setLoading(false));
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
/* ---- Toggle skill ---- */
|
|
119
|
+
const handleToggleSkill = async (skill: SkillInfo) => {
|
|
120
|
+
setTogglingSkills((prev) => new Set(prev).add(skill.name));
|
|
121
|
+
try {
|
|
122
|
+
await api.toggleSkill(skill.name, !skill.enabled);
|
|
123
|
+
setSkills((prev) =>
|
|
124
|
+
prev.map((s) =>
|
|
125
|
+
s.name === skill.name ? { ...s, enabled: !s.enabled } : s,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
showToast(
|
|
129
|
+
`${skill.name} ${skill.enabled ? t.common.disabled : t.common.enabled}`,
|
|
130
|
+
"success",
|
|
131
|
+
);
|
|
132
|
+
} catch {
|
|
133
|
+
showToast(`${t.common.failedToToggle} ${skill.name}`, "error");
|
|
134
|
+
} finally {
|
|
135
|
+
setTogglingSkills((prev) => {
|
|
136
|
+
const next = new Set(prev);
|
|
137
|
+
next.delete(skill.name);
|
|
138
|
+
return next;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/* ---- Derived data ---- */
|
|
144
|
+
const lowerSearch = search.toLowerCase();
|
|
145
|
+
const isSearching = search.trim().length > 0;
|
|
146
|
+
|
|
147
|
+
const searchMatchedSkills = useMemo(() => {
|
|
148
|
+
if (!isSearching) return [];
|
|
149
|
+
return skills.filter(
|
|
150
|
+
(s) =>
|
|
151
|
+
s.name.toLowerCase().includes(lowerSearch) ||
|
|
152
|
+
s.description.toLowerCase().includes(lowerSearch) ||
|
|
153
|
+
(s.category ?? "").toLowerCase().includes(lowerSearch),
|
|
154
|
+
);
|
|
155
|
+
}, [skills, isSearching, lowerSearch]);
|
|
156
|
+
|
|
157
|
+
const activeSkills = useMemo(() => {
|
|
158
|
+
if (isSearching) return [];
|
|
159
|
+
if (!activeCategory)
|
|
160
|
+
return [...skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
161
|
+
return skills
|
|
162
|
+
.filter((s) =>
|
|
163
|
+
activeCategory === "__none__"
|
|
164
|
+
? !s.category
|
|
165
|
+
: s.category === activeCategory,
|
|
166
|
+
)
|
|
167
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
168
|
+
}, [skills, activeCategory, isSearching]);
|
|
169
|
+
|
|
170
|
+
const allCategories = useMemo(() => {
|
|
171
|
+
const cats = new Map<string, number>();
|
|
172
|
+
for (const s of skills) {
|
|
173
|
+
const key = s.category || "__none__";
|
|
174
|
+
cats.set(key, (cats.get(key) || 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
return [...cats.entries()]
|
|
177
|
+
.sort((a, b) => {
|
|
178
|
+
if (a[0] === "__none__") return -1;
|
|
179
|
+
if (b[0] === "__none__") return 1;
|
|
180
|
+
return a[0].localeCompare(b[0]);
|
|
181
|
+
})
|
|
182
|
+
.map(([key, count]) => ({
|
|
183
|
+
key,
|
|
184
|
+
name: prettyCategory(key === "__none__" ? null : key, t.common.general),
|
|
185
|
+
count,
|
|
186
|
+
}));
|
|
187
|
+
}, [skills, t]);
|
|
188
|
+
|
|
189
|
+
const enabledCount = skills.filter((s) => s.enabled).length;
|
|
190
|
+
|
|
191
|
+
useLayoutEffect(() => {
|
|
192
|
+
if (loading) {
|
|
193
|
+
setAfterTitle(null);
|
|
194
|
+
setEnd(null);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
setAfterTitle(
|
|
198
|
+
<span className="whitespace-nowrap text-xs text-muted-foreground">
|
|
199
|
+
{t.skills.enabledOf
|
|
200
|
+
.replace("{enabled}", String(enabledCount))
|
|
201
|
+
.replace("{total}", String(skills.length))}
|
|
202
|
+
</span>,
|
|
203
|
+
);
|
|
204
|
+
setEnd(
|
|
205
|
+
<div className="relative w-full min-w-0 sm:max-w-xs">
|
|
206
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
207
|
+
<Input
|
|
208
|
+
className="h-8 rounded-none pl-8 pr-7 text-xs"
|
|
209
|
+
placeholder={t.common.search}
|
|
210
|
+
value={search}
|
|
211
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
212
|
+
/>
|
|
213
|
+
{search && (
|
|
214
|
+
<Button
|
|
215
|
+
ghost
|
|
216
|
+
size="xs"
|
|
217
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
218
|
+
onClick={() => setSearch("")}
|
|
219
|
+
aria-label={t.common.clear}
|
|
220
|
+
>
|
|
221
|
+
<X />
|
|
222
|
+
</Button>
|
|
223
|
+
)}
|
|
224
|
+
</div>,
|
|
225
|
+
);
|
|
226
|
+
return () => {
|
|
227
|
+
setAfterTitle(null);
|
|
228
|
+
setEnd(null);
|
|
229
|
+
};
|
|
230
|
+
}, [enabledCount, loading, search, setAfterTitle, setEnd, skills.length, t]);
|
|
231
|
+
|
|
232
|
+
const filteredToolsets = useMemo(() => {
|
|
233
|
+
return toolsets.filter(
|
|
234
|
+
(ts) =>
|
|
235
|
+
!search ||
|
|
236
|
+
ts.name.toLowerCase().includes(lowerSearch) ||
|
|
237
|
+
ts.label.toLowerCase().includes(lowerSearch) ||
|
|
238
|
+
ts.description.toLowerCase().includes(lowerSearch),
|
|
239
|
+
);
|
|
240
|
+
}, [toolsets, search, lowerSearch]);
|
|
241
|
+
|
|
242
|
+
/* ---- Loading ---- */
|
|
243
|
+
if (loading) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex items-center justify-center py-24">
|
|
246
|
+
<Spinner className="text-2xl text-primary" />
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="flex flex-col gap-4">
|
|
253
|
+
<PluginSlot name="skills:top" />
|
|
254
|
+
<Toast toast={toast} />
|
|
255
|
+
|
|
256
|
+
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
|
257
|
+
<aside aria-label={t.skills.title} className="sm:w-56 sm:shrink-0">
|
|
258
|
+
<div className="sm:sticky sm:top-0">
|
|
259
|
+
<div className="flex flex-col rounded-none border border-border bg-muted/20">
|
|
260
|
+
<div className="hidden sm:flex items-center gap-2 px-3 py-2 border-b border-border">
|
|
261
|
+
<Filter className="h-3 w-3 text-text-tertiary" />
|
|
262
|
+
<span className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
|
|
263
|
+
{t.skills.filters}
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="flex sm:flex-col gap-1 overflow-x-auto sm:overflow-x-visible scrollbar-none p-2">
|
|
268
|
+
<PanelItem
|
|
269
|
+
icon={Package}
|
|
270
|
+
label={`${t.skills.all} (${skills.length})`}
|
|
271
|
+
active={view === "skills" && !isSearching}
|
|
272
|
+
onClick={() => {
|
|
273
|
+
setView("skills");
|
|
274
|
+
setActiveCategory(null);
|
|
275
|
+
setSearch("");
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
<PanelItem
|
|
279
|
+
icon={Wrench}
|
|
280
|
+
label={`${t.skills.toolsets} (${toolsets.length})`}
|
|
281
|
+
active={view === "toolsets"}
|
|
282
|
+
onClick={() => {
|
|
283
|
+
setView("toolsets");
|
|
284
|
+
setSearch("");
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{view === "skills" &&
|
|
290
|
+
!isSearching &&
|
|
291
|
+
allCategories.length > 0 && (
|
|
292
|
+
<div className="hidden sm:flex flex-col border-t border-border">
|
|
293
|
+
<div className="px-3 pt-2 pb-1 font-mondwest text-display text-xs tracking-[0.12em] text-text-tertiary">
|
|
294
|
+
{t.skills.categories}
|
|
295
|
+
</div>
|
|
296
|
+
<div className="flex flex-col p-2 pt-1 gap-px max-h-[calc(100vh-340px)] overflow-y-auto">
|
|
297
|
+
{allCategories.map(({ key, name, count }) => {
|
|
298
|
+
const isActive = activeCategory === key;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<ListItem
|
|
302
|
+
key={key}
|
|
303
|
+
active={isActive}
|
|
304
|
+
onClick={() =>
|
|
305
|
+
setActiveCategory(isActive ? null : key)
|
|
306
|
+
}
|
|
307
|
+
className="rounded-none px-2 py-1 text-xs"
|
|
308
|
+
>
|
|
309
|
+
<span className="flex-1 truncate">{name}</span>
|
|
310
|
+
<span
|
|
311
|
+
className={`text-xs tabular-nums ${
|
|
312
|
+
isActive
|
|
313
|
+
? "text-text-secondary"
|
|
314
|
+
: "text-text-tertiary"
|
|
315
|
+
}`}
|
|
316
|
+
>
|
|
317
|
+
{count}
|
|
318
|
+
</span>
|
|
319
|
+
</ListItem>
|
|
320
|
+
);
|
|
321
|
+
})}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</aside>
|
|
328
|
+
|
|
329
|
+
<div className="flex-1 min-w-0">
|
|
330
|
+
{isSearching ? (
|
|
331
|
+
<Card className="rounded-none">
|
|
332
|
+
<CardHeader className="py-3 px-4">
|
|
333
|
+
<div className="flex items-center justify-between">
|
|
334
|
+
<CardTitle className="text-sm flex items-center gap-2">
|
|
335
|
+
<Search className="h-4 w-4" />
|
|
336
|
+
{t.skills.title}
|
|
337
|
+
</CardTitle>
|
|
338
|
+
<Badge tone="secondary" className="text-xs">
|
|
339
|
+
{t.skills.resultCount
|
|
340
|
+
.replace("{count}", String(searchMatchedSkills.length))
|
|
341
|
+
.replace(
|
|
342
|
+
"{s}",
|
|
343
|
+
searchMatchedSkills.length !== 1 ? "s" : "",
|
|
344
|
+
)}
|
|
345
|
+
</Badge>
|
|
346
|
+
</div>
|
|
347
|
+
</CardHeader>
|
|
348
|
+
<CardContent className="px-4 pb-4">
|
|
349
|
+
{searchMatchedSkills.length === 0 ? (
|
|
350
|
+
<p className="text-sm text-muted-foreground text-center py-8">
|
|
351
|
+
{t.skills.noSkillsMatch}
|
|
352
|
+
</p>
|
|
353
|
+
) : (
|
|
354
|
+
<div className="grid gap-1">
|
|
355
|
+
{searchMatchedSkills.map((skill) => (
|
|
356
|
+
<SkillRow
|
|
357
|
+
key={skill.name}
|
|
358
|
+
skill={skill}
|
|
359
|
+
toggling={togglingSkills.has(skill.name)}
|
|
360
|
+
onToggle={() => handleToggleSkill(skill)}
|
|
361
|
+
noDescriptionLabel={t.skills.noDescription}
|
|
362
|
+
/>
|
|
363
|
+
))}
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</CardContent>
|
|
367
|
+
</Card>
|
|
368
|
+
) : view === "skills" ? (
|
|
369
|
+
/* Skills list */
|
|
370
|
+
<Card className="rounded-none">
|
|
371
|
+
<CardHeader className="py-3 px-4">
|
|
372
|
+
<div className="flex items-center justify-between">
|
|
373
|
+
<CardTitle className="text-sm flex items-center gap-2">
|
|
374
|
+
<Package className="h-4 w-4" />
|
|
375
|
+
{activeCategory
|
|
376
|
+
? prettyCategory(
|
|
377
|
+
activeCategory === "__none__" ? null : activeCategory,
|
|
378
|
+
t.common.general,
|
|
379
|
+
)
|
|
380
|
+
: t.skills.all}
|
|
381
|
+
</CardTitle>
|
|
382
|
+
<Badge tone="secondary" className="text-xs">
|
|
383
|
+
{t.skills.skillCount
|
|
384
|
+
.replace("{count}", String(activeSkills.length))
|
|
385
|
+
.replace("{s}", activeSkills.length !== 1 ? "s" : "")}
|
|
386
|
+
</Badge>
|
|
387
|
+
</div>
|
|
388
|
+
</CardHeader>
|
|
389
|
+
<CardContent className="px-4 pb-4">
|
|
390
|
+
{activeSkills.length === 0 ? (
|
|
391
|
+
<p className="text-sm text-muted-foreground text-center py-8">
|
|
392
|
+
{skills.length === 0
|
|
393
|
+
? t.skills.noSkills
|
|
394
|
+
: t.skills.noSkillsMatch}
|
|
395
|
+
</p>
|
|
396
|
+
) : (
|
|
397
|
+
<div className="grid gap-1">
|
|
398
|
+
{activeSkills.map((skill) => (
|
|
399
|
+
<SkillRow
|
|
400
|
+
key={skill.name}
|
|
401
|
+
skill={skill}
|
|
402
|
+
toggling={togglingSkills.has(skill.name)}
|
|
403
|
+
onToggle={() => handleToggleSkill(skill)}
|
|
404
|
+
noDescriptionLabel={t.skills.noDescription}
|
|
405
|
+
/>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</CardContent>
|
|
410
|
+
</Card>
|
|
411
|
+
) : (
|
|
412
|
+
/* Toolsets grid */
|
|
413
|
+
<>
|
|
414
|
+
{filteredToolsets.length === 0 ? (
|
|
415
|
+
<Card className="rounded-none">
|
|
416
|
+
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
|
417
|
+
{t.skills.noToolsetsMatch}
|
|
418
|
+
</CardContent>
|
|
419
|
+
</Card>
|
|
420
|
+
) : (
|
|
421
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
422
|
+
{filteredToolsets.map((ts) => {
|
|
423
|
+
const TsIcon = toolsetIcon(ts.name);
|
|
424
|
+
const labelText =
|
|
425
|
+
ts.label.replace(/^[\p{Emoji}\s]+/u, "").trim() ||
|
|
426
|
+
ts.name;
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<Card key={ts.name} className="relative rounded-none">
|
|
430
|
+
<CardContent className="py-4">
|
|
431
|
+
<div className="flex items-start gap-3">
|
|
432
|
+
<TsIcon className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
|
433
|
+
<div className="flex-1 min-w-0">
|
|
434
|
+
<div className="flex items-center gap-2 mb-1">
|
|
435
|
+
<span className="font-medium text-sm">
|
|
436
|
+
{labelText}
|
|
437
|
+
</span>
|
|
438
|
+
<Badge
|
|
439
|
+
tone={ts.enabled ? "success" : "outline"}
|
|
440
|
+
className="text-xs"
|
|
441
|
+
>
|
|
442
|
+
{ts.enabled
|
|
443
|
+
? t.common.active
|
|
444
|
+
: t.common.inactive}
|
|
445
|
+
</Badge>
|
|
446
|
+
</div>
|
|
447
|
+
<p className="text-xs text-text-secondary mb-2">
|
|
448
|
+
{ts.description}
|
|
449
|
+
</p>
|
|
450
|
+
{ts.enabled && !ts.configured && (
|
|
451
|
+
<p className="text-xs text-amber-300 mb-2">
|
|
452
|
+
{t.skills.setupNeeded}
|
|
453
|
+
</p>
|
|
454
|
+
)}
|
|
455
|
+
{ts.tools.length > 0 && (
|
|
456
|
+
<div className="flex flex-wrap gap-1">
|
|
457
|
+
{ts.tools.map((tool) => (
|
|
458
|
+
<Badge
|
|
459
|
+
key={tool}
|
|
460
|
+
tone="secondary"
|
|
461
|
+
className="text-xs font-mono"
|
|
462
|
+
>
|
|
463
|
+
{tool}
|
|
464
|
+
</Badge>
|
|
465
|
+
))}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
{ts.tools.length === 0 && (
|
|
469
|
+
<span className="text-xs text-text-tertiary">
|
|
470
|
+
{ts.enabled
|
|
471
|
+
? t.skills.toolsetLabel.replace(
|
|
472
|
+
"{name}",
|
|
473
|
+
ts.name,
|
|
474
|
+
)
|
|
475
|
+
: t.skills.disabledForCli}
|
|
476
|
+
</span>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</CardContent>
|
|
481
|
+
</Card>
|
|
482
|
+
);
|
|
483
|
+
})}
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
<PluginSlot name="skills:bottom" />
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function SkillRow({
|
|
496
|
+
skill,
|
|
497
|
+
toggling,
|
|
498
|
+
onToggle,
|
|
499
|
+
noDescriptionLabel,
|
|
500
|
+
}: SkillRowProps) {
|
|
501
|
+
return (
|
|
502
|
+
<div className="group flex items-start gap-3 px-3 py-2.5 transition-colors hover:bg-muted/40">
|
|
503
|
+
<div className="pt-0.5 shrink-0">
|
|
504
|
+
<Switch
|
|
505
|
+
checked={skill.enabled}
|
|
506
|
+
onCheckedChange={onToggle}
|
|
507
|
+
disabled={toggling}
|
|
508
|
+
/>
|
|
509
|
+
</div>
|
|
510
|
+
<div className="flex-1 min-w-0">
|
|
511
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
512
|
+
<span
|
|
513
|
+
className={`font-mono-ui text-sm ${
|
|
514
|
+
skill.enabled ? "text-foreground" : "text-muted-foreground"
|
|
515
|
+
}`}
|
|
516
|
+
>
|
|
517
|
+
{skill.name}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">
|
|
521
|
+
{skill.description || noDescriptionLabel}
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function PanelItem({ active, icon: Icon, label, onClick }: PanelItemProps) {
|
|
529
|
+
return (
|
|
530
|
+
<ListItem
|
|
531
|
+
active={active}
|
|
532
|
+
onClick={onClick}
|
|
533
|
+
className={cn(
|
|
534
|
+
"rounded-none whitespace-nowrap px-2.5 py-1.5",
|
|
535
|
+
"font-mondwest text-[0.7rem] tracking-[0.08em] uppercase",
|
|
536
|
+
active && "bg-foreground/90 text-background hover:text-background",
|
|
537
|
+
)}
|
|
538
|
+
>
|
|
539
|
+
<Icon className="h-3.5 w-3.5 shrink-0" />
|
|
540
|
+
<span className="flex-1 truncate">{label}</span>
|
|
541
|
+
</ListItem>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
interface PanelItemProps {
|
|
546
|
+
active: boolean;
|
|
547
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
548
|
+
label: string;
|
|
549
|
+
onClick: () => void;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
interface SkillRowProps {
|
|
553
|
+
noDescriptionLabel: string;
|
|
554
|
+
onToggle: () => void;
|
|
555
|
+
skill: SkillInfo;
|
|
556
|
+
toggling: boolean;
|
|
557
|
+
}
|