claudeup 3.7.2 → 3.9.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/package.json +1 -1
- package/src/data/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/data/skill-repos.js +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +101 -1
- package/src/ui/App.js +23 -18
- package/src/ui/App.tsx +27 -23
- package/src/ui/components/TabBar.js +9 -8
- package/src/ui/components/TabBar.tsx +15 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +154 -66
- package/src/ui/screens/PluginsScreen.tsx +280 -97
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +3 -2
- package/src/ui/screens/index.ts +3 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +151 -19
- package/src/ui/state/reducer.ts +167 -19
- package/src/ui/state/types.ts +58 -14
- package/src/utils/clipboard.js +56 -0
- package/src/utils/clipboard.ts +58 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import React, { useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { useApp, useModal } from "../state/AppContext.js";
|
|
3
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
|
+
import {
|
|
8
|
+
fetchAvailableSkills,
|
|
9
|
+
fetchSkillFrontmatter,
|
|
10
|
+
installSkill,
|
|
11
|
+
uninstallSkill,
|
|
12
|
+
} from "../../services/skills-manager.js";
|
|
13
|
+
import { DEFAULT_SKILL_REPOS, RECOMMENDED_SKILLS } from "../../data/skill-repos.js";
|
|
14
|
+
import type { SkillInfo } from "../../types/index.js";
|
|
15
|
+
|
|
16
|
+
interface SkillListItem {
|
|
17
|
+
id: string;
|
|
18
|
+
type: "category" | "skill";
|
|
19
|
+
label: string;
|
|
20
|
+
skill?: SkillInfo;
|
|
21
|
+
categoryKey?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CATEGORY_COLORS: Record<string, string> = {
|
|
25
|
+
recommended: "#2e7d32",
|
|
26
|
+
frontend: "#1565c0",
|
|
27
|
+
design: "#6a1b9a",
|
|
28
|
+
media: "#e65100",
|
|
29
|
+
security: "#b71c1c",
|
|
30
|
+
debugging: "#00838f",
|
|
31
|
+
database: "#4527a0",
|
|
32
|
+
search: "#4e342e",
|
|
33
|
+
general: "#333333",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const RECOMMENDED_NAMES = new Set(RECOMMENDED_SKILLS.map((r) => r.name));
|
|
37
|
+
|
|
38
|
+
export function SkillsScreen() {
|
|
39
|
+
const { state, dispatch } = useApp();
|
|
40
|
+
const { skills: skillsState } = state;
|
|
41
|
+
const modal = useModal();
|
|
42
|
+
const dimensions = useDimensions();
|
|
43
|
+
|
|
44
|
+
const isSearchActive =
|
|
45
|
+
state.isSearching &&
|
|
46
|
+
state.currentRoute.screen === "skills" &&
|
|
47
|
+
!state.modal;
|
|
48
|
+
|
|
49
|
+
// Fetch data
|
|
50
|
+
const fetchData = useCallback(async () => {
|
|
51
|
+
dispatch({ type: "SKILLS_DATA_LOADING" });
|
|
52
|
+
try {
|
|
53
|
+
const skills = await fetchAvailableSkills(
|
|
54
|
+
DEFAULT_SKILL_REPOS,
|
|
55
|
+
state.projectPath,
|
|
56
|
+
);
|
|
57
|
+
dispatch({ type: "SKILLS_DATA_SUCCESS", skills });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
dispatch({
|
|
60
|
+
type: "SKILLS_DATA_ERROR",
|
|
61
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, [dispatch, state.projectPath]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
fetchData();
|
|
68
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
69
|
+
|
|
70
|
+
// Build flat list: recommended first (as their own category), then by repo
|
|
71
|
+
const allItems = useMemo((): SkillListItem[] => {
|
|
72
|
+
if (skillsState.skills.status !== "success") return [];
|
|
73
|
+
|
|
74
|
+
const skills = skillsState.skills.data;
|
|
75
|
+
const query = skillsState.searchQuery.toLowerCase();
|
|
76
|
+
|
|
77
|
+
const filtered = query
|
|
78
|
+
? skills.filter(
|
|
79
|
+
(s) =>
|
|
80
|
+
s.name.toLowerCase().includes(query) ||
|
|
81
|
+
s.source.repo.toLowerCase().includes(query) ||
|
|
82
|
+
s.frontmatter?.description?.toLowerCase().includes(query),
|
|
83
|
+
)
|
|
84
|
+
: skills;
|
|
85
|
+
|
|
86
|
+
const items: SkillListItem[] = [];
|
|
87
|
+
|
|
88
|
+
// Recommended section
|
|
89
|
+
const recommendedSkills = filtered.filter((s) =>
|
|
90
|
+
RECOMMENDED_NAMES.has(
|
|
91
|
+
s.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
92
|
+
) || RECOMMENDED_SKILLS.some((r) => r.skillPath === s.name),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (recommendedSkills.length > 0) {
|
|
96
|
+
items.push({
|
|
97
|
+
id: "cat:recommended",
|
|
98
|
+
type: "category",
|
|
99
|
+
label: "Recommended",
|
|
100
|
+
categoryKey: "recommended",
|
|
101
|
+
});
|
|
102
|
+
for (const skill of recommendedSkills) {
|
|
103
|
+
items.push({
|
|
104
|
+
id: `skill:${skill.id}`,
|
|
105
|
+
type: "skill",
|
|
106
|
+
label: skill.name,
|
|
107
|
+
skill,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Group remaining skills by repo
|
|
113
|
+
const repoMap = new Map<string, SkillInfo[]>();
|
|
114
|
+
for (const skill of filtered) {
|
|
115
|
+
const isRec = recommendedSkills.includes(skill);
|
|
116
|
+
if (isRec) continue;
|
|
117
|
+
const existing = repoMap.get(skill.source.repo) || [];
|
|
118
|
+
existing.push(skill);
|
|
119
|
+
repoMap.set(skill.source.repo, existing);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [repo, repoSkills] of repoMap) {
|
|
123
|
+
items.push({
|
|
124
|
+
id: `cat:${repo}`,
|
|
125
|
+
type: "category",
|
|
126
|
+
label: `${repo} (${repoSkills.length})`,
|
|
127
|
+
categoryKey: repo,
|
|
128
|
+
});
|
|
129
|
+
for (const skill of repoSkills) {
|
|
130
|
+
items.push({
|
|
131
|
+
id: `skill:${skill.id}`,
|
|
132
|
+
type: "skill",
|
|
133
|
+
label: skill.name,
|
|
134
|
+
skill,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return items;
|
|
140
|
+
}, [skillsState.skills, skillsState.searchQuery]);
|
|
141
|
+
|
|
142
|
+
const selectableItems = useMemo(
|
|
143
|
+
() => allItems.filter((item) => item.type === "skill" || item.type === "category"),
|
|
144
|
+
[allItems],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const selectedItem = selectableItems[skillsState.selectedIndex];
|
|
148
|
+
const selectedSkill = selectedItem?.type === "skill" ? selectedItem.skill : undefined;
|
|
149
|
+
|
|
150
|
+
// Lazy-load frontmatter for selected skill
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!selectedSkill || selectedSkill.frontmatter) return;
|
|
153
|
+
|
|
154
|
+
fetchSkillFrontmatter(selectedSkill).then((fm) => {
|
|
155
|
+
dispatch({
|
|
156
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
157
|
+
name: selectedSkill.name,
|
|
158
|
+
updates: { frontmatter: fm },
|
|
159
|
+
});
|
|
160
|
+
}).catch(() => {});
|
|
161
|
+
}, [selectedSkill?.id, dispatch]);
|
|
162
|
+
|
|
163
|
+
// Install handler
|
|
164
|
+
const handleInstall = useCallback(async (scope: "user" | "project") => {
|
|
165
|
+
if (!selectedSkill) return;
|
|
166
|
+
|
|
167
|
+
modal.loading(`Installing ${selectedSkill.name}...`);
|
|
168
|
+
try {
|
|
169
|
+
await installSkill(selectedSkill, scope, state.projectPath);
|
|
170
|
+
modal.hideModal();
|
|
171
|
+
dispatch({
|
|
172
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
173
|
+
name: selectedSkill.name,
|
|
174
|
+
updates: {
|
|
175
|
+
installed: true,
|
|
176
|
+
installedScope: scope,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
await modal.message(
|
|
180
|
+
"Installed",
|
|
181
|
+
`${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`,
|
|
182
|
+
"success",
|
|
183
|
+
);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
modal.hideModal();
|
|
186
|
+
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
187
|
+
}
|
|
188
|
+
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
189
|
+
|
|
190
|
+
// Uninstall handler
|
|
191
|
+
const handleUninstall = useCallback(async () => {
|
|
192
|
+
if (!selectedSkill || !selectedSkill.installed) return;
|
|
193
|
+
|
|
194
|
+
const scope = selectedSkill.installedScope;
|
|
195
|
+
if (!scope) return;
|
|
196
|
+
|
|
197
|
+
const confirmed = await modal.confirm(
|
|
198
|
+
`Uninstall "${selectedSkill.name}"?`,
|
|
199
|
+
`This will remove it from the ${scope} scope.`,
|
|
200
|
+
);
|
|
201
|
+
if (!confirmed) return;
|
|
202
|
+
|
|
203
|
+
modal.loading(`Uninstalling ${selectedSkill.name}...`);
|
|
204
|
+
try {
|
|
205
|
+
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
206
|
+
modal.hideModal();
|
|
207
|
+
dispatch({
|
|
208
|
+
type: "SKILLS_UPDATE_ITEM",
|
|
209
|
+
name: selectedSkill.name,
|
|
210
|
+
updates: {
|
|
211
|
+
installed: false,
|
|
212
|
+
installedScope: null,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
|
|
216
|
+
} catch (error) {
|
|
217
|
+
modal.hideModal();
|
|
218
|
+
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
219
|
+
}
|
|
220
|
+
}, [selectedSkill, state.projectPath, dispatch, modal]);
|
|
221
|
+
|
|
222
|
+
// Keyboard handling
|
|
223
|
+
useKeyboard((event) => {
|
|
224
|
+
if (state.modal) return;
|
|
225
|
+
|
|
226
|
+
if (event.name === "up" || event.name === "k") {
|
|
227
|
+
if (state.isSearching) return;
|
|
228
|
+
const newIndex = Math.max(0, skillsState.selectedIndex - 1);
|
|
229
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
230
|
+
} else if (event.name === "down" || event.name === "j") {
|
|
231
|
+
if (state.isSearching) return;
|
|
232
|
+
const newIndex = Math.min(
|
|
233
|
+
Math.max(0, selectableItems.length - 1),
|
|
234
|
+
skillsState.selectedIndex + 1,
|
|
235
|
+
);
|
|
236
|
+
dispatch({ type: "SKILLS_SELECT", index: newIndex });
|
|
237
|
+
} else if (event.name === "u") {
|
|
238
|
+
if (state.isSearching) return;
|
|
239
|
+
if (selectedSkill) {
|
|
240
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "user") {
|
|
241
|
+
handleUninstall();
|
|
242
|
+
} else {
|
|
243
|
+
handleInstall("user");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (event.name === "p") {
|
|
247
|
+
if (state.isSearching) return;
|
|
248
|
+
if (selectedSkill) {
|
|
249
|
+
if (selectedSkill.installed && selectedSkill.installedScope === "project") {
|
|
250
|
+
handleUninstall();
|
|
251
|
+
} else {
|
|
252
|
+
handleInstall("project");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (event.name === "return" || event.name === "enter") {
|
|
256
|
+
if (state.isSearching) return;
|
|
257
|
+
if (selectedSkill && !selectedSkill.installed) {
|
|
258
|
+
handleInstall("project");
|
|
259
|
+
}
|
|
260
|
+
} else if (event.name === "d") {
|
|
261
|
+
if (state.isSearching) return;
|
|
262
|
+
if (selectedSkill?.installed) {
|
|
263
|
+
handleUninstall();
|
|
264
|
+
}
|
|
265
|
+
} else if (event.name === "r") {
|
|
266
|
+
if (state.isSearching) return;
|
|
267
|
+
fetchData();
|
|
268
|
+
} else if (event.name === "escape") {
|
|
269
|
+
if (skillsState.searchQuery) {
|
|
270
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: "" });
|
|
271
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
272
|
+
}
|
|
273
|
+
} else if (event.name === "backspace") {
|
|
274
|
+
if (skillsState.searchQuery) {
|
|
275
|
+
const newQuery = skillsState.searchQuery.slice(0, -1);
|
|
276
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
277
|
+
if (!newQuery) {
|
|
278
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (
|
|
282
|
+
event.name &&
|
|
283
|
+
event.name.length === 1 &&
|
|
284
|
+
!/[0-9]/.test(event.name) &&
|
|
285
|
+
!event.ctrl &&
|
|
286
|
+
!event.meta
|
|
287
|
+
) {
|
|
288
|
+
// Inline search: type to filter
|
|
289
|
+
const newQuery = skillsState.searchQuery + event.name;
|
|
290
|
+
dispatch({ type: "SKILLS_SET_SEARCH", query: newQuery });
|
|
291
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const renderListItem = (
|
|
296
|
+
item: SkillListItem,
|
|
297
|
+
_idx: number,
|
|
298
|
+
isSelected: boolean,
|
|
299
|
+
) => {
|
|
300
|
+
if (item.type === "category") {
|
|
301
|
+
const catKey = item.categoryKey || "general";
|
|
302
|
+
const isRec = catKey === "recommended";
|
|
303
|
+
const bgColor = CATEGORY_COLORS[catKey] || CATEGORY_COLORS.general;
|
|
304
|
+
const star = isRec ? "★ " : "";
|
|
305
|
+
|
|
306
|
+
if (isSelected) {
|
|
307
|
+
return (
|
|
308
|
+
<text bg="magenta" fg="white">
|
|
309
|
+
<strong> {star}{item.label} </strong>
|
|
310
|
+
</text>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<text bg={bgColor} fg="white">
|
|
315
|
+
<strong> {star}{item.label} </strong>
|
|
316
|
+
</text>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (item.type === "skill" && item.skill) {
|
|
321
|
+
const skill = item.skill;
|
|
322
|
+
const indicator = skill.installed ? "●" : "○";
|
|
323
|
+
const indicatorColor = skill.installed ? "cyan" : "gray";
|
|
324
|
+
const scopeLabel = skill.installedScope
|
|
325
|
+
? skill.installedScope === "user"
|
|
326
|
+
? "[u]"
|
|
327
|
+
: "[p]"
|
|
328
|
+
: " ";
|
|
329
|
+
const updateBadge = skill.hasUpdate ? "[UPDT] " : "";
|
|
330
|
+
|
|
331
|
+
if (isSelected) {
|
|
332
|
+
return (
|
|
333
|
+
<text bg="magenta" fg="white">
|
|
334
|
+
{" "}
|
|
335
|
+
{indicator} {scopeLabel} {updateBadge}{skill.name.padEnd(30)}{skill.source.repo}{" "}
|
|
336
|
+
</text>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<text>
|
|
342
|
+
<span fg={indicatorColor}> {indicator} </span>
|
|
343
|
+
<span fg={skill.installedScope === "user" ? "cyan" : skill.installedScope === "project" ? "green" : "gray"}>
|
|
344
|
+
{scopeLabel}
|
|
345
|
+
</span>
|
|
346
|
+
<span> </span>
|
|
347
|
+
{skill.hasUpdate && (
|
|
348
|
+
<span bg="yellow" fg="black">{updateBadge}</span>
|
|
349
|
+
)}
|
|
350
|
+
<span fg="white">{skill.name.padEnd(30)}</span>
|
|
351
|
+
<span fg="gray">{skill.source.repo}</span>
|
|
352
|
+
</text>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return <text fg="gray">{item.label}</text>;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const renderDetail = () => {
|
|
360
|
+
if (skillsState.skills.status === "loading") {
|
|
361
|
+
return <text fg="gray">Loading skills...</text>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (skillsState.skills.status === "error") {
|
|
365
|
+
return (
|
|
366
|
+
<box flexDirection="column">
|
|
367
|
+
<text fg="red">Failed to load skills</text>
|
|
368
|
+
<box marginTop={1}>
|
|
369
|
+
<text fg="gray">{skillsState.skills.error.message}</text>
|
|
370
|
+
</box>
|
|
371
|
+
<box marginTop={1}>
|
|
372
|
+
<text fg="gray">Set GITHUB_TOKEN to increase rate limits.</text>
|
|
373
|
+
</box>
|
|
374
|
+
</box>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!selectedItem) {
|
|
379
|
+
return <text fg="gray">Select a skill to see details</text>;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (selectedItem.type === "category") {
|
|
383
|
+
const catKey = selectedItem.categoryKey || "general";
|
|
384
|
+
const color = CATEGORY_COLORS[catKey] ? "green" : "cyan";
|
|
385
|
+
return (
|
|
386
|
+
<box flexDirection="column">
|
|
387
|
+
<text fg={color}>
|
|
388
|
+
<strong>{selectedItem.label}</strong>
|
|
389
|
+
</text>
|
|
390
|
+
<box marginTop={1}>
|
|
391
|
+
<text fg="gray">Skills in this category</text>
|
|
392
|
+
</box>
|
|
393
|
+
</box>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!selectedSkill) return null;
|
|
398
|
+
|
|
399
|
+
const fm = selectedSkill.frontmatter;
|
|
400
|
+
const scopeColor = selectedSkill.installedScope === "user" ? "cyan" : "green";
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<box flexDirection="column">
|
|
404
|
+
<text fg="cyan">
|
|
405
|
+
<strong>{selectedSkill.name}</strong>
|
|
406
|
+
</text>
|
|
407
|
+
|
|
408
|
+
<box marginTop={1}>
|
|
409
|
+
<text fg="white">
|
|
410
|
+
{fm ? fm.description : "Loading..."}
|
|
411
|
+
</text>
|
|
412
|
+
</box>
|
|
413
|
+
|
|
414
|
+
{fm?.category && (
|
|
415
|
+
<box marginTop={1}>
|
|
416
|
+
<text>
|
|
417
|
+
<span fg="gray">Category </span>
|
|
418
|
+
<span fg="cyan">{fm.category}</span>
|
|
419
|
+
</text>
|
|
420
|
+
</box>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{fm?.author && (
|
|
424
|
+
<box>
|
|
425
|
+
<text>
|
|
426
|
+
<span fg="gray">Author </span>
|
|
427
|
+
<span>{fm.author}</span>
|
|
428
|
+
</text>
|
|
429
|
+
</box>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{fm?.version && (
|
|
433
|
+
<box>
|
|
434
|
+
<text>
|
|
435
|
+
<span fg="gray">Version </span>
|
|
436
|
+
<span>{fm.version}</span>
|
|
437
|
+
</text>
|
|
438
|
+
</box>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{fm?.tags && fm.tags.length > 0 && (
|
|
442
|
+
<box>
|
|
443
|
+
<text>
|
|
444
|
+
<span fg="gray">Tags </span>
|
|
445
|
+
<span>{fm.tags.join(", ")}</span>
|
|
446
|
+
</text>
|
|
447
|
+
</box>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
<box marginTop={1} flexDirection="column">
|
|
451
|
+
<text>
|
|
452
|
+
<span fg="gray">Source </span>
|
|
453
|
+
<span fg="#5c9aff">{selectedSkill.source.repo}</span>
|
|
454
|
+
</text>
|
|
455
|
+
<text>
|
|
456
|
+
<span fg="gray"> </span>
|
|
457
|
+
<span fg="gray">{selectedSkill.repoPath}</span>
|
|
458
|
+
</text>
|
|
459
|
+
</box>
|
|
460
|
+
|
|
461
|
+
{selectedSkill.installed && selectedSkill.installedScope && (
|
|
462
|
+
<box marginTop={1} flexDirection="column">
|
|
463
|
+
<text>{"─".repeat(24)}</text>
|
|
464
|
+
<text>
|
|
465
|
+
<span fg="gray">Installed </span>
|
|
466
|
+
<span fg={scopeColor}>
|
|
467
|
+
{selectedSkill.installedScope === "user"
|
|
468
|
+
? "~/.claude/skills/"
|
|
469
|
+
: ".claude/skills/"}
|
|
470
|
+
{selectedSkill.name}/
|
|
471
|
+
</span>
|
|
472
|
+
</text>
|
|
473
|
+
</box>
|
|
474
|
+
)}
|
|
475
|
+
|
|
476
|
+
<box marginTop={1} flexDirection="column">
|
|
477
|
+
<text>{"─".repeat(24)}</text>
|
|
478
|
+
<text>
|
|
479
|
+
<strong>Install scope:</strong>
|
|
480
|
+
</text>
|
|
481
|
+
<box marginTop={1} flexDirection="column">
|
|
482
|
+
<text>
|
|
483
|
+
<span bg="cyan" fg="black"> u </span>
|
|
484
|
+
<span fg={selectedSkill.installedScope === "user" ? "cyan" : "gray"}>
|
|
485
|
+
{selectedSkill.installedScope === "user" ? " ● " : " ○ "}
|
|
486
|
+
</span>
|
|
487
|
+
<span fg="cyan">User</span>
|
|
488
|
+
<span fg="gray"> ~/.claude/skills/</span>
|
|
489
|
+
</text>
|
|
490
|
+
<text>
|
|
491
|
+
<span bg="green" fg="black"> p </span>
|
|
492
|
+
<span fg={selectedSkill.installedScope === "project" ? "green" : "gray"}>
|
|
493
|
+
{selectedSkill.installedScope === "project" ? " ● " : " ○ "}
|
|
494
|
+
</span>
|
|
495
|
+
<span fg="green">Project</span>
|
|
496
|
+
<span fg="gray"> .claude/skills/</span>
|
|
497
|
+
</text>
|
|
498
|
+
</box>
|
|
499
|
+
</box>
|
|
500
|
+
|
|
501
|
+
{selectedSkill.hasUpdate && (
|
|
502
|
+
<box marginTop={1}>
|
|
503
|
+
<text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
|
|
504
|
+
</box>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
<box marginTop={1} flexDirection="column">
|
|
508
|
+
{!selectedSkill.installed && (
|
|
509
|
+
<text fg="gray">Press u/p to install in scope</text>
|
|
510
|
+
)}
|
|
511
|
+
{selectedSkill.installed && (
|
|
512
|
+
<text fg="gray">Press d to uninstall</text>
|
|
513
|
+
)}
|
|
514
|
+
</box>
|
|
515
|
+
</box>
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const skills =
|
|
520
|
+
skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
521
|
+
const installedCount = skills.filter((s) => s.installed).length;
|
|
522
|
+
const totalCount = skills.length;
|
|
523
|
+
const updateCount = skills.filter((s) => s.hasUpdate).length;
|
|
524
|
+
|
|
525
|
+
const statusContent = (
|
|
526
|
+
<text>
|
|
527
|
+
<span fg="gray">Skills: </span>
|
|
528
|
+
<span fg="cyan">{installedCount} installed</span>
|
|
529
|
+
<span fg="gray"> │ {totalCount} available</span>
|
|
530
|
+
{updateCount > 0 && (
|
|
531
|
+
<span fg="yellow"> │ {updateCount} updates</span>
|
|
532
|
+
)}
|
|
533
|
+
</text>
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<ScreenLayout
|
|
538
|
+
title="claudeup Skills"
|
|
539
|
+
currentScreen="skills"
|
|
540
|
+
statusLine={statusContent}
|
|
541
|
+
search={
|
|
542
|
+
skillsState.searchQuery || isSearchActive
|
|
543
|
+
? {
|
|
544
|
+
isActive: isSearchActive,
|
|
545
|
+
query: skillsState.searchQuery,
|
|
546
|
+
placeholder: "type to search",
|
|
547
|
+
}
|
|
548
|
+
: undefined
|
|
549
|
+
}
|
|
550
|
+
footerHints="↑↓:nav │ u:user scope │ p:project scope │ Enter:install │ d:uninstall │ type to search"
|
|
551
|
+
listPanel={
|
|
552
|
+
skillsState.skills.status !== "success" ? (
|
|
553
|
+
<text fg="gray">
|
|
554
|
+
{skillsState.skills.status === "loading"
|
|
555
|
+
? "Loading skills..."
|
|
556
|
+
: skillsState.skills.status === "error"
|
|
557
|
+
? "Error loading skills"
|
|
558
|
+
: "Press r to load skills"}
|
|
559
|
+
</text>
|
|
560
|
+
) : (
|
|
561
|
+
<ScrollableList
|
|
562
|
+
items={selectableItems}
|
|
563
|
+
selectedIndex={skillsState.selectedIndex}
|
|
564
|
+
renderItem={renderListItem}
|
|
565
|
+
maxHeight={dimensions.listPanelHeight}
|
|
566
|
+
/>
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
detailPanel={renderDetail()}
|
|
570
|
+
/>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export default SkillsScreen;
|
|
@@ -162,7 +162,7 @@ export function StatusLineScreen() {
|
|
|
162
162
|
return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: "gray", children: "Select a theme to see preview" }) }));
|
|
163
163
|
}
|
|
164
164
|
if (selectedItem.isCustom) {
|
|
165
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: "\u2728 Custom Status Line" }) }), _jsx("text", { fg: "gray", children: "Create your own unique status line!" }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Available variables:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Model name"] }), _jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model_short}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Short name"] }), _jsxs("text", { children: [_jsx("span", { fg: "yellow", children: _jsx("strong", { children: "{cost}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Session cost"] }), _jsxs("text", { children: [_jsx("span", { fg: "
|
|
165
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: "\u2728 Custom Status Line" }) }), _jsx("text", { fg: "gray", children: "Create your own unique status line!" }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Available variables:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Model name"] }), _jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model_short}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Short name"] }), _jsxs("text", { children: [_jsx("span", { fg: "yellow", children: _jsx("strong", { children: "{cost}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Session cost"] }), _jsxs("text", { children: [_jsx("span", { fg: "#5c9aff", children: _jsx("strong", { children: "{cwd}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Working directory"] }), _jsxs("text", { children: [_jsx("span", { fg: "magenta", children: _jsx("strong", { children: "{git_branch}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Git branch"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{input_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Input tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{output_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Output tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "red", children: _jsx("strong", { children: "{session_duration}" }) }), _jsx("span", { fg: "gray", children: "\u2192" }), " Duration"] })] })] })] }));
|
|
166
166
|
}
|
|
167
167
|
if (selectedItem.preset) {
|
|
168
168
|
const example = selectedItem.preset.template
|
|
@@ -183,7 +183,7 @@ export function StatusLineScreen() {
|
|
|
183
183
|
return (_jsx("text", { fg: "magenta", children: _jsxs("strong", { children: ["\u25B8 ", item.label] }) }));
|
|
184
184
|
}
|
|
185
185
|
if (item.isCustom) {
|
|
186
|
-
return isSelected ? (_jsx("text", { bg: "cyan", fg: "black", children:
|
|
186
|
+
return isSelected ? (_jsx("text", { bg: "cyan", fg: "black", children: _jsx("strong", { children: " \u2795 Custom Status Line " }) })) : (_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [" ", "\u2795 Custom Status Line"] }) }));
|
|
187
187
|
}
|
|
188
188
|
const currentForScope = getCurrentStatusLine();
|
|
189
189
|
const isActive = item.preset && currentForScope === item.preset.template;
|
|
@@ -251,7 +251,7 @@ export function StatusLineScreen() {
|
|
|
251
251
|
<span fg="gray">→</span> Session cost
|
|
252
252
|
</text>
|
|
253
253
|
<text>
|
|
254
|
-
<span fg="
|
|
254
|
+
<span fg="#5c9aff">
|
|
255
255
|
<strong>{"{cwd}"}</strong>
|
|
256
256
|
</span>{" "}
|
|
257
257
|
<span fg="gray">→</span> Working directory
|
|
@@ -310,7 +310,8 @@ export function StatusLineScreen() {
|
|
|
310
310
|
</text>
|
|
311
311
|
<box
|
|
312
312
|
marginTop={1}
|
|
313
|
-
paddingLeft={1}
|
|
313
|
+
paddingLeft={1}
|
|
314
|
+
paddingRight={1}
|
|
314
315
|
borderStyle="rounded"
|
|
315
316
|
borderColor="green"
|
|
316
317
|
>
|
|
@@ -336,7 +337,7 @@ export function StatusLineScreen() {
|
|
|
336
337
|
) => {
|
|
337
338
|
if (item.isCategory) {
|
|
338
339
|
return (
|
|
339
|
-
<text fg="magenta"
|
|
340
|
+
<text fg="magenta">
|
|
340
341
|
<strong>▸ {item.label}</strong>
|
|
341
342
|
</text>
|
|
342
343
|
);
|
|
@@ -344,14 +345,11 @@ export function StatusLineScreen() {
|
|
|
344
345
|
|
|
345
346
|
if (item.isCustom) {
|
|
346
347
|
return isSelected ? (
|
|
347
|
-
<text bg="cyan" fg="black"
|
|
348
|
-
<strong>
|
|
349
|
-
{" "}
|
|
350
|
-
➕ Custom Status Line{" "}
|
|
351
|
-
</strong>
|
|
348
|
+
<text bg="cyan" fg="black">
|
|
349
|
+
<strong> ➕ Custom Status Line </strong>
|
|
352
350
|
</text>
|
|
353
351
|
) : (
|
|
354
|
-
<text fg="cyan"
|
|
352
|
+
<text fg="cyan">
|
|
355
353
|
<strong>{" "}➕ Custom Status Line</strong>
|
|
356
354
|
</text>
|
|
357
355
|
);
|
|
@@ -361,12 +359,12 @@ export function StatusLineScreen() {
|
|
|
361
359
|
const isActive = item.preset && currentForScope === item.preset.template;
|
|
362
360
|
|
|
363
361
|
return isSelected ? (
|
|
364
|
-
<text bg="magenta" fg="white"
|
|
362
|
+
<text bg="magenta" fg="white">
|
|
365
363
|
{" "}
|
|
366
364
|
{isActive ? "●" : "○"} {item.preset?.name || ""}{" "}
|
|
367
365
|
</text>
|
|
368
366
|
) : (
|
|
369
|
-
<text fg={isActive ? "green" : "white"}
|
|
367
|
+
<text fg={isActive ? "green" : "white"}>
|
|
370
368
|
{" "}
|
|
371
369
|
{isActive ? "●" : "○"} {item.preset?.name || ""}
|
|
372
370
|
</text>
|
|
@@ -392,7 +390,7 @@ export function StatusLineScreen() {
|
|
|
392
390
|
return (
|
|
393
391
|
<ScreenLayout
|
|
394
392
|
title="claudeup Status Line"
|
|
395
|
-
currentScreen="statusline"
|
|
393
|
+
currentScreen={"statusline" as never}
|
|
396
394
|
statusLine={statusContent}
|
|
397
395
|
footerHints="↑↓:nav │ Enter:apply │ p:project │ g:global │ r:reset"
|
|
398
396
|
listPanel={
|
package/src/ui/screens/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { PluginsScreen } from "./PluginsScreen.js";
|
|
2
2
|
export { McpScreen } from "./McpScreen.js";
|
|
3
3
|
export { McpRegistryScreen } from "./McpRegistryScreen.js";
|
|
4
|
-
export {
|
|
5
|
-
export { EnvVarsScreen } from "./EnvVarsScreen.js";
|
|
4
|
+
export { SettingsScreen } from "./EnvVarsScreen.js";
|
|
6
5
|
export { CliToolsScreen } from "./CliToolsScreen.js";
|
|
7
6
|
export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
|
|
7
|
+
export { ProfilesScreen } from "./ProfilesScreen.js";
|
|
8
|
+
export { SkillsScreen } from "./SkillsScreen.js";
|
package/src/ui/screens/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { PluginsScreen } from "./PluginsScreen.js";
|
|
2
2
|
export { McpScreen } from "./McpScreen.js";
|
|
3
3
|
export { McpRegistryScreen } from "./McpRegistryScreen.js";
|
|
4
|
-
export {
|
|
5
|
-
export { EnvVarsScreen } from "./EnvVarsScreen.js";
|
|
4
|
+
export { SettingsScreen } from "./EnvVarsScreen.js";
|
|
6
5
|
export { CliToolsScreen } from "./CliToolsScreen.js";
|
|
7
6
|
export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
|
|
7
|
+
export { ProfilesScreen } from "./ProfilesScreen.js";
|
|
8
|
+
export { SkillsScreen } from "./SkillsScreen.js";
|