@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.
Files changed (98) hide show
  1. package/eslint.config.js +23 -0
  2. package/index.html +24 -0
  3. package/package.json +54 -26
  4. package/package.json.bak +89 -0
  5. package/package.json.pub +88 -0
  6. package/src/App.tsx +1173 -0
  7. package/src/components/AuthWidget.tsx +150 -0
  8. package/src/components/AutoField.tsx +206 -0
  9. package/src/components/Backdrop.tsx +93 -0
  10. package/src/components/ChatSidebar.tsx +394 -0
  11. package/src/components/DeleteConfirmDialog.tsx +40 -0
  12. package/src/components/LanguageSwitcher.tsx +186 -0
  13. package/src/components/Markdown.tsx +383 -0
  14. package/src/components/ModelInfoCard.tsx +112 -0
  15. package/src/components/ModelPickerDialog.tsx +470 -0
  16. package/src/components/OAuthLoginModal.tsx +374 -0
  17. package/src/components/OAuthProvidersCard.tsx +287 -0
  18. package/src/components/PlatformsCard.tsx +97 -0
  19. package/src/components/ScheduleBuilder.tsx +273 -0
  20. package/src/components/SidebarFooter.tsx +42 -0
  21. package/src/components/SidebarStatusStrip.tsx +72 -0
  22. package/src/components/SlashPopover.tsx +171 -0
  23. package/src/components/ThemeSwitcher.tsx +243 -0
  24. package/src/components/ToolCall.tsx +228 -0
  25. package/src/components/ToolsetConfigDrawer.tsx +448 -0
  26. package/src/contexts/PageHeaderProvider.tsx +139 -0
  27. package/src/contexts/SystemActions.tsx +120 -0
  28. package/src/contexts/page-header-context.ts +12 -0
  29. package/src/contexts/system-actions-context.ts +18 -0
  30. package/src/contexts/usePageHeader.ts +10 -0
  31. package/src/contexts/useSystemActions.ts +15 -0
  32. package/src/hooks/useModalBehavior.ts +44 -0
  33. package/src/hooks/useSidebarStatus.ts +27 -0
  34. package/src/i18n/af.ts +702 -0
  35. package/src/i18n/context.tsx +123 -0
  36. package/src/i18n/de.ts +701 -0
  37. package/src/i18n/en.ts +708 -0
  38. package/src/i18n/es.ts +701 -0
  39. package/src/i18n/fr.ts +701 -0
  40. package/src/i18n/ga.ts +702 -0
  41. package/src/i18n/hu.ts +702 -0
  42. package/src/i18n/index.ts +2 -0
  43. package/src/i18n/it.ts +701 -0
  44. package/src/i18n/ja.ts +702 -0
  45. package/src/i18n/ko.ts +702 -0
  46. package/src/i18n/pt.ts +702 -0
  47. package/src/i18n/ru.ts +702 -0
  48. package/src/i18n/tr.ts +702 -0
  49. package/src/i18n/types.ts +710 -0
  50. package/src/i18n/uk.ts +702 -0
  51. package/src/i18n/zh-hant.ts +702 -0
  52. package/src/i18n/zh.ts +698 -0
  53. package/src/index.css +274 -0
  54. package/src/lib/api.ts +1585 -0
  55. package/src/lib/dashboard-flags.ts +15 -0
  56. package/src/lib/format.ts +9 -0
  57. package/src/lib/fuzzy.ts +192 -0
  58. package/src/lib/gatewayClient.ts +253 -0
  59. package/src/lib/nested.ts +23 -0
  60. package/src/lib/resolve-page-title.ts +41 -0
  61. package/src/lib/schedule.ts +382 -0
  62. package/src/lib/slashExec.ts +163 -0
  63. package/src/lib/utils.ts +35 -0
  64. package/src/main.tsx +25 -0
  65. package/src/pages/AnalyticsPage.tsx +601 -0
  66. package/src/pages/ChannelsPage.tsx +772 -0
  67. package/src/pages/ChatPage.tsx +889 -0
  68. package/src/pages/ConfigPage.tsx +660 -0
  69. package/src/pages/CronPage.tsx +524 -0
  70. package/src/pages/DocsPage.tsx +69 -0
  71. package/src/pages/EnvPage.tsx +918 -0
  72. package/src/pages/LogsPage.tsx +246 -0
  73. package/src/pages/McpPage.tsx +757 -0
  74. package/src/pages/ModelsPage.tsx +994 -0
  75. package/src/pages/PairingPage.tsx +276 -0
  76. package/src/pages/PluginsPage.tsx +580 -0
  77. package/src/pages/ProfilesPage.tsx +559 -0
  78. package/src/pages/SessionsPage.tsx +936 -0
  79. package/src/pages/SkillsPage.tsx +557 -0
  80. package/src/pages/SystemPage.tsx +1259 -0
  81. package/src/pages/WebhooksPage.tsx +483 -0
  82. package/src/plugins/PluginPage.tsx +64 -0
  83. package/src/plugins/index.ts +6 -0
  84. package/src/plugins/registry.ts +151 -0
  85. package/src/plugins/sdk.d.ts +160 -0
  86. package/src/plugins/slots.ts +199 -0
  87. package/src/plugins/types.ts +37 -0
  88. package/src/plugins/usePlugins.ts +133 -0
  89. package/src/themes/context.tsx +443 -0
  90. package/src/themes/fonts.ts +160 -0
  91. package/src/themes/index.ts +3 -0
  92. package/src/themes/presets.ts +477 -0
  93. package/src/themes/types.ts +187 -0
  94. package/tsconfig.app.json +34 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +26 -0
  97. package/vite.config.ts +124 -0
  98. 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
+ }