magus-cli 0.0.0-placeholder → 7.0.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/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
|
|
3
|
+
import {
|
|
4
|
+
compareVersions,
|
|
5
|
+
isEnabledButNotInstalled,
|
|
6
|
+
isInstalledInScope,
|
|
7
|
+
} from "../../services/plugin-manager.js";
|
|
8
|
+
import type { ScopeStatus } from "../../services/plugin-manager.js";
|
|
9
|
+
import type { PluginComponentKind } from "../../services/skills-manager.js";
|
|
10
|
+
import type {
|
|
11
|
+
PluginUpdateAction,
|
|
12
|
+
PluginUpdateItem,
|
|
13
|
+
} from "../../services/update-plan.js";
|
|
14
|
+
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
15
|
+
import type { PluginRelease } from "../../types/index.js";
|
|
16
|
+
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
17
|
+
import type {
|
|
18
|
+
PluginBrowserItem,
|
|
19
|
+
PluginCategoryItem,
|
|
20
|
+
PluginComponentItem,
|
|
21
|
+
PluginContentsStatusItem,
|
|
22
|
+
PluginPluginItem,
|
|
23
|
+
} from "../adapters/pluginsAdapter.js";
|
|
24
|
+
import { CategoryHeader } from "../components/CategoryHeader.js";
|
|
25
|
+
import {
|
|
26
|
+
ActionHints,
|
|
27
|
+
DetailSection,
|
|
28
|
+
KeyValueLine,
|
|
29
|
+
MetaText,
|
|
30
|
+
ScopeSquares,
|
|
31
|
+
SelectableRow,
|
|
32
|
+
} from "../components/primitives/index.js";
|
|
33
|
+
import type {
|
|
34
|
+
PluginContentsEntry,
|
|
35
|
+
PluginSkillDetailEntry,
|
|
36
|
+
} from "../state/types.js";
|
|
37
|
+
import { componentBadge as badgeColors } from "../theme-mode.js";
|
|
38
|
+
import { type UiColor, theme } from "../theme.js";
|
|
39
|
+
import { renderMarkdown } from "./markdownRenderer.js";
|
|
40
|
+
import { type PluginRowSegment, pluginRowText } from "./pluginRowText.js";
|
|
41
|
+
import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
|
|
42
|
+
|
|
43
|
+
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function categoryRow(
|
|
46
|
+
item: PluginCategoryItem,
|
|
47
|
+
isSelected: boolean,
|
|
48
|
+
): React.ReactNode {
|
|
49
|
+
const mp = item.marketplace;
|
|
50
|
+
// Marketplaces that declare a `metadata.version` render "Name v8.0.0 (15)";
|
|
51
|
+
// those without keep the plain "Name (15)" form.
|
|
52
|
+
const version = getMarketplaceVersion(mp.name);
|
|
53
|
+
|
|
54
|
+
if (isSelected) {
|
|
55
|
+
const arrow = item.isExpanded ? "▼" : "▶";
|
|
56
|
+
const versionLabel = version ? ` v${version}` : "";
|
|
57
|
+
const count = item.pluginCount > 0 ? ` (${item.pluginCount})` : "";
|
|
58
|
+
return (
|
|
59
|
+
<SelectableRow selected={true}>
|
|
60
|
+
<strong>
|
|
61
|
+
{" "}
|
|
62
|
+
{arrow} {mp.displayName}
|
|
63
|
+
{versionLabel}
|
|
64
|
+
{count}{" "}
|
|
65
|
+
</strong>
|
|
66
|
+
</SelectableRow>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Map tone to a statusColor for CategoryHeader (legacy component)
|
|
71
|
+
const statusColorMap: Record<PluginCategoryItem["tone"], UiColor> = {
|
|
72
|
+
yellow: theme.colors.warning,
|
|
73
|
+
gray: theme.colors.muted,
|
|
74
|
+
green: theme.colors.success,
|
|
75
|
+
red: theme.colors.danger,
|
|
76
|
+
purple: theme.colors.accent,
|
|
77
|
+
teal: theme.colors.info,
|
|
78
|
+
byline: theme.colors.byline,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<CategoryHeader
|
|
83
|
+
title={mp.displayName}
|
|
84
|
+
version={version}
|
|
85
|
+
expanded={item.isExpanded}
|
|
86
|
+
count={item.pluginCount}
|
|
87
|
+
status={item.badge}
|
|
88
|
+
statusColor={statusColorMap[item.tone]}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function categoryDetail(
|
|
94
|
+
item: PluginCategoryItem,
|
|
95
|
+
collapsedMarketplaces: Set<string>,
|
|
96
|
+
): React.ReactNode {
|
|
97
|
+
const mp = item.marketplace;
|
|
98
|
+
const isEnabled = item.marketplaceEnabled;
|
|
99
|
+
const isCollapsed = collapsedMarketplaces.has(mp.name);
|
|
100
|
+
const hasPlugins = item.pluginCount > 0;
|
|
101
|
+
|
|
102
|
+
let actionHint = "Add";
|
|
103
|
+
if (isEnabled) {
|
|
104
|
+
if (isCollapsed) {
|
|
105
|
+
actionHint = "Expand";
|
|
106
|
+
} else if (hasPlugins) {
|
|
107
|
+
actionHint = "Collapse";
|
|
108
|
+
} else {
|
|
109
|
+
actionHint = "Remove";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<box flexDirection="column">
|
|
115
|
+
<text fg={theme.colors.info}>
|
|
116
|
+
<strong>
|
|
117
|
+
{mp.displayName}
|
|
118
|
+
{item.badge ? ` ${item.badge}` : ""}
|
|
119
|
+
</strong>
|
|
120
|
+
</text>
|
|
121
|
+
<text fg={theme.colors.muted}>{mp.description || "No description"}</text>
|
|
122
|
+
<text fg={isEnabled ? theme.colors.success : theme.colors.muted}>
|
|
123
|
+
{isEnabled ? "● Added" : "○ Not added"}
|
|
124
|
+
</text>
|
|
125
|
+
<text fg={theme.colors.link}>github.com/{mp.source.repo}</text>
|
|
126
|
+
<text fg={theme.colors.text}>Plugins: {item.pluginCount}</text>
|
|
127
|
+
<ActionHints
|
|
128
|
+
hints={[
|
|
129
|
+
{
|
|
130
|
+
key: "Enter",
|
|
131
|
+
label: actionHint,
|
|
132
|
+
tone: isEnabled ? "primary" : "default",
|
|
133
|
+
},
|
|
134
|
+
]}
|
|
135
|
+
/>
|
|
136
|
+
{isEnabled ? (
|
|
137
|
+
<box>
|
|
138
|
+
<text fg={theme.colors.muted}>← → to expand/collapse</text>
|
|
139
|
+
</box>
|
|
140
|
+
) : null}
|
|
141
|
+
</box>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Plugin renderers ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The row's text after the name, drawn exactly as `pluginRowText` produced it.
|
|
149
|
+
*
|
|
150
|
+
* On a selected row the tone's hue is surrendered to the selection (see
|
|
151
|
+
* MetaText); the words are the same either way.
|
|
152
|
+
*/
|
|
153
|
+
function RowSegments({
|
|
154
|
+
segments,
|
|
155
|
+
selected,
|
|
156
|
+
}: {
|
|
157
|
+
segments: PluginRowSegment[];
|
|
158
|
+
selected: boolean;
|
|
159
|
+
}): React.ReactNode {
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{segments.map((seg, i) => {
|
|
163
|
+
const key = `${i}:${seg.text}`;
|
|
164
|
+
if (seg.tone === "text") return <span key={key}>{seg.text}</span>;
|
|
165
|
+
if (seg.tone === "info") {
|
|
166
|
+
return (
|
|
167
|
+
<span
|
|
168
|
+
key={key}
|
|
169
|
+
fg={selected ? theme.selection.fg : theme.colors.info}
|
|
170
|
+
>
|
|
171
|
+
{seg.text}
|
|
172
|
+
</span>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return (
|
|
176
|
+
<MetaText
|
|
177
|
+
key={key}
|
|
178
|
+
text={seg.text}
|
|
179
|
+
tone={seg.tone}
|
|
180
|
+
selected={selected}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function pluginRow(
|
|
189
|
+
item: PluginPluginItem,
|
|
190
|
+
isSelected: boolean,
|
|
191
|
+
): React.ReactNode {
|
|
192
|
+
const { plugin } = item;
|
|
193
|
+
const hasUser = !!plugin.userScope?.enabled;
|
|
194
|
+
const hasProject = !!plugin.projectScope?.enabled;
|
|
195
|
+
const hasLocal = !!plugin.localScope?.enabled;
|
|
196
|
+
const hasAnyScope = hasUser || hasProject || hasLocal;
|
|
197
|
+
|
|
198
|
+
// Everything after the name — version, history badge, status — comes from
|
|
199
|
+
// ONE pure function over the classifier's answer, and is drawn verbatim in
|
|
200
|
+
// every branch below. No branch here decides plugin state.
|
|
201
|
+
const { segments } = pluginRowText(item);
|
|
202
|
+
|
|
203
|
+
// Same glyphs as a marketplace row, in a cell that is always two columns
|
|
204
|
+
// wide. Both halves matter: a different arrow at each level reads as a
|
|
205
|
+
// different kind of control, and drawing the cell only on the selected row
|
|
206
|
+
// shifted every plugin name two columns sideways as the cursor passed over
|
|
207
|
+
// it.
|
|
208
|
+
const arrow = item.isExpandable ? (item.isExpanded ? "▼ " : "▶ ") : " ";
|
|
209
|
+
|
|
210
|
+
if (isSelected) {
|
|
211
|
+
return (
|
|
212
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
213
|
+
{" "}
|
|
214
|
+
<ScopeSquares
|
|
215
|
+
user={hasUser}
|
|
216
|
+
project={hasProject}
|
|
217
|
+
local={hasLocal}
|
|
218
|
+
selected={true}
|
|
219
|
+
/>{" "}
|
|
220
|
+
{arrow}
|
|
221
|
+
{plugin.name}
|
|
222
|
+
<RowSegments segments={segments} selected={true} />{" "}
|
|
223
|
+
</text>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fuzzy highlight
|
|
228
|
+
const nameSegments = item.matches
|
|
229
|
+
? highlightMatches(plugin.name, item.matches)
|
|
230
|
+
: null;
|
|
231
|
+
const displayName = nameSegments
|
|
232
|
+
? nameSegments.map((seg) => seg.text).join("")
|
|
233
|
+
: plugin.name;
|
|
234
|
+
|
|
235
|
+
if (plugin.isOrphaned) {
|
|
236
|
+
// The red squares are the presence marker for "published by no marketplace
|
|
237
|
+
// we read". WHAT is known about it — moved, withdrawn, or its marketplace
|
|
238
|
+
// was never read — is the status suffix, the same words the CLI prints.
|
|
239
|
+
return (
|
|
240
|
+
<text fg={theme.colors.text}>
|
|
241
|
+
<span fg={theme.colors.danger}> ■■■ </span>
|
|
242
|
+
{/* An orphan is never expandable, but it keeps the arrow cell so its
|
|
243
|
+
name lands in the same column as every other plugin's. */}
|
|
244
|
+
<span>{" "}</span>
|
|
245
|
+
<span fg={theme.colors.muted}>{displayName}</span>
|
|
246
|
+
<RowSegments segments={segments} selected={false} />
|
|
247
|
+
</text>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const installedSomewhere =
|
|
252
|
+
isInstalledInScope(plugin.userScope) ||
|
|
253
|
+
isInstalledInScope(plugin.projectScope) ||
|
|
254
|
+
isInstalledInScope(plugin.localScope);
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<text fg={theme.colors.text}>
|
|
258
|
+
<span> </span>
|
|
259
|
+
<ScopeSquares user={hasUser} project={hasProject} local={hasLocal} />
|
|
260
|
+
<span> </span>
|
|
261
|
+
<span fg={theme.colors.dim}>{arrow}</span>
|
|
262
|
+
<span
|
|
263
|
+
fg={
|
|
264
|
+
hasAnyScope && installedSomewhere
|
|
265
|
+
? theme.colors.text
|
|
266
|
+
: theme.colors.muted
|
|
267
|
+
}
|
|
268
|
+
>
|
|
269
|
+
{displayName}
|
|
270
|
+
</span>
|
|
271
|
+
<RowSegments segments={segments} selected={false} />
|
|
272
|
+
</text>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ACTION_LABEL: Record<PluginUpdateAction, string> = {
|
|
277
|
+
install: "NOT INSTALLED",
|
|
278
|
+
update: "UPDATE AVAILABLE",
|
|
279
|
+
repair: "NEEDS REINSTALL",
|
|
280
|
+
current: "UP TO DATE",
|
|
281
|
+
unknown: "UNKNOWN",
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** Resolved at render time, so a theme switch is honoured. */
|
|
285
|
+
function toneColor(tone: StatusTone): UiColor {
|
|
286
|
+
switch (tone) {
|
|
287
|
+
case "danger":
|
|
288
|
+
return theme.colors.danger;
|
|
289
|
+
case "warning":
|
|
290
|
+
return theme.colors.warning;
|
|
291
|
+
case "success":
|
|
292
|
+
return theme.colors.success;
|
|
293
|
+
case "info":
|
|
294
|
+
return theme.colors.info;
|
|
295
|
+
case "muted":
|
|
296
|
+
return theme.colors.muted;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The detail panel's `Status:` line: the classifier's action, and its note
|
|
302
|
+
* VERBATIM — the same words `magus update` prints for this plugin.
|
|
303
|
+
*/
|
|
304
|
+
function StatusLine({
|
|
305
|
+
status,
|
|
306
|
+
}: {
|
|
307
|
+
status: PluginUpdateItem | null;
|
|
308
|
+
}): React.ReactNode {
|
|
309
|
+
if (!status) return null;
|
|
310
|
+
const tone =
|
|
311
|
+
status.action === "current" ? "success" : pluginStatusText(status).tone;
|
|
312
|
+
const detail = status.note
|
|
313
|
+
? status.note
|
|
314
|
+
: status.action === "update" && status.target
|
|
315
|
+
? `→ v${status.target}`
|
|
316
|
+
: "";
|
|
317
|
+
return (
|
|
318
|
+
<KeyValueLine
|
|
319
|
+
label="Status"
|
|
320
|
+
value={
|
|
321
|
+
<span>
|
|
322
|
+
<span fg={toneColor(tone)}>{ACTION_LABEL[status.action]}</span>
|
|
323
|
+
{detail ? <span fg={theme.colors.text}> {detail}</span> : null}
|
|
324
|
+
</span>
|
|
325
|
+
}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function pluginDetail(
|
|
331
|
+
item: PluginPluginItem,
|
|
332
|
+
fetched?: PluginContentsEntry,
|
|
333
|
+
): React.ReactNode {
|
|
334
|
+
const { plugin, status } = item;
|
|
335
|
+
// "Installed" has to mean the same thing here as in the list row. Reading the
|
|
336
|
+
// `enabledPlugins` flag alone made this panel print "● Installed" for the
|
|
337
|
+
// exact plugin the row beside it was labelling "not installed".
|
|
338
|
+
const isInstalled =
|
|
339
|
+
isInstalledInScope(plugin.userScope) ||
|
|
340
|
+
isInstalledInScope(plugin.projectScope) ||
|
|
341
|
+
isInstalledInScope(plugin.localScope);
|
|
342
|
+
const brokenInstall = isEnabledButNotInstalled(plugin);
|
|
343
|
+
// The ⬆ marker and the U hint claim that pressing U does something. That is
|
|
344
|
+
// exactly the classifier's update/repair, and nothing else.
|
|
345
|
+
const needsWork = status?.action === "update" || status?.action === "repair";
|
|
346
|
+
|
|
347
|
+
// Published by no marketplace we read. The status line says which of the
|
|
348
|
+
// three things that means — moved, withdrawn, or its marketplace unread.
|
|
349
|
+
if (plugin.isOrphaned) {
|
|
350
|
+
return (
|
|
351
|
+
<box flexDirection="column">
|
|
352
|
+
<box justifyContent="center">
|
|
353
|
+
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
354
|
+
<strong> {plugin.name} — NOT LISTED </strong>
|
|
355
|
+
</text>
|
|
356
|
+
</box>
|
|
357
|
+
<box marginTop={1}>
|
|
358
|
+
<StatusLine status={status} />
|
|
359
|
+
</box>
|
|
360
|
+
<box marginTop={1}>
|
|
361
|
+
<text fg={theme.colors.warning}>
|
|
362
|
+
No marketplace magus read lists this plugin.
|
|
363
|
+
</text>
|
|
364
|
+
</box>
|
|
365
|
+
<box marginTop={1}>
|
|
366
|
+
<text fg={theme.colors.muted}>
|
|
367
|
+
It is still installed or referenced in your settings. Press d to
|
|
368
|
+
uninstall and clean up, or to migrate it if it moved.
|
|
369
|
+
</text>
|
|
370
|
+
</box>
|
|
371
|
+
<ActionHints
|
|
372
|
+
hints={[
|
|
373
|
+
{
|
|
374
|
+
key: "d",
|
|
375
|
+
label: isInstalled
|
|
376
|
+
? "Remove from all scopes"
|
|
377
|
+
: "Clean up stale reference",
|
|
378
|
+
tone: "danger",
|
|
379
|
+
},
|
|
380
|
+
]}
|
|
381
|
+
/>
|
|
382
|
+
</box>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Component counts. A listing fetched from the repo tree wins over the
|
|
387
|
+
// manifest scan on every count it can supply: the scan only sees a
|
|
388
|
+
// marketplace that is cloned locally, a `featured` marketplace the user has
|
|
389
|
+
// not added has no clone at all, and the scan misses an MCP server declared
|
|
390
|
+
// in `.mcp.json` rather than an `mcp-servers/` directory — which is how 5 of
|
|
391
|
+
// this marketplace's 7 MCP plugins declare theirs, mnemex included.
|
|
392
|
+
const listed =
|
|
393
|
+
fetched?.status === "success" ? (fetched.components ?? null) : null;
|
|
394
|
+
const countOf = (kind: PluginComponentKind, fallback?: string[]) =>
|
|
395
|
+
listed
|
|
396
|
+
? listed.filter((c) => c.kind === kind).length
|
|
397
|
+
: (fallback?.length ?? 0);
|
|
398
|
+
|
|
399
|
+
const components: string[] = [];
|
|
400
|
+
const agentCount = countOf("agent", plugin.agents);
|
|
401
|
+
const commandCount = countOf("command", plugin.commands);
|
|
402
|
+
const skillCount = countOf("skill", plugin.skills);
|
|
403
|
+
const mcpCount = countOf("mcp", plugin.mcpServers);
|
|
404
|
+
/** "1 agent", "2 agents" — a count of one is common enough to read wrong. */
|
|
405
|
+
const plural = (n: number, noun: string) =>
|
|
406
|
+
`${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
407
|
+
if (agentCount) components.push(plural(agentCount, "agent"));
|
|
408
|
+
if (commandCount) components.push(plural(commandCount, "command"));
|
|
409
|
+
if (skillCount) components.push(plural(skillCount, "skill"));
|
|
410
|
+
if (mcpCount) components.push(plural(mcpCount, "MCP server"));
|
|
411
|
+
if (plugin.lspServers && Object.keys(plugin.lspServers).length) {
|
|
412
|
+
components.push(`${Object.keys(plugin.lspServers).length} LSP`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const showVersion = isKnownVersion(plugin.version ?? undefined);
|
|
416
|
+
// The shared comparator, not `!==`: "1.2" and "1.2.0" are the same version,
|
|
417
|
+
// and the classifier's `Status:` line right above says so. Raw inequality
|
|
418
|
+
// made this line contradict it. `null` (no shared ordering) is a genuine
|
|
419
|
+
// difference and shows.
|
|
420
|
+
const showInstalledVersion =
|
|
421
|
+
isInstalled &&
|
|
422
|
+
isKnownVersion(plugin.installedVersion) &&
|
|
423
|
+
plugin.installedVersion !== undefined &&
|
|
424
|
+
plugin.version !== null &&
|
|
425
|
+
compareVersions(plugin.version, plugin.installedVersion) !== 0;
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<box flexDirection="column">
|
|
429
|
+
{/* Plugin name header */}
|
|
430
|
+
<box justifyContent="center">
|
|
431
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
432
|
+
<strong>
|
|
433
|
+
{" "}
|
|
434
|
+
{plugin.name}
|
|
435
|
+
{needsWork ? " ⬆" : ""}{" "}
|
|
436
|
+
</strong>
|
|
437
|
+
</text>
|
|
438
|
+
</box>
|
|
439
|
+
|
|
440
|
+
{/* Status line */}
|
|
441
|
+
<box marginTop={1}>
|
|
442
|
+
<text
|
|
443
|
+
fg={
|
|
444
|
+
isInstalled
|
|
445
|
+
? theme.colors.success
|
|
446
|
+
: brokenInstall
|
|
447
|
+
? theme.colors.warning
|
|
448
|
+
: theme.colors.muted
|
|
449
|
+
}
|
|
450
|
+
>
|
|
451
|
+
{isInstalled
|
|
452
|
+
? "● Installed"
|
|
453
|
+
: brokenInstall
|
|
454
|
+
? "○ Not installed — enabled in settings, no files on disk"
|
|
455
|
+
: "○ Not installed"}
|
|
456
|
+
</text>
|
|
457
|
+
</box>
|
|
458
|
+
{brokenInstall ? (
|
|
459
|
+
<box>
|
|
460
|
+
<text fg={theme.colors.muted}>
|
|
461
|
+
Claude Code cannot load it in this state. Press the scope key to
|
|
462
|
+
repair.
|
|
463
|
+
</text>
|
|
464
|
+
</box>
|
|
465
|
+
) : null}
|
|
466
|
+
|
|
467
|
+
{/* Description */}
|
|
468
|
+
<box marginTop={1} marginBottom={1}>
|
|
469
|
+
<text fg={theme.colors.text}>{plugin.description}</text>
|
|
470
|
+
</box>
|
|
471
|
+
|
|
472
|
+
{/* Metadata */}
|
|
473
|
+
<StatusLine status={status} />
|
|
474
|
+
{showVersion ? (
|
|
475
|
+
<KeyValueLine
|
|
476
|
+
label="Version"
|
|
477
|
+
value={
|
|
478
|
+
<span>
|
|
479
|
+
<span fg={theme.colors.link}>v{plugin.version}</span>
|
|
480
|
+
{showInstalledVersion ? (
|
|
481
|
+
<span> (v{plugin.installedVersion} installed)</span>
|
|
482
|
+
) : null}
|
|
483
|
+
</span>
|
|
484
|
+
}
|
|
485
|
+
/>
|
|
486
|
+
) : null}
|
|
487
|
+
{/*
|
|
488
|
+
* Name the source of the version above whenever it is not authoritative.
|
|
489
|
+
* Without this the panel showed a version and an installed version that
|
|
490
|
+
* happened to match, and the user reasonably read that as "current" —
|
|
491
|
+
* when in fact GitHub was never reached and the number came off a clone
|
|
492
|
+
* that had not moved in days.
|
|
493
|
+
*/}
|
|
494
|
+
{status?.reason === "catalog-unverified" ? (
|
|
495
|
+
<>
|
|
496
|
+
<KeyValueLine
|
|
497
|
+
label="Latest"
|
|
498
|
+
value={
|
|
499
|
+
<span fg={theme.colors.warning}>unverified — not checked</span>
|
|
500
|
+
}
|
|
501
|
+
/>
|
|
502
|
+
<box>
|
|
503
|
+
<text fg={theme.colors.muted}>
|
|
504
|
+
{" "}
|
|
505
|
+
{plugin.catalogSource === "local-clone"
|
|
506
|
+
? "Version shown is from the pinned marketplace clone, so a newer release may exist. Press r to retry."
|
|
507
|
+
: "Press r to retry."}
|
|
508
|
+
</text>
|
|
509
|
+
</box>
|
|
510
|
+
</>
|
|
511
|
+
) : null}
|
|
512
|
+
{plugin.category ? (
|
|
513
|
+
<KeyValueLine
|
|
514
|
+
label="Category"
|
|
515
|
+
value={<span fg={theme.colors.accent}>{plugin.category}</span>}
|
|
516
|
+
/>
|
|
517
|
+
) : null}
|
|
518
|
+
{plugin.author ? (
|
|
519
|
+
<KeyValueLine
|
|
520
|
+
label="Author"
|
|
521
|
+
value={<span>{plugin.author.name}</span>}
|
|
522
|
+
/>
|
|
523
|
+
) : null}
|
|
524
|
+
{components.length > 0 ? (
|
|
525
|
+
<KeyValueLine
|
|
526
|
+
label="Contains"
|
|
527
|
+
value={
|
|
528
|
+
<span fg={theme.colors.warning}>{components.join(" · ")}</span>
|
|
529
|
+
}
|
|
530
|
+
/>
|
|
531
|
+
) : null}
|
|
532
|
+
{item.isExpandable ? (
|
|
533
|
+
<box>
|
|
534
|
+
<text fg={theme.colors.muted}>
|
|
535
|
+
← → to {item.isExpanded ? "hide" : "list"} what it contains
|
|
536
|
+
</text>
|
|
537
|
+
</box>
|
|
538
|
+
) : null}
|
|
539
|
+
|
|
540
|
+
{/* Scope Status */}
|
|
541
|
+
<DetailSection>
|
|
542
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
543
|
+
<text fg={theme.colors.text}>
|
|
544
|
+
<strong>Scopes:</strong>
|
|
545
|
+
</text>
|
|
546
|
+
<box marginTop={1} flexDirection="column">
|
|
547
|
+
<ScopeLine
|
|
548
|
+
keyHint="u"
|
|
549
|
+
name="User"
|
|
550
|
+
qualifier="global"
|
|
551
|
+
color={theme.scopes.user}
|
|
552
|
+
scope={plugin.userScope}
|
|
553
|
+
/>
|
|
554
|
+
<ScopeLine
|
|
555
|
+
keyHint="p"
|
|
556
|
+
name="Project"
|
|
557
|
+
qualifier="team"
|
|
558
|
+
color={theme.scopes.project}
|
|
559
|
+
scope={plugin.projectScope}
|
|
560
|
+
/>
|
|
561
|
+
<ScopeLine
|
|
562
|
+
keyHint="l"
|
|
563
|
+
name="Local"
|
|
564
|
+
qualifier="private"
|
|
565
|
+
color={theme.scopes.local}
|
|
566
|
+
scope={plugin.localScope}
|
|
567
|
+
/>
|
|
568
|
+
</box>
|
|
569
|
+
</DetailSection>
|
|
570
|
+
|
|
571
|
+
{/* Recent releases */}
|
|
572
|
+
<ReleaseHistory
|
|
573
|
+
releases={plugin.releases}
|
|
574
|
+
installedVersion={plugin.installedVersion}
|
|
575
|
+
/>
|
|
576
|
+
|
|
577
|
+
{/* Update action */}
|
|
578
|
+
{needsWork ? (
|
|
579
|
+
<ActionHints
|
|
580
|
+
hints={[
|
|
581
|
+
{
|
|
582
|
+
key: "U",
|
|
583
|
+
label:
|
|
584
|
+
status?.action === "repair"
|
|
585
|
+
? "Reinstall (files changed)"
|
|
586
|
+
: status?.target
|
|
587
|
+
? `Update to v${status.target}`
|
|
588
|
+
: "Update",
|
|
589
|
+
tone: "primary",
|
|
590
|
+
},
|
|
591
|
+
]}
|
|
592
|
+
/>
|
|
593
|
+
) : null}
|
|
594
|
+
</box>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* One line of the detail panel's scope breakdown.
|
|
600
|
+
*
|
|
601
|
+
* The filled dot means installed — registry-backed, same as everywhere else.
|
|
602
|
+
* A scope that is enabled with nothing installed is called out explicitly
|
|
603
|
+
* rather than shown as a plain empty dot, because those two states need
|
|
604
|
+
* different actions from the user and used to look identical.
|
|
605
|
+
*/
|
|
606
|
+
function ScopeLine({
|
|
607
|
+
keyHint,
|
|
608
|
+
name,
|
|
609
|
+
qualifier,
|
|
610
|
+
color,
|
|
611
|
+
scope,
|
|
612
|
+
}: {
|
|
613
|
+
keyHint: string;
|
|
614
|
+
name: string;
|
|
615
|
+
qualifier: string;
|
|
616
|
+
color: UiColor;
|
|
617
|
+
scope?: ScopeStatus;
|
|
618
|
+
}): React.ReactNode {
|
|
619
|
+
const installed = isInstalledInScope(scope);
|
|
620
|
+
return (
|
|
621
|
+
<text fg={theme.colors.text}>
|
|
622
|
+
<span bg={color} fg={theme.hints.fg}>
|
|
623
|
+
{" "}
|
|
624
|
+
{keyHint}{" "}
|
|
625
|
+
</span>
|
|
626
|
+
<span fg={installed ? color : theme.colors.muted}>
|
|
627
|
+
{installed ? " ● " : " ○ "}
|
|
628
|
+
</span>
|
|
629
|
+
<span fg={color}>{name}</span>
|
|
630
|
+
<span> {qualifier}</span>
|
|
631
|
+
{scope?.version ? <span fg={color}> v{scope.version}</span> : null}
|
|
632
|
+
{!installed && scope?.enabled ? (
|
|
633
|
+
<span fg={theme.colors.warning}> enabled, not installed</span>
|
|
634
|
+
) : null}
|
|
635
|
+
</text>
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Recent release history, newest first.
|
|
641
|
+
*
|
|
642
|
+
* Releases the user does not have yet are marked and coloured; the installed one is
|
|
643
|
+
* flagged; older ones are dimmed. The list is already ordered newest-first, so the
|
|
644
|
+
* installed entry's index is all that's needed to tell those three groups apart —
|
|
645
|
+
* no version comparison, and no way for it to disagree with the published order.
|
|
646
|
+
* When the installed version isn't in the window (unknown "0.0.0", or older than the
|
|
647
|
+
* releases published), everything renders neutrally rather than guessing.
|
|
648
|
+
*/
|
|
649
|
+
function ReleaseHistory({
|
|
650
|
+
releases,
|
|
651
|
+
installedVersion,
|
|
652
|
+
}: {
|
|
653
|
+
releases?: PluginRelease[];
|
|
654
|
+
installedVersion?: string;
|
|
655
|
+
}): React.ReactNode {
|
|
656
|
+
if (!releases?.length) return null;
|
|
657
|
+
|
|
658
|
+
const installedAt = installedVersion
|
|
659
|
+
? releases.findIndex((r) => r.version === installedVersion)
|
|
660
|
+
: -1;
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
<DetailSection>
|
|
664
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
665
|
+
<text fg={theme.colors.text}>
|
|
666
|
+
<strong>Recent releases:</strong>
|
|
667
|
+
</text>
|
|
668
|
+
<box marginTop={1} flexDirection="column">
|
|
669
|
+
{releases.map((release, i) => {
|
|
670
|
+
const isNewer = installedAt > 0 && i < installedAt;
|
|
671
|
+
const isInstalled = installedAt >= 0 && i === installedAt;
|
|
672
|
+
const versionColor = isNewer
|
|
673
|
+
? theme.colors.warning
|
|
674
|
+
: isInstalled
|
|
675
|
+
? theme.colors.success
|
|
676
|
+
: theme.colors.link;
|
|
677
|
+
|
|
678
|
+
return (
|
|
679
|
+
<box key={release.version} flexDirection="column" marginBottom={1}>
|
|
680
|
+
<text fg={theme.colors.text}>
|
|
681
|
+
<span fg={versionColor}>
|
|
682
|
+
{isNewer ? "▲ " : isInstalled ? "● " : " "}v{release.version}
|
|
683
|
+
</span>
|
|
684
|
+
{release.date ? (
|
|
685
|
+
<span fg={theme.colors.muted}> {release.date}</span>
|
|
686
|
+
) : null}
|
|
687
|
+
{release.kinds?.length ? (
|
|
688
|
+
<span fg={theme.colors.dim}>
|
|
689
|
+
{" "}
|
|
690
|
+
{release.kinds.join(" · ")}
|
|
691
|
+
</span>
|
|
692
|
+
) : null}
|
|
693
|
+
{isInstalled ? (
|
|
694
|
+
<span fg={theme.colors.success}> installed</span>
|
|
695
|
+
) : null}
|
|
696
|
+
</text>
|
|
697
|
+
<text fg={isNewer ? theme.colors.text : theme.colors.muted}>
|
|
698
|
+
{" "}
|
|
699
|
+
{release.summary}
|
|
700
|
+
</text>
|
|
701
|
+
</box>
|
|
702
|
+
);
|
|
703
|
+
})}
|
|
704
|
+
</box>
|
|
705
|
+
</DetailSection>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ─── Skill renderers ─────────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Left padding for a component row.
|
|
713
|
+
*
|
|
714
|
+
* A plugin name starts at column 7: one leading space, three scope segments,
|
|
715
|
+
* one space, then the two-column arrow cell. A component's badge occupies
|
|
716
|
+
* columns 5-7 and its name starts at 9.
|
|
717
|
+
*
|
|
718
|
+
* Deliberately no tree glyph. An earlier version drew a `└` on every row, which
|
|
719
|
+
* introduced a second visual grammar halfway down the list — scope segments
|
|
720
|
+
* above, box-drawing below — and read as a broken tree, since a real one would
|
|
721
|
+
* branch with `├` on every row but the last. The badge anchors the row instead,
|
|
722
|
+
* and says what kind of thing it is at the same time.
|
|
723
|
+
*/
|
|
724
|
+
const COMPONENT_INDENT = " ".repeat(5);
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Width of the badge cell plus the gap after it, so status rows line their text
|
|
728
|
+
* up with the component names.
|
|
729
|
+
*
|
|
730
|
+
* The gap is outside the chip, not inside it. Widening the chip to four cells
|
|
731
|
+
* would have put the extra space on its coloured fill, which makes the chip
|
|
732
|
+
* bigger rather than separating it from the name.
|
|
733
|
+
*/
|
|
734
|
+
const BADGE_WIDTH = 3;
|
|
735
|
+
const BADGE_GAP = 1;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Type icons.
|
|
739
|
+
*
|
|
740
|
+
* Commands and agents take the glyph you address them with — `/name`, `@name` —
|
|
741
|
+
* so the badge and the thing you type match.
|
|
742
|
+
*
|
|
743
|
+
* The skill glyph follows the only convention the ecosystem has. There is no
|
|
744
|
+
* agreed terminal symbol for a skill: Anthropic ships none (its Skills write-up
|
|
745
|
+
* uses a folder metaphor in prose only), and the large community directories
|
|
746
|
+
* distinguish skills from plugins by section heading rather than by symbol. The
|
|
747
|
+
* two products that do mark them converged on the same idea — Cursor's docs say
|
|
748
|
+
* a skill badge "defaults to a lightning icon", and the community Skills
|
|
749
|
+
* sidebar for VS Code uses a sparkle.
|
|
750
|
+
*
|
|
751
|
+
* `✱` is the sparkle idea at single width. The canonical glyphs cannot be used:
|
|
752
|
+
* `⚡` (U+26A1) and `✨` (U+2728) are both East-Asian-Width Wide, i.e. two cells,
|
|
753
|
+
* which would shift every name on the row. An *asterisk* rather than a
|
|
754
|
+
* four-pointed star also keeps it clear of `◆` and `✦`, which read as a generic
|
|
755
|
+
* ornament next to `/` and `@`.
|
|
756
|
+
*
|
|
757
|
+
* `⬢` for an MCP server is ours; nobody has a convention there either.
|
|
758
|
+
*
|
|
759
|
+
* Every glyph here is single-width — check with `unicodedata.east_asian_width`
|
|
760
|
+
* before swapping one, and reject anything reporting W or F.
|
|
761
|
+
*/
|
|
762
|
+
export const COMPONENT_ICON: Record<PluginComponentKind, string> = {
|
|
763
|
+
skill: "✱",
|
|
764
|
+
command: "/",
|
|
765
|
+
agent: "@",
|
|
766
|
+
mcp: "⬢",
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const COMPONENT_COLOR: Record<PluginComponentKind, UiColor> = {
|
|
770
|
+
skill: theme.colors.accent,
|
|
771
|
+
command: theme.colors.info,
|
|
772
|
+
agent: theme.colors.link,
|
|
773
|
+
mcp: theme.colors.warning,
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const COMPONENT_NOUN: Record<PluginComponentKind, string> = {
|
|
777
|
+
skill: "Skill",
|
|
778
|
+
command: "Command",
|
|
779
|
+
agent: "Subagent",
|
|
780
|
+
mcp: "MCP server",
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* The type badge: one glyph in a padded, washed cell.
|
|
785
|
+
*
|
|
786
|
+
* Two states, and each exists for a reason.
|
|
787
|
+
*
|
|
788
|
+
* On a *selected* row the whole line is painted in the selection colour, so the
|
|
789
|
+
* badge drops its own fill — a chip of a third colour inside an inverted row is
|
|
790
|
+
* the noisiest thing on the screen, and the selection already says where you
|
|
791
|
+
* are.
|
|
792
|
+
*
|
|
793
|
+
* Otherwise: a wash of the kind's hue for the resolved mode, quiet enough to
|
|
794
|
+
* sit on hundreds of rows.
|
|
795
|
+
*/
|
|
796
|
+
function componentBadge(
|
|
797
|
+
kind: PluginComponentKind,
|
|
798
|
+
isSelected: boolean,
|
|
799
|
+
): React.ReactNode {
|
|
800
|
+
const icon = COMPONENT_ICON[kind];
|
|
801
|
+
if (isSelected) return ` ${icon} `;
|
|
802
|
+
|
|
803
|
+
const chip = badgeColors(kind);
|
|
804
|
+
return <span bg={chip.bg} fg={chip.fg}>{` ${icon} `}</span>;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function componentRow(
|
|
808
|
+
item: PluginComponentItem,
|
|
809
|
+
isSelected: boolean,
|
|
810
|
+
): React.ReactNode {
|
|
811
|
+
const { component } = item;
|
|
812
|
+
const label = component.group
|
|
813
|
+
? `${component.group}/${component.name}`
|
|
814
|
+
: component.name;
|
|
815
|
+
|
|
816
|
+
if (isSelected) {
|
|
817
|
+
return (
|
|
818
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
819
|
+
{COMPONENT_INDENT}
|
|
820
|
+
{componentBadge(component.kind, true)}
|
|
821
|
+
{`${" ".repeat(BADGE_GAP)}${label} `}
|
|
822
|
+
</text>
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
return (
|
|
826
|
+
<text fg={theme.colors.muted}>
|
|
827
|
+
{COMPONENT_INDENT}
|
|
828
|
+
{componentBadge(component.kind, false)}
|
|
829
|
+
{`${" ".repeat(BADGE_GAP)}${label}`}
|
|
830
|
+
</text>
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function contentsStatusRow(
|
|
835
|
+
item: PluginContentsStatusItem,
|
|
836
|
+
isSelected: boolean,
|
|
837
|
+
): React.ReactNode {
|
|
838
|
+
const pad = COMPONENT_INDENT + " ".repeat(BADGE_WIDTH + BADGE_GAP);
|
|
839
|
+
if (isSelected) {
|
|
840
|
+
return (
|
|
841
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
842
|
+
{`${pad}${item.label} `}
|
|
843
|
+
</text>
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return (
|
|
847
|
+
<text
|
|
848
|
+
fg={item.status === "error" ? theme.colors.warning : theme.colors.dim}
|
|
849
|
+
>
|
|
850
|
+
{`${pad}${item.label}`}
|
|
851
|
+
</text>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* How much of a component's file the panel prints.
|
|
857
|
+
*
|
|
858
|
+
* The detail pane is a scroll box (PgUp/PgDn, Ctrl+U/Ctrl+D), so this is a
|
|
859
|
+
* render-cost guard, not a reading limit — high enough that essentially every
|
|
860
|
+
* real file prints whole. When it does bite, the panel says how many lines it
|
|
861
|
+
* withheld: a body that simply stops reads as a truncated *file*, which is a
|
|
862
|
+
* different and more alarming claim than a truncated view.
|
|
863
|
+
*/
|
|
864
|
+
const BODY_LINES = 400;
|
|
865
|
+
|
|
866
|
+
/** Frontmatter keys already shown in the header, so the table skips them. */
|
|
867
|
+
const HEADER_KEYS = new Set(["name", "description"]);
|
|
868
|
+
|
|
869
|
+
function componentDetail(
|
|
870
|
+
item: PluginComponentItem,
|
|
871
|
+
entry?: PluginSkillDetailEntry,
|
|
872
|
+
width = 80,
|
|
873
|
+
): React.ReactNode {
|
|
874
|
+
const { component, plugin } = item;
|
|
875
|
+
const detail = entry?.status === "success" ? entry.detail : undefined;
|
|
876
|
+
const isMcp = component.kind === "mcp";
|
|
877
|
+
|
|
878
|
+
const described = detail?.frontmatter.find((f) => f.key === "description");
|
|
879
|
+
const titled = detail?.frontmatter.find((f) => f.key === "name");
|
|
880
|
+
const rest = (detail?.frontmatter ?? []).filter(
|
|
881
|
+
(f) => !HEADER_KEYS.has(f.key) && f.value,
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
const rendered = detail
|
|
885
|
+
? renderMarkdown(detail.body, { width, maxLines: BODY_LINES })
|
|
886
|
+
: null;
|
|
887
|
+
|
|
888
|
+
return (
|
|
889
|
+
<box flexDirection="column">
|
|
890
|
+
<box justifyContent="center">
|
|
891
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
892
|
+
<strong>
|
|
893
|
+
{" "}
|
|
894
|
+
{COMPONENT_ICON[component.kind]} {titled?.value ||
|
|
895
|
+
component.name}{" "}
|
|
896
|
+
</strong>
|
|
897
|
+
</text>
|
|
898
|
+
</box>
|
|
899
|
+
|
|
900
|
+
{described ? (
|
|
901
|
+
<box marginTop={1}>
|
|
902
|
+
<text fg={theme.colors.text}>{described.value}</text>
|
|
903
|
+
</box>
|
|
904
|
+
) : null}
|
|
905
|
+
|
|
906
|
+
<box marginTop={1} flexDirection="column">
|
|
907
|
+
<KeyValueLine
|
|
908
|
+
label="Kind"
|
|
909
|
+
value={
|
|
910
|
+
<span>
|
|
911
|
+
{componentBadge(component.kind, false)}
|
|
912
|
+
<span fg={COMPONENT_COLOR[component.kind]}>
|
|
913
|
+
{` ${COMPONENT_NOUN[component.kind]}`}
|
|
914
|
+
</span>
|
|
915
|
+
</span>
|
|
916
|
+
}
|
|
917
|
+
/>
|
|
918
|
+
<KeyValueLine
|
|
919
|
+
label="Plugin"
|
|
920
|
+
value={<span fg={theme.colors.accent}>{plugin.name}</span>}
|
|
921
|
+
/>
|
|
922
|
+
{component.group ? (
|
|
923
|
+
<KeyValueLine
|
|
924
|
+
label="Group"
|
|
925
|
+
value={<span fg={theme.colors.accent}>{component.group}</span>}
|
|
926
|
+
/>
|
|
927
|
+
) : null}
|
|
928
|
+
{component.mcpCommand ? (
|
|
929
|
+
<KeyValueLine
|
|
930
|
+
label="Runs"
|
|
931
|
+
value={<span fg={theme.colors.link}>{component.mcpCommand}</span>}
|
|
932
|
+
/>
|
|
933
|
+
) : null}
|
|
934
|
+
<KeyValueLine
|
|
935
|
+
label="Path"
|
|
936
|
+
value={<span fg={theme.colors.link}>{component.repoPath}</span>}
|
|
937
|
+
/>
|
|
938
|
+
{rest.map((field) => (
|
|
939
|
+
<KeyValueLine
|
|
940
|
+
key={field.key}
|
|
941
|
+
label={field.key}
|
|
942
|
+
value={<span fg={theme.colors.warning}>{field.value}</span>}
|
|
943
|
+
/>
|
|
944
|
+
))}
|
|
945
|
+
</box>
|
|
946
|
+
|
|
947
|
+
{/* An MCP server is declared in JSON, not a markdown document, so there
|
|
948
|
+
is no body to fetch — the Runs line above is the whole of it. */}
|
|
949
|
+
{!isMcp && (entry?.status === "loading" || !entry) ? (
|
|
950
|
+
<box marginTop={1}>
|
|
951
|
+
<text fg={theme.colors.muted}>Reading {component.repoPath}…</text>
|
|
952
|
+
</box>
|
|
953
|
+
) : null}
|
|
954
|
+
|
|
955
|
+
{entry?.status === "error" ? (
|
|
956
|
+
<box marginTop={1} flexDirection="column">
|
|
957
|
+
<text fg={theme.colors.warning}>
|
|
958
|
+
Could not read it: {entry.error}
|
|
959
|
+
</text>
|
|
960
|
+
<text fg={theme.colors.muted}>
|
|
961
|
+
Set GITHUB_TOKEN to raise the 60-per-hour unauthenticated limit.
|
|
962
|
+
</text>
|
|
963
|
+
</box>
|
|
964
|
+
) : null}
|
|
965
|
+
|
|
966
|
+
{rendered && rendered.nodes.length > 0 ? (
|
|
967
|
+
<DetailSection>
|
|
968
|
+
<text fg={theme.colors.text}>{"─".repeat(24)}</text>
|
|
969
|
+
<box marginTop={1} flexDirection="column">
|
|
970
|
+
{rendered.nodes}
|
|
971
|
+
</box>
|
|
972
|
+
{rendered.withheld > 0 ? (
|
|
973
|
+
<box marginTop={1}>
|
|
974
|
+
<text fg={theme.colors.dim}>
|
|
975
|
+
… {rendered.withheld} more lines in the file
|
|
976
|
+
</text>
|
|
977
|
+
</box>
|
|
978
|
+
) : null}
|
|
979
|
+
</DetailSection>
|
|
980
|
+
) : null}
|
|
981
|
+
|
|
982
|
+
{detail && detail.body.length === 0 ? (
|
|
983
|
+
<box marginTop={1}>
|
|
984
|
+
<text fg={theme.colors.muted}>Frontmatter only — no body.</text>
|
|
985
|
+
</box>
|
|
986
|
+
) : null}
|
|
987
|
+
|
|
988
|
+
<box marginTop={1}>
|
|
989
|
+
<text fg={theme.colors.dim}>
|
|
990
|
+
Installing {plugin.name} installs this. Plugin components are not
|
|
991
|
+
installed one at a time.
|
|
992
|
+
</text>
|
|
993
|
+
</box>
|
|
994
|
+
</box>
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function contentsStatusDetail(item: PluginContentsStatusItem): React.ReactNode {
|
|
999
|
+
if (item.status === "error") {
|
|
1000
|
+
return (
|
|
1001
|
+
<box flexDirection="column">
|
|
1002
|
+
<text fg={theme.colors.warning}>
|
|
1003
|
+
<strong>Could not read {item.plugin.name}</strong>
|
|
1004
|
+
</text>
|
|
1005
|
+
<box marginTop={1}>
|
|
1006
|
+
<text fg={theme.colors.muted}>{item.error || "Unknown error."}</text>
|
|
1007
|
+
</box>
|
|
1008
|
+
<box marginTop={1}>
|
|
1009
|
+
<text fg={theme.colors.muted}>
|
|
1010
|
+
A marketplace you have added is read from its local clone and needs
|
|
1011
|
+
no network. This one is not added, so its contents come from the
|
|
1012
|
+
GitHub API — where an unauthenticated client gets 60 requests an
|
|
1013
|
+
hour across every repo on screen combined.
|
|
1014
|
+
</text>
|
|
1015
|
+
</box>
|
|
1016
|
+
<box marginTop={1}>
|
|
1017
|
+
<text fg={theme.colors.muted}>
|
|
1018
|
+
Add the marketplace to read it from disk instead, or set
|
|
1019
|
+
GITHUB_TOKEN to raise the limit. Collapse and expand to retry — the
|
|
1020
|
+
failure is not cached.
|
|
1021
|
+
</text>
|
|
1022
|
+
</box>
|
|
1023
|
+
</box>
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
if (item.status === "empty") {
|
|
1027
|
+
return (
|
|
1028
|
+
<box flexDirection="column">
|
|
1029
|
+
<text fg={theme.colors.text}>
|
|
1030
|
+
<strong>{item.plugin.name} ships no components</strong>
|
|
1031
|
+
</text>
|
|
1032
|
+
<box marginTop={1}>
|
|
1033
|
+
<text fg={theme.colors.muted}>
|
|
1034
|
+
No skills, commands, subagents or MCP servers were found in its
|
|
1035
|
+
directory. It may ship hooks, an output style, or nothing but
|
|
1036
|
+
documentation.
|
|
1037
|
+
</text>
|
|
1038
|
+
</box>
|
|
1039
|
+
</box>
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
return <text fg={theme.colors.muted}>Reading the repo tree…</text>;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ─── Public dispatch functions ────────────────────────────────────────────────
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Render a plugin browser list row by item kind.
|
|
1049
|
+
*/
|
|
1050
|
+
export function renderPluginRow(
|
|
1051
|
+
item: PluginBrowserItem,
|
|
1052
|
+
_index: number,
|
|
1053
|
+
isSelected: boolean,
|
|
1054
|
+
): React.ReactNode {
|
|
1055
|
+
if (item.kind === "category") {
|
|
1056
|
+
return categoryRow(item, isSelected);
|
|
1057
|
+
}
|
|
1058
|
+
if (item.kind === "component") {
|
|
1059
|
+
return componentRow(item, isSelected);
|
|
1060
|
+
}
|
|
1061
|
+
if (item.kind === "contents-status") {
|
|
1062
|
+
return contentsStatusRow(item, isSelected);
|
|
1063
|
+
}
|
|
1064
|
+
return pluginRow(item, isSelected);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Render the detail panel for a plugin browser item.
|
|
1069
|
+
*/
|
|
1070
|
+
export function renderPluginDetail(
|
|
1071
|
+
item: PluginBrowserItem | undefined,
|
|
1072
|
+
collapsedMarketplaces: Set<string>,
|
|
1073
|
+
pluginContents?: Map<string, PluginContentsEntry>,
|
|
1074
|
+
skillDetails?: Map<string, PluginSkillDetailEntry>,
|
|
1075
|
+
/** Columns the detail pane has, so a markdown table can size its columns. */
|
|
1076
|
+
width?: number,
|
|
1077
|
+
): React.ReactNode {
|
|
1078
|
+
if (!item) return <text fg={theme.colors.muted}>Select an item</text>;
|
|
1079
|
+
if (item.kind === "category") {
|
|
1080
|
+
return categoryDetail(item, collapsedMarketplaces);
|
|
1081
|
+
}
|
|
1082
|
+
if (item.kind === "component") {
|
|
1083
|
+
return componentDetail(item, skillDetails?.get(item.detailKey), width);
|
|
1084
|
+
}
|
|
1085
|
+
if (item.kind === "contents-status") {
|
|
1086
|
+
return contentsStatusDetail(item);
|
|
1087
|
+
}
|
|
1088
|
+
return pluginDetail(item, pluginContents?.get(item.plugin.id));
|
|
1089
|
+
}
|