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