claudeup 4.35.0 → 4.36.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 +4 -4
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/scope-squares.test.tsx +165 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/main.tsx +21 -5
- package/src/opentui.d.ts +21 -12
- package/src/services/claude-settings.ts +37 -7
- package/src/services/git-worktree.ts +129 -0
- package/src/services/plugin-manager.ts +10 -1
- package/src/ui/App.tsx +19 -12
- package/src/ui/adapters/pluginsAdapter.ts +174 -168
- package/src/ui/adapters/settingsAdapter.ts +119 -116
- package/src/ui/adapters/skillsAdapter.ts +203 -196
- package/src/ui/components/CategoryHeader.tsx +9 -8
- package/src/ui/components/EmptyFilterState.tsx +10 -5
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +10 -6
- package/src/ui/components/ScrollableList.tsx +3 -2
- package/src/ui/components/SearchInput.tsx +2 -1
- package/src/ui/components/StyledText.tsx +5 -4
- package/src/ui/components/TabBar.tsx +4 -3
- package/src/ui/components/layout/FooterHints.tsx +37 -30
- package/src/ui/components/layout/Panel.tsx +6 -5
- package/src/ui/components/layout/ProgressBar.tsx +7 -6
- package/src/ui/components/layout/ScopeTabs.tsx +6 -5
- package/src/ui/components/layout/ScreenLayout.tsx +27 -24
- package/src/ui/components/layout/index.ts +3 -3
- package/src/ui/components/modals/ConfirmModal.tsx +12 -11
- package/src/ui/components/modals/InputModal.tsx +14 -6
- package/src/ui/components/modals/LoadingModal.tsx +6 -5
- package/src/ui/components/modals/MessageModal.tsx +9 -8
- package/src/ui/components/modals/SelectModal.tsx +11 -7
- package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
- package/src/ui/components/primitives/ActionHints.tsx +26 -26
- package/src/ui/components/primitives/DetailSection.tsx +13 -12
- package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
- package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
- package/src/ui/components/primitives/MetaText.tsx +3 -3
- package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
- package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
- package/src/ui/components/primitives/SelectableRow.tsx +22 -16
- package/src/ui/hooks/useGitignoreModal.ts +78 -74
- package/src/ui/registry.ts +11 -11
- package/src/ui/renderers/cliToolRenderers.tsx +260 -203
- package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
- package/src/ui/renderers/mcpRenderers.tsx +121 -117
- package/src/ui/renderers/pluginRenderers.tsx +528 -471
- package/src/ui/renderers/profileRenderers.tsx +346 -300
- package/src/ui/renderers/settingsRenderers.tsx +183 -176
- package/src/ui/renderers/skillRenderers.tsx +410 -326
- package/src/ui/screens/AliasScreen.tsx +1336 -1309
- package/src/ui/screens/CliToolsScreen.tsx +92 -40
- package/src/ui/screens/EnvVarsScreen.tsx +19 -13
- package/src/ui/screens/GitignoreScreen.tsx +510 -493
- package/src/ui/screens/McpRegistryScreen.tsx +28 -21
- package/src/ui/screens/McpScreen.tsx +12 -3
- package/src/ui/screens/PluginsScreen.tsx +17 -7
- package/src/ui/screens/ProfilesScreen.tsx +39 -23
- package/src/ui/screens/SkillsScreen.tsx +832 -688
- package/src/ui/theme-mode.ts +73 -0
- package/src/ui/theme.ts +147 -53
|
@@ -1,399 +1,453 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import type {
|
|
3
|
-
PluginBrowserItem,
|
|
4
|
-
PluginCategoryItem,
|
|
5
|
-
PluginPluginItem,
|
|
6
|
-
} from "../adapters/pluginsAdapter.js";
|
|
7
|
-
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
8
|
-
import {
|
|
9
|
-
SelectableRow,
|
|
10
|
-
ScopeSquares,
|
|
11
|
-
ActionHints,
|
|
12
|
-
MetaText,
|
|
13
|
-
KeyValueLine,
|
|
14
|
-
DetailSection,
|
|
15
|
-
} from "../components/primitives/index.js";
|
|
16
|
-
import { theme } from "../theme.js";
|
|
17
|
-
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
1
|
+
import type React from "react";
|
|
18
2
|
import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
|
|
19
3
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
4
|
+
isEnabledButNotInstalled,
|
|
5
|
+
isInstalledInScope,
|
|
22
6
|
} from "../../services/plugin-manager.js";
|
|
7
|
+
import type { ScopeStatus } from "../../services/plugin-manager.js";
|
|
23
8
|
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
24
9
|
import type { PluginRelease } from "../../types/index.js";
|
|
25
|
-
import
|
|
10
|
+
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
11
|
+
import type {
|
|
12
|
+
PluginBrowserItem,
|
|
13
|
+
PluginCategoryItem,
|
|
14
|
+
PluginPluginItem,
|
|
15
|
+
} from "../adapters/pluginsAdapter.js";
|
|
16
|
+
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
17
|
+
import {
|
|
18
|
+
ActionHints,
|
|
19
|
+
DetailSection,
|
|
20
|
+
KeyValueLine,
|
|
21
|
+
MetaText,
|
|
22
|
+
ScopeSquares,
|
|
23
|
+
SelectableRow,
|
|
24
|
+
} from "../components/primitives/index.js";
|
|
25
|
+
import { type UiColor, theme } from "../theme.js";
|
|
26
26
|
|
|
27
27
|
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
-
function categoryRow(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
29
|
+
function categoryRow(
|
|
30
|
+
item: PluginCategoryItem,
|
|
31
|
+
isSelected: boolean,
|
|
32
|
+
): React.ReactNode {
|
|
33
|
+
const mp = item.marketplace;
|
|
34
|
+
// Marketplaces that declare a `metadata.version` render "Name v8.0.0 (15)";
|
|
35
|
+
// those without keep the plain "Name (15)" form.
|
|
36
|
+
const version = getMarketplaceVersion(mp.name);
|
|
37
|
+
|
|
38
|
+
if (isSelected) {
|
|
39
|
+
const arrow = item.isExpanded ? "▼" : "▶";
|
|
40
|
+
const versionLabel = version ? ` v${version}` : "";
|
|
41
|
+
const count = item.pluginCount > 0 ? ` (${item.pluginCount})` : "";
|
|
42
|
+
return (
|
|
43
|
+
<SelectableRow selected={true}>
|
|
44
|
+
<strong>
|
|
45
|
+
{" "}
|
|
46
|
+
{arrow} {mp.displayName}
|
|
47
|
+
{versionLabel}
|
|
48
|
+
{count}{" "}
|
|
49
|
+
</strong>
|
|
50
|
+
</SelectableRow>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Map tone to a statusColor for CategoryHeader (legacy component)
|
|
55
|
+
const statusColorMap: Record<PluginCategoryItem["tone"], UiColor> = {
|
|
56
|
+
yellow: theme.colors.warning,
|
|
57
|
+
gray: theme.colors.muted,
|
|
58
|
+
green: theme.colors.success,
|
|
59
|
+
red: theme.colors.danger,
|
|
60
|
+
purple: theme.colors.accent,
|
|
61
|
+
teal: theme.colors.info,
|
|
62
|
+
byline: theme.colors.byline,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<CategoryHeader
|
|
67
|
+
title={mp.displayName}
|
|
68
|
+
version={version}
|
|
69
|
+
expanded={item.isExpanded}
|
|
70
|
+
count={item.pluginCount}
|
|
71
|
+
status={item.badge}
|
|
72
|
+
statusColor={statusColorMap[item.tone]}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
function categoryDetail(
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
item: PluginCategoryItem,
|
|
79
|
+
collapsedMarketplaces: Set<string>,
|
|
77
80
|
): React.ReactNode {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
81
|
+
const mp = item.marketplace;
|
|
82
|
+
const isEnabled = item.marketplaceEnabled;
|
|
83
|
+
const isCollapsed = collapsedMarketplaces.has(mp.name);
|
|
84
|
+
const hasPlugins = item.pluginCount > 0;
|
|
85
|
+
|
|
86
|
+
let actionHint = "Add";
|
|
87
|
+
if (isEnabled) {
|
|
88
|
+
if (isCollapsed) {
|
|
89
|
+
actionHint = "Expand";
|
|
90
|
+
} else if (hasPlugins) {
|
|
91
|
+
actionHint = "Collapse";
|
|
92
|
+
} else {
|
|
93
|
+
actionHint = "Remove";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<box flexDirection="column">
|
|
99
|
+
<text fg={theme.colors.info}>
|
|
100
|
+
<strong>
|
|
101
|
+
{mp.displayName}
|
|
102
|
+
{item.badge ? ` ${item.badge}` : ""}
|
|
103
|
+
</strong>
|
|
104
|
+
</text>
|
|
105
|
+
<text fg={theme.colors.muted}>{mp.description || "No description"}</text>
|
|
106
|
+
<text fg={isEnabled ? theme.colors.success : theme.colors.muted}>
|
|
107
|
+
{isEnabled ? "● Added" : "○ Not added"}
|
|
108
|
+
</text>
|
|
109
|
+
<text fg={theme.colors.link}>github.com/{mp.source.repo}</text>
|
|
110
|
+
<text fg={theme.colors.text}>Plugins: {item.pluginCount}</text>
|
|
111
|
+
<ActionHints
|
|
112
|
+
hints={[
|
|
113
|
+
{
|
|
114
|
+
key: "Enter",
|
|
115
|
+
label: actionHint,
|
|
116
|
+
tone: isEnabled ? "primary" : "default",
|
|
117
|
+
},
|
|
118
|
+
]}
|
|
119
|
+
/>
|
|
120
|
+
{isEnabled ? (
|
|
121
|
+
<box>
|
|
122
|
+
<text fg={theme.colors.muted}>← → to expand/collapse</text>
|
|
123
|
+
</box>
|
|
124
|
+
) : null}
|
|
125
|
+
</box>
|
|
126
|
+
);
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
// ─── Plugin renderers ─────────────────────────────────────────────────────────
|
|
123
130
|
|
|
124
|
-
function pluginRow(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
131
|
+
function pluginRow(
|
|
132
|
+
item: PluginPluginItem,
|
|
133
|
+
isSelected: boolean,
|
|
134
|
+
): React.ReactNode {
|
|
135
|
+
const { plugin } = item;
|
|
136
|
+
const hasUser = !!plugin.userScope?.enabled;
|
|
137
|
+
const hasProject = !!plugin.projectScope?.enabled;
|
|
138
|
+
const hasLocal = !!plugin.localScope?.enabled;
|
|
139
|
+
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
140
|
+
|
|
141
|
+
// Enabled somewhere but nothing actually installed — a broken state that used
|
|
142
|
+
// to render exactly like a healthy install.
|
|
143
|
+
const notInstalled = isEnabledButNotInstalled(plugin);
|
|
144
|
+
|
|
145
|
+
// A known version is one we can actually show. "0.0.0" means installed with an
|
|
146
|
+
// unknown version (every official Anthropic plugin records it) — never render it.
|
|
147
|
+
const shownVersion = isKnownVersion(plugin.installedVersion)
|
|
148
|
+
? plugin.installedVersion
|
|
149
|
+
: undefined;
|
|
150
|
+
|
|
151
|
+
let versionStr = "";
|
|
152
|
+
if (plugin.isOrphaned) {
|
|
153
|
+
// "deprecated" alone reads as "delete me" — but a plugin that merely moved
|
|
154
|
+
// marketplace is still published, and deleting it loses it for no reason.
|
|
155
|
+
versionStr = plugin.movedTo ? ` moved → ${plugin.movedTo}` : " deprecated";
|
|
156
|
+
} else if (notInstalled) {
|
|
157
|
+
versionStr = " not installed";
|
|
158
|
+
} else if (hasAnyScope) {
|
|
159
|
+
// "what just changed" and "what is still available" are independent facts,
|
|
160
|
+
// and both can be true at once: the prerunner can move a plugin 2.9.0→3.0.1
|
|
161
|
+
// while the marketplace already offers 3.3.1. These used to share one
|
|
162
|
+
// if/else chain, so the "updated" badge silently swallowed the pending
|
|
163
|
+
// update — and the update only reappeared on the next refresh, which read
|
|
164
|
+
// as the state spontaneously changing. Render them as separate clauses.
|
|
165
|
+
if (plugin.recentlyUpdatedFrom && shownVersion) {
|
|
166
|
+
// Show what actually happened, not just where it landed — the update may
|
|
167
|
+
// have come from Claude Code or the prerunner without ever telling us.
|
|
168
|
+
versionStr = ` v${plugin.recentlyUpdatedFrom} → v${shownVersion} updated`;
|
|
169
|
+
} else if (plugin.recentlyInstalled) {
|
|
170
|
+
// Newly seen. Version is optional here: a plugin can be genuinely
|
|
171
|
+
// installed with no version we can name.
|
|
172
|
+
versionStr = shownVersion ? ` v${shownVersion} new` : " new";
|
|
173
|
+
} else if (shownVersion) {
|
|
174
|
+
versionStr = ` v${shownVersion}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Pending update, regardless of which badge above applied.
|
|
178
|
+
if (plugin.hasUpdate && plugin.version && versionStr) {
|
|
179
|
+
versionStr += ` → v${plugin.version} available`;
|
|
180
|
+
} else if (plugin.contentStale && versionStr) {
|
|
181
|
+
// Same version, different files. The version compare says "up to date"
|
|
182
|
+
// and is wrong — only a reinstall delivers the current content.
|
|
183
|
+
versionStr += " stale — reinstall";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (isSelected) {
|
|
188
|
+
return (
|
|
189
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
190
|
+
{" "}
|
|
191
|
+
<ScopeSquares
|
|
192
|
+
user={hasUser}
|
|
193
|
+
project={hasProject}
|
|
194
|
+
local={hasLocal}
|
|
195
|
+
selected={true}
|
|
196
|
+
/>{" "}
|
|
197
|
+
{plugin.name}
|
|
198
|
+
{versionStr}{" "}
|
|
199
|
+
</text>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fuzzy highlight
|
|
204
|
+
const segments = item.matches
|
|
205
|
+
? highlightMatches(plugin.name, item.matches)
|
|
206
|
+
: null;
|
|
207
|
+
const displayName = segments
|
|
208
|
+
? segments.map((seg) => seg.text).join("")
|
|
209
|
+
: plugin.name;
|
|
210
|
+
|
|
211
|
+
if (plugin.isOrphaned) {
|
|
212
|
+
const ver = isKnownVersion(plugin.installedVersion)
|
|
213
|
+
? ` v${plugin.installedVersion}`
|
|
214
|
+
: "";
|
|
215
|
+
return (
|
|
216
|
+
<text fg={theme.colors.text}>
|
|
217
|
+
<span fg={theme.colors.danger}> ■■■ </span>
|
|
218
|
+
<span fg={theme.colors.muted}>{displayName}</span>
|
|
219
|
+
{ver ? <span fg={theme.colors.warning}>{ver}</span> : null}
|
|
220
|
+
<span fg={theme.colors.danger}> deprecated</span>
|
|
221
|
+
</text>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<text fg={theme.colors.text}>
|
|
227
|
+
<span> </span>
|
|
228
|
+
<ScopeSquares user={hasUser} project={hasProject} local={hasLocal} />
|
|
229
|
+
<span> </span>
|
|
230
|
+
<span
|
|
231
|
+
fg={
|
|
232
|
+
hasAnyScope && !notInstalled ? theme.colors.text : theme.colors.muted
|
|
233
|
+
}
|
|
234
|
+
>
|
|
235
|
+
{displayName}
|
|
236
|
+
</span>
|
|
237
|
+
<MetaText
|
|
238
|
+
text={versionStr}
|
|
239
|
+
tone={
|
|
240
|
+
plugin.recentlyUpdatedFrom || plugin.recentlyInstalled
|
|
241
|
+
? "success"
|
|
242
|
+
: // "not installed" is a broken state — enabled somewhere with
|
|
243
|
+
// nothing actually installed — while a pending update is
|
|
244
|
+
// routine. They shared `warning` and so read as the same
|
|
245
|
+
// severity; red vs amber is the distinction that matters.
|
|
246
|
+
notInstalled
|
|
247
|
+
? "danger"
|
|
248
|
+
: plugin.hasUpdate
|
|
249
|
+
? "warning"
|
|
250
|
+
: "muted"
|
|
251
|
+
}
|
|
252
|
+
/>
|
|
253
|
+
</text>
|
|
254
|
+
);
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
258
|
+
const { plugin } = item;
|
|
259
|
+
// "Installed" has to mean the same thing here as in the list row. Reading the
|
|
260
|
+
// `enabledPlugins` flag alone made this panel print "● Installed" for the
|
|
261
|
+
// exact plugin the row beside it was labelling "not installed".
|
|
262
|
+
const isInstalled =
|
|
263
|
+
isInstalledInScope(plugin.userScope) ||
|
|
264
|
+
isInstalledInScope(plugin.projectScope) ||
|
|
265
|
+
isInstalledInScope(plugin.localScope);
|
|
266
|
+
const brokenInstall = isEnabledButNotInstalled(plugin);
|
|
267
|
+
|
|
268
|
+
// Orphaned/deprecated plugin
|
|
269
|
+
if (plugin.isOrphaned) {
|
|
270
|
+
return (
|
|
271
|
+
<box flexDirection="column">
|
|
272
|
+
<box justifyContent="center">
|
|
273
|
+
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
274
|
+
<strong> {plugin.name} — DEPRECATED </strong>
|
|
275
|
+
</text>
|
|
276
|
+
</box>
|
|
277
|
+
<box marginTop={1}>
|
|
278
|
+
<text fg={theme.colors.warning}>
|
|
279
|
+
This plugin is no longer in the marketplace.
|
|
280
|
+
</text>
|
|
281
|
+
</box>
|
|
282
|
+
<box marginTop={1}>
|
|
283
|
+
<text fg={theme.colors.muted}>
|
|
284
|
+
It was removed from the marketplace but still referenced in your
|
|
285
|
+
settings. Press d to uninstall and clean up.
|
|
286
|
+
</text>
|
|
287
|
+
</box>
|
|
288
|
+
<ActionHints
|
|
289
|
+
hints={[
|
|
290
|
+
{
|
|
291
|
+
key: "d",
|
|
292
|
+
label: isInstalled
|
|
293
|
+
? "Remove from all scopes"
|
|
294
|
+
: "Clean up stale reference",
|
|
295
|
+
tone: "danger",
|
|
296
|
+
},
|
|
297
|
+
]}
|
|
298
|
+
/>
|
|
299
|
+
</box>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build component counts
|
|
304
|
+
const components: string[] = [];
|
|
305
|
+
if (plugin.agents?.length) components.push(`${plugin.agents.length} agents`);
|
|
306
|
+
if (plugin.commands?.length)
|
|
307
|
+
components.push(`${plugin.commands.length} commands`);
|
|
308
|
+
if (plugin.skills?.length) components.push(`${plugin.skills.length} skills`);
|
|
309
|
+
if (plugin.mcpServers?.length)
|
|
310
|
+
components.push(`${plugin.mcpServers.length} MCP`);
|
|
311
|
+
if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
|
|
312
|
+
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const showVersion = isKnownVersion(plugin.version ?? undefined);
|
|
316
|
+
const showInstalledVersion =
|
|
317
|
+
isInstalled && isKnownVersion(plugin.installedVersion);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<box flexDirection="column">
|
|
321
|
+
{/* Plugin name header */}
|
|
322
|
+
<box justifyContent="center">
|
|
323
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
324
|
+
<strong>
|
|
325
|
+
{" "}
|
|
326
|
+
{plugin.name}
|
|
327
|
+
{isInstalled && plugin.hasUpdate ? " ⬆" : ""}{" "}
|
|
328
|
+
</strong>
|
|
329
|
+
</text>
|
|
330
|
+
</box>
|
|
331
|
+
|
|
332
|
+
{/* Status line */}
|
|
333
|
+
<box marginTop={1}>
|
|
334
|
+
<text
|
|
335
|
+
fg={
|
|
336
|
+
isInstalled
|
|
337
|
+
? theme.colors.success
|
|
338
|
+
: brokenInstall
|
|
339
|
+
? theme.colors.warning
|
|
340
|
+
: theme.colors.muted
|
|
341
|
+
}
|
|
342
|
+
>
|
|
343
|
+
{isInstalled
|
|
344
|
+
? "● Installed"
|
|
345
|
+
: brokenInstall
|
|
346
|
+
? "○ Not installed — enabled in settings, no files on disk"
|
|
347
|
+
: "○ Not installed"}
|
|
348
|
+
</text>
|
|
349
|
+
</box>
|
|
350
|
+
{brokenInstall ? (
|
|
351
|
+
<box>
|
|
352
|
+
<text fg={theme.colors.muted}>
|
|
353
|
+
Claude Code cannot load it in this state. Press the scope key to
|
|
354
|
+
repair.
|
|
355
|
+
</text>
|
|
356
|
+
</box>
|
|
357
|
+
) : null}
|
|
358
|
+
|
|
359
|
+
{/* Description */}
|
|
360
|
+
<box marginTop={1} marginBottom={1}>
|
|
361
|
+
<text fg={theme.colors.text}>{plugin.description}</text>
|
|
362
|
+
</box>
|
|
363
|
+
|
|
364
|
+
{/* Metadata */}
|
|
365
|
+
{showVersion ? (
|
|
366
|
+
<KeyValueLine
|
|
367
|
+
label="Version"
|
|
368
|
+
value={
|
|
369
|
+
<span>
|
|
370
|
+
<span fg={theme.colors.link}>v{plugin.version}</span>
|
|
371
|
+
{showInstalledVersion &&
|
|
372
|
+
plugin.installedVersion !== plugin.version ? (
|
|
373
|
+
<span> (v{plugin.installedVersion} installed)</span>
|
|
374
|
+
) : null}
|
|
375
|
+
</span>
|
|
376
|
+
}
|
|
377
|
+
/>
|
|
378
|
+
) : null}
|
|
379
|
+
{plugin.category ? (
|
|
380
|
+
<KeyValueLine
|
|
381
|
+
label="Category"
|
|
382
|
+
value={<span fg={theme.colors.accent}>{plugin.category}</span>}
|
|
383
|
+
/>
|
|
384
|
+
) : null}
|
|
385
|
+
{plugin.author ? (
|
|
386
|
+
<KeyValueLine
|
|
387
|
+
label="Author"
|
|
388
|
+
value={<span>{plugin.author.name}</span>}
|
|
389
|
+
/>
|
|
390
|
+
) : null}
|
|
391
|
+
{components.length > 0 ? (
|
|
392
|
+
<KeyValueLine
|
|
393
|
+
label="Contains"
|
|
394
|
+
value={
|
|
395
|
+
<span fg={theme.colors.warning}>{components.join(" · ")}</span>
|
|
396
|
+
}
|
|
397
|
+
/>
|
|
398
|
+
) : null}
|
|
399
|
+
|
|
400
|
+
{/* Scope Status */}
|
|
401
|
+
<DetailSection>
|
|
402
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
403
|
+
<text fg={theme.colors.text}>
|
|
404
|
+
<strong>Scopes:</strong>
|
|
405
|
+
</text>
|
|
406
|
+
<box marginTop={1} flexDirection="column">
|
|
407
|
+
<ScopeLine
|
|
408
|
+
keyHint="u"
|
|
409
|
+
name="User"
|
|
410
|
+
qualifier="global"
|
|
411
|
+
color={theme.scopes.user}
|
|
412
|
+
scope={plugin.userScope}
|
|
413
|
+
/>
|
|
414
|
+
<ScopeLine
|
|
415
|
+
keyHint="p"
|
|
416
|
+
name="Project"
|
|
417
|
+
qualifier="team"
|
|
418
|
+
color={theme.scopes.project}
|
|
419
|
+
scope={plugin.projectScope}
|
|
420
|
+
/>
|
|
421
|
+
<ScopeLine
|
|
422
|
+
keyHint="l"
|
|
423
|
+
name="Local"
|
|
424
|
+
qualifier="private"
|
|
425
|
+
color={theme.scopes.local}
|
|
426
|
+
scope={plugin.localScope}
|
|
427
|
+
/>
|
|
428
|
+
</box>
|
|
429
|
+
</DetailSection>
|
|
430
|
+
|
|
431
|
+
{/* Recent releases */}
|
|
432
|
+
<ReleaseHistory
|
|
433
|
+
releases={plugin.releases}
|
|
434
|
+
installedVersion={plugin.installedVersion}
|
|
435
|
+
/>
|
|
436
|
+
|
|
437
|
+
{/* Update action */}
|
|
438
|
+
{isInstalled && plugin.hasUpdate ? (
|
|
439
|
+
<ActionHints
|
|
440
|
+
hints={[
|
|
441
|
+
{
|
|
442
|
+
key: "U",
|
|
443
|
+
label: `Update to v${plugin.version}`,
|
|
444
|
+
tone: "primary",
|
|
445
|
+
},
|
|
446
|
+
]}
|
|
447
|
+
/>
|
|
448
|
+
) : null}
|
|
449
|
+
</box>
|
|
450
|
+
);
|
|
397
451
|
}
|
|
398
452
|
|
|
399
453
|
/**
|
|
@@ -405,36 +459,36 @@ function pluginDetail(item: PluginPluginItem): React.ReactNode {
|
|
|
405
459
|
* different actions from the user and used to look identical.
|
|
406
460
|
*/
|
|
407
461
|
function ScopeLine({
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
462
|
+
keyHint,
|
|
463
|
+
name,
|
|
464
|
+
qualifier,
|
|
465
|
+
color,
|
|
466
|
+
scope,
|
|
413
467
|
}: {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
468
|
+
keyHint: string;
|
|
469
|
+
name: string;
|
|
470
|
+
qualifier: string;
|
|
471
|
+
color: UiColor;
|
|
472
|
+
scope?: ScopeStatus;
|
|
419
473
|
}): React.ReactNode {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
474
|
+
const installed = isInstalledInScope(scope);
|
|
475
|
+
return (
|
|
476
|
+
<text fg={theme.colors.text}>
|
|
477
|
+
<span bg={color} fg={theme.hints.fg}>
|
|
478
|
+
{" "}
|
|
479
|
+
{keyHint}{" "}
|
|
480
|
+
</span>
|
|
481
|
+
<span fg={installed ? color : theme.colors.muted}>
|
|
482
|
+
{installed ? " ● " : " ○ "}
|
|
483
|
+
</span>
|
|
484
|
+
<span fg={color}>{name}</span>
|
|
485
|
+
<span> {qualifier}</span>
|
|
486
|
+
{scope?.version ? <span fg={color}> v{scope.version}</span> : null}
|
|
487
|
+
{!installed && scope?.enabled ? (
|
|
488
|
+
<span fg={theme.colors.warning}> enabled, not installed</span>
|
|
489
|
+
) : null}
|
|
490
|
+
</text>
|
|
491
|
+
);
|
|
438
492
|
}
|
|
439
493
|
|
|
440
494
|
/**
|
|
@@ -448,60 +502,63 @@ function ScopeLine({
|
|
|
448
502
|
* releases published), everything renders neutrally rather than guessing.
|
|
449
503
|
*/
|
|
450
504
|
function ReleaseHistory({
|
|
451
|
-
|
|
452
|
-
|
|
505
|
+
releases,
|
|
506
|
+
installedVersion,
|
|
453
507
|
}: {
|
|
454
|
-
|
|
455
|
-
|
|
508
|
+
releases?: PluginRelease[];
|
|
509
|
+
installedVersion?: string;
|
|
456
510
|
}): React.ReactNode {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
511
|
+
if (!releases?.length) return null;
|
|
512
|
+
|
|
513
|
+
const installedAt = installedVersion
|
|
514
|
+
? releases.findIndex((r) => r.version === installedVersion)
|
|
515
|
+
: -1;
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<DetailSection>
|
|
519
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
520
|
+
<text fg={theme.colors.text}>
|
|
521
|
+
<strong>Recent releases:</strong>
|
|
522
|
+
</text>
|
|
523
|
+
<box marginTop={1} flexDirection="column">
|
|
524
|
+
{releases.map((release, i) => {
|
|
525
|
+
const isNewer = installedAt > 0 && i < installedAt;
|
|
526
|
+
const isInstalled = installedAt >= 0 && i === installedAt;
|
|
527
|
+
const versionColor = isNewer
|
|
528
|
+
? theme.colors.warning
|
|
529
|
+
: isInstalled
|
|
530
|
+
? theme.colors.success
|
|
531
|
+
: theme.colors.link;
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<box key={release.version} flexDirection="column" marginBottom={1}>
|
|
535
|
+
<text fg={theme.colors.text}>
|
|
536
|
+
<span fg={versionColor}>
|
|
537
|
+
{isNewer ? "▲ " : isInstalled ? "● " : " "}v{release.version}
|
|
538
|
+
</span>
|
|
539
|
+
{release.date ? (
|
|
540
|
+
<span fg={theme.colors.muted}> {release.date}</span>
|
|
541
|
+
) : null}
|
|
542
|
+
{release.kinds?.length ? (
|
|
543
|
+
<span fg={theme.colors.dim}>
|
|
544
|
+
{" "}
|
|
545
|
+
{release.kinds.join(" · ")}
|
|
546
|
+
</span>
|
|
547
|
+
) : null}
|
|
548
|
+
{isInstalled ? (
|
|
549
|
+
<span fg={theme.colors.success}> installed</span>
|
|
550
|
+
) : null}
|
|
551
|
+
</text>
|
|
552
|
+
<text fg={isNewer ? theme.colors.text : theme.colors.muted}>
|
|
553
|
+
{" "}
|
|
554
|
+
{release.summary}
|
|
555
|
+
</text>
|
|
556
|
+
</box>
|
|
557
|
+
);
|
|
558
|
+
})}
|
|
559
|
+
</box>
|
|
560
|
+
</DetailSection>
|
|
561
|
+
);
|
|
505
562
|
}
|
|
506
563
|
|
|
507
564
|
// ─── Public dispatch functions ────────────────────────────────────────────────
|
|
@@ -510,26 +567,26 @@ function ReleaseHistory({
|
|
|
510
567
|
* Render a plugin browser list row by item kind.
|
|
511
568
|
*/
|
|
512
569
|
export function renderPluginRow(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
570
|
+
item: PluginBrowserItem,
|
|
571
|
+
_index: number,
|
|
572
|
+
isSelected: boolean,
|
|
516
573
|
): React.ReactNode {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
574
|
+
if (item.kind === "category") {
|
|
575
|
+
return categoryRow(item, isSelected);
|
|
576
|
+
}
|
|
577
|
+
return pluginRow(item, isSelected);
|
|
521
578
|
}
|
|
522
579
|
|
|
523
580
|
/**
|
|
524
581
|
* Render the detail panel for a plugin browser item.
|
|
525
582
|
*/
|
|
526
583
|
export function renderPluginDetail(
|
|
527
|
-
|
|
528
|
-
|
|
584
|
+
item: PluginBrowserItem | undefined,
|
|
585
|
+
collapsedMarketplaces: Set<string>,
|
|
529
586
|
): React.ReactNode {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
587
|
+
if (!item) return <text fg={theme.colors.muted}>Select an item</text>;
|
|
588
|
+
if (item.kind === "category") {
|
|
589
|
+
return categoryDetail(item, collapsedMarketplaces);
|
|
590
|
+
}
|
|
591
|
+
return pluginDetail(item);
|
|
535
592
|
}
|