claudeup 3.12.0 → 3.13.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -164,9 +164,11 @@ export async function fetchPopularSkills(limit = 30) {
164
164
  export async function fetchAvailableSkills(_repos, projectPath) {
165
165
  const userInstalled = await getInstalledSkillNames("user");
166
166
  const projectInstalled = await getInstalledSkillNames("project", projectPath);
167
+ const slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
167
168
  const markInstalled = (skill) => {
168
- const isUserInstalled = userInstalled.has(skill.name);
169
- const isProjInstalled = projectInstalled.has(skill.name);
169
+ const slug = slugify(skill.name);
170
+ const isUserInstalled = userInstalled.has(slug) || userInstalled.has(skill.name);
171
+ const isProjInstalled = projectInstalled.has(slug) || projectInstalled.has(skill.name);
170
172
  const installed = isUserInstalled || isProjInstalled;
171
173
  const installedScope = isProjInstalled
172
174
  ? "project"
@@ -256,16 +258,25 @@ export async function installSkill(skill, scope, projectPath) {
256
258
  if (!content) {
257
259
  throw new Error(`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`);
258
260
  }
261
+ // Use slug for directory name — display names can have slashes/spaces
262
+ const dirName = skill.name
263
+ .toLowerCase()
264
+ .replace(/[^a-z0-9]+/g, "-")
265
+ .replace(/^-|-$/g, "");
259
266
  const installDir = scope === "user"
260
- ? path.join(getUserSkillsDir(), skill.name)
261
- : path.join(getProjectSkillsDir(projectPath), skill.name);
267
+ ? path.join(getUserSkillsDir(), dirName)
268
+ : path.join(getProjectSkillsDir(projectPath), dirName);
262
269
  await fs.ensureDir(installDir);
263
270
  await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
264
271
  }
265
272
  export async function uninstallSkill(skillName, scope, projectPath) {
273
+ const dirName = skillName
274
+ .toLowerCase()
275
+ .replace(/[^a-z0-9]+/g, "-")
276
+ .replace(/^-|-$/g, "");
266
277
  const installDir = scope === "user"
267
- ? path.join(getUserSkillsDir(), skillName)
268
- : path.join(getProjectSkillsDir(projectPath), skillName);
278
+ ? path.join(getUserSkillsDir(), dirName)
279
+ : path.join(getProjectSkillsDir(projectPath), dirName);
269
280
  const skillMdPath = path.join(installDir, "SKILL.md");
270
281
  if (await fs.pathExists(skillMdPath)) {
271
282
  await fs.remove(skillMdPath);
@@ -228,9 +228,13 @@ export async function fetchAvailableSkills(
228
228
  const userInstalled = await getInstalledSkillNames("user");
229
229
  const projectInstalled = await getInstalledSkillNames("project", projectPath);
230
230
 
231
+ const slugify = (name: string) =>
232
+ name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
233
+
231
234
  const markInstalled = (skill: SkillInfo): SkillInfo => {
232
- const isUserInstalled = userInstalled.has(skill.name);
233
- const isProjInstalled = projectInstalled.has(skill.name);
235
+ const slug = slugify(skill.name);
236
+ const isUserInstalled = userInstalled.has(slug) || userInstalled.has(skill.name);
237
+ const isProjInstalled = projectInstalled.has(slug) || projectInstalled.has(skill.name);
234
238
  const installed = isUserInstalled || isProjInstalled;
235
239
  const installedScope: "user" | "project" | null = isProjInstalled
236
240
  ? "project"
@@ -334,10 +338,16 @@ export async function installSkill(
334
338
  );
335
339
  }
336
340
 
341
+ // Use slug for directory name — display names can have slashes/spaces
342
+ const dirName = skill.name
343
+ .toLowerCase()
344
+ .replace(/[^a-z0-9]+/g, "-")
345
+ .replace(/^-|-$/g, "");
346
+
337
347
  const installDir =
338
348
  scope === "user"
339
- ? path.join(getUserSkillsDir(), skill.name)
340
- : path.join(getProjectSkillsDir(projectPath), skill.name);
349
+ ? path.join(getUserSkillsDir(), dirName)
350
+ : path.join(getProjectSkillsDir(projectPath), dirName);
341
351
 
342
352
  await fs.ensureDir(installDir);
343
353
  await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
@@ -348,10 +358,15 @@ export async function uninstallSkill(
348
358
  scope: "user" | "project",
349
359
  projectPath?: string,
350
360
  ): Promise<void> {
361
+ const dirName = skillName
362
+ .toLowerCase()
363
+ .replace(/[^a-z0-9]+/g, "-")
364
+ .replace(/^-|-$/g, "");
365
+
351
366
  const installDir =
352
367
  scope === "user"
353
- ? path.join(getUserSkillsDir(), skillName)
354
- : path.join(getProjectSkillsDir(projectPath), skillName);
368
+ ? path.join(getUserSkillsDir(), dirName)
369
+ : path.join(getProjectSkillsDir(projectPath), dirName);
355
370
 
356
371
  const skillMdPath = path.join(installDir, "SKILL.md");
357
372
 
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ /**
3
+ * Scope indicator for list items.
4
+ * Shows colored letter badges when installed, empty space when not.
5
+ *
6
+ * Installed in user+project: u p
7
+ * Installed in project only: p
8
+ * Not installed: (3 spaces)
9
+ *
10
+ * Colors match the detail panel: cyan=user, green=project, yellow=local
11
+ */
12
+ export function scopeIndicatorText(user, project, local) {
13
+ const segments = [
14
+ user
15
+ ? { char: "u", bg: "cyan", fg: "black" }
16
+ : { char: " ", bg: "", fg: "" },
17
+ project
18
+ ? { char: "p", bg: "green", fg: "black" }
19
+ : { char: " ", bg: "", fg: "" },
20
+ local
21
+ ? { char: "l", bg: "yellow", fg: "black" }
22
+ : { char: " ", bg: "", fg: "" },
23
+ ];
24
+ const text = segments.map((s) => s.char).join("");
25
+ return { text, segments };
26
+ }
27
+ export function ScopeIndicator({ user, project, local }) {
28
+ const { segments } = scopeIndicatorText(user, project, local);
29
+ return (_jsx("text", { children: segments.map((s, i) => s.bg ? (_jsx("span", { bg: s.bg, fg: s.fg, children: s.char }, i)) : (_jsx("span", { children: " " }, i))) }));
30
+ }
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Scope indicator for list items.
5
+ * Shows colored letter badges when installed, empty space when not.
6
+ *
7
+ * Installed in user+project: u p
8
+ * Installed in project only: p
9
+ * Not installed: (3 spaces)
10
+ *
11
+ * Colors match the detail panel: cyan=user, green=project, yellow=local
12
+ */
13
+
14
+ export function scopeIndicatorText(
15
+ user?: boolean,
16
+ project?: boolean,
17
+ local?: boolean,
18
+ ): { text: string; segments: Array<{ char: string; bg: string; fg: string }> } {
19
+ const segments: Array<{ char: string; bg: string; fg: string }> = [
20
+ user
21
+ ? { char: "u", bg: "cyan", fg: "black" }
22
+ : { char: " ", bg: "", fg: "" },
23
+ project
24
+ ? { char: "p", bg: "green", fg: "black" }
25
+ : { char: " ", bg: "", fg: "" },
26
+ local
27
+ ? { char: "l", bg: "yellow", fg: "black" }
28
+ : { char: " ", bg: "", fg: "" },
29
+ ];
30
+
31
+ const text = segments.map((s) => s.char).join("");
32
+ return { text, segments };
33
+ }
34
+
35
+ interface ScopeIndicatorProps {
36
+ user?: boolean;
37
+ project?: boolean;
38
+ local?: boolean;
39
+ }
40
+
41
+ export function ScopeIndicator({ user, project, local }: ScopeIndicatorProps) {
42
+ const { segments } = scopeIndicatorText(user, project, local);
43
+
44
+ return (
45
+ <text>
46
+ {segments.map((s, i) =>
47
+ s.bg ? (
48
+ <span key={i} bg={s.bg} fg={s.fg}>
49
+ {s.char}
50
+ </span>
51
+ ) : (
52
+ <span key={i}> </span>
53
+ ),
54
+ )}
55
+ </text>
56
+ );
57
+ }
@@ -111,15 +111,15 @@ export function PluginsScreen() {
111
111
  if (communityPlugins.length > 0) {
112
112
  const communityVirtualMp = {
113
113
  name: COMMUNITY_VIRTUAL_MARKETPLACE,
114
- displayName: "Community",
114
+ displayName: "Anthropic Official — 3rd Party",
115
115
  source: marketplace.source,
116
- description: "Third-party plugins from the community",
116
+ description: "Third-party plugins in the Anthropic Official marketplace",
117
117
  };
118
118
  const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
119
119
  items.push({
120
120
  id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
121
121
  type: "category",
122
- label: "Community",
122
+ label: "Anthropic Official — 3rd Party",
123
123
  marketplace: communityVirtualMp,
124
124
  marketplaceEnabled: true,
125
125
  pluginCount: communityPlugins.length,
@@ -708,9 +708,10 @@ export function PluginsScreen() {
708
708
  installedVersion &&
709
709
  latestVersion !== "0.0.0" &&
710
710
  installedVersion !== latestVersion;
711
- // Determine action: if installed in this scope → uninstall
712
- // If installed anywhere else but not this scope uninstall from detected scope
713
- // Otherwiseinstall
711
+ // Determine action for THIS scope:
712
+ // - installed in this scope + has update update
713
+ // - installed in this scope uninstall from this scope
714
+ // - not installed in this scope → install to this scope
714
715
  let action;
715
716
  if (isInstalledInScope && hasUpdateInScope) {
716
717
  action = "update";
@@ -718,12 +719,8 @@ export function PluginsScreen() {
718
719
  else if (isInstalledInScope) {
719
720
  action = "uninstall";
720
721
  }
721
- else if (!isInstalledAnywhere) {
722
- action = "install";
723
- }
724
722
  else {
725
- // Installed in a different scope — uninstall from the scope it's actually in
726
- action = "uninstall";
723
+ action = "install";
727
724
  }
728
725
  const actionLabel = action === "update"
729
726
  ? `Updating ${scopeLabel}`
@@ -883,17 +880,12 @@ export function PluginsScreen() {
883
880
  }
884
881
  if (item.type === "plugin" && item.plugin) {
885
882
  const plugin = item.plugin;
886
- let statusIcon = "○";
887
- let statusColor = "gray";
888
883
  const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
889
- if (plugin.isOrphaned) {
890
- statusIcon = "x";
891
- statusColor = "red";
892
- }
893
- else if (isAnyScope) {
894
- statusIcon = "●";
895
- statusColor = "green";
896
- }
884
+ // Build scope parts for colored rendering
885
+ const hasUser = plugin.userScope?.enabled;
886
+ const hasProject = plugin.projectScope?.enabled;
887
+ const hasLocal = plugin.localScope?.enabled;
888
+ const hasAnyScope = hasUser || hasProject || hasLocal;
897
889
  // Build version string
898
890
  let versionStr = "";
899
891
  if (plugin.isOrphaned) {
@@ -909,19 +901,18 @@ export function PluginsScreen() {
909
901
  const matches = item._matches;
910
902
  const segments = matches ? highlightMatches(plugin.name, matches) : null;
911
903
  if (isSelected) {
912
- const displayText = ` ${statusIcon} ${plugin.name}${versionStr} `;
913
- return (_jsx("text", { bg: "magenta", fg: "white", children: displayText }));
904
+ const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}${hasLocal ? "l" : "."}]`;
905
+ return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", plugin.name, versionStr, " "] }));
914
906
  }
915
- // For non-selected, render with colors
916
907
  const displayName = segments
917
908
  ? segments.map((seg) => seg.text).join("")
918
909
  : plugin.name;
919
910
  if (plugin.isOrphaned) {
920
911
  const ver = plugin.installedVersion && plugin.installedVersion !== "0.0.0"
921
912
  ? ` v${plugin.installedVersion}` : "";
922
- return (_jsxs("text", { children: [_jsxs("span", { fg: "red", children: [" ", statusIcon, " "] }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
913
+ return (_jsxs("text", { children: [_jsx("span", { fg: "red", children: " [x..] " }), _jsx("span", { fg: "gray", children: displayName }), ver && _jsx("span", { fg: "yellow", children: ver }), _jsx("span", { fg: "red", children: " deprecated" })] }));
923
914
  }
924
- return (_jsxs("text", { children: [_jsxs("span", { fg: statusColor, children: [" ", statusIcon, " "] }), _jsx("span", { children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
915
+ return (_jsxs("text", { children: [_jsx("span", { fg: "#555555", children: " [" }), _jsx("span", { fg: hasUser ? "cyan" : "#555555", children: hasUser ? "u" : "." }), _jsx("span", { fg: hasProject ? "green" : "#555555", children: hasProject ? "p" : "." }), _jsx("span", { fg: hasLocal ? "yellow" : "#555555", children: hasLocal ? "l" : "." }), _jsx("span", { fg: "#555555", children: "]" }), _jsx("span", { children: " " }), _jsx("span", { fg: hasAnyScope ? "white" : "gray", children: displayName }), _jsx("span", { fg: plugin.hasUpdate ? "yellow" : "gray", children: versionStr })] }));
925
916
  }
926
917
  return _jsx("text", { fg: "gray", children: item.label });
927
918
  };
@@ -6,6 +6,7 @@ import { ScreenLayout } from "../components/layout/index.js";
6
6
  import { CategoryHeader } from "../components/CategoryHeader.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
8
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
9
+ import { scopeIndicatorText } from "../components/ScopeIndicator.js";
9
10
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
10
11
  import { getAllMarketplaces } from "../../data/marketplaces.js";
11
12
  import {
@@ -171,15 +172,15 @@ export function PluginsScreen() {
171
172
  if (communityPlugins.length > 0) {
172
173
  const communityVirtualMp: Marketplace = {
173
174
  name: COMMUNITY_VIRTUAL_MARKETPLACE,
174
- displayName: "Community",
175
+ displayName: "Anthropic Official — 3rd Party",
175
176
  source: marketplace.source,
176
- description: "Third-party plugins from the community",
177
+ description: "Third-party plugins in the Anthropic Official marketplace",
177
178
  };
178
179
  const communityCollapsed = collapsed.has(COMMUNITY_VIRTUAL_MARKETPLACE);
179
180
  items.push({
180
181
  id: `mp:${COMMUNITY_VIRTUAL_MARKETPLACE}`,
181
182
  type: "category",
182
- label: "Community",
183
+ label: "Anthropic Official — 3rd Party",
183
184
  marketplace: communityVirtualMp,
184
185
  marketplaceEnabled: true,
185
186
  pluginCount: communityPlugins.length,
@@ -897,19 +898,17 @@ export function PluginsScreen() {
897
898
  latestVersion !== "0.0.0" &&
898
899
  installedVersion !== latestVersion;
899
900
 
900
- // Determine action: if installed in this scope → uninstall
901
- // If installed anywhere else but not this scope uninstall from detected scope
902
- // Otherwiseinstall
901
+ // Determine action for THIS scope:
902
+ // - installed in this scope + has update update
903
+ // - installed in this scope uninstall from this scope
904
+ // - not installed in this scope → install to this scope
903
905
  let action: "update" | "install" | "uninstall";
904
906
  if (isInstalledInScope && hasUpdateInScope) {
905
907
  action = "update";
906
908
  } else if (isInstalledInScope) {
907
909
  action = "uninstall";
908
- } else if (!isInstalledAnywhere) {
909
- action = "install";
910
910
  } else {
911
- // Installed in a different scope — uninstall from the scope it's actually in
912
- action = "uninstall";
911
+ action = "install";
913
912
  }
914
913
 
915
914
  const actionLabel =
@@ -1139,17 +1138,13 @@ export function PluginsScreen() {
1139
1138
 
1140
1139
  if (item.type === "plugin" && item.plugin) {
1141
1140
  const plugin = item.plugin;
1142
- let statusIcon = "○";
1143
- let statusColor = "gray";
1144
-
1145
1141
  const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
1146
- if (plugin.isOrphaned) {
1147
- statusIcon = "x";
1148
- statusColor = "red";
1149
- } else if (isAnyScope) {
1150
- statusIcon = "●";
1151
- statusColor = "green";
1152
- }
1142
+
1143
+ // Build scope parts for colored rendering
1144
+ const hasUser = plugin.userScope?.enabled;
1145
+ const hasProject = plugin.projectScope?.enabled;
1146
+ const hasLocal = plugin.localScope?.enabled;
1147
+ const hasAnyScope = hasUser || hasProject || hasLocal;
1153
1148
 
1154
1149
  // Build version string
1155
1150
  let versionStr = "";
@@ -1167,15 +1162,14 @@ export function PluginsScreen() {
1167
1162
  const segments = matches ? highlightMatches(plugin.name, matches) : null;
1168
1163
 
1169
1164
  if (isSelected) {
1170
- const displayText = ` ${statusIcon} ${plugin.name}${versionStr} `;
1165
+ const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}${hasLocal ? "l" : "."}]`;
1171
1166
  return (
1172
1167
  <text bg="magenta" fg="white">
1173
- {displayText}
1168
+ {" "}{scopeStr} {plugin.name}{versionStr}{" "}
1174
1169
  </text>
1175
1170
  );
1176
1171
  }
1177
1172
 
1178
- // For non-selected, render with colors
1179
1173
  const displayName = segments
1180
1174
  ? segments.map((seg) => seg.text).join("")
1181
1175
  : plugin.name;
@@ -1185,7 +1179,7 @@ export function PluginsScreen() {
1185
1179
  ? ` v${plugin.installedVersion}` : "";
1186
1180
  return (
1187
1181
  <text>
1188
- <span fg="red">{" "}{statusIcon} </span>
1182
+ <span fg="red"> [x..] </span>
1189
1183
  <span fg="gray">{displayName}</span>
1190
1184
  {ver && <span fg="yellow">{ver}</span>}
1191
1185
  <span fg="red"> deprecated</span>
@@ -1195,11 +1189,13 @@ export function PluginsScreen() {
1195
1189
 
1196
1190
  return (
1197
1191
  <text>
1198
- <span fg={statusColor}>
1199
- {" "}
1200
- {statusIcon}{" "}
1201
- </span>
1202
- <span>{displayName}</span>
1192
+ <span fg="#555555"> [</span>
1193
+ <span fg={hasUser ? "cyan" : "#555555"}>{hasUser ? "u" : "."}</span>
1194
+ <span fg={hasProject ? "green" : "#555555"}>{hasProject ? "p" : "."}</span>
1195
+ <span fg={hasLocal ? "yellow" : "#555555"}>{hasLocal ? "l" : "."}</span>
1196
+ <span fg="#555555">]</span>
1197
+ <span> </span>
1198
+ <span fg={hasAnyScope ? "white" : "gray"}>{displayName}</span>
1203
1199
  <span fg={plugin.hasUpdate ? "yellow" : "gray"}>{versionStr}</span>
1204
1200
  </text>
1205
1201
  );
@@ -229,21 +229,15 @@ export function SkillsScreen() {
229
229
  try {
230
230
  await installSkill(selectedSkill, scope, state.projectPath);
231
231
  modal.hideModal();
232
- dispatch({
233
- type: "SKILLS_UPDATE_ITEM",
234
- name: selectedSkill.name,
235
- updates: {
236
- installed: true,
237
- installedScope: scope,
238
- },
239
- });
232
+ // Refetch to pick up install status for all skills including recommended
233
+ await fetchData();
240
234
  await modal.message("Installed", `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`, "success");
241
235
  }
242
236
  catch (error) {
243
237
  modal.hideModal();
244
238
  await modal.message("Error", `Failed to install: ${error}`, "error");
245
239
  }
246
- }, [selectedSkill, state.projectPath, dispatch, modal]);
240
+ }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
247
241
  // Uninstall handler
248
242
  const handleUninstall = useCallback(async () => {
249
243
  if (!selectedSkill || !selectedSkill.installed)
@@ -258,21 +252,15 @@ export function SkillsScreen() {
258
252
  try {
259
253
  await uninstallSkill(selectedSkill.name, scope, state.projectPath);
260
254
  modal.hideModal();
261
- dispatch({
262
- type: "SKILLS_UPDATE_ITEM",
263
- name: selectedSkill.name,
264
- updates: {
265
- installed: false,
266
- installedScope: null,
267
- },
268
- });
255
+ // Refetch to pick up uninstall status
256
+ await fetchData();
269
257
  await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
270
258
  }
271
259
  catch (error) {
272
260
  modal.hideModal();
273
261
  await modal.message("Error", `Failed to uninstall: ${error}`, "error");
274
262
  }
275
- }, [selectedSkill, state.projectPath, dispatch, modal]);
263
+ }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
276
264
  // Keyboard handling — same pattern as PluginsScreen
277
265
  useKeyboard((event) => {
278
266
  if (state.modal)
@@ -374,14 +362,15 @@ export function SkillsScreen() {
374
362
  }
375
363
  if (item.type === "skill" && item.skill) {
376
364
  const skill = item.skill;
377
- const indicator = skill.installed ? "●" : "○";
378
- const indicatorColor = skill.installed ? "cyan" : "gray";
379
- const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
380
365
  const starsStr = formatStars(skill.stars);
366
+ const hasUser = skill.installedScope === "user";
367
+ const hasProject = skill.installedScope === "project";
368
+ const nameColor = skill.installed ? "white" : "gray";
381
369
  if (isSelected) {
382
- return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ", skill.name, skill.hasUpdate ? "" : "", scopeTag ? ` [${scopeTag}]` : "", starsStr ? ` ${starsStr}` : "", " "] }));
370
+ const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
371
+ return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", scopeStr, " ", skill.name, skill.hasUpdate ? " ⬆" : "", starsStr ? ` ${starsStr}` : "", " "] }));
383
372
  }
384
- return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { fg: "white", children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), scopeTag && (_jsxs("span", { fg: scopeTag === "u" ? "cyan" : "green", children: [" [", scopeTag, "]"] })), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
373
+ return (_jsxs("text", { children: [_jsx("span", { fg: "#555555", children: " [" }), _jsx("span", { fg: hasUser ? "cyan" : "#555555", children: hasUser ? "u" : "." }), _jsx("span", { fg: hasProject ? "green" : "#555555", children: hasProject ? "p" : "." }), _jsx("span", { fg: "#555555", children: "] " }), _jsx("span", { fg: nameColor, children: skill.name }), skill.hasUpdate && _jsx("span", { fg: "yellow", children: " \u2B06" }), starsStr && (_jsxs("span", { fg: "yellow", children: [" ", starsStr] }))] }));
385
374
  }
386
375
  return _jsx("text", { fg: "gray", children: item.label });
387
376
  };
@@ -5,6 +5,7 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
6
  import { ScrollableList } from "../components/ScrollableList.js";
7
7
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
8
+ import { scopeIndicatorText } from "../components/ScopeIndicator.js";
8
9
  import {
9
10
  fetchAvailableSkills,
10
11
  fetchSkillFrontmatter,
@@ -271,14 +272,8 @@ export function SkillsScreen() {
271
272
  try {
272
273
  await installSkill(selectedSkill, scope, state.projectPath);
273
274
  modal.hideModal();
274
- dispatch({
275
- type: "SKILLS_UPDATE_ITEM",
276
- name: selectedSkill.name,
277
- updates: {
278
- installed: true,
279
- installedScope: scope,
280
- },
281
- });
275
+ // Refetch to pick up install status for all skills including recommended
276
+ await fetchData();
282
277
  await modal.message(
283
278
  "Installed",
284
279
  `${selectedSkill.name} installed to ${scope === "user" ? "~/.claude/skills/" : ".claude/skills/"}`,
@@ -288,7 +283,7 @@ export function SkillsScreen() {
288
283
  modal.hideModal();
289
284
  await modal.message("Error", `Failed to install: ${error}`, "error");
290
285
  }
291
- }, [selectedSkill, state.projectPath, dispatch, modal]);
286
+ }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
292
287
 
293
288
  // Uninstall handler
294
289
  const handleUninstall = useCallback(async () => {
@@ -307,20 +302,14 @@ export function SkillsScreen() {
307
302
  try {
308
303
  await uninstallSkill(selectedSkill.name, scope, state.projectPath);
309
304
  modal.hideModal();
310
- dispatch({
311
- type: "SKILLS_UPDATE_ITEM",
312
- name: selectedSkill.name,
313
- updates: {
314
- installed: false,
315
- installedScope: null,
316
- },
317
- });
305
+ // Refetch to pick up uninstall status
306
+ await fetchData();
318
307
  await modal.message("Uninstalled", `${selectedSkill.name} removed.`, "success");
319
308
  } catch (error) {
320
309
  modal.hideModal();
321
310
  await modal.message("Error", `Failed to uninstall: ${error}`, "error");
322
311
  }
323
- }, [selectedSkill, state.projectPath, dispatch, modal]);
312
+ }, [selectedSkill, state.projectPath, dispatch, modal, fetchData]);
324
313
 
325
314
  // Keyboard handling — same pattern as PluginsScreen
326
315
  useKeyboard((event) => {
@@ -437,27 +426,28 @@ export function SkillsScreen() {
437
426
 
438
427
  if (item.type === "skill" && item.skill) {
439
428
  const skill = item.skill;
440
- const indicator = skill.installed ? "●" : "○";
441
- const indicatorColor = skill.installed ? "cyan" : "gray";
442
- const scopeTag = skill.installedScope === "user" ? "u" : skill.installedScope === "project" ? "p" : "";
443
429
  const starsStr = formatStars(skill.stars);
430
+ const hasUser = skill.installedScope === "user";
431
+ const hasProject = skill.installedScope === "project";
432
+ const nameColor = skill.installed ? "white" : "gray";
444
433
 
445
434
  if (isSelected) {
435
+ const scopeStr = `[${hasUser ? "u" : "."}${hasProject ? "p" : "."}]`;
446
436
  return (
447
437
  <text bg="magenta" fg="white">
448
- {" "}{indicator} {skill.name}{skill.hasUpdate ? " ⬆" : ""}{scopeTag ? ` [${scopeTag}]` : ""}{starsStr ? ` ${starsStr}` : ""}{" "}
438
+ {" "}{scopeStr} {skill.name}{skill.hasUpdate ? " ⬆" : ""}{starsStr ? ` ${starsStr}` : ""}{" "}
449
439
  </text>
450
440
  );
451
441
  }
452
442
 
453
443
  return (
454
444
  <text>
455
- <span fg={indicatorColor}> {indicator} </span>
456
- <span fg="white">{skill.name}</span>
445
+ <span fg="#555555"> [</span>
446
+ <span fg={hasUser ? "cyan" : "#555555"}>{hasUser ? "u" : "."}</span>
447
+ <span fg={hasProject ? "green" : "#555555"}>{hasProject ? "p" : "."}</span>
448
+ <span fg="#555555">] </span>
449
+ <span fg={nameColor}>{skill.name}</span>
457
450
  {skill.hasUpdate && <span fg="yellow"> ⬆</span>}
458
- {scopeTag && (
459
- <span fg={scopeTag === "u" ? "cyan" : "green"}> [{scopeTag}]</span>
460
- )}
461
451
  {starsStr && (
462
452
  <span fg="yellow">{" "}{starsStr}</span>
463
453
  )}