@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.
Files changed (96) hide show
  1. package/dist/index.mjs +105368 -274002
  2. package/package.json +13 -19
  3. package/src/colors.ts +15 -15
  4. package/src/commands/chat.tsx +32 -23
  5. package/src/commands/provider.tsx +11 -238
  6. package/src/commands/repo.tsx +66 -120
  7. package/src/commands/timeline.tsx +11 -22
  8. package/src/components/ChatView.tsx +238 -0
  9. package/src/components/Message.tsx +46 -0
  10. package/src/components/ToolCall.tsx +67 -0
  11. package/src/components/chat/ChatView.tsx +550 -0
  12. package/src/components/chat/Message.tsx +152 -0
  13. package/src/components/chat/StatusBar.tsx +214 -0
  14. package/src/components/chat/TextArea.tsx +173 -176
  15. package/src/components/provider/ApiKeyStep.tsx +207 -199
  16. package/src/components/provider/ModelStep.tsx +90 -88
  17. package/src/components/provider/ProviderSetup.tsx +331 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +53 -61
  19. package/src/components/repo/StepRow.tsx +68 -69
  20. package/src/components/timeline/TimelineView.tsx +840 -0
  21. package/src/components/toolcall-utils.ts +103 -0
  22. package/src/components/watch/RunView.tsx +497 -0
  23. package/src/hooks/useChatInput.ts +49 -0
  24. package/src/hooks/useCommandHandler.ts +117 -0
  25. package/src/index.tsx +386 -139
  26. package/src/utils/git.ts +149 -155
  27. package/src/utils/repo.ts +62 -69
  28. package/src/utils/thinking.tsx +64 -0
  29. package/src/utils/watch.ts +165 -307
  30. package/tests/message.test.ts +38 -0
  31. package/tests/toolcall-utils.test.ts +111 -0
  32. package/tsconfig.json +8 -24
  33. package/CLAUDE.md +0 -50
  34. package/LENS.md +0 -48
  35. package/LICENSE +0 -21
  36. package/README.md +0 -93
  37. package/addons/README.md +0 -55
  38. package/addons/clean-cache.js +0 -48
  39. package/addons/generate-readme.js +0 -67
  40. package/addons/git-stats.js +0 -29
  41. package/addons/run-tests.js +0 -127
  42. package/src/commands/commit.tsx +0 -668
  43. package/src/commands/review.tsx +0 -294
  44. package/src/commands/run.tsx +0 -56
  45. package/src/commands/task.tsx +0 -36
  46. package/src/components/chat/ChatMessage.tsx +0 -195
  47. package/src/components/chat/ChatOverlays.tsx +0 -399
  48. package/src/components/chat/ChatRunner.tsx +0 -517
  49. package/src/components/chat/hooks/useChat.ts +0 -631
  50. package/src/components/chat/hooks/useChatInput.ts +0 -79
  51. package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
  52. package/src/components/provider/ProviderPicker.tsx +0 -76
  53. package/src/components/provider/RemoveProviderStep.tsx +0 -82
  54. package/src/components/repo/DiffViewer.tsx +0 -175
  55. package/src/components/repo/FileReviewer.tsx +0 -70
  56. package/src/components/repo/FileViewer.tsx +0 -60
  57. package/src/components/repo/IssueFixer.tsx +0 -666
  58. package/src/components/repo/LensFileMenu.tsx +0 -115
  59. package/src/components/repo/NoProviderPrompt.tsx +0 -28
  60. package/src/components/repo/PreviewRunner.tsx +0 -217
  61. package/src/components/repo/RepoAnalysis.tsx +0 -534
  62. package/src/components/task/TaskRunner.tsx +0 -396
  63. package/src/components/timeline/CommitDetail.tsx +0 -272
  64. package/src/components/timeline/CommitList.tsx +0 -162
  65. package/src/components/timeline/TimelineChat.tsx +0 -166
  66. package/src/components/timeline/TimelineRunner.tsx +0 -1285
  67. package/src/components/watch/RunRunner.tsx +0 -929
  68. package/src/prompts/fewshot.ts +0 -252
  69. package/src/prompts/index.ts +0 -2
  70. package/src/prompts/system.ts +0 -285
  71. package/src/tools/chart.ts +0 -202
  72. package/src/tools/convert-image.ts +0 -312
  73. package/src/tools/files.ts +0 -253
  74. package/src/tools/git.ts +0 -603
  75. package/src/tools/index.ts +0 -17
  76. package/src/tools/pdf.ts +0 -164
  77. package/src/tools/shell.ts +0 -96
  78. package/src/tools/view-image.ts +0 -335
  79. package/src/tools/web.ts +0 -212
  80. package/src/types/chat.ts +0 -86
  81. package/src/types/config.ts +0 -20
  82. package/src/types/repo.ts +0 -54
  83. package/src/utils/addons/loadAddons.ts +0 -34
  84. package/src/utils/ai.ts +0 -321
  85. package/src/utils/chat.ts +0 -326
  86. package/src/utils/chatHistory.ts +0 -121
  87. package/src/utils/config.ts +0 -61
  88. package/src/utils/files.ts +0 -105
  89. package/src/utils/intentClassifier.ts +0 -58
  90. package/src/utils/lensfile.ts +0 -142
  91. package/src/utils/llm.ts +0 -81
  92. package/src/utils/memory.ts +0 -209
  93. package/src/utils/preview.ts +0 -119
  94. package/src/utils/stats.ts +0 -174
  95. package/src/utils/tools/builtins.ts +0 -377
  96. package/src/utils/tools/registry.ts +0 -105
@@ -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
- }