magus-cli 7.3.0 → 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.
Files changed (130) hide show
  1. package/package.json +4 -4
  2. package/scripts/build-binaries.ts +4 -2
  3. package/src/__tests__/blind/c1-npm-package.test.ts +206 -0
  4. package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
  5. package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
  6. package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
  7. package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
  8. package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
  9. package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
  10. package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
  11. package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
  12. package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
  13. package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
  14. package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
  15. package/src/__tests__/blind/c5-keymap.test.ts +461 -0
  16. package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
  17. package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
  18. package/src/__tests__/blind/c8-text-width.test.ts +137 -0
  19. package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
  20. package/src/__tests__/blind/child-run.ts +114 -0
  21. package/src/__tests__/cli-tools-adapter.test.ts +5 -2
  22. package/src/__tests__/component-badge.test.ts +20 -36
  23. package/src/__tests__/footer-hints.test.ts +1 -1
  24. package/src/__tests__/manifest.test.ts +25 -17
  25. package/src/__tests__/marketplace-badge.test.ts +4 -4
  26. package/src/__tests__/models-screen-state.test.ts +36 -13
  27. package/src/__tests__/npm-package-name-api.test.ts +111 -0
  28. package/src/__tests__/npm-package-name.test.ts +280 -0
  29. package/src/__tests__/scope-squares.test.tsx +28 -114
  30. package/src/__tests__/shutdown.test.ts +578 -0
  31. package/src/__tests__/styles-manager.test.ts +4 -4
  32. package/src/__tests__/styles-screen-state.test.ts +26 -12
  33. package/src/__tests__/tabbar-layout.test.ts +44 -32
  34. package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
  35. package/src/__tests__/tool-probe.test.ts +47 -22
  36. package/src/__tests__/upgrade-spawn.test.ts +355 -0
  37. package/src/__tests__/uppercase-keybindings.test.ts +25 -12
  38. package/src/cli/ansi.ts +2 -2
  39. package/src/cli/bootstrap.ts +33 -2
  40. package/src/cli/doctor.ts +105 -14
  41. package/src/cli/install.ts +8 -2
  42. package/src/cli/profile.ts +18 -1
  43. package/src/cli/router.ts +31 -18
  44. package/src/cli/update-view.ts +1 -1
  45. package/src/cli/upgrade.ts +37 -9
  46. package/src/data/cli-tools.ts +13 -2
  47. package/src/data/npm-package.ts +15 -0
  48. package/src/services/claude-settings.ts +151 -73
  49. package/src/services/cli-tool-commands.ts +15 -5
  50. package/src/services/cli-tool-version.ts +101 -0
  51. package/src/services/hook-registration.ts +23 -6
  52. package/src/services/manifest.ts +117 -52
  53. package/src/services/plugin-manager.ts +19 -5
  54. package/src/services/profile-adopt.ts +7 -2
  55. package/src/services/profile-startup.ts +87 -7
  56. package/src/services/profiles.ts +36 -23
  57. package/src/services/settings-file.ts +158 -4
  58. package/src/services/settings-manager.ts +19 -5
  59. package/src/services/skills-manager.ts +46 -0
  60. package/src/services/tool-probe.ts +272 -45
  61. package/src/services/toolchain.ts +39 -0
  62. package/src/services/update-engine.ts +5 -0
  63. package/src/services/update-plan.ts +9 -0
  64. package/src/services/version-check.ts +20 -10
  65. package/src/tui.tsx +48 -21
  66. package/src/ui/App.tsx +58 -197
  67. package/src/ui/adapters/cliToolsAdapter.ts +141 -12
  68. package/src/ui/adapters/pluginsAdapter.ts +10 -7
  69. package/src/ui/adapters/settingsAdapter.ts +0 -21
  70. package/src/ui/adapters/skillsAdapter.ts +27 -8
  71. package/src/ui/adapters/stylesAdapter.ts +15 -11
  72. package/src/ui/components/CategoryHeader.tsx +7 -1
  73. package/src/ui/components/EmptyFilterState.tsx +2 -2
  74. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  75. package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
  76. package/src/ui/components/TabBar.tsx +93 -21
  77. package/src/ui/components/layout/FooterHints.tsx +6 -27
  78. package/src/ui/components/layout/ScreenLayout.tsx +68 -42
  79. package/src/ui/components/layout/index.ts +1 -5
  80. package/src/ui/components/modals/ConfirmModal.tsx +2 -19
  81. package/src/ui/components/modals/HelpOverlay.tsx +186 -0
  82. package/src/ui/components/modals/MessageModal.tsx +3 -15
  83. package/src/ui/components/modals/ModalContainer.tsx +99 -60
  84. package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
  85. package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
  86. package/src/ui/components/primitives/MetaText.tsx +1 -1
  87. package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
  88. package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
  89. package/src/ui/components/primitives/SelectableRow.tsx +24 -1
  90. package/src/ui/components/primitives/index.ts +4 -3
  91. package/src/ui/hooks/useProfileStartupModal.ts +15 -3
  92. package/src/ui/input/KeyRouter.tsx +180 -0
  93. package/src/ui/input/key-dispatch.ts +149 -0
  94. package/src/ui/keymap.ts +898 -0
  95. package/src/ui/renderers/cliToolRenderers.tsx +128 -66
  96. package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
  97. package/src/ui/renderers/markdownRenderer.tsx +2 -2
  98. package/src/ui/renderers/mcpRenderers.tsx +28 -14
  99. package/src/ui/renderers/modelRenderers.tsx +12 -13
  100. package/src/ui/renderers/modelVisuals.tsx +0 -2
  101. package/src/ui/renderers/pluginRenderers.tsx +47 -54
  102. package/src/ui/renderers/profileRenderers.tsx +32 -37
  103. package/src/ui/renderers/settingsRenderers.tsx +84 -55
  104. package/src/ui/renderers/skillRenderers.tsx +237 -190
  105. package/src/ui/renderers/styleRenderers.tsx +13 -2
  106. package/src/ui/runtime/shutdown.ts +268 -0
  107. package/src/ui/screens/AliasScreen.tsx +118 -100
  108. package/src/ui/screens/CliToolsScreen.tsx +98 -87
  109. package/src/ui/screens/EnvVarsScreen.tsx +44 -42
  110. package/src/ui/screens/GitignoreScreen.tsx +50 -55
  111. package/src/ui/screens/McpRegistryScreen.tsx +52 -86
  112. package/src/ui/screens/McpScreen.tsx +47 -33
  113. package/src/ui/screens/ModelsScreen.tsx +59 -104
  114. package/src/ui/screens/PluginsScreen.tsx +123 -127
  115. package/src/ui/screens/ProfilesScreen.tsx +49 -46
  116. package/src/ui/screens/SkillsScreen.tsx +184 -204
  117. package/src/ui/screens/StylesScreen.tsx +104 -157
  118. package/src/ui/state/AppContext.tsx +6 -0
  119. package/src/ui/state/DimensionsContext.tsx +1 -10
  120. package/src/ui/state/reducer.ts +5 -8
  121. package/src/ui/state/types.ts +11 -9
  122. package/src/ui/text-width.ts +341 -0
  123. package/src/ui/theme-mode.ts +34 -71
  124. package/src/ui/theme.ts +50 -28
  125. package/src/utils/request-gate.ts +37 -0
  126. package/src/ui/components/ScopeIndicator.tsx +0 -63
  127. package/src/ui/components/SearchInput.tsx +0 -57
  128. package/src/ui/hooks/index.ts +0 -2
  129. package/src/ui/hooks/useKeyboard.ts +0 -24
  130. package/src/ui/hooks/useKeyboardHandler.ts +0 -63
@@ -5,30 +5,37 @@ import type {
5
5
  SettingsSettingItem,
6
6
  } from "../adapters/settingsAdapter.js";
7
7
  import {
8
- CATEGORY_BG,
9
- CATEGORY_COLOR,
10
8
  CATEGORY_DESCRIPTIONS,
11
9
  CATEGORY_LABELS,
12
10
  formatValue,
13
11
  } from "../adapters/settingsAdapter.js";
14
- import { MetaText, SelectableRow } from "../components/primitives/index.js";
12
+ import {
13
+ ListCategoryRow,
14
+ ScopeDot,
15
+ ScopeKeyChip,
16
+ rowLead,
17
+ } from "../components/primitives/index.js";
15
18
  import type { ItemRenderer } from "../registry.js";
19
+ import { displayWidth, padTo } from "../text-width.js";
16
20
  import { theme } from "../theme.js";
17
21
 
18
22
  // ─── Category renderer ─────────────────────────────────────────────────────────
19
23
 
24
+ /**
25
+ * A category is a group, so every header wears the one neutral header style.
26
+ * They used to wear four unrelated hues — green, teal, purple, blue, amber — none
27
+ * of which meant anything, and two of which (amber, green) meant "warning" and
28
+ * "on" everywhere else. "Recommended" keeps its ★, which is the distinction.
29
+ */
20
30
  const categoryRenderer: ItemRenderer<SettingsCategoryItem> = {
21
31
  renderRow: ({ item, isSelected }) => {
22
32
  const star = item.category === "recommended" ? "★ " : "";
23
- const label = `${star}${CATEGORY_LABELS[item.category]}`;
24
- const bg = isSelected ? theme.selection.bg : CATEGORY_BG[item.category];
25
-
26
33
  return (
27
- <box width="100%">
28
- <text bg={bg} fg={theme.hints.fg}>
29
- <strong> {label} </strong>
30
- </text>
31
- </box>
34
+ <ListCategoryRow
35
+ selected={isSelected}
36
+ title={`${star}${CATEGORY_LABELS[item.category]}`}
37
+ expandable={false}
38
+ />
32
39
  );
33
40
  },
34
41
 
@@ -36,7 +43,7 @@ const categoryRenderer: ItemRenderer<SettingsCategoryItem> = {
36
43
  const cat = item.category;
37
44
  return (
38
45
  <box flexDirection="column">
39
- <text fg={CATEGORY_COLOR[cat]}>
46
+ <text fg={theme.colors.info}>
40
47
  <strong>{CATEGORY_LABELS[cat]}</strong>
41
48
  </text>
42
49
  <box marginTop={1}>
@@ -49,34 +56,70 @@ const categoryRenderer: ItemRenderer<SettingsCategoryItem> = {
49
56
 
50
57
  // ─── Setting renderer ──────────────────────────────────────────────────────────
51
58
 
59
+ /** The lead cell, the ●/○ indicator and the space after it. */
60
+ const ROW_PREFIX = 3;
61
+ /** Columns always kept between a label and its value. */
62
+ const LABEL_GAP = 1;
63
+ /** Width assumed when a caller does not say — the old fixed label column. */
64
+ const DEFAULT_ROW_WIDTH = 45;
65
+
66
+ /**
67
+ * One setting row: `▌● Label……………… value`, fitted to `width` in cells.
68
+ *
69
+ * The label used to be `name.padEnd(28)`, which only pads: a name of 28 or more
70
+ * characters ran straight into its value — "Auto-approve Project MCP Serversoff",
71
+ * "AI Attribution in Commits & PRson". Now the value is measured first and keeps
72
+ * its whole width, the label gets what is left less a one-cell gap, and a label
73
+ * that does not fit is cut with `…`. Everything is counted in terminal cells, so
74
+ * a wide glyph in a value cannot push it into the divider.
75
+ */
76
+ function settingRowParts(
77
+ name: string,
78
+ value: string,
79
+ width: number,
80
+ ): { label: string; value: string } {
81
+ const valueWidth = displayWidth(value);
82
+ const labelWidth = Math.max(
83
+ 1,
84
+ width - ROW_PREFIX - LABEL_GAP - valueWidth - 1,
85
+ );
86
+ return { label: padTo(name, labelWidth), value };
87
+ }
88
+
52
89
  const settingRenderer: ItemRenderer<SettingsSettingItem> = {
53
- renderRow: ({ item, isSelected }) => {
90
+ renderRow: ({ item, isSelected, width = DEFAULT_ROW_WIDTH }) => {
54
91
  const { setting } = item;
55
92
  const indicator = item.isDefault ? "○" : "●";
56
- const displayValue = formatValue(setting, item.effectiveValue);
93
+ const parts = settingRowParts(
94
+ setting.name,
95
+ formatValue(setting, item.effectiveValue),
96
+ width,
97
+ );
98
+ const gap = " ".repeat(LABEL_GAP);
57
99
 
58
100
  if (isSelected) {
59
101
  return (
60
102
  <text bg={theme.selection.bg} fg={theme.selection.fg}>
61
- {" "}
62
- {indicator} {setting.name.padEnd(28)}
63
- {displayValue}{" "}
103
+ {rowLead(true)}
104
+ {indicator} {parts.label}
105
+ {gap}
106
+ {parts.value}{" "}
64
107
  </text>
65
108
  );
66
109
  }
67
110
 
68
- const indicatorColor = item.isDefault
69
- ? theme.colors.muted
70
- : theme.colors.info;
71
- const valueColor = item.isDefault
72
- ? theme.colors.muted
73
- : theme.colors.success;
111
+ // Set vs default is one fact, said once: the dot, and the value's ink.
112
+ const tone = item.isDefault ? theme.colors.muted : theme.colors.success;
74
113
 
75
114
  return (
76
115
  <text fg={theme.colors.text}>
77
- <span fg={indicatorColor}> {indicator} </span>
78
- <span>{setting.name.padEnd(28)}</span>
79
- <span fg={valueColor}>{displayValue}</span>
116
+ <span fg={tone}>
117
+ {rowLead(false)}
118
+ {indicator}{" "}
119
+ </span>
120
+ <span>{parts.label}</span>
121
+ <span>{gap}</span>
122
+ <span fg={tone}>{parts.value}</span>
80
123
  </text>
81
124
  );
82
125
  },
@@ -117,7 +160,7 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
117
160
  <box marginTop={1}>
118
161
  <text fg={theme.colors.text}>
119
162
  <span fg={theme.colors.muted}>Stored </span>
120
- <span fg={theme.colors.link}>{storageDesc}</span>
163
+ <span fg={theme.colors.info}>{storageDesc}</span>
121
164
  </text>
122
165
  </box>
123
166
  {setting.defaultValue !== undefined && (
@@ -136,34 +179,22 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
136
179
  </text>
137
180
  <box marginTop={1} flexDirection="column">
138
181
  <text fg={theme.colors.text}>
139
- <span bg={theme.scopes.user} fg={theme.hints.fg}>
140
- {" "}
141
- u{" "}
142
- </span>
143
- <span fg={userIsSet ? theme.scopes.user : theme.colors.muted}>
144
- {userIsSet ? " ● " : " ○ "}
145
- </span>
146
- <span fg={theme.scopes.user}>User</span>
147
- <span> global</span>
148
- <span fg={userIsSet ? theme.scopes.user : theme.colors.muted}>
182
+ <ScopeKeyChip scopeKey="u" />
183
+ <ScopeDot installed={userIsSet} />
184
+ <span>User</span>
185
+ <span fg={theme.colors.muted}> global</span>
186
+ <span fg={userIsSet ? theme.colors.success : theme.colors.muted}>
149
187
  {" "}
150
188
  {userValue}
151
189
  </span>
152
190
  </text>
153
191
  <text fg={theme.colors.text}>
154
- <span bg={theme.scopes.project} fg={theme.hints.fg}>
155
- {" "}
156
- p{" "}
157
- </span>
192
+ <ScopeKeyChip scopeKey="p" />
193
+ <ScopeDot installed={projectIsSet} />
194
+ <span>Project</span>
195
+ <span fg={theme.colors.muted}> team</span>
158
196
  <span
159
- fg={projectIsSet ? theme.scopes.project : theme.colors.muted}
160
- >
161
- {projectIsSet ? " ● " : " ○ "}
162
- </span>
163
- <span fg={theme.scopes.project}>Project</span>
164
- <span> team</span>
165
- <span
166
- fg={projectIsSet ? theme.scopes.project : theme.colors.muted}
197
+ fg={projectIsSet ? theme.colors.success : theme.colors.muted}
167
198
  >
168
199
  {" "}
169
200
  {projectValue}
@@ -192,15 +223,16 @@ export const settingsRenderers: {
192
223
  setting: settingRenderer,
193
224
  };
194
225
 
226
+ /** A list row, fitted to `width` — the list panel's usable columns. */
195
227
  export function renderSettingRow(
196
228
  item: SettingsBrowserItem,
197
- _index: number,
198
229
  isSelected: boolean,
230
+ width: number,
199
231
  ): React.ReactNode {
200
232
  if (item.kind === "category") {
201
- return settingsRenderers.category.renderRow({ item, isSelected });
233
+ return settingsRenderers.category.renderRow({ item, isSelected, width });
202
234
  }
203
- return settingsRenderers.setting.renderRow({ item, isSelected });
235
+ return settingsRenderers.setting.renderRow({ item, isSelected, width });
204
236
  }
205
237
 
206
238
  export function renderSettingDetail(
@@ -213,6 +245,3 @@ export function renderSettingDetail(
213
245
  }
214
246
  return settingsRenderers.setting.renderDetail({ item });
215
247
  }
216
-
217
- // Suppress unused import warning — MetaText may be used in future extensions
218
- void (MetaText as unknown);