magus-cli 7.6.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 +9 -34
- package/src/__tests__/gitignore-profile.test.ts +0 -4
- 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 -42
- 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/gitignore-service.ts +0 -19
- 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-absorb.ts +9 -0
- package/src/services/profile-activate.ts +1 -1
- package/src/services/profile-startup.ts +30 -27
- package/src/services/profile-state.ts +81 -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 +186 -123
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +299 -28
- 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 -13
- 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 +31 -31
- package/src/ui/hooks/useProfileStartupModal.ts +127 -122
- package/src/ui/hooks/useStaleLoadModal.ts +33 -53
- 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 -44
- package/src/ui/screens/CliToolsScreen.tsx +65 -51
- package/src/ui/screens/EnvVarsScreen.tsx +21 -6
- package/src/ui/screens/GitignoreScreen.tsx +49 -75
- package/src/ui/screens/McpRegistryScreen.tsx +72 -40
- package/src/ui/screens/McpScreen.tsx +55 -24
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +316 -245
- package/src/ui/screens/ProfilesScreen.tsx +88 -73
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +72 -7
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +25 -14
- package/src/ui/state/types.ts +61 -31
- package/src/utils/user-path.ts +60 -0
- 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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { FailureReport, FailureRow } from "../../state/types.js";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
/** Border (2) and horizontal padding (3 + 3). */
|
|
5
|
+
const CHROME = 8;
|
|
6
|
+
const MAX_WIDTH = 88;
|
|
7
|
+
const MIN_WIDTH = 40;
|
|
8
|
+
/** "✗ " before each row's subject. */
|
|
9
|
+
const MARK = "✗ ";
|
|
10
|
+
/**
|
|
11
|
+
* Rows of the popup that are not failure rows: border (2), padding (2), the
|
|
12
|
+
* headline and its gap (2), the footer and its gap (2), and a row of air
|
|
13
|
+
* between the popup and the terminal's edge (2).
|
|
14
|
+
*/
|
|
15
|
+
const FIXED_ROWS = 10;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The popup's width for a terminal `terminalWidth` columns wide: as wide as the
|
|
19
|
+
* rows need, never wider than the terminal leaves room for.
|
|
20
|
+
*/
|
|
21
|
+
export function failureListWidth(terminalWidth: number): number {
|
|
22
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, terminalWidth - 4));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a row's reason goes on its own line under the subject. The subject
|
|
27
|
+
* never wraps — it is what the user scans for — so a long one (several plugins
|
|
28
|
+
* sharing a reason) takes the line to itself and the reason goes beneath it,
|
|
29
|
+
* rather than being squeezed into a sliver beside it.
|
|
30
|
+
*/
|
|
31
|
+
export function reasonBelow(subject: string, innerWidth: number): boolean {
|
|
32
|
+
return MARK.length + subject.length + 2 > innerWidth / 2;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Lines `text` takes when word-wrapped at `width` columns. */
|
|
36
|
+
function wrappedLines(text: string, width: number): number {
|
|
37
|
+
if (width <= 0) return text.length;
|
|
38
|
+
let lines = 1;
|
|
39
|
+
let used = 0;
|
|
40
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
41
|
+
const len = word.length;
|
|
42
|
+
if (used === 0) {
|
|
43
|
+
used = len;
|
|
44
|
+
} else if (used + 1 + len <= width) {
|
|
45
|
+
used += 1 + len;
|
|
46
|
+
} else {
|
|
47
|
+
lines++;
|
|
48
|
+
used = len;
|
|
49
|
+
}
|
|
50
|
+
while (used > width) {
|
|
51
|
+
lines++;
|
|
52
|
+
used -= width;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Terminal rows one failure row takes at an inner width of `inner`. */
|
|
59
|
+
function rowHeight(row: FailureRow, inner: number): number {
|
|
60
|
+
if (reasonBelow(row.subject, inner)) {
|
|
61
|
+
const column = inner - MARK.length;
|
|
62
|
+
return (
|
|
63
|
+
1 +
|
|
64
|
+
wrappedLines(row.reason, column) +
|
|
65
|
+
(row.fix ? wrappedLines(row.fix, column) : 0)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const column = inner - (MARK.length + row.subject.length + 2);
|
|
69
|
+
return (
|
|
70
|
+
wrappedLines(row.reason, column) +
|
|
71
|
+
(row.fix ? wrappedLines(row.fix, column) : 0)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The rows that fit a terminal `terminalHeight` rows tall, and how many did
|
|
77
|
+
* not. Without a height every row is drawn. A popup taller than the terminal
|
|
78
|
+
* would push its own headline and footer off screen, so it stops early and
|
|
79
|
+
* says `+N more` instead — and always shows at least one row.
|
|
80
|
+
*/
|
|
81
|
+
export function visibleFailureRows(
|
|
82
|
+
report: FailureReport,
|
|
83
|
+
terminalWidth: number,
|
|
84
|
+
terminalHeight?: number,
|
|
85
|
+
): { rows: FailureRow[]; hidden: number } {
|
|
86
|
+
if (terminalHeight === undefined) return { rows: report.rows, hidden: 0 };
|
|
87
|
+
const inner = failureListWidth(terminalWidth) - CHROME;
|
|
88
|
+
const fixRows = report.fix ? 1 + wrappedLines(report.fix, inner) : 0;
|
|
89
|
+
const budget = terminalHeight - FIXED_ROWS - fixRows;
|
|
90
|
+
const total = report.rows.reduce(
|
|
91
|
+
(sum, row) => sum + rowHeight(row, inner),
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
if (total <= budget) return { rows: report.rows, hidden: 0 };
|
|
95
|
+
|
|
96
|
+
// One row is kept back for the "+N more" line.
|
|
97
|
+
const rows: FailureRow[] = [];
|
|
98
|
+
let used = 0;
|
|
99
|
+
for (const row of report.rows) {
|
|
100
|
+
const height = rowHeight(row, inner);
|
|
101
|
+
if (rows.length > 0 && used + height > budget - 1) break;
|
|
102
|
+
rows.push(row);
|
|
103
|
+
used += height;
|
|
104
|
+
}
|
|
105
|
+
return { rows, hidden: report.rows.length - rows.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A row's reason and, when it has its own, its fix beneath it. A plain call,
|
|
110
|
+
* not a component, so the whole popup is one element tree.
|
|
111
|
+
*/
|
|
112
|
+
function reasonColumn(row: FailureRow) {
|
|
113
|
+
return (
|
|
114
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1}>
|
|
115
|
+
<text fg={theme.colors.muted} wrapMode="word">
|
|
116
|
+
{row.reason}
|
|
117
|
+
</text>
|
|
118
|
+
{row.fix ? (
|
|
119
|
+
<text fg={theme.colors.text} wrapMode="word">
|
|
120
|
+
{row.fix}
|
|
121
|
+
</text>
|
|
122
|
+
) : null}
|
|
123
|
+
</box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* THE failure popup: what did not happen, counted; each thing that failed with
|
|
129
|
+
* why; and the one fix, when there is one. The shape is {@link FailureReport},
|
|
130
|
+
* built by a pure function per screen, so every failure reads the same way.
|
|
131
|
+
*
|
|
132
|
+
* Theme tokens only: the ✗ in danger, the subject in the text colour, the
|
|
133
|
+
* reason muted — the reason is the detail, the subject is what the user scans.
|
|
134
|
+
* A fix is in the text colour, under its row when only that row has it, or
|
|
135
|
+
* once under all of them. Reasons wrap; subjects do not. Given the terminal's
|
|
136
|
+
* height, it draws only the rows that fit and ends with `+N more`. Any key
|
|
137
|
+
* dismisses it (ModalContainer).
|
|
138
|
+
*/
|
|
139
|
+
export function FailureListModal({
|
|
140
|
+
report,
|
|
141
|
+
terminalWidth,
|
|
142
|
+
terminalHeight,
|
|
143
|
+
}: {
|
|
144
|
+
report: FailureReport;
|
|
145
|
+
terminalWidth: number;
|
|
146
|
+
/** Cap the rows to this many terminal rows; every row is drawn without it. */
|
|
147
|
+
terminalHeight?: number;
|
|
148
|
+
}) {
|
|
149
|
+
const width = failureListWidth(terminalWidth);
|
|
150
|
+
const inner = width - CHROME;
|
|
151
|
+
const { rows, hidden } = visibleFailureRows(
|
|
152
|
+
report,
|
|
153
|
+
terminalWidth,
|
|
154
|
+
terminalHeight,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<box
|
|
159
|
+
flexDirection="column"
|
|
160
|
+
border
|
|
161
|
+
borderStyle="rounded"
|
|
162
|
+
borderColor={theme.colors.dim}
|
|
163
|
+
backgroundColor={theme.surface.bg}
|
|
164
|
+
paddingLeft={3}
|
|
165
|
+
paddingRight={3}
|
|
166
|
+
paddingTop={1}
|
|
167
|
+
paddingBottom={1}
|
|
168
|
+
width={width}
|
|
169
|
+
>
|
|
170
|
+
<box marginBottom={1}>
|
|
171
|
+
<text fg={theme.colors.text}>
|
|
172
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
173
|
+
<strong>{report.headline}</strong>
|
|
174
|
+
</text>
|
|
175
|
+
</box>
|
|
176
|
+
|
|
177
|
+
{/*
|
|
178
|
+
* The index is part of the key: a report built with `group: false`
|
|
179
|
+
* keeps rows apart even when subject and reason match, and a repeated
|
|
180
|
+
* key makes React drop or reuse one of them.
|
|
181
|
+
*/}
|
|
182
|
+
{rows.map((row, index) =>
|
|
183
|
+
reasonBelow(row.subject, inner) ? (
|
|
184
|
+
<box
|
|
185
|
+
key={`${row.subject}:${row.reason}:${index}`}
|
|
186
|
+
flexDirection="column"
|
|
187
|
+
>
|
|
188
|
+
<text fg={theme.colors.text}>
|
|
189
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
190
|
+
<span fg={theme.colors.text}>{row.subject}</span>
|
|
191
|
+
</text>
|
|
192
|
+
<box paddingLeft={MARK.length}>{reasonColumn(row)}</box>
|
|
193
|
+
</box>
|
|
194
|
+
) : (
|
|
195
|
+
<box
|
|
196
|
+
key={`${row.subject}:${row.reason}:${index}`}
|
|
197
|
+
flexDirection="row"
|
|
198
|
+
>
|
|
199
|
+
<box flexShrink={0}>
|
|
200
|
+
<text fg={theme.colors.text}>
|
|
201
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
202
|
+
<span fg={theme.colors.text}>{`${row.subject} `}</span>
|
|
203
|
+
</text>
|
|
204
|
+
</box>
|
|
205
|
+
{reasonColumn(row)}
|
|
206
|
+
</box>
|
|
207
|
+
),
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{hidden > 0 ? (
|
|
211
|
+
<box paddingLeft={MARK.length}>
|
|
212
|
+
<text fg={theme.colors.muted}>{`+${hidden} more`}</text>
|
|
213
|
+
</box>
|
|
214
|
+
) : null}
|
|
215
|
+
|
|
216
|
+
{report.fix ? (
|
|
217
|
+
<box marginTop={1}>
|
|
218
|
+
<text fg={theme.colors.text} wrapMode="word">
|
|
219
|
+
{report.fix}
|
|
220
|
+
</text>
|
|
221
|
+
</box>
|
|
222
|
+
) : null}
|
|
223
|
+
|
|
224
|
+
<box marginTop={1}>
|
|
225
|
+
<text fg={theme.colors.muted}>Press any key to continue</text>
|
|
226
|
+
</box>
|
|
227
|
+
</box>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default FailureListModal;
|
|
@@ -6,20 +6,14 @@ interface MessageModalProps {
|
|
|
6
6
|
title: string;
|
|
7
7
|
/** Modal message */
|
|
8
8
|
message: string;
|
|
9
|
-
/** Message variant */
|
|
10
|
-
variant: "info" | "success" | "error";
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** Draws the message. Any key dismisses it — ModalContainer handles that. */
|
|
20
|
-
export function MessageModal({ title, message, variant }: MessageModalProps) {
|
|
21
|
-
const config = variantConfig[variant];
|
|
22
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Draws information to read: help text, a command to run. Never a failure —
|
|
13
|
+
* those go in `FailureListModal`. Any key dismisses it — ModalContainer
|
|
14
|
+
* handles that.
|
|
15
|
+
*/
|
|
16
|
+
export function MessageModal({ title, message }: MessageModalProps) {
|
|
23
17
|
return (
|
|
24
18
|
<box
|
|
25
19
|
flexDirection="column"
|
|
@@ -34,7 +28,7 @@ export function MessageModal({ title, message, variant }: MessageModalProps) {
|
|
|
34
28
|
width={60}
|
|
35
29
|
>
|
|
36
30
|
<box marginBottom={1}>
|
|
37
|
-
<text fg={
|
|
31
|
+
<text fg={theme.colors.info}>ℹ </text>
|
|
38
32
|
<text fg={theme.colors.text}>
|
|
39
33
|
<strong>{title}</strong>
|
|
40
34
|
</text>
|
|
@@ -5,6 +5,7 @@ import { type KeyPress, isScrollKey } from "../../input/key-dispatch.js";
|
|
|
5
5
|
import { useApp, useModal } from "../../state/AppContext.js";
|
|
6
6
|
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
7
7
|
import { ConfirmModal } from "./ConfirmModal.js";
|
|
8
|
+
import { FailureListModal } from "./FailureListModal.js";
|
|
8
9
|
import { HelpOverlay, helpOverlaySize } from "./HelpOverlay.js";
|
|
9
10
|
import { InputModal } from "./InputModal.js";
|
|
10
11
|
import { LoadingModal } from "./LoadingModal.js";
|
|
@@ -37,7 +38,7 @@ const isPlain = (key: KeyPress) => !key.ctrl && !key.meta;
|
|
|
37
38
|
* Only an <input> that was ALREADY focused when the key arrived:
|
|
38
39
|
* the Enter that opens an input modal from a select never reaches
|
|
39
40
|
* it (KeyRouter's `dispatchThenGuardFocus`).
|
|
40
|
-
* - message: any key dismisses.
|
|
41
|
+
* - message, failures: any key dismisses.
|
|
41
42
|
* - loading: keys are swallowed until the work finishes.
|
|
42
43
|
* - help: ↑↓ / PgUp / PgDn / Ctrl+U / Ctrl+D scroll; Esc or `?` close.
|
|
43
44
|
*/
|
|
@@ -102,6 +103,7 @@ export function ModalContainer() {
|
|
|
102
103
|
break;
|
|
103
104
|
|
|
104
105
|
case "message":
|
|
106
|
+
case "failures":
|
|
105
107
|
modal.onDismiss();
|
|
106
108
|
break;
|
|
107
109
|
|
|
@@ -174,11 +176,14 @@ export function ModalContainer() {
|
|
|
174
176
|
);
|
|
175
177
|
|
|
176
178
|
case "message":
|
|
179
|
+
return <MessageModal title={modal.title} message={modal.message} />;
|
|
180
|
+
|
|
181
|
+
case "failures":
|
|
177
182
|
return (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
<FailureListModal
|
|
184
|
+
report={modal.report}
|
|
185
|
+
terminalWidth={dimensions.terminalWidth}
|
|
186
|
+
terminalHeight={dimensions.terminalHeight}
|
|
182
187
|
/>
|
|
183
188
|
);
|
|
184
189
|
|
|
@@ -2,5 +2,6 @@ export { ConfirmModal } from "./ConfirmModal.js";
|
|
|
2
2
|
export { InputModal } from "./InputModal.js";
|
|
3
3
|
export { SelectModal } from "./SelectModal.js";
|
|
4
4
|
export { MessageModal } from "./MessageModal.js";
|
|
5
|
+
export { FailureListModal } from "./FailureListModal.js";
|
|
5
6
|
export { LoadingModal } from "./LoadingModal.js";
|
|
6
7
|
export { ModalContainer } from "./ModalContainer.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app shows next at startup, when the modal slot is free — the ONE
|
|
3
|
+
* order every startup popup and question is asked in. Pure, so the order is
|
|
4
|
+
* tested without mounting the app; `App.tsx`'s startup effect acts on it.
|
|
5
|
+
*
|
|
6
|
+
* The order:
|
|
7
|
+
*
|
|
8
|
+
* 1. Failure reports, oldest first. A failure is something that already
|
|
9
|
+
* happened, and the question after it may be answered differently once
|
|
10
|
+
* it is known — so the profile gate's refusals, queued in the same tick as
|
|
11
|
+
* the profile question, are read BEFORE it and never replace it.
|
|
12
|
+
* 2. The gitignore question, once no newer check is pending.
|
|
13
|
+
* 3. The broken-plugins question.
|
|
14
|
+
* 4. The profile question — last, and asked again whenever the slot is free
|
|
15
|
+
* and it has not been answered (it cannot be dismissed).
|
|
16
|
+
*
|
|
17
|
+
* Nothing is shown while any modal is open: two needs arriving in one commit
|
|
18
|
+
* would otherwise both see an empty slot, and the later would replace the
|
|
19
|
+
* earlier after its need was cleared.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { FailureReport } from "../state/types.js";
|
|
23
|
+
|
|
24
|
+
export interface StartupQueueState<ProfileNeed> {
|
|
25
|
+
/** A modal (any modal) is on screen. */
|
|
26
|
+
modalOpen: boolean;
|
|
27
|
+
/** Failure reports not yet shown, oldest first. */
|
|
28
|
+
reports: readonly FailureReport[];
|
|
29
|
+
/** A gitignore answer is waiting and no newer check is running. */
|
|
30
|
+
gitignoreReady: boolean;
|
|
31
|
+
/** Plugins found loading an older version than installed, or null. */
|
|
32
|
+
brokenCount: number | null;
|
|
33
|
+
/** The unanswered profile question, or null. */
|
|
34
|
+
profileNeed: ProfileNeed | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type StartupStep<ProfileNeed> =
|
|
38
|
+
| { kind: "report"; report: FailureReport; rest: FailureReport[] }
|
|
39
|
+
| { kind: "gitignore" }
|
|
40
|
+
| { kind: "stale-load"; brokenCount: number }
|
|
41
|
+
| { kind: "profile"; need: ProfileNeed }
|
|
42
|
+
| null;
|
|
43
|
+
|
|
44
|
+
export function nextStartupStep<ProfileNeed>(
|
|
45
|
+
s: StartupQueueState<ProfileNeed>,
|
|
46
|
+
): StartupStep<ProfileNeed> {
|
|
47
|
+
if (s.modalOpen) return null;
|
|
48
|
+
const [report, ...rest] = s.reports;
|
|
49
|
+
if (report) return { kind: "report", report, rest };
|
|
50
|
+
if (s.gitignoreReady) return { kind: "gitignore" };
|
|
51
|
+
if (s.brokenCount !== null) {
|
|
52
|
+
return { kind: "stale-load", brokenCount: s.brokenCount };
|
|
53
|
+
}
|
|
54
|
+
if (s.profileNeed !== null) return { kind: "profile", need: s.profileNeed };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
2
3
|
import { useApp, useNavigation } from "../state/AppContext.js";
|
|
3
|
-
import type { ModalState } from "../state/types.js";
|
|
4
|
+
import type { FailureReport, ModalState } from "../state/types.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Startup gitignore-violation modal: magus launches, detects a problem, and
|
|
@@ -88,8 +89,16 @@ export function useGitignoreModal() {
|
|
|
88
89
|
violationCount: number;
|
|
89
90
|
safeCount: number;
|
|
90
91
|
generated?: readonly string[];
|
|
91
|
-
/**
|
|
92
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Runs the fix and answers what it could not do — null when all of it
|
|
94
|
+
* applied. The Git State tab shows what changed; failures go into the
|
|
95
|
+
* one failure popup.
|
|
96
|
+
*/
|
|
97
|
+
onFixSafe: () =>
|
|
98
|
+
| Promise<FailureReport | null | undefined>
|
|
99
|
+
| FailureReport
|
|
100
|
+
| null
|
|
101
|
+
| undefined;
|
|
93
102
|
}) => {
|
|
94
103
|
if (args.violationCount === 0) return;
|
|
95
104
|
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
@@ -101,9 +110,9 @@ export function useGitignoreModal() {
|
|
|
101
110
|
// A loading modal, NOT HIDE_MODAL. The fix is async, and an empty
|
|
102
111
|
// modal slot is App's signal that the next startup question may be
|
|
103
112
|
// asked: hiding here let the profile question show during the await,
|
|
104
|
-
// be marked as asked, and then be replaced by
|
|
113
|
+
// be marked as asked, and then be replaced by the fix's result —
|
|
105
114
|
// lost for the whole session. The slot stays occupied until the
|
|
106
|
-
//
|
|
115
|
+
// fix settles.
|
|
107
116
|
dispatch({
|
|
108
117
|
type: "SHOW_MODAL",
|
|
109
118
|
modal: { type: "loading", message: "Fixing gitignore…" },
|
|
@@ -111,37 +120,28 @@ export function useGitignoreModal() {
|
|
|
111
120
|
// Called inside `.then`, not passed to `Promise.resolve`: a fixer
|
|
112
121
|
// that throws synchronously must land in `.catch` below too, or
|
|
113
122
|
// the loading modal above stays up and swallows every key.
|
|
123
|
+
const fail = (report: FailureReport) =>
|
|
124
|
+
dispatch({
|
|
125
|
+
type: "SHOW_MODAL",
|
|
126
|
+
modal: { type: "failures", report, onDismiss: dismiss },
|
|
127
|
+
});
|
|
114
128
|
void Promise.resolve()
|
|
115
129
|
.then(() => args.onFixSafe())
|
|
116
130
|
.then((report) => {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
dispatch({
|
|
124
|
-
type: "SHOW_MODAL",
|
|
125
|
-
modal: {
|
|
126
|
-
type: "message",
|
|
127
|
-
title: "Gitignore fixed",
|
|
128
|
-
message: report,
|
|
129
|
-
variant: "success",
|
|
130
|
-
onDismiss: dismiss,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
131
|
+
// The loading modal must go — it swallows every key — and the
|
|
132
|
+
// empty slot lets the next startup question in. What the fix
|
|
133
|
+
// could not do takes the slot first, in the failure popup.
|
|
134
|
+
if (report) fail(report);
|
|
135
|
+
else dispatch({ type: "HIDE_MODAL" });
|
|
133
136
|
})
|
|
134
137
|
.catch((err) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
onDismiss: dismiss,
|
|
143
|
-
},
|
|
144
|
-
});
|
|
138
|
+
fail(
|
|
139
|
+
stepFailureReport(
|
|
140
|
+
"The gitignore fixes were not applied",
|
|
141
|
+
".gitignore",
|
|
142
|
+
err,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
145
|
});
|
|
146
146
|
},
|
|
147
147
|
onOpenTab: () => {
|