claudeup 4.11.1 → 4.12.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 +1 -1
- package/src/__tests__/plugin-version-check.test.ts +1 -1
- package/src/services/plugin-version-check.js +10 -5
- package/src/services/plugin-version-check.ts +11 -11
- package/src/ui/App.js +6 -63
- package/src/ui/App.tsx +5 -64
- package/src/ui/components/modals/ModalContainer.js +26 -0
- package/src/ui/components/modals/ModalContainer.tsx +26 -0
- package/src/ui/components/modals/VersionMismatchModal.js +36 -0
- package/src/ui/components/modals/VersionMismatchModal.tsx +140 -0
- package/src/ui/hooks/index.js +1 -0
- package/src/ui/hooks/index.ts +1 -0
- package/src/ui/hooks/useMismatchModal.js +77 -0
- package/src/ui/hooks/useMismatchModal.ts +95 -0
- package/src/ui/screens/PluginsScreen.js +23 -12
- package/src/ui/screens/PluginsScreen.tsx +24 -16
- package/src/ui/state/types.ts +7 -0
package/package.json
CHANGED
|
@@ -757,6 +757,6 @@ describe("formatMismatchModal", () => {
|
|
|
757
757
|
expect(modal).toContain("v2.0.0");
|
|
758
758
|
expect(modal).toContain("v4.0.2");
|
|
759
759
|
expect(modal).toContain("45997");
|
|
760
|
-
expect(modal).toContain("
|
|
760
|
+
expect(modal).toContain("not work correctly");
|
|
761
761
|
});
|
|
762
762
|
});
|
|
@@ -189,14 +189,19 @@ export function formatMismatchWarning(mismatches) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function formatMismatchModal(mismatches) {
|
|
191
191
|
const lines = [];
|
|
192
|
-
lines.push("
|
|
193
|
-
lines.push("
|
|
194
|
-
lines.push("
|
|
192
|
+
lines.push("Your plugins will not work correctly in this project.");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("Due to a Claude Code bug, the plugin loader ignores");
|
|
195
|
+
lines.push("per-project versions and loads an older version instead.");
|
|
196
|
+
lines.push("Commands, MCP tools, and skills will be outdated.");
|
|
197
|
+
lines.push("");
|
|
195
198
|
for (const m of mismatches) {
|
|
196
199
|
const name = m.pluginId.split("@")[0];
|
|
197
|
-
lines.push(` ${name}:
|
|
200
|
+
lines.push(` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`);
|
|
198
201
|
}
|
|
199
|
-
lines.push(
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push("Fix: align all projects to the same version.");
|
|
204
|
+
lines.push("Details: github.com/anthropics/claude-code/issues/45997");
|
|
200
205
|
return lines.join("\n");
|
|
201
206
|
}
|
|
202
207
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -275,22 +275,22 @@ export function formatMismatchModal(
|
|
|
275
275
|
mismatches: VersionMismatchInfo[],
|
|
276
276
|
): string {
|
|
277
277
|
const lines: string[] = [];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
);
|
|
281
|
-
lines.push(
|
|
282
|
-
|
|
283
|
-
);
|
|
284
|
-
lines.push("
|
|
278
|
+
|
|
279
|
+
lines.push("Your plugins will not work correctly in this project.");
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("Due to a Claude Code bug, the plugin loader ignores");
|
|
282
|
+
lines.push("per-project versions and loads an older version instead.");
|
|
283
|
+
lines.push("Commands, MCP tools, and skills will be outdated.");
|
|
284
|
+
lines.push("");
|
|
285
285
|
|
|
286
286
|
for (const m of mismatches) {
|
|
287
287
|
const name = m.pluginId.split("@")[0];
|
|
288
|
-
lines.push(
|
|
289
|
-
` ${name}: will load v${m.firstEntryVersion} instead of v${m.currentProjectVersion}`,
|
|
290
|
-
);
|
|
288
|
+
lines.push(` ${name}: v${m.firstEntryVersion} loaded, expected v${m.currentProjectVersion}`);
|
|
291
289
|
}
|
|
292
290
|
|
|
293
|
-
lines.push(
|
|
291
|
+
lines.push("");
|
|
292
|
+
lines.push("Fix: align all projects to the same version.");
|
|
293
|
+
lines.push("Details: github.com/anthropics/claude-code/issues/45997");
|
|
294
294
|
|
|
295
295
|
return lines.join("\n");
|
|
296
296
|
}
|
package/src/ui/App.js
CHANGED
|
@@ -8,7 +8,8 @@ import { ModalContainer } from "./components/modals/index.js";
|
|
|
8
8
|
import { PluginsScreen, McpScreen, McpRegistryScreen, SettingsScreen, CliToolsScreen, ModelSelectorScreen, ProfilesScreen, SkillsScreen, } from "./screens/index.js";
|
|
9
9
|
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
10
10
|
import { migrateMarketplaceRename, recoverMarketplaceSettings, } from "../services/claude-settings.js";
|
|
11
|
-
import { checkPluginVersionMismatches,
|
|
11
|
+
import { checkPluginVersionMismatches, } from "../services/plugin-version-check.js";
|
|
12
|
+
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
12
13
|
import { checkForUpdates, getCurrentVersion, } from "../services/version-check.js";
|
|
13
14
|
import { useKeyboardHandler } from "./hooks/useKeyboardHandler.js";
|
|
14
15
|
import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
@@ -189,6 +190,7 @@ function AppContent({ onExit }) {
|
|
|
189
190
|
const [updateInfo, setUpdateInfo] = useState(null);
|
|
190
191
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
191
192
|
const [mismatchData, setMismatchData] = useState(null);
|
|
193
|
+
const mismatchModal = useMismatchModal();
|
|
192
194
|
// Check for updates on startup (non-blocking)
|
|
193
195
|
useEffect(() => {
|
|
194
196
|
checkForUpdates()
|
|
@@ -206,68 +208,9 @@ function AppContent({ onExit }) {
|
|
|
206
208
|
useEffect(() => {
|
|
207
209
|
if (!mismatchData || mismatchData.length === 0)
|
|
208
210
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
type: "select",
|
|
213
|
-
title: "⚠ Plugin Version Mismatch",
|
|
214
|
-
message: formatMismatchModal(mismatchData),
|
|
215
|
-
options: [
|
|
216
|
-
{
|
|
217
|
-
label: "Fix all projects",
|
|
218
|
-
value: "fix",
|
|
219
|
-
description: "Align all projects to this project's versions",
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
label: "Dismiss",
|
|
223
|
-
value: "dismiss",
|
|
224
|
-
description: "Ignore for now",
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
onSelect: async (value) => {
|
|
228
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
229
|
-
if (value === "fix") {
|
|
230
|
-
dispatch({
|
|
231
|
-
type: "SHOW_PROGRESS",
|
|
232
|
-
state: { message: "Fixing plugin versions..." },
|
|
233
|
-
});
|
|
234
|
-
try {
|
|
235
|
-
await fixAllPluginVersionMismatches(mismatchData);
|
|
236
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
237
|
-
dispatch({
|
|
238
|
-
type: "SHOW_MODAL",
|
|
239
|
-
modal: {
|
|
240
|
-
type: "message",
|
|
241
|
-
title: "Fixed",
|
|
242
|
-
message: "All plugin versions aligned. Restart Claude Code for changes to take effect.",
|
|
243
|
-
variant: "success",
|
|
244
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
250
|
-
dispatch({
|
|
251
|
-
type: "SHOW_MODAL",
|
|
252
|
-
modal: {
|
|
253
|
-
type: "message",
|
|
254
|
-
title: "Error",
|
|
255
|
-
message: "Failed to fix plugin versions. Try running claudeup again.",
|
|
256
|
-
variant: "error",
|
|
257
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
setMismatchData(null);
|
|
263
|
-
},
|
|
264
|
-
onCancel: () => {
|
|
265
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
266
|
-
setMismatchData(null);
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
}, [mismatchData, dispatch]);
|
|
211
|
+
mismatchModal.show(mismatchData);
|
|
212
|
+
setMismatchData(null);
|
|
213
|
+
}, [mismatchData, mismatchModal]);
|
|
271
214
|
// Auto-refresh marketplaces on startup
|
|
272
215
|
useEffect(() => {
|
|
273
216
|
const noRefresh = process.argv.includes("--no-refresh");
|
package/src/ui/App.tsx
CHANGED
|
@@ -30,10 +30,9 @@ import {
|
|
|
30
30
|
} from "../services/claude-settings.js";
|
|
31
31
|
import {
|
|
32
32
|
checkPluginVersionMismatches,
|
|
33
|
-
fixAllPluginVersionMismatches,
|
|
34
|
-
formatMismatchModal,
|
|
35
33
|
type VersionMismatchInfo,
|
|
36
34
|
} from "../services/plugin-version-check.js";
|
|
35
|
+
import { useMismatchModal } from "./hooks/useMismatchModal.js";
|
|
37
36
|
import {
|
|
38
37
|
checkForUpdates,
|
|
39
38
|
getCurrentVersion,
|
|
@@ -317,6 +316,7 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
317
316
|
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
318
317
|
const [recoveryReport, setRecoveryReport] = useState<string | null>(null);
|
|
319
318
|
const [mismatchData, setMismatchData] = useState<VersionMismatchInfo[] | null>(null);
|
|
319
|
+
const mismatchModal = useMismatchModal();
|
|
320
320
|
|
|
321
321
|
// Check for updates on startup (non-blocking)
|
|
322
322
|
useEffect(() => {
|
|
@@ -335,68 +335,9 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
335
335
|
// Show mismatch modal when data arrives
|
|
336
336
|
useEffect(() => {
|
|
337
337
|
if (!mismatchData || mismatchData.length === 0) return;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
type: "select",
|
|
342
|
-
title: "⚠ Plugin Version Mismatch",
|
|
343
|
-
message: formatMismatchModal(mismatchData),
|
|
344
|
-
options: [
|
|
345
|
-
{
|
|
346
|
-
label: "Fix all projects",
|
|
347
|
-
value: "fix",
|
|
348
|
-
description: "Align all projects to this project's versions",
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
label: "Dismiss",
|
|
352
|
-
value: "dismiss",
|
|
353
|
-
description: "Ignore for now",
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
onSelect: async (value: string) => {
|
|
357
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
358
|
-
if (value === "fix") {
|
|
359
|
-
dispatch({
|
|
360
|
-
type: "SHOW_PROGRESS",
|
|
361
|
-
state: { message: "Fixing plugin versions..." },
|
|
362
|
-
});
|
|
363
|
-
try {
|
|
364
|
-
await fixAllPluginVersionMismatches(mismatchData);
|
|
365
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
366
|
-
dispatch({
|
|
367
|
-
type: "SHOW_MODAL",
|
|
368
|
-
modal: {
|
|
369
|
-
type: "message",
|
|
370
|
-
title: "Fixed",
|
|
371
|
-
message:
|
|
372
|
-
"All plugin versions aligned. Restart Claude Code for changes to take effect.",
|
|
373
|
-
variant: "success",
|
|
374
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
} catch {
|
|
378
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
379
|
-
dispatch({
|
|
380
|
-
type: "SHOW_MODAL",
|
|
381
|
-
modal: {
|
|
382
|
-
type: "message",
|
|
383
|
-
title: "Error",
|
|
384
|
-
message: "Failed to fix plugin versions. Try running claudeup again.",
|
|
385
|
-
variant: "error",
|
|
386
|
-
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
setMismatchData(null);
|
|
392
|
-
},
|
|
393
|
-
onCancel: () => {
|
|
394
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
395
|
-
setMismatchData(null);
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}, [mismatchData, dispatch]);
|
|
338
|
+
mismatchModal.show(mismatchData);
|
|
339
|
+
setMismatchData(null);
|
|
340
|
+
}, [mismatchData, mismatchModal]);
|
|
400
341
|
|
|
401
342
|
// Auto-refresh marketplaces on startup
|
|
402
343
|
useEffect(() => {
|
|
@@ -7,6 +7,7 @@ import { InputModal } from "./InputModal.js";
|
|
|
7
7
|
import { SelectModal } from "./SelectModal.js";
|
|
8
8
|
import { MessageModal } from "./MessageModal.js";
|
|
9
9
|
import { LoadingModal } from "./LoadingModal.js";
|
|
10
|
+
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
10
11
|
/**
|
|
11
12
|
* Container that renders the active modal as an overlay
|
|
12
13
|
* Handles ALL keyboard events when a modal is open to avoid
|
|
@@ -27,6 +28,9 @@ export function ModalContainer() {
|
|
|
27
28
|
if (modal?.type === "select") {
|
|
28
29
|
setSelectIndex(modal.defaultIndex ?? 0);
|
|
29
30
|
}
|
|
31
|
+
else if (modal?.type === "version-mismatch") {
|
|
32
|
+
setSelectIndex(0);
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
// Handle keyboard events for modals
|
|
32
36
|
useKeyboard((key) => {
|
|
@@ -44,6 +48,8 @@ export function ModalContainer() {
|
|
|
44
48
|
modal.onCancel();
|
|
45
49
|
else if (modal.type === "message")
|
|
46
50
|
modal.onDismiss();
|
|
51
|
+
else if (modal.type === "version-mismatch")
|
|
52
|
+
modal.onDismiss();
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
55
|
// 'q' to close — but NOT for input modals (need to type 'q')
|
|
@@ -54,6 +60,8 @@ export function ModalContainer() {
|
|
|
54
60
|
modal.onCancel();
|
|
55
61
|
else if (modal.type === "message")
|
|
56
62
|
modal.onDismiss();
|
|
63
|
+
else if (modal.type === "version-mismatch")
|
|
64
|
+
modal.onDismiss();
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
67
|
// Input modal — let OpenTUI <input> handle Enter via onSubmit
|
|
@@ -73,6 +81,22 @@ export function ModalContainer() {
|
|
|
73
81
|
}
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
84
|
+
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
85
|
+
if (modal.type === "version-mismatch") {
|
|
86
|
+
if (key.name === "return" || key.name === "enter") {
|
|
87
|
+
if (selectIndex === 0)
|
|
88
|
+
modal.onFix();
|
|
89
|
+
else
|
|
90
|
+
modal.onDismiss();
|
|
91
|
+
}
|
|
92
|
+
else if (key.name === "up" || key.name === "k") {
|
|
93
|
+
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
94
|
+
}
|
|
95
|
+
else if (key.name === "down" || key.name === "j") {
|
|
96
|
+
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
76
100
|
// Message modal — Enter to dismiss
|
|
77
101
|
if (modal.type === "message") {
|
|
78
102
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -101,6 +125,8 @@ export function ModalContainer() {
|
|
|
101
125
|
return (_jsx(MessageModal, { title: modal.title, message: modal.message, variant: modal.variant, onDismiss: modal.onDismiss }));
|
|
102
126
|
case "loading":
|
|
103
127
|
return _jsx(LoadingModal, { message: modal.message });
|
|
128
|
+
case "version-mismatch":
|
|
129
|
+
return (_jsx(VersionMismatchModal, { mismatches: modal.mismatches, defaultIndex: selectIndex }));
|
|
104
130
|
default:
|
|
105
131
|
return null;
|
|
106
132
|
}
|
|
@@ -6,6 +6,7 @@ import { InputModal } from "./InputModal.js";
|
|
|
6
6
|
import { SelectModal } from "./SelectModal.js";
|
|
7
7
|
import { MessageModal } from "./MessageModal.js";
|
|
8
8
|
import { LoadingModal } from "./LoadingModal.js";
|
|
9
|
+
import { VersionMismatchModal } from "./VersionMismatchModal.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Container that renders the active modal as an overlay
|
|
@@ -28,6 +29,8 @@ export function ModalContainer() {
|
|
|
28
29
|
modalRef.current = modal;
|
|
29
30
|
if (modal?.type === "select") {
|
|
30
31
|
setSelectIndex(modal.defaultIndex ?? 0);
|
|
32
|
+
} else if (modal?.type === "version-mismatch") {
|
|
33
|
+
setSelectIndex(0);
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -42,6 +45,7 @@ export function ModalContainer() {
|
|
|
42
45
|
else if (modal.type === "input") modal.onCancel();
|
|
43
46
|
else if (modal.type === "select") modal.onCancel();
|
|
44
47
|
else if (modal.type === "message") modal.onDismiss();
|
|
48
|
+
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -50,6 +54,7 @@ export function ModalContainer() {
|
|
|
50
54
|
if (modal.type === "confirm") modal.onCancel();
|
|
51
55
|
else if (modal.type === "select") modal.onCancel();
|
|
52
56
|
else if (modal.type === "message") modal.onDismiss();
|
|
57
|
+
else if (modal.type === "version-mismatch") modal.onDismiss();
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -70,6 +75,19 @@ export function ModalContainer() {
|
|
|
70
75
|
return;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
// Version-mismatch modal — handle navigation and selection (2 options)
|
|
79
|
+
if (modal.type === "version-mismatch") {
|
|
80
|
+
if (key.name === "return" || key.name === "enter") {
|
|
81
|
+
if (selectIndex === 0) modal.onFix();
|
|
82
|
+
else modal.onDismiss();
|
|
83
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
84
|
+
setSelectIndex((prev) => Math.max(0, prev - 1));
|
|
85
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
86
|
+
setSelectIndex((prev) => Math.min(1, prev + 1));
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
// Message modal — Enter to dismiss
|
|
74
92
|
if (modal.type === "message") {
|
|
75
93
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -137,6 +155,14 @@ export function ModalContainer() {
|
|
|
137
155
|
case "loading":
|
|
138
156
|
return <LoadingModal message={modal.message} />;
|
|
139
157
|
|
|
158
|
+
case "version-mismatch":
|
|
159
|
+
return (
|
|
160
|
+
<VersionMismatchModal
|
|
161
|
+
mismatches={modal.mismatches}
|
|
162
|
+
defaultIndex={selectIndex}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
140
166
|
default:
|
|
141
167
|
return null;
|
|
142
168
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
const COLOR_BG = "#1C1C1E";
|
|
3
|
+
const COLOR_BORDER = "#525252";
|
|
4
|
+
const COLOR_TITLE = "#EDEDED";
|
|
5
|
+
const COLOR_MUTED = "#A1A1AA";
|
|
6
|
+
const COLOR_DIM = "#71717A";
|
|
7
|
+
const COLOR_WARN = "#FBBF24";
|
|
8
|
+
const COLOR_BAD = "#F87171";
|
|
9
|
+
const COLOR_GOOD = "#4ADE80";
|
|
10
|
+
const COLOR_ACCENT = "#F4F4F5";
|
|
11
|
+
/** Truncate a plugin name to fit the column width */
|
|
12
|
+
function shortName(pluginId, max) {
|
|
13
|
+
const name = pluginId.split("@")[0];
|
|
14
|
+
if (name.length <= max)
|
|
15
|
+
return name;
|
|
16
|
+
return `${name.slice(0, max - 1)}…`;
|
|
17
|
+
}
|
|
18
|
+
/** Truncate a version string to fit the column width */
|
|
19
|
+
function shortVersion(v, max) {
|
|
20
|
+
if (v.length <= max)
|
|
21
|
+
return v;
|
|
22
|
+
return `${v.slice(0, max - 1)}…`;
|
|
23
|
+
}
|
|
24
|
+
export function VersionMismatchModal({ mismatches, defaultIndex, }) {
|
|
25
|
+
const selectedIndex = defaultIndex ?? 0;
|
|
26
|
+
// Column widths chosen to fit a 64-wide modal with padding
|
|
27
|
+
const NAME_W = 22;
|
|
28
|
+
const VER_W = 12;
|
|
29
|
+
return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: COLOR_BORDER, backgroundColor: COLOR_BG, paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 64, children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: COLOR_WARN, children: "\u26A0 " }), _jsx("text", { fg: COLOR_TITLE, children: _jsx("strong", { children: "Plugin Version Mismatch" }) })] }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsxs("text", { fg: COLOR_MUTED, children: ["Plugins in this project will load ", _jsx("strong", { children: "wrong versions" })] }), _jsx("text", { fg: COLOR_MUTED, children: "due to a Claude Code bug (#45997)." })] }), _jsxs("box", { flexDirection: "column", marginBottom: 1, children: [_jsx("text", { fg: COLOR_DIM, children: _jsxs("strong", { children: ["Plugin".padEnd(NAME_W), "Loaded".padEnd(VER_W), "Expected".padEnd(VER_W)] }) }), _jsxs("text", { fg: COLOR_BORDER, children: ["─".repeat(NAME_W - 1), " ", "─".repeat(VER_W - 1), " ", "─".repeat(VER_W - 1)] }), mismatches.map((m) => {
|
|
30
|
+
const name = shortName(m.pluginId, NAME_W - 1);
|
|
31
|
+
const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
|
|
32
|
+
const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
|
|
33
|
+
return (_jsxs("text", { fg: COLOR_MUTED, children: [_jsx("span", { fg: COLOR_ACCENT, children: name.padEnd(NAME_W) }), _jsx("span", { fg: COLOR_BAD, children: loaded.padEnd(VER_W) }), _jsx("span", { fg: COLOR_GOOD, children: expected.padEnd(VER_W) })] }, m.pluginId));
|
|
34
|
+
})] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: COLOR_DIM, children: "Details: github.com/anthropics/claude-code/issues/45997" }) }), _jsxs("box", { flexDirection: "column", paddingLeft: 1, children: [_jsxs("text", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 0 ? "❯ " : " " }), selectedIndex === 0 ? (_jsx("strong", { children: "Fix all projects" })) : ("Fix all projects")] }), _jsxs("text", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED, children: [_jsx("span", { fg: selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM, children: selectedIndex === 1 ? "❯ " : " " }), selectedIndex === 1 ? _jsx("strong", { children: "Dismiss" }) : "Dismiss"] })] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: COLOR_DIM, children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
|
|
35
|
+
}
|
|
36
|
+
export default VersionMismatchModal;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { VersionMismatchInfo } from "../../../services/plugin-version-check.js";
|
|
3
|
+
|
|
4
|
+
interface VersionMismatchModalProps {
|
|
5
|
+
/** List of version mismatches detected */
|
|
6
|
+
mismatches: VersionMismatchInfo[];
|
|
7
|
+
/** Currently selected option index (0 = Fix, 1 = Dismiss) */
|
|
8
|
+
defaultIndex?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const COLOR_BG = "#1C1C1E";
|
|
12
|
+
const COLOR_BORDER = "#525252";
|
|
13
|
+
const COLOR_TITLE = "#EDEDED";
|
|
14
|
+
const COLOR_MUTED = "#A1A1AA";
|
|
15
|
+
const COLOR_DIM = "#71717A";
|
|
16
|
+
const COLOR_WARN = "#FBBF24";
|
|
17
|
+
const COLOR_BAD = "#F87171";
|
|
18
|
+
const COLOR_GOOD = "#4ADE80";
|
|
19
|
+
const COLOR_ACCENT = "#F4F4F5";
|
|
20
|
+
|
|
21
|
+
/** Truncate a plugin name to fit the column width */
|
|
22
|
+
function shortName(pluginId: string, max: number): string {
|
|
23
|
+
const name = pluginId.split("@")[0];
|
|
24
|
+
if (name.length <= max) return name;
|
|
25
|
+
return `${name.slice(0, max - 1)}…`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Truncate a version string to fit the column width */
|
|
29
|
+
function shortVersion(v: string, max: number): string {
|
|
30
|
+
if (v.length <= max) return v;
|
|
31
|
+
return `${v.slice(0, max - 1)}…`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function VersionMismatchModal({
|
|
35
|
+
mismatches,
|
|
36
|
+
defaultIndex,
|
|
37
|
+
}: VersionMismatchModalProps) {
|
|
38
|
+
const selectedIndex = defaultIndex ?? 0;
|
|
39
|
+
|
|
40
|
+
// Column widths chosen to fit a 64-wide modal with padding
|
|
41
|
+
const NAME_W = 22;
|
|
42
|
+
const VER_W = 12;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box
|
|
46
|
+
flexDirection="column"
|
|
47
|
+
border
|
|
48
|
+
borderStyle="rounded"
|
|
49
|
+
borderColor={COLOR_BORDER}
|
|
50
|
+
backgroundColor={COLOR_BG}
|
|
51
|
+
paddingLeft={3}
|
|
52
|
+
paddingRight={3}
|
|
53
|
+
paddingTop={1}
|
|
54
|
+
paddingBottom={1}
|
|
55
|
+
width={64}
|
|
56
|
+
>
|
|
57
|
+
{/* Title */}
|
|
58
|
+
<box marginBottom={1}>
|
|
59
|
+
<text fg={COLOR_WARN}>⚠ </text>
|
|
60
|
+
<text fg={COLOR_TITLE}>
|
|
61
|
+
<strong>Plugin Version Mismatch</strong>
|
|
62
|
+
</text>
|
|
63
|
+
</box>
|
|
64
|
+
|
|
65
|
+
{/* Description */}
|
|
66
|
+
<box flexDirection="column" marginBottom={1}>
|
|
67
|
+
<text fg={COLOR_MUTED}>
|
|
68
|
+
Plugins in this project will load <strong>wrong versions</strong>
|
|
69
|
+
</text>
|
|
70
|
+
<text fg={COLOR_MUTED}>due to a Claude Code bug (#45997).</text>
|
|
71
|
+
</box>
|
|
72
|
+
|
|
73
|
+
{/* Table header */}
|
|
74
|
+
<box flexDirection="column" marginBottom={1}>
|
|
75
|
+
<text fg={COLOR_DIM}>
|
|
76
|
+
<strong>
|
|
77
|
+
{"Plugin".padEnd(NAME_W)}
|
|
78
|
+
{"Loaded".padEnd(VER_W)}
|
|
79
|
+
{"Expected".padEnd(VER_W)}
|
|
80
|
+
</strong>
|
|
81
|
+
</text>
|
|
82
|
+
<text fg={COLOR_BORDER}>
|
|
83
|
+
{"─".repeat(NAME_W - 1)} {"─".repeat(VER_W - 1)} {"─".repeat(VER_W - 1)}
|
|
84
|
+
</text>
|
|
85
|
+
|
|
86
|
+
{/* Table rows */}
|
|
87
|
+
{mismatches.map((m) => {
|
|
88
|
+
const name = shortName(m.pluginId, NAME_W - 1);
|
|
89
|
+
const loaded = `v${shortVersion(m.firstEntryVersion, VER_W - 2)}`;
|
|
90
|
+
const expected = `v${shortVersion(m.currentProjectVersion, VER_W - 2)}`;
|
|
91
|
+
return (
|
|
92
|
+
<text key={m.pluginId} fg={COLOR_MUTED}>
|
|
93
|
+
<span fg={COLOR_ACCENT}>{name.padEnd(NAME_W)}</span>
|
|
94
|
+
<span fg={COLOR_BAD}>{loaded.padEnd(VER_W)}</span>
|
|
95
|
+
<span fg={COLOR_GOOD}>{expected.padEnd(VER_W)}</span>
|
|
96
|
+
</text>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</box>
|
|
100
|
+
|
|
101
|
+
{/* Footer note */}
|
|
102
|
+
<box marginBottom={1}>
|
|
103
|
+
<text fg={COLOR_DIM}>
|
|
104
|
+
Details: github.com/anthropics/claude-code/issues/45997
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
|
|
108
|
+
{/* Options */}
|
|
109
|
+
<box flexDirection="column" paddingLeft={1}>
|
|
110
|
+
<text
|
|
111
|
+
fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_MUTED}
|
|
112
|
+
>
|
|
113
|
+
<span fg={selectedIndex === 0 ? COLOR_ACCENT : COLOR_DIM}>
|
|
114
|
+
{selectedIndex === 0 ? "❯ " : " "}
|
|
115
|
+
</span>
|
|
116
|
+
{selectedIndex === 0 ? (
|
|
117
|
+
<strong>Fix all projects</strong>
|
|
118
|
+
) : (
|
|
119
|
+
"Fix all projects"
|
|
120
|
+
)}
|
|
121
|
+
</text>
|
|
122
|
+
<text
|
|
123
|
+
fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_MUTED}
|
|
124
|
+
>
|
|
125
|
+
<span fg={selectedIndex === 1 ? COLOR_ACCENT : COLOR_DIM}>
|
|
126
|
+
{selectedIndex === 1 ? "❯ " : " "}
|
|
127
|
+
</span>
|
|
128
|
+
{selectedIndex === 1 ? <strong>Dismiss</strong> : "Dismiss"}
|
|
129
|
+
</text>
|
|
130
|
+
</box>
|
|
131
|
+
|
|
132
|
+
{/* Keybind hints */}
|
|
133
|
+
<box marginTop={1}>
|
|
134
|
+
<text fg={COLOR_DIM}>↑↓ Select • ↵ Confirm • Esc Cancel</text>
|
|
135
|
+
</box>
|
|
136
|
+
</box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default VersionMismatchModal;
|
package/src/ui/hooks/index.js
CHANGED
package/src/ui/hooks/index.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp } from "../state/AppContext.js";
|
|
3
|
+
import { fixAllPluginVersionMismatches, } from "../../services/plugin-version-check.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared hook for showing the plugin version mismatch modal.
|
|
6
|
+
*
|
|
7
|
+
* Used in two places:
|
|
8
|
+
* 1. App.tsx — startup check shows mismatches found across all enabled plugins
|
|
9
|
+
* 2. PluginsScreen.tsx — post-install/update check shows a single plugin mismatch
|
|
10
|
+
*
|
|
11
|
+
* Both flows now share the same interactive modal with a "Fix all projects"
|
|
12
|
+
* button, so users never have to restart claudeup to access the fix.
|
|
13
|
+
*
|
|
14
|
+
* The fix uses fixAllPluginVersionMismatches() which rewrites
|
|
15
|
+
* installed_plugins.json in-place to align all entries to the current
|
|
16
|
+
* project's expected versions. After success, shows a hint to restart
|
|
17
|
+
* Claude Code so the new versions actually load (the running session
|
|
18
|
+
* has the old plugin code in memory).
|
|
19
|
+
*/
|
|
20
|
+
export function useMismatchModal() {
|
|
21
|
+
const { dispatch } = useApp();
|
|
22
|
+
const show = useCallback((mismatches) => {
|
|
23
|
+
if (mismatches.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
26
|
+
const fix = async () => {
|
|
27
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
28
|
+
dispatch({
|
|
29
|
+
type: "SHOW_PROGRESS",
|
|
30
|
+
state: { message: "Aligning plugin versions across projects..." },
|
|
31
|
+
});
|
|
32
|
+
try {
|
|
33
|
+
const results = await fixAllPluginVersionMismatches(mismatches);
|
|
34
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
35
|
+
// Count how many projects were updated across all plugins
|
|
36
|
+
const totalUpdated = Array.from(results.values()).reduce((sum, r) => sum + r.updated, 0);
|
|
37
|
+
dispatch({
|
|
38
|
+
type: "SHOW_MODAL",
|
|
39
|
+
modal: {
|
|
40
|
+
type: "message",
|
|
41
|
+
title: "Plugin Versions Aligned",
|
|
42
|
+
message: `Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
|
|
43
|
+
`installed_plugins.json.\n\n` +
|
|
44
|
+
`Restart Claude Code for the new versions to load. ` +
|
|
45
|
+
`The running session still has the old plugin code in memory.`,
|
|
46
|
+
variant: "success",
|
|
47
|
+
onDismiss: dismiss,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
53
|
+
dispatch({
|
|
54
|
+
type: "SHOW_MODAL",
|
|
55
|
+
modal: {
|
|
56
|
+
type: "message",
|
|
57
|
+
title: "Fix Failed",
|
|
58
|
+
message: `Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
59
|
+
`Try running claudeup again, or manually update the plugin in each project.`,
|
|
60
|
+
variant: "error",
|
|
61
|
+
onDismiss: dismiss,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
dispatch({
|
|
67
|
+
type: "SHOW_MODAL",
|
|
68
|
+
modal: {
|
|
69
|
+
type: "version-mismatch",
|
|
70
|
+
mismatches,
|
|
71
|
+
onFix: fix,
|
|
72
|
+
onDismiss: dismiss,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}, [dispatch]);
|
|
76
|
+
return { show };
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useApp } from "../state/AppContext.js";
|
|
3
|
+
import {
|
|
4
|
+
fixAllPluginVersionMismatches,
|
|
5
|
+
type VersionMismatchInfo,
|
|
6
|
+
} from "../../services/plugin-version-check.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared hook for showing the plugin version mismatch modal.
|
|
10
|
+
*
|
|
11
|
+
* Used in two places:
|
|
12
|
+
* 1. App.tsx — startup check shows mismatches found across all enabled plugins
|
|
13
|
+
* 2. PluginsScreen.tsx — post-install/update check shows a single plugin mismatch
|
|
14
|
+
*
|
|
15
|
+
* Both flows now share the same interactive modal with a "Fix all projects"
|
|
16
|
+
* button, so users never have to restart claudeup to access the fix.
|
|
17
|
+
*
|
|
18
|
+
* The fix uses fixAllPluginVersionMismatches() which rewrites
|
|
19
|
+
* installed_plugins.json in-place to align all entries to the current
|
|
20
|
+
* project's expected versions. After success, shows a hint to restart
|
|
21
|
+
* Claude Code so the new versions actually load (the running session
|
|
22
|
+
* has the old plugin code in memory).
|
|
23
|
+
*/
|
|
24
|
+
export function useMismatchModal() {
|
|
25
|
+
const { dispatch } = useApp();
|
|
26
|
+
|
|
27
|
+
const show = useCallback(
|
|
28
|
+
(mismatches: VersionMismatchInfo[]) => {
|
|
29
|
+
if (mismatches.length === 0) return;
|
|
30
|
+
|
|
31
|
+
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
32
|
+
|
|
33
|
+
const fix = async () => {
|
|
34
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
35
|
+
dispatch({
|
|
36
|
+
type: "SHOW_PROGRESS",
|
|
37
|
+
state: { message: "Aligning plugin versions across projects..." },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const results = await fixAllPluginVersionMismatches(mismatches);
|
|
42
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
43
|
+
|
|
44
|
+
// Count how many projects were updated across all plugins
|
|
45
|
+
const totalUpdated = Array.from(results.values()).reduce(
|
|
46
|
+
(sum, r) => sum + r.updated,
|
|
47
|
+
0,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
dispatch({
|
|
51
|
+
type: "SHOW_MODAL",
|
|
52
|
+
modal: {
|
|
53
|
+
type: "message",
|
|
54
|
+
title: "Plugin Versions Aligned",
|
|
55
|
+
message:
|
|
56
|
+
`Updated ${totalUpdated} project entr${totalUpdated === 1 ? "y" : "ies"} in ` +
|
|
57
|
+
`installed_plugins.json.\n\n` +
|
|
58
|
+
`Restart Claude Code for the new versions to load. ` +
|
|
59
|
+
`The running session still has the old plugin code in memory.`,
|
|
60
|
+
variant: "success",
|
|
61
|
+
onDismiss: dismiss,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
dispatch({ type: "HIDE_PROGRESS" });
|
|
66
|
+
dispatch({
|
|
67
|
+
type: "SHOW_MODAL",
|
|
68
|
+
modal: {
|
|
69
|
+
type: "message",
|
|
70
|
+
title: "Fix Failed",
|
|
71
|
+
message:
|
|
72
|
+
`Could not align plugin versions: ${error instanceof Error ? error.message : String(error)}\n\n` +
|
|
73
|
+
`Try running claudeup again, or manually update the plugin in each project.`,
|
|
74
|
+
variant: "error",
|
|
75
|
+
onDismiss: dismiss,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
dispatch({
|
|
82
|
+
type: "SHOW_MODAL",
|
|
83
|
+
modal: {
|
|
84
|
+
type: "version-mismatch",
|
|
85
|
+
mismatches,
|
|
86
|
+
onFix: fix,
|
|
87
|
+
onDismiss: dismiss,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
[dispatch],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { show };
|
|
95
|
+
}
|
|
@@ -14,7 +14,8 @@ import { saveProfile } from "../../services/profiles.js";
|
|
|
14
14
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
15
15
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
16
16
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
17
|
-
import { checkSinglePluginMismatch } from "../../services/plugin-version-check.js";
|
|
17
|
+
import { checkSinglePluginMismatch, } from "../../services/plugin-version-check.js";
|
|
18
|
+
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
18
19
|
import { buildPluginBrowserItems, } from "../adapters/pluginsAdapter.js";
|
|
19
20
|
import { renderPluginRow, renderPluginDetail, } from "../renderers/pluginRenderers.js";
|
|
20
21
|
export function PluginsScreen() {
|
|
@@ -23,6 +24,7 @@ export function PluginsScreen() {
|
|
|
23
24
|
const modal = useModal();
|
|
24
25
|
const progress = useProgress();
|
|
25
26
|
const dimensions = useDimensions();
|
|
27
|
+
const mismatchModal = useMismatchModal();
|
|
26
28
|
const isSearchActive = state.isSearching &&
|
|
27
29
|
state.currentRoute.screen === "plugins" &&
|
|
28
30
|
!state.modal;
|
|
@@ -234,22 +236,19 @@ export function PluginsScreen() {
|
|
|
234
236
|
}
|
|
235
237
|
};
|
|
236
238
|
/**
|
|
237
|
-
* Check for version mismatch after a plugin update and
|
|
239
|
+
* Check for version mismatch after a plugin update and show the
|
|
240
|
+
* interactive mismatch modal with a "Fix all projects" button.
|
|
241
|
+
*
|
|
238
242
|
* The [0] index bug in Claude Code means the update may not actually
|
|
239
|
-
* take effect if another project's entry is at index 0.
|
|
243
|
+
* take effect if another project's entry is at index 0. The shared
|
|
244
|
+
* useMismatchModal hook provides the same fix flow available at startup.
|
|
240
245
|
*/
|
|
241
246
|
const warnIfVersionMismatch = async (pluginId) => {
|
|
242
247
|
try {
|
|
243
248
|
const projectPath = state.projectPath || process.cwd();
|
|
244
249
|
const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
245
250
|
if (mismatch) {
|
|
246
|
-
|
|
247
|
-
`but Claude Code will load v${mismatch.firstEntryVersion} due to a known bug.\n\n` +
|
|
248
|
-
`The plugin loader always uses the first entry in installed_plugins.json, ` +
|
|
249
|
-
`which belongs to another project.\n\n` +
|
|
250
|
-
`Bug: https://github.com/anthropics/claude-code/issues/45997\n\n` +
|
|
251
|
-
`To fix: open claudeup and use the version alignment tool, ` +
|
|
252
|
-
`or update the plugin in all projects to the same version.`, "error");
|
|
251
|
+
mismatchModal.show([mismatch]);
|
|
253
252
|
}
|
|
254
253
|
}
|
|
255
254
|
catch {
|
|
@@ -567,9 +566,21 @@ export function PluginsScreen() {
|
|
|
567
566
|
updatedPluginIds.push(plugin.id);
|
|
568
567
|
}
|
|
569
568
|
modal.hideModal();
|
|
570
|
-
//
|
|
569
|
+
// Batch mismatch checks into a single modal
|
|
570
|
+
const projectPath = state.projectPath || process.cwd();
|
|
571
|
+
const allMismatches = [];
|
|
571
572
|
for (const pluginId of updatedPluginIds) {
|
|
572
|
-
|
|
573
|
+
try {
|
|
574
|
+
const m = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
575
|
+
if (m)
|
|
576
|
+
allMismatches.push(m);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// best-effort
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (allMismatches.length > 0) {
|
|
583
|
+
mismatchModal.show(allMismatches);
|
|
573
584
|
}
|
|
574
585
|
fetchData();
|
|
575
586
|
}
|
|
@@ -38,7 +38,11 @@ import {
|
|
|
38
38
|
checkMissingDeps,
|
|
39
39
|
installPluginDeps,
|
|
40
40
|
} from "../../services/plugin-setup.js";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
checkSinglePluginMismatch,
|
|
43
|
+
type VersionMismatchInfo,
|
|
44
|
+
} from "../../services/plugin-version-check.js";
|
|
45
|
+
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
42
46
|
import {
|
|
43
47
|
buildPluginBrowserItems,
|
|
44
48
|
type PluginBrowserItem,
|
|
@@ -54,6 +58,7 @@ export function PluginsScreen() {
|
|
|
54
58
|
const modal = useModal();
|
|
55
59
|
const progress = useProgress();
|
|
56
60
|
const dimensions = useDimensions();
|
|
61
|
+
const mismatchModal = useMismatchModal();
|
|
57
62
|
|
|
58
63
|
const isSearchActive =
|
|
59
64
|
state.isSearching &&
|
|
@@ -290,26 +295,19 @@ export function PluginsScreen() {
|
|
|
290
295
|
};
|
|
291
296
|
|
|
292
297
|
/**
|
|
293
|
-
* Check for version mismatch after a plugin update and
|
|
298
|
+
* Check for version mismatch after a plugin update and show the
|
|
299
|
+
* interactive mismatch modal with a "Fix all projects" button.
|
|
300
|
+
*
|
|
294
301
|
* The [0] index bug in Claude Code means the update may not actually
|
|
295
|
-
* take effect if another project's entry is at index 0.
|
|
302
|
+
* take effect if another project's entry is at index 0. The shared
|
|
303
|
+
* useMismatchModal hook provides the same fix flow available at startup.
|
|
296
304
|
*/
|
|
297
305
|
const warnIfVersionMismatch = async (pluginId: string): Promise<void> => {
|
|
298
306
|
try {
|
|
299
307
|
const projectPath = state.projectPath || process.cwd();
|
|
300
308
|
const mismatch = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
301
309
|
if (mismatch) {
|
|
302
|
-
|
|
303
|
-
"Version Mismatch Warning",
|
|
304
|
-
`${pluginId} was updated to v${mismatch.currentProjectVersion} for this project, ` +
|
|
305
|
-
`but Claude Code will load v${mismatch.firstEntryVersion} due to a known bug.\n\n` +
|
|
306
|
-
`The plugin loader always uses the first entry in installed_plugins.json, ` +
|
|
307
|
-
`which belongs to another project.\n\n` +
|
|
308
|
-
`Bug: https://github.com/anthropics/claude-code/issues/45997\n\n` +
|
|
309
|
-
`To fix: open claudeup and use the version alignment tool, ` +
|
|
310
|
-
`or update the plugin in all projects to the same version.`,
|
|
311
|
-
"error",
|
|
312
|
-
);
|
|
310
|
+
mismatchModal.show([mismatch]);
|
|
313
311
|
}
|
|
314
312
|
} catch {
|
|
315
313
|
// Non-fatal: mismatch check is best-effort
|
|
@@ -722,9 +720,19 @@ export function PluginsScreen() {
|
|
|
722
720
|
updatedPluginIds.push(plugin.id);
|
|
723
721
|
}
|
|
724
722
|
modal.hideModal();
|
|
725
|
-
//
|
|
723
|
+
// Batch mismatch checks into a single modal
|
|
724
|
+
const projectPath = state.projectPath || process.cwd();
|
|
725
|
+
const allMismatches: VersionMismatchInfo[] = [];
|
|
726
726
|
for (const pluginId of updatedPluginIds) {
|
|
727
|
-
|
|
727
|
+
try {
|
|
728
|
+
const m = await checkSinglePluginMismatch(pluginId, projectPath);
|
|
729
|
+
if (m) allMismatches.push(m);
|
|
730
|
+
} catch {
|
|
731
|
+
// best-effort
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (allMismatches.length > 0) {
|
|
735
|
+
mismatchModal.show(allMismatches);
|
|
728
736
|
}
|
|
729
737
|
fetchData();
|
|
730
738
|
} catch (error) {
|
package/src/ui/state/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SkillInfo,
|
|
7
7
|
} from "../../types/index.js";
|
|
8
8
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
9
|
+
import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
|
|
9
10
|
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Route Types
|
|
@@ -86,6 +87,12 @@ export type ModalState =
|
|
|
86
87
|
| {
|
|
87
88
|
type: "loading";
|
|
88
89
|
message: string;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
type: "version-mismatch";
|
|
93
|
+
mismatches: VersionMismatchInfo[];
|
|
94
|
+
onFix: () => void;
|
|
95
|
+
onDismiss: () => void;
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
// ============================================================================
|