magus-cli 7.3.1 → 7.4.1
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 +4 -4
- package/src/__tests__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c19-paste-routing.test.ts +252 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c20-registry-and-file-safety.test.ts +560 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/stale-load-repair.test.ts +5 -4
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +120 -15
- package/src/cli/install.ts +35 -4
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +434 -198
- package/src/services/cli-tool-commands.ts +45 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/gitignore-service.ts +39 -22
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +164 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +316 -44
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +107 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +304 -0
- package/src/ui/input/key-dispatch.ts +197 -0
- package/src/ui/keymap.ts +905 -0
- package/src/ui/renderers/cliToolRenderers.tsx +192 -92
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +173 -101
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +129 -129
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { STAR_RELIABILITY_INFO } from "../../data/skill-repos.js";
|
|
3
|
-
import type { StarReliability } from "../../types/index.js";
|
|
3
|
+
import type { SkillInfo, StarReliability } from "../../types/index.js";
|
|
4
4
|
import type {
|
|
5
5
|
SkillBrowserItem,
|
|
6
6
|
SkillCategoryItem,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SelectableRow,
|
|
19
19
|
} from "../components/primitives/index.js";
|
|
20
20
|
import type { ItemRenderer } from "../registry.js";
|
|
21
|
+
import { displayWidth, padTo, truncate } from "../text-width.js";
|
|
21
22
|
import { type UiColor, theme } from "../theme.js";
|
|
22
23
|
|
|
23
24
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -30,17 +31,26 @@ function formatStarsNum(stars?: number): string {
|
|
|
30
31
|
return `${stars}`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** Star icon
|
|
34
|
+
/** Star icon based on reliability classification */
|
|
34
35
|
function starIcon(reliability?: StarReliability): string {
|
|
35
36
|
if (reliability === "mega-repo") return "☆";
|
|
36
37
|
if (reliability === "skill-dump") return "☆";
|
|
37
38
|
return "★";
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* A star count is a reference fact, so a meaningful one is `info`. A hollow
|
|
43
|
+
* star — a count that says little about the skill — recedes to `muted`. Never
|
|
44
|
+
* `warning`: amber means "needs a look", and a star count asks for nothing.
|
|
45
|
+
*/
|
|
46
|
+
function starTone(reliability?: StarReliability): "info" | "muted" {
|
|
47
|
+
return reliability === "mega-repo" || reliability === "skill-dump"
|
|
48
|
+
? "muted"
|
|
49
|
+
: "info";
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
function starColor(reliability?: StarReliability): UiColor {
|
|
41
|
-
|
|
42
|
-
if (reliability === "skill-dump") return theme.colors.warning;
|
|
43
|
-
return theme.colors.warning;
|
|
53
|
+
return theme.meta[starTone(reliability)];
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
function formatStars(stars?: number, reliability?: StarReliability): string {
|
|
@@ -68,7 +78,7 @@ const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
|
|
|
68
78
|
const isRec = item.categoryKey === "recommended";
|
|
69
79
|
return (
|
|
70
80
|
<box flexDirection="column">
|
|
71
|
-
<text fg={
|
|
81
|
+
<text fg={theme.colors.info}>
|
|
72
82
|
<strong>
|
|
73
83
|
{item.star ?? ""}
|
|
74
84
|
{item.title}
|
|
@@ -89,23 +99,28 @@ const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
|
|
|
89
99
|
// ─── Skill renderer ───────────────────────────────────────────────────────────
|
|
90
100
|
|
|
91
101
|
const MAX_SKILL_NAME_LEN = 35;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return name.length > MAX_SKILL_NAME_LEN
|
|
95
|
-
? `${name.slice(0, MAX_SKILL_NAME_LEN - 1)}\u2026`
|
|
96
|
-
: name;
|
|
97
|
-
}
|
|
102
|
+
/** Width a row assumes when its caller does not pass the panel's. */
|
|
103
|
+
const DEFAULT_ROW_WIDTH = 45;
|
|
98
104
|
|
|
99
105
|
const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
100
|
-
renderRow: ({ item, isSelected }) => {
|
|
106
|
+
renderRow: ({ item, isSelected, width = DEFAULT_ROW_WIDTH }) => {
|
|
101
107
|
const { skill } = item;
|
|
102
108
|
const hasUser = skill.installedScope === "user";
|
|
103
109
|
const hasProject = skill.installedScope === "project";
|
|
104
110
|
const reliability = skill.starReliability;
|
|
105
111
|
const starsStr = formatStars(skill.stars, reliability);
|
|
106
|
-
const sColor = starColor(reliability);
|
|
107
|
-
const displayName = truncateName(skill.name);
|
|
108
112
|
const indentLevel = item.indent ?? 1;
|
|
113
|
+
// Cells before and after the name: indent, two squares and a space; the
|
|
114
|
+
// update arrow; the star count. The name takes what is left, in cells.
|
|
115
|
+
const fixed =
|
|
116
|
+
indentLevel +
|
|
117
|
+
3 +
|
|
118
|
+
(skill.hasUpdate ? 2 : 0) +
|
|
119
|
+
(starsStr ? displayWidth(starsStr) + 2 : 0);
|
|
120
|
+
const displayName = truncate(
|
|
121
|
+
skill.name,
|
|
122
|
+
Math.max(1, Math.min(MAX_SKILL_NAME_LEN, width - fixed)),
|
|
123
|
+
);
|
|
109
124
|
|
|
110
125
|
return (
|
|
111
126
|
<SelectableRow selected={isSelected} indent={indentLevel}>
|
|
@@ -118,7 +133,7 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
|
118
133
|
<span
|
|
119
134
|
fg={
|
|
120
135
|
isSelected
|
|
121
|
-
? theme.
|
|
136
|
+
? theme.selection.fg
|
|
122
137
|
: skill.installed
|
|
123
138
|
? theme.colors.text
|
|
124
139
|
: theme.colors.muted
|
|
@@ -129,170 +144,208 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
|
129
144
|
{skill.hasUpdate ? (
|
|
130
145
|
<MetaText text=" ⬆" tone="warning" selected={isSelected} />
|
|
131
146
|
) : null}
|
|
132
|
-
{starsStr ?
|
|
147
|
+
{starsStr ? (
|
|
148
|
+
<MetaText
|
|
149
|
+
text={` ${starsStr}`}
|
|
150
|
+
tone={starTone(reliability)}
|
|
151
|
+
selected={isSelected}
|
|
152
|
+
/>
|
|
153
|
+
) : null}
|
|
133
154
|
</SelectableRow>
|
|
134
155
|
);
|
|
135
156
|
},
|
|
136
157
|
|
|
137
|
-
renderDetail: ({ item }) => {
|
|
138
|
-
|
|
139
|
-
const fm = skill.frontmatter;
|
|
140
|
-
const description = fm?.description || skill.description || "Loading...";
|
|
141
|
-
const reliability = skill.starReliability;
|
|
142
|
-
const starsStr = formatStars(skill.stars, reliability);
|
|
143
|
-
const sColor = starColor(reliability);
|
|
144
|
-
const reliabilityInfo = reliability
|
|
145
|
-
? STAR_RELIABILITY_INFO[reliability]
|
|
146
|
-
: null;
|
|
158
|
+
renderDetail: ({ item }) => skillDetail(item.skill, {}),
|
|
159
|
+
};
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</text>
|
|
161
|
+
/** What the detail pane knows about a skill's description fetch. */
|
|
162
|
+
export interface SkillDetailState {
|
|
163
|
+
/** The remote SKILL.md fetch failed. */
|
|
164
|
+
descriptionFailed?: boolean;
|
|
165
|
+
}
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
/**
|
|
168
|
+
* The description line, always in a definite state.
|
|
169
|
+
*
|
|
170
|
+
* It used to fall back to "Loading..." whenever the description was empty, so a
|
|
171
|
+
* LOCAL skill — which is never fetched — and a remote one whose fetch had failed
|
|
172
|
+
* both sat on "Loading..." for good. Now: the text when there is one; for a local
|
|
173
|
+
* skill, the fact that its SKILL.md has none; for a remote one, loading only
|
|
174
|
+
* while a fetch is outstanding, and the failure once it is not.
|
|
175
|
+
*/
|
|
176
|
+
function descriptionLine(
|
|
177
|
+
skill: SkillInfo,
|
|
178
|
+
state: SkillDetailState,
|
|
179
|
+
): { text: string; tone: UiColor } {
|
|
180
|
+
const text = skill.frontmatter?.description || skill.description;
|
|
181
|
+
if (text) return { text, tone: theme.colors.text };
|
|
182
|
+
if (skill.source.repo === "local") {
|
|
183
|
+
return {
|
|
184
|
+
text: "No description in its SKILL.md.",
|
|
185
|
+
tone: theme.colors.muted,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (state.descriptionFailed) {
|
|
189
|
+
return {
|
|
190
|
+
text: "Could not fetch the description. Press r to retry.",
|
|
191
|
+
tone: theme.colors.warning,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (skill.frontmatter) {
|
|
195
|
+
return { text: "No description.", tone: theme.colors.muted };
|
|
196
|
+
}
|
|
197
|
+
return { text: "Loading the description…", tone: theme.colors.muted };
|
|
198
|
+
}
|
|
158
199
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
function skillDetail(
|
|
201
|
+
skill: SkillInfo,
|
|
202
|
+
state: SkillDetailState,
|
|
203
|
+
): React.ReactNode {
|
|
204
|
+
const fm = skill.frontmatter;
|
|
205
|
+
const description = descriptionLine(skill, state);
|
|
206
|
+
const reliability = skill.starReliability;
|
|
207
|
+
const starsStr = formatStars(skill.stars, reliability);
|
|
208
|
+
const sColor = starColor(reliability);
|
|
209
|
+
const reliabilityInfo = reliability
|
|
210
|
+
? STAR_RELIABILITY_INFO[reliability]
|
|
211
|
+
: null;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<box flexDirection="column">
|
|
215
|
+
<text fg={theme.colors.info}>
|
|
216
|
+
<strong>{skill.name}</strong>
|
|
217
|
+
{starsStr ? <span fg={sColor}> {starsStr}</span> : null}
|
|
218
|
+
</text>
|
|
219
|
+
|
|
220
|
+
<box marginTop={1}>
|
|
221
|
+
<text fg={description.tone}>{description.text}</text>
|
|
222
|
+
</box>
|
|
177
223
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
224
|
+
{fm?.category ? (
|
|
225
|
+
<KeyValueLine
|
|
226
|
+
label="Category"
|
|
227
|
+
value={<span fg={theme.colors.info}>{fm.category}</span>}
|
|
228
|
+
/>
|
|
229
|
+
) : null}
|
|
230
|
+
{fm?.author ? (
|
|
231
|
+
<KeyValueLine label="Author" value={<span>{fm.author}</span>} />
|
|
232
|
+
) : null}
|
|
233
|
+
{fm?.version ? (
|
|
234
|
+
<KeyValueLine label="Version" value={<span>{fm.version}</span>} />
|
|
235
|
+
) : null}
|
|
236
|
+
{fm?.tags && fm.tags.length > 0 ? (
|
|
237
|
+
<KeyValueLine
|
|
238
|
+
label="Tags"
|
|
239
|
+
value={<span>{(fm.tags as string[]).join(", ")}</span>}
|
|
240
|
+
/>
|
|
241
|
+
) : null}
|
|
188
242
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
</span>
|
|
200
|
-
</text>
|
|
201
|
-
</box>
|
|
202
|
-
)}
|
|
243
|
+
<DetailSection>
|
|
244
|
+
<text fg={theme.colors.text}>
|
|
245
|
+
<span fg={theme.colors.muted}>Source </span>
|
|
246
|
+
<span fg={theme.colors.info}>{skill.source.repo}</span>
|
|
247
|
+
</text>
|
|
248
|
+
<text fg={theme.colors.text}>
|
|
249
|
+
<span fg={theme.colors.muted}>{" "}</span>
|
|
250
|
+
<span fg={theme.colors.muted}>{skill.repoPath}</span>
|
|
251
|
+
</text>
|
|
252
|
+
</DetailSection>
|
|
203
253
|
|
|
204
|
-
|
|
205
|
-
|
|
254
|
+
{reliabilityInfo && reliability !== "dedicated" && (
|
|
255
|
+
<box marginTop={1}>
|
|
206
256
|
<text fg={theme.colors.text}>
|
|
207
|
-
<
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{" "}
|
|
213
|
-
u{" "}
|
|
214
|
-
</span>
|
|
215
|
-
<span
|
|
216
|
-
fg={
|
|
217
|
-
skill.installedScope === "user"
|
|
218
|
-
? theme.colors.info
|
|
219
|
-
: theme.colors.muted
|
|
220
|
-
}
|
|
221
|
-
>
|
|
222
|
-
{skill.installedScope === "user" ? " ● " : " ○ "}
|
|
223
|
-
</span>
|
|
224
|
-
<span fg={theme.colors.info}>User</span>
|
|
225
|
-
<span fg={theme.colors.muted}> ~/.claude/skills/</span>
|
|
226
|
-
</text>
|
|
227
|
-
<text fg={theme.colors.text}>
|
|
228
|
-
<span bg={theme.colors.success} fg={theme.hints.fg}>
|
|
229
|
-
{" "}
|
|
230
|
-
p{" "}
|
|
231
|
-
</span>
|
|
232
|
-
<span
|
|
233
|
-
fg={
|
|
234
|
-
skill.installedScope === "project"
|
|
235
|
-
? theme.colors.success
|
|
236
|
-
: theme.colors.muted
|
|
237
|
-
}
|
|
238
|
-
>
|
|
239
|
-
{skill.installedScope === "project" ? " ● " : " ○ "}
|
|
240
|
-
</span>
|
|
241
|
-
<span fg={theme.colors.success}>Project</span>
|
|
242
|
-
<span fg={theme.colors.muted}> .claude/skills/</span>
|
|
243
|
-
</text>
|
|
244
|
-
</box>
|
|
245
|
-
</DetailSection>
|
|
246
|
-
|
|
247
|
-
{skill.hasUpdate && (
|
|
248
|
-
<box marginTop={1}>
|
|
249
|
-
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
257
|
+
<span fg={sColor}>{starIcon(reliability)} </span>
|
|
258
|
+
<span fg={sColor}>
|
|
259
|
+
<strong>{reliabilityInfo.label}</strong>
|
|
260
|
+
</span>
|
|
261
|
+
<span fg={theme.colors.muted}>
|
|
250
262
|
{" "}
|
|
251
|
-
|
|
252
|
-
</
|
|
253
|
-
</
|
|
254
|
-
|
|
263
|
+
— {reliabilityInfo.description}
|
|
264
|
+
</span>
|
|
265
|
+
</text>
|
|
266
|
+
</box>
|
|
267
|
+
)}
|
|
255
268
|
|
|
269
|
+
<DetailSection>
|
|
270
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
271
|
+
<text fg={theme.colors.text}>
|
|
272
|
+
<strong>Scopes:</strong>
|
|
273
|
+
</text>
|
|
256
274
|
<box marginTop={1}>
|
|
257
|
-
<
|
|
258
|
-
{
|
|
259
|
-
|
|
260
|
-
:
|
|
261
|
-
|
|
275
|
+
<ScopeDetail
|
|
276
|
+
scopes={{
|
|
277
|
+
user: skill.installedScope === "user",
|
|
278
|
+
project: skill.installedScope === "project",
|
|
279
|
+
}}
|
|
280
|
+
paths={{ user: "~/.claude/skills/", project: ".claude/skills/" }}
|
|
281
|
+
/>
|
|
262
282
|
</box>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
</DetailSection>
|
|
284
|
+
|
|
285
|
+
{skill.hasUpdate && (
|
|
286
|
+
<box marginTop={1}>
|
|
287
|
+
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
288
|
+
{" "}
|
|
289
|
+
UPDATE AVAILABLE{" "}
|
|
270
290
|
</text>
|
|
271
291
|
</box>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
<box marginTop={1}>
|
|
295
|
+
<text fg={theme.colors.muted}>
|
|
296
|
+
{skill.installed
|
|
297
|
+
? "Press u/p to toggle scope"
|
|
298
|
+
: "Press u/p to install"}
|
|
299
|
+
</text>
|
|
272
300
|
</box>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
301
|
+
<box>
|
|
302
|
+
<text fg={theme.colors.text}>
|
|
303
|
+
<span bg={theme.colors.dim} fg={theme.hints.fg}>
|
|
304
|
+
{" "}
|
|
305
|
+
o{" "}
|
|
306
|
+
</span>
|
|
307
|
+
<span fg={theme.colors.muted}> Open in browser</span>
|
|
308
|
+
</text>
|
|
309
|
+
</box>
|
|
310
|
+
</box>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
276
313
|
|
|
277
314
|
// ─── Skill Set renderer ──────────────────────────────────────────────────────
|
|
278
315
|
|
|
279
|
-
|
|
316
|
+
/**
|
|
317
|
+
* A skill set's icon in exactly two cells.
|
|
318
|
+
*
|
|
319
|
+
* The icons come from a remote catalogue, so their widths are whatever its
|
|
320
|
+
* author typed: most are wide emoji (\uD83E\uDD17 \uD83C\uDF10 \uD83E\uDDF1 \uD83E\uDDF0, two cells), one is `\u2601\uFE0F` \u2014 a
|
|
321
|
+
* one-cell symbol plus U+FE0F asking for emoji presentation. MEASURED in tmux
|
|
322
|
+
* under OpenTUI 0.1.107, that pair paints as two BLANK cells: the icon vanished.
|
|
323
|
+
* Dropping the selector draws the plain `\u2601`, and padding to two cells in
|
|
324
|
+
* `displayWidth` terms puts every name after it in the same column, whatever
|
|
325
|
+
* the icon's width.
|
|
326
|
+
*/
|
|
327
|
+
function iconCell(icon: string): string {
|
|
328
|
+
return padTo(icon.replaceAll("\uFE0F", ""), 2);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Row cells before the name: lead, arrow, space, icon, space. */
|
|
332
|
+
const SET_ROW_HEAD = 6;
|
|
333
|
+
/** Row cells after the stars: a space and the two scope squares. */
|
|
334
|
+
const SET_ROW_TAIL = 3;
|
|
335
|
+
/** Below this many cells for the name, the star count is dropped first. */
|
|
336
|
+
const MIN_SET_NAME = 8;
|
|
280
337
|
|
|
281
338
|
const skillSetRenderer: ItemRenderer<SkillSetItem> = {
|
|
282
|
-
renderRow: ({ item, isSelected }) => {
|
|
339
|
+
renderRow: ({ item, isSelected, width = DEFAULT_ROW_WIDTH }) => {
|
|
283
340
|
const { skillSet, expanded } = item;
|
|
284
341
|
const arrow = expanded ? "\u25BC" : "\u25B6";
|
|
285
|
-
const displayName =
|
|
286
|
-
skillSet.name.length > MAX_SET_NAME_LEN
|
|
287
|
-
? `${skillSet.name.slice(0, MAX_SET_NAME_LEN - 1)}\u2026`
|
|
288
|
-
: skillSet.name;
|
|
289
342
|
|
|
290
343
|
// Count installed child skills
|
|
291
344
|
const installedCount = skillSet.loaded
|
|
292
345
|
? skillSet.skills.filter((s) => s.installed).length
|
|
293
346
|
: 0;
|
|
294
347
|
const totalCount = skillSet.loaded ? skillSet.skills.length : "...";
|
|
295
|
-
const
|
|
348
|
+
const count = ` (${installedCount}/${totalCount})`;
|
|
296
349
|
|
|
297
350
|
// Any child installed at user or project scope?
|
|
298
351
|
const hasUser =
|
|
@@ -302,28 +355,34 @@ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
|
|
|
302
355
|
skillSet.loaded &&
|
|
303
356
|
skillSet.skills.some((s) => s.installedScope === "project");
|
|
304
357
|
|
|
305
|
-
|
|
358
|
+
// Fitted in cells, not characters: the row is measured end to end so the
|
|
359
|
+
// panel never has to clip it, and a narrow panel loses the star count
|
|
360
|
+
// before it loses the name.
|
|
361
|
+
const fixed = SET_ROW_HEAD + displayWidth(count) + SET_ROW_TAIL;
|
|
362
|
+
const starsFull = skillSet.stars ? ` ${formatStars(skillSet.stars)}` : "";
|
|
363
|
+
const stars =
|
|
364
|
+
width - fixed - displayWidth(starsFull) >= MIN_SET_NAME ? starsFull : "";
|
|
365
|
+
const displayName = truncate(
|
|
366
|
+
skillSet.name,
|
|
367
|
+
Math.max(1, width - fixed - displayWidth(stars)),
|
|
368
|
+
);
|
|
306
369
|
|
|
307
370
|
return (
|
|
308
371
|
<SelectableRow selected={isSelected} indent={1}>
|
|
309
|
-
<span fg={isSelected ? theme.
|
|
372
|
+
<span fg={isSelected ? theme.selection.fg : theme.colors.muted}>
|
|
310
373
|
{arrow}{" "}
|
|
311
374
|
</span>
|
|
312
|
-
<span fg={isSelected ? theme.
|
|
313
|
-
{skillSet.icon}{" "}
|
|
375
|
+
<span fg={isSelected ? theme.selection.fg : theme.colors.text}>
|
|
376
|
+
{iconCell(skillSet.icon)}{" "}
|
|
314
377
|
</span>
|
|
315
|
-
<span fg={isSelected ? theme.
|
|
378
|
+
<span fg={isSelected ? theme.selection.fg : theme.colors.info}>
|
|
316
379
|
<strong>{displayName}</strong>
|
|
317
380
|
</span>
|
|
318
381
|
<span fg={isSelected ? theme.selection.dim : theme.colors.muted}>
|
|
319
|
-
{
|
|
382
|
+
{count}
|
|
320
383
|
</span>
|
|
321
|
-
{
|
|
322
|
-
<MetaText
|
|
323
|
-
text={` ${starsStr}`}
|
|
324
|
-
tone="warning"
|
|
325
|
-
selected={isSelected}
|
|
326
|
-
/>
|
|
384
|
+
{stars ? (
|
|
385
|
+
<MetaText text={stars} tone="info" selected={isSelected} />
|
|
327
386
|
) : null}
|
|
328
387
|
<span> </span>
|
|
329
388
|
<ScopeSquares
|
|
@@ -343,9 +402,9 @@ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
|
|
|
343
402
|
<box flexDirection="column">
|
|
344
403
|
<text fg={theme.colors.info}>
|
|
345
404
|
<strong>
|
|
346
|
-
{skillSet.icon} {skillSet.name}
|
|
405
|
+
{iconCell(skillSet.icon)} {skillSet.name}
|
|
347
406
|
</strong>
|
|
348
|
-
{starsStr ? <span fg={theme.colors.
|
|
407
|
+
{starsStr ? <span fg={theme.colors.info}> {starsStr}</span> : null}
|
|
349
408
|
</text>
|
|
350
409
|
|
|
351
410
|
<box marginTop={1}>
|
|
@@ -355,13 +414,13 @@ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
|
|
|
355
414
|
<DetailSection>
|
|
356
415
|
<text fg={theme.colors.text}>
|
|
357
416
|
<span fg={theme.colors.muted}>Source </span>
|
|
358
|
-
<span fg={theme.colors.
|
|
417
|
+
<span fg={theme.colors.info}>{skillSet.repo}</span>
|
|
359
418
|
</text>
|
|
360
419
|
</DetailSection>
|
|
361
420
|
|
|
362
421
|
{skillSet.loading && (
|
|
363
422
|
<box marginTop={1}>
|
|
364
|
-
<text fg={theme.colors.
|
|
423
|
+
<text fg={theme.colors.muted}>Loading skills…</text>
|
|
365
424
|
</box>
|
|
366
425
|
)}
|
|
367
426
|
|
|
@@ -383,25 +442,11 @@ const skillSetRenderer: ItemRenderer<SkillSetItem> = {
|
|
|
383
442
|
</text>
|
|
384
443
|
<box marginTop={1} flexDirection="column">
|
|
385
444
|
{skillSet.skills.map((s) => (
|
|
386
|
-
<text key={s.id}>
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
: theme.colors.muted
|
|
392
|
-
}
|
|
393
|
-
>
|
|
394
|
-
{s.installedScope === "user" ? "\u25A0" : "\u25A1"}
|
|
395
|
-
</span>
|
|
396
|
-
<span
|
|
397
|
-
fg={
|
|
398
|
-
s.installedScope === "project"
|
|
399
|
-
? theme.colors.success
|
|
400
|
-
: theme.colors.muted
|
|
401
|
-
}
|
|
402
|
-
>
|
|
403
|
-
{s.installedScope === "project" ? "\u25A0" : "\u25A1"}
|
|
404
|
-
</span>
|
|
445
|
+
<text key={s.id} fg={theme.colors.text}>
|
|
446
|
+
<ScopeSquares
|
|
447
|
+
user={s.installedScope === "user"}
|
|
448
|
+
project={s.installedScope === "project"}
|
|
449
|
+
/>
|
|
405
450
|
<span
|
|
406
451
|
fg={s.installed ? theme.colors.text : theme.colors.muted}
|
|
407
452
|
>
|
|
@@ -448,24 +493,26 @@ export const skillRenderers: {
|
|
|
448
493
|
};
|
|
449
494
|
|
|
450
495
|
/**
|
|
451
|
-
* Dispatch rendering by item kind
|
|
496
|
+
* Dispatch rendering by item kind, fitted to `width` — the list panel's
|
|
497
|
+
* usable columns.
|
|
452
498
|
*/
|
|
453
499
|
export function renderSkillRow(
|
|
454
500
|
item: SkillBrowserItem,
|
|
455
|
-
_index: number,
|
|
456
501
|
isSelected: boolean,
|
|
502
|
+
width: number,
|
|
457
503
|
): React.ReactNode {
|
|
458
504
|
if (item.kind === "category") {
|
|
459
|
-
return skillRenderers.category.renderRow({ item, isSelected });
|
|
505
|
+
return skillRenderers.category.renderRow({ item, isSelected, width });
|
|
460
506
|
}
|
|
461
507
|
if (item.kind === "skillset") {
|
|
462
|
-
return skillRenderers.skillset.renderRow({ item, isSelected });
|
|
508
|
+
return skillRenderers.skillset.renderRow({ item, isSelected, width });
|
|
463
509
|
}
|
|
464
|
-
return skillRenderers.skill.renderRow({ item, isSelected });
|
|
510
|
+
return skillRenderers.skill.renderRow({ item, isSelected, width });
|
|
465
511
|
}
|
|
466
512
|
|
|
467
513
|
export function renderSkillDetail(
|
|
468
514
|
item: SkillBrowserItem | undefined,
|
|
515
|
+
state: SkillDetailState = {},
|
|
469
516
|
): React.ReactNode {
|
|
470
517
|
if (!item)
|
|
471
518
|
return <text fg={theme.colors.muted}>Select a skill to see details</text>;
|
|
@@ -475,5 +522,5 @@ export function renderSkillDetail(
|
|
|
475
522
|
if (item.kind === "skillset") {
|
|
476
523
|
return skillRenderers.skillset.renderDetail({ item });
|
|
477
524
|
}
|
|
478
|
-
return
|
|
525
|
+
return skillDetail(item.skill, state);
|
|
479
526
|
}
|
|
@@ -32,13 +32,24 @@ function truncate(text: string, max: number): string {
|
|
|
32
32
|
* selection) are distinct on purpose: the whole point of a staged multi-select
|
|
33
33
|
* is that you can see, before pressing apply, how the pending set differs from
|
|
34
34
|
* what is live. A single glyph for both would hide exactly that.
|
|
35
|
+
*
|
|
36
|
+
* The glyph carries the state; the colour only repeats it. Both pending states
|
|
37
|
+
* — `[+]` will be added, `[~]` will be removed — are unapplied changes, so both
|
|
38
|
+
* are `warning`. `[~]` used to be `danger`, but nothing is broken: it is a
|
|
39
|
+
* change waiting for `a`, exactly like `[+]`.
|
|
40
|
+
*
|
|
41
|
+
* `[–]` is an EN DASH, and must stay one: it is the symbol the help legend
|
|
42
|
+
* explains ("a template; fill it before it can be ticked"). A hyphen drew a
|
|
43
|
+
* glyph the legend did not name.
|
|
35
44
|
*/
|
|
45
|
+
const TEMPLATE_GLYPH = "[–]";
|
|
46
|
+
|
|
36
47
|
function checkbox(item: StyleEntryItem): { glyph: string; tone: string } {
|
|
37
|
-
if (item.disabled) return { glyph:
|
|
48
|
+
if (item.disabled) return { glyph: TEMPLATE_GLYPH, tone: theme.colors.muted };
|
|
38
49
|
if (item.checked && item.applied)
|
|
39
50
|
return { glyph: "[x]", tone: theme.colors.success };
|
|
40
51
|
if (item.checked) return { glyph: "[+]", tone: theme.colors.warning };
|
|
41
|
-
if (item.applied) return { glyph: "[~]", tone: theme.colors.
|
|
52
|
+
if (item.applied) return { glyph: "[~]", tone: theme.colors.warning };
|
|
42
53
|
return { glyph: "[ ]", tone: theme.colors.muted };
|
|
43
54
|
}
|
|
44
55
|
|