claudeup 3.17.0 → 4.0.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/ui/adapters/pluginsAdapter.js +139 -0
- package/src/ui/adapters/pluginsAdapter.ts +202 -0
- package/src/ui/adapters/settingsAdapter.js +111 -0
- package/src/ui/adapters/settingsAdapter.ts +165 -0
- package/src/ui/components/ScrollableList.js +4 -4
- package/src/ui/components/ScrollableList.tsx +4 -4
- package/src/ui/components/SearchInput.js +2 -2
- package/src/ui/components/SearchInput.tsx +3 -3
- package/src/ui/components/StyledText.js +1 -1
- package/src/ui/components/StyledText.tsx +5 -1
- package/src/ui/components/layout/ProgressBar.js +1 -1
- package/src/ui/components/layout/ProgressBar.tsx +1 -5
- package/src/ui/components/modals/InputModal.tsx +1 -6
- package/src/ui/components/modals/LoadingModal.js +1 -1
- package/src/ui/components/modals/LoadingModal.tsx +1 -3
- package/src/ui/hooks/index.js +3 -3
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useKeyboard.ts +1 -3
- package/src/ui/hooks/useKeyboardHandler.js +9 -9
- package/src/ui/hooks/useKeyboardHandler.ts +9 -9
- package/src/ui/renderers/cliToolRenderers.js +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +153 -0
- package/src/ui/renderers/mcpRenderers.js +26 -0
- package/src/ui/renderers/mcpRenderers.tsx +145 -0
- package/src/ui/renderers/pluginRenderers.js +124 -0
- package/src/ui/renderers/pluginRenderers.tsx +362 -0
- package/src/ui/renderers/profileRenderers.js +172 -0
- package/src/ui/renderers/profileRenderers.tsx +410 -0
- package/src/ui/renderers/settingsRenderers.js +69 -0
- package/src/ui/renderers/settingsRenderers.tsx +205 -0
- package/src/ui/screens/CliToolsScreen.js +14 -58
- package/src/ui/screens/CliToolsScreen.tsx +36 -196
- package/src/ui/screens/EnvVarsScreen.js +12 -168
- package/src/ui/screens/EnvVarsScreen.tsx +16 -327
- package/src/ui/screens/McpScreen.js +12 -62
- package/src/ui/screens/McpScreen.tsx +21 -190
- package/src/ui/screens/PluginsScreen.js +52 -425
- package/src/ui/screens/PluginsScreen.tsx +70 -758
- package/src/ui/screens/ProfilesScreen.js +32 -97
- package/src/ui/screens/ProfilesScreen.tsx +58 -328
- package/src/ui/screens/SkillsScreen.js +16 -16
- package/src/ui/screens/SkillsScreen.tsx +20 -23
|
@@ -21,31 +21,16 @@ import {
|
|
|
21
21
|
readClipboard,
|
|
22
22
|
ClipboardUnavailableError,
|
|
23
23
|
} from "../../utils/clipboard.js";
|
|
24
|
-
import type { ProfileEntry } from "../../types/index.js";
|
|
25
24
|
import {
|
|
26
25
|
PREDEFINED_PROFILES,
|
|
27
26
|
type PredefinedProfile,
|
|
28
27
|
} from "../../data/predefined-profiles.js";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function buildListItems(profileList: ProfileEntry[]): ListItem[] {
|
|
37
|
-
const predefined: ListItem[] = PREDEFINED_PROFILES.map((p) => ({
|
|
38
|
-
kind: "predefined" as const,
|
|
39
|
-
profile: p,
|
|
40
|
-
}));
|
|
41
|
-
const saved: ListItem[] = profileList.map((e) => ({
|
|
42
|
-
kind: "saved" as const,
|
|
43
|
-
entry: e,
|
|
44
|
-
}));
|
|
45
|
-
return [...predefined, ...saved];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── Component ────────────────────────────────────────────────────────────────
|
|
28
|
+
import {
|
|
29
|
+
buildProfileListItems,
|
|
30
|
+
renderProfileRow,
|
|
31
|
+
renderProfileDetail,
|
|
32
|
+
type ProfileListItem,
|
|
33
|
+
} from "../renderers/profileRenderers.js";
|
|
49
34
|
|
|
50
35
|
export function ProfilesScreen() {
|
|
51
36
|
const { state, dispatch } = useApp();
|
|
@@ -53,7 +38,6 @@ export function ProfilesScreen() {
|
|
|
53
38
|
const modal = useModal();
|
|
54
39
|
const dimensions = useDimensions();
|
|
55
40
|
|
|
56
|
-
// Fetch data
|
|
57
41
|
const fetchData = useCallback(async () => {
|
|
58
42
|
dispatch({ type: "PROFILES_DATA_LOADING" });
|
|
59
43
|
try {
|
|
@@ -76,26 +60,38 @@ export function ProfilesScreen() {
|
|
|
76
60
|
? profilesState.profiles.data
|
|
77
61
|
: [];
|
|
78
62
|
|
|
79
|
-
const allItems =
|
|
80
|
-
const selectedItem:
|
|
63
|
+
const allItems = buildProfileListItems(profileList, PREDEFINED_PROFILES);
|
|
64
|
+
const selectedItem: ProfileListItem | undefined =
|
|
65
|
+
allItems[profilesState.selectedIndex];
|
|
66
|
+
|
|
67
|
+
const isNavigable = (item: ProfileListItem) => item.kind !== "header";
|
|
81
68
|
|
|
82
|
-
// Keyboard handling
|
|
83
69
|
useKeyboard((event) => {
|
|
84
70
|
if (state.isSearching || state.modal) return;
|
|
85
71
|
|
|
86
72
|
if (event.name === "up" || event.name === "k") {
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
let newIndex = profilesState.selectedIndex - 1;
|
|
74
|
+
while (newIndex > 0 && !isNavigable(allItems[newIndex]!)) {
|
|
75
|
+
newIndex--;
|
|
76
|
+
}
|
|
77
|
+
if (newIndex >= 0 && isNavigable(allItems[newIndex]!)) {
|
|
78
|
+
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
79
|
+
}
|
|
89
80
|
} else if (event.name === "down" || event.name === "j") {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
let newIndex = profilesState.selectedIndex + 1;
|
|
82
|
+
while (
|
|
83
|
+
newIndex < allItems.length - 1 &&
|
|
84
|
+
!isNavigable(allItems[newIndex]!)
|
|
85
|
+
) {
|
|
86
|
+
newIndex++;
|
|
87
|
+
}
|
|
88
|
+
if (newIndex < allItems.length && isNavigable(allItems[newIndex]!)) {
|
|
89
|
+
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
90
|
+
}
|
|
95
91
|
} else if (event.name === "enter" || event.name === "a") {
|
|
96
92
|
if (selectedItem?.kind === "predefined") {
|
|
97
93
|
void handleApplyPredefined(selectedItem.profile);
|
|
98
|
-
} else {
|
|
94
|
+
} else if (selectedItem?.kind === "saved") {
|
|
99
95
|
void handleApply();
|
|
100
96
|
}
|
|
101
97
|
} else if (event.name === "r") {
|
|
@@ -109,7 +105,7 @@ export function ProfilesScreen() {
|
|
|
109
105
|
}
|
|
110
106
|
});
|
|
111
107
|
|
|
112
|
-
// ───
|
|
108
|
+
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
113
109
|
|
|
114
110
|
const handleApplyPredefined = async (profile: PredefinedProfile) => {
|
|
115
111
|
const allPlugins = [
|
|
@@ -129,16 +125,12 @@ export function ProfilesScreen() {
|
|
|
129
125
|
modal.loading(`Applying "${profile.name}"...`);
|
|
130
126
|
try {
|
|
131
127
|
const settings = await readSettings(state.projectPath);
|
|
132
|
-
|
|
133
|
-
// Merge plugins (additive only)
|
|
134
128
|
settings.enabledPlugins = settings.enabledPlugins ?? {};
|
|
135
129
|
for (const plugin of allPlugins) {
|
|
136
130
|
if (!settings.enabledPlugins[plugin]) {
|
|
137
131
|
settings.enabledPlugins[plugin] = true;
|
|
138
132
|
}
|
|
139
133
|
}
|
|
140
|
-
|
|
141
|
-
// Merge top-level settings (additive — only set if not already set)
|
|
142
134
|
for (const [key, value] of Object.entries(profile.settings)) {
|
|
143
135
|
if (key === "env") {
|
|
144
136
|
const envMap = value as Record<string, string>;
|
|
@@ -158,7 +150,6 @@ export function ProfilesScreen() {
|
|
|
158
150
|
}
|
|
159
151
|
}
|
|
160
152
|
}
|
|
161
|
-
|
|
162
153
|
await writeSettings(settings, state.projectPath);
|
|
163
154
|
modal.hideModal();
|
|
164
155
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
@@ -169,16 +160,10 @@ export function ProfilesScreen() {
|
|
|
169
160
|
);
|
|
170
161
|
} catch (error) {
|
|
171
162
|
modal.hideModal();
|
|
172
|
-
await modal.message(
|
|
173
|
-
"Error",
|
|
174
|
-
`Failed to apply profile: ${error}`,
|
|
175
|
-
"error",
|
|
176
|
-
);
|
|
163
|
+
await modal.message("Error", `Failed to apply profile: ${error}`, "error");
|
|
177
164
|
}
|
|
178
165
|
};
|
|
179
166
|
|
|
180
|
-
// ─── Saved profile actions ────────────────────────────────────────────────
|
|
181
|
-
|
|
182
167
|
const handleApply = async () => {
|
|
183
168
|
if (selectedItem?.kind !== "saved") return;
|
|
184
169
|
const selectedProfile = selectedItem.entry;
|
|
@@ -223,7 +208,6 @@ export function ProfilesScreen() {
|
|
|
223
208
|
state.projectPath,
|
|
224
209
|
);
|
|
225
210
|
modal.hideModal();
|
|
226
|
-
// Trigger PluginsScreen to refetch
|
|
227
211
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
228
212
|
await modal.message(
|
|
229
213
|
"Applied",
|
|
@@ -232,11 +216,7 @@ export function ProfilesScreen() {
|
|
|
232
216
|
);
|
|
233
217
|
} catch (error) {
|
|
234
218
|
modal.hideModal();
|
|
235
|
-
await modal.message(
|
|
236
|
-
"Error",
|
|
237
|
-
`Failed to apply profile: ${error}`,
|
|
238
|
-
"error",
|
|
239
|
-
);
|
|
219
|
+
await modal.message("Error", `Failed to apply profile: ${error}`, "error");
|
|
240
220
|
}
|
|
241
221
|
};
|
|
242
222
|
|
|
@@ -261,11 +241,7 @@ export function ProfilesScreen() {
|
|
|
261
241
|
);
|
|
262
242
|
modal.hideModal();
|
|
263
243
|
await fetchData();
|
|
264
|
-
await modal.message(
|
|
265
|
-
"Renamed",
|
|
266
|
-
`Profile renamed to "${newName.trim()}".`,
|
|
267
|
-
"success",
|
|
268
|
-
);
|
|
244
|
+
await modal.message("Renamed", `Profile renamed to "${newName.trim()}".`, "success");
|
|
269
245
|
} catch (error) {
|
|
270
246
|
modal.hideModal();
|
|
271
247
|
await modal.message("Error", `Failed to rename: ${error}`, "error");
|
|
@@ -290,7 +266,6 @@ export function ProfilesScreen() {
|
|
|
290
266
|
state.projectPath,
|
|
291
267
|
);
|
|
292
268
|
modal.hideModal();
|
|
293
|
-
// Adjust selection if we deleted the last item
|
|
294
269
|
const newIndex = Math.max(
|
|
295
270
|
0,
|
|
296
271
|
Math.min(profilesState.selectedIndex, allItems.length - 2),
|
|
@@ -325,7 +300,6 @@ export function ProfilesScreen() {
|
|
|
325
300
|
);
|
|
326
301
|
} catch (err) {
|
|
327
302
|
if (err instanceof ClipboardUnavailableError) {
|
|
328
|
-
// Fallback: show JSON in modal for manual copy
|
|
329
303
|
await modal.message("Profile JSON (copy manually)", json, "info");
|
|
330
304
|
} else {
|
|
331
305
|
throw err;
|
|
@@ -340,12 +314,10 @@ export function ProfilesScreen() {
|
|
|
340
314
|
const handleImport = async () => {
|
|
341
315
|
let json: string | null = null;
|
|
342
316
|
|
|
343
|
-
// Try to read from clipboard first
|
|
344
317
|
try {
|
|
345
318
|
json = await readClipboard();
|
|
346
319
|
} catch (err) {
|
|
347
320
|
if (err instanceof ClipboardUnavailableError) {
|
|
348
|
-
// Fallback: ask user to paste JSON manually
|
|
349
321
|
json = await modal.input("Import Profile", "Paste profile JSON:");
|
|
350
322
|
} else {
|
|
351
323
|
await modal.message("Error", `Clipboard error: ${err}`, "error");
|
|
@@ -372,278 +344,17 @@ export function ProfilesScreen() {
|
|
|
372
344
|
|
|
373
345
|
modal.loading("Importing...");
|
|
374
346
|
try {
|
|
375
|
-
const id = await importProfileFromJson(
|
|
376
|
-
json,
|
|
377
|
-
targetScope,
|
|
378
|
-
state.projectPath,
|
|
379
|
-
);
|
|
347
|
+
const id = await importProfileFromJson(json, targetScope, state.projectPath);
|
|
380
348
|
modal.hideModal();
|
|
381
349
|
await fetchData();
|
|
382
|
-
await modal.message(
|
|
383
|
-
"Imported",
|
|
384
|
-
`Profile imported successfully (id: ${id}).`,
|
|
385
|
-
"success",
|
|
386
|
-
);
|
|
350
|
+
await modal.message("Imported", `Profile imported successfully (id: ${id}).`, "success");
|
|
387
351
|
} catch (error) {
|
|
388
352
|
modal.hideModal();
|
|
389
353
|
await modal.message("Error", `Failed to import: ${error}`, "error");
|
|
390
354
|
}
|
|
391
355
|
};
|
|
392
356
|
|
|
393
|
-
// ───
|
|
394
|
-
|
|
395
|
-
const formatDate = (iso: string): string => {
|
|
396
|
-
try {
|
|
397
|
-
const d = new Date(iso);
|
|
398
|
-
return d.toLocaleDateString("en-US", {
|
|
399
|
-
month: "short",
|
|
400
|
-
day: "numeric",
|
|
401
|
-
year: "numeric",
|
|
402
|
-
});
|
|
403
|
-
} catch {
|
|
404
|
-
return iso;
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const renderListItem = (item: ListItem, _idx: number, isSelected: boolean) => {
|
|
409
|
-
if (item.kind === "predefined") {
|
|
410
|
-
const { profile } = item;
|
|
411
|
-
const pluginCount =
|
|
412
|
-
profile.magusPlugins.length + profile.anthropicPlugins.length;
|
|
413
|
-
const skillCount = profile.skills.length;
|
|
414
|
-
|
|
415
|
-
if (isSelected) {
|
|
416
|
-
return (
|
|
417
|
-
<text bg="blue" fg="white">
|
|
418
|
-
{" "}
|
|
419
|
-
{profile.icon} {profile.name} — {pluginCount} plugins · {skillCount}{" "}
|
|
420
|
-
skill{skillCount !== 1 ? "s" : ""}{" "}
|
|
421
|
-
</text>
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return (
|
|
426
|
-
<text>
|
|
427
|
-
<span fg="blue">[preset]</span>
|
|
428
|
-
<span> </span>
|
|
429
|
-
<span fg="white">
|
|
430
|
-
{profile.icon} {profile.name}
|
|
431
|
-
</span>
|
|
432
|
-
<span fg="gray">
|
|
433
|
-
{" "}
|
|
434
|
-
— {pluginCount} plugins · {skillCount} skill
|
|
435
|
-
{skillCount !== 1 ? "s" : ""}
|
|
436
|
-
</span>
|
|
437
|
-
</text>
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Saved profile
|
|
442
|
-
const { entry } = item;
|
|
443
|
-
const pluginCount = Object.keys(entry.plugins).length;
|
|
444
|
-
const dateStr = formatDate(entry.updatedAt);
|
|
445
|
-
const scopeColor = entry.scope === "user" ? "cyan" : "green";
|
|
446
|
-
const scopeLabel = entry.scope === "user" ? "[user]" : "[proj]";
|
|
447
|
-
|
|
448
|
-
if (isSelected) {
|
|
449
|
-
return (
|
|
450
|
-
<text bg="magenta" fg="white">
|
|
451
|
-
{" "}
|
|
452
|
-
{scopeLabel} {entry.name} — {pluginCount} plugin
|
|
453
|
-
{pluginCount !== 1 ? "s" : ""} · {dateStr}{" "}
|
|
454
|
-
</text>
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return (
|
|
459
|
-
<text>
|
|
460
|
-
<span fg={scopeColor}>{scopeLabel}</span>
|
|
461
|
-
<span> </span>
|
|
462
|
-
<span fg="white">{entry.name}</span>
|
|
463
|
-
<span fg="gray">
|
|
464
|
-
{" "}
|
|
465
|
-
— {pluginCount} plugin{pluginCount !== 1 ? "s" : ""} · {dateStr}
|
|
466
|
-
</span>
|
|
467
|
-
</text>
|
|
468
|
-
);
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const renderDetail = () => {
|
|
472
|
-
if (profilesState.profiles.status === "loading") {
|
|
473
|
-
return <text fg="gray">Loading profiles...</text>;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (profilesState.profiles.status === "error") {
|
|
477
|
-
return (
|
|
478
|
-
<text fg="red">Error: {profilesState.profiles.error.message}</text>
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!selectedItem) {
|
|
483
|
-
return <text fg="gray">Select a profile to see details</text>;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (selectedItem.kind === "predefined") {
|
|
487
|
-
return renderPredefinedDetail(selectedItem.profile);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return renderSavedDetail(selectedItem.entry);
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
const renderPredefinedDetail = (profile: PredefinedProfile) => {
|
|
494
|
-
const allPlugins = [
|
|
495
|
-
...profile.magusPlugins.map((p) => `${p}@magus`),
|
|
496
|
-
...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
|
|
497
|
-
];
|
|
498
|
-
|
|
499
|
-
return (
|
|
500
|
-
<box flexDirection="column">
|
|
501
|
-
<text fg="blue">
|
|
502
|
-
<strong>
|
|
503
|
-
{profile.icon} {profile.name}
|
|
504
|
-
</strong>
|
|
505
|
-
</text>
|
|
506
|
-
<box marginTop={1}>
|
|
507
|
-
<text fg="gray">{profile.description}</text>
|
|
508
|
-
</box>
|
|
509
|
-
<box marginTop={1} flexDirection="column">
|
|
510
|
-
<text fg="gray">
|
|
511
|
-
Magus plugins ({profile.magusPlugins.length}):
|
|
512
|
-
</text>
|
|
513
|
-
{profile.magusPlugins.map((p) => (
|
|
514
|
-
<box key={p}>
|
|
515
|
-
<text fg="cyan"> {p}@magus</text>
|
|
516
|
-
</box>
|
|
517
|
-
))}
|
|
518
|
-
</box>
|
|
519
|
-
<box marginTop={1} flexDirection="column">
|
|
520
|
-
<text fg="gray">
|
|
521
|
-
Anthropic plugins ({profile.anthropicPlugins.length}):
|
|
522
|
-
</text>
|
|
523
|
-
{profile.anthropicPlugins.map((p) => (
|
|
524
|
-
<box key={p}>
|
|
525
|
-
<text fg="yellow"> {p}@claude-plugins-official</text>
|
|
526
|
-
</box>
|
|
527
|
-
))}
|
|
528
|
-
</box>
|
|
529
|
-
<box marginTop={1} flexDirection="column">
|
|
530
|
-
<text fg="gray">Skills ({profile.skills.length}):</text>
|
|
531
|
-
{profile.skills.map((s) => (
|
|
532
|
-
<box key={s}>
|
|
533
|
-
<text fg="white"> {s}</text>
|
|
534
|
-
</box>
|
|
535
|
-
))}
|
|
536
|
-
</box>
|
|
537
|
-
<box marginTop={1} flexDirection="column">
|
|
538
|
-
<text fg="gray">
|
|
539
|
-
Settings ({Object.keys(profile.settings).length}):
|
|
540
|
-
</text>
|
|
541
|
-
{Object.entries(profile.settings)
|
|
542
|
-
.filter(([k]) => k !== "env")
|
|
543
|
-
.map(([k, v]) => (
|
|
544
|
-
<box key={k}>
|
|
545
|
-
<text fg="white">
|
|
546
|
-
{" "}
|
|
547
|
-
{k}: {String(v)}
|
|
548
|
-
</text>
|
|
549
|
-
</box>
|
|
550
|
-
))}
|
|
551
|
-
</box>
|
|
552
|
-
<box marginTop={2} flexDirection="column">
|
|
553
|
-
<box>
|
|
554
|
-
<text bg="blue" fg="white">
|
|
555
|
-
{" "}
|
|
556
|
-
Enter/a{" "}
|
|
557
|
-
</text>
|
|
558
|
-
<text fg="gray">
|
|
559
|
-
{" "}
|
|
560
|
-
Apply (merges {allPlugins.length} plugins into project settings)
|
|
561
|
-
</text>
|
|
562
|
-
</box>
|
|
563
|
-
</box>
|
|
564
|
-
</box>
|
|
565
|
-
);
|
|
566
|
-
};
|
|
567
|
-
|
|
568
|
-
const renderSavedDetail = (selectedProfile: ProfileEntry) => {
|
|
569
|
-
const plugins = Object.keys(selectedProfile.plugins);
|
|
570
|
-
const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
|
|
571
|
-
const scopeLabel =
|
|
572
|
-
selectedProfile.scope === "user"
|
|
573
|
-
? "User (~/.claude/profiles.json)"
|
|
574
|
-
: "Project (.claude/profiles.json — committed to git)";
|
|
575
|
-
|
|
576
|
-
return (
|
|
577
|
-
<box flexDirection="column">
|
|
578
|
-
<text fg="cyan">
|
|
579
|
-
<strong>{selectedProfile.name}</strong>
|
|
580
|
-
</text>
|
|
581
|
-
<box marginTop={1}>
|
|
582
|
-
<text fg="gray">Scope: </text>
|
|
583
|
-
<text fg={scopeColor}>{scopeLabel}</text>
|
|
584
|
-
</box>
|
|
585
|
-
<box marginTop={1}>
|
|
586
|
-
<text fg="gray">
|
|
587
|
-
Created: {formatDate(selectedProfile.createdAt)} · Updated:{" "}
|
|
588
|
-
{formatDate(selectedProfile.updatedAt)}
|
|
589
|
-
</text>
|
|
590
|
-
</box>
|
|
591
|
-
<box marginTop={1} flexDirection="column">
|
|
592
|
-
<text fg="gray">
|
|
593
|
-
Plugins ({plugins.length}
|
|
594
|
-
{plugins.length === 0 ? " — applying will disable all plugins" : ""}
|
|
595
|
-
):
|
|
596
|
-
</text>
|
|
597
|
-
{plugins.length === 0 ? (
|
|
598
|
-
<text fg="yellow"> (none)</text>
|
|
599
|
-
) : (
|
|
600
|
-
plugins.map((p) => (
|
|
601
|
-
<box key={p}>
|
|
602
|
-
<text fg="white"> {p}</text>
|
|
603
|
-
</box>
|
|
604
|
-
))
|
|
605
|
-
)}
|
|
606
|
-
</box>
|
|
607
|
-
<box marginTop={2} flexDirection="column">
|
|
608
|
-
<box>
|
|
609
|
-
<text bg="magenta" fg="white">
|
|
610
|
-
{" "}
|
|
611
|
-
Enter/a{" "}
|
|
612
|
-
</text>
|
|
613
|
-
<text fg="gray"> Apply profile</text>
|
|
614
|
-
</box>
|
|
615
|
-
<box marginTop={1}>
|
|
616
|
-
<text bg="#333333" fg="white">
|
|
617
|
-
{" "}
|
|
618
|
-
r{" "}
|
|
619
|
-
</text>
|
|
620
|
-
<text fg="gray"> Rename</text>
|
|
621
|
-
</box>
|
|
622
|
-
<box marginTop={1}>
|
|
623
|
-
<text bg="red" fg="white">
|
|
624
|
-
{" "}
|
|
625
|
-
d{" "}
|
|
626
|
-
</text>
|
|
627
|
-
<text fg="gray"> Delete</text>
|
|
628
|
-
</box>
|
|
629
|
-
<box marginTop={1}>
|
|
630
|
-
<text bg="blue" fg="white">
|
|
631
|
-
{" "}
|
|
632
|
-
c{" "}
|
|
633
|
-
</text>
|
|
634
|
-
<text fg="gray"> Copy JSON to clipboard</text>
|
|
635
|
-
</box>
|
|
636
|
-
<box marginTop={1}>
|
|
637
|
-
<text bg="green" fg="white">
|
|
638
|
-
{" "}
|
|
639
|
-
i{" "}
|
|
640
|
-
</text>
|
|
641
|
-
<text fg="gray"> Import from clipboard</text>
|
|
642
|
-
</box>
|
|
643
|
-
</box>
|
|
644
|
-
</box>
|
|
645
|
-
);
|
|
646
|
-
};
|
|
357
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
647
358
|
|
|
648
359
|
const profileCount = profileList.length;
|
|
649
360
|
const userCount = profileList.filter((p) => p.scope === "user").length;
|
|
@@ -661,6 +372,21 @@ export function ProfilesScreen() {
|
|
|
661
372
|
</text>
|
|
662
373
|
);
|
|
663
374
|
|
|
375
|
+
const firstNavigableIndex = allItems.findIndex(isNavigable);
|
|
376
|
+
const effectiveIndex =
|
|
377
|
+
profilesState.selectedIndex === 0 && selectedItem?.kind === "header"
|
|
378
|
+
? firstNavigableIndex
|
|
379
|
+
: profilesState.selectedIndex;
|
|
380
|
+
|
|
381
|
+
const loadingStatus =
|
|
382
|
+
profilesState.profiles.status === "loading"
|
|
383
|
+
? true
|
|
384
|
+
: false;
|
|
385
|
+
const errorMessage =
|
|
386
|
+
profilesState.profiles.status === "error"
|
|
387
|
+
? profilesState.profiles.error.message
|
|
388
|
+
: undefined;
|
|
389
|
+
|
|
664
390
|
return (
|
|
665
391
|
<ScreenLayout
|
|
666
392
|
title="claudeup Plugin Profiles"
|
|
@@ -670,12 +396,16 @@ export function ProfilesScreen() {
|
|
|
670
396
|
listPanel={
|
|
671
397
|
<ScrollableList
|
|
672
398
|
items={allItems}
|
|
673
|
-
selectedIndex={
|
|
674
|
-
renderItem={
|
|
399
|
+
selectedIndex={effectiveIndex}
|
|
400
|
+
renderItem={renderProfileRow}
|
|
675
401
|
maxHeight={dimensions.listPanelHeight}
|
|
676
402
|
/>
|
|
677
403
|
}
|
|
678
|
-
detailPanel={
|
|
404
|
+
detailPanel={renderProfileDetail(
|
|
405
|
+
allItems[effectiveIndex],
|
|
406
|
+
loadingStatus,
|
|
407
|
+
errorMessage,
|
|
408
|
+
)}
|
|
679
409
|
/>
|
|
680
410
|
);
|
|
681
411
|
}
|
|
@@ -218,42 +218,42 @@ export function SkillsScreen() {
|
|
|
218
218
|
}).catch(() => { });
|
|
219
219
|
}, [selectedSkill?.id, dispatch]);
|
|
220
220
|
// ── Action handlers ───────────────────────────────────────────────────────
|
|
221
|
+
// Status bar message (auto-clears)
|
|
222
|
+
const [statusMsg, setStatusMsg] = useState(null);
|
|
223
|
+
const statusTimerRef = useRef(null);
|
|
224
|
+
const showStatus = useCallback((text, tone = "success") => {
|
|
225
|
+
setStatusMsg({ text, tone });
|
|
226
|
+
if (statusTimerRef.current)
|
|
227
|
+
clearTimeout(statusTimerRef.current);
|
|
228
|
+
statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
|
|
229
|
+
}, []);
|
|
221
230
|
const handleInstall = useCallback(async (scope) => {
|
|
222
231
|
if (!selectedSkill)
|
|
223
232
|
return;
|
|
224
|
-
modal.loading(`Installing ${selectedSkill.name}...`);
|
|
225
233
|
try {
|
|
226
234
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
227
|
-
modal.hideModal();
|
|
228
235
|
await fetchData();
|
|
229
|
-
|
|
236
|
+
showStatus(`Installed ${selectedSkill.name} to ${scope}`);
|
|
230
237
|
}
|
|
231
238
|
catch (error) {
|
|
232
|
-
|
|
233
|
-
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
239
|
+
showStatus(`Failed: ${error}`, "error");
|
|
234
240
|
}
|
|
235
|
-
}, [selectedSkill, state.projectPath,
|
|
241
|
+
}, [selectedSkill, state.projectPath, fetchData, showStatus]);
|
|
236
242
|
const handleUninstall = useCallback(async () => {
|
|
237
243
|
if (!selectedSkill || !selectedSkill.installed)
|
|
238
244
|
return;
|
|
239
245
|
const scope = selectedSkill.installedScope;
|
|
240
246
|
if (!scope)
|
|
241
247
|
return;
|
|
242
|
-
const confirmed = await modal.confirm(`Uninstall "${selectedSkill.name}"?`, `This will remove it from the ${scope} scope.`);
|
|
243
|
-
if (!confirmed)
|
|
244
|
-
return;
|
|
245
|
-
modal.loading(`Uninstalling ${selectedSkill.name}...`);
|
|
246
248
|
try {
|
|
247
249
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
248
|
-
modal.hideModal();
|
|
249
250
|
await fetchData();
|
|
250
|
-
|
|
251
|
+
showStatus(`Removed ${selectedSkill.name} from ${scope}`);
|
|
251
252
|
}
|
|
252
253
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
254
|
+
showStatus(`Failed: ${error}`, "error");
|
|
255
255
|
}
|
|
256
|
-
}, [selectedSkill, state.projectPath,
|
|
256
|
+
}, [selectedSkill, state.projectPath, fetchData, showStatus]);
|
|
257
257
|
// ── Keyboard handling ─────────────────────────────────────────────────────
|
|
258
258
|
useKeyboard((event) => {
|
|
259
259
|
if (state.modal)
|
|
@@ -350,7 +350,7 @@ export function SkillsScreen() {
|
|
|
350
350
|
const skills = skillsState.skills.status === "success" ? skillsState.skills.data : [];
|
|
351
351
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
352
352
|
const query = skillsState.searchQuery.trim();
|
|
353
|
-
const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
|
|
353
|
+
const statusContent = statusMsg ? (_jsx("text", { children: _jsx("span", { fg: statusMsg.tone === "success" ? "green" : "red", children: statusMsg.text }) })) : (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Skills: " }), _jsxs("span", { fg: "cyan", children: [installedCount, " installed"] }), query.length >= 2 && isSearchLoading && (_jsx("span", { fg: "yellow", children: " \u2502 searching..." })), query.length >= 2 && !isSearchLoading && searchResults.length > 0 && (_jsxs("span", { fg: "green", children: [" \u2502 ", searchResults.length, " found"] })), !query && _jsx("span", { fg: "gray", children: " \u2502 89K+ searchable" })] }));
|
|
354
354
|
// ── Render ────────────────────────────────────────────────────────────────
|
|
355
355
|
return (_jsx(ScreenLayout, { title: "claudeup Skills", currentScreen: "skills", statusLine: statusContent, search: skillsState.searchQuery || isSearchActive
|
|
356
356
|
? {
|
|
@@ -253,46 +253,39 @@ export function SkillsScreen() {
|
|
|
253
253
|
|
|
254
254
|
// ── Action handlers ───────────────────────────────────────────────────────
|
|
255
255
|
|
|
256
|
+
// Status bar message (auto-clears)
|
|
257
|
+
const [statusMsg, setStatusMsg] = useState<{ text: string; tone: "success" | "error" } | null>(null);
|
|
258
|
+
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
259
|
+
const showStatus = useCallback((text: string, tone: "success" | "error" = "success") => {
|
|
260
|
+
setStatusMsg({ text, tone });
|
|
261
|
+
if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
|
|
262
|
+
statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
|
|
263
|
+
}, []);
|
|
264
|
+
|
|
256
265
|
const handleInstall = useCallback(async (scope: "user" | "project") => {
|
|
257
266
|
if (!selectedSkill) return;
|
|
258
|
-
modal.loading(`Installing ${selectedSkill.name}...`);
|
|
259
267
|
try {
|
|
260
268
|
await installSkill(selectedSkill, scope, state.projectPath);
|
|
261
|
-
modal.hideModal();
|
|
262
269
|
await fetchData();
|
|
263
|
-
|
|
264
|
-
"Installed",
|
|
265
|
-
`${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`,
|
|
266
|
-
"success",
|
|
267
|
-
);
|
|
270
|
+
showStatus(`Installed ${selectedSkill.name} to ${scope}`);
|
|
268
271
|
} catch (error) {
|
|
269
|
-
|
|
270
|
-
await modal.message("Error", `Failed to install: ${error}`, "error");
|
|
272
|
+
showStatus(`Failed: ${error}`, "error");
|
|
271
273
|
}
|
|
272
|
-
}, [selectedSkill, state.projectPath,
|
|
274
|
+
}, [selectedSkill, state.projectPath, fetchData, showStatus]);
|
|
273
275
|
|
|
274
276
|
const handleUninstall = useCallback(async () => {
|
|
275
277
|
if (!selectedSkill || !selectedSkill.installed) return;
|
|
276
278
|
const scope = selectedSkill.installedScope;
|
|
277
279
|
if (!scope) return;
|
|
278
280
|
|
|
279
|
-
const confirmed = await modal.confirm(
|
|
280
|
-
`Uninstall "${selectedSkill.name}"?`,
|
|
281
|
-
`This will remove it from the ${scope} scope.`,
|
|
282
|
-
);
|
|
283
|
-
if (!confirmed) return;
|
|
284
|
-
|
|
285
|
-
modal.loading(`Uninstalling ${selectedSkill.name}...`);
|
|
286
281
|
try {
|
|
287
282
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
288
|
-
modal.hideModal();
|
|
289
283
|
await fetchData();
|
|
290
|
-
|
|
284
|
+
showStatus(`Removed ${selectedSkill.name} from ${scope}`);
|
|
291
285
|
} catch (error) {
|
|
292
|
-
|
|
293
|
-
await modal.message("Error", `Failed to uninstall: ${error}`, "error");
|
|
286
|
+
showStatus(`Failed: ${error}`, "error");
|
|
294
287
|
}
|
|
295
|
-
}, [selectedSkill, state.projectPath,
|
|
288
|
+
}, [selectedSkill, state.projectPath, fetchData, showStatus]);
|
|
296
289
|
|
|
297
290
|
// ── Keyboard handling ─────────────────────────────────────────────────────
|
|
298
291
|
|
|
@@ -390,7 +383,11 @@ export function SkillsScreen() {
|
|
|
390
383
|
const installedCount = skills.filter((s) => s.installed).length;
|
|
391
384
|
const query = skillsState.searchQuery.trim();
|
|
392
385
|
|
|
393
|
-
const statusContent = (
|
|
386
|
+
const statusContent = statusMsg ? (
|
|
387
|
+
<text>
|
|
388
|
+
<span fg={statusMsg.tone === "success" ? "green" : "red"}>{statusMsg.text}</span>
|
|
389
|
+
</text>
|
|
390
|
+
) : (
|
|
394
391
|
<text>
|
|
395
392
|
<span fg="gray">Skills: </span>
|
|
396
393
|
<span fg="cyan">{installedCount} installed</span>
|