@ridit/lens 0.3.6 → 0.3.7
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/CLAUDE.md +50 -0
- package/dist/index.mjs +1342 -1036
- package/package.json +1 -1
- package/src/components/chat/ChatOverlays.tsx +12 -13
- package/src/components/chat/ChatRunner.tsx +2 -2
- package/src/components/chat/TextArea.tsx +176 -0
- package/src/components/chat/hooks/useChat.ts +151 -60
- package/src/components/repo/RepoAnalysis.tsx +2 -2
- package/src/components/timeline/TimelineRunner.tsx +2 -2
- package/src/components/watch/RunRunner.tsx +2 -1
- package/src/prompts/fewshot.ts +18 -0
- package/src/prompts/system.ts +16 -2
- package/src/utils/chat.ts +16 -4
- package/src/utils/memory.ts +103 -26
- package/src/utils/tools/builtins.ts +31 -5
package/src/utils/chat.ts
CHANGED
|
@@ -19,6 +19,11 @@ import { FEW_SHOT_MESSAGES } from "../prompts";
|
|
|
19
19
|
import { registry } from "../utils/tools/registry";
|
|
20
20
|
import type { FilePatch } from "../components/repo/DiffViewer";
|
|
21
21
|
|
|
22
|
+
export type ChatResult = {
|
|
23
|
+
text: string;
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
22
27
|
export type ParsedResponse =
|
|
23
28
|
| { kind: "text"; content: string; remainder?: string }
|
|
24
29
|
| {
|
|
@@ -219,7 +224,7 @@ export async function callChat(
|
|
|
219
224
|
messages: Message[],
|
|
220
225
|
abortSignal?: AbortSignal,
|
|
221
226
|
retries = 2,
|
|
222
|
-
): Promise<
|
|
227
|
+
): Promise<ChatResult> {
|
|
223
228
|
const apiMessages = [
|
|
224
229
|
...buildFewShotMessages(),
|
|
225
230
|
...buildApiMessages(messages),
|
|
@@ -288,13 +293,20 @@ export async function callChat(
|
|
|
288
293
|
|
|
289
294
|
if (provider.type === "anthropic") {
|
|
290
295
|
const content = data.content as { type: string; text: string }[];
|
|
291
|
-
|
|
296
|
+
const text = content
|
|
292
297
|
.filter((b) => b.type === "text")
|
|
293
298
|
.map((b) => b.text)
|
|
294
299
|
.join("");
|
|
300
|
+
const truncated = (data as any).stop_reason === "max_tokens";
|
|
301
|
+
return { text, truncated };
|
|
295
302
|
} else {
|
|
296
|
-
const choices = data.choices as {
|
|
297
|
-
|
|
303
|
+
const choices = data.choices as {
|
|
304
|
+
message: { content: string };
|
|
305
|
+
finish_reason?: string;
|
|
306
|
+
}[];
|
|
307
|
+
const text = choices[0]?.message.content ?? "";
|
|
308
|
+
const truncated = choices[0]?.finish_reason === "length";
|
|
309
|
+
return { text, truncated };
|
|
298
310
|
}
|
|
299
311
|
} catch (err) {
|
|
300
312
|
clearTimeout(timer);
|
package/src/utils/memory.ts
CHANGED
|
@@ -15,16 +15,18 @@ export type MemoryEntry = {
|
|
|
15
15
|
detail: string;
|
|
16
16
|
summary: string;
|
|
17
17
|
timestamp: string;
|
|
18
|
+
repoPath?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type Memory = {
|
|
21
22
|
id: string;
|
|
22
23
|
content: string;
|
|
23
24
|
timestamp: string;
|
|
25
|
+
repoPath?: string;
|
|
26
|
+
scope: "repo" | "global";
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export type MemoryFile = {
|
|
27
|
-
entries: MemoryEntry[];
|
|
28
30
|
memories: Memory[];
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -32,17 +34,14 @@ const LENS_DIR = path.join(os.homedir(), ".lens");
|
|
|
32
34
|
const MEMORY_PATH = path.join(LENS_DIR, "memory.json");
|
|
33
35
|
|
|
34
36
|
function loadMemoryFile(): MemoryFile {
|
|
35
|
-
if (!existsSync(MEMORY_PATH)) return {
|
|
37
|
+
if (!existsSync(MEMORY_PATH)) return { memories: [] };
|
|
36
38
|
try {
|
|
37
39
|
const data = JSON.parse(
|
|
38
40
|
readFileSync(MEMORY_PATH, "utf-8"),
|
|
39
41
|
) as Partial<MemoryFile>;
|
|
40
|
-
return {
|
|
41
|
-
entries: data.entries ?? [],
|
|
42
|
-
memories: data.memories ?? [],
|
|
43
|
-
};
|
|
42
|
+
return { memories: data.memories ?? [] };
|
|
44
43
|
} catch {
|
|
45
|
-
return {
|
|
44
|
+
return { memories: [] };
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -51,26 +50,48 @@ function saveMemoryFile(m: MemoryFile): void {
|
|
|
51
50
|
writeFileSync(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// ──
|
|
53
|
+
// ── Session-only action entries (in-memory, never written to disk) ────────────
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const sessionEntries: MemoryEntry[] = [];
|
|
56
|
+
|
|
57
|
+
export function appendMemory(
|
|
58
|
+
entry: Omit<MemoryEntry, "timestamp">,
|
|
59
|
+
repoPath?: string,
|
|
60
|
+
): void {
|
|
61
|
+
sessionEntries.push({
|
|
62
|
+
...entry,
|
|
63
|
+
repoPath,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
if (sessionEntries.length > 200)
|
|
67
|
+
sessionEntries.splice(0, sessionEntries.length - 200);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function buildMemorySummary(repoPath: string): string {
|
|
64
71
|
const m = loadMemoryFile();
|
|
65
|
-
const relevant = m.entries.slice(-50);
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const globalMemories = m.memories.filter((mem) => mem.scope === "global");
|
|
74
|
+
const repoMemories = m.memories.filter(
|
|
75
|
+
(mem) => mem.scope === "repo" && mem.repoPath === repoPath,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const relevant = sessionEntries
|
|
79
|
+
.filter((e) => !e.repoPath || e.repoPath === repoPath)
|
|
80
|
+
.slice(-50);
|
|
68
81
|
|
|
69
82
|
const parts: string[] = [];
|
|
70
83
|
|
|
71
|
-
if (
|
|
84
|
+
if (globalMemories.length > 0) {
|
|
85
|
+
parts.push(
|
|
86
|
+
`## GLOBAL MEMORIES (apply to all repos)\n\n${globalMemories
|
|
87
|
+
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
88
|
+
.join("\n")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (repoMemories.length > 0) {
|
|
72
93
|
parts.push(
|
|
73
|
-
`## MEMORIES ABOUT THIS REPO\n\n${
|
|
94
|
+
`## MEMORIES ABOUT THIS REPO\n\n${repoMemories
|
|
74
95
|
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
75
96
|
.join("\n")}`,
|
|
76
97
|
);
|
|
@@ -82,7 +103,7 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
82
103
|
return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
|
|
83
104
|
});
|
|
84
105
|
parts.push(
|
|
85
|
-
`## WHAT YOU HAVE ALREADY DONE
|
|
106
|
+
`## WHAT YOU HAVE ALREADY DONE THIS SESSION\n\nThe following actions have already been completed. Do NOT repeat them unless the user explicitly asks:\n\n${lines.join("\n")}`,
|
|
86
107
|
);
|
|
87
108
|
}
|
|
88
109
|
|
|
@@ -90,27 +111,42 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
export function getRepoMemory(repoPath: string): MemoryEntry[] {
|
|
93
|
-
return
|
|
114
|
+
return sessionEntries.filter((e) => !e.repoPath || e.repoPath === repoPath);
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
export function clearRepoMemory(repoPath: string): void {
|
|
118
|
+
// clear session entries for this repo
|
|
119
|
+
const toRemove = sessionEntries
|
|
120
|
+
.map((e, i) => (e.repoPath === repoPath ? i : -1))
|
|
121
|
+
.filter((i) => i >= 0)
|
|
122
|
+
.reverse();
|
|
123
|
+
for (const i of toRemove) sessionEntries.splice(i, 1);
|
|
124
|
+
|
|
125
|
+
// clear persisted memories for this repo (keep global)
|
|
97
126
|
const m = loadMemoryFile();
|
|
98
|
-
m.
|
|
99
|
-
|
|
127
|
+
m.memories = m.memories.filter(
|
|
128
|
+
(mem) => mem.scope === "global" || mem.repoPath !== repoPath,
|
|
129
|
+
);
|
|
100
130
|
saveMemoryFile(m);
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
// ── User/model memories ───────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
133
|
function generateId(): string {
|
|
106
134
|
return Math.random().toString(36).slice(2, 8);
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
export function addMemory(content: string, repoPath: string): Memory {
|
|
110
138
|
const m = loadMemoryFile();
|
|
139
|
+
|
|
140
|
+
const isGlobal = content.startsWith("[global]");
|
|
141
|
+
const cleanContent = isGlobal
|
|
142
|
+
? content.replace("[global]", "").trim()
|
|
143
|
+
: content;
|
|
144
|
+
|
|
111
145
|
const memory: Memory = {
|
|
112
146
|
id: generateId(),
|
|
113
|
-
content,
|
|
147
|
+
content: cleanContent,
|
|
148
|
+
repoPath: isGlobal ? undefined : repoPath,
|
|
149
|
+
scope: isGlobal ? "global" : "repo",
|
|
114
150
|
timestamp: new Date().toISOString(),
|
|
115
151
|
};
|
|
116
152
|
m.memories.push(memory);
|
|
@@ -121,12 +157,53 @@ export function addMemory(content: string, repoPath: string): Memory {
|
|
|
121
157
|
export function deleteMemory(id: string, repoPath: string): boolean {
|
|
122
158
|
const m = loadMemoryFile();
|
|
123
159
|
const before = m.memories.length;
|
|
124
|
-
m.memories = m.memories.filter((mem) =>
|
|
160
|
+
m.memories = m.memories.filter((mem) => mem.id !== id);
|
|
125
161
|
if (m.memories.length === before) return false;
|
|
126
162
|
saveMemoryFile(m);
|
|
127
163
|
return true;
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
export function listMemories(repoPath: string): Memory[] {
|
|
131
|
-
return loadMemoryFile().memories
|
|
167
|
+
return loadMemoryFile().memories.filter(
|
|
168
|
+
(mem) => mem.scope === "global" || mem.repoPath === repoPath,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type SessionToolLog = {
|
|
173
|
+
toolName: string;
|
|
174
|
+
input: string;
|
|
175
|
+
resultPreview: string;
|
|
176
|
+
timestamp: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const sessionToolLog: SessionToolLog[] = [];
|
|
180
|
+
|
|
181
|
+
export function logToolCall(
|
|
182
|
+
toolName: string,
|
|
183
|
+
input: string,
|
|
184
|
+
result: string,
|
|
185
|
+
repoPath?: string,
|
|
186
|
+
): void {
|
|
187
|
+
sessionToolLog.push({
|
|
188
|
+
toolName,
|
|
189
|
+
input,
|
|
190
|
+
resultPreview: result.slice(0, 120),
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
if (sessionToolLog.length > 100)
|
|
194
|
+
sessionToolLog.splice(0, sessionToolLog.length - 100);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getSessionToolSummary(repoPath: string): string {
|
|
198
|
+
if (sessionToolLog.length === 0) return "";
|
|
199
|
+
const recent = sessionToolLog.slice(-30);
|
|
200
|
+
const lines = recent.map((e) => {
|
|
201
|
+
const input = e.input.length > 60 ? e.input.slice(0, 60) + "…" : e.input;
|
|
202
|
+
return `- ${e.toolName}: ${input}`;
|
|
203
|
+
});
|
|
204
|
+
return `## TOOLS ALREADY USED THIS SESSION\n\nDo NOT call these again unless the user explicitly asks:\n\n${lines.join("\n")}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function clearSessionLog(): void {
|
|
208
|
+
sessionToolLog.splice(0, sessionToolLog.length);
|
|
132
209
|
}
|
|
@@ -136,13 +136,39 @@ export const writeFileTool: Tool<WriteFileInput> = {
|
|
|
136
136
|
systemPromptEntry: (i) =>
|
|
137
137
|
`### ${i}. write-file — create or overwrite a file\n<write-file>\n{"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}\n</write-file>`,
|
|
138
138
|
parseInput: (body) => {
|
|
139
|
+
const tryParse = (s: string) => {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(s) as { path: string; content: string };
|
|
142
|
+
if (!parsed.path || parsed.content === undefined) return null;
|
|
143
|
+
return { ...parsed, path: parsed.path.replace(/\\/g, "/") };
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const first = tryParse(body.trim());
|
|
150
|
+
if (first) return first;
|
|
151
|
+
|
|
139
152
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
const sanitized = body
|
|
154
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
155
|
+
.replace(/\n/g, "\\n")
|
|
156
|
+
.replace(/\r/g, "\\r")
|
|
157
|
+
.replace(/\t/g, "\\t");
|
|
158
|
+
const second = tryParse(sanitized);
|
|
159
|
+
if (second) return second;
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
const pathMatch = body.match(/"path"\s*:\s*"([^"]+)"/);
|
|
163
|
+
const contentMatch = body.match(/"content"\s*:\s*"([\s\S]*)"\s*}?\s*$/);
|
|
164
|
+
if (pathMatch && contentMatch && contentMatch[1] !== undefined) {
|
|
165
|
+
return {
|
|
166
|
+
path: pathMatch[1]!.replace(/\\/g, "/"),
|
|
167
|
+
content: contentMatch[1]!.replace(/\\n/g, "\n").replace(/\\t/g, "\t"),
|
|
168
|
+
};
|
|
145
169
|
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
146
172
|
},
|
|
147
173
|
summariseInput: ({ path, content }) => `${path} (${content.length} bytes)`,
|
|
148
174
|
execute: ({ path: filePath, content }, ctx) => ({
|