@ridit/lens 0.2.2 → 0.2.5
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 +18 -11
- package/README.md +91 -0
- package/dist/index.mjs +69273 -2244
- package/package.json +7 -3
- package/src/commands/commit.tsx +713 -0
- package/src/components/chat/ChatOverlays.tsx +14 -4
- package/src/components/chat/ChatRunner.tsx +197 -30
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +505 -422
- package/src/index.tsx +41 -0
- package/src/prompts/fewshot.ts +100 -3
- package/src/prompts/system.ts +16 -20
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +29 -0
- package/src/utils/chat.ts +8 -18
- package/src/utils/memory.ts +7 -12
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +9 -32
- package/src/utils/tools/registry.ts +3 -59
- package/hello.py +0 -51
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { ACCENT } from "../colors";
|
|
9
|
+
import { ProviderPicker } from "../components/repo/ProviderPicker";
|
|
10
|
+
import { callChat } from "../utils/chat";
|
|
11
|
+
import type { Provider } from "../types/config";
|
|
12
|
+
import type { Message } from "../types/chat";
|
|
13
|
+
|
|
14
|
+
function gitRun(cmd: string, cwd: string): { ok: boolean; out: string } {
|
|
15
|
+
try {
|
|
16
|
+
const out = execSync(cmd, {
|
|
17
|
+
cwd,
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
20
|
+
timeout: 30_000,
|
|
21
|
+
}).trim();
|
|
22
|
+
return { ok: true, out };
|
|
23
|
+
} catch (e: any) {
|
|
24
|
+
const msg =
|
|
25
|
+
[e.stdout, e.stderr].filter(Boolean).join("\n").trim() || e.message;
|
|
26
|
+
return { ok: false, out: msg };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Commit with a multi-line message correctly.
|
|
32
|
+
* Uses execFileSync with an args array — no shell, no quoting issues.
|
|
33
|
+
* Splits the message into paragraphs and passes each as a separate -m flag
|
|
34
|
+
* so git formats the commit body properly (blank line between subject and body).
|
|
35
|
+
*/
|
|
36
|
+
function gitCommit(message: string, cwd: string): { ok: boolean; out: string } {
|
|
37
|
+
const { execFileSync } =
|
|
38
|
+
require("child_process") as typeof import("child_process");
|
|
39
|
+
|
|
40
|
+
const paragraphs = message
|
|
41
|
+
.split(/\n\n+/)
|
|
42
|
+
.map((p) => p.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
|
|
45
|
+
const mArgs: string[] = [];
|
|
46
|
+
for (const p of paragraphs) {
|
|
47
|
+
mArgs.push("-m", p);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const out = execFileSync("git", ["commit", ...mArgs], {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
55
|
+
timeout: 30_000,
|
|
56
|
+
}).trim();
|
|
57
|
+
return { ok: true, out };
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
const msg =
|
|
60
|
+
[e.stdout, e.stderr].filter(Boolean).join("\n").trim() || e.message;
|
|
61
|
+
return { ok: false, out: msg };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Stage specific files or everything (-A) */
|
|
66
|
+
function stageFiles(
|
|
67
|
+
files: string[],
|
|
68
|
+
cwd: string,
|
|
69
|
+
): { ok: boolean; out: string } {
|
|
70
|
+
if (files.length === 0) return gitRun("git add -A", cwd);
|
|
71
|
+
|
|
72
|
+
const paths = files.map((f) => `"${f}"`).join(" ");
|
|
73
|
+
return gitRun(`git add -- ${paths}`, cwd);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getStagedDiff(cwd: string): string {
|
|
77
|
+
return gitRun("git diff --staged", cwd).out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Diff only the specified files (unstaged) */
|
|
81
|
+
function getFileDiff(files: string[], cwd: string): string {
|
|
82
|
+
if (files.length === 0) {
|
|
83
|
+
const tracked = gitRun("git diff HEAD", cwd).out;
|
|
84
|
+
const untracked = gitRun("git ls-files --others --exclude-standard", cwd)
|
|
85
|
+
.out.split("\n")
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.slice(0, 10)
|
|
88
|
+
.map((f) => `=== new file: ${f} ===`)
|
|
89
|
+
.join("\n");
|
|
90
|
+
return [tracked, untracked].filter(Boolean).join("\n\n");
|
|
91
|
+
}
|
|
92
|
+
const paths = files.map((f) => `"${f}"`).join(" ");
|
|
93
|
+
return gitRun(`git diff HEAD -- ${paths}`, cwd).out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hasStagedChanges(cwd: string): boolean {
|
|
97
|
+
return !gitRun("git diff --staged --quiet", cwd).ok;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasAnyChanges(cwd: string): boolean {
|
|
101
|
+
return gitRun("git status --porcelain", cwd).out.trim().length > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Validate that all specified files exist on disk */
|
|
105
|
+
function validateFiles(
|
|
106
|
+
files: string[],
|
|
107
|
+
cwd: string,
|
|
108
|
+
): { missing: string[]; valid: string[] } {
|
|
109
|
+
const missing: string[] = [];
|
|
110
|
+
const valid: string[] = [];
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
const abs = path.isAbsolute(f) ? f : path.join(cwd, f);
|
|
113
|
+
if (existsSync(abs)) {
|
|
114
|
+
valid.push(path.relative(cwd, abs).replace(/\\/g, "/"));
|
|
115
|
+
} else {
|
|
116
|
+
missing.push(f);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { missing, valid };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function detectSplitOpportunity(diff: string): string[] {
|
|
123
|
+
const fileMatches = [...diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)];
|
|
124
|
+
const files = fileMatches.map((m) => m[1]!);
|
|
125
|
+
if (files.length <= 3) return [];
|
|
126
|
+
|
|
127
|
+
const groups = new Map<string, string[]>();
|
|
128
|
+
for (const f of files) {
|
|
129
|
+
const parts = f.split("/");
|
|
130
|
+
const group =
|
|
131
|
+
parts[0] === "src" && parts.length > 1 ? parts[1]! : parts[0]!;
|
|
132
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
133
|
+
groups.get(group)!.push(f);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const meaningful = [...groups.entries()].filter(([, fs]) => fs.length >= 2);
|
|
137
|
+
return meaningful.length >= 2
|
|
138
|
+
? meaningful.map(([g, fs]) => `${g}/ (${fs.length} files)`)
|
|
139
|
+
: [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const SYSTEM_PROMPT = `You are an expert at writing conventional commit messages.
|
|
143
|
+
Given a git diff, analyze the changes and write a single commit message.
|
|
144
|
+
|
|
145
|
+
Rules:
|
|
146
|
+
- Use conventional commits format: type(scope): description
|
|
147
|
+
- Types: feat, fix, refactor, perf, docs, style, test, chore, ci, build
|
|
148
|
+
- First line: max 72 chars, imperative mood (add, fix, update — not added/fixed)
|
|
149
|
+
- After the first line, add a blank line then bullet points for each logical change
|
|
150
|
+
- Bullet format: "- <what changed and why>"
|
|
151
|
+
- Group related changes into 2–5 bullets max
|
|
152
|
+
- Be specific — mention file names, feature names, component names
|
|
153
|
+
- No markdown, no backticks, no code blocks
|
|
154
|
+
- Output ONLY the commit message, nothing else
|
|
155
|
+
|
|
156
|
+
Example output:
|
|
157
|
+
feat(editor): add syntax highlighting for TypeScript
|
|
158
|
+
|
|
159
|
+
- add Monaco tokenizer for .ts and .tsx files
|
|
160
|
+
- configure theme tokens to match dark mode palette
|
|
161
|
+
- expose highlight API for external extensions`;
|
|
162
|
+
|
|
163
|
+
async function generateCommitMessage(
|
|
164
|
+
provider: Provider,
|
|
165
|
+
diff: string,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const msgs: Message[] = [
|
|
168
|
+
{
|
|
169
|
+
role: "user",
|
|
170
|
+
content: `Write a conventional commit message for this diff:\n\n${diff.slice(0, 8000)}`,
|
|
171
|
+
type: "text",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
const raw = await callChat(provider, SYSTEM_PROMPT, msgs);
|
|
175
|
+
return typeof raw === "string" ? raw.trim() : "chore: update files";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function trunc(s: string, n: number) {
|
|
179
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const PHRASES = [
|
|
183
|
+
"reading your crimes…",
|
|
184
|
+
"sniffing the diff…",
|
|
185
|
+
"crafting the perfect message…",
|
|
186
|
+
"turning chaos into conventional commits…",
|
|
187
|
+
"pretending this was intentional…",
|
|
188
|
+
"72 chars or bust…",
|
|
189
|
+
"making main proud…",
|
|
190
|
+
"git blame: not it…",
|
|
191
|
+
"this commit brought to you by AI…",
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function randomPhrase() {
|
|
195
|
+
return PHRASES[Math.floor(Math.random() * PHRASES.length)]!;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type Phase =
|
|
199
|
+
| { type: "checking" }
|
|
200
|
+
| { type: "no-changes" }
|
|
201
|
+
| { type: "no-staged"; hasUnstaged: boolean; files: string[] }
|
|
202
|
+
| { type: "staging"; files: string[] }
|
|
203
|
+
| { type: "generating" }
|
|
204
|
+
| { type: "preview"; message: string; splitGroups: string[]; diff: string }
|
|
205
|
+
| { type: "editing"; message: string; diff: string }
|
|
206
|
+
| { type: "committing"; message: string }
|
|
207
|
+
| { type: "pushing"; message: string; hash: string }
|
|
208
|
+
| { type: "done"; message: string; hash: string; pushed: boolean }
|
|
209
|
+
| { type: "preview-only"; message: string }
|
|
210
|
+
| { type: "error"; message: string };
|
|
211
|
+
|
|
212
|
+
function CommitRunner({
|
|
213
|
+
cwd,
|
|
214
|
+
provider,
|
|
215
|
+
files,
|
|
216
|
+
auto,
|
|
217
|
+
preview,
|
|
218
|
+
push,
|
|
219
|
+
confirm,
|
|
220
|
+
}: {
|
|
221
|
+
cwd: string;
|
|
222
|
+
provider: Provider;
|
|
223
|
+
/** Specific files to stage. Empty = all (-A when --auto, or use existing staged) */
|
|
224
|
+
files: string[];
|
|
225
|
+
auto: boolean;
|
|
226
|
+
preview: boolean;
|
|
227
|
+
push: boolean;
|
|
228
|
+
/** Show preview even with --auto before committing */
|
|
229
|
+
confirm: boolean;
|
|
230
|
+
}) {
|
|
231
|
+
const [phase, setPhase] = useState<Phase>({ type: "checking" });
|
|
232
|
+
const [phraseText, setPhraseText] = useState(randomPhrase());
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (phase.type !== "generating") return;
|
|
236
|
+
const id = setInterval(() => setPhraseText(randomPhrase()), 2800);
|
|
237
|
+
return () => clearInterval(id);
|
|
238
|
+
}, [phase.type]);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
(async () => {
|
|
242
|
+
if (!gitRun("git rev-parse --git-dir", cwd).ok) {
|
|
243
|
+
setPhase({ type: "error", message: "not a git repository" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (files.length > 0) {
|
|
248
|
+
const { missing, valid } = validateFiles(files, cwd);
|
|
249
|
+
if (missing.length > 0) {
|
|
250
|
+
setPhase({
|
|
251
|
+
type: "error",
|
|
252
|
+
message: `file${missing.length > 1 ? "s" : ""} not found:\n${missing.map((f) => ` ${f}`).join("\n")}`,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setPhase({ type: "staging", files: valid });
|
|
258
|
+
const r = stageFiles(valid, cwd);
|
|
259
|
+
if (!r.ok) {
|
|
260
|
+
setPhase({ type: "error", message: `staging failed: ${r.out}` });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
} else if (auto) {
|
|
264
|
+
if (!hasAnyChanges(cwd)) {
|
|
265
|
+
setPhase({ type: "no-changes" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
setPhase({ type: "staging", files: [] });
|
|
269
|
+
gitRun("git add -A", cwd);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!hasStagedChanges(cwd)) {
|
|
273
|
+
const unstaged = hasAnyChanges(cwd);
|
|
274
|
+
setPhase({ type: "no-staged", hasUnstaged: unstaged, files });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const diff = getStagedDiff(cwd) || getFileDiff(files, cwd);
|
|
279
|
+
if (!diff.trim()) {
|
|
280
|
+
setPhase({ type: "no-changes" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
setPhase({ type: "generating" });
|
|
285
|
+
|
|
286
|
+
const commitAndMaybePush = (message: string) => {
|
|
287
|
+
setPhase({ type: "committing", message });
|
|
288
|
+
const r = gitCommit(message, cwd);
|
|
289
|
+
if (!r.ok) {
|
|
290
|
+
setPhase({ type: "error", message: r.out });
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const hash = gitRun("git rev-parse --short HEAD", cwd).out || "?";
|
|
294
|
+
if (push) {
|
|
295
|
+
setPhase({ type: "pushing", message, hash });
|
|
296
|
+
const pr = gitRun("git push", cwd);
|
|
297
|
+
if (!pr.ok) {
|
|
298
|
+
setPhase({ type: "error", message: `push failed: ${pr.out}` });
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
setPhase({ type: "done", message, hash, pushed: push });
|
|
303
|
+
return true;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const message = await generateCommitMessage(provider, diff);
|
|
308
|
+
const splitGroups = detectSplitOpportunity(diff);
|
|
309
|
+
|
|
310
|
+
if (preview) {
|
|
311
|
+
setPhase({ type: "preview-only", message });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (auto && files.length === 0 && !confirm) {
|
|
316
|
+
commitAndMaybePush(message);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
setPhase({ type: "preview", message, splitGroups, diff });
|
|
321
|
+
} catch (e: any) {
|
|
322
|
+
setPhase({
|
|
323
|
+
type: "error",
|
|
324
|
+
message: `AI error: ${e.message ?? String(e)}`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
useInput((inp, key) => {
|
|
331
|
+
if (phase.type === "preview") {
|
|
332
|
+
if (inp === "y" || inp === "Y" || key.return) {
|
|
333
|
+
const message = phase.message;
|
|
334
|
+
setPhase({ type: "committing", message });
|
|
335
|
+
const r = gitCommit(message, cwd);
|
|
336
|
+
if (!r.ok) {
|
|
337
|
+
setPhase({ type: "error", message: r.out });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const hash = gitRun("git rev-parse --short HEAD", cwd).out || "?";
|
|
341
|
+
if (push) {
|
|
342
|
+
setPhase({ type: "pushing", message, hash });
|
|
343
|
+
const pr = gitRun("git push", cwd);
|
|
344
|
+
if (!pr.ok) {
|
|
345
|
+
setPhase({ type: "error", message: `push failed: ${pr.out}` });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
setPhase({ type: "done", message, hash, pushed: push });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (inp === "e" || inp === "E") {
|
|
353
|
+
setPhase({ type: "editing", message: phase.message, diff: phase.diff });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (inp === "n" || inp === "N" || key.escape) {
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (phase.type === "editing" && key.escape) {
|
|
362
|
+
setPhase((prev) =>
|
|
363
|
+
prev.type === "editing"
|
|
364
|
+
? {
|
|
365
|
+
type: "preview",
|
|
366
|
+
message: prev.message,
|
|
367
|
+
splitGroups: [],
|
|
368
|
+
diff: prev.diff,
|
|
369
|
+
}
|
|
370
|
+
: prev,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (
|
|
375
|
+
(phase.type === "done" ||
|
|
376
|
+
phase.type === "no-changes" ||
|
|
377
|
+
phase.type === "no-staged" ||
|
|
378
|
+
phase.type === "preview-only" ||
|
|
379
|
+
phase.type === "error") &&
|
|
380
|
+
(key.return || key.escape || inp === "q")
|
|
381
|
+
) {
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const w = process.stdout.columns ?? 80;
|
|
387
|
+
const div = "─".repeat(w);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<Box flexDirection="column" paddingY={1}>
|
|
391
|
+
<Box gap={2} marginBottom={1}>
|
|
392
|
+
<Text color={ACCENT} bold>
|
|
393
|
+
◈ COMMIT
|
|
394
|
+
</Text>
|
|
395
|
+
<Text color="gray" dimColor>
|
|
396
|
+
{cwd}
|
|
397
|
+
</Text>
|
|
398
|
+
{files.length > 0 && (
|
|
399
|
+
<Text color="cyan" dimColor>
|
|
400
|
+
{files.length} file{files.length !== 1 ? "s" : ""}
|
|
401
|
+
</Text>
|
|
402
|
+
)}
|
|
403
|
+
</Box>
|
|
404
|
+
|
|
405
|
+
{files.length > 0 && (
|
|
406
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
407
|
+
{files.map((f, i) => (
|
|
408
|
+
<Box key={i} gap={1}>
|
|
409
|
+
<Text color="gray" dimColor>
|
|
410
|
+
{" ·"}
|
|
411
|
+
</Text>
|
|
412
|
+
<Text color="white">{f}</Text>
|
|
413
|
+
</Box>
|
|
414
|
+
))}
|
|
415
|
+
</Box>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
<Text color="gray" dimColor>
|
|
419
|
+
{div}
|
|
420
|
+
</Text>
|
|
421
|
+
|
|
422
|
+
{phase.type === "checking" && (
|
|
423
|
+
<Box gap={1} marginTop={1}>
|
|
424
|
+
<Text color={ACCENT}>*</Text>
|
|
425
|
+
<Text color="gray" dimColor>
|
|
426
|
+
checking changes…
|
|
427
|
+
</Text>
|
|
428
|
+
</Box>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
{phase.type === "staging" && (
|
|
432
|
+
<Box gap={1} marginTop={1}>
|
|
433
|
+
<Text color={ACCENT}>*</Text>
|
|
434
|
+
<Text color="gray" dimColor>
|
|
435
|
+
{phase.files.length > 0
|
|
436
|
+
? `staging ${phase.files.length} file${phase.files.length !== 1 ? "s" : ""}…`
|
|
437
|
+
: "staging all changes…"}
|
|
438
|
+
</Text>
|
|
439
|
+
</Box>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
{phase.type === "no-changes" && (
|
|
443
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
444
|
+
<Box gap={1}>
|
|
445
|
+
<Text color="yellow">{figures.warning}</Text>
|
|
446
|
+
<Text color="white">nothing to commit — working tree is clean</Text>
|
|
447
|
+
</Box>
|
|
448
|
+
</Box>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{phase.type === "no-staged" && (
|
|
452
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
453
|
+
<Box gap={1}>
|
|
454
|
+
<Text color="yellow">{figures.warning}</Text>
|
|
455
|
+
<Text color="white">no staged changes found</Text>
|
|
456
|
+
</Box>
|
|
457
|
+
{phase.hasUnstaged && (
|
|
458
|
+
<Box flexDirection="column" marginLeft={2} gap={1}>
|
|
459
|
+
<Text color="gray" dimColor>
|
|
460
|
+
you have unstaged changes. try:
|
|
461
|
+
</Text>
|
|
462
|
+
{phase.files.length > 0 ? (
|
|
463
|
+
<Text color="gray" dimColor>
|
|
464
|
+
{" "}
|
|
465
|
+
<Text color={ACCENT}>
|
|
466
|
+
lens commit {phase.files.join(" ")}
|
|
467
|
+
</Text>
|
|
468
|
+
{" "}(stages and commits those files)
|
|
469
|
+
</Text>
|
|
470
|
+
) : (
|
|
471
|
+
<Text color="gray" dimColor>
|
|
472
|
+
{" "}
|
|
473
|
+
<Text color={ACCENT}>git add {"<files>"}</Text>
|
|
474
|
+
{" "}or{" "}
|
|
475
|
+
<Text color={ACCENT}>lens commit --auto</Text>
|
|
476
|
+
</Text>
|
|
477
|
+
)}
|
|
478
|
+
</Box>
|
|
479
|
+
)}
|
|
480
|
+
</Box>
|
|
481
|
+
)}
|
|
482
|
+
|
|
483
|
+
{phase.type === "generating" && (
|
|
484
|
+
<Box gap={1} marginTop={1}>
|
|
485
|
+
<Text color={ACCENT}>●</Text>
|
|
486
|
+
<Text color="gray" dimColor>
|
|
487
|
+
{phraseText}
|
|
488
|
+
</Text>
|
|
489
|
+
</Box>
|
|
490
|
+
)}
|
|
491
|
+
|
|
492
|
+
{phase.type === "preview" && (
|
|
493
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
494
|
+
<Text color={ACCENT} bold>
|
|
495
|
+
GENERATED MESSAGE
|
|
496
|
+
</Text>
|
|
497
|
+
<Box
|
|
498
|
+
flexDirection="column"
|
|
499
|
+
marginLeft={2}
|
|
500
|
+
marginTop={1}
|
|
501
|
+
marginBottom={1}
|
|
502
|
+
>
|
|
503
|
+
{phase.message.split("\n").map((line, i) => (
|
|
504
|
+
<Text key={i} color={i === 0 ? "white" : "gray"} bold={i === 0}>
|
|
505
|
+
{line || " "}
|
|
506
|
+
</Text>
|
|
507
|
+
))}
|
|
508
|
+
</Box>
|
|
509
|
+
|
|
510
|
+
{phase.splitGroups.length > 0 && (
|
|
511
|
+
<Box flexDirection="column" marginLeft={2} marginBottom={1}>
|
|
512
|
+
<Text color="yellow" dimColor>
|
|
513
|
+
⚡ large diff — consider splitting into{" "}
|
|
514
|
+
{phase.splitGroups.length} commits:
|
|
515
|
+
</Text>
|
|
516
|
+
{phase.splitGroups.map((g, i) => (
|
|
517
|
+
<Text key={i} color="gray" dimColor>
|
|
518
|
+
{" · "}
|
|
519
|
+
{g}
|
|
520
|
+
</Text>
|
|
521
|
+
))}
|
|
522
|
+
</Box>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
<Text color="gray" dimColor>
|
|
526
|
+
{div}
|
|
527
|
+
</Text>
|
|
528
|
+
<Box gap={3} marginTop={1}>
|
|
529
|
+
<Text color="green">y/enter commit</Text>
|
|
530
|
+
<Text color="cyan">e edit</Text>
|
|
531
|
+
<Text color="gray" dimColor>
|
|
532
|
+
n/esc cancel
|
|
533
|
+
</Text>
|
|
534
|
+
</Box>
|
|
535
|
+
</Box>
|
|
536
|
+
)}
|
|
537
|
+
|
|
538
|
+
{phase.type === "editing" && (
|
|
539
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
540
|
+
<Text color={ACCENT} bold>
|
|
541
|
+
EDIT MESSAGE
|
|
542
|
+
</Text>
|
|
543
|
+
<Box marginLeft={2} marginTop={1} flexDirection="column" gap={1}>
|
|
544
|
+
<TextInput
|
|
545
|
+
value={phase.message}
|
|
546
|
+
onChange={(msg) =>
|
|
547
|
+
setPhase((prev) =>
|
|
548
|
+
prev.type === "editing" ? { ...prev, message: msg } : prev,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
onSubmit={(msg) =>
|
|
552
|
+
setPhase((prev) =>
|
|
553
|
+
prev.type === "editing"
|
|
554
|
+
? {
|
|
555
|
+
type: "preview",
|
|
556
|
+
message: msg,
|
|
557
|
+
splitGroups: [],
|
|
558
|
+
diff: prev.diff,
|
|
559
|
+
}
|
|
560
|
+
: prev,
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
/>
|
|
564
|
+
<Text color="gray" dimColor>
|
|
565
|
+
enter confirm · esc back
|
|
566
|
+
</Text>
|
|
567
|
+
</Box>
|
|
568
|
+
</Box>
|
|
569
|
+
)}
|
|
570
|
+
|
|
571
|
+
{phase.type === "committing" && (
|
|
572
|
+
<Box gap={1} marginTop={1}>
|
|
573
|
+
<Text color={ACCENT}>*</Text>
|
|
574
|
+
<Text color="gray" dimColor>
|
|
575
|
+
committing…
|
|
576
|
+
</Text>
|
|
577
|
+
</Box>
|
|
578
|
+
)}
|
|
579
|
+
|
|
580
|
+
{phase.type === "pushing" && (
|
|
581
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
582
|
+
<Box gap={2}>
|
|
583
|
+
<Text color="green">{figures.tick}</Text>
|
|
584
|
+
<Text color={ACCENT}>{phase.hash}</Text>
|
|
585
|
+
<Text color="white">
|
|
586
|
+
{trunc(phase.message.split("\n")[0]!, 65)}
|
|
587
|
+
</Text>
|
|
588
|
+
</Box>
|
|
589
|
+
<Box gap={1} marginLeft={2}>
|
|
590
|
+
<Text color={ACCENT}>*</Text>
|
|
591
|
+
<Text color="gray" dimColor>
|
|
592
|
+
pushing…
|
|
593
|
+
</Text>
|
|
594
|
+
</Box>
|
|
595
|
+
</Box>
|
|
596
|
+
)}
|
|
597
|
+
|
|
598
|
+
{phase.type === "done" && (
|
|
599
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
600
|
+
<Box gap={2}>
|
|
601
|
+
<Text color="green">{figures.tick}</Text>
|
|
602
|
+
<Text color={ACCENT}>{phase.hash}</Text>
|
|
603
|
+
<Text color="white" bold>
|
|
604
|
+
{trunc(phase.message.split("\n")[0]!, 65)}
|
|
605
|
+
</Text>
|
|
606
|
+
</Box>
|
|
607
|
+
{phase.message
|
|
608
|
+
.split("\n")
|
|
609
|
+
.slice(2)
|
|
610
|
+
.filter(Boolean)
|
|
611
|
+
.map((line, i) => (
|
|
612
|
+
<Text key={i} color="gray" dimColor>
|
|
613
|
+
{line}
|
|
614
|
+
</Text>
|
|
615
|
+
))}
|
|
616
|
+
{phase.pushed && (
|
|
617
|
+
<Box gap={2} marginTop={1}>
|
|
618
|
+
<Text color="green">{figures.tick}</Text>
|
|
619
|
+
<Text color="gray" dimColor>
|
|
620
|
+
pushed to remote
|
|
621
|
+
</Text>
|
|
622
|
+
</Box>
|
|
623
|
+
)}
|
|
624
|
+
<Text color="gray" dimColor>
|
|
625
|
+
press any key to exit
|
|
626
|
+
</Text>
|
|
627
|
+
</Box>
|
|
628
|
+
)}
|
|
629
|
+
|
|
630
|
+
{phase.type === "preview-only" && (
|
|
631
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
632
|
+
<Text color={ACCENT} bold>
|
|
633
|
+
GENERATED MESSAGE
|
|
634
|
+
</Text>
|
|
635
|
+
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
|
636
|
+
{phase.message.split("\n").map((line, i) => (
|
|
637
|
+
<Text key={i} color={i === 0 ? "white" : "gray"} bold={i === 0}>
|
|
638
|
+
{line || " "}
|
|
639
|
+
</Text>
|
|
640
|
+
))}
|
|
641
|
+
</Box>
|
|
642
|
+
<Text color="gray" dimColor>
|
|
643
|
+
(preview only — not committed)
|
|
644
|
+
</Text>
|
|
645
|
+
</Box>
|
|
646
|
+
)}
|
|
647
|
+
|
|
648
|
+
{phase.type === "error" && (
|
|
649
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
650
|
+
<Box gap={1}>
|
|
651
|
+
<Text color="red">{figures.cross}</Text>
|
|
652
|
+
<Text color="white">{phase.message.split("\n")[0]}</Text>
|
|
653
|
+
</Box>
|
|
654
|
+
{phase.message
|
|
655
|
+
.split("\n")
|
|
656
|
+
.slice(1)
|
|
657
|
+
.map((line, i) => (
|
|
658
|
+
<Text key={i} color="gray" dimColor>
|
|
659
|
+
{line}
|
|
660
|
+
</Text>
|
|
661
|
+
))}
|
|
662
|
+
</Box>
|
|
663
|
+
)}
|
|
664
|
+
</Box>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
interface Props {
|
|
669
|
+
path: string;
|
|
670
|
+
files: string[];
|
|
671
|
+
auto: boolean;
|
|
672
|
+
preview: boolean;
|
|
673
|
+
push: boolean;
|
|
674
|
+
confirm: boolean;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function CommitCommand({
|
|
678
|
+
path: inputPath,
|
|
679
|
+
files,
|
|
680
|
+
auto,
|
|
681
|
+
preview,
|
|
682
|
+
push,
|
|
683
|
+
confirm,
|
|
684
|
+
}: Props) {
|
|
685
|
+
const cwd = path.resolve(inputPath);
|
|
686
|
+
const [provider, setProvider] = useState<Provider | null>(null);
|
|
687
|
+
|
|
688
|
+
if (!existsSync(cwd)) {
|
|
689
|
+
return (
|
|
690
|
+
<Box marginTop={1}>
|
|
691
|
+
<Text color="red">
|
|
692
|
+
{figures.cross} path not found: {cwd}
|
|
693
|
+
</Text>
|
|
694
|
+
</Box>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!provider) {
|
|
699
|
+
return <ProviderPicker onDone={setProvider} />;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return (
|
|
703
|
+
<CommitRunner
|
|
704
|
+
cwd={cwd}
|
|
705
|
+
provider={provider}
|
|
706
|
+
files={files}
|
|
707
|
+
auto={auto}
|
|
708
|
+
preview={preview}
|
|
709
|
+
push={push}
|
|
710
|
+
confirm={confirm}
|
|
711
|
+
/>
|
|
712
|
+
);
|
|
713
|
+
}
|