@ridit/lens 0.1.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/LENS.md +25 -0
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/index.js +49363 -0
- package/package.json +38 -0
- package/src/colors.ts +1 -0
- package/src/commands/chat.tsx +23 -0
- package/src/commands/provider.tsx +224 -0
- package/src/commands/repo.tsx +120 -0
- package/src/commands/review.tsx +294 -0
- package/src/commands/task.tsx +36 -0
- package/src/commands/timeline.tsx +22 -0
- package/src/components/chat/ChatMessage.tsx +176 -0
- package/src/components/chat/ChatOverlays.tsx +329 -0
- package/src/components/chat/ChatRunner.tsx +732 -0
- package/src/components/provider/ApiKeyStep.tsx +243 -0
- package/src/components/provider/ModelStep.tsx +73 -0
- package/src/components/provider/ProviderTypeStep.tsx +54 -0
- package/src/components/provider/RemoveProviderStep.tsx +83 -0
- package/src/components/repo/DiffViewer.tsx +175 -0
- package/src/components/repo/FileReviewer.tsx +70 -0
- package/src/components/repo/FileViewer.tsx +60 -0
- package/src/components/repo/IssueFixer.tsx +666 -0
- package/src/components/repo/LensFileMenu.tsx +122 -0
- package/src/components/repo/NoProviderPrompt.tsx +28 -0
- package/src/components/repo/PreviewRunner.tsx +217 -0
- package/src/components/repo/ProviderPicker.tsx +76 -0
- package/src/components/repo/RepoAnalysis.tsx +343 -0
- package/src/components/repo/StepRow.tsx +69 -0
- package/src/components/task/TaskRunner.tsx +396 -0
- package/src/components/timeline/CommitDetail.tsx +274 -0
- package/src/components/timeline/CommitList.tsx +174 -0
- package/src/components/timeline/TimelineChat.tsx +167 -0
- package/src/components/timeline/TimelineRunner.tsx +1209 -0
- package/src/index.tsx +60 -0
- package/src/types/chat.ts +69 -0
- package/src/types/config.ts +20 -0
- package/src/types/repo.ts +42 -0
- package/src/utils/ai.ts +233 -0
- package/src/utils/chat.ts +833 -0
- package/src/utils/config.ts +61 -0
- package/src/utils/files.ts +104 -0
- package/src/utils/git.ts +155 -0
- package/src/utils/history.ts +86 -0
- package/src/utils/lensfile.ts +77 -0
- package/src/utils/llm.ts +81 -0
- package/src/utils/preview.ts +119 -0
- package/src/utils/repo.ts +69 -0
- package/src/utils/stats.ts +174 -0
- package/src/utils/thinking.tsx +191 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { ACCENT } from "../../colors";
|
|
9
|
+
import { callModelRaw } from "../../utils/ai";
|
|
10
|
+
import { DiffViewer, buildDiffs } from "./DiffViewer";
|
|
11
|
+
import type { DiffLine, FilePatch } from "./DiffViewer";
|
|
12
|
+
import type { Provider } from "../../types/config";
|
|
13
|
+
import type { AnalysisResult, ImportantFile } from "../../types/repo";
|
|
14
|
+
|
|
15
|
+
type FixStage =
|
|
16
|
+
| { type: "picking-issue" }
|
|
17
|
+
| {
|
|
18
|
+
type: "fixing";
|
|
19
|
+
issue: string;
|
|
20
|
+
progress?: { current: number; total: number };
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
type: "preview";
|
|
24
|
+
issue: string;
|
|
25
|
+
plan: FixPlan;
|
|
26
|
+
diffLines: DiffLine[][];
|
|
27
|
+
isFixAll: boolean;
|
|
28
|
+
remainingIssues: FixableIssue[];
|
|
29
|
+
scrollOffset: number;
|
|
30
|
+
}
|
|
31
|
+
| { type: "applying"; issue: string; plan: FixPlan }
|
|
32
|
+
| { type: "done"; applied: AppliedFix[]; remainingIssues: FixableIssue[] }
|
|
33
|
+
| {
|
|
34
|
+
type: "fix-all-summary";
|
|
35
|
+
allApplied: AppliedFix[];
|
|
36
|
+
failed: string[];
|
|
37
|
+
selectedFile: number;
|
|
38
|
+
scrollOffset: number;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: "viewing-file";
|
|
42
|
+
file: AppliedFix;
|
|
43
|
+
diffLines: DiffLine[];
|
|
44
|
+
scrollOffset: number;
|
|
45
|
+
returnTo: "done" | "fix-all-summary";
|
|
46
|
+
doneState?: { applied: AppliedFix[]; remainingIssues: FixableIssue[] };
|
|
47
|
+
summaryState?: { allApplied: AppliedFix[]; failed: string[] };
|
|
48
|
+
}
|
|
49
|
+
| { type: "error"; message: string };
|
|
50
|
+
|
|
51
|
+
type FixPlan = {
|
|
52
|
+
summary: string;
|
|
53
|
+
patches: FilePatch[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type AppliedFix = {
|
|
57
|
+
path: string;
|
|
58
|
+
isNew: boolean;
|
|
59
|
+
issueLabel: string;
|
|
60
|
+
patch: FilePatch;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type FixableIssue = {
|
|
64
|
+
label: string;
|
|
65
|
+
category: "security" | "config" | "suggestion";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function buildFixPrompt(
|
|
69
|
+
repoPath: string,
|
|
70
|
+
issue: string,
|
|
71
|
+
requestedFiles: ImportantFile[],
|
|
72
|
+
): string {
|
|
73
|
+
const fileList = requestedFiles
|
|
74
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
75
|
+
.join("\n\n");
|
|
76
|
+
|
|
77
|
+
return `You are a senior software engineer. You need to fix the following issue in a codebase:
|
|
78
|
+
|
|
79
|
+
Issue: ${issue}
|
|
80
|
+
|
|
81
|
+
Here are the relevant files:
|
|
82
|
+
|
|
83
|
+
${fileList}
|
|
84
|
+
|
|
85
|
+
Fix this issue by providing the complete new content for any files that need to be created or modified.
|
|
86
|
+
|
|
87
|
+
Respond ONLY with a JSON object (no markdown, no explanation) with this exact shape:
|
|
88
|
+
{
|
|
89
|
+
"summary": "1-2 sentence explanation of what you changed and why",
|
|
90
|
+
"patches": [
|
|
91
|
+
{
|
|
92
|
+
"path": "relative/path/to/file.ts",
|
|
93
|
+
"content": "complete new file content here",
|
|
94
|
+
"isNew": false
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
- Always provide the COMPLETE file content, not diffs or partial content
|
|
101
|
+
- isNew should be true only if you are creating a brand new file
|
|
102
|
+
- Only include files that actually need changes
|
|
103
|
+
- Keep changes minimal and focused on the issue
|
|
104
|
+
- Do not change unrelated code`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function applyPatches(
|
|
108
|
+
repoPath: string,
|
|
109
|
+
plan: FixPlan,
|
|
110
|
+
issueLabel: string,
|
|
111
|
+
): AppliedFix[] {
|
|
112
|
+
const applied: AppliedFix[] = [];
|
|
113
|
+
for (const patch of plan.patches) {
|
|
114
|
+
const fullPath = path.join(repoPath, patch.path);
|
|
115
|
+
const dir = path.dirname(fullPath);
|
|
116
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
117
|
+
writeFileSync(fullPath, patch.content, "utf-8");
|
|
118
|
+
applied.push({ path: patch.path, isNew: patch.isNew, issueLabel, patch });
|
|
119
|
+
}
|
|
120
|
+
return applied;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runFixAll(
|
|
124
|
+
repoPath: string,
|
|
125
|
+
issues: FixableIssue[],
|
|
126
|
+
requestedFiles: ImportantFile[],
|
|
127
|
+
provider: Provider,
|
|
128
|
+
onProgress: (current: number, total: number, issue: string) => void,
|
|
129
|
+
): Promise<{ allApplied: AppliedFix[]; failed: string[] }> {
|
|
130
|
+
const allApplied: AppliedFix[] = [];
|
|
131
|
+
const failed: string[] = [];
|
|
132
|
+
|
|
133
|
+
for (let idx = 0; idx < issues.length; idx++) {
|
|
134
|
+
const issue = issues[idx]!;
|
|
135
|
+
onProgress(idx + 1, issues.length, issue.label);
|
|
136
|
+
try {
|
|
137
|
+
const text = await callModelRaw(
|
|
138
|
+
provider,
|
|
139
|
+
buildFixPrompt(repoPath, issue.label, requestedFiles),
|
|
140
|
+
);
|
|
141
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
142
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
143
|
+
if (!match) throw new Error("No JSON in response");
|
|
144
|
+
const plan = JSON.parse(match[0]) as FixPlan;
|
|
145
|
+
const applied = applyPatches(repoPath, plan, issue.label);
|
|
146
|
+
allApplied.push(...applied);
|
|
147
|
+
} catch {
|
|
148
|
+
failed.push(issue.label);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { allApplied, failed };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const IssueFixer = ({
|
|
156
|
+
repoPath,
|
|
157
|
+
result,
|
|
158
|
+
requestedFiles,
|
|
159
|
+
provider,
|
|
160
|
+
onDone,
|
|
161
|
+
}: {
|
|
162
|
+
repoPath: string;
|
|
163
|
+
result: AnalysisResult;
|
|
164
|
+
requestedFiles: ImportantFile[];
|
|
165
|
+
provider: Provider;
|
|
166
|
+
onDone: () => void;
|
|
167
|
+
}) => {
|
|
168
|
+
const [stage, setStage] = useState<FixStage>({ type: "picking-issue" });
|
|
169
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
170
|
+
const [fixedLabels, setFixedLabels] = useState<Set<string>>(new Set());
|
|
171
|
+
|
|
172
|
+
const allIssues: FixableIssue[] = [
|
|
173
|
+
...result.securityIssues.map((s) => ({
|
|
174
|
+
label: s,
|
|
175
|
+
category: "security" as const,
|
|
176
|
+
})),
|
|
177
|
+
...result.missingConfigs.map((s) => ({
|
|
178
|
+
label: s,
|
|
179
|
+
category: "config" as const,
|
|
180
|
+
})),
|
|
181
|
+
...result.suggestions.map((s) => ({
|
|
182
|
+
label: s,
|
|
183
|
+
category: "suggestion" as const,
|
|
184
|
+
})),
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const fixableIssues = allIssues.filter((i) => !fixedLabels.has(i.label));
|
|
188
|
+
|
|
189
|
+
const markFixed = (labels: string[]) => {
|
|
190
|
+
setFixedLabels((prev) => {
|
|
191
|
+
const next = new Set(prev);
|
|
192
|
+
labels.forEach((l) => next.add(l));
|
|
193
|
+
return next;
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const FIX_ALL_INDEX = 0;
|
|
198
|
+
const totalOptions = fixableIssues.length + 1;
|
|
199
|
+
|
|
200
|
+
useInput((_, key) => {
|
|
201
|
+
if (stage.type === "picking-issue") {
|
|
202
|
+
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
|
203
|
+
if (key.downArrow)
|
|
204
|
+
setSelectedIndex((i) => Math.min(totalOptions - 1, i + 1));
|
|
205
|
+
if (key.escape) {
|
|
206
|
+
onDone();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (key.return) {
|
|
210
|
+
if (selectedIndex === FIX_ALL_INDEX) {
|
|
211
|
+
setStage({
|
|
212
|
+
type: "fixing",
|
|
213
|
+
issue: "all issues",
|
|
214
|
+
progress: { current: 0, total: fixableIssues.length },
|
|
215
|
+
});
|
|
216
|
+
runFixAll(
|
|
217
|
+
repoPath,
|
|
218
|
+
fixableIssues,
|
|
219
|
+
requestedFiles,
|
|
220
|
+
provider,
|
|
221
|
+
(current, total, issue) => {
|
|
222
|
+
setStage({ type: "fixing", issue, progress: { current, total } });
|
|
223
|
+
},
|
|
224
|
+
).then(({ allApplied, failed }) => {
|
|
225
|
+
const failedSet = new Set(failed);
|
|
226
|
+
markFixed(
|
|
227
|
+
fixableIssues
|
|
228
|
+
.filter((i) => !failedSet.has(i.label))
|
|
229
|
+
.map((i) => i.label),
|
|
230
|
+
);
|
|
231
|
+
setStage({
|
|
232
|
+
type: "fix-all-summary",
|
|
233
|
+
allApplied,
|
|
234
|
+
failed,
|
|
235
|
+
selectedFile: 0,
|
|
236
|
+
scrollOffset: 0,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const issue = fixableIssues[selectedIndex - 1];
|
|
242
|
+
if (!issue) return;
|
|
243
|
+
setStage({ type: "fixing", issue: issue.label });
|
|
244
|
+
callModelRaw(
|
|
245
|
+
provider,
|
|
246
|
+
buildFixPrompt(repoPath, issue.label, requestedFiles),
|
|
247
|
+
)
|
|
248
|
+
.then((text: string) => {
|
|
249
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
250
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
251
|
+
if (!match) throw new Error("No JSON in response");
|
|
252
|
+
const plan = JSON.parse(match[0]) as FixPlan;
|
|
253
|
+
const diffLines = buildDiffs(repoPath, plan.patches);
|
|
254
|
+
const remaining = fixableIssues.filter(
|
|
255
|
+
(_, i) => i !== selectedIndex - 1,
|
|
256
|
+
);
|
|
257
|
+
setStage({
|
|
258
|
+
type: "preview",
|
|
259
|
+
issue: issue.label,
|
|
260
|
+
plan,
|
|
261
|
+
diffLines,
|
|
262
|
+
isFixAll: false,
|
|
263
|
+
remainingIssues: remaining,
|
|
264
|
+
scrollOffset: 0,
|
|
265
|
+
});
|
|
266
|
+
})
|
|
267
|
+
.catch((err: unknown) =>
|
|
268
|
+
setStage({
|
|
269
|
+
type: "error",
|
|
270
|
+
message: err instanceof Error ? err.message : "Fix failed",
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (stage.type === "preview") {
|
|
278
|
+
if (key.escape) {
|
|
279
|
+
setStage({ type: "picking-issue" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (key.upArrow)
|
|
283
|
+
setStage({
|
|
284
|
+
...stage,
|
|
285
|
+
scrollOffset: Math.max(0, stage.scrollOffset - 1),
|
|
286
|
+
});
|
|
287
|
+
if (key.downArrow)
|
|
288
|
+
setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
289
|
+
if (key.return) {
|
|
290
|
+
const { plan, issue, remainingIssues } = stage;
|
|
291
|
+
setStage({ type: "applying", issue, plan });
|
|
292
|
+
try {
|
|
293
|
+
const applied = applyPatches(repoPath, plan, issue);
|
|
294
|
+
markFixed([issue]);
|
|
295
|
+
setSelectedIndex(0);
|
|
296
|
+
setStage({ type: "done", applied, remainingIssues });
|
|
297
|
+
} catch (err: unknown) {
|
|
298
|
+
setStage({
|
|
299
|
+
type: "error",
|
|
300
|
+
message:
|
|
301
|
+
err instanceof Error ? err.message : "Failed to write files",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (stage.type === "done") {
|
|
309
|
+
if (key.escape) {
|
|
310
|
+
onDone();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
|
|
314
|
+
if (key.downArrow)
|
|
315
|
+
setSelectedIndex((i) => Math.min(stage.applied.length - 1, i + 1));
|
|
316
|
+
if (key.return) {
|
|
317
|
+
const file = stage.applied[selectedIndex];
|
|
318
|
+
if (!file) return;
|
|
319
|
+
const diffLines = buildDiffs(repoPath, [file.patch])[0] ?? [];
|
|
320
|
+
setStage({
|
|
321
|
+
type: "viewing-file",
|
|
322
|
+
file,
|
|
323
|
+
diffLines,
|
|
324
|
+
scrollOffset: 0,
|
|
325
|
+
returnTo: "done",
|
|
326
|
+
doneState: {
|
|
327
|
+
applied: stage.applied,
|
|
328
|
+
remainingIssues: stage.remainingIssues,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (stage.type === "fix-all-summary") {
|
|
336
|
+
if (key.escape) {
|
|
337
|
+
onDone();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (key.upArrow)
|
|
341
|
+
setStage({
|
|
342
|
+
...stage,
|
|
343
|
+
selectedFile: Math.max(0, stage.selectedFile - 1),
|
|
344
|
+
});
|
|
345
|
+
if (key.downArrow)
|
|
346
|
+
setStage({
|
|
347
|
+
...stage,
|
|
348
|
+
selectedFile: Math.min(
|
|
349
|
+
stage.allApplied.length - 1,
|
|
350
|
+
stage.selectedFile + 1,
|
|
351
|
+
),
|
|
352
|
+
});
|
|
353
|
+
if (key.return) {
|
|
354
|
+
const file = stage.allApplied[stage.selectedFile];
|
|
355
|
+
if (!file) return;
|
|
356
|
+
const diffLines = buildDiffs(repoPath, [file.patch])[0] ?? [];
|
|
357
|
+
setStage({
|
|
358
|
+
type: "viewing-file",
|
|
359
|
+
file,
|
|
360
|
+
diffLines,
|
|
361
|
+
scrollOffset: 0,
|
|
362
|
+
returnTo: "fix-all-summary",
|
|
363
|
+
summaryState: { allApplied: stage.allApplied, failed: stage.failed },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (stage.type === "viewing-file") {
|
|
370
|
+
if (key.upArrow)
|
|
371
|
+
setStage({
|
|
372
|
+
...stage,
|
|
373
|
+
scrollOffset: Math.max(0, stage.scrollOffset - 1),
|
|
374
|
+
});
|
|
375
|
+
if (key.downArrow)
|
|
376
|
+
setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
377
|
+
if (key.escape || key.return) {
|
|
378
|
+
if (stage.returnTo === "done" && stage.doneState) {
|
|
379
|
+
setStage({ type: "done", ...stage.doneState });
|
|
380
|
+
setSelectedIndex(0);
|
|
381
|
+
} else if (stage.returnTo === "fix-all-summary" && stage.summaryState) {
|
|
382
|
+
setStage({
|
|
383
|
+
type: "fix-all-summary",
|
|
384
|
+
...stage.summaryState,
|
|
385
|
+
selectedFile: 0,
|
|
386
|
+
scrollOffset: 0,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (stage.type === "error") {
|
|
394
|
+
if (key.return || key.escape) onDone();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (stage.type === "picking-issue") {
|
|
399
|
+
const allDone = fixableIssues.length === 0;
|
|
400
|
+
return (
|
|
401
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
402
|
+
<Box gap={2}>
|
|
403
|
+
<Text bold color="cyan">
|
|
404
|
+
⚙ Issues
|
|
405
|
+
</Text>
|
|
406
|
+
{fixedLabels.size > 0 && (
|
|
407
|
+
<Text color="gray">
|
|
408
|
+
{figures.tick} {fixedLabels.size} fixed
|
|
409
|
+
{allDone
|
|
410
|
+
? " · all done!"
|
|
411
|
+
: ` · ${fixableIssues.length} remaining`}
|
|
412
|
+
</Text>
|
|
413
|
+
)}
|
|
414
|
+
</Box>
|
|
415
|
+
{allIssues
|
|
416
|
+
.filter((i) => fixedLabels.has(i.label))
|
|
417
|
+
.map((issue, i) => (
|
|
418
|
+
<Box key={`fixed-${i}`} marginLeft={1}>
|
|
419
|
+
<Text color="gray">
|
|
420
|
+
{figures.tick}
|
|
421
|
+
{" "}
|
|
422
|
+
{issue.label}
|
|
423
|
+
</Text>
|
|
424
|
+
</Box>
|
|
425
|
+
))}
|
|
426
|
+
{allDone ? (
|
|
427
|
+
<>
|
|
428
|
+
<Text color="green">{figures.tick} All issues fixed!</Text>
|
|
429
|
+
<Text color="gray">esc to go back</Text>
|
|
430
|
+
</>
|
|
431
|
+
) : (
|
|
432
|
+
<>
|
|
433
|
+
<Box marginLeft={1}>
|
|
434
|
+
<Text color={selectedIndex === FIX_ALL_INDEX ? "cyan" : "white"}>
|
|
435
|
+
{selectedIndex === FIX_ALL_INDEX ? figures.arrowRight : " "}
|
|
436
|
+
{" "}
|
|
437
|
+
<Text bold>Fix all remaining</Text>
|
|
438
|
+
<Text color="gray">
|
|
439
|
+
{" "}
|
|
440
|
+
{fixableIssues.length} issues · no preview
|
|
441
|
+
</Text>
|
|
442
|
+
</Text>
|
|
443
|
+
</Box>
|
|
444
|
+
{fixableIssues.map((issue, i) => {
|
|
445
|
+
const isSelected = i + 1 === selectedIndex;
|
|
446
|
+
const color =
|
|
447
|
+
issue.category === "security"
|
|
448
|
+
? "red"
|
|
449
|
+
: issue.category === "config"
|
|
450
|
+
? "yellow"
|
|
451
|
+
: "white";
|
|
452
|
+
return (
|
|
453
|
+
<Box key={i} marginLeft={1}>
|
|
454
|
+
<Text color={isSelected ? "cyan" : color}>
|
|
455
|
+
{isSelected ? figures.arrowRight : " "}
|
|
456
|
+
{" "}
|
|
457
|
+
{issue.label}
|
|
458
|
+
</Text>
|
|
459
|
+
</Box>
|
|
460
|
+
);
|
|
461
|
+
})}
|
|
462
|
+
<Text color="gray">
|
|
463
|
+
↑↓ navigate · enter to fix · esc to go back
|
|
464
|
+
</Text>
|
|
465
|
+
</>
|
|
466
|
+
)}
|
|
467
|
+
</Box>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (stage.type === "fixing") {
|
|
472
|
+
const { progress } = stage;
|
|
473
|
+
return (
|
|
474
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
475
|
+
<Box>
|
|
476
|
+
<Text color={ACCENT}>
|
|
477
|
+
<Spinner />
|
|
478
|
+
</Text>
|
|
479
|
+
<Box marginLeft={1}>
|
|
480
|
+
{progress ? (
|
|
481
|
+
<Text>
|
|
482
|
+
[{progress.current}/{progress.total}] Fixing:{" "}
|
|
483
|
+
<Text color="cyan">{stage.issue}</Text>
|
|
484
|
+
</Text>
|
|
485
|
+
) : (
|
|
486
|
+
<Text>
|
|
487
|
+
Generating fix for: <Text color="cyan">{stage.issue}</Text>
|
|
488
|
+
</Text>
|
|
489
|
+
)}
|
|
490
|
+
</Box>
|
|
491
|
+
</Box>
|
|
492
|
+
{progress && (
|
|
493
|
+
<Box marginLeft={2}>
|
|
494
|
+
<Text color="gray">
|
|
495
|
+
{"█".repeat(progress.current)}
|
|
496
|
+
{"░".repeat(progress.total - progress.current)}{" "}
|
|
497
|
+
{Math.round((progress.current / progress.total) * 100)}%
|
|
498
|
+
</Text>
|
|
499
|
+
</Box>
|
|
500
|
+
)}
|
|
501
|
+
</Box>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (stage.type === "preview") {
|
|
506
|
+
const { plan, diffLines, scrollOffset } = stage;
|
|
507
|
+
return (
|
|
508
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
509
|
+
<Text bold color="cyan">
|
|
510
|
+
{figures.info} Fix Preview
|
|
511
|
+
</Text>
|
|
512
|
+
<Text color="white">{plan.summary}</Text>
|
|
513
|
+
<Box flexDirection="column" marginTop={1}>
|
|
514
|
+
<DiffViewer
|
|
515
|
+
patches={plan.patches}
|
|
516
|
+
diffs={diffLines}
|
|
517
|
+
scrollOffset={scrollOffset}
|
|
518
|
+
/>
|
|
519
|
+
</Box>
|
|
520
|
+
<Text color="gray">↑↓ scroll · enter to apply · esc to go back</Text>
|
|
521
|
+
</Box>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (stage.type === "applying") {
|
|
526
|
+
return (
|
|
527
|
+
<Box marginTop={1}>
|
|
528
|
+
<Text color={ACCENT}>
|
|
529
|
+
<Spinner />
|
|
530
|
+
</Text>
|
|
531
|
+
<Box marginLeft={1}>
|
|
532
|
+
<Text>Applying fixes...</Text>
|
|
533
|
+
</Box>
|
|
534
|
+
</Box>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (stage.type === "done") {
|
|
539
|
+
return (
|
|
540
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
541
|
+
<Text bold color="green">
|
|
542
|
+
{figures.tick} Fix applied
|
|
543
|
+
</Text>
|
|
544
|
+
{stage.applied.map((f, i) => {
|
|
545
|
+
const isSelected = i === selectedIndex;
|
|
546
|
+
return (
|
|
547
|
+
<Box key={f.path} marginLeft={1}>
|
|
548
|
+
<Text color={isSelected ? "cyan" : "green"}>
|
|
549
|
+
{isSelected
|
|
550
|
+
? figures.arrowRight
|
|
551
|
+
: f.isNew
|
|
552
|
+
? figures.tick
|
|
553
|
+
: figures.bullet}{" "}
|
|
554
|
+
{f.path}
|
|
555
|
+
{isSelected && <Text color="gray"> · enter to view diff</Text>}
|
|
556
|
+
</Text>
|
|
557
|
+
</Box>
|
|
558
|
+
);
|
|
559
|
+
})}
|
|
560
|
+
{stage.remainingIssues.length > 0 && (
|
|
561
|
+
<Text color="gray">
|
|
562
|
+
{figures.info} {stage.remainingIssues.length} issue(s) remaining ·
|
|
563
|
+
esc to go back to list
|
|
564
|
+
</Text>
|
|
565
|
+
)}
|
|
566
|
+
<Text color="gray">
|
|
567
|
+
↑↓ navigate · enter to view diff · esc to go back
|
|
568
|
+
</Text>
|
|
569
|
+
</Box>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (stage.type === "fix-all-summary") {
|
|
574
|
+
const { allApplied, failed, selectedFile } = stage;
|
|
575
|
+
return (
|
|
576
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
577
|
+
<Text bold color="green">
|
|
578
|
+
{figures.tick} Fix all complete
|
|
579
|
+
</Text>
|
|
580
|
+
<Text color="gray">
|
|
581
|
+
{allApplied.length} file(s) written · {failed.length} issue(s) failed
|
|
582
|
+
</Text>
|
|
583
|
+
{allApplied.length > 0 && (
|
|
584
|
+
<Box flexDirection="column" gap={0}>
|
|
585
|
+
<Text color="gray">Applied:</Text>
|
|
586
|
+
{allApplied.map((f, i) => {
|
|
587
|
+
const isSelected = i === selectedFile;
|
|
588
|
+
return (
|
|
589
|
+
<Box key={i} marginLeft={1}>
|
|
590
|
+
<Text color={isSelected ? "cyan" : "green"}>
|
|
591
|
+
{isSelected
|
|
592
|
+
? figures.arrowRight
|
|
593
|
+
: f.isNew
|
|
594
|
+
? figures.tick
|
|
595
|
+
: figures.bullet}{" "}
|
|
596
|
+
{f.path}
|
|
597
|
+
<Text color="gray">
|
|
598
|
+
{isSelected
|
|
599
|
+
? " · enter to view diff"
|
|
600
|
+
: ` ← ${f.issueLabel.slice(0, 40)}${f.issueLabel.length > 40 ? "…" : ""}`}
|
|
601
|
+
</Text>
|
|
602
|
+
</Text>
|
|
603
|
+
</Box>
|
|
604
|
+
);
|
|
605
|
+
})}
|
|
606
|
+
</Box>
|
|
607
|
+
)}
|
|
608
|
+
{failed.length > 0 && (
|
|
609
|
+
<Box flexDirection="column" gap={0}>
|
|
610
|
+
<Text color="red">Failed:</Text>
|
|
611
|
+
{failed.map((label, i) => (
|
|
612
|
+
<Box key={i} marginLeft={1}>
|
|
613
|
+
<Text color="red">
|
|
614
|
+
{figures.cross} {label}
|
|
615
|
+
</Text>
|
|
616
|
+
</Box>
|
|
617
|
+
))}
|
|
618
|
+
</Box>
|
|
619
|
+
)}
|
|
620
|
+
<Text color="gray">
|
|
621
|
+
↑↓ navigate · enter to view diff · esc to go back
|
|
622
|
+
</Text>
|
|
623
|
+
</Box>
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (stage.type === "viewing-file") {
|
|
628
|
+
const { file, diffLines, scrollOffset: viewScroll } = stage;
|
|
629
|
+
return (
|
|
630
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
631
|
+
<Box gap={1}>
|
|
632
|
+
<Text bold color="cyan">
|
|
633
|
+
{figures.info}
|
|
634
|
+
</Text>
|
|
635
|
+
<Text bold color="cyan">
|
|
636
|
+
{file.path}
|
|
637
|
+
</Text>
|
|
638
|
+
<Text color="gray">{file.isNew ? "(new file)" : "(modified)"}</Text>
|
|
639
|
+
</Box>
|
|
640
|
+
<Text color="gray" dimColor>
|
|
641
|
+
{file.issueLabel.slice(0, 80)}
|
|
642
|
+
{file.issueLabel.length > 80 ? "…" : ""}
|
|
643
|
+
</Text>
|
|
644
|
+
<DiffViewer
|
|
645
|
+
patches={[file.patch]}
|
|
646
|
+
diffs={[diffLines]}
|
|
647
|
+
scrollOffset={viewScroll}
|
|
648
|
+
/>
|
|
649
|
+
<Text color="gray">↑↓ scroll · esc or enter to go back</Text>
|
|
650
|
+
</Box>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (stage.type === "error") {
|
|
655
|
+
return (
|
|
656
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
657
|
+
<Text color="red">
|
|
658
|
+
{figures.cross} {stage.message}
|
|
659
|
+
</Text>
|
|
660
|
+
<Text color="gray">enter or esc to go back</Text>
|
|
661
|
+
</Box>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return null;
|
|
666
|
+
};
|