@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.
Files changed (51) hide show
  1. package/LENS.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -0
  4. package/dist/index.js +49363 -0
  5. package/package.json +38 -0
  6. package/src/colors.ts +1 -0
  7. package/src/commands/chat.tsx +23 -0
  8. package/src/commands/provider.tsx +224 -0
  9. package/src/commands/repo.tsx +120 -0
  10. package/src/commands/review.tsx +294 -0
  11. package/src/commands/task.tsx +36 -0
  12. package/src/commands/timeline.tsx +22 -0
  13. package/src/components/chat/ChatMessage.tsx +176 -0
  14. package/src/components/chat/ChatOverlays.tsx +329 -0
  15. package/src/components/chat/ChatRunner.tsx +732 -0
  16. package/src/components/provider/ApiKeyStep.tsx +243 -0
  17. package/src/components/provider/ModelStep.tsx +73 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +54 -0
  19. package/src/components/provider/RemoveProviderStep.tsx +83 -0
  20. package/src/components/repo/DiffViewer.tsx +175 -0
  21. package/src/components/repo/FileReviewer.tsx +70 -0
  22. package/src/components/repo/FileViewer.tsx +60 -0
  23. package/src/components/repo/IssueFixer.tsx +666 -0
  24. package/src/components/repo/LensFileMenu.tsx +122 -0
  25. package/src/components/repo/NoProviderPrompt.tsx +28 -0
  26. package/src/components/repo/PreviewRunner.tsx +217 -0
  27. package/src/components/repo/ProviderPicker.tsx +76 -0
  28. package/src/components/repo/RepoAnalysis.tsx +343 -0
  29. package/src/components/repo/StepRow.tsx +69 -0
  30. package/src/components/task/TaskRunner.tsx +396 -0
  31. package/src/components/timeline/CommitDetail.tsx +274 -0
  32. package/src/components/timeline/CommitList.tsx +174 -0
  33. package/src/components/timeline/TimelineChat.tsx +167 -0
  34. package/src/components/timeline/TimelineRunner.tsx +1209 -0
  35. package/src/index.tsx +60 -0
  36. package/src/types/chat.ts +69 -0
  37. package/src/types/config.ts +20 -0
  38. package/src/types/repo.ts +42 -0
  39. package/src/utils/ai.ts +233 -0
  40. package/src/utils/chat.ts +833 -0
  41. package/src/utils/config.ts +61 -0
  42. package/src/utils/files.ts +104 -0
  43. package/src/utils/git.ts +155 -0
  44. package/src/utils/history.ts +86 -0
  45. package/src/utils/lensfile.ts +77 -0
  46. package/src/utils/llm.ts +81 -0
  47. package/src/utils/preview.ts +119 -0
  48. package/src/utils/repo.ts +69 -0
  49. package/src/utils/stats.ts +174 -0
  50. package/src/utils/thinking.tsx +191 -0
  51. package/tsconfig.json +24 -0
@@ -0,0 +1,174 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { Commit } from "../../utils/git";
4
+
5
+ const ACCENT = "#FF8C00";
6
+
7
+ type Props = {
8
+ commits: Commit[];
9
+ selectedIndex: number;
10
+ scrollOffset: number;
11
+ visibleCount: number;
12
+ searchQuery: string;
13
+ width: number;
14
+ };
15
+
16
+ function formatRefs(refs: string): string {
17
+ if (!refs) return "";
18
+ return refs
19
+ .split(",")
20
+ .map((r) => r.trim())
21
+ .filter(Boolean)
22
+ .map((r) => {
23
+ if (r.startsWith("HEAD -> ")) return `[${r.slice(8)}]`;
24
+ if (r.startsWith("origin/")) return `[${r}]`;
25
+ if (r.startsWith("tag: ")) return `<${r.slice(5)}>`;
26
+ return `[${r}]`;
27
+ })
28
+ .join(" ");
29
+ }
30
+
31
+ function shortDate(dateStr: string): string {
32
+ // "2026-03-12 14:22:01 +0530" → "Mar 12"
33
+ try {
34
+ const d = new Date(dateStr);
35
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
36
+ } catch {
37
+ return dateStr.slice(0, 10);
38
+ }
39
+ }
40
+
41
+ function graphSymbol(
42
+ commit: Commit,
43
+ index: number,
44
+ ): { symbol: string; color: string } {
45
+ if (commit.parents.length > 1) return { symbol: "⎇", color: "magenta" };
46
+ if (index === 0) return { symbol: "◉", color: ACCENT };
47
+ return { symbol: "●", color: "gray" };
48
+ }
49
+
50
+ export function CommitList({
51
+ commits,
52
+ selectedIndex,
53
+ scrollOffset,
54
+ visibleCount,
55
+ searchQuery,
56
+ width,
57
+ }: Props) {
58
+ const visible = commits.slice(scrollOffset, scrollOffset + visibleCount);
59
+
60
+ return (
61
+ <Box flexDirection="column" width={width}>
62
+ {/* header */}
63
+ <Box paddingX={1} marginBottom={1}>
64
+ <Text color="gray" dimColor>
65
+ {"─".repeat(Math.max(0, width - 2))}
66
+ </Text>
67
+ </Box>
68
+ <Box paddingX={1} marginBottom={1}>
69
+ <Text color={ACCENT} bold>
70
+ {" COMMITS "}
71
+ </Text>
72
+ <Text color="gray" dimColor>
73
+ {commits.length} total
74
+ {searchQuery ? ` / ${searchQuery}` : ""}
75
+ </Text>
76
+ </Box>
77
+
78
+ {visible.map((commit, i) => {
79
+ const absoluteIndex = scrollOffset + i;
80
+ const isSelected = absoluteIndex === selectedIndex;
81
+ const { symbol, color } = graphSymbol(commit, absoluteIndex);
82
+ const refs = formatRefs(commit.refs);
83
+ const date = shortDate(commit.date);
84
+
85
+ // truncate message to fit width
86
+ const prefixLen = 14; // symbol + hash + date
87
+ const maxMsg = Math.max(10, width - prefixLen - 3);
88
+ const msg =
89
+ commit.message.length > maxMsg
90
+ ? commit.message.slice(0, maxMsg - 1) + "…"
91
+ : commit.message;
92
+
93
+ return (
94
+ <Box key={commit.hash} paddingX={1} flexDirection="column">
95
+ {/* graph line above (not first) */}
96
+ {i > 0 && (
97
+ <Text color="gray" dimColor>
98
+ {"│"}
99
+ </Text>
100
+ )}
101
+ <Box gap={1}>
102
+ {/* selection indicator */}
103
+ <Text color={isSelected ? ACCENT : "gray"}>
104
+ {isSelected ? "▶" : " "}
105
+ </Text>
106
+
107
+ {/* graph node */}
108
+ <Text color={isSelected ? ACCENT : color}>{symbol}</Text>
109
+
110
+ {/* short hash */}
111
+ <Text
112
+ color={isSelected ? "white" : "gray"}
113
+ dimColor={!isSelected}
114
+ >
115
+ {commit.shortHash}
116
+ </Text>
117
+
118
+ {/* date */}
119
+ <Text color="cyan" dimColor={!isSelected}>
120
+ {date}
121
+ </Text>
122
+
123
+ {/* message */}
124
+ <Text
125
+ color={isSelected ? "white" : "gray"}
126
+ bold={isSelected}
127
+ wrap="truncate"
128
+ >
129
+ {msg}
130
+ </Text>
131
+ </Box>
132
+
133
+ {/* refs on selected */}
134
+ {isSelected && refs && (
135
+ <Box paddingLeft={4}>
136
+ <Text color="yellow">{refs}</Text>
137
+ </Box>
138
+ )}
139
+
140
+ {/* stat summary on selected */}
141
+ {isSelected && (
142
+ <Box paddingLeft={4} gap={2}>
143
+ <Text color="gray" dimColor>
144
+ {commit.author}
145
+ </Text>
146
+ {commit.filesChanged > 0 && (
147
+ <>
148
+ <Text color="green">+{commit.insertions}</Text>
149
+ <Text color="red">-{commit.deletions}</Text>
150
+ <Text color="gray" dimColor>
151
+ {commit.filesChanged} file
152
+ {commit.filesChanged !== 1 ? "s" : ""}
153
+ </Text>
154
+ </>
155
+ )}
156
+ </Box>
157
+ )}
158
+ </Box>
159
+ );
160
+ })}
161
+
162
+ {/* scroll hint */}
163
+ <Box paddingX={1} marginTop={1}>
164
+ <Text color="gray" dimColor>
165
+ {scrollOffset > 0 ? "↑ more above" : ""}
166
+ {scrollOffset > 0 && scrollOffset + visibleCount < commits.length
167
+ ? " "
168
+ : ""}
169
+ {scrollOffset + visibleCount < commits.length ? "↓ more below" : ""}
170
+ </Text>
171
+ </Box>
172
+ </Box>
173
+ );
174
+ }
@@ -0,0 +1,167 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, Static } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import type { Commit } from "../../utils/git";
5
+ import { summarizeTimeline } from "../../utils/git";
6
+ import type { Provider } from "../../types/config";
7
+ import { callChat } from "../../utils/chat";
8
+
9
+ const ACCENT = "#FF8C00";
10
+
11
+ type TLMessage = { role: "user" | "assistant"; content: string; type: "text" };
12
+
13
+ type StaticMsg =
14
+ | { kind: "user"; content: string; id: number }
15
+ | { kind: "assistant"; content: string; id: number };
16
+
17
+ type Props = {
18
+ commits: Commit[];
19
+ repoPath: string;
20
+ provider: Provider | null;
21
+ onExit: () => void;
22
+ width: number;
23
+ height: number;
24
+ };
25
+
26
+ const SUGGESTIONS = [
27
+ "which commit changed the most files?",
28
+ "who made the most commits?",
29
+ "what happened last week?",
30
+ "show me all merge commits",
31
+ "which day had the most activity?",
32
+ ];
33
+
34
+ let msgId = 0;
35
+
36
+ export function TimelineChat({
37
+ commits,
38
+ provider,
39
+ onExit,
40
+ width,
41
+ height,
42
+ }: Props) {
43
+ const [committed, setCommitted] = useState<StaticMsg[]>([]);
44
+ const [live, setLive] = useState<TLMessage[]>([]);
45
+ const [input, setInput] = useState("");
46
+ const [thinking, setThinking] = useState(false);
47
+
48
+ const divider = "─".repeat(Math.max(0, width - 2));
49
+
50
+ const systemPrompt = `You are a git history analyst. You have access to the full commit timeline of a repository.
51
+ Answer questions about the git history concisely and accurately.
52
+ Use only the data provided — never make up commits or dates.
53
+ Keep answers short. Plain text only, no markdown headers.
54
+
55
+ ${summarizeTimeline(commits)}`;
56
+
57
+ const sendMessage = async (text: string) => {
58
+ if (!text.trim() || thinking || !provider) return;
59
+
60
+ const userTL: TLMessage = { role: "user", content: text, type: "text" };
61
+ const nextLive = [...live, userTL];
62
+
63
+ setCommitted((prev) => [
64
+ ...prev,
65
+ { kind: "user", content: text, id: ++msgId },
66
+ ]);
67
+ setLive(nextLive);
68
+ setThinking(true);
69
+ setInput("");
70
+
71
+ try {
72
+ const raw = await callChat(provider, systemPrompt, nextLive as any);
73
+ const answer = typeof raw === "string" ? raw : "(no response)";
74
+ const assistantTL: TLMessage = {
75
+ role: "assistant",
76
+ content: answer,
77
+ type: "text",
78
+ };
79
+ setLive((prev) => [...prev, assistantTL]);
80
+ setCommitted((prev) => [
81
+ ...prev,
82
+ { kind: "assistant", content: answer, id: ++msgId },
83
+ ]);
84
+ } catch (e) {
85
+ const errText = `Error: ${e instanceof Error ? e.message : String(e)}`;
86
+ setCommitted((prev) => [
87
+ ...prev,
88
+ { kind: "assistant", content: errText, id: ++msgId },
89
+ ]);
90
+ } finally {
91
+ setThinking(false);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <Box width={width} flexDirection="column">
97
+ <Box paddingX={1}>
98
+ <Text color="gray" dimColor>
99
+ {divider}
100
+ </Text>
101
+ </Box>
102
+ <Box paddingX={1} marginBottom={1} gap={2}>
103
+ <Text color={ACCENT} bold>
104
+ ASK TIMELINE
105
+ </Text>
106
+ <Text color="gray" dimColor>
107
+ tab · esc to go back
108
+ </Text>
109
+ </Box>
110
+
111
+ <Static items={committed}>
112
+ {(msg) => (
113
+ <Box key={msg.id} paddingX={1} marginBottom={1} gap={1}>
114
+ <Text color={msg.kind === "user" ? "gray" : ACCENT}>
115
+ {msg.kind === "user" ? ">" : "*"}
116
+ </Text>
117
+ <Text color="white" wrap="wrap">
118
+ {msg.content}
119
+ </Text>
120
+ </Box>
121
+ )}
122
+ </Static>
123
+
124
+ {thinking && (
125
+ <Box paddingX={1} marginBottom={1} gap={1}>
126
+ <Text color={ACCENT}>*</Text>
127
+ <Text color="gray" dimColor>
128
+ thinking…
129
+ </Text>
130
+ </Box>
131
+ )}
132
+
133
+ {committed.length === 0 && !thinking && (
134
+ <Box paddingX={1} flexDirection="column" marginBottom={1}>
135
+ <Text color="gray" dimColor>
136
+ try asking:
137
+ </Text>
138
+ {SUGGESTIONS.map((s, i) => (
139
+ <Text key={i} color="gray" dimColor>
140
+ {" "}
141
+ {s}
142
+ </Text>
143
+ ))}
144
+ </Box>
145
+ )}
146
+
147
+ <Box paddingX={1}>
148
+ <Text color="gray" dimColor>
149
+ {divider}
150
+ </Text>
151
+ </Box>
152
+ <Box paddingX={1} paddingY={1} gap={1}>
153
+ <Text color={ACCENT}>{">"}</Text>
154
+ <TextInput
155
+ value={input}
156
+ onChange={setInput}
157
+ onSubmit={sendMessage}
158
+ placeholder={
159
+ provider
160
+ ? "ask about the timeline…"
161
+ : "no provider — run lens provider first"
162
+ }
163
+ />
164
+ </Box>
165
+ </Box>
166
+ );
167
+ }