magus-cli 7.3.1 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/__tests__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +105 -14
- package/src/cli/install.ts +8 -2
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +151 -73
- package/src/services/cli-tool-commands.ts +15 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +158 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +272 -45
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +99 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +180 -0
- package/src/ui/input/key-dispatch.ts +149 -0
- package/src/ui/keymap.ts +898 -0
- package/src/ui/renderers/cliToolRenderers.tsx +128 -66
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +98 -87
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +123 -127
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
uninstallPlugin as cliUninstallPlugin,
|
|
13
13
|
} from "../../services/claude-cli.js";
|
|
14
14
|
import {
|
|
15
|
+
SettingsParseError,
|
|
15
16
|
getMcpEnvVars,
|
|
16
17
|
readSettings,
|
|
17
18
|
setMcpEnvVar,
|
|
@@ -68,6 +69,7 @@ import {
|
|
|
68
69
|
saveSeenVersions,
|
|
69
70
|
} from "../../services/version-snapshot.js";
|
|
70
71
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
72
|
+
import { createRequestGate } from "../../utils/request-gate.js";
|
|
71
73
|
import { buildCatalogNoticeText } from "../adapters/catalogNotice.js";
|
|
72
74
|
import {
|
|
73
75
|
type PluginBrowserItem,
|
|
@@ -80,7 +82,8 @@ import {
|
|
|
80
82
|
import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
81
83
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
82
84
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
83
|
-
import {
|
|
85
|
+
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
86
|
+
import { type BindingId, moveDirection, tabKey } from "../keymap.js";
|
|
84
87
|
import {
|
|
85
88
|
renderPluginDetail,
|
|
86
89
|
renderPluginRow,
|
|
@@ -90,6 +93,17 @@ import { useDimensions } from "../state/DimensionsContext.js";
|
|
|
90
93
|
import { asyncValue } from "../state/types.js";
|
|
91
94
|
import { theme } from "../theme.js";
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The latest Plugins load wins.
|
|
98
|
+
*
|
|
99
|
+
* A load starts on mount and again on every `DATA_REFRESH_COMPLETE` — at
|
|
100
|
+
* startup that is two loads racing, and whichever finished LAST used to be the
|
|
101
|
+
* one on screen, often the older. Module scope rather than a ref, because the
|
|
102
|
+
* router remounts this screen on every tab switch and a load begun by the
|
|
103
|
+
* previous mount still has to lose to the new one.
|
|
104
|
+
*/
|
|
105
|
+
const pluginsLoadGate = createRequestGate();
|
|
106
|
+
|
|
93
107
|
/** Wraps the notice text in the warning-coloured row ScreenLayout renders. */
|
|
94
108
|
function buildCatalogNotice(
|
|
95
109
|
input: Parameters<typeof buildCatalogNoticeText>[0],
|
|
@@ -107,9 +121,7 @@ export function PluginsScreen() {
|
|
|
107
121
|
const dimensions = useDimensions();
|
|
108
122
|
|
|
109
123
|
const isSearchActive =
|
|
110
|
-
state.isSearching &&
|
|
111
|
-
state.currentRoute.screen === "plugins" &&
|
|
112
|
-
!state.modal;
|
|
124
|
+
state.isSearching && state.currentRoute.screen === "plugins";
|
|
113
125
|
|
|
114
126
|
/**
|
|
115
127
|
* Catalogs that could not be read from any authoritative source.
|
|
@@ -136,8 +148,11 @@ export function PluginsScreen() {
|
|
|
136
148
|
Readonly<Record<string, string>> | null | undefined
|
|
137
149
|
>(undefined);
|
|
138
150
|
|
|
139
|
-
// Fetch data (always fetches all scopes)
|
|
151
|
+
// Fetch data (always fetches all scopes). Only the most recently started
|
|
152
|
+
// load may touch the screen — or the seen-versions baseline, which a stale
|
|
153
|
+
// load would rewind.
|
|
140
154
|
const fetchData = useCallback(async () => {
|
|
155
|
+
const token = pluginsLoadGate.begin();
|
|
141
156
|
dispatch({ type: "PLUGINS_DATA_LOADING" });
|
|
142
157
|
try {
|
|
143
158
|
const localMarketplaces = await getLocalMarketplacesInfo();
|
|
@@ -150,6 +165,7 @@ export function PluginsScreen() {
|
|
|
150
165
|
resolveBins: false,
|
|
151
166
|
}),
|
|
152
167
|
]);
|
|
168
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
153
169
|
setPins(active?.closure.plugins ?? null);
|
|
154
170
|
|
|
155
171
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
@@ -178,6 +194,7 @@ export function PluginsScreen() {
|
|
|
178
194
|
}
|
|
179
195
|
const projectPath = state.projectPath || process.cwd();
|
|
180
196
|
const previous = await loadSeenVersions(projectPath);
|
|
197
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
181
198
|
for (const change of diffVersions(previous, current)) {
|
|
182
199
|
const target = pluginData.find((p) => p.id === change.pluginId);
|
|
183
200
|
if (!target) continue;
|
|
@@ -191,6 +208,7 @@ export function PluginsScreen() {
|
|
|
191
208
|
// refresh or a post-action refetch could load the baseline before the
|
|
192
209
|
// previous save landed and re-report the same change.
|
|
193
210
|
await saveSeenVersions(projectPath, current);
|
|
211
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
194
212
|
|
|
195
213
|
dispatch({
|
|
196
214
|
type: "PLUGINS_DATA_SUCCESS",
|
|
@@ -198,6 +216,7 @@ export function PluginsScreen() {
|
|
|
198
216
|
plugins: pluginData,
|
|
199
217
|
});
|
|
200
218
|
} catch (error) {
|
|
219
|
+
if (!pluginsLoadGate.isCurrent(token)) return;
|
|
201
220
|
dispatch({
|
|
202
221
|
type: "PLUGINS_DATA_ERROR",
|
|
203
222
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -441,119 +460,79 @@ export function PluginsScreen() {
|
|
|
441
460
|
[dispatch, pluginsState.pluginContents, shownMarketplaces],
|
|
442
461
|
);
|
|
443
462
|
|
|
444
|
-
//
|
|
445
|
-
useKeyboard((event) => {
|
|
446
|
-
if (state.modal) return;
|
|
447
|
-
|
|
448
|
-
const hasQuery = pluginsState.searchQuery.length > 0;
|
|
449
|
-
|
|
450
|
-
if (event.name === "escape") {
|
|
451
|
-
if (hasQuery || isSearchActive) {
|
|
452
|
-
dispatch({ type: "PLUGINS_SET_SEARCH", query: "" });
|
|
453
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
454
|
-
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
463
|
+
// ── Keys ──────────────────────────────────────────────────────────────────
|
|
458
464
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
464
|
-
if (newQuery.length === 0) {
|
|
465
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (event.name === "up" || event.name === "k") {
|
|
472
|
-
if (isSearchActive)
|
|
473
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
474
|
-
dispatch({
|
|
475
|
-
type: "PLUGINS_SELECT",
|
|
476
|
-
index: Math.max(0, pluginsState.selectedIndex - 1),
|
|
477
|
-
});
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
if (event.name === "down" || event.name === "j") {
|
|
481
|
-
if (isSearchActive)
|
|
482
|
-
dispatch({ type: "SET_SEARCHING", isSearching: false });
|
|
483
|
-
dispatch({
|
|
484
|
-
type: "PLUGINS_SELECT",
|
|
485
|
-
index: Math.min(
|
|
486
|
-
selectableItems.length - 1,
|
|
487
|
-
pluginsState.selectedIndex + 1,
|
|
488
|
-
),
|
|
489
|
-
});
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
465
|
+
const setQuery = (query: string) => {
|
|
466
|
+
dispatch({ type: "PLUGINS_SET_SEARCH", query });
|
|
467
|
+
dispatch({ type: "PLUGINS_SELECT", index: 0 });
|
|
468
|
+
};
|
|
492
469
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
470
|
+
// The search box, while it has focus: digits, j/k, q and ? are typed here.
|
|
471
|
+
useSearchInput({
|
|
472
|
+
focused: isSearchActive,
|
|
473
|
+
query: pluginsState.searchQuery,
|
|
474
|
+
append: (char) => setQuery(pluginsState.searchQuery + char),
|
|
475
|
+
erase: () => setQuery(pluginsState.searchQuery.slice(0, -1)),
|
|
476
|
+
clear: () => setQuery(""),
|
|
477
|
+
blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
|
|
478
|
+
});
|
|
501
479
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
480
|
+
useScreenKeys("plugins", {
|
|
481
|
+
// Three keys, one binding: the footer reads "u/p/l toggle".
|
|
482
|
+
scope: (key) => {
|
|
483
|
+
void handleScopeToggle(
|
|
484
|
+
key.name === "u" ? "user" : key.name === "p" ? "project" : "local",
|
|
485
|
+
);
|
|
486
|
+
},
|
|
487
|
+
update: () => {
|
|
488
|
+
void handleUpdate();
|
|
489
|
+
},
|
|
490
|
+
"update-all": () => {
|
|
491
|
+
void handleUpdateAll();
|
|
492
|
+
},
|
|
493
|
+
open: () => {
|
|
494
|
+
void handleSelect();
|
|
495
|
+
},
|
|
496
|
+
expand: () => {
|
|
508
497
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
509
498
|
if (item?.kind === "category") {
|
|
510
499
|
dispatch({
|
|
511
500
|
type: "PLUGINS_TOGGLE_MARKETPLACE",
|
|
512
501
|
name: item.marketplace.name,
|
|
513
502
|
});
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
if (item?.kind === "plugin" && item.isExpandable) {
|
|
503
|
+
} else if (item?.kind === "plugin" && item.isExpandable) {
|
|
517
504
|
togglePluginContents(item);
|
|
518
|
-
return;
|
|
519
505
|
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
else if (event.name === "u" && event.shift) handleUpdate();
|
|
551
|
-
else if (event.name === "u") handleScopeToggle("user");
|
|
552
|
-
else if (event.name === "p") handleScopeToggle("project");
|
|
553
|
-
else if (event.name === "l") handleScopeToggle("local");
|
|
554
|
-
else if (event.name === "d") handleRemoveDeprecated();
|
|
555
|
-
else if (event.name === "a") handleUpdateAll();
|
|
556
|
-
else if (event.name === "s") handleSaveAsProfile();
|
|
506
|
+
},
|
|
507
|
+
"save-profile": () => {
|
|
508
|
+
void handleSaveAsProfile();
|
|
509
|
+
},
|
|
510
|
+
remove: () => {
|
|
511
|
+
void handleRemoveDeprecated();
|
|
512
|
+
},
|
|
513
|
+
"add-marketplace": () => {
|
|
514
|
+
void handleShowAddMarketplaceInstructions();
|
|
515
|
+
},
|
|
516
|
+
"team-help": () => {
|
|
517
|
+
void handleShowTeamConfigHelp();
|
|
518
|
+
},
|
|
519
|
+
refresh: () => {
|
|
520
|
+
void handleRefresh();
|
|
521
|
+
},
|
|
522
|
+
search: () => dispatch({ type: "SET_SEARCHING", isSearching: true }),
|
|
523
|
+
// Only while a committed filter is set; otherwise Esc is the global back.
|
|
524
|
+
"clear-filter": () => {
|
|
525
|
+
if (!pluginsState.searchQuery) return false;
|
|
526
|
+
setQuery("");
|
|
527
|
+
return true;
|
|
528
|
+
},
|
|
529
|
+
move: (key) => {
|
|
530
|
+
const index = pluginsState.selectedIndex + moveDirection(key);
|
|
531
|
+
dispatch({
|
|
532
|
+
type: "PLUGINS_SELECT",
|
|
533
|
+
index: Math.max(0, Math.min(selectableItems.length - 1, index)),
|
|
534
|
+
});
|
|
535
|
+
},
|
|
557
536
|
});
|
|
558
537
|
|
|
559
538
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
@@ -713,7 +692,7 @@ export function PluginsScreen() {
|
|
|
713
692
|
if (!wantToConfigure) {
|
|
714
693
|
await modal.message(
|
|
715
694
|
"Skipped Configuration",
|
|
716
|
-
|
|
695
|
+
`You can configure these variables later in the Settings tab (press ${tabKey("settings")}).`,
|
|
717
696
|
"info",
|
|
718
697
|
);
|
|
719
698
|
return true;
|
|
@@ -741,7 +720,7 @@ export function PluginsScreen() {
|
|
|
741
720
|
if (value === null) {
|
|
742
721
|
await modal.message(
|
|
743
722
|
"Configuration Incomplete",
|
|
744
|
-
|
|
723
|
+
`Skipped remaining configuration.\nYou can configure these later in the Settings tab (press ${tabKey("settings")}).`,
|
|
745
724
|
"info",
|
|
746
725
|
);
|
|
747
726
|
return true;
|
|
@@ -754,7 +733,18 @@ export function PluginsScreen() {
|
|
|
754
733
|
|
|
755
734
|
return true;
|
|
756
735
|
} catch (error) {
|
|
757
|
-
|
|
736
|
+
// The plugin itself is installed either way. But a settings.local.json
|
|
737
|
+
// that does not parse is something the user has to fix by hand before
|
|
738
|
+
// any variable can be saved, so it is said, not logged out of sight.
|
|
739
|
+
if (error instanceof SettingsParseError) {
|
|
740
|
+
await modal.message(
|
|
741
|
+
"Environment variables not saved",
|
|
742
|
+
error.message,
|
|
743
|
+
"error",
|
|
744
|
+
);
|
|
745
|
+
} else {
|
|
746
|
+
console.error("Error collecting plugin env vars:", error);
|
|
747
|
+
}
|
|
758
748
|
return true;
|
|
759
749
|
}
|
|
760
750
|
};
|
|
@@ -1251,8 +1241,20 @@ export function PluginsScreen() {
|
|
|
1251
1241
|
};
|
|
1252
1242
|
|
|
1253
1243
|
const handleSaveAsProfile = async () => {
|
|
1254
|
-
|
|
1255
|
-
|
|
1244
|
+
// Inside a catch: this runs fire-and-forget from a keypress, so a rejected
|
|
1245
|
+
// read would be an unhandled rejection — which ends the app.
|
|
1246
|
+
let enabledPlugins: Record<string, boolean>;
|
|
1247
|
+
try {
|
|
1248
|
+
enabledPlugins =
|
|
1249
|
+
(await readSettings(state.projectPath)).enabledPlugins ?? {};
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
await modal.message(
|
|
1252
|
+
"Error",
|
|
1253
|
+
`Cannot save a profile: ${error instanceof Error ? error.message : String(error)}`,
|
|
1254
|
+
"error",
|
|
1255
|
+
);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1256
1258
|
|
|
1257
1259
|
const name = await modal.input("Save Profile", "Profile name:");
|
|
1258
1260
|
if (name === null || !name.trim()) return;
|
|
@@ -1281,7 +1283,7 @@ export function PluginsScreen() {
|
|
|
1281
1283
|
modal.hideModal();
|
|
1282
1284
|
await modal.message(
|
|
1283
1285
|
"Saved",
|
|
1284
|
-
`Profile "${name.trim()}" saved.\nPress
|
|
1286
|
+
`Profile "${name.trim()}" saved.\nPress ${tabKey("profiles")} to manage profiles.`,
|
|
1285
1287
|
"success",
|
|
1286
1288
|
);
|
|
1287
1289
|
} catch (error) {
|
|
@@ -1339,19 +1341,13 @@ export function PluginsScreen() {
|
|
|
1339
1341
|
|
|
1340
1342
|
const selectedItem = selectableItems[pluginsState.selectedIndex];
|
|
1341
1343
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
]
|
|
1348
|
-
|
|
1349
|
-
{ keys: ["u", "/", "p", "/", "l"], label: "toggle" },
|
|
1350
|
-
{ keys: ["U"], label: "update" },
|
|
1351
|
-
{ keys: ["a"], label: "all" },
|
|
1352
|
-
{ keys: ["s"], label: "profile" },
|
|
1353
|
-
{ keys: ["/"], label: "search" },
|
|
1354
|
-
];
|
|
1344
|
+
// Advertised only where they act; they stay bound, and help lists them.
|
|
1345
|
+
const hiddenHints: BindingId<"plugins">[] = [
|
|
1346
|
+
...(pluginsState.searchQuery ? [] : ["clear-filter" as const]),
|
|
1347
|
+
...(selectedItem?.kind === "plugin" && selectedItem.plugin.isOrphaned
|
|
1348
|
+
? []
|
|
1349
|
+
: ["remove" as const]),
|
|
1350
|
+
];
|
|
1355
1351
|
|
|
1356
1352
|
const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
|
|
1357
1353
|
const plugins: PluginInfo[] = shownPlugins ?? [];
|
|
@@ -1417,7 +1413,7 @@ export function PluginsScreen() {
|
|
|
1417
1413
|
query: pluginsState.searchQuery,
|
|
1418
1414
|
placeholder: searchPlaceholder,
|
|
1419
1415
|
}}
|
|
1420
|
-
|
|
1416
|
+
footer={{ hide: hiddenHints }}
|
|
1421
1417
|
listPanel={
|
|
1422
1418
|
<box flexDirection="column">
|
|
1423
1419
|
<ScrollableList
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useCallback } from "react";
|
|
2
2
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
4
|
+
import { moveDirection } from "../keymap.js";
|
|
4
5
|
import { useApp, useModal } from "../state/AppContext.js";
|
|
5
6
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
6
7
|
|
|
@@ -8,7 +9,11 @@ import {
|
|
|
8
9
|
PREDEFINED_PROFILES,
|
|
9
10
|
type PredefinedProfile,
|
|
10
11
|
} from "../../data/predefined-profiles.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getSettingsPath,
|
|
14
|
+
readSettings,
|
|
15
|
+
writeSettings,
|
|
16
|
+
} from "../../services/claude-settings.js";
|
|
12
17
|
import {
|
|
13
18
|
applyProfile,
|
|
14
19
|
deleteProfile,
|
|
@@ -17,6 +22,7 @@ import {
|
|
|
17
22
|
listProfiles,
|
|
18
23
|
renameProfile,
|
|
19
24
|
} from "../../services/profiles.js";
|
|
25
|
+
import { writableMap } from "../../services/settings-file.js";
|
|
20
26
|
import {
|
|
21
27
|
ClipboardUnavailableError,
|
|
22
28
|
readClipboard,
|
|
@@ -67,43 +73,38 @@ export function ProfilesScreen() {
|
|
|
67
73
|
const isNavigable = (item: ProfileListItem | undefined) =>
|
|
68
74
|
item !== undefined && item.kind !== "header";
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (event.name === "up" || event.name === "k") {
|
|
74
|
-
let newIndex = profilesState.selectedIndex - 1;
|
|
75
|
-
while (newIndex > 0 && !isNavigable(allItems[newIndex])) {
|
|
76
|
-
newIndex--;
|
|
77
|
-
}
|
|
78
|
-
if (newIndex >= 0 && isNavigable(allItems[newIndex])) {
|
|
79
|
-
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
80
|
-
}
|
|
81
|
-
} else if (event.name === "down" || event.name === "j") {
|
|
82
|
-
let newIndex = profilesState.selectedIndex + 1;
|
|
83
|
-
while (
|
|
84
|
-
newIndex < allItems.length - 1 &&
|
|
85
|
-
!isNavigable(allItems[newIndex])
|
|
86
|
-
) {
|
|
87
|
-
newIndex++;
|
|
88
|
-
}
|
|
89
|
-
if (newIndex < allItems.length && isNavigable(allItems[newIndex])) {
|
|
90
|
-
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
91
|
-
}
|
|
92
|
-
} else if (event.name === "enter" || event.name === "a") {
|
|
76
|
+
useScreenKeys("profiles", {
|
|
77
|
+
apply: () => {
|
|
93
78
|
if (selectedItem?.kind === "predefined") {
|
|
94
79
|
void handleApplyPredefined(selectedItem.profile);
|
|
95
80
|
} else if (selectedItem?.kind === "saved") {
|
|
96
81
|
void handleApply();
|
|
97
82
|
}
|
|
98
|
-
}
|
|
83
|
+
},
|
|
84
|
+
rename: () => {
|
|
99
85
|
if (selectedItem?.kind === "saved") void handleRename();
|
|
100
|
-
}
|
|
86
|
+
},
|
|
87
|
+
delete: () => {
|
|
101
88
|
if (selectedItem?.kind === "saved") void handleDelete();
|
|
102
|
-
}
|
|
89
|
+
},
|
|
90
|
+
copy: () => {
|
|
103
91
|
if (selectedItem?.kind === "saved") void handleCopy();
|
|
104
|
-
}
|
|
92
|
+
},
|
|
93
|
+
import: () => {
|
|
105
94
|
void handleImport();
|
|
106
|
-
}
|
|
95
|
+
},
|
|
96
|
+
// Section headers are labels, not rows: step over them.
|
|
97
|
+
move: (key) => {
|
|
98
|
+
const delta = moveDirection(key);
|
|
99
|
+
let index = profilesState.selectedIndex + delta;
|
|
100
|
+
while (index >= 0 && index < allItems.length) {
|
|
101
|
+
if (isNavigable(allItems[index])) {
|
|
102
|
+
dispatch({ type: "PROFILES_SELECT", index });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
index += delta;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
@@ -124,18 +125,23 @@ export function ProfilesScreen() {
|
|
|
124
125
|
modal.loading(`Applying "${profile.name}"...`);
|
|
125
126
|
try {
|
|
126
127
|
const settings = await readSettings(state.projectPath);
|
|
127
|
-
|
|
128
|
+
const settingsPath = getSettingsPath(state.projectPath);
|
|
129
|
+
// `writableMap` refuses a map that is present with the wrong shape; a
|
|
130
|
+
// bare `?? {}` kept an array, and the entries set on it were never saved.
|
|
131
|
+
const enabledPlugins =
|
|
132
|
+
writableMap(settings, "enabledPlugins", settingsPath) ?? {};
|
|
133
|
+
settings.enabledPlugins = enabledPlugins;
|
|
128
134
|
for (const plugin of allPlugins) {
|
|
129
|
-
if (!
|
|
130
|
-
|
|
135
|
+
if (!enabledPlugins[plugin]) {
|
|
136
|
+
enabledPlugins[plugin] = true;
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
for (const [key, value] of Object.entries(profile.settings)) {
|
|
134
140
|
if (key === "env") {
|
|
135
141
|
const envMap = value as Record<string, string>;
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
142
|
+
// `ClaudeSettings` does not declare `env`; this view types the one key read.
|
|
143
|
+
const envView = settings as { env?: Record<string, string> };
|
|
144
|
+
const existingEnv = writableMap(envView, "env", settingsPath) ?? {};
|
|
139
145
|
for (const [envKey, envVal] of Object.entries(envMap)) {
|
|
140
146
|
if (!existingEnv[envKey]) {
|
|
141
147
|
existingEnv[envKey] = envVal;
|
|
@@ -381,11 +387,11 @@ export function ProfilesScreen() {
|
|
|
381
387
|
|
|
382
388
|
const statusContent = (
|
|
383
389
|
<text fg={theme.colors.text}>
|
|
384
|
-
<span fg={theme.colors.
|
|
390
|
+
<span fg={theme.colors.info}>{PREDEFINED_PROFILES.length} presets</span>
|
|
385
391
|
<span fg={theme.colors.muted}> + </span>
|
|
386
392
|
<span fg={theme.colors.info}>{userCount} user</span>
|
|
387
393
|
<span fg={theme.colors.muted}> + </span>
|
|
388
|
-
<span fg={theme.colors.
|
|
394
|
+
<span fg={theme.colors.info}>{projCount} project</span>
|
|
389
395
|
<span fg={theme.colors.muted}> = </span>
|
|
390
396
|
<span fg={theme.colors.text}>
|
|
391
397
|
{PREDEFINED_PROFILES.length + profileCount} total
|
|
@@ -410,14 +416,11 @@ export function ProfilesScreen() {
|
|
|
410
416
|
title="magus Plugin Profiles"
|
|
411
417
|
currentScreen="profiles"
|
|
412
418
|
statusLine={statusContent}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
{ keys: ["c"], label: "copy" },
|
|
419
|
-
{ keys: ["i"], label: "import" },
|
|
420
|
-
]}
|
|
419
|
+
footer={{
|
|
420
|
+
// rename / delete / copy act on a saved profile only.
|
|
421
|
+
hide:
|
|
422
|
+
selectedItem?.kind === "saved" ? [] : ["rename", "delete", "copy"],
|
|
423
|
+
}}
|
|
421
424
|
listPanel={
|
|
422
425
|
<ScrollableList
|
|
423
426
|
items={allItems}
|