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,542 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { areMatesAvailable } from "../../services/mate-availability.js";
|
|
3
|
+
import { loadMateCatalog } from "../../services/mate-catalog.js";
|
|
4
|
+
import {
|
|
5
|
+
applyModelPreset,
|
|
6
|
+
clearModels,
|
|
7
|
+
readModelsConfig,
|
|
8
|
+
readModelsStatus,
|
|
9
|
+
} from "../../services/models-manager.js";
|
|
10
|
+
import {
|
|
11
|
+
type ModelsBrowserItem,
|
|
12
|
+
buildModelsItems,
|
|
13
|
+
firstSelectableIndex,
|
|
14
|
+
} from "../adapters/modelsAdapter.js";
|
|
15
|
+
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
16
|
+
import { ScrollableList } from "../components/ScrollableList.js";
|
|
17
|
+
import { ScreenLayout } from "../components/layout/index.js";
|
|
18
|
+
import { useKeyboard } from "../hooks/useKeyboard.js";
|
|
19
|
+
import {
|
|
20
|
+
fitPresetList,
|
|
21
|
+
renderModelDetail,
|
|
22
|
+
renderModelRow,
|
|
23
|
+
renderModelsSummary,
|
|
24
|
+
} from "../renderers/modelRenderers.js";
|
|
25
|
+
import { useApp } from "../state/AppContext.js";
|
|
26
|
+
import { useDimensions } from "../state/DimensionsContext.js";
|
|
27
|
+
import { asyncValue } from "../state/types.js";
|
|
28
|
+
import { theme } from "../theme.js";
|
|
29
|
+
|
|
30
|
+
export function ModelsScreen() {
|
|
31
|
+
const { state, dispatch } = useApp();
|
|
32
|
+
const { models: modelsState } = state;
|
|
33
|
+
const dimensions = useDimensions();
|
|
34
|
+
|
|
35
|
+
const isSearchActive =
|
|
36
|
+
state.isSearching && state.currentRoute.screen === "models" && !state.modal;
|
|
37
|
+
|
|
38
|
+
// ── Data ──────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Both reads, in one dispatch.
|
|
42
|
+
*
|
|
43
|
+
* `readModelsStatus` answers "is this routing in force"; `readModelsConfig`
|
|
44
|
+
* is the routing itself, which the list needs in full to give a hand-edited
|
|
45
|
+
* config a row of its own. Neither is derivable from the other — a `status`
|
|
46
|
+
* of `invalid` reports no preset at all, and an `off` project still has a
|
|
47
|
+
* path worth naming.
|
|
48
|
+
*/
|
|
49
|
+
const fetchData = useCallback(async () => {
|
|
50
|
+
dispatch({ type: "MODELS_DATA_LOADING" });
|
|
51
|
+
try {
|
|
52
|
+
const [status, config] = await Promise.all([
|
|
53
|
+
readModelsStatus(state.projectPath),
|
|
54
|
+
readModelsConfig(state.projectPath),
|
|
55
|
+
]);
|
|
56
|
+
dispatch({
|
|
57
|
+
type: "MODELS_DATA_SUCCESS",
|
|
58
|
+
snapshot: { status, config: config.config, path: config.path },
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
dispatch({
|
|
62
|
+
type: "MODELS_DATA_ERROR",
|
|
63
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}, [dispatch, state.projectPath]);
|
|
67
|
+
|
|
68
|
+
// dataRefreshVersion is a refetch signal, not a value this effect reads:
|
|
69
|
+
// bumping it is how the app tells every screen its data changed.
|
|
70
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: signal, not a read
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
fetchData();
|
|
73
|
+
}, [fetchData, state.dataRefreshVersion]);
|
|
74
|
+
|
|
75
|
+
const snapshot = asyncValue(modelsState.data) ?? null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the mate slots have anything to serve them.
|
|
79
|
+
*
|
|
80
|
+
* Its OWN effect, deliberately outside the `fetchData` pair above. That pair is two file
|
|
81
|
+
* reads and lands in milliseconds; this one goes through the plugin registry and the
|
|
82
|
+
* marketplace resolution behind it, which does network work with its own timeouts.
|
|
83
|
+
* Awaiting it in the same `Promise.all` would hold the whole screen — the presets, the
|
|
84
|
+
* tier table, the status — on a question that decides three rows.
|
|
85
|
+
*
|
|
86
|
+
* App state rather than `useState`, and NOT only to satisfy the rule: `Router` swaps the
|
|
87
|
+
* component type on a tab change, so a local flag dies on `0 → 1 → 0` and this is the one
|
|
88
|
+
* piece of this screen's state that is expensive to fetch again. Held here, the lookup
|
|
89
|
+
* happens once per session instead of once per visit.
|
|
90
|
+
*
|
|
91
|
+
* It starts false and the rows appear when the answer lands. False is also what a failure
|
|
92
|
+
* yields (`areMatesAvailable` fails closed), so both render identically: the screen
|
|
93
|
+
* exactly as it was before mates existed.
|
|
94
|
+
*/
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let live = true;
|
|
97
|
+
areMatesAvailable(state.projectPath).then((available) => {
|
|
98
|
+
// The screen can be left while this is in flight. Dispatching into a reducer that
|
|
99
|
+
// outlives the component is harmless, but re-dispatching a stale answer after the
|
|
100
|
+
// project path changed is not.
|
|
101
|
+
if (live) dispatch({ type: "MODELS_MATES_AVAILABLE", available });
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
live = false;
|
|
105
|
+
};
|
|
106
|
+
}, [state.projectPath, dispatch]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* claudish's live model catalogue, for the staleness advisory.
|
|
110
|
+
*
|
|
111
|
+
* A third effect rather than a branch of the second, even though both are about mates:
|
|
112
|
+
* this one spawns a process and the other reads a registry, and chaining them would make
|
|
113
|
+
* the advisory wait on a plugin lookup it does not depend on. Neither blocks the screen.
|
|
114
|
+
*
|
|
115
|
+
* NOT gated on `matesAvailable`. The catalogue answers "is this bound id real", which is
|
|
116
|
+
* worth knowing about a committed config whether or not this particular machine has the
|
|
117
|
+
* plugin installed — and gating it would make the note appear only on machines that need
|
|
118
|
+
* it least. The lookup is cached per session, so it costs one spawn either way.
|
|
119
|
+
*
|
|
120
|
+
* No project path in the deps: a model catalogue is a property of the world, not of the
|
|
121
|
+
* project. `loadMateCatalog` caches it for the session, so this re-runs into a resolved
|
|
122
|
+
* promise on every remount.
|
|
123
|
+
*/
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
let live = true;
|
|
126
|
+
// Failure is already [] — `loadMateCatalog` never rejects — so there is no `.catch`
|
|
127
|
+
// here to swallow. If one becomes necessary the module's contract has been broken.
|
|
128
|
+
loadMateCatalog().then((catalog) => {
|
|
129
|
+
if (live) dispatch({ type: "MODELS_CATALOG_LOADED", catalog });
|
|
130
|
+
});
|
|
131
|
+
return () => {
|
|
132
|
+
live = false;
|
|
133
|
+
};
|
|
134
|
+
}, [dispatch]);
|
|
135
|
+
|
|
136
|
+
// ── Status line ───────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set the status line.
|
|
140
|
+
*
|
|
141
|
+
* No timer, matching the Styles screen: a message that erased itself after a
|
|
142
|
+
* few seconds was usually gone by the time anyone looked. It is cleared when
|
|
143
|
+
* the NEXT action starts instead.
|
|
144
|
+
*/
|
|
145
|
+
const showStatus = useCallback(
|
|
146
|
+
(text: string, tone: "success" | "error" = "success") => {
|
|
147
|
+
dispatch({ type: "MODELS_STATUS_SET", status: { text, tone } });
|
|
148
|
+
},
|
|
149
|
+
[dispatch],
|
|
150
|
+
);
|
|
151
|
+
const statusMsg = modelsState.status;
|
|
152
|
+
|
|
153
|
+
// ── Derived ───────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
const allItems = useMemo(
|
|
156
|
+
() =>
|
|
157
|
+
buildModelsItems({
|
|
158
|
+
status: snapshot?.status ?? null,
|
|
159
|
+
config: snapshot?.config ?? null,
|
|
160
|
+
query: modelsState.searchQuery,
|
|
161
|
+
}),
|
|
162
|
+
[snapshot, modelsState.searchQuery],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Keep the cursor off the status header — it is a label, not a row.
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const item = allItems[modelsState.selectedIndex];
|
|
168
|
+
if (allItems.length > 0 && (!item || item.kind === "status")) {
|
|
169
|
+
dispatch({
|
|
170
|
+
type: "MODELS_SELECT",
|
|
171
|
+
index: firstSelectableIndex(allItems),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}, [allItems, modelsState.selectedIndex, dispatch]);
|
|
175
|
+
|
|
176
|
+
const selectedItem: ModelsBrowserItem | undefined =
|
|
177
|
+
allItems[modelsState.selectedIndex];
|
|
178
|
+
|
|
179
|
+
// ── Actions ───────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Write the selected preset's routing to this project.
|
|
183
|
+
*
|
|
184
|
+
* A hand-edited config is re-applied as an OBJECT rather than by name —
|
|
185
|
+
* `applyModelPreset` resolves a string against the built-ins, and this one is
|
|
186
|
+
* by definition not among them. Re-applying it is also the fix for `stale`:
|
|
187
|
+
* it rewrites the settings the config implies.
|
|
188
|
+
*
|
|
189
|
+
* No `ensureManifest` is passed. That helper prompts on the terminal, and
|
|
190
|
+
* this process's terminal belongs to OpenTUI — a project with no profiles
|
|
191
|
+
* gets a plain `.claude/models.json`, and the status line says so.
|
|
192
|
+
*/
|
|
193
|
+
const handleApply = useCallback(async () => {
|
|
194
|
+
if (modelsState.isApplying) {
|
|
195
|
+
showStatus("Already writing the routing — one at a time", "error");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!selectedItem || selectedItem.kind !== "preset") {
|
|
199
|
+
showStatus("Move to a preset row and press a", "error");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const { config, custom } = selectedItem;
|
|
203
|
+
|
|
204
|
+
dispatch({ type: "MODELS_APPLY_START" });
|
|
205
|
+
try {
|
|
206
|
+
const result = await applyModelPreset({
|
|
207
|
+
// `config.preset`, NOT `label`. The row is titled with a human name now
|
|
208
|
+
// ("Opus with Fable help"), and `applyModelPreset` resolves an id against the
|
|
209
|
+
// built-ins — so passing the label refused every apply with `Unknown preset
|
|
210
|
+
// "Opus with Fable help"`. Found by pressing the key, not by a test: the
|
|
211
|
+
// types agree, both are strings.
|
|
212
|
+
projectPath: state.projectPath,
|
|
213
|
+
preset: custom ? config : config.preset,
|
|
214
|
+
});
|
|
215
|
+
await fetchData();
|
|
216
|
+
const where = result.profile
|
|
217
|
+
? `profile "${result.profile}"`
|
|
218
|
+
: "this project";
|
|
219
|
+
// Say plainly when the manifest was NOT updated: without that record a
|
|
220
|
+
// `magus install` rewrites the profile and the routing disappears.
|
|
221
|
+
const durability =
|
|
222
|
+
result.profile && !result.recordedInManifest
|
|
223
|
+
? " — not in profiles.json, so `magus install` will drop it"
|
|
224
|
+
: "";
|
|
225
|
+
showStatus(
|
|
226
|
+
`Applied ${selectedItem.label} in ${where}, hook ${result.hook}${durability}`,
|
|
227
|
+
result.profile && !result.recordedInManifest ? "error" : "success",
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
showStatus(
|
|
231
|
+
error instanceof Error ? error.message : String(error),
|
|
232
|
+
"error",
|
|
233
|
+
);
|
|
234
|
+
} finally {
|
|
235
|
+
dispatch({ type: "MODELS_APPLY_END" });
|
|
236
|
+
}
|
|
237
|
+
}, [
|
|
238
|
+
modelsState.isApplying,
|
|
239
|
+
selectedItem,
|
|
240
|
+
state.projectPath,
|
|
241
|
+
fetchData,
|
|
242
|
+
showStatus,
|
|
243
|
+
dispatch,
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const handleClear = useCallback(async () => {
|
|
247
|
+
if (modelsState.isApplying) {
|
|
248
|
+
showStatus("Already writing the routing — one at a time", "error");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
dispatch({ type: "MODELS_APPLY_START" });
|
|
252
|
+
try {
|
|
253
|
+
await clearModels(state.projectPath);
|
|
254
|
+
await fetchData();
|
|
255
|
+
showStatus(
|
|
256
|
+
"Model tiers off — subagents inherit the session model again. The hook stays registered and is a no-op without a config.",
|
|
257
|
+
);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
showStatus(
|
|
260
|
+
error instanceof Error ? error.message : String(error),
|
|
261
|
+
"error",
|
|
262
|
+
);
|
|
263
|
+
} finally {
|
|
264
|
+
dispatch({ type: "MODELS_APPLY_END" });
|
|
265
|
+
}
|
|
266
|
+
}, [
|
|
267
|
+
modelsState.isApplying,
|
|
268
|
+
state.projectPath,
|
|
269
|
+
fetchData,
|
|
270
|
+
showStatus,
|
|
271
|
+
dispatch,
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
// ── Keyboard ──────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
const move = useCallback(
|
|
277
|
+
(delta: -1 | 1) => {
|
|
278
|
+
let index = modelsState.selectedIndex + delta;
|
|
279
|
+
while (index >= 0 && index < allItems.length) {
|
|
280
|
+
if (allItems[index] && allItems[index].kind !== "status") {
|
|
281
|
+
dispatch({ type: "MODELS_SELECT", index });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
index += delta;
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[modelsState.selectedIndex, allItems, dispatch],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
useKeyboard((event) => {
|
|
291
|
+
if (state.modal) return;
|
|
292
|
+
|
|
293
|
+
// The previous message belongs to the previous ACTION, so clearing it here
|
|
294
|
+
// covers every action key in one place. Navigation is excluded: a digit or
|
|
295
|
+
// Tab is handled by the global handler, and clearing on it wiped the
|
|
296
|
+
// message on the way OUT of the tab, losing it on a round trip.
|
|
297
|
+
const isNavigation =
|
|
298
|
+
/^[0-9]$/.test(event.name ?? "") || event.name === "tab";
|
|
299
|
+
if (modelsState.status && !isNavigation) {
|
|
300
|
+
dispatch({ type: "MODELS_STATUS_CLEAR" });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const hasQuery = modelsState.searchQuery.length > 0;
|
|
304
|
+
|
|
305
|
+
if (event.name === "escape") {
|
|
306
|
+
if (hasQuery || isSearchActive) {
|
|
307
|
+
dispatch({ type: "MODELS_SET_SEARCH", query: "" });
|
|
308
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
309
|
+
dispatch({ type: "MODELS_SELECT", index: 0 });
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (event.name === "backspace" || event.name === "delete") {
|
|
315
|
+
if (hasQuery) {
|
|
316
|
+
dispatch({ type: "MODELS_SEARCH_BACKSPACE" });
|
|
317
|
+
// Driven by what was there BEFORE this keystroke — the one thing the
|
|
318
|
+
// closure knows reliably.
|
|
319
|
+
if (modelsState.searchQuery.length <= 1) {
|
|
320
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (event.name === "up") {
|
|
327
|
+
if (isSearchActive)
|
|
328
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
329
|
+
move(-1);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (event.name === "down") {
|
|
333
|
+
if (isSearchActive)
|
|
334
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
335
|
+
move(1);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (event.name === "return" || event.name === "enter") {
|
|
340
|
+
if (isSearchActive) {
|
|
341
|
+
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Enter and `a` are the same action here. There is nothing to "open" on
|
|
345
|
+
// this screen — a preset row has one thing it can do — so binding Enter
|
|
346
|
+
// to anything else would leave the most obvious key doing nothing.
|
|
347
|
+
handleApply();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// While typing a filter, letters are text — not commands.
|
|
352
|
+
if (isSearchActive) {
|
|
353
|
+
if (
|
|
354
|
+
event.name &&
|
|
355
|
+
event.name.length === 1 &&
|
|
356
|
+
!event.ctrl &&
|
|
357
|
+
!event.meta &&
|
|
358
|
+
!/[0-9]/.test(event.name)
|
|
359
|
+
) {
|
|
360
|
+
dispatch({ type: "MODELS_SEARCH_APPEND", char: event.name });
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (event.name === "k") {
|
|
366
|
+
move(-1);
|
|
367
|
+
} else if (event.name === "j") {
|
|
368
|
+
move(1);
|
|
369
|
+
} else if (event.name === "a") {
|
|
370
|
+
handleApply();
|
|
371
|
+
} else if (event.name === "c") {
|
|
372
|
+
handleClear();
|
|
373
|
+
} else if (event.name === "r") {
|
|
374
|
+
fetchData();
|
|
375
|
+
} else if (event.name === "/") {
|
|
376
|
+
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ── Chrome ────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
// Transient feedback takes the full-width notice row under the header, never
|
|
383
|
+
// the header itself: a long message beside the title shrinks the title away
|
|
384
|
+
// and the two read as one garbled sentence.
|
|
385
|
+
const noticeContent = statusMsg ? (
|
|
386
|
+
<text
|
|
387
|
+
fg={
|
|
388
|
+
statusMsg.tone === "success"
|
|
389
|
+
? theme.colors.success
|
|
390
|
+
: theme.colors.danger
|
|
391
|
+
}
|
|
392
|
+
>
|
|
393
|
+
{statusMsg.text}
|
|
394
|
+
</text>
|
|
395
|
+
) : undefined;
|
|
396
|
+
|
|
397
|
+
// The file's `preset` string is deliberately not shown beside the state: it records what
|
|
398
|
+
// was last APPLIED, so a config edited afterwards would wear a built-in's name here while
|
|
399
|
+
// the list correctly calls it `Custom`.
|
|
400
|
+
const routingState = snapshot?.status.state ?? null;
|
|
401
|
+
const statusContent = (
|
|
402
|
+
<text fg={theme.colors.text}>
|
|
403
|
+
<span fg={theme.colors.muted}>Model tiers: </span>
|
|
404
|
+
<span
|
|
405
|
+
fg={
|
|
406
|
+
routingState === "on"
|
|
407
|
+
? theme.colors.success
|
|
408
|
+
: routingState === "off" || routingState === null
|
|
409
|
+
? theme.colors.muted
|
|
410
|
+
: theme.colors.danger
|
|
411
|
+
}
|
|
412
|
+
>
|
|
413
|
+
{routingState ?? "reading…"}
|
|
414
|
+
</span>
|
|
415
|
+
{modelsState.isApplying ? (
|
|
416
|
+
<span fg={theme.colors.warning}> │ writing…</span>
|
|
417
|
+
) : null}
|
|
418
|
+
</text>
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
// The panel widths come from ScreenLayout, which owns the split, the padding
|
|
424
|
+
// and the scrollbar column. This screen used to re-derive them from
|
|
425
|
+
// `terminalWidth` with a copy of the formula, and the copy was wrong.
|
|
426
|
+
const query = modelsState.searchQuery.trim();
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<ScreenLayout
|
|
430
|
+
title="magus Models"
|
|
431
|
+
currentScreen="models"
|
|
432
|
+
statusLine={statusContent}
|
|
433
|
+
notice={noticeContent}
|
|
434
|
+
search={
|
|
435
|
+
modelsState.searchQuery || isSearchActive
|
|
436
|
+
? {
|
|
437
|
+
isActive: isSearchActive,
|
|
438
|
+
query: modelsState.searchQuery,
|
|
439
|
+
placeholder: "type to filter",
|
|
440
|
+
}
|
|
441
|
+
: undefined
|
|
442
|
+
}
|
|
443
|
+
footerHints={
|
|
444
|
+
isSearchActive
|
|
445
|
+
? [
|
|
446
|
+
{ keys: ["type"], label: "filter" },
|
|
447
|
+
{ keys: ["Enter"], label: "done" },
|
|
448
|
+
{ keys: ["Esc"], label: "clear" },
|
|
449
|
+
]
|
|
450
|
+
: [
|
|
451
|
+
{ keys: ["Enter", "a"], label: "apply" },
|
|
452
|
+
{ keys: ["c"], label: "off" },
|
|
453
|
+
{ keys: ["r"], label: "reload" },
|
|
454
|
+
{ keys: ["/"], label: "filter" },
|
|
455
|
+
]
|
|
456
|
+
}
|
|
457
|
+
listPanel={(listWidth) => {
|
|
458
|
+
// ONE layout for every row, measured from the widest of them.
|
|
459
|
+
// Rows used to fit themselves, so at 120×30 the selected row
|
|
460
|
+
// dropped its `main` / `critical` labels while its neighbours kept
|
|
461
|
+
// theirs — four rows in two formats, nothing lining up.
|
|
462
|
+
const rowLayout = fitPresetList(allItems, listWidth);
|
|
463
|
+
return (
|
|
464
|
+
<box flexDirection="column">
|
|
465
|
+
{modelsState.data.status === "loading" && !snapshot && (
|
|
466
|
+
<box paddingLeft={2}>
|
|
467
|
+
<text fg={theme.colors.warning}>Reading the routing…</text>
|
|
468
|
+
</box>
|
|
469
|
+
)}
|
|
470
|
+
|
|
471
|
+
{modelsState.data.status === "error" && (
|
|
472
|
+
<box flexDirection="column" paddingLeft={2} paddingRight={2}>
|
|
473
|
+
<text fg={theme.colors.danger}>
|
|
474
|
+
Could not read the model routing.
|
|
475
|
+
</text>
|
|
476
|
+
<box marginTop={1}>
|
|
477
|
+
<text fg={theme.colors.muted}>
|
|
478
|
+
{modelsState.data.error.message}
|
|
479
|
+
</text>
|
|
480
|
+
</box>
|
|
481
|
+
</box>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{allItems.length > 0 && (
|
|
485
|
+
<ScrollableList
|
|
486
|
+
items={allItems}
|
|
487
|
+
selectedIndex={modelsState.selectedIndex}
|
|
488
|
+
renderItem={(item, index, isSelected) =>
|
|
489
|
+
renderModelRow(item, index, isSelected, rowLayout)
|
|
490
|
+
}
|
|
491
|
+
maxHeight={dimensions.listPanelHeight}
|
|
492
|
+
getKey={(item, index) => `${index}:${item.id}`}
|
|
493
|
+
/>
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
{query.length > 0 &&
|
|
497
|
+
allItems.length === 0 &&
|
|
498
|
+
modelsState.data.status !== "error" && (
|
|
499
|
+
<EmptyFilterState
|
|
500
|
+
query={modelsState.searchQuery}
|
|
501
|
+
entityName="presets"
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{/* Under the list, not in the detail pane. Four preset rows left
|
|
506
|
+
the rest of this panel unpainted, and the screen's one graphic
|
|
507
|
+
was buried below the fold of a pane nobody had scrolled. It
|
|
508
|
+
follows the cursor, so it answers "what would THIS preset do"
|
|
509
|
+
while you are choosing. */}
|
|
510
|
+
{allItems.length > 0 &&
|
|
511
|
+
renderModelsSummary({
|
|
512
|
+
selected: selectedItem,
|
|
513
|
+
status: snapshot?.status ?? null,
|
|
514
|
+
configPath: snapshot?.path ?? null,
|
|
515
|
+
width: listWidth,
|
|
516
|
+
// Rows the list did not use. One held back because the
|
|
517
|
+
// search bar and the notice each take a row from the panel
|
|
518
|
+
// without taking one from `listPanelHeight` — and an
|
|
519
|
+
// over-tall panel overprints rather than clipping.
|
|
520
|
+
available:
|
|
521
|
+
dimensions.listPanelHeight -
|
|
522
|
+
Math.min(allItems.length, dimensions.listPanelHeight) -
|
|
523
|
+
1,
|
|
524
|
+
})}
|
|
525
|
+
</box>
|
|
526
|
+
);
|
|
527
|
+
}}
|
|
528
|
+
detailPanel={(detailWidth) =>
|
|
529
|
+
renderModelDetail(
|
|
530
|
+
selectedItem,
|
|
531
|
+
snapshot?.status ?? null,
|
|
532
|
+
detailWidth,
|
|
533
|
+
modelsState.matesAvailable,
|
|
534
|
+
modelsState.mateCatalog,
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
detailKey={selectedItem?.id ?? ""}
|
|
538
|
+
/>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export default ModelsScreen;
|