@polderlabs/bizar 4.4.13 → 4.5.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/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +96 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/lightrag/SKILL.md +75 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +55 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +12 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
- package/bizar-dash/src/server/memory-store.mjs +38 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +241 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +4 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +118 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +0 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Settings.tsx +6 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +82 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -1209,6 +1209,12 @@ export function SettingsView({ settings: initial, refreshSnapshot }: Props) {
|
|
|
1209
1209
|
Dashboard tokens are generated on first boot and saved to{' '}
|
|
1210
1210
|
<code>~/.config/bizar/dashboard-secret</code> (mode 0600).
|
|
1211
1211
|
</p>
|
|
1212
|
+
{/* v3.6.2 */}
|
|
1213
|
+
<p className="muted" style={{ fontSize: 12, margin: '4px 0' }}>
|
|
1214
|
+
For Tailscale Serve or any reverse-proxy access, paste this token
|
|
1215
|
+
once via the boot screen — it is saved per-origin and works for all
|
|
1216
|
+
subsequent visits.
|
|
1217
|
+
</p>
|
|
1212
1218
|
</div>
|
|
1213
1219
|
<div className="field" data-setting-id="auth.token">
|
|
1214
1220
|
<label className="field-label">Token (this browser)</label>
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
// src/views/Skills.tsx —
|
|
1
|
+
// src/views/Skills.tsx — v4.0.0 skills browser: scans all local SKILL.md sources.
|
|
2
2
|
import { useEffect, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Sparkles,
|
|
5
5
|
Search,
|
|
6
6
|
RefreshCw,
|
|
7
|
-
Download,
|
|
8
|
-
Power,
|
|
9
|
-
PowerOff,
|
|
10
|
-
Code,
|
|
11
|
-
Layers,
|
|
12
|
-
Wrench,
|
|
13
|
-
FlaskConical,
|
|
14
|
-
Palette,
|
|
15
|
-
Brain,
|
|
16
|
-
Map as MapIcon,
|
|
17
|
-
GitBranch,
|
|
18
|
-
Book,
|
|
19
|
-
CheckCircle2,
|
|
20
|
-
XCircle,
|
|
21
|
-
Tag as TagIcon,
|
|
22
7
|
ExternalLink,
|
|
8
|
+
XCircle,
|
|
9
|
+
BookOpen,
|
|
10
|
+
Folder,
|
|
11
|
+
Users,
|
|
12
|
+
Package,
|
|
23
13
|
} from 'lucide-react';
|
|
24
14
|
import { Button } from '../components/Button';
|
|
25
|
-
import { Card
|
|
15
|
+
import { Card } from '../components/Card';
|
|
26
16
|
import { EmptyState } from '../components/EmptyState';
|
|
27
17
|
import { Spinner } from '../components/Spinner';
|
|
28
|
-
import { StatusBadge } from '../components/StatusBadge';
|
|
29
18
|
import { useToast } from '../components/Toast';
|
|
30
19
|
import { useModal } from '../components/Modal';
|
|
31
20
|
import { api } from '../lib/api';
|
|
@@ -41,55 +30,51 @@ type Props = {
|
|
|
41
30
|
};
|
|
42
31
|
|
|
43
32
|
type Skill = {
|
|
44
|
-
id: string;
|
|
45
33
|
name: string;
|
|
46
34
|
description: string;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
source: 'shipped' | 'user' | 'project';
|
|
36
|
+
path: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Tab = { id: 'all' | 'shipped' | 'user' | 'project'; label: string; icon: typeof Sparkles };
|
|
41
|
+
|
|
42
|
+
const TABS: Tab[] = [
|
|
43
|
+
{ id: 'all', label: 'All', icon: Sparkles },
|
|
44
|
+
{ id: 'shipped', label: 'Shipped', icon: Package },
|
|
45
|
+
{ id: 'user', label: 'User', icon: Users },
|
|
46
|
+
{ id: 'project', label: 'Project', icon: Folder },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const SOURCE_LABEL: Record<string, string> = {
|
|
50
|
+
shipped: 'Shipped',
|
|
51
|
+
user: 'User',
|
|
52
|
+
project: 'Project',
|
|
56
53
|
};
|
|
57
54
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tools: Wrench,
|
|
63
|
-
testing: FlaskConical,
|
|
64
|
-
design: Palette,
|
|
65
|
-
reasoning: Brain,
|
|
66
|
-
planning: MapIcon,
|
|
67
|
-
gitops: GitBranch,
|
|
68
|
-
docs: Book,
|
|
55
|
+
const SOURCE_ICON: Record<string, typeof Package> = {
|
|
56
|
+
shipped: Package,
|
|
57
|
+
user: Users,
|
|
58
|
+
project: Folder,
|
|
69
59
|
};
|
|
70
60
|
|
|
71
61
|
export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
72
62
|
const toast = useToast();
|
|
73
63
|
const modal = useModal();
|
|
74
|
-
const [skills, setSkills]
|
|
75
|
-
const [
|
|
76
|
-
const [loading, setLoading]
|
|
77
|
-
const [
|
|
78
|
-
const [searchQ, setSearchQ]
|
|
64
|
+
const [skills, setSkills] = useState<Skill[]>([]);
|
|
65
|
+
const [counts, setCounts] = useState({ shipped: 0, user: 0, project: 0, all: 0 });
|
|
66
|
+
const [loading, setLoading] = useState(true);
|
|
67
|
+
const [activeTab, setActiveTab] = useState<'all' | 'shipped' | 'user' | 'project'>('all');
|
|
68
|
+
const [searchQ, setSearchQ] = useState('');
|
|
79
69
|
const [searchResults, setSearchResults] = useState<Skill[] | null>(null);
|
|
80
70
|
const [searching, setSearching] = useState(false);
|
|
81
|
-
const [installing, setInstalling] = useState<string | null>(null);
|
|
82
|
-
// v3.3.1 — tracks which category <details> elements are open. Default: first 3.
|
|
83
|
-
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
|
84
|
-
new Set(['all', 'languages', 'frameworks']),
|
|
85
|
-
);
|
|
86
71
|
|
|
87
72
|
const reload = async () => {
|
|
88
73
|
setLoading(true);
|
|
89
74
|
try {
|
|
90
|
-
const d = await api.get<{ skills: Skill[];
|
|
75
|
+
const d = await api.get<{ skills: Skill[]; counts: Record<string, number> }>('/skills');
|
|
91
76
|
setSkills(d.skills || []);
|
|
92
|
-
|
|
77
|
+
setCounts({ shipped: d.counts?.shipped ?? 0, user: d.counts?.user ?? 0, project: d.counts?.project ?? 0, all: (d.skills || []).length });
|
|
93
78
|
} catch (err) {
|
|
94
79
|
toast.error(`Skills load failed: ${(err as Error).message}`);
|
|
95
80
|
} finally {
|
|
@@ -97,11 +82,9 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
97
82
|
}
|
|
98
83
|
};
|
|
99
84
|
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
reload();
|
|
102
|
-
}, []);
|
|
85
|
+
useEffect(() => { reload(); }, []);
|
|
103
86
|
|
|
104
|
-
//
|
|
87
|
+
// Debounced search
|
|
105
88
|
useEffect(() => {
|
|
106
89
|
if (!searchQ.trim()) {
|
|
107
90
|
setSearchResults(null);
|
|
@@ -110,7 +93,9 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
110
93
|
setSearching(true);
|
|
111
94
|
const id = setTimeout(async () => {
|
|
112
95
|
try {
|
|
113
|
-
const d = await api.get<{ results: Skill[] }>(
|
|
96
|
+
const d = await api.get<{ results: Skill[] }>(
|
|
97
|
+
`/skills/search?q=${encodeURIComponent(searchQ.trim())}`,
|
|
98
|
+
);
|
|
114
99
|
setSearchResults(d.results || []);
|
|
115
100
|
} catch (err) {
|
|
116
101
|
toast.error(`Search failed: ${(err as Error).message}`);
|
|
@@ -119,98 +104,89 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
119
104
|
}
|
|
120
105
|
}, 280);
|
|
121
106
|
return () => clearTimeout(id);
|
|
122
|
-
}, [searchQ
|
|
107
|
+
}, [searchQ]);
|
|
123
108
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return out;
|
|
130
|
-
}, [skills, activeCategory]);
|
|
109
|
+
const displayed = useMemo<Skill[]>(() => {
|
|
110
|
+
if (searchResults !== null) return searchResults;
|
|
111
|
+
if (activeTab === 'all') return skills;
|
|
112
|
+
return skills.filter((s) => s.source === activeTab);
|
|
113
|
+
}, [skills, activeTab, searchResults]);
|
|
131
114
|
|
|
132
|
-
const
|
|
133
|
-
if (!confirm(`Install skill "${s.name}"?\n\n${s.installCmd || `skills add ${s.id}`}`)) return;
|
|
134
|
-
setInstalling(s.id);
|
|
115
|
+
const onRefresh = async () => {
|
|
135
116
|
try {
|
|
136
|
-
await api.post('/skills/
|
|
137
|
-
toast.success(
|
|
138
|
-
setSearchResults((cur) => cur?.filter((x) => x.id !== s.id) || cur);
|
|
117
|
+
await api.post('/skills/refresh', {});
|
|
118
|
+
toast.success('Skills refreshed.');
|
|
139
119
|
await reload();
|
|
140
120
|
} catch (err) {
|
|
141
|
-
toast.error(`
|
|
142
|
-
} finally {
|
|
143
|
-
setInstalling(null);
|
|
121
|
+
toast.error(`Refresh failed: ${(err as Error).message}`);
|
|
144
122
|
}
|
|
145
123
|
};
|
|
146
124
|
|
|
147
|
-
const
|
|
125
|
+
const onShowDetail = async (skill: Skill) => {
|
|
148
126
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
{s.path && <span><strong>Path:</strong> <code>{s.path}</code></span>}
|
|
172
|
-
</div>
|
|
173
|
-
{s.tags && s.tags.length > 0 && (
|
|
174
|
-
<div className="skill-detail-tags">
|
|
175
|
-
{s.tags.map((t) => (
|
|
176
|
-
<span key={t} className="tag">{t}</span>
|
|
177
|
-
))}
|
|
127
|
+
const full = await api.get<Skill>(
|
|
128
|
+
`/skills/${encodeURIComponent(skill.source)}/${encodeURIComponent(skill.name)}`,
|
|
129
|
+
);
|
|
130
|
+
modal.open({
|
|
131
|
+
title: full.name,
|
|
132
|
+
width: 640,
|
|
133
|
+
children: (
|
|
134
|
+
<div className="skill-detail">
|
|
135
|
+
<div className="skill-detail-meta">
|
|
136
|
+
<span className="skill-source-badge" data-source={full.source}>
|
|
137
|
+
{SOURCE_LABEL[full.source] || full.source}
|
|
138
|
+
</span>
|
|
139
|
+
<code className="mono text-sm muted">{full.path}</code>
|
|
140
|
+
</div>
|
|
141
|
+
{full.description && (
|
|
142
|
+
<p className="skill-detail-desc">{full.description}</p>
|
|
143
|
+
)}
|
|
144
|
+
{full.body && (
|
|
145
|
+
<div className="skill-detail-body">
|
|
146
|
+
<pre className="skill-body-pre">{full.body}</pre>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
178
149
|
</div>
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
150
|
+
),
|
|
151
|
+
footer: (
|
|
152
|
+
<Button variant="ghost" onClick={() => modal.close()}>Close</Button>
|
|
153
|
+
),
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
// Fallback: show what we already have
|
|
157
|
+
modal.open({
|
|
158
|
+
title: skill.name,
|
|
159
|
+
width: 560,
|
|
160
|
+
children: (
|
|
161
|
+
<div className="skill-detail">
|
|
162
|
+
<div className="skill-detail-meta">
|
|
163
|
+
<span className="skill-source-badge" data-source={skill.source}>
|
|
164
|
+
{SOURCE_LABEL[skill.source] || skill.source}
|
|
165
|
+
</span>
|
|
166
|
+
<code className="mono text-sm muted">{skill.path}</code>
|
|
167
|
+
</div>
|
|
168
|
+
{skill.description && (
|
|
169
|
+
<p className="skill-detail-desc">{skill.description}</p>
|
|
170
|
+
)}
|
|
184
171
|
</div>
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
);
|
|
188
|
-
modal.open({
|
|
189
|
-
title: s.name,
|
|
190
|
-
width: 560,
|
|
191
|
-
children: content,
|
|
192
|
-
footer: (
|
|
193
|
-
<div className="modal-footer-actions">
|
|
172
|
+
),
|
|
173
|
+
footer: (
|
|
194
174
|
<Button variant="ghost" onClick={() => modal.close()}>Close</Button>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
</Button>
|
|
199
|
-
)}
|
|
200
|
-
</div>
|
|
201
|
-
),
|
|
202
|
-
});
|
|
175
|
+
),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
203
178
|
};
|
|
204
179
|
|
|
205
180
|
return (
|
|
206
181
|
<div className="view view-skills">
|
|
182
|
+
{/* ── Header ─────────────────────────────────────────────────────── */}
|
|
207
183
|
<header className="view-header">
|
|
208
184
|
<div className="view-header-text">
|
|
209
185
|
<h2 className="view-title">
|
|
210
186
|
<Sparkles size={18} /> Skills
|
|
211
187
|
</h2>
|
|
212
188
|
<p className="view-subtitle">
|
|
213
|
-
|
|
189
|
+
Browse all available skills — shipped, user, and project-local.
|
|
214
190
|
</p>
|
|
215
191
|
</div>
|
|
216
192
|
<div className="view-actions">
|
|
@@ -219,7 +195,7 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
219
195
|
<input
|
|
220
196
|
className="input"
|
|
221
197
|
type="text"
|
|
222
|
-
placeholder="
|
|
198
|
+
placeholder="Search skills…"
|
|
223
199
|
value={searchQ}
|
|
224
200
|
onChange={(e) => setSearchQ(e.target.value)}
|
|
225
201
|
/>
|
|
@@ -229,19 +205,42 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
229
205
|
className="icon-btn"
|
|
230
206
|
aria-label="Clear search"
|
|
231
207
|
onClick={() => setSearchQ('')}
|
|
232
|
-
title="Clear"
|
|
233
208
|
>
|
|
234
209
|
<XCircle size={12} />
|
|
235
210
|
</button>
|
|
236
211
|
)}
|
|
237
212
|
</div>
|
|
238
|
-
<Button variant="secondary" size="sm" onClick={
|
|
213
|
+
<Button variant="secondary" size="sm" onClick={onRefresh}>
|
|
239
214
|
<RefreshCw size={14} /> Refresh
|
|
240
215
|
</Button>
|
|
241
216
|
</div>
|
|
242
217
|
</header>
|
|
243
218
|
|
|
244
|
-
{/*
|
|
219
|
+
{/* ── Source tabs ────────────────────────────────────────────────── */}
|
|
220
|
+
{!searchQ.trim() && (
|
|
221
|
+
<div className="skills-tabs">
|
|
222
|
+
{TABS.map((tab) => {
|
|
223
|
+
const Icon = tab.icon;
|
|
224
|
+
const count = tab.id === 'all'
|
|
225
|
+
? counts.all
|
|
226
|
+
: counts[tab.id as keyof typeof counts] ?? 0;
|
|
227
|
+
return (
|
|
228
|
+
<button
|
|
229
|
+
key={tab.id}
|
|
230
|
+
type="button"
|
|
231
|
+
className={cn('skills-tab', activeTab === tab.id && 'skills-tab-active')}
|
|
232
|
+
onClick={() => setActiveTab(tab.id)}
|
|
233
|
+
>
|
|
234
|
+
<Icon size={14} />
|
|
235
|
+
<span>{tab.label}</span>
|
|
236
|
+
<span className="skills-tab-count">{count}</span>
|
|
237
|
+
</button>
|
|
238
|
+
);
|
|
239
|
+
})}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* ── Search results ───────────────────────────────────────────── */}
|
|
245
244
|
{searchQ.trim() && (
|
|
246
245
|
<section className="skills-section">
|
|
247
246
|
<h3 className="skills-section-title">
|
|
@@ -262,14 +261,12 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
262
261
|
/>
|
|
263
262
|
) : (
|
|
264
263
|
<div className="skills-grid">
|
|
265
|
-
{(searchResults || []).map((s) => (
|
|
264
|
+
{(searchResults || []).map((s, i) => (
|
|
266
265
|
<SkillCard
|
|
267
|
-
key={`search-${
|
|
266
|
+
key={`search-${i}`}
|
|
268
267
|
skill={s}
|
|
269
|
-
onShow={() =>
|
|
270
|
-
|
|
271
|
-
installing={installing === s.id}
|
|
272
|
-
installed={!!skills.find((x) => x.id === s.id)}
|
|
268
|
+
onShow={() => onShowDetail(s)}
|
|
269
|
+
searchQ={searchQ}
|
|
273
270
|
/>
|
|
274
271
|
))}
|
|
275
272
|
</div>
|
|
@@ -277,155 +274,106 @@ export function Skills({ snapshot, refreshSnapshot }: Props) {
|
|
|
277
274
|
</section>
|
|
278
275
|
)}
|
|
279
276
|
|
|
280
|
-
{/*
|
|
277
|
+
{/* ── Skills grid (tab view) ───────────────────────────────────── */}
|
|
281
278
|
{!searchQ.trim() && (
|
|
282
|
-
|
|
283
|
-
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
279
|
+
<section className="skills-section">
|
|
280
|
+
{loading ? (
|
|
281
|
+
<div className="view-loading"><Spinner size="lg" /></div>
|
|
282
|
+
) : displayed.length === 0 ? (
|
|
283
|
+
<EmptyState
|
|
284
|
+
icon={<BookOpen size={28} />}
|
|
285
|
+
title="No skills here"
|
|
286
|
+
message={
|
|
287
|
+
activeTab === 'all'
|
|
288
|
+
? 'No skills found. Install skills with the skills CLI or add SKILL.md files to a skills directory.'
|
|
289
|
+
: `No skills in the ${SOURCE_LABEL[activeTab]} tab.`
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
) : (
|
|
293
|
+
<div className="skills-grid">
|
|
294
|
+
{displayed.map((s, i) => (
|
|
295
|
+
<SkillCard
|
|
296
|
+
key={`${s.source}-${s.name}-${i}`}
|
|
297
|
+
skill={s}
|
|
298
|
+
onShow={() => onShowDetail(s)}
|
|
299
|
+
/>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</section>
|
|
304
|
+
)}
|
|
302
305
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const catSkills = filteredInstalled.filter((s) => (s.category || 'tools') === cat.id);
|
|
314
|
-
if (catSkills.length === 0) return null;
|
|
315
|
-
const isOpen = expandedCategories.has(cat.id);
|
|
316
|
-
return (
|
|
317
|
-
<details
|
|
318
|
-
key={cat.id}
|
|
319
|
-
className="skills-category"
|
|
320
|
-
open={isOpen}
|
|
321
|
-
onToggle={(e) => {
|
|
322
|
-
const tgt = e.currentTarget as HTMLDetailsElement;
|
|
323
|
-
setExpandedCategories((prev) => {
|
|
324
|
-
const next = new Set(prev);
|
|
325
|
-
if (tgt.open) next.add(cat.id);
|
|
326
|
-
else next.delete(cat.id);
|
|
327
|
-
return next;
|
|
328
|
-
});
|
|
329
|
-
}}
|
|
330
|
-
>
|
|
331
|
-
<summary className="skills-category-summary">
|
|
332
|
-
<span className="skills-category-name">
|
|
333
|
-
{ICONS[cat.id] && (() => { const Icon = ICONS[cat.id]; return <Icon size={14} />; })()}
|
|
334
|
-
{cat.label}
|
|
335
|
-
</span>
|
|
336
|
-
<span className="skill-category-count">{catSkills.length}</span>
|
|
337
|
-
</summary>
|
|
338
|
-
<div className="skills-grid">
|
|
339
|
-
{catSkills.map((s) => (
|
|
340
|
-
<SkillCard
|
|
341
|
-
key={s.id}
|
|
342
|
-
skill={s}
|
|
343
|
-
onShow={() => onShowDetails(s)}
|
|
344
|
-
onToggle={() => onToggle(s)}
|
|
345
|
-
/>
|
|
346
|
-
))}
|
|
347
|
-
</div>
|
|
348
|
-
</details>
|
|
349
|
-
);
|
|
350
|
-
})}
|
|
351
|
-
</div>
|
|
352
|
-
)}
|
|
353
|
-
</section>
|
|
354
|
-
</>
|
|
306
|
+
{/* ── Footer ────────────────────────────────────────────────────── */}
|
|
307
|
+
{!searchQ.trim() && (
|
|
308
|
+
<footer className="view-footer">
|
|
309
|
+
<span className="text-sm muted">
|
|
310
|
+
Skills are discovered from{' '}
|
|
311
|
+
<code>~/.opencode/skills/</code>,{' '}
|
|
312
|
+
<code>~/.agents/skills/</code>,{' '}
|
|
313
|
+
<code>bizar-dash/skills/</code>, and project-local directories.
|
|
314
|
+
</span>
|
|
315
|
+
</footer>
|
|
355
316
|
)}
|
|
356
317
|
</div>
|
|
357
318
|
);
|
|
358
319
|
}
|
|
359
320
|
|
|
321
|
+
// ── SkillCard ─────────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
360
323
|
function SkillCard({
|
|
361
324
|
skill,
|
|
362
325
|
onShow,
|
|
363
|
-
|
|
364
|
-
onToggle,
|
|
365
|
-
installing,
|
|
366
|
-
installed,
|
|
326
|
+
searchQ = '',
|
|
367
327
|
}: {
|
|
368
328
|
skill: Skill;
|
|
369
329
|
onShow: () => void;
|
|
370
|
-
|
|
371
|
-
onToggle?: () => void;
|
|
372
|
-
installing?: boolean;
|
|
373
|
-
installed?: boolean;
|
|
330
|
+
searchQ?: string;
|
|
374
331
|
}) {
|
|
375
|
-
const Icon =
|
|
376
|
-
const
|
|
332
|
+
const Icon = SOURCE_ICON[skill.source] || Package;
|
|
333
|
+
const desc = skill.description
|
|
334
|
+
? skill.description.length > 200
|
|
335
|
+
? skill.description.slice(0, 200) + '…'
|
|
336
|
+
: skill.description
|
|
337
|
+
: 'No description.';
|
|
338
|
+
|
|
377
339
|
return (
|
|
378
|
-
<Card variant="elevated" interactive className=
|
|
340
|
+
<Card variant="elevated" interactive className="skill-card">
|
|
379
341
|
<div className="skill-card-head">
|
|
380
|
-
<div
|
|
342
|
+
<div
|
|
343
|
+
className="skill-card-icon"
|
|
344
|
+
style={{ background: `color-mix(in srgb, var(--accent) 12%, transparent)` }}
|
|
345
|
+
>
|
|
381
346
|
<Icon size={18} />
|
|
382
347
|
</div>
|
|
383
348
|
<div className="skill-card-title-area">
|
|
384
|
-
<div className="skill-card-title">
|
|
385
|
-
|
|
349
|
+
<div className="skill-card-title">
|
|
350
|
+
{highlightMatch(skill.name, searchQ)}
|
|
351
|
+
</div>
|
|
352
|
+
<span className="skill-source-badge" data-source={skill.source}>
|
|
353
|
+
{SOURCE_LABEL[skill.source] || skill.source}
|
|
354
|
+
</span>
|
|
386
355
|
</div>
|
|
387
|
-
{installed ? (
|
|
388
|
-
<StatusBadge kind={enabled ? 'success' : 'neutral'}>
|
|
389
|
-
{enabled ? 'on' : 'off'}
|
|
390
|
-
</StatusBadge>
|
|
391
|
-
) : (
|
|
392
|
-
<StatusBadge kind="accent">new</StatusBadge>
|
|
393
|
-
)}
|
|
394
|
-
</div>
|
|
395
|
-
<p className="skill-card-desc">{skill.description || 'No description.'}</p>
|
|
396
|
-
<div className="skill-card-meta">
|
|
397
|
-
<span className="mono text-sm">v{skill.version}</span>
|
|
398
|
-
{skill.category && <span className="text-sm muted">{skill.category}</span>}
|
|
399
|
-
{skill.tags && skill.tags.length > 0 && skill.tags.slice(0, 3).map((t) => (
|
|
400
|
-
<span key={t} className="skill-card-tag">{t}</span>
|
|
401
|
-
))}
|
|
402
356
|
</div>
|
|
357
|
+
<p className="skill-card-desc">{desc}</p>
|
|
403
358
|
<div className="skill-card-actions">
|
|
404
359
|
<Button variant="ghost" size="sm" onClick={onShow}>
|
|
405
|
-
<ExternalLink size={12} />
|
|
360
|
+
<ExternalLink size={12} /> View
|
|
406
361
|
</Button>
|
|
407
|
-
{installed && onToggle && (
|
|
408
|
-
<Button
|
|
409
|
-
variant={enabled ? 'ghost' : 'secondary'}
|
|
410
|
-
size="sm"
|
|
411
|
-
onClick={onToggle}
|
|
412
|
-
title={enabled ? 'Disable' : 'Enable'}
|
|
413
|
-
>
|
|
414
|
-
{enabled ? <PowerOff size={12} /> : <Power size={12} />}
|
|
415
|
-
{enabled ? 'Disable' : 'Enable'}
|
|
416
|
-
</Button>
|
|
417
|
-
)}
|
|
418
|
-
{!installed && onInstall && (
|
|
419
|
-
<Button
|
|
420
|
-
variant="primary"
|
|
421
|
-
size="sm"
|
|
422
|
-
onClick={onInstall}
|
|
423
|
-
loading={installing}
|
|
424
|
-
>
|
|
425
|
-
<Download size={12} /> Install
|
|
426
|
-
</Button>
|
|
427
|
-
)}
|
|
428
362
|
</div>
|
|
429
363
|
</Card>
|
|
430
364
|
);
|
|
431
365
|
}
|
|
366
|
+
|
|
367
|
+
/** Simple substring highlight — wraps matched term in <mark>. */
|
|
368
|
+
function highlightMatch(text: string, q: string): React.ReactNode {
|
|
369
|
+
if (!q.trim()) return text;
|
|
370
|
+
const idx = text.toLowerCase().indexOf(q.toLowerCase());
|
|
371
|
+
if (idx < 0) return text;
|
|
372
|
+
return (
|
|
373
|
+
<>
|
|
374
|
+
{text.slice(0, idx)}
|
|
375
|
+
<mark className="skill-highlight">{text.slice(idx, idx + q.length)}</mark>
|
|
376
|
+
{text.slice(idx + q.length)}
|
|
377
|
+
</>
|
|
378
|
+
);
|
|
379
|
+
}
|