@ridit/lens 0.2.2 → 0.2.5
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 +18 -11
- package/README.md +91 -0
- package/dist/index.mjs +69273 -2244
- package/package.json +7 -3
- package/src/commands/commit.tsx +713 -0
- package/src/components/chat/ChatOverlays.tsx +14 -4
- package/src/components/chat/ChatRunner.tsx +197 -30
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +505 -422
- package/src/index.tsx +41 -0
- package/src/prompts/fewshot.ts +100 -3
- package/src/prompts/system.ts +16 -20
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +29 -0
- package/src/utils/chat.ts +8 -18
- package/src/utils/memory.ts +7 -12
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +9 -32
- package/src/utils/tools/registry.ts +3 -59
- package/hello.py +0 -51
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// ── tools/view-image.ts ───────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Display images in the terminal using the best available protocol:
|
|
4
|
+
//
|
|
5
|
+
// 1. iTerm2 inline image protocol (OSC 1337)
|
|
6
|
+
// Supported by: iTerm2, WezTerm, VSCode integrated terminal,
|
|
7
|
+
// Windows Terminal (via some builds), Tabby, Hyper
|
|
8
|
+
//
|
|
9
|
+
// 2. Kitty graphics protocol
|
|
10
|
+
// Supported by: Kitty, WezTerm
|
|
11
|
+
//
|
|
12
|
+
// 3. Sixel
|
|
13
|
+
// Supported by: Windows Terminal 1.22+, xterm, mlterm, WezTerm
|
|
14
|
+
// Requires: chafa or img2sixel on PATH (auto-detected)
|
|
15
|
+
//
|
|
16
|
+
// 4. Half-block fallback via terminal-image
|
|
17
|
+
// Works everywhere that supports 256 colors.
|
|
18
|
+
// Quality is limited to block characters (▄▀) — better than nothing.
|
|
19
|
+
//
|
|
20
|
+
// Protocol is auto-detected from environment variables and terminal responses.
|
|
21
|
+
// Can be overridden by passing "protocol" in the JSON input.
|
|
22
|
+
|
|
23
|
+
import path from "path";
|
|
24
|
+
import { existsSync, readFileSync } from "fs";
|
|
25
|
+
import type { Tool } from "@ridit/lens-sdk";
|
|
26
|
+
import { execSync } from "child_process";
|
|
27
|
+
|
|
28
|
+
// ── input ─────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
type Protocol = "iterm2" | "kitty" | "sixel" | "halfblock" | "auto";
|
|
31
|
+
|
|
32
|
+
interface ViewImageInput {
|
|
33
|
+
/** File path (repo-relative or absolute) or URL */
|
|
34
|
+
src: string;
|
|
35
|
+
/** Width: percentage "50%" or column count 40. Default "80%" */
|
|
36
|
+
width?: string | number;
|
|
37
|
+
/** Force a specific protocol instead of auto-detecting */
|
|
38
|
+
protocol?: Protocol;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseViewImageInput(body: string): ViewImageInput | null {
|
|
42
|
+
const trimmed = body.trim();
|
|
43
|
+
if (!trimmed) return null;
|
|
44
|
+
|
|
45
|
+
if (trimmed.startsWith("{")) {
|
|
46
|
+
try {
|
|
47
|
+
const p = JSON.parse(trimmed) as {
|
|
48
|
+
src?: string;
|
|
49
|
+
path?: string;
|
|
50
|
+
url?: string;
|
|
51
|
+
width?: string | number;
|
|
52
|
+
protocol?: Protocol;
|
|
53
|
+
};
|
|
54
|
+
const src = p.src ?? p.path ?? p.url ?? "";
|
|
55
|
+
if (!src) return null;
|
|
56
|
+
return { src, width: p.width ?? "80%", protocol: p.protocol ?? "auto" };
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { src: trimmed, width: "80%", protocol: "auto" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── fetch image bytes ─────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async function fetchBytes(src: string, repoPath: string): Promise<Buffer> {
|
|
68
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
69
|
+
const res = await fetch(src, { signal: AbortSignal.timeout(20_000) });
|
|
70
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
71
|
+
return Buffer.from(await res.arrayBuffer());
|
|
72
|
+
}
|
|
73
|
+
const resolved = path.isAbsolute(src) ? src : path.join(repoPath, src);
|
|
74
|
+
if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
|
|
75
|
+
return readFileSync(resolved);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── protocol detection ────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function detectProtocol(): Exclude<Protocol, "auto"> {
|
|
81
|
+
const term = (process.env["TERM"] ?? "").toLowerCase();
|
|
82
|
+
const termProgram = (process.env["TERM_PROGRAM"] ?? "").toLowerCase();
|
|
83
|
+
const termEmulator = (process.env["TERM_EMULATOR"] ?? "").toLowerCase();
|
|
84
|
+
|
|
85
|
+
// Kitty
|
|
86
|
+
if (term === "xterm-kitty" || process.env["KITTY_WINDOW_ID"]) {
|
|
87
|
+
return "kitty";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// iTerm2
|
|
91
|
+
if (
|
|
92
|
+
termProgram === "iterm.app" ||
|
|
93
|
+
termProgram === "iterm2" ||
|
|
94
|
+
process.env["ITERM_SESSION_ID"]
|
|
95
|
+
) {
|
|
96
|
+
return "iterm2";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// WezTerm — supports both kitty and iterm2; prefer kitty
|
|
100
|
+
if (termProgram === "wezterm" || process.env["WEZTERM_EXECUTABLE"]) {
|
|
101
|
+
return "kitty";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// VSCode integrated terminal — supports iTerm2 protocol
|
|
105
|
+
if (
|
|
106
|
+
process.env["TERM_PROGRAM"] === "vscode" ||
|
|
107
|
+
process.env["VSCODE_INJECTION"] ||
|
|
108
|
+
process.env["VSCODE_GIT_ASKPASS_NODE"]
|
|
109
|
+
) {
|
|
110
|
+
return "iterm2";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Windows Terminal 1.22+ supports Sixel
|
|
114
|
+
// WT_SESSION is set in Windows Terminal
|
|
115
|
+
if (process.env["WT_SESSION"]) {
|
|
116
|
+
// Check if chafa or img2sixel is available for sixel encoding
|
|
117
|
+
if (commandExists("chafa") || commandExists("img2sixel")) {
|
|
118
|
+
return "sixel";
|
|
119
|
+
}
|
|
120
|
+
// Fall through to halfblock — Windows Terminal also does colors fine
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Tabby / Hyper typically support iTerm2
|
|
124
|
+
if (termEmulator.includes("tabby") || termEmulator.includes("hyper")) {
|
|
125
|
+
return "iterm2";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// xterm with sixel support
|
|
129
|
+
if (term.includes("xterm") && commandExists("img2sixel")) {
|
|
130
|
+
return "sixel";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return "halfblock";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function commandExists(cmd: string): boolean {
|
|
137
|
+
try {
|
|
138
|
+
execSync(process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`, {
|
|
139
|
+
stdio: "pipe",
|
|
140
|
+
timeout: 2000,
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── renderers ─────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
// iTerm2 inline image protocol (OSC 1337)
|
|
151
|
+
function renderITerm2(buf: Buffer, width: string | number): string {
|
|
152
|
+
const b64 = buf.toString("base64");
|
|
153
|
+
const widthParam =
|
|
154
|
+
typeof width === "number" ? `width=${width}` : `width=${width}`;
|
|
155
|
+
// ESC ] 1337 ; File=inline=1;width=...: <base64> BEL
|
|
156
|
+
return `\x1b]1337;File=inline=1;${widthParam};preserveAspectRatio=1:${b64}\x07\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Kitty graphics protocol (APC, chunked base64)
|
|
160
|
+
function renderKitty(buf: Buffer, width: string | number): string {
|
|
161
|
+
const b64 = buf.toString("base64");
|
|
162
|
+
const chunkSize = 4096;
|
|
163
|
+
const chunks: string[] = [];
|
|
164
|
+
for (let i = 0; i < b64.length; i += chunkSize) {
|
|
165
|
+
chunks.push(b64.slice(i, i + chunkSize));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cols =
|
|
169
|
+
typeof width === "number"
|
|
170
|
+
? width
|
|
171
|
+
: width.endsWith("%")
|
|
172
|
+
? Math.floor((process.stdout.columns ?? 80) * (parseInt(width) / 100))
|
|
173
|
+
: parseInt(width);
|
|
174
|
+
|
|
175
|
+
let result = "";
|
|
176
|
+
chunks.forEach((chunk, idx) => {
|
|
177
|
+
const isLast = idx === chunks.length - 1;
|
|
178
|
+
const more = isLast ? 0 : 1;
|
|
179
|
+
if (idx === 0) {
|
|
180
|
+
// First chunk: f=100 (PNG), a=T (transmit+display), c=columns
|
|
181
|
+
result += `\x1b_Ga=T,f=100,m=${more},c=${cols};${chunk}\x1b\\`;
|
|
182
|
+
} else {
|
|
183
|
+
result += `\x1b_Gm=${more};${chunk}\x1b\\`;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return result + "\n";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sixel via chafa (preferred, auto-detects format) or img2sixel
|
|
190
|
+
function renderSixel(
|
|
191
|
+
buf: Buffer,
|
|
192
|
+
src: string,
|
|
193
|
+
repoPath: string,
|
|
194
|
+
width: string | number,
|
|
195
|
+
): string {
|
|
196
|
+
const widthCols =
|
|
197
|
+
typeof width === "number"
|
|
198
|
+
? width
|
|
199
|
+
: width.endsWith("%")
|
|
200
|
+
? Math.floor((process.stdout.columns ?? 80) * (parseInt(width) / 100))
|
|
201
|
+
: parseInt(width);
|
|
202
|
+
|
|
203
|
+
// Write buf to a temp file so we can pass it to CLI tools
|
|
204
|
+
const tmpPath = path.join(
|
|
205
|
+
process.env["TEMP"] ?? process.env["TMPDIR"] ?? "/tmp",
|
|
206
|
+
`lens_img_${Date.now()}.bin`,
|
|
207
|
+
);
|
|
208
|
+
const { writeFileSync, unlinkSync } = require("fs") as typeof import("fs");
|
|
209
|
+
writeFileSync(tmpPath, buf);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
if (commandExists("chafa")) {
|
|
213
|
+
const result = execSync(
|
|
214
|
+
`chafa --format sixel --size ${widthCols}x40 "${tmpPath}"`,
|
|
215
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15_000 },
|
|
216
|
+
);
|
|
217
|
+
return result + "\n";
|
|
218
|
+
}
|
|
219
|
+
if (commandExists("img2sixel")) {
|
|
220
|
+
const result = execSync(`img2sixel -w ${widthCols * 8} "${tmpPath}"`, {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
223
|
+
timeout: 15_000,
|
|
224
|
+
});
|
|
225
|
+
return result + "\n";
|
|
226
|
+
}
|
|
227
|
+
throw new Error("no sixel encoder found");
|
|
228
|
+
} finally {
|
|
229
|
+
try {
|
|
230
|
+
unlinkSync(tmpPath);
|
|
231
|
+
} catch {
|
|
232
|
+
/* ignore */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Half-block fallback via terminal-image
|
|
238
|
+
async function renderHalfBlock(
|
|
239
|
+
buf: Buffer,
|
|
240
|
+
width: string | number,
|
|
241
|
+
): Promise<string> {
|
|
242
|
+
let terminalImage: any;
|
|
243
|
+
try {
|
|
244
|
+
terminalImage = await import("terminal-image");
|
|
245
|
+
terminalImage = terminalImage.default ?? terminalImage;
|
|
246
|
+
} catch {
|
|
247
|
+
return (
|
|
248
|
+
"Error: terminal-image not installed (npm install terminal-image).\n" +
|
|
249
|
+
"For better image display, install chafa: https://hpjansson.org/chafa/\n" +
|
|
250
|
+
" Windows: winget install hpjansson.chafa\n" +
|
|
251
|
+
" macOS: brew install chafa\n" +
|
|
252
|
+
" Linux: sudo apt install chafa"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return await terminalImage.buffer(buf, {
|
|
256
|
+
width,
|
|
257
|
+
preserveAspectRatio: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── main render ───────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
async function renderImage(
|
|
264
|
+
input: ViewImageInput,
|
|
265
|
+
repoPath: string,
|
|
266
|
+
): Promise<string> {
|
|
267
|
+
const buf = await fetchBytes(input.src, repoPath);
|
|
268
|
+
const width = input.width ?? "80%";
|
|
269
|
+
const protocol =
|
|
270
|
+
input.protocol === "auto" || !input.protocol
|
|
271
|
+
? detectProtocol()
|
|
272
|
+
: input.protocol;
|
|
273
|
+
|
|
274
|
+
switch (protocol) {
|
|
275
|
+
case "iterm2":
|
|
276
|
+
return renderITerm2(buf, width);
|
|
277
|
+
|
|
278
|
+
case "kitty":
|
|
279
|
+
return renderKitty(buf, width);
|
|
280
|
+
|
|
281
|
+
case "sixel":
|
|
282
|
+
try {
|
|
283
|
+
return renderSixel(buf, input.src, repoPath, width);
|
|
284
|
+
} catch (e: any) {
|
|
285
|
+
// Sixel encoder missing — fall through to halfblock
|
|
286
|
+
return (
|
|
287
|
+
`(sixel encoder not found — install chafa for better quality)\n` +
|
|
288
|
+
(await renderHalfBlock(buf, width))
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case "halfblock":
|
|
293
|
+
default:
|
|
294
|
+
return await renderHalfBlock(buf, width);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── tool ──────────────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
export const viewImageTool: Tool<ViewImageInput> = {
|
|
301
|
+
name: "view-image",
|
|
302
|
+
description:
|
|
303
|
+
"display an image in the terminal (auto-detects iTerm2/Kitty/Sixel/half-block)",
|
|
304
|
+
safe: true,
|
|
305
|
+
permissionLabel: "view image",
|
|
306
|
+
|
|
307
|
+
systemPromptEntry: (i) =>
|
|
308
|
+
[
|
|
309
|
+
`### ${i}. view-image — display an image in the terminal`,
|
|
310
|
+
`<view-image>screenshot.png</view-image>`,
|
|
311
|
+
`<view-image>https://example.com/banner.jpg</view-image>`,
|
|
312
|
+
`<view-image>{"src": "assets/logo.png", "width": "60%"}</view-image>`,
|
|
313
|
+
`<view-image>{"src": "photo.jpg", "width": "80%", "protocol": "iterm2"}</view-image>`,
|
|
314
|
+
`Protocols: auto (default), iterm2, kitty, sixel, halfblock`,
|
|
315
|
+
`Width: percentage "50%" or column count 40. Default "80%"`,
|
|
316
|
+
`Supported formats: PNG, JPG, GIF, WebP, BMP, TIFF`,
|
|
317
|
+
].join("\n"),
|
|
318
|
+
|
|
319
|
+
parseInput: parseViewImageInput,
|
|
320
|
+
|
|
321
|
+
summariseInput: ({ src, width }) =>
|
|
322
|
+
`${path.basename(src)} (${width ?? "80%"})`,
|
|
323
|
+
|
|
324
|
+
execute: async (input, ctx) => {
|
|
325
|
+
try {
|
|
326
|
+
const ansi = await renderImage(input, ctx.repoPath);
|
|
327
|
+
return { kind: "image" as any, value: ansi };
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return {
|
|
330
|
+
kind: "text",
|
|
331
|
+
value: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
};
|
package/src/tools/web.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// ── HTML helpers ──────────────────────────────────────────────────────────────
|
|
2
|
-
|
|
3
1
|
function stripTags(html: string): string {
|
|
4
2
|
return html
|
|
5
3
|
.replace(/<[^>]+>/g, " ")
|
|
@@ -73,8 +71,6 @@ function extractLists(html: string): string {
|
|
|
73
71
|
: "";
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
// ── Fetch ─────────────────────────────────────────────────────────────────────
|
|
77
|
-
|
|
78
74
|
export async function fetchUrl(url: string): Promise<string> {
|
|
79
75
|
const res = await fetch(url, {
|
|
80
76
|
headers: {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { existsSync, readdirSync } from "fs";
|
|
4
|
+
|
|
5
|
+
const ADDONS_DIR = path.join(os.homedir(), ".lens", "addons");
|
|
6
|
+
|
|
7
|
+
export async function loadAddons(): Promise<void> {
|
|
8
|
+
if (!existsSync(ADDONS_DIR)) {
|
|
9
|
+
// Silently skip — no addons directory yet
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const files = readdirSync(ADDONS_DIR).filter(
|
|
14
|
+
(f) => f.endsWith(".js") && !f.startsWith("_"),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const fullPath = path.join(ADDONS_DIR, file);
|
|
19
|
+
try {
|
|
20
|
+
await import(fullPath);
|
|
21
|
+
console.log(`[addons] loaded: ${file}\n`);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(
|
|
24
|
+
`[addons] failed to load ${file}:`,
|
|
25
|
+
err instanceof Error ? err.message : String(err),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/utils/chat.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
// ── chat.ts ───────────────────────────────────────────────────────────────────
|
|
2
1
|
//
|
|
3
|
-
// Response parsing and API calls.
|
|
4
|
-
// Tool parsing is now fully driven by the ToolRegistry — adding a new tool
|
|
5
|
-
// to the registry automatically makes it parseable here.
|
|
6
2
|
|
|
7
3
|
export {
|
|
8
4
|
walkDir,
|
|
@@ -22,7 +18,7 @@ export { buildSystemPrompt, FEW_SHOT_MESSAGES } from "../prompts";
|
|
|
22
18
|
import type { Message } from "../types/chat";
|
|
23
19
|
import type { Provider } from "../types/config";
|
|
24
20
|
import { FEW_SHOT_MESSAGES } from "../prompts";
|
|
25
|
-
import { registry } from "
|
|
21
|
+
import { registry } from "../utils/tools/registry";
|
|
26
22
|
import type { FilePatch } from "../components/repo/DiffViewer";
|
|
27
23
|
|
|
28
24
|
export type ParsedResponse =
|
|
@@ -43,6 +39,8 @@ export type ParsedResponse =
|
|
|
43
39
|
remainder?: string;
|
|
44
40
|
};
|
|
45
41
|
|
|
42
|
+
const MAX_HISTORY = 30;
|
|
43
|
+
|
|
46
44
|
export function parseResponse(text: string): ParsedResponse {
|
|
47
45
|
const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
|
|
48
46
|
|
|
@@ -56,7 +54,6 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
56
54
|
for (const toolName of registry.names()) {
|
|
57
55
|
const escaped = toolName.replace(/[-]/g, "\\-");
|
|
58
56
|
|
|
59
|
-
// XML tag
|
|
60
57
|
const xmlRe = new RegExp(`<${escaped}>([\\s\\S]*?)<\\/${escaped}>`, "g");
|
|
61
58
|
xmlRe.lastIndex = 0;
|
|
62
59
|
const xmlM = xmlRe.exec(scanText);
|
|
@@ -78,7 +75,6 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
78
75
|
}
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
// Fenced code block fallback
|
|
82
78
|
const fencedRe = new RegExp(
|
|
83
79
|
`\`\`\`${escaped}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\``,
|
|
84
80
|
"g",
|
|
@@ -114,7 +110,6 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
114
110
|
const afterMatch = text.slice(match.index + match[0].length).trim();
|
|
115
111
|
const remainder = afterMatch.length > 0 ? afterMatch : undefined;
|
|
116
112
|
|
|
117
|
-
// Special UI variants
|
|
118
113
|
if (toolName === "changes") {
|
|
119
114
|
try {
|
|
120
115
|
const parsed = JSON.parse(body) as {
|
|
@@ -142,7 +137,6 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
142
137
|
};
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
// Generic tool
|
|
146
140
|
const tool = registry.get(toolName);
|
|
147
141
|
if (!tool) return { kind: "text", content: text.trim() };
|
|
148
142
|
|
|
@@ -159,15 +153,11 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
159
153
|
};
|
|
160
154
|
}
|
|
161
155
|
|
|
162
|
-
// ── Clone tag helper ──────────────────────────────────────────────────────────
|
|
163
|
-
|
|
164
156
|
export function parseCloneTag(text: string): string | null {
|
|
165
157
|
const m = text.match(/<clone>([\s\S]*?)<\/clone>/);
|
|
166
158
|
return m ? m[1]!.trim() : null;
|
|
167
159
|
}
|
|
168
160
|
|
|
169
|
-
// ── GitHub URL detection ──────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
161
|
export function extractGithubUrl(text: string): string | null {
|
|
172
162
|
const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
|
|
173
163
|
return match ? match[0]! : null;
|
|
@@ -178,12 +168,12 @@ export function toCloneUrl(url: string): string {
|
|
|
178
168
|
return clean.endsWith(".git") ? clean : `${clean}.git`;
|
|
179
169
|
}
|
|
180
170
|
|
|
181
|
-
// ── API call ──────────────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
171
|
function buildApiMessages(
|
|
184
172
|
messages: Message[],
|
|
185
173
|
): { role: string; content: string }[] {
|
|
186
|
-
|
|
174
|
+
const recent = messages.slice(-MAX_HISTORY);
|
|
175
|
+
|
|
176
|
+
return recent.map((m) => {
|
|
187
177
|
if (m.type === "tool") {
|
|
188
178
|
if (!m.approved) {
|
|
189
179
|
return {
|
|
@@ -222,7 +212,7 @@ export async function callChat(
|
|
|
222
212
|
};
|
|
223
213
|
body = {
|
|
224
214
|
model: provider.model,
|
|
225
|
-
max_tokens:
|
|
215
|
+
max_tokens: 16384,
|
|
226
216
|
system: systemPrompt,
|
|
227
217
|
messages: apiMessages,
|
|
228
218
|
};
|
|
@@ -235,7 +225,7 @@ export async function callChat(
|
|
|
235
225
|
};
|
|
236
226
|
body = {
|
|
237
227
|
model: provider.model,
|
|
238
|
-
max_tokens:
|
|
228
|
+
max_tokens: 16384,
|
|
239
229
|
messages: [{ role: "system", content: systemPrompt }, ...apiMessages],
|
|
240
230
|
};
|
|
241
231
|
}
|
package/src/utils/memory.ts
CHANGED
|
@@ -15,14 +15,12 @@ export type MemoryEntry = {
|
|
|
15
15
|
detail: string;
|
|
16
16
|
summary: string;
|
|
17
17
|
timestamp: string;
|
|
18
|
-
repoPath: string;
|
|
19
18
|
};
|
|
20
19
|
|
|
21
20
|
export type Memory = {
|
|
22
21
|
id: string;
|
|
23
22
|
content: string;
|
|
24
23
|
timestamp: string;
|
|
25
|
-
repoPath: string;
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
export type MemoryFile = {
|
|
@@ -64,9 +62,9 @@ export function appendMemory(entry: Omit<MemoryEntry, "timestamp">): void {
|
|
|
64
62
|
|
|
65
63
|
export function buildMemorySummary(repoPath: string): string {
|
|
66
64
|
const m = loadMemoryFile();
|
|
67
|
-
const relevant = m.entries.
|
|
65
|
+
const relevant = m.entries.slice(-50);
|
|
68
66
|
|
|
69
|
-
const memories = m.memories
|
|
67
|
+
const memories = m.memories;
|
|
70
68
|
|
|
71
69
|
const parts: string[] = [];
|
|
72
70
|
|
|
@@ -92,13 +90,13 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
export function getRepoMemory(repoPath: string): MemoryEntry[] {
|
|
95
|
-
return loadMemoryFile().entries
|
|
93
|
+
return loadMemoryFile().entries;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
export function clearRepoMemory(repoPath: string): void {
|
|
99
97
|
const m = loadMemoryFile();
|
|
100
|
-
m.entries = m.entries
|
|
101
|
-
m.memories = m.memories
|
|
98
|
+
m.entries = m.entries = [];
|
|
99
|
+
m.memories = m.memories = [];
|
|
102
100
|
saveMemoryFile(m);
|
|
103
101
|
}
|
|
104
102
|
|
|
@@ -114,7 +112,6 @@ export function addMemory(content: string, repoPath: string): Memory {
|
|
|
114
112
|
id: generateId(),
|
|
115
113
|
content,
|
|
116
114
|
timestamp: new Date().toISOString(),
|
|
117
|
-
repoPath,
|
|
118
115
|
};
|
|
119
116
|
m.memories.push(memory);
|
|
120
117
|
saveMemoryFile(m);
|
|
@@ -124,14 +121,12 @@ export function addMemory(content: string, repoPath: string): Memory {
|
|
|
124
121
|
export function deleteMemory(id: string, repoPath: string): boolean {
|
|
125
122
|
const m = loadMemoryFile();
|
|
126
123
|
const before = m.memories.length;
|
|
127
|
-
m.memories = m.memories.filter(
|
|
128
|
-
(mem) => !(mem.id === id && mem.repoPath === repoPath),
|
|
129
|
-
);
|
|
124
|
+
m.memories = m.memories.filter((mem) => !(mem.id === id));
|
|
130
125
|
if (m.memories.length === before) return false;
|
|
131
126
|
saveMemoryFile(m);
|
|
132
127
|
return true;
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
export function listMemories(repoPath: string): Memory[] {
|
|
136
|
-
return loadMemoryFile().memories
|
|
131
|
+
return loadMemoryFile().memories;
|
|
137
132
|
}
|