@ridit/lens 0.3.6 → 0.3.8
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 +1967 -1396
- package/package.json +1 -1
- package/src/commands/chat.tsx +14 -20
- package/src/components/chat/ChatMessage.tsx +46 -4
- package/src/components/chat/ChatOverlays.tsx +27 -22
- package/src/components/chat/ChatRunner.tsx +55 -15
- package/src/components/chat/TextArea.tsx +177 -0
- package/src/components/chat/hooks/useChat.ts +417 -226
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -4
- package/src/components/repo/RepoAnalysis.tsx +5 -5
- package/src/components/task/TaskRunner.tsx +3 -3
- package/src/components/timeline/TimelineRunner.tsx +2 -2
- package/src/components/watch/RunRunner.tsx +2 -1
- package/src/index.tsx +13 -2
- package/src/prompts/fewshot.ts +18 -0
- package/src/prompts/system.ts +30 -17
- package/src/types/chat.ts +3 -1
- package/src/utils/chat.ts +73 -8
- package/src/utils/intentClassifier.ts +0 -15
- package/src/utils/memory.ts +103 -26
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/tools/builtins.ts +47 -6
package/src/utils/thinking.tsx
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
+
const TIPS = [
|
|
4
|
+
"use /auto to toggle auto-approve for safe tools",
|
|
5
|
+
"ctrl+f to toggle force-all mode",
|
|
6
|
+
"shift+enter for a new line in the input",
|
|
7
|
+
"↑ / ↓ arrows navigate message history",
|
|
8
|
+
"ctrl+w deletes the previous word",
|
|
9
|
+
"ctrl+delete deletes the next word",
|
|
10
|
+
"/timeline to browse commit history",
|
|
11
|
+
"/review to analyze the current codebase",
|
|
12
|
+
"/memory list to view stored memories",
|
|
13
|
+
"/chat list to see saved conversations",
|
|
14
|
+
"esc cancels a running response",
|
|
15
|
+
"/clear history resets session memory",
|
|
16
|
+
"tab autocompletes slash commands",
|
|
17
|
+
"lens commit --auto for fast AI commits",
|
|
18
|
+
"lens run \"bun dev\" to watch and auto-fix errors",
|
|
19
|
+
];
|
|
20
|
+
|
|
3
21
|
const PHRASES: Record<string, string[]> = {
|
|
4
22
|
general: [
|
|
5
23
|
"marinating on that... 🍖",
|
|
@@ -321,6 +339,52 @@ const PHRASES: Record<string, string[]> = {
|
|
|
321
339
|
|
|
322
340
|
export type ThinkingKind = keyof typeof PHRASES;
|
|
323
341
|
|
|
342
|
+
export function useThinkingTip(active: boolean, intervalMs = 8000): string {
|
|
343
|
+
const [index, setIndex] = useState(() => Math.floor(Math.random() * TIPS.length));
|
|
344
|
+
const usedRef = useRef<Set<number>>(new Set());
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!active) return;
|
|
348
|
+
const pickUnused = () => {
|
|
349
|
+
if (usedRef.current.size >= TIPS.length) usedRef.current.clear();
|
|
350
|
+
let next: number;
|
|
351
|
+
do {
|
|
352
|
+
next = Math.floor(Math.random() * TIPS.length);
|
|
353
|
+
} while (usedRef.current.has(next));
|
|
354
|
+
usedRef.current.add(next);
|
|
355
|
+
return next;
|
|
356
|
+
};
|
|
357
|
+
setIndex(pickUnused());
|
|
358
|
+
const id = setInterval(() => setIndex(pickUnused()), intervalMs);
|
|
359
|
+
return () => clearInterval(id);
|
|
360
|
+
}, [active, intervalMs]);
|
|
361
|
+
|
|
362
|
+
return TIPS[index]!;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function useThinkingTimer(active: boolean): string {
|
|
366
|
+
const [seconds, setSeconds] = useState(0);
|
|
367
|
+
const startRef = useRef<number | null>(null);
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (active) {
|
|
371
|
+
startRef.current = Date.now();
|
|
372
|
+
setSeconds(0);
|
|
373
|
+
const id = setInterval(() => {
|
|
374
|
+
setSeconds(Math.floor((Date.now() - (startRef.current ?? Date.now())) / 1000));
|
|
375
|
+
}, 1000);
|
|
376
|
+
return () => clearInterval(id);
|
|
377
|
+
} else {
|
|
378
|
+
startRef.current = null;
|
|
379
|
+
setSeconds(0);
|
|
380
|
+
}
|
|
381
|
+
}, [active]);
|
|
382
|
+
|
|
383
|
+
if (!active || seconds === 0) return "";
|
|
384
|
+
if (seconds < 60) return `${seconds}s`;
|
|
385
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
386
|
+
}
|
|
387
|
+
|
|
324
388
|
export function useThinkingPhrase(
|
|
325
389
|
active: boolean,
|
|
326
390
|
kind: ThinkingKind = "general",
|
|
@@ -136,15 +136,56 @@ 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): WriteFileInput | null => {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(s) as { path?: unknown; content?: unknown };
|
|
142
|
+
if (
|
|
143
|
+
typeof parsed.path !== "string" ||
|
|
144
|
+
typeof parsed.content !== "string"
|
|
145
|
+
)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
path: parsed.path.replace(/\\/g, "/"),
|
|
149
|
+
content: parsed.content,
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// attempt 1: parse as-is
|
|
157
|
+
const first = tryParse(body.trim());
|
|
158
|
+
if (first) return first;
|
|
159
|
+
|
|
160
|
+
// attempt 2: escape unescaped control characters
|
|
139
161
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
const sanitized = body
|
|
163
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
164
|
+
.replace(/(?<!\\)\n/g, "\\n")
|
|
165
|
+
.replace(/(?<!\\)\r/g, "\\r")
|
|
166
|
+
.replace(/(?<!\\)\t/g, "\\t");
|
|
167
|
+
const second = tryParse(sanitized);
|
|
168
|
+
if (second) return second;
|
|
169
|
+
} catch {}
|
|
170
|
+
|
|
171
|
+
// attempt 3: regex extraction as last resort
|
|
172
|
+
const pathMatch = body.match(/"path"\s*:\s*"([^"]+)"/);
|
|
173
|
+
const contentMatch = body.match(/"content"\s*:\s*"([\s\S]*?)"\s*}?\s*$/);
|
|
174
|
+
if (pathMatch?.[1] && contentMatch?.[1] !== undefined) {
|
|
175
|
+
return {
|
|
176
|
+
path: pathMatch[1].replace(/\\/g, "/"),
|
|
177
|
+
content: contentMatch[1]
|
|
178
|
+
.replace(/\\n/g, "\n")
|
|
179
|
+
.replace(/\\r/g, "\r")
|
|
180
|
+
.replace(/\\t/g, "\t"),
|
|
181
|
+
};
|
|
145
182
|
}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
146
185
|
},
|
|
147
|
-
|
|
186
|
+
// null-safe — parseInput can return null if AI sends malformed JSON
|
|
187
|
+
summariseInput: (input) =>
|
|
188
|
+
input ? `${input.path} (${input.content.length} bytes)` : "unknown file",
|
|
148
189
|
execute: ({ path: filePath, content }, ctx) => ({
|
|
149
190
|
kind: "text",
|
|
150
191
|
value: writeFile(filePath, content, ctx.repoPath),
|