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.
- package/package.json +4 -4
- package/scripts/build-binaries.ts +4 -2
- 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__/npm-package-name-api.test.ts +111 -0
- package/src/__tests__/npm-package-name.test.ts +280 -0
- 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__/upgrade-spawn.test.ts +355 -0
- 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/cli/upgrade.ts +37 -9
- package/src/data/cli-tools.ts +13 -2
- package/src/data/npm-package.ts +15 -0
- 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/services/version-check.ts +20 -10
- 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
|
@@ -6,16 +6,46 @@ import {
|
|
|
6
6
|
} from "../../services/cli-tool-commands.js";
|
|
7
7
|
import { formatCommand } from "../../utils/command-utils.js";
|
|
8
8
|
import {
|
|
9
|
+
type CliToolEnterAction,
|
|
9
10
|
type CliToolRow,
|
|
11
|
+
cliToolEnterAction,
|
|
10
12
|
cliToolStatusText,
|
|
11
13
|
displayVersion,
|
|
12
14
|
isInstalled,
|
|
13
15
|
} from "../adapters/cliToolsAdapter.js";
|
|
16
|
+
import { rowLead } from "../components/primitives/index.js";
|
|
14
17
|
import { type UiColor, theme } from "../theme.js";
|
|
15
18
|
import type { StatusTone } from "./pluginStatusText.js";
|
|
19
|
+
import { wrapText } from "./styleRenderers.js";
|
|
16
20
|
|
|
17
21
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
18
22
|
|
|
23
|
+
/** The Enter chip's background, by what Enter does. */
|
|
24
|
+
const ENTER_CHIP_BG: Record<CliToolEnterAction["kind"], UiColor> = {
|
|
25
|
+
install: theme.colors.success,
|
|
26
|
+
update: theme.colors.warning,
|
|
27
|
+
"update-anyway": theme.colors.muted,
|
|
28
|
+
reinstall: theme.colors.muted,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** "Status ", "Update " …: every detail label is this many columns. */
|
|
32
|
+
const LABEL_COLS = 9;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A detail value broken at SPACES, to the columns beside the label.
|
|
36
|
+
*
|
|
37
|
+
* Left to itself OpenTUI breaks a long line at the column edge, mid-token, and
|
|
38
|
+
* these values are commands and notes carrying paths. MEASURED at 150 columns
|
|
39
|
+
* (a 63-cell value column): `npm install -g --prefix /Users/…/node/v24.14.0
|
|
40
|
+
* @charmland/crush@latest` broke after `v24.14.`, with `0 @charmland/…` below
|
|
41
|
+
* — which reads as two paths, neither real. Broken here, every token that fits
|
|
42
|
+
* the column stays whole. One longer than the column (that same path at 100
|
|
43
|
+
* columns, where the value column is 38 cells) still has to break.
|
|
44
|
+
*/
|
|
45
|
+
function wrapValue(text: string, width: number): string {
|
|
46
|
+
return wrapText(text, width - LABEL_COLS).join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
/**
|
|
20
50
|
* The uninstall command as a line the reader can copy.
|
|
21
51
|
*
|
|
@@ -93,7 +123,7 @@ export function renderCliToolRow(
|
|
|
93
123
|
if (isSelected) {
|
|
94
124
|
return (
|
|
95
125
|
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
96
|
-
{
|
|
126
|
+
{rowLead(true)}
|
|
97
127
|
{icon} {tool.displayName}
|
|
98
128
|
{versionText}
|
|
99
129
|
{methodTag}
|
|
@@ -104,7 +134,10 @@ export function renderCliToolRow(
|
|
|
104
134
|
|
|
105
135
|
return (
|
|
106
136
|
<text fg={theme.colors.text}>
|
|
107
|
-
<span fg={iconColor}>
|
|
137
|
+
<span fg={iconColor}>
|
|
138
|
+
{rowLead(false)}
|
|
139
|
+
{icon}
|
|
140
|
+
</span>
|
|
108
141
|
<span fg={theme.colors.text}> {tool.displayName}</span>
|
|
109
142
|
{versionText ? (
|
|
110
143
|
<span fg={theme.colors.success}>{versionText}</span>
|
|
@@ -123,6 +156,8 @@ export function renderCliToolRow(
|
|
|
123
156
|
|
|
124
157
|
export function renderCliToolDetail(
|
|
125
158
|
row: CliToolRow | undefined,
|
|
159
|
+
/** Usable detail-panel columns (`detailPanelWidth`); the scrollbox does not constrain width. */
|
|
160
|
+
width: number,
|
|
126
161
|
): React.ReactNode {
|
|
127
162
|
if (!row) {
|
|
128
163
|
return (
|
|
@@ -144,15 +179,13 @@ export function renderCliToolDetail(
|
|
|
144
179
|
const brewFormula = install?.brewFormula;
|
|
145
180
|
const hasConflict = allMethods.length > 1;
|
|
146
181
|
const state = status?.state;
|
|
182
|
+
const enter = cliToolEnterAction(row);
|
|
147
183
|
|
|
148
184
|
return (
|
|
149
|
-
<box flexDirection="column">
|
|
150
|
-
<box marginBottom={1}>
|
|
185
|
+
<box flexDirection="column" width={width}>
|
|
186
|
+
<box flexDirection="row" marginBottom={1}>
|
|
151
187
|
<text fg={theme.colors.info}>
|
|
152
|
-
<strong>
|
|
153
|
-
{"⚙ "}
|
|
154
|
-
{tool.displayName}
|
|
155
|
-
</strong>
|
|
188
|
+
<strong>{tool.displayName}</strong>
|
|
156
189
|
</text>
|
|
157
190
|
{state === "upgrade" ? <text fg={theme.colors.warning}> ⬆</text> : null}
|
|
158
191
|
{hasConflict ? <text fg={theme.colors.danger}> !</text> : null}
|
|
@@ -161,36 +194,58 @@ export function renderCliToolDetail(
|
|
|
161
194
|
<text fg={theme.colors.muted}>{tool.description}</text>
|
|
162
195
|
|
|
163
196
|
<box marginTop={1} flexDirection="column">
|
|
164
|
-
<box>
|
|
165
|
-
<text fg={theme.colors.muted}
|
|
197
|
+
<box flexDirection="row">
|
|
198
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
199
|
+
{"Status "}
|
|
200
|
+
</text>
|
|
166
201
|
{checking || !status ? (
|
|
167
|
-
<text fg={theme.colors.muted}
|
|
202
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
203
|
+
{"● Checking..."}
|
|
204
|
+
</text>
|
|
168
205
|
) : state === "install" ? (
|
|
169
|
-
<text fg={theme.colors.muted}
|
|
206
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
207
|
+
{"○ Not installed"}
|
|
208
|
+
</text>
|
|
170
209
|
) : state === "upgrade" ? (
|
|
171
210
|
<text fg={theme.colors.warning}>{"● Update available"}</text>
|
|
172
211
|
) : state === "unknown" ? (
|
|
173
|
-
|
|
212
|
+
// A version that was read but cannot be checked for updates is
|
|
213
|
+
// still a version: say "installed", and let Why say the rest.
|
|
214
|
+
<text fg={theme.colors.warning}>
|
|
215
|
+
{status.installed
|
|
216
|
+
? "? Installed — not checked"
|
|
217
|
+
: "? Version unreadable"}
|
|
218
|
+
</text>
|
|
174
219
|
) : (
|
|
175
220
|
<text fg={theme.colors.success}>{"● Up to date"}</text>
|
|
176
221
|
)}
|
|
177
222
|
</box>
|
|
178
223
|
{state === "unknown" && status?.note ? (
|
|
179
|
-
<box>
|
|
180
|
-
<text fg={theme.colors.muted}
|
|
181
|
-
|
|
224
|
+
<box flexDirection="row">
|
|
225
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
226
|
+
{"Why "}
|
|
227
|
+
</text>
|
|
228
|
+
<text fg={theme.colors.warning} minWidth={0}>
|
|
229
|
+
{wrapValue(status.note, width)}
|
|
230
|
+
</text>
|
|
182
231
|
</box>
|
|
183
232
|
) : null}
|
|
184
233
|
{/* An upgrade that also takes the tool over from another installer. */}
|
|
185
234
|
{status?.adopted ? (
|
|
186
|
-
<box>
|
|
187
|
-
<text fg={theme.colors.muted}
|
|
188
|
-
|
|
235
|
+
<box flexDirection="row">
|
|
236
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
237
|
+
{"Note "}
|
|
238
|
+
</text>
|
|
239
|
+
<text fg={theme.colors.warning} minWidth={0}>
|
|
240
|
+
{status.adopted}
|
|
241
|
+
</text>
|
|
189
242
|
</box>
|
|
190
243
|
) : null}
|
|
191
244
|
{status?.installed ? (
|
|
192
|
-
<box>
|
|
193
|
-
<text fg={theme.colors.muted}
|
|
245
|
+
<box flexDirection="row">
|
|
246
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
247
|
+
{"Version "}
|
|
248
|
+
</text>
|
|
194
249
|
<text fg={theme.colors.text}>
|
|
195
250
|
<span fg={theme.colors.success}>
|
|
196
251
|
{displayVersion(status.installed)}
|
|
@@ -204,33 +259,51 @@ export function renderCliToolDetail(
|
|
|
204
259
|
</text>
|
|
205
260
|
</box>
|
|
206
261
|
) : status?.latest ? (
|
|
207
|
-
<box>
|
|
208
|
-
<text fg={theme.colors.muted}
|
|
262
|
+
<box flexDirection="row">
|
|
263
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
264
|
+
{"Latest "}
|
|
265
|
+
</text>
|
|
209
266
|
<text fg={theme.colors.text}>{displayVersion(status.latest)}</text>
|
|
210
267
|
</box>
|
|
211
268
|
) : null}
|
|
212
269
|
{status?.pin ? (
|
|
213
|
-
<box>
|
|
214
|
-
<text fg={theme.colors.muted}
|
|
270
|
+
<box flexDirection="row">
|
|
271
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
272
|
+
{"Pinned "}
|
|
273
|
+
</text>
|
|
215
274
|
<text fg={theme.colors.text}>{displayVersion(status.pin)}</text>
|
|
216
275
|
</box>
|
|
217
276
|
) : null}
|
|
218
277
|
{installed && updateCommand ? (
|
|
219
|
-
<box>
|
|
220
|
-
<text fg={theme.colors.muted}
|
|
221
|
-
|
|
278
|
+
<box flexDirection="row">
|
|
279
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
280
|
+
{"Update "}
|
|
281
|
+
</text>
|
|
282
|
+
<text fg={theme.colors.info} minWidth={0}>
|
|
283
|
+
{wrapValue(formatCommand(updateCommand), width)}
|
|
284
|
+
</text>
|
|
222
285
|
</box>
|
|
223
286
|
) : !installed && !checking ? (
|
|
224
|
-
<box>
|
|
225
|
-
<text fg={theme.colors.muted}
|
|
226
|
-
|
|
227
|
-
|
|
287
|
+
<box flexDirection="row">
|
|
288
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
289
|
+
{"Install "}
|
|
290
|
+
</text>
|
|
291
|
+
<text fg={theme.colors.info} minWidth={0}>
|
|
292
|
+
{wrapValue(formatCommand(installCommand), width)}
|
|
228
293
|
</text>
|
|
229
294
|
</box>
|
|
230
295
|
) : null}
|
|
231
|
-
<box>
|
|
232
|
-
<text fg={theme.colors.muted}
|
|
233
|
-
|
|
296
|
+
<box flexDirection="row">
|
|
297
|
+
<text fg={theme.colors.muted} flexShrink={0}>
|
|
298
|
+
{"Website "}
|
|
299
|
+
</text>
|
|
300
|
+
{/* A URL has no spaces, so word wrap cannot break it: it breaks anywhere,
|
|
301
|
+
inside the width the label leaves. */}
|
|
302
|
+
<box flexGrow={1} minWidth={0}>
|
|
303
|
+
<text fg={theme.colors.info} wrapMode="char">
|
|
304
|
+
{tool.website}
|
|
305
|
+
</text>
|
|
306
|
+
</box>
|
|
234
307
|
</box>
|
|
235
308
|
</box>
|
|
236
309
|
|
|
@@ -268,7 +341,7 @@ export function renderCliToolDetail(
|
|
|
268
341
|
</text>
|
|
269
342
|
</box>
|
|
270
343
|
))}
|
|
271
|
-
<box marginTop={1}>
|
|
344
|
+
<box flexDirection="row" marginTop={1}>
|
|
272
345
|
<text bg={theme.colors.danger} fg={theme.hints.fg}>
|
|
273
346
|
{" "}
|
|
274
347
|
c{" "}
|
|
@@ -279,47 +352,36 @@ export function renderCliToolDetail(
|
|
|
279
352
|
) : null}
|
|
280
353
|
|
|
281
354
|
{/* Actions */}
|
|
355
|
+
{/* The words come from `cliToolEnterAction`, which the footer hint and
|
|
356
|
+
the Enter handler read too — so all three name the same action. */}
|
|
282
357
|
<box marginTop={2} flexDirection="column">
|
|
283
|
-
{
|
|
284
|
-
<box>
|
|
285
|
-
<text bg={theme.colors.success} fg={theme.hints.fg}>
|
|
286
|
-
{" "}
|
|
287
|
-
Enter{" "}
|
|
288
|
-
</text>
|
|
289
|
-
<text fg={theme.colors.muted}> Install</text>
|
|
290
|
-
<text fg={theme.colors.dim}> {formatCommand(installCommand)}</text>
|
|
291
|
-
</box>
|
|
292
|
-
) : state === "upgrade" ? (
|
|
293
|
-
<box>
|
|
294
|
-
<text bg={theme.colors.warning} fg={theme.hints.fg}>
|
|
295
|
-
{" "}
|
|
296
|
-
Enter{" "}
|
|
297
|
-
</text>
|
|
298
|
-
<text fg={theme.colors.muted}>
|
|
299
|
-
{" "}
|
|
300
|
-
Update
|
|
301
|
-
{status.target ? ` to ${displayVersion(status.target)}` : ""}
|
|
302
|
-
</text>
|
|
303
|
-
</box>
|
|
304
|
-
) : (
|
|
358
|
+
{enter ? (
|
|
305
359
|
<box flexDirection="column">
|
|
306
|
-
<box>
|
|
307
|
-
<text bg={
|
|
360
|
+
<box flexDirection="row">
|
|
361
|
+
<text bg={ENTER_CHIP_BG[enter.kind]} fg={theme.hints.fg}>
|
|
308
362
|
{" "}
|
|
309
363
|
Enter{" "}
|
|
310
364
|
</text>
|
|
311
|
-
<text fg={theme.colors.muted}>
|
|
312
|
-
|
|
313
|
-
|
|
365
|
+
<text fg={theme.colors.muted}> {enter.detail}</text>
|
|
366
|
+
{enter.kind === "install" ? (
|
|
367
|
+
<text fg={theme.colors.dim}>
|
|
368
|
+
{" "}
|
|
369
|
+
{formatCommand(enter.command)}
|
|
370
|
+
</text>
|
|
371
|
+
) : null}
|
|
314
372
|
</box>
|
|
315
373
|
{/* The status warns that this command may not replace the
|
|
316
374
|
running binary; the menu still offers it, as an explicit
|
|
317
375
|
user action, and says so beside it. */}
|
|
318
|
-
{
|
|
319
|
-
|
|
376
|
+
{enter.kind === "update-anyway" &&
|
|
377
|
+
status?.reason === "not-managed" &&
|
|
378
|
+
status.note ? (
|
|
379
|
+
<text fg={theme.colors.warning}>
|
|
380
|
+
{wrapText(` ${status.note}`, width).join("\n")}
|
|
381
|
+
</text>
|
|
320
382
|
) : null}
|
|
321
383
|
</box>
|
|
322
|
-
)}
|
|
384
|
+
) : null}
|
|
323
385
|
</box>
|
|
324
386
|
</box>
|
|
325
387
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { ResolvedRule, Violation } from "../../types/gitignore.js";
|
|
3
|
+
import { rowLead } from "../components/primitives/SelectableRow.js";
|
|
4
|
+
import { truncate } from "../text-width.js";
|
|
3
5
|
import { theme } from "../theme.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -11,69 +13,65 @@ export interface RuleRow {
|
|
|
11
13
|
violations: Violation[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
// One colour channel, and it means one thing: something is wrong.
|
|
17
|
+
//
|
|
18
|
+
// • The action word ("ignore" / "track") is what the rule INTENDS. An intended
|
|
19
|
+
// state is not a problem, so it is neutral on every row. It used to be red
|
|
20
|
+
// for "ignore" and green for "track", which put the danger hue on every
|
|
21
|
+
// ignore rule in the list — half the screen read as broken while nothing was.
|
|
22
|
+
// • A violation is what is wrong, so its path wears `danger`, and its action
|
|
23
|
+
// sits on a neutral chip so the row reads as "this needs fixing" at a glance
|
|
24
|
+
// without the chip's colour claiming anything the word does not.
|
|
25
|
+
// • A clean row stays quiet: action and path in `muted`/`text`.
|
|
26
|
+
const INTENT_FG = theme.colors.muted;
|
|
27
|
+
const CLEAN_PATH_FG = theme.colors.text;
|
|
28
|
+
const VIOLATION_PATH_FG = theme.colors.danger;
|
|
29
|
+
const CHIP_BG = theme.colors.dim;
|
|
30
|
+
const CHIP_FG = theme.hints.fg;
|
|
24
31
|
|
|
25
|
-
//
|
|
26
|
-
// green chip = will be tracked, red chip = will be ignored/dropped.
|
|
27
|
-
const TRACK_CHIP_BG = theme.colors.success;
|
|
28
|
-
const IGNORE_CHIP_BG = theme.colors.danger;
|
|
29
|
-
const CHIP_FG = theme.colors.text;
|
|
30
|
-
|
|
31
|
-
// The only marker is the cursor: ▶ on the selected row, blank otherwise.
|
|
32
|
-
// Action (track/ignore) is read from the badge word and, for clean items,
|
|
33
|
-
// from the group the row sits under — so no per-row dot is needed.
|
|
34
|
-
const MARKER_SELECTED = "▶";
|
|
35
|
-
const MARKER_NONE = " ";
|
|
36
|
-
|
|
37
|
-
// Layout: " M action path" — keep the prefix compact so the narrow terminal
|
|
32
|
+
// Layout: "▌ action path" — keep the prefix compact so the narrow terminal
|
|
38
33
|
// case still has room for the path without wrapping.
|
|
39
34
|
const MAX_PATH_LEN = 22;
|
|
40
35
|
|
|
41
|
-
function truncatePath(p: string): string {
|
|
42
|
-
return p.length > MAX_PATH_LEN ? `${p.slice(0, MAX_PATH_LEN - 1)}…` : p;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
36
|
export function renderRuleRow(
|
|
46
37
|
row: RuleRow,
|
|
47
38
|
isSelected: boolean,
|
|
48
39
|
): React.ReactNode {
|
|
49
40
|
const { rule, violations } = row;
|
|
50
|
-
const path =
|
|
41
|
+
const path = truncate(rule.pattern, MAX_PATH_LEN);
|
|
51
42
|
const key = `${rule.action}:${rule.pattern}`;
|
|
52
|
-
// Badge text padded to a fixed width
|
|
43
|
+
// Badge text padded to a fixed width, and drawn in the same padded cell on
|
|
44
|
+
// every row — chip or not — so paths line up in one column.
|
|
53
45
|
const badge = rule.action === "ignore" ? "ignore" : "track ";
|
|
54
|
-
const badgeFg = rule.action === "ignore" ? IGNORE_FG : TRACK_FG;
|
|
55
46
|
const suffix = violations.length > 1 ? ` +${violations.length - 1}` : "";
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
const lead = `${rowLead(isSelected)} `;
|
|
48
|
+
|
|
49
|
+
// The selection owns the colour channel, as on every other screen: the mark
|
|
50
|
+
// and the fill say where the cursor is, the words say the rest.
|
|
51
|
+
if (isSelected) {
|
|
52
|
+
return (
|
|
53
|
+
<text key={key} bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
54
|
+
{lead}
|
|
55
|
+
{` ${badge} `}
|
|
56
|
+
{` ${path}${suffix} `}
|
|
57
|
+
</text>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
59
60
|
|
|
60
|
-
if (
|
|
61
|
-
// Solid badge chip + red filename. The chip color is the fix direction.
|
|
62
|
-
const chipBg = rule.action === "ignore" ? IGNORE_CHIP_BG : TRACK_CHIP_BG;
|
|
61
|
+
if (violations.length > 0) {
|
|
63
62
|
return (
|
|
64
|
-
<text key={key}
|
|
65
|
-
<span fg={VIOLATION_PATH_FG}>{
|
|
66
|
-
<span bg={
|
|
63
|
+
<text key={key} fg={theme.colors.text}>
|
|
64
|
+
<span fg={VIOLATION_PATH_FG}>{lead}</span>
|
|
65
|
+
<span bg={CHIP_BG} fg={CHIP_FG}>{` ${badge} `}</span>
|
|
67
66
|
<span fg={VIOLATION_PATH_FG}>{` ${path}${suffix}`}</span>
|
|
68
67
|
</text>
|
|
69
68
|
);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
// Clean row: flat colored badge word, dim path, no chip.
|
|
73
71
|
return (
|
|
74
|
-
<text key={key}
|
|
75
|
-
<span fg={
|
|
76
|
-
<span fg={
|
|
72
|
+
<text key={key} fg={theme.colors.text}>
|
|
73
|
+
<span fg={INTENT_FG}>{lead}</span>
|
|
74
|
+
<span fg={INTENT_FG}>{` ${badge} `}</span>
|
|
77
75
|
<span fg={CLEAN_PATH_FG}>{` ${path}`}</span>
|
|
78
76
|
</text>
|
|
79
77
|
);
|
|
@@ -55,7 +55,7 @@ function inlineSpans(text: string, keyPrefix: string): React.ReactNode[] {
|
|
|
55
55
|
}
|
|
56
56
|
if (match[1]) {
|
|
57
57
|
nodes.push(
|
|
58
|
-
<span key={`${keyPrefix}-c${i}`} fg={theme.colors.
|
|
58
|
+
<span key={`${keyPrefix}-c${i}`} fg={theme.colors.info}>
|
|
59
59
|
{match[1].slice(1, -1)}
|
|
60
60
|
</span>,
|
|
61
61
|
);
|
|
@@ -212,7 +212,7 @@ export function renderMarkdown(
|
|
|
212
212
|
while (i < lines.length && !/^\s*```/.test(lines[i])) {
|
|
213
213
|
const code = lines[i];
|
|
214
214
|
push(
|
|
215
|
-
<text key={`fc${i}`} fg={theme.colors.
|
|
215
|
+
<text key={`fc${i}`} fg={theme.colors.info}>
|
|
216
216
|
{` ${code}` || " "}
|
|
217
217
|
</text>,
|
|
218
218
|
);
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import type { McpServer } from "../../types/index.js";
|
|
3
|
+
import { rowLead } from "../components/primitives/index.js";
|
|
3
4
|
import { theme } from "../theme.js";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The MCP-server glyph, shared with the Plugins screen's component chips.
|
|
8
|
+
* Single-width on purpose: `⚡` (U+26A1) is East-Asian-Width Wide, two cells,
|
|
9
|
+
* which shifted everything after it on the line.
|
|
10
|
+
*/
|
|
11
|
+
const SERVER_ICON = "⬢";
|
|
12
|
+
|
|
5
13
|
// ─── List item types ───────────────────────────────────────────────────────────
|
|
6
14
|
|
|
7
15
|
export type McpListItem =
|
|
@@ -16,8 +24,10 @@ export function renderMcpRow(
|
|
|
16
24
|
isSelected: boolean,
|
|
17
25
|
): React.ReactNode {
|
|
18
26
|
if (item.kind === "category") {
|
|
27
|
+
// A section heading, so the accent — as a word, never as the fill the
|
|
28
|
+
// cursor wears.
|
|
19
29
|
return (
|
|
20
|
-
<text fg={theme.
|
|
30
|
+
<text fg={theme.colors.accent}>
|
|
21
31
|
<strong>
|
|
22
32
|
{"▸ "}
|
|
23
33
|
{item.label}
|
|
@@ -33,15 +43,19 @@ export function renderMcpRow(
|
|
|
33
43
|
if (isSelected) {
|
|
34
44
|
return (
|
|
35
45
|
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
36
|
-
{
|
|
37
|
-
{icon} {server.name}
|
|
46
|
+
{rowLead(true)}
|
|
47
|
+
{icon} {server.name}
|
|
48
|
+
{server.requiresConfig ? " ⚙" : ""}{" "}
|
|
38
49
|
</text>
|
|
39
50
|
);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
return (
|
|
43
54
|
<text fg={theme.colors.text}>
|
|
44
|
-
<span fg={iconColor}>
|
|
55
|
+
<span fg={iconColor}>
|
|
56
|
+
{rowLead(false)}
|
|
57
|
+
{icon}
|
|
58
|
+
</span>
|
|
45
59
|
<span fg={theme.colors.text}> {server.name}</span>
|
|
46
60
|
{server.requiresConfig ? <span fg={theme.colors.warning}> ⚙</span> : null}
|
|
47
61
|
</text>
|
|
@@ -75,10 +89,10 @@ export function renderMcpDetail(
|
|
|
75
89
|
|
|
76
90
|
return (
|
|
77
91
|
<box flexDirection="column">
|
|
78
|
-
<box marginBottom={1}>
|
|
92
|
+
<box flexDirection="row" marginBottom={1}>
|
|
79
93
|
<text fg={theme.colors.info}>
|
|
80
94
|
<strong>
|
|
81
|
-
{
|
|
95
|
+
{`${SERVER_ICON} `}
|
|
82
96
|
{server.name}
|
|
83
97
|
</strong>
|
|
84
98
|
</text>
|
|
@@ -90,7 +104,7 @@ export function renderMcpDetail(
|
|
|
90
104
|
<text fg={theme.colors.muted}>{server.description}</text>
|
|
91
105
|
|
|
92
106
|
<box marginTop={1} flexDirection="column">
|
|
93
|
-
<box>
|
|
107
|
+
<box flexDirection="row">
|
|
94
108
|
<text fg={theme.colors.muted}>{"Status "}</text>
|
|
95
109
|
{isInstalled ? (
|
|
96
110
|
<text fg={theme.colors.success}>{"● Installed"}</text>
|
|
@@ -98,25 +112,25 @@ export function renderMcpDetail(
|
|
|
98
112
|
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
99
113
|
)}
|
|
100
114
|
</box>
|
|
101
|
-
<box>
|
|
115
|
+
<box flexDirection="row">
|
|
102
116
|
<text fg={theme.colors.muted}>{"Type "}</text>
|
|
103
117
|
<text fg={theme.colors.text}>
|
|
104
118
|
{server.type === "http" ? "HTTP" : "Command"}
|
|
105
119
|
</text>
|
|
106
120
|
</box>
|
|
107
121
|
{server.type === "http" ? (
|
|
108
|
-
<box>
|
|
122
|
+
<box flexDirection="row">
|
|
109
123
|
<text fg={theme.colors.muted}>{"URL "}</text>
|
|
110
|
-
<text fg={theme.colors.
|
|
124
|
+
<text fg={theme.colors.info}>{server.url}</text>
|
|
111
125
|
</box>
|
|
112
126
|
) : (
|
|
113
|
-
<box>
|
|
127
|
+
<box flexDirection="row">
|
|
114
128
|
<text fg={theme.colors.muted}>{"Command "}</text>
|
|
115
129
|
<text fg={theme.colors.info}>{server.command}</text>
|
|
116
130
|
</box>
|
|
117
131
|
)}
|
|
118
132
|
{server.requiresConfig ? (
|
|
119
|
-
<box>
|
|
133
|
+
<box flexDirection="row">
|
|
120
134
|
<text fg={theme.colors.muted}>{"Config "}</text>
|
|
121
135
|
<text fg={theme.colors.warning}>
|
|
122
136
|
{server.configFields?.length ?? 0} fields required
|
|
@@ -127,7 +141,7 @@ export function renderMcpDetail(
|
|
|
127
141
|
|
|
128
142
|
<box marginTop={2}>
|
|
129
143
|
{isInstalled ? (
|
|
130
|
-
<box>
|
|
144
|
+
<box flexDirection="row">
|
|
131
145
|
<text bg={theme.colors.danger} fg={theme.hints.fg}>
|
|
132
146
|
{" "}
|
|
133
147
|
Enter{" "}
|
|
@@ -135,7 +149,7 @@ export function renderMcpDetail(
|
|
|
135
149
|
<text fg={theme.colors.muted}> Remove server</text>
|
|
136
150
|
</box>
|
|
137
151
|
) : (
|
|
138
|
-
<box>
|
|
152
|
+
<box flexDirection="row">
|
|
139
153
|
<text bg={theme.colors.success} fg={theme.hints.fg}>
|
|
140
154
|
{" "}
|
|
141
155
|
Enter{" "}
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
SelectableRow,
|
|
31
31
|
} from "../components/primitives/index.js";
|
|
32
32
|
import { effortInk, trackFill } from "../theme-mode.js";
|
|
33
|
-
import { type UiColor, theme } from "../theme.js";
|
|
33
|
+
import { type BadgeTone, type UiColor, theme } from "../theme.js";
|
|
34
34
|
import {
|
|
35
35
|
type AgentRow,
|
|
36
36
|
type AgentTableLayout,
|
|
@@ -85,14 +85,13 @@ const STATE_TONE: Record<ModelsState, UiColor> = {
|
|
|
85
85
|
unhooked: theme.colors.danger,
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
/** Badge tone for the header row. `unhooked` is
|
|
89
|
-
const STATE_CATEGORY: Record<ModelsState,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
88
|
+
/** Badge tone for the header row. `unhooked` is danger with `invalid`: both route nothing. */
|
|
89
|
+
const STATE_CATEGORY: Record<ModelsState, BadgeTone> = {
|
|
90
|
+
off: "neutral",
|
|
91
|
+
on: "success",
|
|
92
|
+
invalid: "danger",
|
|
93
|
+
unhooked: "danger",
|
|
94
|
+
};
|
|
96
95
|
|
|
97
96
|
/**
|
|
98
97
|
* How many validation errors to draw before summarising the rest.
|
|
@@ -131,9 +130,9 @@ function Chip({
|
|
|
131
130
|
tone,
|
|
132
131
|
}: {
|
|
133
132
|
label: string;
|
|
134
|
-
tone:
|
|
133
|
+
tone: BadgeTone;
|
|
135
134
|
}): React.ReactNode {
|
|
136
|
-
const colors = theme.
|
|
135
|
+
const colors = theme.badge[tone];
|
|
137
136
|
return (
|
|
138
137
|
<span bg={colors.bg} fg={colors.fg}>
|
|
139
138
|
<strong>{` ${label} `}</strong>
|
|
@@ -206,7 +205,7 @@ function StateBlock({
|
|
|
206
205
|
<text fg={theme.colors.text}>
|
|
207
206
|
<Chip
|
|
208
207
|
label={`${isInvalid ? "errors" : "drift"} ${status.drift.length}`}
|
|
209
|
-
tone={isInvalid ? "
|
|
208
|
+
tone={isInvalid ? "danger" : "warning"}
|
|
210
209
|
/>
|
|
211
210
|
</text>
|
|
212
211
|
{status.drift.slice(0, MAX_DRIFT_LINES).map((line) => (
|
|
@@ -231,7 +230,7 @@ function StateBlock({
|
|
|
231
230
|
{status.warnings.length > 0 ? (
|
|
232
231
|
<box flexDirection="column" marginTop={1}>
|
|
233
232
|
<text fg={theme.colors.text}>
|
|
234
|
-
<Chip label={`warnings ${status.warnings.length}`} tone="
|
|
233
|
+
<Chip label={`warnings ${status.warnings.length}`} tone="warning" />
|
|
235
234
|
</text>
|
|
236
235
|
{status.warnings.map((line) => (
|
|
237
236
|
<Wrapped
|
|
@@ -55,8 +55,6 @@ import { type UiColor, theme } from "../theme.js";
|
|
|
55
55
|
* `theme-mode.ts`, so the palette is a theme decision and this file is a consumer of one.
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
-
export type CategoryTone = keyof typeof theme.category;
|
|
59
|
-
|
|
60
58
|
/**
|
|
61
59
|
* The ink a model's NAME is written in.
|
|
62
60
|
*
|