claudeup 4.35.1 → 4.37.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__/catalog-cache-store.test.ts +271 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/github-budget.test.ts +200 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +200 -8
- 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/main.tsx +21 -5
- package/src/opentui.d.ts +21 -12
- package/src/services/catalog-cache-store.ts +218 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/marketplace-catalog-git.ts +170 -0
- package/src/services/marketplace-catalog.ts +95 -0
- package/src/services/marketplace-fetcher.ts +310 -87
- package/src/services/plugin-manager.ts +103 -92
- package/src/ui/App.tsx +19 -12
- package/src/ui/adapters/catalogNotice.ts +122 -0
- 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 +46 -26
- 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 +566 -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 +152 -33
- package/src/ui/screens/ProfilesScreen.tsx +39 -23
- package/src/ui/screens/SkillsScreen.tsx +832 -688
- package/src/ui/state/reducer.ts +11 -2
- package/src/ui/state/types.ts +16 -1
- package/src/ui/theme-mode.ts +73 -0
- package/src/ui/theme.ts +147 -53
- package/src/utils/config-dir.ts +47 -0
|
@@ -8,6 +8,7 @@ import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
|
|
|
8
8
|
import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
|
|
9
9
|
import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
|
|
10
10
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
11
|
+
import { theme } from "../theme.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Deduplicate servers by name, keeping only the latest version.
|
|
@@ -182,7 +183,9 @@ export function McpRegistryScreen() {
|
|
|
182
183
|
dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
|
|
183
184
|
} else if (event.name === "l") {
|
|
184
185
|
navigateToScreen("mcp");
|
|
185
|
-
} else if (event.name === "
|
|
186
|
+
} else if (event.name === "r" && event.shift) {
|
|
187
|
+
// `name` is always the unshifted key, so `=== "R"` never matched and
|
|
188
|
+
// refresh silently did nothing.
|
|
186
189
|
loadServers(searchQuery);
|
|
187
190
|
} else if (event.name === "enter") {
|
|
188
191
|
handleInstall();
|
|
@@ -219,15 +222,17 @@ export function McpRegistryScreen() {
|
|
|
219
222
|
|
|
220
223
|
const renderDetail = () => {
|
|
221
224
|
if (isLoading) {
|
|
222
|
-
return <text fg=
|
|
225
|
+
return <text fg={theme.colors.muted}>Loading...</text>;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
if (error) {
|
|
226
|
-
return <text fg=
|
|
229
|
+
return <text fg={theme.colors.danger}>Error: {error}</text>;
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
if (!selectedServer) {
|
|
230
|
-
return
|
|
233
|
+
return (
|
|
234
|
+
<text fg={theme.colors.muted}>Select a server to see details</text>
|
|
235
|
+
);
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
const dateDisplay = selectedServer.published_at
|
|
@@ -240,40 +245,42 @@ export function McpRegistryScreen() {
|
|
|
240
245
|
|
|
241
246
|
return (
|
|
242
247
|
<box flexDirection="column">
|
|
243
|
-
<text fg=
|
|
248
|
+
<text fg={theme.colors.accent}>
|
|
244
249
|
<strong>{selectedServer.name}</strong>
|
|
245
250
|
</text>
|
|
246
251
|
<box marginTop={1}>
|
|
247
|
-
<text>{selectedServer.short_description}</text>
|
|
252
|
+
<text fg={theme.colors.text}>{selectedServer.short_description}</text>
|
|
248
253
|
</box>
|
|
249
254
|
<box marginTop={1}>
|
|
250
|
-
<text>
|
|
255
|
+
<text fg={theme.colors.text}>
|
|
251
256
|
<strong>Version: </strong>
|
|
252
257
|
</text>
|
|
253
|
-
<text fg=
|
|
258
|
+
<text fg={theme.colors.success}>{versionDisplay}</text>
|
|
254
259
|
</box>
|
|
255
260
|
<box>
|
|
256
|
-
<text>
|
|
261
|
+
<text fg={theme.colors.text}>
|
|
257
262
|
<strong>Published: </strong>
|
|
258
263
|
</text>
|
|
259
|
-
<text fg=
|
|
264
|
+
<text fg={theme.colors.info}>{dateDisplay}</text>
|
|
260
265
|
</box>
|
|
261
266
|
<box marginTop={1} flexDirection="column">
|
|
262
|
-
<text>
|
|
267
|
+
<text fg={theme.colors.text}>
|
|
263
268
|
<strong>URL:</strong>
|
|
264
269
|
</text>
|
|
265
|
-
<text fg=
|
|
270
|
+
<text fg={theme.colors.info}>{selectedServer.url}</text>
|
|
266
271
|
</box>
|
|
267
272
|
{selectedServer.source_code_url && (
|
|
268
273
|
<box marginTop={1} flexDirection="column">
|
|
269
|
-
<text>
|
|
274
|
+
<text fg={theme.colors.text}>
|
|
270
275
|
<strong>Source:</strong>
|
|
271
276
|
</text>
|
|
272
|
-
<text fg=
|
|
277
|
+
<text fg={theme.colors.muted}>
|
|
278
|
+
{selectedServer.source_code_url}
|
|
279
|
+
</text>
|
|
273
280
|
</box>
|
|
274
281
|
)}
|
|
275
282
|
<box marginTop={1}>
|
|
276
|
-
<text fg=
|
|
283
|
+
<text fg={theme.colors.success}>Press Enter to install</text>
|
|
277
284
|
</box>
|
|
278
285
|
</box>
|
|
279
286
|
);
|
|
@@ -286,17 +293,17 @@ export function McpRegistryScreen() {
|
|
|
286
293
|
) => {
|
|
287
294
|
const version = server.version ? ` v${server.version}` : "";
|
|
288
295
|
return isSelected ? (
|
|
289
|
-
<text bg=
|
|
296
|
+
<text bg={theme.colors.accent} fg={theme.hints.fg}>
|
|
290
297
|
{" "}
|
|
291
298
|
{server.name}
|
|
292
299
|
{version}{" "}
|
|
293
300
|
</text>
|
|
294
301
|
) : (
|
|
295
|
-
<text>
|
|
302
|
+
<text fg={theme.colors.text}>
|
|
296
303
|
<span>
|
|
297
304
|
<strong>{server.name}</strong>
|
|
298
305
|
</span>
|
|
299
|
-
<span fg=
|
|
306
|
+
<span fg={theme.colors.success}>{version}</span>
|
|
300
307
|
</text>
|
|
301
308
|
);
|
|
302
309
|
};
|
|
@@ -333,11 +340,11 @@ export function McpRegistryScreen() {
|
|
|
333
340
|
footerHints={footerHints}
|
|
334
341
|
listPanel={
|
|
335
342
|
isLoading ? (
|
|
336
|
-
<text fg=
|
|
343
|
+
<text fg={theme.colors.muted}>Loading...</text>
|
|
337
344
|
) : error ? (
|
|
338
|
-
<text fg=
|
|
345
|
+
<text fg={theme.colors.danger}>Error: {error}</text>
|
|
339
346
|
) : servers.length === 0 ? (
|
|
340
|
-
<text fg=
|
|
347
|
+
<text fg={theme.colors.muted}>No servers found</text>
|
|
341
348
|
) : (
|
|
342
349
|
<ScrollableList
|
|
343
350
|
items={servers}
|
|
@@ -130,7 +130,11 @@ export function McpScreen() {
|
|
|
130
130
|
try {
|
|
131
131
|
await removeMcpServer(server.name, state.projectPath);
|
|
132
132
|
modal.hideModal();
|
|
133
|
-
await modal.message(
|
|
133
|
+
await modal.message(
|
|
134
|
+
"Removed",
|
|
135
|
+
`${server.name} has been removed.`,
|
|
136
|
+
"success",
|
|
137
|
+
);
|
|
134
138
|
fetchData();
|
|
135
139
|
} catch (error) {
|
|
136
140
|
modal.hideModal();
|
|
@@ -146,7 +150,8 @@ export function McpScreen() {
|
|
|
146
150
|
let config: McpServerConfig;
|
|
147
151
|
|
|
148
152
|
if (server.type === "http") {
|
|
149
|
-
if (!server.url)
|
|
153
|
+
if (!server.url)
|
|
154
|
+
throw new Error(`${server.name} is missing its HTTP URL`);
|
|
150
155
|
config = { type: "http", url: server.url };
|
|
151
156
|
} else {
|
|
152
157
|
if (!server.command) {
|
|
@@ -184,7 +189,11 @@ export function McpScreen() {
|
|
|
184
189
|
if (value === null) return;
|
|
185
190
|
|
|
186
191
|
if (field.required && !value) {
|
|
187
|
-
await modal.message(
|
|
192
|
+
await modal.message(
|
|
193
|
+
"Required Field",
|
|
194
|
+
`${field.label} is required.`,
|
|
195
|
+
"error",
|
|
196
|
+
);
|
|
188
197
|
return;
|
|
189
198
|
}
|
|
190
199
|
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import React, { useEffect, useCallback, useMemo } from "react";
|
|
1
|
+
import React, { useEffect, useCallback, useMemo, useState } from "react";
|
|
2
2
|
import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
3
|
+
import { asyncValue } from "../state/types.js";
|
|
3
4
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
4
5
|
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
5
6
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
6
7
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
7
8
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
8
9
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
defaultMarketplaces,
|
|
12
|
+
getAllMarketplaces,
|
|
13
|
+
} from "../../data/marketplaces.js";
|
|
10
14
|
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
11
15
|
import {
|
|
12
16
|
diffVersions,
|
|
@@ -15,12 +19,14 @@ import {
|
|
|
15
19
|
} from "../../services/version-snapshot.js";
|
|
16
20
|
import {
|
|
17
21
|
getAvailablePlugins,
|
|
22
|
+
getMarketplaceFetchFailures,
|
|
18
23
|
refreshAllMarketplaces,
|
|
19
24
|
clearMarketplaceCache,
|
|
20
25
|
getLocalMarketplacesInfo,
|
|
21
26
|
isInstalledInScope,
|
|
22
27
|
resolveScopeAction,
|
|
23
28
|
saveInstalledPluginVersion,
|
|
29
|
+
type MarketplaceFetchFailure,
|
|
24
30
|
type PluginInfo,
|
|
25
31
|
type ScopeStatus,
|
|
26
32
|
} from "../../services/plugin-manager.js";
|
|
@@ -63,6 +69,17 @@ import {
|
|
|
63
69
|
renderPluginRow,
|
|
64
70
|
renderPluginDetail,
|
|
65
71
|
} from "../renderers/pluginRenderers.js";
|
|
72
|
+
import { buildCatalogNoticeText } from "../adapters/catalogNotice.js";
|
|
73
|
+
import { theme } from "../theme.js";
|
|
74
|
+
|
|
75
|
+
/** Wraps the notice text in the warning-coloured row ScreenLayout renders. */
|
|
76
|
+
function buildCatalogNotice(
|
|
77
|
+
input: Parameters<typeof buildCatalogNoticeText>[0],
|
|
78
|
+
): React.ReactNode {
|
|
79
|
+
const text = buildCatalogNoticeText(input);
|
|
80
|
+
if (!text) return null;
|
|
81
|
+
return <text fg={theme.colors.warning}>{text}</text>;
|
|
82
|
+
}
|
|
66
83
|
|
|
67
84
|
export function PluginsScreen() {
|
|
68
85
|
const { state, dispatch } = useApp();
|
|
@@ -77,6 +94,20 @@ export function PluginsScreen() {
|
|
|
77
94
|
state.currentRoute.screen === "plugins" &&
|
|
78
95
|
!state.modal;
|
|
79
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Catalogs that could not be read from any authoritative source.
|
|
99
|
+
*
|
|
100
|
+
* Held in state rather than derived at render time because it is an answer
|
|
101
|
+
* from the last fetch and must survive until the next one — a failure the user
|
|
102
|
+
* cannot see is the bug this screen shipped with.
|
|
103
|
+
*/
|
|
104
|
+
const [catalogFailures, setCatalogFailures] = useState<
|
|
105
|
+
MarketplaceFetchFailure[]
|
|
106
|
+
>([]);
|
|
107
|
+
|
|
108
|
+
/** Re-render tick so the retry countdown in the notice actually counts down. */
|
|
109
|
+
const [nowTick, setNowTick] = useState(() => Date.now());
|
|
110
|
+
|
|
80
111
|
// Fetch data (always fetches all scopes)
|
|
81
112
|
const fetchData = useCallback(async () => {
|
|
82
113
|
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
@@ -85,6 +116,24 @@ export function PluginsScreen() {
|
|
|
85
116
|
const allMarketplaces = getAllMarketplaces(localMarketplaces);
|
|
86
117
|
const pluginData = await getAvailablePlugins(state.projectPath);
|
|
87
118
|
|
|
119
|
+
// Which catalogs answered, and which only appeared to. Read straight
|
|
120
|
+
// after the fetch, while the record still describes this attempt.
|
|
121
|
+
//
|
|
122
|
+
// Only failures that survived every authoritative source are worth
|
|
123
|
+
// reporting: a rate-limited HTTP fetch that the git remote ref then
|
|
124
|
+
// answered is a non-event for the user, and reporting it would fire a
|
|
125
|
+
// warning about intended, working behaviour.
|
|
126
|
+
const unresolved = new Set(
|
|
127
|
+
pluginData
|
|
128
|
+
.filter((p) => p.updateCheckFailed)
|
|
129
|
+
.map((p) => p.marketplace),
|
|
130
|
+
);
|
|
131
|
+
setCatalogFailures(
|
|
132
|
+
getMarketplaceFetchFailures().filter((f) =>
|
|
133
|
+
unresolved.has(f.marketplace),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
88
137
|
// Surface version changes made outside claudeup (Claude Code's own
|
|
89
138
|
// plugin update, the prerunner, a manual CLI call). Diffing against
|
|
90
139
|
// what we last rendered catches all of them; logging only what
|
|
@@ -127,24 +176,60 @@ export function PluginsScreen() {
|
|
|
127
176
|
fetchData();
|
|
128
177
|
}, [fetchData, state.dataRefreshVersion]);
|
|
129
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Tick the countdown, and refetch by itself once the rate-limit window reopens.
|
|
181
|
+
*
|
|
182
|
+
* The user asked "when can I try again" — so the screen answers it and then
|
|
183
|
+
* acts on it. Leaving them to press `r` and guess is what makes a rate limit
|
|
184
|
+
* feel like a dead end, and a mistimed guess spends another request against the
|
|
185
|
+
* limit for nothing.
|
|
186
|
+
*
|
|
187
|
+
* Only armed while a retry is actually pending, so an untroubled screen runs no
|
|
188
|
+
* timer at all.
|
|
189
|
+
*/
|
|
190
|
+
const nextRetryAt = useMemo(() => {
|
|
191
|
+
const times = catalogFailures
|
|
192
|
+
.map((f) => f.retryAt)
|
|
193
|
+
.filter((t): t is number => typeof t === "number");
|
|
194
|
+
return times.length > 0 ? Math.min(...times) : undefined;
|
|
195
|
+
}, [catalogFailures]);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (nextRetryAt === undefined) return;
|
|
199
|
+
|
|
200
|
+
const tick = setInterval(() => {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
setNowTick(now);
|
|
203
|
+
if (now >= nextRetryAt) {
|
|
204
|
+
clearInterval(tick);
|
|
205
|
+
// The cooldown has expired, so the fetcher will really try again
|
|
206
|
+
// rather than short-circuit on its own negative cache.
|
|
207
|
+
clearMarketplaceCache();
|
|
208
|
+
fetchData();
|
|
209
|
+
}
|
|
210
|
+
}, 1000);
|
|
211
|
+
|
|
212
|
+
return () => clearInterval(tick);
|
|
213
|
+
}, [nextRetryAt, fetchData]);
|
|
214
|
+
|
|
215
|
+
// The data to render: fresh when loaded, else the previous list while a
|
|
216
|
+
// reload is in flight. Reading `status === "success"` alone is what made a
|
|
217
|
+
// tab switch blank the screen — the router remounts this component, which
|
|
218
|
+
// refetches, and every intermediate frame had nothing to show.
|
|
219
|
+
const shownMarketplaces = asyncValue(pluginsState.marketplaces);
|
|
220
|
+
const shownPlugins = asyncValue(pluginsState.plugins);
|
|
221
|
+
const isReloading =
|
|
222
|
+
pluginsState.plugins.status === "loading" && shownPlugins !== undefined;
|
|
223
|
+
|
|
130
224
|
// Build list items (categories + plugins)
|
|
131
225
|
const allItems = useMemo((): PluginBrowserItem[] => {
|
|
132
|
-
if (
|
|
133
|
-
pluginsState.marketplaces.status !== "success" ||
|
|
134
|
-
pluginsState.plugins.status !== "success"
|
|
135
|
-
) {
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
226
|
+
if (!shownMarketplaces || !shownPlugins) return [];
|
|
138
227
|
return buildPluginBrowserItems({
|
|
139
|
-
marketplaces:
|
|
140
|
-
plugins:
|
|
228
|
+
marketplaces: shownMarketplaces,
|
|
229
|
+
plugins: shownPlugins,
|
|
141
230
|
collapsedMarketplaces: pluginsState.collapsedMarketplaces,
|
|
142
231
|
});
|
|
143
|
-
}, [
|
|
144
|
-
pluginsState.marketplaces,
|
|
145
|
-
pluginsState.plugins,
|
|
146
|
-
pluginsState.collapsedMarketplaces,
|
|
147
|
-
]);
|
|
232
|
+
}, [shownMarketplaces, shownPlugins, pluginsState.collapsedMarketplaces]);
|
|
148
233
|
|
|
149
234
|
// Filter items by search query
|
|
150
235
|
const filteredItems = useMemo(() => {
|
|
@@ -297,11 +382,15 @@ export function PluginsScreen() {
|
|
|
297
382
|
if (event.name === "r") handleRefresh();
|
|
298
383
|
else if (event.name === "n") handleShowAddMarketplaceInstructions();
|
|
299
384
|
else if (event.name === "t") handleShowTeamConfigHelp();
|
|
385
|
+
// `name` is ALWAYS the unshifted key — Shift+U parses as
|
|
386
|
+
// {name:"u", shift:true, sequence:"U"}, so `name === "U"` can never be
|
|
387
|
+
// true. It sat below the plain "u" branch, so pressing Shift+U silently
|
|
388
|
+
// installed at user scope instead of updating.
|
|
389
|
+
else if (event.name === "u" && event.shift) handleUpdate();
|
|
300
390
|
else if (event.name === "u") handleScopeToggle("user");
|
|
301
391
|
else if (event.name === "p") handleScopeToggle("project");
|
|
302
392
|
else if (event.name === "l") handleScopeToggle("local");
|
|
303
393
|
else if (event.name === "d") handleRemoveDeprecated();
|
|
304
|
-
else if (event.name === "U") handleUpdate();
|
|
305
394
|
else if (event.name === "a") handleUpdateAll();
|
|
306
395
|
else if (event.name === "m") handleCheckMismatches();
|
|
307
396
|
else if (event.name === "s") handleSaveAsProfile();
|
|
@@ -993,7 +1082,9 @@ export function PluginsScreen() {
|
|
|
993
1082
|
// Reinstall under the new namespace in every scope it occupied, so
|
|
994
1083
|
// the migration does not quietly change where the plugin is enabled.
|
|
995
1084
|
const newId = `${plugin.name}@${migrateTo}`;
|
|
996
|
-
for (const scope of removedFrom.length > 0
|
|
1085
|
+
for (const scope of removedFrom.length > 0
|
|
1086
|
+
? removedFrom
|
|
1087
|
+
: ["project" as PluginScope]) {
|
|
997
1088
|
modal.loading(
|
|
998
1089
|
`Installing ${newId} (${scope})…\nclaude plugin install ${newId} --scope ${scope}`,
|
|
999
1090
|
);
|
|
@@ -1051,30 +1142,47 @@ export function PluginsScreen() {
|
|
|
1051
1142
|
|
|
1052
1143
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
1053
1144
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1145
|
+
// Only a genuine first load takes over the screen. A reload keeps the list on
|
|
1146
|
+
// screen and says so in the notice line instead.
|
|
1147
|
+
if (!shownMarketplaces || !shownPlugins) {
|
|
1148
|
+
if (
|
|
1149
|
+
pluginsState.marketplaces.status === "loading" ||
|
|
1150
|
+
pluginsState.plugins.status === "loading"
|
|
1151
|
+
) {
|
|
1152
|
+
return (
|
|
1153
|
+
<box flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
1154
|
+
<text fg={theme.colors.accent}>
|
|
1155
|
+
<strong>claudeup Plugins</strong>
|
|
1156
|
+
</text>
|
|
1157
|
+
<text fg={theme.colors.muted}>
|
|
1158
|
+
Loading plugin catalogs from GitHub…
|
|
1159
|
+
</text>
|
|
1160
|
+
</box>
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1066
1163
|
}
|
|
1067
1164
|
|
|
1068
1165
|
if (
|
|
1069
1166
|
pluginsState.marketplaces.status === "error" ||
|
|
1070
1167
|
pluginsState.plugins.status === "error"
|
|
1071
1168
|
) {
|
|
1169
|
+
// Name the error. "Error loading data" told the user nothing and sent them
|
|
1170
|
+
// to the debug pane to find out what claudeup already knew.
|
|
1171
|
+
const failure =
|
|
1172
|
+
pluginsState.plugins.status === "error"
|
|
1173
|
+
? pluginsState.plugins.error
|
|
1174
|
+
: pluginsState.marketplaces.status === "error"
|
|
1175
|
+
? pluginsState.marketplaces.error
|
|
1176
|
+
: undefined;
|
|
1072
1177
|
return (
|
|
1073
1178
|
<box flexDirection="column" paddingLeft={1} paddingRight={1}>
|
|
1074
|
-
<text fg=
|
|
1179
|
+
<text fg={theme.colors.accent}>
|
|
1075
1180
|
<strong>claudeup Plugins</strong>
|
|
1076
1181
|
</text>
|
|
1077
|
-
<text fg=
|
|
1182
|
+
<text fg={theme.colors.danger}>
|
|
1183
|
+
Could not load plugins: {failure?.message ?? "unknown error"}
|
|
1184
|
+
</text>
|
|
1185
|
+
<text fg={theme.colors.muted}>Press r to retry.</text>
|
|
1078
1186
|
</box>
|
|
1079
1187
|
);
|
|
1080
1188
|
}
|
|
@@ -1097,8 +1205,7 @@ export function PluginsScreen() {
|
|
|
1097
1205
|
];
|
|
1098
1206
|
|
|
1099
1207
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
1100
|
-
const plugins: PluginInfo[] =
|
|
1101
|
-
pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
|
|
1208
|
+
const plugins: PluginInfo[] = shownPlugins ?? [];
|
|
1102
1209
|
const installedCount = plugins.filter((p) => p.enabled).length;
|
|
1103
1210
|
const updateCount = plugins.filter(
|
|
1104
1211
|
(p) =>
|
|
@@ -1110,10 +1217,22 @@ export function PluginsScreen() {
|
|
|
1110
1217
|
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}`;
|
|
1111
1218
|
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""} │ / to search`;
|
|
1112
1219
|
|
|
1220
|
+
const notice = isReloading ? (
|
|
1221
|
+
<text fg={theme.colors.muted}>⟳ Refreshing catalogs from GitHub…</text>
|
|
1222
|
+
) : (
|
|
1223
|
+
buildCatalogNotice({
|
|
1224
|
+
failures: catalogFailures,
|
|
1225
|
+
unverifiedPlugins: plugins.filter((p) => p.updateCheckFailed).length,
|
|
1226
|
+
width: dimensions.terminalWidth,
|
|
1227
|
+
now: nowTick,
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1113
1231
|
return (
|
|
1114
1232
|
<ScreenLayout
|
|
1115
1233
|
title="claudeup Plugins"
|
|
1116
1234
|
subtitle={subtitle}
|
|
1235
|
+
notice={notice}
|
|
1117
1236
|
currentScreen="plugins"
|
|
1118
1237
|
search={{
|
|
1119
1238
|
isActive: isSearchActive,
|
|
@@ -12,10 +12,7 @@ import {
|
|
|
12
12
|
exportProfileToJson,
|
|
13
13
|
importProfileFromJson,
|
|
14
14
|
} from "../../services/profiles.js";
|
|
15
|
-
import {
|
|
16
|
-
readSettings,
|
|
17
|
-
writeSettings,
|
|
18
|
-
} from "../../services/claude-settings.js";
|
|
15
|
+
import { readSettings, writeSettings } from "../../services/claude-settings.js";
|
|
19
16
|
import {
|
|
20
17
|
writeClipboard,
|
|
21
18
|
readClipboard,
|
|
@@ -32,6 +29,7 @@ import {
|
|
|
32
29
|
type ProfileListItem,
|
|
33
30
|
} from "../renderers/profileRenderers.js";
|
|
34
31
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
32
|
+
import { theme } from "../theme.js";
|
|
35
33
|
|
|
36
34
|
export function ProfilesScreen() {
|
|
37
35
|
const { state, dispatch } = useApp();
|
|
@@ -111,9 +109,7 @@ export function ProfilesScreen() {
|
|
|
111
109
|
const handleApplyPredefined = async (profile: PredefinedProfile) => {
|
|
112
110
|
const allPlugins = [
|
|
113
111
|
...profile.magusPlugins.map((p) => `${p}@magus`),
|
|
114
|
-
...profile.anthropicPlugins.map(
|
|
115
|
-
(p) => `${p}@claude-plugins-official`,
|
|
116
|
-
),
|
|
112
|
+
...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
|
|
117
113
|
];
|
|
118
114
|
const settingsCount = Object.keys(profile.settings).length;
|
|
119
115
|
|
|
@@ -161,7 +157,11 @@ export function ProfilesScreen() {
|
|
|
161
157
|
);
|
|
162
158
|
} catch (error) {
|
|
163
159
|
modal.hideModal();
|
|
164
|
-
await modal.message(
|
|
160
|
+
await modal.message(
|
|
161
|
+
"Error",
|
|
162
|
+
`Failed to apply profile: ${error}`,
|
|
163
|
+
"error",
|
|
164
|
+
);
|
|
165
165
|
}
|
|
166
166
|
};
|
|
167
167
|
|
|
@@ -217,7 +217,11 @@ export function ProfilesScreen() {
|
|
|
217
217
|
);
|
|
218
218
|
} catch (error) {
|
|
219
219
|
modal.hideModal();
|
|
220
|
-
await modal.message(
|
|
220
|
+
await modal.message(
|
|
221
|
+
"Error",
|
|
222
|
+
`Failed to apply profile: ${error}`,
|
|
223
|
+
"error",
|
|
224
|
+
);
|
|
221
225
|
}
|
|
222
226
|
};
|
|
223
227
|
|
|
@@ -242,7 +246,11 @@ export function ProfilesScreen() {
|
|
|
242
246
|
);
|
|
243
247
|
modal.hideModal();
|
|
244
248
|
await fetchData();
|
|
245
|
-
await modal.message(
|
|
249
|
+
await modal.message(
|
|
250
|
+
"Renamed",
|
|
251
|
+
`Profile renamed to "${newName.trim()}".`,
|
|
252
|
+
"success",
|
|
253
|
+
);
|
|
246
254
|
} catch (error) {
|
|
247
255
|
modal.hideModal();
|
|
248
256
|
await modal.message("Error", `Failed to rename: ${error}`, "error");
|
|
@@ -345,10 +353,18 @@ export function ProfilesScreen() {
|
|
|
345
353
|
|
|
346
354
|
modal.loading("Importing...");
|
|
347
355
|
try {
|
|
348
|
-
const id = await importProfileFromJson(
|
|
356
|
+
const id = await importProfileFromJson(
|
|
357
|
+
json,
|
|
358
|
+
targetScope,
|
|
359
|
+
state.projectPath,
|
|
360
|
+
);
|
|
349
361
|
modal.hideModal();
|
|
350
362
|
await fetchData();
|
|
351
|
-
await modal.message(
|
|
363
|
+
await modal.message(
|
|
364
|
+
"Imported",
|
|
365
|
+
`Profile imported successfully (id: ${id}).`,
|
|
366
|
+
"success",
|
|
367
|
+
);
|
|
352
368
|
} catch (error) {
|
|
353
369
|
modal.hideModal();
|
|
354
370
|
await modal.message("Error", `Failed to import: ${error}`, "error");
|
|
@@ -362,14 +378,16 @@ export function ProfilesScreen() {
|
|
|
362
378
|
const projCount = profileList.filter((p) => p.scope === "project").length;
|
|
363
379
|
|
|
364
380
|
const statusContent = (
|
|
365
|
-
<text>
|
|
366
|
-
<span fg=
|
|
367
|
-
<span fg=
|
|
368
|
-
<span fg=
|
|
369
|
-
<span fg=
|
|
370
|
-
<span fg=
|
|
371
|
-
<span fg=
|
|
372
|
-
<span fg=
|
|
381
|
+
<text fg={theme.colors.text}>
|
|
382
|
+
<span fg={theme.colors.link}>{PREDEFINED_PROFILES.length} presets</span>
|
|
383
|
+
<span fg={theme.colors.muted}> + </span>
|
|
384
|
+
<span fg={theme.colors.info}>{userCount} user</span>
|
|
385
|
+
<span fg={theme.colors.muted}> + </span>
|
|
386
|
+
<span fg={theme.colors.success}>{projCount} project</span>
|
|
387
|
+
<span fg={theme.colors.muted}> = </span>
|
|
388
|
+
<span fg={theme.colors.text}>
|
|
389
|
+
{PREDEFINED_PROFILES.length + profileCount} total
|
|
390
|
+
</span>
|
|
373
391
|
</text>
|
|
374
392
|
);
|
|
375
393
|
|
|
@@ -380,9 +398,7 @@ export function ProfilesScreen() {
|
|
|
380
398
|
: profilesState.selectedIndex;
|
|
381
399
|
|
|
382
400
|
const loadingStatus =
|
|
383
|
-
profilesState.profiles.status === "loading"
|
|
384
|
-
? true
|
|
385
|
-
: false;
|
|
401
|
+
profilesState.profiles.status === "loading" ? true : false;
|
|
386
402
|
const errorMessage =
|
|
387
403
|
profilesState.profiles.status === "error"
|
|
388
404
|
? profilesState.profiles.error.message
|