magus-cli 7.7.0 → 7.8.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__/blackbox-parity.test.ts +29 -15
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1203 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +920 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +724 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +23 -17
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/doctor.ts +151 -30
- package/src/cli/install.ts +5 -6
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +27 -5
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +84 -36
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +31 -19
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +95 -1
- package/src/services/update-engine.ts +326 -31
- package/src/services/update-plan.ts +293 -33
- package/src/types/index.ts +7 -0
- package/src/ui/App.tsx +90 -146
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +282 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -45
- package/src/ui/screens/CliToolsScreen.tsx +65 -54
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +313 -221
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/version-snapshot.ts +0 -189
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
acceptPendingUpdate,
|
|
17
17
|
checkAllSources,
|
|
18
18
|
checkSourceForUpdates,
|
|
19
|
-
describeCommunityFailure,
|
|
20
19
|
fetchCommunityStyles,
|
|
21
20
|
readCommunityStatuses,
|
|
22
21
|
} from "../../services/community-styles.js";
|
|
@@ -33,6 +32,9 @@ import {
|
|
|
33
32
|
} from "../../services/styles-manager.js";
|
|
34
33
|
import { fillTerminology } from "../../services/terminology-filler.js";
|
|
35
34
|
import { openInDefaultApp } from "../../utils/open-file.js";
|
|
35
|
+
import { counted, failureReport } from "../adapters/failureReport.js";
|
|
36
|
+
import { exceptionReason } from "../adapters/pluginsAdapter.js";
|
|
37
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
36
38
|
import {
|
|
37
39
|
type StyleBrowserItem,
|
|
38
40
|
buildStyleBrowserItems,
|
|
@@ -47,8 +49,9 @@ import {
|
|
|
47
49
|
renderStyleDetail,
|
|
48
50
|
renderStyleRow,
|
|
49
51
|
} from "../renderers/styleRenderers.js";
|
|
50
|
-
import { useApp, useModal } from "../state/AppContext.js";
|
|
52
|
+
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
51
53
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
54
|
+
import type { FailureRow } from "../state/types.js";
|
|
52
55
|
import { theme } from "../theme.js";
|
|
53
56
|
|
|
54
57
|
export function StylesScreen() {
|
|
@@ -132,24 +135,25 @@ export function StylesScreen() {
|
|
|
132
135
|
[snapshot],
|
|
133
136
|
);
|
|
134
137
|
|
|
135
|
-
// ──
|
|
138
|
+
// ── Failures ──────────────────────────────────────────────────────────────
|
|
136
139
|
|
|
137
140
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* cleared when the NEXT action starts instead, so the screen always reports
|
|
144
|
-
* the most recent thing that happened.
|
|
141
|
+
* No status line and no messages. A success shows as the screen's state —
|
|
142
|
+
* the active style, a row's upstream badge, "talking to GitHub…" while a
|
|
143
|
+
* request runs. What did not happen goes into the one failure popup, with
|
|
144
|
+
* why and, when there is one, what to do. A key pressed on a row it does
|
|
145
|
+
* nothing on (the footer does not offer it there) does nothing.
|
|
145
146
|
*/
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
const showFailures = useFailurePopup();
|
|
148
|
+
const refuse = useCallback(
|
|
149
|
+
(headline: string, subject: string, reason: string, fix?: string) =>
|
|
150
|
+
showFailures(
|
|
151
|
+
failureReport(headline, [
|
|
152
|
+
fix ? { subject, reason, fix } : { subject, reason },
|
|
153
|
+
]),
|
|
154
|
+
),
|
|
155
|
+
[showFailures],
|
|
151
156
|
);
|
|
152
|
-
const statusMsg = stylesState.status;
|
|
153
157
|
|
|
154
158
|
// ── Derived ───────────────────────────────────────────────────────────────
|
|
155
159
|
|
|
@@ -226,14 +230,14 @@ export function StylesScreen() {
|
|
|
226
230
|
* the right section.
|
|
227
231
|
*/
|
|
228
232
|
const handleFetch = useCallback(async () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
+
// One request at a time; the header already says "talking to GitHub…".
|
|
234
|
+
if (isFetching) return;
|
|
233
235
|
if (!cacheDir) {
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
"
|
|
236
|
+
await refuse(
|
|
237
|
+
"Nothing was fetched",
|
|
238
|
+
"styles cache",
|
|
239
|
+
"magus could not find its cache directory",
|
|
240
|
+
"Set CLAUDE_CONFIG_DIR",
|
|
237
241
|
);
|
|
238
242
|
return;
|
|
239
243
|
}
|
|
@@ -249,9 +253,10 @@ export function StylesScreen() {
|
|
|
249
253
|
selectedItem.source.origin === "community"
|
|
250
254
|
) {
|
|
251
255
|
if (!selectedItem.source.managed) {
|
|
252
|
-
|
|
253
|
-
`${selectedItem.label}
|
|
254
|
-
|
|
256
|
+
await refuse(
|
|
257
|
+
`${selectedItem.label} was not fetched`,
|
|
258
|
+
selectedItem.label,
|
|
259
|
+
"it is your own copy, and magus will not overwrite it",
|
|
255
260
|
);
|
|
256
261
|
return;
|
|
257
262
|
}
|
|
@@ -267,10 +272,7 @@ export function StylesScreen() {
|
|
|
267
272
|
: id,
|
|
268
273
|
);
|
|
269
274
|
} else {
|
|
270
|
-
|
|
271
|
-
"Nothing to fetch here — move to a Community row and press f",
|
|
272
|
-
"error",
|
|
273
|
-
);
|
|
275
|
+
// Not a row `f` acts on, and the footer does not offer it here.
|
|
274
276
|
return;
|
|
275
277
|
}
|
|
276
278
|
|
|
@@ -280,19 +282,17 @@ export function StylesScreen() {
|
|
|
280
282
|
const result = await acceptPendingUpdate({ cacheDir, id: accept });
|
|
281
283
|
await fetchData();
|
|
282
284
|
await refreshUpstream();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
if (result.outcome !== "accepted") {
|
|
286
|
+
await refuse(
|
|
287
|
+
`${accept} was not updated`,
|
|
288
|
+
accept,
|
|
289
|
+
"no update has been downloaded for it",
|
|
290
|
+
"Press u to check for one",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
289
293
|
return;
|
|
290
294
|
}
|
|
291
295
|
|
|
292
|
-
showStatus(
|
|
293
|
-
`Fetching ${ids.length} style${ids.length === 1 ? "" : "s"} from GitHub…`,
|
|
294
|
-
"success",
|
|
295
|
-
);
|
|
296
296
|
const result = await fetchCommunityStyles({
|
|
297
297
|
ids,
|
|
298
298
|
fetcher: githubFetcher,
|
|
@@ -301,33 +301,33 @@ export function StylesScreen() {
|
|
|
301
301
|
await fetchData();
|
|
302
302
|
await refreshUpstream();
|
|
303
303
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
304
|
+
// Every failure with its own diagnosis — a 404 blames our registry, a
|
|
305
|
+
// DNS failure names Tailscale — never "3 styles failed".
|
|
306
|
+
const rows: FailureRow[] = [
|
|
307
|
+
...result.failed.map(({ failure, label }) =>
|
|
308
|
+
failure.kind === "rate-limited"
|
|
309
|
+
? {
|
|
310
|
+
subject: label,
|
|
311
|
+
reason: failure.detail,
|
|
312
|
+
fix: "Set GITHUB_TOKEN to raise the limit",
|
|
313
|
+
}
|
|
314
|
+
: { subject: label, reason: failure.detail },
|
|
315
|
+
),
|
|
316
|
+
...result.unknown.map((id) => ({
|
|
317
|
+
subject: id,
|
|
318
|
+
reason: "magus does not know where to fetch this style from",
|
|
319
|
+
fix: "Please report it",
|
|
320
|
+
})),
|
|
321
|
+
];
|
|
322
|
+
await showFailures(
|
|
323
|
+
failureReport(
|
|
324
|
+
`${counted(rows.length, "style")} ${rows.length === 1 ? "was" : "were"} not fetched`,
|
|
325
|
+
rows,
|
|
326
|
+
),
|
|
326
327
|
);
|
|
327
328
|
} catch (error) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"error",
|
|
329
|
+
await showFailures(
|
|
330
|
+
stepFailureReport("Nothing was fetched", "styles", error),
|
|
331
331
|
);
|
|
332
332
|
} finally {
|
|
333
333
|
setIsFetching(false);
|
|
@@ -339,7 +339,8 @@ export function StylesScreen() {
|
|
|
339
339
|
snapshot,
|
|
340
340
|
fetchData,
|
|
341
341
|
refreshUpstream,
|
|
342
|
-
|
|
342
|
+
refuse,
|
|
343
|
+
showFailures,
|
|
343
344
|
]);
|
|
344
345
|
|
|
345
346
|
/**
|
|
@@ -352,17 +353,10 @@ export function StylesScreen() {
|
|
|
352
353
|
*/
|
|
353
354
|
const handleCheckUpdates = useCallback(
|
|
354
355
|
async (all: boolean) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
if (!cacheDir || cachedCommunity.length === 0) {
|
|
363
|
-
showStatus("No fetched community styles to check", "error");
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
356
|
+
// One request at a time; the header already says "talking to GitHub…".
|
|
357
|
+
if (isFetching) return;
|
|
358
|
+
// Nothing fetched, nothing to check: the Community rows say so.
|
|
359
|
+
if (!cacheDir || cachedCommunity.length === 0) return;
|
|
366
360
|
|
|
367
361
|
let cached = cachedCommunity;
|
|
368
362
|
let only: CommunityStyleSource | null = null;
|
|
@@ -374,13 +368,8 @@ export function StylesScreen() {
|
|
|
374
368
|
? selectedItem.source.name
|
|
375
369
|
: null;
|
|
376
370
|
const resolved = name ? resolveCommunityStyle(name) : null;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"Move to a fetched community style and press u, or U to check all",
|
|
380
|
-
"error",
|
|
381
|
-
);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
371
|
+
// Not a row `u` acts on, and the footer does not offer it here.
|
|
372
|
+
if (!resolved) return;
|
|
384
373
|
only = resolved.source;
|
|
385
374
|
// Every style of this REPO, not just the selected row — one API call
|
|
386
375
|
// answers for all of them, so narrowing it further would pay the same
|
|
@@ -389,16 +378,16 @@ export function StylesScreen() {
|
|
|
389
378
|
(entry) => resolveCommunityStyle(entry.id)?.source.id === only?.id,
|
|
390
379
|
);
|
|
391
380
|
if (cached.length === 0) {
|
|
392
|
-
|
|
393
|
-
`${name}
|
|
394
|
-
"
|
|
381
|
+
await refuse(
|
|
382
|
+
`${name} was not checked`,
|
|
383
|
+
name ?? "style",
|
|
384
|
+
"it is not a copy magus manages",
|
|
395
385
|
);
|
|
396
386
|
return;
|
|
397
387
|
}
|
|
398
388
|
}
|
|
399
389
|
|
|
400
390
|
setIsFetching(true);
|
|
401
|
-
showStatus("Checking GitHub for newer versions…");
|
|
402
391
|
try {
|
|
403
392
|
const statuses = only
|
|
404
393
|
? (
|
|
@@ -420,29 +409,26 @@ export function StylesScreen() {
|
|
|
420
409
|
).statuses;
|
|
421
410
|
|
|
422
411
|
await refreshUpstream();
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
).
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
441
|
-
}
|
|
412
|
+
// An update found shows on its row. A style that could not be
|
|
413
|
+
// checked is a failure, said with its reason — a rate-limited check
|
|
414
|
+
// must never read as up to date.
|
|
415
|
+
const unknown = Object.entries(statuses).filter(
|
|
416
|
+
([, s]) => s.state === "unknown",
|
|
417
|
+
);
|
|
418
|
+
await showFailures(
|
|
419
|
+
failureReport(
|
|
420
|
+
`${counted(unknown.length, "style")} could not be checked for updates`,
|
|
421
|
+
unknown.map(([id, s]) => ({
|
|
422
|
+
subject: id.startsWith(COMMUNITY_ID_PREFIX)
|
|
423
|
+
? id.slice(COMMUNITY_ID_PREFIX.length)
|
|
424
|
+
: id,
|
|
425
|
+
reason: s.detail || "GitHub did not answer",
|
|
426
|
+
})),
|
|
427
|
+
),
|
|
428
|
+
);
|
|
442
429
|
} catch (error) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
"error",
|
|
430
|
+
await showFailures(
|
|
431
|
+
stepFailureReport("Nothing was checked for updates", "styles", error),
|
|
446
432
|
);
|
|
447
433
|
} finally {
|
|
448
434
|
setIsFetching(false);
|
|
@@ -454,7 +440,8 @@ export function StylesScreen() {
|
|
|
454
440
|
cachedCommunity,
|
|
455
441
|
selectedItem,
|
|
456
442
|
refreshUpstream,
|
|
457
|
-
|
|
443
|
+
refuse,
|
|
444
|
+
showFailures,
|
|
458
445
|
],
|
|
459
446
|
);
|
|
460
447
|
|
|
@@ -477,9 +464,11 @@ export function StylesScreen() {
|
|
|
477
464
|
const { source } = selectedItem;
|
|
478
465
|
|
|
479
466
|
if (selectedItem.disabled) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
467
|
+
void refuse(
|
|
468
|
+
`${source.name} was not selected`,
|
|
469
|
+
source.name,
|
|
470
|
+
"it is a template",
|
|
471
|
+
"Fill it in and import it instead",
|
|
483
472
|
);
|
|
484
473
|
return;
|
|
485
474
|
}
|
|
@@ -506,7 +495,7 @@ export function StylesScreen() {
|
|
|
506
495
|
stylesState.selected,
|
|
507
496
|
snapshot,
|
|
508
497
|
dispatch,
|
|
509
|
-
|
|
498
|
+
refuse,
|
|
510
499
|
handleFetch,
|
|
511
500
|
]);
|
|
512
501
|
|
|
@@ -515,11 +504,20 @@ export function StylesScreen() {
|
|
|
515
504
|
const handleApply = useCallback(async () => {
|
|
516
505
|
if (isApplying) return;
|
|
517
506
|
if (errors.length > 0) {
|
|
518
|
-
|
|
507
|
+
await refuse(
|
|
508
|
+
"No style was applied",
|
|
509
|
+
"selection",
|
|
510
|
+
errors[0] ?? "the selection is not valid",
|
|
511
|
+
);
|
|
519
512
|
return;
|
|
520
513
|
}
|
|
521
514
|
if (chosen.presets.length === 0 && chosen.imports.length === 0) {
|
|
522
|
-
|
|
515
|
+
await refuse(
|
|
516
|
+
"No style was applied",
|
|
517
|
+
"selection",
|
|
518
|
+
"nothing is ticked",
|
|
519
|
+
"Press Space to tick a style",
|
|
520
|
+
);
|
|
523
521
|
return;
|
|
524
522
|
}
|
|
525
523
|
|
|
@@ -531,43 +529,48 @@ export function StylesScreen() {
|
|
|
531
529
|
imports: chosen.imports as ImportedStyle[],
|
|
532
530
|
});
|
|
533
531
|
await fetchData();
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
showStatus(
|
|
545
|
-
`Applied ${result.styleName} to ${where} — commit .claude/style.json to share it${durability}`,
|
|
546
|
-
result.profile && !result.recordedInManifest ? "error" : "success",
|
|
547
|
-
);
|
|
532
|
+
// Applied: the header shows the active style. Said only when the
|
|
533
|
+
// profile did NOT record it — the next activation renders the live
|
|
534
|
+
// settings from the profile and drops the style.
|
|
535
|
+
if (result.profile && !result.recordedInManifest) {
|
|
536
|
+
await refuse(
|
|
537
|
+
`${result.styleName} is active, but not saved in the profile`,
|
|
538
|
+
`profile ${result.profile}`,
|
|
539
|
+
"the style is not recorded in it, so the next magus install drops it",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
548
542
|
} catch (error) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
"error",
|
|
543
|
+
await showFailures(
|
|
544
|
+
stepFailureReport("No style was applied", "output style", error),
|
|
552
545
|
);
|
|
553
546
|
} finally {
|
|
554
547
|
setIsApplying(false);
|
|
555
548
|
}
|
|
556
|
-
}, [
|
|
549
|
+
}, [
|
|
550
|
+
isApplying,
|
|
551
|
+
errors,
|
|
552
|
+
chosen,
|
|
553
|
+
state.projectPath,
|
|
554
|
+
fetchData,
|
|
555
|
+
refuse,
|
|
556
|
+
showFailures,
|
|
557
|
+
]);
|
|
557
558
|
|
|
558
559
|
const handleClear = useCallback(async () => {
|
|
559
560
|
try {
|
|
560
561
|
await clearStyle(state.projectPath);
|
|
561
562
|
await fetchData();
|
|
562
563
|
dispatch({ type: "STYLES_SET_SELECTION", ids: [] });
|
|
563
|
-
showStatus("Cleared the active output style");
|
|
564
564
|
} catch (error) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
565
|
+
await showFailures(
|
|
566
|
+
stepFailureReport(
|
|
567
|
+
"The output style was not cleared",
|
|
568
|
+
"output style",
|
|
569
|
+
error,
|
|
570
|
+
),
|
|
568
571
|
);
|
|
569
572
|
}
|
|
570
|
-
}, [state.projectPath, fetchData, dispatch,
|
|
573
|
+
}, [state.projectPath, fetchData, dispatch, showFailures]);
|
|
571
574
|
|
|
572
575
|
const handleReset = useCallback(() => {
|
|
573
576
|
dispatch({
|
|
@@ -577,8 +580,7 @@ export function StylesScreen() {
|
|
|
577
580
|
...(snapshot?.applied?.imports ?? []),
|
|
578
581
|
],
|
|
579
582
|
});
|
|
580
|
-
|
|
581
|
-
}, [snapshot, dispatch, showStatus]);
|
|
583
|
+
}, [snapshot, dispatch]);
|
|
582
584
|
|
|
583
585
|
/**
|
|
584
586
|
* Start a new team style: scaffold a file in the project's
|
|
@@ -586,7 +588,7 @@ export function StylesScreen() {
|
|
|
586
588
|
*
|
|
587
589
|
* The file is created rather than opened in an editor — magus owns the
|
|
588
590
|
* TTY, and handing it to `$EDITOR` from inside OpenTUI is a good way to
|
|
589
|
-
* leave the terminal wedged.
|
|
591
|
+
* leave the terminal wedged. It opens in the default app; `r` reloads.
|
|
590
592
|
*/
|
|
591
593
|
const handleNewTeamStyle = useCallback(async () => {
|
|
592
594
|
const name = await modal.input(
|
|
@@ -597,35 +599,36 @@ export function StylesScreen() {
|
|
|
597
599
|
try {
|
|
598
600
|
const created = await createTeamStyle(state.projectPath, name);
|
|
599
601
|
await fetchData();
|
|
600
|
-
// Project-relative:
|
|
601
|
-
//
|
|
602
|
-
// the filename — the only part the reader needs.
|
|
602
|
+
// Project-relative: an absolute path under a long temp or worktree
|
|
603
|
+
// prefix is cut before it reaches the filename — the part that matters.
|
|
603
604
|
const shown = created.path.startsWith(`${state.projectPath}/`)
|
|
604
605
|
? created.path.slice(state.projectPath.length + 1)
|
|
605
606
|
: created.path;
|
|
606
|
-
// Open it straight away
|
|
607
|
-
//
|
|
608
|
-
|
|
607
|
+
// Open it straight away — an existing one too, which is opened rather
|
|
608
|
+
// than overwritten: a scaffold nobody edits is worse than no scaffold,
|
|
609
|
+
// and the whole point of creating one is to write in it. The new row
|
|
610
|
+
// is the confirmation; only a file that would not open is said.
|
|
609
611
|
try {
|
|
610
612
|
await openInDefaultApp(created.path);
|
|
611
|
-
} catch {
|
|
612
|
-
|
|
613
|
+
} catch (error) {
|
|
614
|
+
await showFailures(
|
|
615
|
+
failureReport(`${shown} was not opened`, [
|
|
616
|
+
{
|
|
617
|
+
subject: shown,
|
|
618
|
+
reason: exceptionReason(
|
|
619
|
+
error instanceof Error ? error.message : String(error),
|
|
620
|
+
).reason,
|
|
621
|
+
fix: "Open it to edit, then press r",
|
|
622
|
+
},
|
|
623
|
+
]),
|
|
624
|
+
);
|
|
613
625
|
}
|
|
614
|
-
showStatus(
|
|
615
|
-
created.existed
|
|
616
|
-
? `${shown} already exists — opened it instead of overwriting`
|
|
617
|
-
: opened
|
|
618
|
-
? `Created and opened ${shown} — press r after saving`
|
|
619
|
-
: `Created ${shown} — open it to edit, then press r`,
|
|
620
|
-
created.existed ? "error" : "success",
|
|
621
|
-
);
|
|
622
626
|
} catch (error) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
"error",
|
|
627
|
+
await showFailures(
|
|
628
|
+
stepFailureReport("The team style was not created", name, error),
|
|
626
629
|
);
|
|
627
630
|
}
|
|
628
|
-
}, [modal, state.projectPath, fetchData,
|
|
631
|
+
}, [modal, state.projectPath, fetchData, showFailures]);
|
|
629
632
|
|
|
630
633
|
/**
|
|
631
634
|
* Open the selected style's file in the system's default application.
|
|
@@ -636,9 +639,11 @@ export function StylesScreen() {
|
|
|
636
639
|
*/
|
|
637
640
|
const handleOpen = useCallback(async () => {
|
|
638
641
|
if (selectedItem?.kind === "offer") {
|
|
639
|
-
|
|
640
|
-
`${selectedItem.label}
|
|
641
|
-
|
|
642
|
+
await refuse(
|
|
643
|
+
`${selectedItem.label} was not opened`,
|
|
644
|
+
selectedItem.label,
|
|
645
|
+
"it is not on this machine yet",
|
|
646
|
+
"Press f to fetch it",
|
|
642
647
|
);
|
|
643
648
|
return;
|
|
644
649
|
}
|
|
@@ -646,29 +651,29 @@ export function StylesScreen() {
|
|
|
646
651
|
const { source } = selectedItem;
|
|
647
652
|
|
|
648
653
|
if (source.kind === "preset") {
|
|
649
|
-
|
|
650
|
-
`${source.name}
|
|
651
|
-
|
|
654
|
+
await refuse(
|
|
655
|
+
`${source.name} was not opened`,
|
|
656
|
+
source.name,
|
|
657
|
+
"it is built into magus and is replaced on update",
|
|
658
|
+
"Press n to make a team style instead",
|
|
652
659
|
);
|
|
653
660
|
return;
|
|
654
661
|
}
|
|
655
662
|
|
|
663
|
+
// Opened in the default app, which is the confirmation. Whether a later
|
|
664
|
+
// re-fetch or re-capture overwrites it is on the style's detail panel.
|
|
656
665
|
try {
|
|
657
666
|
await openInDefaultApp(source.path);
|
|
658
|
-
showStatus(
|
|
659
|
-
source.origin === "anthropic"
|
|
660
|
-
? `Opened ${source.name}.md — note re-capturing overwrites it`
|
|
661
|
-
: source.origin === "community"
|
|
662
|
-
? `Opened ${source.name}.md — a re-fetch overwrites it, so edit a copy`
|
|
663
|
-
: `Opened ${source.name}.md — press r after saving`,
|
|
664
|
-
);
|
|
665
667
|
} catch (error) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
668
|
+
await showFailures(
|
|
669
|
+
stepFailureReport(
|
|
670
|
+
`${source.name} was not opened`,
|
|
671
|
+
`${source.name}.md`,
|
|
672
|
+
error,
|
|
673
|
+
),
|
|
669
674
|
);
|
|
670
675
|
}
|
|
671
|
-
}, [selectedItem,
|
|
676
|
+
}, [selectedItem, refuse, showFailures]);
|
|
672
677
|
|
|
673
678
|
const isFilling = stylesState.isFilling;
|
|
674
679
|
|
|
@@ -680,44 +685,30 @@ export function StylesScreen() {
|
|
|
680
685
|
* for. It runs read-only and returns table rows; magus writes the file.
|
|
681
686
|
*/
|
|
682
687
|
const handleFillTemplate = useCallback(async () => {
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
"error",
|
|
690
|
-
);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
if (!selectedItem || selectedItem.kind !== "style") {
|
|
694
|
-
showStatus("Select a style row first", "error");
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
688
|
+
// A fill runs for minutes; the header says "reading the codebase…" for
|
|
689
|
+
// all of it, so a second `t` is acknowledged by that state, not ignored.
|
|
690
|
+
if (isFilling) return;
|
|
691
|
+
// `t` is offered only on a template preset (the footer hides it
|
|
692
|
+
// elsewhere), and does nothing anywhere else.
|
|
693
|
+
if (!selectedItem || selectedItem.kind !== "style") return;
|
|
697
694
|
const { source } = selectedItem;
|
|
698
|
-
if (source.kind !== "preset" || !source.template)
|
|
699
|
-
showStatus("Only a template preset can be filled in", "error");
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
695
|
+
if (source.kind !== "preset" || !source.template) return;
|
|
702
696
|
|
|
703
697
|
dispatch({ type: "STYLES_FILL_START" });
|
|
704
|
-
showStatus(
|
|
705
|
-
`Reading the codebase to fill ${source.name}… this can take a few minutes`,
|
|
706
|
-
"success",
|
|
707
|
-
);
|
|
708
698
|
try {
|
|
709
|
-
|
|
699
|
+
// Filled: the new style appears under Team.
|
|
700
|
+
await fillTerminology({
|
|
710
701
|
projectPath: state.projectPath,
|
|
711
702
|
templateBody: source.body,
|
|
712
703
|
});
|
|
713
704
|
await fetchData();
|
|
714
|
-
showStatus(
|
|
715
|
-
`Filled ${source.name} with ${result.rows.length} terms — it is now under Team`,
|
|
716
|
-
);
|
|
717
705
|
} catch (error) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
706
|
+
await showFailures(
|
|
707
|
+
stepFailureReport(
|
|
708
|
+
`${source.name} was not filled in`,
|
|
709
|
+
source.name,
|
|
710
|
+
error,
|
|
711
|
+
),
|
|
721
712
|
);
|
|
722
713
|
} finally {
|
|
723
714
|
dispatch({ type: "STYLES_FILL_END" });
|
|
@@ -727,7 +718,7 @@ export function StylesScreen() {
|
|
|
727
718
|
selectedItem,
|
|
728
719
|
state.projectPath,
|
|
729
720
|
fetchData,
|
|
730
|
-
|
|
721
|
+
showFailures,
|
|
731
722
|
dispatch,
|
|
732
723
|
]);
|
|
733
724
|
|
|
@@ -763,112 +754,56 @@ export function StylesScreen() {
|
|
|
763
754
|
blur: () => dispatch({ type: "SET_SEARCHING", isSearching: false }),
|
|
764
755
|
});
|
|
765
756
|
|
|
766
|
-
/**
|
|
767
|
-
* The previous message belongs to the previous ACTION, so every key this
|
|
768
|
-
* screen acts on clears it first. Keys another layer takes — a digit or Tab
|
|
769
|
-
* leaving the tab, text typed into the search box — leave it alone, so a
|
|
770
|
-
* round trip through another tab keeps it.
|
|
771
|
-
*/
|
|
772
|
-
const clearStatus = () => {
|
|
773
|
-
if (stylesState.status) dispatch({ type: "STYLES_STATUS_CLEAR" });
|
|
774
|
-
};
|
|
775
|
-
|
|
776
757
|
useScreenKeys("styles", {
|
|
777
|
-
toggle: () =>
|
|
778
|
-
clearStatus();
|
|
779
|
-
handleToggle();
|
|
780
|
-
},
|
|
758
|
+
toggle: () => handleToggle(),
|
|
781
759
|
apply: () => {
|
|
782
|
-
clearStatus();
|
|
783
760
|
void handleApply();
|
|
784
761
|
},
|
|
785
762
|
fill: () => {
|
|
786
|
-
clearStatus();
|
|
787
763
|
void handleFillTemplate();
|
|
788
764
|
},
|
|
789
765
|
clear: () => {
|
|
790
|
-
clearStatus();
|
|
791
766
|
void handleClear();
|
|
792
767
|
},
|
|
793
768
|
// Enter opens the style for editing; Space is the ticking key. Enter
|
|
794
769
|
// reads as "go into this thing", and ticking already has a key that
|
|
795
770
|
// means exactly that and nothing else.
|
|
796
771
|
edit: () => {
|
|
797
|
-
clearStatus();
|
|
798
772
|
void handleOpen();
|
|
799
773
|
},
|
|
800
774
|
fetch: () => {
|
|
801
|
-
clearStatus();
|
|
802
775
|
void handleFetch();
|
|
803
776
|
},
|
|
804
777
|
new: () => {
|
|
805
|
-
clearStatus();
|
|
806
778
|
void handleNewTeamStyle();
|
|
807
779
|
},
|
|
808
780
|
check: () => {
|
|
809
|
-
clearStatus();
|
|
810
781
|
void handleCheckUpdates(false);
|
|
811
782
|
},
|
|
812
783
|
"check-all": () => {
|
|
813
|
-
clearStatus();
|
|
814
784
|
void handleCheckUpdates(true);
|
|
815
785
|
},
|
|
816
|
-
reset: () =>
|
|
817
|
-
clearStatus();
|
|
818
|
-
handleReset();
|
|
819
|
-
},
|
|
786
|
+
reset: () => handleReset(),
|
|
820
787
|
refresh: () => {
|
|
821
|
-
clearStatus();
|
|
822
788
|
void fetchData();
|
|
823
789
|
},
|
|
824
|
-
search: () => {
|
|
825
|
-
clearStatus();
|
|
826
|
-
dispatch({ type: "SET_SEARCHING", isSearching: true });
|
|
827
|
-
},
|
|
790
|
+
search: () => dispatch({ type: "SET_SEARCHING", isSearching: true }),
|
|
828
791
|
// Only while a committed filter is set; otherwise Esc is the global back.
|
|
829
792
|
"clear-filter": () => {
|
|
830
793
|
if (!stylesState.searchQuery) return false;
|
|
831
|
-
clearStatus();
|
|
832
794
|
dispatch({ type: "STYLES_SET_SEARCH", query: "" });
|
|
833
795
|
dispatch({ type: "STYLES_SELECT", index: 0 });
|
|
834
796
|
return true;
|
|
835
797
|
},
|
|
836
|
-
move: (key) =>
|
|
837
|
-
clearStatus();
|
|
838
|
-
move(moveDirection(key));
|
|
839
|
-
},
|
|
798
|
+
move: (key) => move(moveDirection(key)),
|
|
840
799
|
});
|
|
841
800
|
|
|
842
801
|
// ── Status line ───────────────────────────────────────────────────────────
|
|
843
802
|
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
// message wins while it lasts, so pressing a key always acknowledges the
|
|
849
|
-
// press — otherwise the fill banner below would shadow "already reading"
|
|
850
|
-
// and the second press would look ignored again, which is the bug this
|
|
851
|
-
// path exists to fix. The banner is the fallback because `statusMsg` is
|
|
852
|
-
// local and dies when the screen unmounts, leaving it the only thing that
|
|
853
|
-
// still reports work in flight after a tab switch.
|
|
854
|
-
const noticeContent = statusMsg ? (
|
|
855
|
-
<text
|
|
856
|
-
fg={
|
|
857
|
-
statusMsg.tone === "success"
|
|
858
|
-
? theme.colors.success
|
|
859
|
-
: theme.colors.danger
|
|
860
|
-
}
|
|
861
|
-
>
|
|
862
|
-
{statusMsg.text}
|
|
863
|
-
</text>
|
|
864
|
-
) : isFilling ? (
|
|
865
|
-
<text fg={theme.colors.muted}>
|
|
866
|
-
Reading the codebase to fill a template… this can take a few minutes
|
|
867
|
-
</text>
|
|
868
|
-
) : undefined;
|
|
869
|
-
|
|
870
|
-
// The header keeps only the short ambient state — it shares its row with
|
|
871
|
-
// the title and must never crowd it.
|
|
803
|
+
// The header carries state only — it shares its row with the title and must
|
|
804
|
+
// never crowd it. Work in flight is state too: `isFilling` lives in app
|
|
805
|
+
// state, so "reading the codebase…" survives a tab switch while a fill that
|
|
806
|
+
// takes minutes runs.
|
|
872
807
|
const statusContent = (
|
|
873
808
|
<text fg={theme.colors.text}>
|
|
874
809
|
<span fg={theme.colors.muted}>Active: </span>
|
|
@@ -886,6 +821,8 @@ export function StylesScreen() {
|
|
|
886
821
|
) : null}
|
|
887
822
|
{isApplying ? (
|
|
888
823
|
<span fg={theme.colors.muted}> │ applying…</span>
|
|
824
|
+
) : isFilling ? (
|
|
825
|
+
<span fg={theme.colors.muted}> │ reading the codebase…</span>
|
|
889
826
|
) : isFetching ? (
|
|
890
827
|
<span fg={theme.colors.muted}> │ talking to GitHub…</span>
|
|
891
828
|
) : errors.length > 0 ? (
|
|
@@ -949,7 +886,6 @@ export function StylesScreen() {
|
|
|
949
886
|
title="magus Styles"
|
|
950
887
|
currentScreen="styles"
|
|
951
888
|
statusLine={statusContent}
|
|
952
|
-
notice={noticeContent}
|
|
953
889
|
search={
|
|
954
890
|
stylesState.searchQuery || isSearchActive
|
|
955
891
|
? {
|