@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,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
|
+
}
|