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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/ui/adapters/pluginsAdapter.js +139 -0
  3. package/src/ui/adapters/pluginsAdapter.ts +202 -0
  4. package/src/ui/adapters/settingsAdapter.js +111 -0
  5. package/src/ui/adapters/settingsAdapter.ts +165 -0
  6. package/src/ui/components/ScrollableList.js +4 -4
  7. package/src/ui/components/ScrollableList.tsx +4 -4
  8. package/src/ui/components/SearchInput.js +2 -2
  9. package/src/ui/components/SearchInput.tsx +3 -3
  10. package/src/ui/components/StyledText.js +1 -1
  11. package/src/ui/components/StyledText.tsx +5 -1
  12. package/src/ui/components/layout/ProgressBar.js +1 -1
  13. package/src/ui/components/layout/ProgressBar.tsx +1 -5
  14. package/src/ui/components/modals/InputModal.tsx +1 -6
  15. package/src/ui/components/modals/LoadingModal.js +1 -1
  16. package/src/ui/components/modals/LoadingModal.tsx +1 -3
  17. package/src/ui/hooks/index.js +3 -3
  18. package/src/ui/hooks/index.ts +3 -3
  19. package/src/ui/hooks/useKeyboard.ts +1 -3
  20. package/src/ui/hooks/useKeyboardHandler.js +9 -9
  21. package/src/ui/hooks/useKeyboardHandler.ts +9 -9
  22. package/src/ui/renderers/cliToolRenderers.js +33 -0
  23. package/src/ui/renderers/cliToolRenderers.tsx +153 -0
  24. package/src/ui/renderers/mcpRenderers.js +26 -0
  25. package/src/ui/renderers/mcpRenderers.tsx +145 -0
  26. package/src/ui/renderers/pluginRenderers.js +124 -0
  27. package/src/ui/renderers/pluginRenderers.tsx +362 -0
  28. package/src/ui/renderers/profileRenderers.js +172 -0
  29. package/src/ui/renderers/profileRenderers.tsx +410 -0
  30. package/src/ui/renderers/settingsRenderers.js +69 -0
  31. package/src/ui/renderers/settingsRenderers.tsx +205 -0
  32. package/src/ui/screens/CliToolsScreen.js +14 -58
  33. package/src/ui/screens/CliToolsScreen.tsx +36 -196
  34. package/src/ui/screens/EnvVarsScreen.js +12 -168
  35. package/src/ui/screens/EnvVarsScreen.tsx +16 -327
  36. package/src/ui/screens/McpScreen.js +12 -62
  37. package/src/ui/screens/McpScreen.tsx +21 -190
  38. package/src/ui/screens/PluginsScreen.js +52 -425
  39. package/src/ui/screens/PluginsScreen.tsx +70 -758
  40. package/src/ui/screens/ProfilesScreen.js +32 -97
  41. package/src/ui/screens/ProfilesScreen.tsx +58 -328
  42. package/src/ui/screens/SkillsScreen.js +16 -16
  43. 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
- // ─── List item discriminated union ───────────────────────────────────────────
31
-
32
- type ListItem =
33
- | { kind: "predefined"; profile: PredefinedProfile }
34
- | { kind: "saved"; entry: ProfileEntry };
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 = buildListItems(profileList);
80
- const selectedItem: ListItem | undefined = allItems[profilesState.selectedIndex];
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
- const newIndex = Math.max(0, profilesState.selectedIndex - 1);
88
- dispatch({ type: "PROFILES_SELECT", index: newIndex });
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
- const newIndex = Math.min(
91
- Math.max(0, allItems.length - 1),
92
- profilesState.selectedIndex + 1,
93
- );
94
- dispatch({ type: "PROFILES_SELECT", index: newIndex });
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
- // ─── Predefined profile apply ─────────────────────────────────────────────
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
- // ─── Rendering helpers ────────────────────────────────────────────────────
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={profilesState.selectedIndex}
674
- renderItem={renderListItem}
399
+ selectedIndex={effectiveIndex}
400
+ renderItem={renderProfileRow}
675
401
  maxHeight={dimensions.listPanelHeight}
676
402
  />
677
403
  }
678
- detailPanel={renderDetail()}
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
- await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
236
+ showStatus(`Installed ${selectedSkill.name} to ${scope}`);
230
237
  }
231
238
  catch (error) {
232
- modal.hideModal();
233
- await modal.message("Error", `Failed to install: ${error}`, "error");
239
+ showStatus(`Failed: ${error}`, "error");
234
240
  }
235
- }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
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
- await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
251
+ showStatus(`Removed ${selectedSkill.name} from ${scope}`);
251
252
  }
252
253
  catch (error) {
253
- modal.hideModal();
254
- await modal.message("Error", `Failed to uninstall: ${error}`, "error");
254
+ showStatus(`Failed: ${error}`, "error");
255
255
  }
256
- }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
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
- await modal.message(
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
- modal.hideModal();
270
- await modal.message("Error", `Failed to install: ${error}`, "error");
272
+ showStatus(`Failed: ${error}`, "error");
271
273
  }
272
- }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
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
- await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
284
+ showStatus(`Removed ${selectedSkill.name} from ${scope}`);
291
285
  } catch (error) {
292
- modal.hideModal();
293
- await modal.message("Error", `Failed to uninstall: ${error}`, "error");
286
+ showStatus(`Failed: ${error}`, "error");
294
287
  }
295
- }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
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>