magus-cli 0.0.0-placeholder → 7.1.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 +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -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 +524 -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 +248 -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 +340 -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__/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 +345 -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 +310 -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 +1396 -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 +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -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 +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -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 +1564 -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 +657 -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 +245 -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 +847 -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 +377 -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 +820 -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 +535 -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 +1423 -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 +440 -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 +149 -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,229 @@
|
|
|
1
|
+
import type { SkillInfo, SkillSetInfo } from "../../types/index.js";
|
|
2
|
+
|
|
3
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SkillCategoryItem {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: "category";
|
|
8
|
+
label: string;
|
|
9
|
+
title: string;
|
|
10
|
+
categoryKey: string;
|
|
11
|
+
count?: number;
|
|
12
|
+
tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
|
|
13
|
+
badge?: string;
|
|
14
|
+
star?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillSkillItem {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: "skill";
|
|
20
|
+
label: string;
|
|
21
|
+
skill: SkillInfo;
|
|
22
|
+
/** Extra indent level for child skills inside an expanded skill set */
|
|
23
|
+
indent?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SkillSetItem {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: "skillset";
|
|
29
|
+
label: string;
|
|
30
|
+
skillSet: SkillSetInfo;
|
|
31
|
+
expanded: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SkillBrowserItem =
|
|
35
|
+
| SkillCategoryItem
|
|
36
|
+
| SkillSkillItem
|
|
37
|
+
| SkillSetItem;
|
|
38
|
+
|
|
39
|
+
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export interface BuildSkillBrowserItemsArgs {
|
|
42
|
+
recommended: SkillInfo[];
|
|
43
|
+
popular: SkillInfo[];
|
|
44
|
+
installed: SkillInfo[];
|
|
45
|
+
searchResults: SkillInfo[];
|
|
46
|
+
query: string;
|
|
47
|
+
isSearchLoading: boolean;
|
|
48
|
+
skillSets?: SkillSetInfo[];
|
|
49
|
+
expandedSets?: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds the flat list of items for the SkillsScreen list panel.
|
|
54
|
+
* Extracted from SkillsScreen so it can be tested independently.
|
|
55
|
+
*/
|
|
56
|
+
export function buildSkillBrowserItems({
|
|
57
|
+
recommended,
|
|
58
|
+
popular,
|
|
59
|
+
installed,
|
|
60
|
+
searchResults,
|
|
61
|
+
query,
|
|
62
|
+
isSearchLoading,
|
|
63
|
+
skillSets = [],
|
|
64
|
+
expandedSets = new Set(),
|
|
65
|
+
}: BuildSkillBrowserItemsArgs): SkillBrowserItem[] {
|
|
66
|
+
const lowerQuery = query.toLowerCase();
|
|
67
|
+
const items: SkillBrowserItem[] = [];
|
|
68
|
+
|
|
69
|
+
// ── INSTALLED: always shown at top (if any) ──
|
|
70
|
+
const installedFiltered = lowerQuery
|
|
71
|
+
? installed.filter((s) => s.name.toLowerCase().includes(lowerQuery))
|
|
72
|
+
: installed;
|
|
73
|
+
|
|
74
|
+
if (installedFiltered.length > 0) {
|
|
75
|
+
items.push({
|
|
76
|
+
id: "cat:installed",
|
|
77
|
+
kind: "category",
|
|
78
|
+
label: `Installed (${installedFiltered.length})`,
|
|
79
|
+
title: "Installed",
|
|
80
|
+
categoryKey: "installed",
|
|
81
|
+
count: installedFiltered.length,
|
|
82
|
+
tone: "purple",
|
|
83
|
+
star: "\u25CF ",
|
|
84
|
+
});
|
|
85
|
+
for (const skill of installedFiltered) {
|
|
86
|
+
items.push({
|
|
87
|
+
id: `skill:${skill.id}`,
|
|
88
|
+
kind: "skill",
|
|
89
|
+
label: skill.name,
|
|
90
|
+
skill,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── RECOMMENDED: skill sets + individual skills, all as first-class items ──
|
|
96
|
+
const filteredSets = lowerQuery
|
|
97
|
+
? skillSets.filter((s) => {
|
|
98
|
+
if (s.name.toLowerCase().includes(lowerQuery)) return true;
|
|
99
|
+
if (s.description.toLowerCase().includes(lowerQuery)) return true;
|
|
100
|
+
if (
|
|
101
|
+
s.loaded &&
|
|
102
|
+
s.skills.some((sk) => sk.name.toLowerCase().includes(lowerQuery))
|
|
103
|
+
)
|
|
104
|
+
return true;
|
|
105
|
+
return false;
|
|
106
|
+
})
|
|
107
|
+
: skillSets;
|
|
108
|
+
|
|
109
|
+
const filteredRec = lowerQuery
|
|
110
|
+
? recommended.filter(
|
|
111
|
+
(s) =>
|
|
112
|
+
s.name.toLowerCase().includes(lowerQuery) ||
|
|
113
|
+
(s.description || "").toLowerCase().includes(lowerQuery),
|
|
114
|
+
)
|
|
115
|
+
: recommended;
|
|
116
|
+
|
|
117
|
+
const recommendedCount = filteredSets.length + filteredRec.length;
|
|
118
|
+
|
|
119
|
+
items.push({
|
|
120
|
+
id: "cat:recommended",
|
|
121
|
+
kind: "category",
|
|
122
|
+
label: "Recommended",
|
|
123
|
+
title: "Recommended",
|
|
124
|
+
categoryKey: "recommended",
|
|
125
|
+
count: recommendedCount,
|
|
126
|
+
tone: "green",
|
|
127
|
+
star: "\u2605 ",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Skill sets first within recommended
|
|
131
|
+
for (const set of filteredSets) {
|
|
132
|
+
const isExpanded = expandedSets.has(set.id);
|
|
133
|
+
items.push({
|
|
134
|
+
id: `skillset:${set.id}`,
|
|
135
|
+
kind: "skillset",
|
|
136
|
+
label: set.name,
|
|
137
|
+
skillSet: set,
|
|
138
|
+
expanded: isExpanded,
|
|
139
|
+
});
|
|
140
|
+
// When expanded, show child skills as indented skill items
|
|
141
|
+
if (isExpanded && set.loaded) {
|
|
142
|
+
const childSkills = lowerQuery
|
|
143
|
+
? set.skills.filter((sk) => sk.name.toLowerCase().includes(lowerQuery))
|
|
144
|
+
: set.skills;
|
|
145
|
+
for (const skill of childSkills) {
|
|
146
|
+
items.push({
|
|
147
|
+
id: `skill:${skill.id}`,
|
|
148
|
+
kind: "skill",
|
|
149
|
+
label: skill.name,
|
|
150
|
+
skill,
|
|
151
|
+
indent: 2,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Then individual recommended skills
|
|
158
|
+
for (const skill of filteredRec) {
|
|
159
|
+
items.push({
|
|
160
|
+
id: `skill:${skill.id}`,
|
|
161
|
+
kind: "skill",
|
|
162
|
+
label: skill.name,
|
|
163
|
+
skill,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── SEARCH MODE ──
|
|
168
|
+
if (query.length >= 2) {
|
|
169
|
+
if (!isSearchLoading && searchResults.length > 0) {
|
|
170
|
+
const recNames = new Set(recommended.map((s) => s.name));
|
|
171
|
+
const deduped = searchResults
|
|
172
|
+
.filter((s) => !recNames.has(s.name))
|
|
173
|
+
.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
|
|
174
|
+
|
|
175
|
+
if (deduped.length > 0) {
|
|
176
|
+
items.push({
|
|
177
|
+
id: "cat:search",
|
|
178
|
+
kind: "category",
|
|
179
|
+
label: `Search (${deduped.length})`,
|
|
180
|
+
title: "Search",
|
|
181
|
+
categoryKey: "popular",
|
|
182
|
+
count: deduped.length,
|
|
183
|
+
tone: "teal",
|
|
184
|
+
});
|
|
185
|
+
for (const skill of deduped) {
|
|
186
|
+
items.push({
|
|
187
|
+
id: `skill:${skill.id}`,
|
|
188
|
+
kind: "skill",
|
|
189
|
+
label: skill.name,
|
|
190
|
+
skill,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return items;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
199
|
+
// Dedup by name — API can return same skill name from different repos
|
|
200
|
+
const seenPopular = new Set<string>();
|
|
201
|
+
const popularDeduped = popular
|
|
202
|
+
.filter((s) => (s.stars ?? 0) >= 5)
|
|
203
|
+
.filter((s) => {
|
|
204
|
+
if (seenPopular.has(s.name)) return false;
|
|
205
|
+
seenPopular.add(s.name);
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
if (popularDeduped.length > 0) {
|
|
209
|
+
items.push({
|
|
210
|
+
id: "cat:popular",
|
|
211
|
+
kind: "category",
|
|
212
|
+
label: "Popular",
|
|
213
|
+
title: "Popular",
|
|
214
|
+
categoryKey: "popular",
|
|
215
|
+
count: popularDeduped.length,
|
|
216
|
+
tone: "teal",
|
|
217
|
+
});
|
|
218
|
+
for (const skill of popularDeduped) {
|
|
219
|
+
items.push({
|
|
220
|
+
id: `skill:${skill.id}`,
|
|
221
|
+
kind: "skill",
|
|
222
|
+
label: skill.name,
|
|
223
|
+
skill,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return items;
|
|
229
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMMUNITY_STYLES,
|
|
3
|
+
type CommunityStyle,
|
|
4
|
+
type CommunityStyleSource,
|
|
5
|
+
findCommunitySource,
|
|
6
|
+
} from "../../data/community-styles.js";
|
|
7
|
+
import type { CommunityUpstreamStatus } from "../../services/community-styles.js";
|
|
8
|
+
import type {
|
|
9
|
+
ImportedStyle,
|
|
10
|
+
StylePreset,
|
|
11
|
+
StyleSource,
|
|
12
|
+
} from "../../services/styles-manager.js";
|
|
13
|
+
|
|
14
|
+
// ─── Item types ───────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List sections, in display order.
|
|
18
|
+
*
|
|
19
|
+
* Anthropic's official styles lead because they are the familiar starting
|
|
20
|
+
* point — someone who already knows Explanatory or Learning should find it
|
|
21
|
+
* first, then discover the presets that refine it. Team comes after the
|
|
22
|
+
* presets: it is the section a project fills in for itself.
|
|
23
|
+
*
|
|
24
|
+
* Community sorts LAST, and deliberately not second where "styles someone else
|
|
25
|
+
* wrote" would group it with Anthropic's. Two reasons decide it. It is the only
|
|
26
|
+
* section whose contents can change from the network, so keeping it at the
|
|
27
|
+
* bottom means the offline-authored sections never shift position underneath a
|
|
28
|
+
* user. And empty sections are omitted, so inserting it at position two would
|
|
29
|
+
* push Anthropic's familiar row down the screen for every existing user to buy
|
|
30
|
+
* discovery for a section most of them will not use.
|
|
31
|
+
*/
|
|
32
|
+
export type StyleCategoryKey =
|
|
33
|
+
| "anthropic"
|
|
34
|
+
| "verbosity"
|
|
35
|
+
| "modifier"
|
|
36
|
+
| "team"
|
|
37
|
+
| "personal"
|
|
38
|
+
| "community";
|
|
39
|
+
|
|
40
|
+
export interface StyleCategoryItem {
|
|
41
|
+
id: string;
|
|
42
|
+
kind: "category";
|
|
43
|
+
label: string;
|
|
44
|
+
title: string;
|
|
45
|
+
categoryKey: StyleCategoryKey;
|
|
46
|
+
count: number;
|
|
47
|
+
tone: "purple" | "green" | "teal" | "yellow" | "gray" | "red";
|
|
48
|
+
badge?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StyleEntryItem {
|
|
52
|
+
id: string;
|
|
53
|
+
kind: "style";
|
|
54
|
+
label: string;
|
|
55
|
+
source: StyleSource;
|
|
56
|
+
/** In the pending selection (what apply would write). */
|
|
57
|
+
checked: boolean;
|
|
58
|
+
/** In the selection the generated style file records (what is live). */
|
|
59
|
+
applied: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Name of an already-selected style this one cancels out with, or null.
|
|
62
|
+
* Verbosity presets never report one — they behave as a radio group, so
|
|
63
|
+
* selecting a second replaces the first rather than colliding with it.
|
|
64
|
+
*/
|
|
65
|
+
conflictsWith: string | null;
|
|
66
|
+
/** Template presets ship an empty table and cannot be applied directly. */
|
|
67
|
+
disabled: boolean;
|
|
68
|
+
/** For a fetched community style: what the last upstream check found. */
|
|
69
|
+
upstream?: CommunityUpstreamStatus;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A registry entry whose text is NOT on this machine yet.
|
|
74
|
+
*
|
|
75
|
+
* Not an `ImportedStyle`, and that is the point. `ImportedStyle` means "text
|
|
76
|
+
* that exists here"; a synthetic one carrying an empty body would be tickable,
|
|
77
|
+
* composable, and would hash to a lie. Keeping the type honest costs one extra
|
|
78
|
+
* item kind and buys a list where every tickable row has real words behind it.
|
|
79
|
+
*/
|
|
80
|
+
export interface StyleOfferItem {
|
|
81
|
+
id: string;
|
|
82
|
+
kind: "offer";
|
|
83
|
+
label: string;
|
|
84
|
+
entry: CommunityStyle;
|
|
85
|
+
source: CommunityStyleSource;
|
|
86
|
+
/** True when the project's committed declaration asks for this one. */
|
|
87
|
+
declared: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type StyleBrowserItem =
|
|
91
|
+
| StyleCategoryItem
|
|
92
|
+
| StyleEntryItem
|
|
93
|
+
| StyleOfferItem;
|
|
94
|
+
|
|
95
|
+
// ─── Adapter ──────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface BuildStyleBrowserItemsArgs {
|
|
98
|
+
presets: StylePreset[];
|
|
99
|
+
imports: ImportedStyle[];
|
|
100
|
+
/** Pending selection, by `source.id`. */
|
|
101
|
+
selected: Set<string>;
|
|
102
|
+
/** Selection recorded in the generated style file (null if never applied). */
|
|
103
|
+
applied: { presets: string[]; imports: string[] } | null;
|
|
104
|
+
query: string;
|
|
105
|
+
/**
|
|
106
|
+
* Registry entries to offer. Defaults to the shipped registry, which is a
|
|
107
|
+
* compile-time constant, so building the list still costs no I/O whatsoever.
|
|
108
|
+
*/
|
|
109
|
+
registry?: CommunityStyle[];
|
|
110
|
+
/** Last upstream check per coordinate id. Read from the local store only. */
|
|
111
|
+
upstream?: Record<string, CommunityUpstreamStatus>;
|
|
112
|
+
/** `community:` ids the declaration asks for and this machine lacks. */
|
|
113
|
+
fetchable?: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function matches(source: StyleSource, lowerQuery: string): boolean {
|
|
117
|
+
if (!lowerQuery) return true;
|
|
118
|
+
const summary =
|
|
119
|
+
source.kind === "preset" ? source.summary : source.description;
|
|
120
|
+
// Filter on the displayed name too. A captured built-in shows as
|
|
121
|
+
// "Explanatory" and a titled preset as its full standard name; typing what
|
|
122
|
+
// you can see has to find it.
|
|
123
|
+
const shown = source.displayName;
|
|
124
|
+
return (
|
|
125
|
+
source.name.toLowerCase().includes(lowerQuery) ||
|
|
126
|
+
shown.toLowerCase().includes(lowerQuery) ||
|
|
127
|
+
summary.toLowerCase().includes(lowerQuery)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Same rule for an offer, over the fields an offer actually has. */
|
|
132
|
+
function offerMatches(entry: CommunityStyle, lowerQuery: string): boolean {
|
|
133
|
+
if (!lowerQuery) return true;
|
|
134
|
+
return (
|
|
135
|
+
entry.id.toLowerCase().includes(lowerQuery) ||
|
|
136
|
+
entry.displayName.toLowerCase().includes(lowerQuery) ||
|
|
137
|
+
entry.summary.toLowerCase().includes(lowerQuery)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Which already-selected preset cancels `preset` out.
|
|
143
|
+
*
|
|
144
|
+
* `conflicts` is declared on one side of a pair in the shipped presets but the
|
|
145
|
+
* relationship is symmetric, so both directions are checked — otherwise the
|
|
146
|
+
* warning appears or not depending on which of the two the user ticked first.
|
|
147
|
+
*/
|
|
148
|
+
function findConflict(
|
|
149
|
+
preset: StylePreset,
|
|
150
|
+
selectedPresets: StylePreset[],
|
|
151
|
+
): string | null {
|
|
152
|
+
for (const other of selectedPresets) {
|
|
153
|
+
if (other.name === preset.name) continue;
|
|
154
|
+
if (
|
|
155
|
+
preset.conflicts.includes(other.name) ||
|
|
156
|
+
other.conflicts.includes(preset.name)
|
|
157
|
+
) {
|
|
158
|
+
return other.name;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build the flat list for the Styles screen.
|
|
166
|
+
*
|
|
167
|
+
* Section order is Anthropic official, verbosity (exactly one), modifiers (any
|
|
168
|
+
* number), the project's own styles, this machine's, and finally other people's.
|
|
169
|
+
* Category rows are non-selectable headers — the screen's key handler skips
|
|
170
|
+
* them, and an empty section is omitted entirely rather than shown with a zero
|
|
171
|
+
* count.
|
|
172
|
+
*/
|
|
173
|
+
export function buildStyleBrowserItems({
|
|
174
|
+
presets,
|
|
175
|
+
imports,
|
|
176
|
+
selected,
|
|
177
|
+
applied,
|
|
178
|
+
query,
|
|
179
|
+
registry = COMMUNITY_STYLES,
|
|
180
|
+
upstream = {},
|
|
181
|
+
fetchable = [],
|
|
182
|
+
}: BuildStyleBrowserItemsArgs): StyleBrowserItem[] {
|
|
183
|
+
const lowerQuery = query.trim().toLowerCase();
|
|
184
|
+
const items: StyleBrowserItem[] = [];
|
|
185
|
+
|
|
186
|
+
const appliedPresets = new Set(applied?.presets ?? []);
|
|
187
|
+
const appliedImports = new Set(applied?.imports ?? []);
|
|
188
|
+
const selectedPresets = presets.filter((preset) => selected.has(preset.id));
|
|
189
|
+
|
|
190
|
+
const verbosity = presets.filter((preset) => preset.axis === "verbosity");
|
|
191
|
+
const modifiers = presets.filter((preset) => preset.axis === "modifier");
|
|
192
|
+
|
|
193
|
+
const pushPresets = (
|
|
194
|
+
group: StylePreset[],
|
|
195
|
+
categoryKey: StyleCategoryKey,
|
|
196
|
+
title: string,
|
|
197
|
+
tone: StyleCategoryItem["tone"],
|
|
198
|
+
badge: string,
|
|
199
|
+
) => {
|
|
200
|
+
const filtered = group.filter((preset) => matches(preset, lowerQuery));
|
|
201
|
+
if (filtered.length === 0) return;
|
|
202
|
+
items.push({
|
|
203
|
+
id: `cat:${categoryKey}`,
|
|
204
|
+
kind: "category",
|
|
205
|
+
label: title,
|
|
206
|
+
title,
|
|
207
|
+
categoryKey,
|
|
208
|
+
count: filtered.length,
|
|
209
|
+
tone,
|
|
210
|
+
badge,
|
|
211
|
+
});
|
|
212
|
+
for (const preset of filtered) {
|
|
213
|
+
items.push({
|
|
214
|
+
id: preset.id,
|
|
215
|
+
kind: "style",
|
|
216
|
+
label: preset.displayName,
|
|
217
|
+
source: preset,
|
|
218
|
+
checked: selected.has(preset.id),
|
|
219
|
+
applied: appliedPresets.has(preset.name),
|
|
220
|
+
conflictsWith:
|
|
221
|
+
preset.axis === "verbosity"
|
|
222
|
+
? null
|
|
223
|
+
: findConflict(preset, selectedPresets),
|
|
224
|
+
disabled: preset.template,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const pushImports = (
|
|
230
|
+
origin: ImportedStyle["origin"],
|
|
231
|
+
categoryKey: StyleCategoryKey,
|
|
232
|
+
title: string,
|
|
233
|
+
tone: StyleCategoryItem["tone"],
|
|
234
|
+
badge: string,
|
|
235
|
+
) => {
|
|
236
|
+
const filtered = imports.filter(
|
|
237
|
+
(style) => style.origin === origin && matches(style, lowerQuery),
|
|
238
|
+
);
|
|
239
|
+
if (filtered.length === 0) return;
|
|
240
|
+
items.push({
|
|
241
|
+
id: `cat:${categoryKey}`,
|
|
242
|
+
kind: "category",
|
|
243
|
+
label: title,
|
|
244
|
+
title,
|
|
245
|
+
categoryKey,
|
|
246
|
+
count: filtered.length,
|
|
247
|
+
tone,
|
|
248
|
+
badge,
|
|
249
|
+
});
|
|
250
|
+
for (const style of filtered) {
|
|
251
|
+
items.push({
|
|
252
|
+
id: style.id,
|
|
253
|
+
kind: "style",
|
|
254
|
+
// The displayed name, not the on-disk slug — "Explanatory", not
|
|
255
|
+
// "builtin-explanatory". `id` still carries the real one.
|
|
256
|
+
label: style.displayName,
|
|
257
|
+
source: style,
|
|
258
|
+
checked: selected.has(style.id),
|
|
259
|
+
applied: appliedImports.has(style.id),
|
|
260
|
+
conflictsWith: null,
|
|
261
|
+
disabled: false,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* ONE stable order: registry order, by source then display name, whether a
|
|
268
|
+
* style is fetched or still an offer.
|
|
269
|
+
*
|
|
270
|
+
* Grouping fetched ones first was worse in use. Fetching moved the row out
|
|
271
|
+
* of the offers group and up into the fetched group, so everything below it
|
|
272
|
+
* shifted by one and the cursor — which tracks an index — ended up on a
|
|
273
|
+
* different style than the one just acted on. Position now says WHERE a
|
|
274
|
+
* style is in the registry and nothing else; whether it is on disk is the
|
|
275
|
+
* checkbox's job, which is the thing that can change without moving.
|
|
276
|
+
*
|
|
277
|
+
* Never ordered by popularity: the best-known names in this space ship no
|
|
278
|
+
* style files at all, so any such ranking would be one the registry cannot
|
|
279
|
+
* honour.
|
|
280
|
+
*/
|
|
281
|
+
const pushCommunity = () => {
|
|
282
|
+
const declared = new Set(fetchable);
|
|
283
|
+
const fetched = imports.filter((style) => style.origin === "community");
|
|
284
|
+
// A community style's `name` IS its registry coordinate id, which is what
|
|
285
|
+
// lets one row hold either state without moving.
|
|
286
|
+
const onDisk = new Map(fetched.map((style) => [style.name, style]));
|
|
287
|
+
|
|
288
|
+
// Walk the registry in its own order and emit whichever state each entry
|
|
289
|
+
// is in. A fetched entry keeps the slot its offer occupied.
|
|
290
|
+
const rows = registry
|
|
291
|
+
.filter((entry) => !entry.retired || onDisk.has(entry.id))
|
|
292
|
+
.filter(
|
|
293
|
+
(entry) =>
|
|
294
|
+
findCommunitySource(entry.sourceId)?.retired !== true ||
|
|
295
|
+
onDisk.has(entry.id),
|
|
296
|
+
)
|
|
297
|
+
.filter((entry) => {
|
|
298
|
+
const style = onDisk.get(entry.id);
|
|
299
|
+
// Filter on whichever representation is showing, so typing a query
|
|
300
|
+
// cannot make a row vanish just because it changed state.
|
|
301
|
+
return style
|
|
302
|
+
? matches(style, lowerQuery)
|
|
303
|
+
: offerMatches(entry, lowerQuery);
|
|
304
|
+
})
|
|
305
|
+
.sort(
|
|
306
|
+
(a, b) =>
|
|
307
|
+
a.sourceId.localeCompare(b.sourceId) ||
|
|
308
|
+
a.displayName.localeCompare(b.displayName),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Anything on disk the registry no longer lists — a retired entry pulled
|
|
312
|
+
// before it was dropped. It still belongs to the user, so it is shown,
|
|
313
|
+
// after the registry rows so it cannot shift them.
|
|
314
|
+
const listed = new Set(rows.map((entry) => entry.id));
|
|
315
|
+
const orphans = fetched
|
|
316
|
+
.filter((style) => !listed.has(style.name))
|
|
317
|
+
.filter((style) => matches(style, lowerQuery))
|
|
318
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
319
|
+
|
|
320
|
+
if (rows.length === 0 && orphans.length === 0) return;
|
|
321
|
+
|
|
322
|
+
items.push({
|
|
323
|
+
id: "cat:community",
|
|
324
|
+
kind: "category",
|
|
325
|
+
label: "Community",
|
|
326
|
+
title: "Community",
|
|
327
|
+
categoryKey: "community",
|
|
328
|
+
count: rows.length + orphans.length,
|
|
329
|
+
// Yellow, shared with Anthropic captures: both sections are text
|
|
330
|
+
// captured from somewhere else onto this disk, which is the right
|
|
331
|
+
// pairing. Red is the only unused tone and it means danger here — a
|
|
332
|
+
// normal, working section must not wear it.
|
|
333
|
+
tone: "yellow",
|
|
334
|
+
badge: "from GitHub",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const pushFetched = (style: ImportedStyle) => {
|
|
338
|
+
items.push({
|
|
339
|
+
id: style.id,
|
|
340
|
+
kind: "style",
|
|
341
|
+
label: style.displayName,
|
|
342
|
+
source: style,
|
|
343
|
+
checked: selected.has(style.id),
|
|
344
|
+
applied: appliedImports.has(style.id),
|
|
345
|
+
conflictsWith: null,
|
|
346
|
+
disabled: false,
|
|
347
|
+
upstream: upstream[style.name],
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
for (const entry of rows) {
|
|
352
|
+
const style = onDisk.get(entry.id);
|
|
353
|
+
if (style) {
|
|
354
|
+
pushFetched(style);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const source = findCommunitySource(entry.sourceId);
|
|
358
|
+
if (!source) continue;
|
|
359
|
+
items.push({
|
|
360
|
+
id: `offer:${entry.id}`,
|
|
361
|
+
kind: "offer",
|
|
362
|
+
label: entry.displayName,
|
|
363
|
+
entry,
|
|
364
|
+
source,
|
|
365
|
+
declared: declared.has(`community:${entry.id}`),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const style of orphans) pushFetched(style);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Badges are kept short deliberately: the row already carries a title and a
|
|
373
|
+
// count, and a long badge wraps onto a second line on a narrow pane, which
|
|
374
|
+
// costs a whole row and breaks the scan down the left column. The full
|
|
375
|
+
// explanation lives in the detail panel, where there is room for it.
|
|
376
|
+
pushImports(
|
|
377
|
+
"anthropic",
|
|
378
|
+
"anthropic",
|
|
379
|
+
"Anthropic official",
|
|
380
|
+
"yellow",
|
|
381
|
+
"captured",
|
|
382
|
+
);
|
|
383
|
+
pushPresets(verbosity, "verbosity", "Verbosity", "purple", "pick one");
|
|
384
|
+
pushPresets(modifiers, "modifier", "Modifiers", "teal", "combine freely");
|
|
385
|
+
pushImports("team", "team", "Team", "green", "in the repo");
|
|
386
|
+
pushImports("personal", "personal", "Personal", "gray", "this machine");
|
|
387
|
+
pushCommunity();
|
|
388
|
+
|
|
389
|
+
return items;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Index of the first row the user can actually land on, or 0 if none.
|
|
394
|
+
*
|
|
395
|
+
* `kind !== "category"` rather than `kind === "style"`: an offer is landable, it
|
|
396
|
+
* simply cannot be ticked. A cursor that skipped offers would make the whole
|
|
397
|
+
* section unreachable for a user who has fetched nothing — which is every user
|
|
398
|
+
* on their first launch.
|
|
399
|
+
*/
|
|
400
|
+
export function firstSelectableIndex(items: StyleBrowserItem[]): number {
|
|
401
|
+
const index = items.findIndex((item) => item.kind !== "category");
|
|
402
|
+
return index >= 0 ? index : 0;
|
|
403
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type UiColor, theme } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
interface CategoryHeaderProps {
|
|
5
|
+
/** Category title */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Optional version to render after the title (e.g., "8.0.0" → " v8.0.0") */
|
|
8
|
+
version?: string;
|
|
9
|
+
/** Status badge (e.g., "✓ Configured", "3 plugins") */
|
|
10
|
+
status?: string;
|
|
11
|
+
/** Status badge color */
|
|
12
|
+
statusColor?: UiColor;
|
|
13
|
+
/** Whether category is expanded */
|
|
14
|
+
expanded?: boolean;
|
|
15
|
+
/** Number of items in category */
|
|
16
|
+
count?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CategoryHeader({
|
|
20
|
+
title,
|
|
21
|
+
version,
|
|
22
|
+
status,
|
|
23
|
+
statusColor = theme.colors.success,
|
|
24
|
+
expanded = true,
|
|
25
|
+
count,
|
|
26
|
+
}: CategoryHeaderProps) {
|
|
27
|
+
const expandIcon = expanded ? "▼" : "▶";
|
|
28
|
+
const versionBadge = version ? ` v${version}` : "";
|
|
29
|
+
const countBadge = count !== undefined ? ` (${count})` : "";
|
|
30
|
+
const statusText = status ? ` ${status}` : "";
|
|
31
|
+
|
|
32
|
+
// Flex row instead of one flat <text>: a flat text wraps when the pane is
|
|
33
|
+
// narrower than the row ("by MadAppGang" lost its final character and the wrap
|
|
34
|
+
// shifted every item below by one line). Here the dash filler is the only
|
|
35
|
+
// element allowed to shrink to nothing, so the badge survives at any width
|
|
36
|
+
// and the title clips last.
|
|
37
|
+
return (
|
|
38
|
+
<box flexDirection="row" width="100%" height={1} overflow="hidden">
|
|
39
|
+
<box flexShrink={1} minWidth={4} overflow="hidden" height={1}>
|
|
40
|
+
<text fg={theme.colors.text}>
|
|
41
|
+
<span fg={theme.colors.muted}>{expandIcon}</span>
|
|
42
|
+
<span fg={theme.colors.text}>
|
|
43
|
+
<strong> {title}</strong>
|
|
44
|
+
</span>
|
|
45
|
+
<span fg={theme.colors.muted}>{versionBadge}</span>
|
|
46
|
+
<span fg={theme.colors.muted}>{countBadge}</span>
|
|
47
|
+
</text>
|
|
48
|
+
</box>
|
|
49
|
+
{/* flexShrink 100: the filler must be the first thing sacrificed. With equal
|
|
50
|
+
shrink factors yoga takes width from the (larger-basis) title first, which
|
|
51
|
+
clipped a character off the title while all four dashes survived. */}
|
|
52
|
+
<box flexShrink={100} minWidth={0} overflow="hidden" height={1}>
|
|
53
|
+
<text fg={theme.colors.border}> ────</text>
|
|
54
|
+
</box>
|
|
55
|
+
{status ? (
|
|
56
|
+
<box flexShrink={0} height={1}>
|
|
57
|
+
<text fg={statusColor}>{statusText}</text>
|
|
58
|
+
</box>
|
|
59
|
+
) : null}
|
|
60
|
+
</box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default CategoryHeader;
|