claudeup 4.41.0 → 4.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/claudeup.js +16 -3
- package/package.json +4 -4
- package/scripts/build-binaries.ts +12 -1
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/dotenv.test.ts +190 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/markdown-renderer.test.ts +143 -0
- package/src/__tests__/marketplaces.test.ts +4 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/data/marketplaces.ts +22 -0
- package/src/main.tsx +18 -0
- package/src/services/dotenv.ts +176 -0
- package/src/services/local-marketplace.ts +110 -19
- package/src/services/marketplace-catalog.ts +34 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +18 -0
- package/src/services/skills-manager.ts +531 -14
- package/src/ui/adapters/pluginsAdapter.ts +154 -17
- package/src/ui/components/primitives/KeyValueLine.tsx +8 -2
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/pluginRenderers.tsx +415 -9
- package/src/ui/screens/PluginsScreen.tsx +181 -53
- package/src/ui/state/reducer.ts +56 -0
- package/src/ui/state/types.ts +42 -0
- package/src/ui/theme-mode.ts +67 -0
|
@@ -1,75 +1,83 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import { asyncValue } from "../state/types.js";
|
|
4
|
-
import { useDimensions } from "../state/DimensionsContext.js";
|
|
5
|
-
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
6
|
-
import { ScreenLayout } from "../components/layout/index.js";
|
|
7
|
-
import { ScrollableList } from "../components/ScrollableList.js";
|
|
8
|
-
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
9
|
-
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
10
3
|
import {
|
|
11
4
|
defaultMarketplaces,
|
|
12
5
|
getAllMarketplaces,
|
|
13
6
|
} from "../../data/marketplaces.js";
|
|
14
|
-
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
15
|
-
import {
|
|
16
|
-
diffVersions,
|
|
17
|
-
loadSeenVersions,
|
|
18
|
-
saveSeenVersions,
|
|
19
|
-
} from "../../services/version-snapshot.js";
|
|
20
7
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
saveInstalledPluginVersion,
|
|
29
|
-
type MarketplaceFetchFailure,
|
|
30
|
-
type PluginInfo,
|
|
31
|
-
type ScopeStatus,
|
|
32
|
-
} from "../../services/plugin-manager.js";
|
|
8
|
+
type PluginScope,
|
|
9
|
+
addMarketplace as cliAddMarketplace,
|
|
10
|
+
installPlugin as cliInstallPlugin,
|
|
11
|
+
repairPlugin as cliRepairPlugin,
|
|
12
|
+
uninstallPlugin as cliUninstallPlugin,
|
|
13
|
+
updatePlugin as cliUpdatePlugin,
|
|
14
|
+
} from "../../services/claude-cli.js";
|
|
33
15
|
import {
|
|
34
|
-
setMcpEnvVar,
|
|
35
16
|
getMcpEnvVars,
|
|
36
17
|
readSettings,
|
|
37
18
|
saveGlobalInstalledPluginVersion,
|
|
38
19
|
saveLocalInstalledPluginVersion,
|
|
20
|
+
setMcpEnvVar,
|
|
39
21
|
} from "../../services/claude-settings.js";
|
|
40
|
-
import {
|
|
22
|
+
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
41
23
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
type MarketplaceFetchFailure,
|
|
25
|
+
type PluginInfo,
|
|
26
|
+
type ScopeStatus,
|
|
27
|
+
clearMarketplaceCache,
|
|
28
|
+
getAvailablePlugins,
|
|
29
|
+
getLocalMarketplacesInfo,
|
|
30
|
+
getMarketplaceFetchFailures,
|
|
31
|
+
isInstalledInScope,
|
|
32
|
+
refreshAllMarketplaces,
|
|
33
|
+
resolveScopeAction,
|
|
34
|
+
saveInstalledPluginVersion,
|
|
35
|
+
} from "../../services/plugin-manager.js";
|
|
49
36
|
import {
|
|
50
37
|
getPluginEnvRequirements,
|
|
51
38
|
getPluginSourcePath,
|
|
52
39
|
} from "../../services/plugin-mcp-config.js";
|
|
53
40
|
import {
|
|
54
|
-
getPluginSetupFromSource,
|
|
55
41
|
checkMissingDeps,
|
|
42
|
+
getPluginSetupFromSource,
|
|
56
43
|
installPluginDeps,
|
|
57
44
|
} from "../../services/plugin-setup.js";
|
|
58
45
|
import {
|
|
46
|
+
type VersionMismatchInfo,
|
|
59
47
|
checkPluginVersionMismatches,
|
|
60
48
|
checkSinglePluginMismatch,
|
|
61
|
-
type VersionMismatchInfo,
|
|
62
49
|
} from "../../services/plugin-version-check.js";
|
|
63
|
-
import {
|
|
50
|
+
import { saveProfile } from "../../services/profiles.js";
|
|
51
|
+
import {
|
|
52
|
+
fetchPluginContents,
|
|
53
|
+
fetchPluginSkillDetail,
|
|
54
|
+
} from "../../services/skills-manager.js";
|
|
55
|
+
import {
|
|
56
|
+
diffVersions,
|
|
57
|
+
loadSeenVersions,
|
|
58
|
+
saveSeenVersions,
|
|
59
|
+
} from "../../services/version-snapshot.js";
|
|
60
|
+
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
61
|
+
import { buildCatalogNoticeText } from "../adapters/catalogNotice.js";
|
|
64
62
|
import {
|
|
65
|
-
buildPluginBrowserItems,
|
|
66
63
|
type PluginBrowserItem,
|
|
64
|
+
type PluginComponentItem,
|
|
65
|
+
type PluginPluginItem,
|
|
66
|
+
buildPluginBrowserItems,
|
|
67
|
+
pluginContentsRepo,
|
|
67
68
|
} from "../adapters/pluginsAdapter.js";
|
|
69
|
+
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
70
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
71
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
72
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
73
|
+
import { useMismatchModal } from "../hooks/useMismatchModal.js";
|
|
68
74
|
import {
|
|
69
|
-
renderPluginRow,
|
|
70
75
|
renderPluginDetail,
|
|
76
|
+
renderPluginRow,
|
|
71
77
|
} from "../renderers/pluginRenderers.js";
|
|
72
|
-
import {
|
|
78
|
+
import { useApp, useModal, useProgress } from "../state/AppContext.js";
|
|
79
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
80
|
+
import { asyncValue } from "../state/types.js";
|
|
73
81
|
import { theme } from "../theme.js";
|
|
74
82
|
|
|
75
83
|
/** Wraps the notice text in the warning-coloured row ScreenLayout renders. */
|
|
@@ -124,9 +132,7 @@ export function PluginsScreen() {
|
|
|
124
132
|
// answered is a non-event for the user, and reporting it would fire a
|
|
125
133
|
// warning about intended, working behaviour.
|
|
126
134
|
const unresolved = new Set(
|
|
127
|
-
pluginData
|
|
128
|
-
.filter((p) => p.updateCheckFailed)
|
|
129
|
-
.map((p) => p.marketplace),
|
|
135
|
+
pluginData.filter((p) => p.updateCheckFailed).map((p) => p.marketplace),
|
|
130
136
|
);
|
|
131
137
|
setCatalogFailures(
|
|
132
138
|
getMarketplaceFetchFailures().filter((f) =>
|
|
@@ -228,8 +234,16 @@ export function PluginsScreen() {
|
|
|
228
234
|
marketplaces: shownMarketplaces,
|
|
229
235
|
plugins: shownPlugins,
|
|
230
236
|
collapsedMarketplaces: pluginsState.collapsedMarketplaces,
|
|
237
|
+
expandedPlugins: pluginsState.expandedPlugins,
|
|
238
|
+
pluginContents: pluginsState.pluginContents,
|
|
231
239
|
});
|
|
232
|
-
}, [
|
|
240
|
+
}, [
|
|
241
|
+
shownMarketplaces,
|
|
242
|
+
shownPlugins,
|
|
243
|
+
pluginsState.collapsedMarketplaces,
|
|
244
|
+
pluginsState.expandedPlugins,
|
|
245
|
+
pluginsState.pluginContents,
|
|
246
|
+
]);
|
|
233
247
|
|
|
234
248
|
// Filter items by search query
|
|
235
249
|
const filteredItems = useMemo(() => {
|
|
@@ -273,7 +287,13 @@ export function PluginsScreen() {
|
|
|
273
287
|
} else if (item.kind === "plugin" && currentCatIncluded) {
|
|
274
288
|
if (matchedPluginIds.has(item.id)) {
|
|
275
289
|
const matched = fuzzyResults.find((r) => r.item.id === item.id);
|
|
276
|
-
result
|
|
290
|
+
// Skill rows are not in the filtered result, so an expanded arrow
|
|
291
|
+
// here would point at nothing.
|
|
292
|
+
result.push({
|
|
293
|
+
...item,
|
|
294
|
+
matches: matched?.matches,
|
|
295
|
+
isExpanded: false,
|
|
296
|
+
});
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
}
|
|
@@ -283,6 +303,101 @@ export function PluginsScreen() {
|
|
|
283
303
|
|
|
284
304
|
const selectableItems = useMemo(() => filteredItems, [filteredItems]);
|
|
285
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Read the file behind whichever component row the cursor has settled on.
|
|
308
|
+
*
|
|
309
|
+
* Debounced, and that is not cosmetic: each read is one GitHub API call, an
|
|
310
|
+
* unauthenticated client gets 60 an hour, and holding ↓ through the 286
|
|
311
|
+
* skills of a large plugin would otherwise fire a call per keypress and
|
|
312
|
+
* exhaust the budget for every other marketplace on screen. 200ms is below
|
|
313
|
+
* the point a deliberate move feels laggy and above key-repeat.
|
|
314
|
+
*
|
|
315
|
+
* MCP servers are skipped: their whole definition is the JSON already read
|
|
316
|
+
* during the contents listing, so there is no document to fetch.
|
|
317
|
+
*/
|
|
318
|
+
const selectedRow = selectableItems[pluginsState.selectedIndex];
|
|
319
|
+
const selectedComponent =
|
|
320
|
+
selectedRow?.kind === "component" && selectedRow.component.kind !== "mcp"
|
|
321
|
+
? (selectedRow as PluginComponentItem)
|
|
322
|
+
: undefined;
|
|
323
|
+
const detailKey = selectedComponent?.detailKey;
|
|
324
|
+
const detailRepo = selectedComponent?.repo;
|
|
325
|
+
const detailPath = selectedComponent?.component.repoPath;
|
|
326
|
+
const haveDetail = detailKey
|
|
327
|
+
? pluginsState.skillDetails.has(detailKey)
|
|
328
|
+
: true;
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (!detailKey || !detailRepo || !detailPath) return;
|
|
332
|
+
if (haveDetail) return;
|
|
333
|
+
|
|
334
|
+
const timer = setTimeout(() => {
|
|
335
|
+
dispatch({ type: "PLUGINS_SKILL_DETAIL_LOADING", key: detailKey });
|
|
336
|
+
fetchPluginSkillDetail(detailRepo, detailPath)
|
|
337
|
+
.then((detail) =>
|
|
338
|
+
dispatch({
|
|
339
|
+
type: "PLUGINS_SKILL_DETAIL_SUCCESS",
|
|
340
|
+
key: detailKey,
|
|
341
|
+
detail,
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
.catch((error: unknown) =>
|
|
345
|
+
dispatch({
|
|
346
|
+
type: "PLUGINS_SKILL_DETAIL_ERROR",
|
|
347
|
+
key: detailKey,
|
|
348
|
+
error: error instanceof Error ? error.message : String(error),
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
}, 200);
|
|
352
|
+
|
|
353
|
+
return () => clearTimeout(timer);
|
|
354
|
+
}, [dispatch, haveDetail, detailKey, detailPath, detailRepo]);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Open or close a plugin's contents, fetching them the first time.
|
|
358
|
+
*
|
|
359
|
+
* The listing is kept per plugin id for the life of the session rather than
|
|
360
|
+
* refetched on each open: it costs a GitHub Tree API call, and an
|
|
361
|
+
* unauthenticated client gets 60 an hour across every marketplace on screen.
|
|
362
|
+
*/
|
|
363
|
+
const togglePluginContents = useCallback(
|
|
364
|
+
(item: PluginPluginItem) => {
|
|
365
|
+
const pluginId = item.plugin.id;
|
|
366
|
+
dispatch({ type: "PLUGINS_TOGGLE_PLUGIN_CONTENTS", pluginId });
|
|
367
|
+
|
|
368
|
+
if (item.isExpanded) return;
|
|
369
|
+
// A previous *success* is kept for the session. A previous failure is
|
|
370
|
+
// not: the usual cause is the GitHub hourly limit, which clears on its
|
|
371
|
+
// own, and caching the failure made a temporary condition look permanent
|
|
372
|
+
// with no way back except reloading the whole screen.
|
|
373
|
+
if (pluginsState.pluginContents.get(pluginId)?.status === "success") {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const marketplace = shownMarketplaces?.find(
|
|
378
|
+
(m) => m.name === item.plugin.marketplace,
|
|
379
|
+
);
|
|
380
|
+
const repo = marketplace
|
|
381
|
+
? pluginContentsRepo(item.plugin, marketplace)
|
|
382
|
+
: undefined;
|
|
383
|
+
if (!repo) return;
|
|
384
|
+
|
|
385
|
+
dispatch({ type: "PLUGINS_CONTENTS_LOADING", pluginId });
|
|
386
|
+
fetchPluginContents(repo, item.plugin.source, item.plugin.marketplace)
|
|
387
|
+
.then((components) =>
|
|
388
|
+
dispatch({ type: "PLUGINS_CONTENTS_SUCCESS", pluginId, components }),
|
|
389
|
+
)
|
|
390
|
+
.catch((error: unknown) =>
|
|
391
|
+
dispatch({
|
|
392
|
+
type: "PLUGINS_CONTENTS_ERROR",
|
|
393
|
+
pluginId,
|
|
394
|
+
error: error instanceof Error ? error.message : String(error),
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
[dispatch, pluginsState.pluginContents, shownMarketplaces],
|
|
399
|
+
);
|
|
400
|
+
|
|
286
401
|
// Keyboard handling
|
|
287
402
|
useKeyboard((event) => {
|
|
288
403
|
if (state.modal) return;
|
|
@@ -342,11 +457,10 @@ export function PluginsScreen() {
|
|
|
342
457
|
}
|
|
343
458
|
|
|
344
459
|
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
selectableItems[pluginsState.selectedIndex]?.kind === "category"
|
|
460
|
+
event.name === "left" ||
|
|
461
|
+
event.name === "right" ||
|
|
462
|
+
event.name === "<" ||
|
|
463
|
+
event.name === ">"
|
|
350
464
|
) {
|
|
351
465
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
352
466
|
if (item?.kind === "category") {
|
|
@@ -354,8 +468,12 @@ export function PluginsScreen() {
|
|
|
354
468
|
type: "PLUGINS_TOGGLE_MARKETPLACE",
|
|
355
469
|
name: item.marketplace.name,
|
|
356
470
|
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (item?.kind === "plugin" && item.isExpandable) {
|
|
474
|
+
togglePluginContents(item);
|
|
475
|
+
return;
|
|
357
476
|
}
|
|
358
|
-
return;
|
|
359
477
|
}
|
|
360
478
|
|
|
361
479
|
if (isSearchActive) {
|
|
@@ -1257,9 +1375,19 @@ export function PluginsScreen() {
|
|
|
1257
1375
|
)}
|
|
1258
1376
|
</box>
|
|
1259
1377
|
}
|
|
1378
|
+
// A skill's panel now holds its whole SKILL.md, which is long enough to
|
|
1379
|
+
// scroll. Without this the pane kept the previous row's scroll offset,
|
|
1380
|
+
// so moving to the next skill landed you in the middle of a document
|
|
1381
|
+
// you had not started reading.
|
|
1382
|
+
detailKey={selectedItem?.id}
|
|
1260
1383
|
detailPanel={renderPluginDetail(
|
|
1261
1384
|
selectedItem,
|
|
1262
1385
|
pluginsState.collapsedMarketplaces,
|
|
1386
|
+
pluginsState.pluginContents,
|
|
1387
|
+
pluginsState.skillDetails,
|
|
1388
|
+
// ScreenLayout gives the detail pane 50% of the terminal, then takes
|
|
1389
|
+
// a column for its left padding and one for the scrollbar.
|
|
1390
|
+
Math.max(24, Math.floor(dimensions.terminalWidth / 2) - 3),
|
|
1263
1391
|
)}
|
|
1264
1392
|
/>
|
|
1265
1393
|
);
|
package/src/ui/state/reducer.ts
CHANGED
|
@@ -21,6 +21,9 @@ export const initialState: AppState = {
|
|
|
21
21
|
plugins: {
|
|
22
22
|
scope: "project",
|
|
23
23
|
collapsedMarketplaces: new Set(),
|
|
24
|
+
expandedPlugins: new Set(),
|
|
25
|
+
pluginContents: new Map(),
|
|
26
|
+
skillDetails: new Map(),
|
|
24
27
|
selectedIndex: 0,
|
|
25
28
|
searchQuery: "",
|
|
26
29
|
marketplaces: { status: "idle" },
|
|
@@ -123,6 +126,59 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
123
126
|
};
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
case "PLUGINS_TOGGLE_PLUGIN_CONTENTS": {
|
|
130
|
+
const expanded = new Set(state.plugins.expandedPlugins);
|
|
131
|
+
if (expanded.has(action.pluginId)) {
|
|
132
|
+
expanded.delete(action.pluginId);
|
|
133
|
+
} else {
|
|
134
|
+
expanded.add(action.pluginId);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
plugins: { ...state.plugins, expandedPlugins: expanded },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case "PLUGINS_CONTENTS_LOADING":
|
|
143
|
+
case "PLUGINS_CONTENTS_SUCCESS":
|
|
144
|
+
case "PLUGINS_CONTENTS_ERROR": {
|
|
145
|
+
const pluginContents = new Map(state.plugins.pluginContents);
|
|
146
|
+
if (action.type === "PLUGINS_CONTENTS_LOADING") {
|
|
147
|
+
pluginContents.set(action.pluginId, { status: "loading" });
|
|
148
|
+
} else if (action.type === "PLUGINS_CONTENTS_SUCCESS") {
|
|
149
|
+
pluginContents.set(action.pluginId, {
|
|
150
|
+
status: "success",
|
|
151
|
+
components: action.components,
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
pluginContents.set(action.pluginId, {
|
|
155
|
+
status: "error",
|
|
156
|
+
error: action.error,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { ...state, plugins: { ...state.plugins, pluginContents } };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "PLUGINS_SKILL_DETAIL_LOADING":
|
|
163
|
+
case "PLUGINS_SKILL_DETAIL_SUCCESS":
|
|
164
|
+
case "PLUGINS_SKILL_DETAIL_ERROR": {
|
|
165
|
+
const skillDetails = new Map(state.plugins.skillDetails);
|
|
166
|
+
if (action.type === "PLUGINS_SKILL_DETAIL_LOADING") {
|
|
167
|
+
skillDetails.set(action.key, { status: "loading" });
|
|
168
|
+
} else if (action.type === "PLUGINS_SKILL_DETAIL_SUCCESS") {
|
|
169
|
+
skillDetails.set(action.key, {
|
|
170
|
+
status: "success",
|
|
171
|
+
detail: action.detail,
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
skillDetails.set(action.key, {
|
|
175
|
+
status: "error",
|
|
176
|
+
error: action.error,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return { ...state, plugins: { ...state.plugins, skillDetails } };
|
|
180
|
+
}
|
|
181
|
+
|
|
126
182
|
case "PLUGINS_SELECT":
|
|
127
183
|
return {
|
|
128
184
|
...state,
|
package/src/ui/state/types.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { PluginInfo } from "../../services/plugin-manager.js";
|
|
2
2
|
import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
|
|
3
|
+
import type {
|
|
4
|
+
PluginComponent,
|
|
5
|
+
PluginSkillDetail,
|
|
6
|
+
} from "../../services/skills-manager.js";
|
|
3
7
|
import type { StylesSnapshot } from "../../services/styles-manager.js";
|
|
4
8
|
import type {
|
|
5
9
|
Marketplace,
|
|
@@ -128,9 +132,32 @@ export interface ProgressState {
|
|
|
128
132
|
// Screen State Types
|
|
129
133
|
// ============================================================================
|
|
130
134
|
|
|
135
|
+
/** Read-only listing of what one plugin ships, fetched on demand. */
|
|
136
|
+
export interface PluginContentsEntry {
|
|
137
|
+
status: "loading" | "success" | "error";
|
|
138
|
+
components?: PluginComponent[];
|
|
139
|
+
error?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One skill's SKILL.md, fetched when the cursor settles on its row. */
|
|
143
|
+
export interface PluginSkillDetailEntry {
|
|
144
|
+
status: "loading" | "success" | "error";
|
|
145
|
+
detail?: PluginSkillDetail;
|
|
146
|
+
error?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
export interface PluginsScreenState {
|
|
132
150
|
scope: "project" | "global";
|
|
133
151
|
collapsedMarketplaces: Set<string>;
|
|
152
|
+
/**
|
|
153
|
+
* Plugin ids whose contents list is open. Opposite default to
|
|
154
|
+
* `collapsedMarketplaces`: listing a plugin's contents costs an HTTP call,
|
|
155
|
+
* so it stays shut until asked for.
|
|
156
|
+
*/
|
|
157
|
+
expandedPlugins: Set<string>;
|
|
158
|
+
pluginContents: Map<string, PluginContentsEntry>;
|
|
159
|
+
/** SKILL.md content, keyed `<repo>/<repoPath>`. */
|
|
160
|
+
skillDetails: Map<string, PluginSkillDetailEntry>;
|
|
134
161
|
selectedIndex: number;
|
|
135
162
|
searchQuery: string;
|
|
136
163
|
marketplaces: AsyncData<Marketplace[]>;
|
|
@@ -274,6 +301,21 @@ export type AppAction =
|
|
|
274
301
|
| { type: "PLUGINS_SET_SCOPE"; scope: "project" | "global" }
|
|
275
302
|
| { type: "PLUGINS_TOGGLE_SCOPE" }
|
|
276
303
|
| { type: "PLUGINS_TOGGLE_MARKETPLACE"; name: string }
|
|
304
|
+
| { type: "PLUGINS_TOGGLE_PLUGIN_CONTENTS"; pluginId: string }
|
|
305
|
+
| { type: "PLUGINS_CONTENTS_LOADING"; pluginId: string }
|
|
306
|
+
| {
|
|
307
|
+
type: "PLUGINS_CONTENTS_SUCCESS";
|
|
308
|
+
pluginId: string;
|
|
309
|
+
components: PluginComponent[];
|
|
310
|
+
}
|
|
311
|
+
| { type: "PLUGINS_CONTENTS_ERROR"; pluginId: string; error: string }
|
|
312
|
+
| { type: "PLUGINS_SKILL_DETAIL_LOADING"; key: string }
|
|
313
|
+
| {
|
|
314
|
+
type: "PLUGINS_SKILL_DETAIL_SUCCESS";
|
|
315
|
+
key: string;
|
|
316
|
+
detail: PluginSkillDetail;
|
|
317
|
+
}
|
|
318
|
+
| { type: "PLUGINS_SKILL_DETAIL_ERROR"; key: string; error: string }
|
|
277
319
|
| { type: "PLUGINS_SELECT"; index: number }
|
|
278
320
|
| { type: "PLUGINS_SET_SEARCH"; query: string }
|
|
279
321
|
| { type: "PLUGINS_DATA_LOADING" }
|
package/src/ui/theme-mode.ts
CHANGED
|
@@ -71,3 +71,70 @@ export function scopeOffFill(): string {
|
|
|
71
71
|
if (detected === "dark") return SCOPE_OFF_FILL.dark;
|
|
72
72
|
return SCOPE_OFF_FALLBACK;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Badge colours for the four plugin component kinds.
|
|
77
|
+
*
|
|
78
|
+
* These are chips, not accents: they sit on hundreds of rows at once, so the
|
|
79
|
+
* fill has to be a *wash* of the hue rather than the hue itself. That runs into
|
|
80
|
+
* the same wall `SCOPE_OFF_FILL` documents above — a wash is defined relative to
|
|
81
|
+
* the page, ANSI has no alpha, so the two ends are picked by hand.
|
|
82
|
+
*
|
|
83
|
+
* Hues are deliberately far apart (purple / teal / blue / amber) for the reason
|
|
84
|
+
* the scope squares are: one character of colour carries no readable hue shift,
|
|
85
|
+
* only a readable hue *difference*.
|
|
86
|
+
*
|
|
87
|
+
* The glyph colour is set per mode too. Reusing the vivid accent over a dark
|
|
88
|
+
* wash would drop it below the contrast the palette guarantees against the
|
|
89
|
+
* page — the accent's measured ratio is against `CONTRAST_REFERENCE`, not
|
|
90
|
+
* against another tint laid on top of it.
|
|
91
|
+
*/
|
|
92
|
+
export const COMPONENT_BADGE = {
|
|
93
|
+
skill: {
|
|
94
|
+
light: { bg: "#E9DCF5", fg: "#6B21A8" },
|
|
95
|
+
dark: { bg: "#2E2140", fg: "#C4A5F0" },
|
|
96
|
+
},
|
|
97
|
+
command: {
|
|
98
|
+
light: { bg: "#D3E7EC", fg: "#0E5F73" },
|
|
99
|
+
dark: { bg: "#123037", fg: "#7DD3E0" },
|
|
100
|
+
},
|
|
101
|
+
agent: {
|
|
102
|
+
light: { bg: "#DCE4F7", fg: "#1D4ED8" },
|
|
103
|
+
dark: { bg: "#1B2740", fg: "#A5C0F5" },
|
|
104
|
+
},
|
|
105
|
+
mcp: {
|
|
106
|
+
light: { bg: "#F0E2CE", fg: "#92400E" },
|
|
107
|
+
dark: { bg: "#3A2A15", fg: "#E5B877" },
|
|
108
|
+
},
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
111
|
+
export type ComponentBadgeKind = keyof typeof COMPONENT_BADGE;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The two reference pages, mirrored from `theme.ts`'s `CONTRAST_REFERENCE`.
|
|
115
|
+
*
|
|
116
|
+
* Duplicated deliberately: `theme.ts` imports opentui's RGBA, which drags the
|
|
117
|
+
* renderer into anything that touches it. This module is pure data by design so
|
|
118
|
+
* the colour tests can run without a terminal. A test asserts the two agree.
|
|
119
|
+
*/
|
|
120
|
+
export const CONTRAST_REFERENCE_KEYS = {
|
|
121
|
+
light: "#FAFAD2",
|
|
122
|
+
dark: "#1C1C1E",
|
|
123
|
+
} as const;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Chip colours for one component kind, or `null` when the terminal never
|
|
127
|
+
* answered the OSC query.
|
|
128
|
+
*
|
|
129
|
+
* Null means *no chip* — the caller falls back to a bare glyph. That follows the
|
|
130
|
+
* rule at the top of this file: an undetected terminal gets today's behaviour
|
|
131
|
+
* rather than a guess, and here the wrong guess paints a wash of the wrong end
|
|
132
|
+
* behind every row in the list.
|
|
133
|
+
*/
|
|
134
|
+
export function componentBadge(
|
|
135
|
+
kind: ComponentBadgeKind,
|
|
136
|
+
): { bg: string; fg: string } | null {
|
|
137
|
+
if (detected === "light") return COMPONENT_BADGE[kind].light;
|
|
138
|
+
if (detected === "dark") return COMPONENT_BADGE[kind].dark;
|
|
139
|
+
return null;
|
|
140
|
+
}
|