@ridit/lens 0.2.0 → 0.2.2

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/dist/index.mjs CHANGED
@@ -44302,6 +44302,8 @@ function fetchFileTree(repoPath) {
44302
44302
  });
44303
44303
  }
44304
44304
  function readImportantFiles(repoPath, files) {
44305
+ if (files.length > 100)
44306
+ return [];
44305
44307
  return files.filter(isImportantFile).flatMap((filePath) => {
44306
44308
  try {
44307
44309
  const content = readFileSync2(path2.join(repoPath, filePath), "utf-8");
@@ -48630,15 +48632,16 @@ var TaskCommand = ({
48630
48632
  };
48631
48633
 
48632
48634
  // src/commands/chat.tsx
48633
- import { existsSync as existsSync12 } from "fs";
48634
- import path17 from "path";
48635
+ import { existsSync as existsSync14 } from "fs";
48636
+ import path19 from "path";
48635
48637
 
48636
48638
  // src/components/chat/ChatRunner.tsx
48637
48639
  var import_react48 = __toESM(require_react(), 1);
48638
- import path16 from "path";
48639
- import os7 from "os";
48640
+ var import_react49 = __toESM(require_react(), 1);
48641
+ import path18 from "path";
48642
+ import os9 from "os";
48640
48643
 
48641
- // src/utils/chat.ts
48644
+ // src/tools/files.ts
48642
48645
  import path14 from "path";
48643
48646
  import {
48644
48647
  existsSync as existsSync10,
@@ -48648,576 +48651,1009 @@ import {
48648
48651
  statSync as statSync4,
48649
48652
  writeFileSync as writeFileSync6
48650
48653
  } from "fs";
48651
- function buildSystemPrompt(files, historySummary = "") {
48652
- const fileList = files.map((f) => `### ${f.path}
48653
- \`\`\`
48654
- ${f.content.slice(0, 2000)}
48655
- \`\`\``).join(`
48656
-
48657
- `);
48658
- return `You are an expert software engineer assistant with access to the user's codebase and eleven tools.
48659
-
48660
- ## TOOLS
48661
-
48662
- You have exactly eleven tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
48663
-
48664
- ### 1. fetch — load a URL
48665
- <fetch>https://example.com</fetch>
48666
-
48667
- ### 2. shell — run a terminal command
48668
- <shell>node -v</shell>
48669
-
48670
- ### 3. read-file read a file from the repo
48671
- <read-file>src/foo.ts</read-file>
48672
-
48673
- ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
48674
- <read-folder>src/components</read-folder>
48675
-
48676
- ### 5. grep search for a pattern across files in the repo (cross-platform, no shell needed)
48677
- <grep>
48678
- {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
48679
- </grep>
48680
-
48681
- ### 6. write-file — create or overwrite a file
48682
- <write-file>
48683
- {"path": "data/output.csv", "content": "col1,col2
48684
- val1,val2"}
48685
- </write-file>
48686
-
48687
- ### 7. delete-file — permanently delete a single file
48688
- <delete-file>src/old-component.tsx</delete-file>
48689
-
48690
- ### 8. delete-folder — permanently delete a folder and all its contents
48691
- <delete-folder>src/legacy</delete-folder>
48692
-
48693
- ### 9. open-url — open a URL in the user's default browser
48694
- <open-url>https://github.com/owner/repo</open-url>
48695
-
48696
- ### 10. generate-pdf — generate a PDF file from markdown-style content
48697
- <generate-pdf>
48698
- {"path": "output/report.pdf", "content": "# Title
48699
-
48700
- Some body text.
48701
-
48702
- ## Section
48703
-
48704
- More content."}
48705
- </generate-pdf>
48706
-
48707
- ### 11. search search the internet for anything you are unsure about
48708
- <search>how to use React useEffect cleanup function</search>
48709
-
48710
- ### 12. clone — clone a GitHub repo so you can explore and discuss it
48711
- <clone>https://github.com/owner/repo</clone>
48654
+ var SKIP_DIRS4 = new Set([
48655
+ "node_modules",
48656
+ ".git",
48657
+ "dist",
48658
+ "build",
48659
+ ".next",
48660
+ "out",
48661
+ "coverage",
48662
+ "__pycache__",
48663
+ ".venv",
48664
+ "venv"
48665
+ ]);
48666
+ function walkDir3(dir, base2 = dir, results = []) {
48667
+ let entries;
48668
+ try {
48669
+ entries = readdirSync3(dir, { encoding: "utf-8" });
48670
+ } catch {
48671
+ return results;
48672
+ }
48673
+ for (const entry of entries) {
48674
+ if (results.length >= 100)
48675
+ return results;
48676
+ if (SKIP_DIRS4.has(entry))
48677
+ continue;
48678
+ const full = path14.join(dir, entry);
48679
+ const rel = path14.relative(base2, full).replace(/\\/g, "/");
48680
+ let isDir = false;
48681
+ try {
48682
+ isDir = statSync4(full).isDirectory();
48683
+ } catch {
48684
+ continue;
48685
+ }
48686
+ if (isDir)
48687
+ walkDir3(full, base2, results);
48688
+ else
48689
+ results.push(rel);
48690
+ }
48691
+ return results;
48692
+ }
48693
+ function applyPatches3(repoPath, patches) {
48694
+ for (const patch of patches) {
48695
+ const fullPath = path14.join(repoPath, patch.path);
48696
+ const dir = path14.dirname(fullPath);
48697
+ if (!existsSync10(dir))
48698
+ mkdirSync4(dir, { recursive: true });
48699
+ writeFileSync6(fullPath, patch.content, "utf-8");
48700
+ }
48701
+ }
48702
+ function readFile(filePath, repoPath) {
48703
+ const candidates = path14.isAbsolute(filePath) ? [filePath] : [filePath, path14.join(repoPath, filePath)];
48704
+ for (const candidate of candidates) {
48705
+ if (existsSync10(candidate)) {
48706
+ try {
48707
+ const content = readFileSync10(candidate, "utf-8");
48708
+ const lines = content.split(`
48709
+ `).length;
48710
+ return `File: ${candidate} (${lines} lines)
48712
48711
 
48713
- ### 13. changes — propose code edits (shown as a diff for user approval)
48714
- <changes>
48715
- {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
48716
- </changes>
48712
+ ${content.slice(0, 8000)}${content.length > 8000 ? `
48717
48713
 
48718
- ## RULES
48714
+ (truncated)` : ""}`;
48715
+ } catch (err) {
48716
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
48717
+ }
48718
+ }
48719
+ }
48720
+ return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
48721
+ }
48722
+ function readFolder(folderPath, repoPath) {
48723
+ const sanitized = folderPath.replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "").trim();
48724
+ const candidates = path14.isAbsolute(sanitized) ? [sanitized] : [sanitized, path14.join(repoPath, sanitized)];
48725
+ for (const candidate of candidates) {
48726
+ if (!existsSync10(candidate))
48727
+ continue;
48728
+ let stat;
48729
+ try {
48730
+ stat = statSync4(candidate);
48731
+ } catch {
48732
+ continue;
48733
+ }
48734
+ if (!stat.isDirectory()) {
48735
+ return `Not a directory: ${candidate}. Use read-file to read a file.`;
48736
+ }
48737
+ let entries;
48738
+ try {
48739
+ entries = readdirSync3(candidate, { encoding: "utf-8" });
48740
+ } catch (err) {
48741
+ return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
48742
+ }
48743
+ const files = [];
48744
+ const subfolders = [];
48745
+ for (const entry of entries) {
48746
+ if (entry.startsWith(".") && entry !== ".env")
48747
+ continue;
48748
+ const full = path14.join(candidate, entry);
48749
+ try {
48750
+ if (statSync4(full).isDirectory())
48751
+ subfolders.push(`${entry}/`);
48752
+ else
48753
+ files.push(entry);
48754
+ } catch {}
48755
+ }
48756
+ const total = files.length + subfolders.length;
48757
+ const lines = [`Folder: ${candidate} (${total} entries)`, ""];
48758
+ if (files.length > 0) {
48759
+ lines.push("Files:");
48760
+ files.forEach((f) => lines.push(` ${f}`));
48761
+ }
48762
+ if (subfolders.length > 0) {
48763
+ if (files.length > 0)
48764
+ lines.push("");
48765
+ lines.push("Subfolders:");
48766
+ subfolders.forEach((d) => lines.push(` ${d}`));
48767
+ }
48768
+ if (total === 0)
48769
+ lines.push("(empty folder)");
48770
+ return lines.join(`
48771
+ `);
48772
+ }
48773
+ return `Folder not found: ${sanitized}`;
48774
+ }
48775
+ function grepFiles(pattern, glob, repoPath) {
48776
+ let regex2;
48777
+ try {
48778
+ regex2 = new RegExp(pattern, "i");
48779
+ } catch {
48780
+ regex2 = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
48781
+ }
48782
+ const globToFilter = (g) => {
48783
+ const cleaned = g.replace(/^\*\*\//, "");
48784
+ const parts = cleaned.split("/");
48785
+ const ext = parts[parts.length - 1];
48786
+ const prefix = parts.slice(0, -1).join("/");
48787
+ return (rel) => {
48788
+ if (ext?.startsWith("*.")) {
48789
+ if (!rel.endsWith(ext.slice(1)))
48790
+ return false;
48791
+ } else if (ext && !ext.includes("*")) {
48792
+ if (!rel.endsWith(ext))
48793
+ return false;
48794
+ }
48795
+ if (prefix && !prefix.includes("*")) {
48796
+ if (!rel.startsWith(prefix))
48797
+ return false;
48798
+ }
48799
+ return true;
48800
+ };
48801
+ };
48802
+ const filter2 = globToFilter(glob);
48803
+ const allFiles = walkDir3(repoPath);
48804
+ const matchedFiles = allFiles.filter(filter2);
48805
+ if (matchedFiles.length === 0)
48806
+ return `No files matched glob: ${glob}`;
48807
+ const results = [];
48808
+ let totalMatches = 0;
48809
+ for (const relPath of matchedFiles) {
48810
+ const fullPath = path14.join(repoPath, relPath);
48811
+ let content;
48812
+ try {
48813
+ content = readFileSync10(fullPath, "utf-8");
48814
+ } catch {
48815
+ continue;
48816
+ }
48817
+ const fileLines = content.split(`
48818
+ `);
48819
+ const fileMatches = [];
48820
+ fileLines.forEach((line, i) => {
48821
+ if (regex2.test(line)) {
48822
+ fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
48823
+ totalMatches++;
48824
+ }
48825
+ });
48826
+ if (fileMatches.length > 0)
48827
+ results.push(`${relPath}
48828
+ ${fileMatches.join(`
48829
+ `)}`);
48830
+ if (totalMatches >= 200) {
48831
+ results.push("(truncated — too many matches)");
48832
+ break;
48833
+ }
48834
+ }
48835
+ if (results.length === 0)
48836
+ return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
48837
+ return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)
48719
48838
 
48720
- 1. When you need to use a tool, output ONLY the XML tag — nothing before or after it in that response
48721
- 2. ONE tool per response — emit the tag, then stop completely
48722
- 3. After the user approves and you get the result, continue your analysis in the next response
48723
- 4. NEVER print a URL, command, filename, or JSON blob as plain text when you should be using a tool
48724
- 5. NEVER say "I'll fetch" / "run this command" / "here's the write-file" — just emit the tag
48725
- 6. NEVER use shell to run git clone — always use the clone tag instead
48726
- 7. NEVER use shell to list files or folders (no ls, dir, find, git ls-files, tree) — ALWAYS use read-folder instead
48727
- 8. NEVER use shell to read a file (no cat, type, Get-Content) — ALWAYS use read-file instead
48728
- 9. NEVER use shell grep, findstr, or Select-String to search file contents — ALWAYS use grep instead
48729
- 10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
48730
- 11. write-file content field must be the COMPLETE file content, never empty or placeholder
48731
- 12. After a write-file succeeds, do NOT repeat it — trust the result and move on
48732
- 13. After a write-file succeeds, tell the user it is done immediately — do NOT auto-read the file back to verify
48733
- 14. NEVER apologize and redo a tool call you already made — if write-file or shell ran and returned a result, it worked, do not run it again
48734
- 15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
48735
- 16. NEVER second-guess yourself mid-response — commit to your answer
48736
- 17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
48737
- 18. If you have already retrieved a result for a path in this conversation, do NOT request it again — use the result you already have
48738
- 19. Every shell command runs from the repo root — \`cd\` has NO persistent effect. NEVER use \`cd\` alone. Use full paths or combine with && e.g. \`cd list && bun run index.ts\`
48739
- 20. write-file paths are relative to the repo root — if creating files in a subfolder write the full relative path e.g. \`list/src/index.tsx\` NOT \`src/index.tsx\`
48740
- 21. When scaffolding a new project in a subfolder, ALL write-file paths must start with that subfolder name e.g. \`list/package.json\`, \`list/src/index.tsx\`
48741
- 22. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
48742
- 23. When explaining how to use a tool in text, use [tag] bracket notation or a fenced code block — NEVER emit a real XML tool tag as part of an explanation or example
48743
- 24. NEVER chain tool calls unless the user's request explicitly requires multiple steps
48744
- 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
48745
- 26. NEVER use markdown formatting in plain text responses — no **bold**, no *italics*, no # headings, no bullet points with -, *, or +, no numbered lists, no backtick inline code. Write in plain prose. Only use fenced \`\`\` code blocks when showing actual code.
48839
+ ${results.join(`
48746
48840
 
48747
- ## CRITICAL: READ BEFORE YOU WRITE
48748
-
48749
- These rules are mandatory whenever you plan to edit or create a file:
48841
+ `)}`;
48842
+ }
48843
+ function writeFile(filePath, content, repoPath) {
48844
+ const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
48845
+ try {
48846
+ const dir = path14.dirname(fullPath);
48847
+ if (!existsSync10(dir))
48848
+ mkdirSync4(dir, { recursive: true });
48849
+ writeFileSync6(fullPath, content, "utf-8");
48850
+ const lines = content.split(`
48851
+ `).length;
48852
+ return `Written: ${fullPath} (${lines} lines, ${content.length} bytes)`;
48853
+ } catch (err) {
48854
+ return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
48855
+ }
48856
+ }
48857
+ function deleteFile(filePath, repoPath) {
48858
+ const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
48859
+ try {
48860
+ if (!existsSync10(fullPath))
48861
+ return `File not found: ${fullPath}`;
48862
+ const { unlinkSync } = __require("fs");
48863
+ unlinkSync(fullPath);
48864
+ return `Deleted: ${fullPath}`;
48865
+ } catch (err) {
48866
+ return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
48867
+ }
48868
+ }
48869
+ function deleteFolder(folderPath, repoPath) {
48870
+ const fullPath = path14.isAbsolute(folderPath) ? folderPath : path14.join(repoPath, folderPath);
48871
+ try {
48872
+ if (!existsSync10(fullPath))
48873
+ return `Folder not found: ${fullPath}`;
48874
+ const { rmSync } = __require("fs");
48875
+ rmSync(fullPath, { recursive: true, force: true });
48876
+ return `Deleted folder: ${fullPath}`;
48877
+ } catch (err) {
48878
+ return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
48879
+ }
48880
+ }
48881
+ // src/tools/shell.ts
48882
+ async function runShell(command, cwd2) {
48883
+ return new Promise((resolve) => {
48884
+ const { spawn: spawn2 } = __require("child_process");
48885
+ const isWin = process.platform === "win32";
48886
+ const shell = isWin ? "cmd.exe" : "/bin/sh";
48887
+ const shellFlag = isWin ? "/c" : "-c";
48888
+ const proc = spawn2(shell, [shellFlag, command], {
48889
+ cwd: cwd2,
48890
+ env: process.env,
48891
+ stdio: ["ignore", "pipe", "pipe"]
48892
+ });
48893
+ const chunks = [];
48894
+ const errChunks = [];
48895
+ proc.stdout.on("data", (d) => chunks.push(d));
48896
+ proc.stderr.on("data", (d) => errChunks.push(d));
48897
+ const killTimer = setTimeout(() => {
48898
+ proc.kill();
48899
+ resolve("(command timed out after 5 minutes)");
48900
+ }, 5 * 60 * 1000);
48901
+ proc.on("close", (code) => {
48902
+ clearTimeout(killTimer);
48903
+ const stdout = Buffer.concat(chunks).toString("utf-8").trim();
48904
+ const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
48905
+ const combined = [stdout, stderr].filter(Boolean).join(`
48906
+ `);
48907
+ resolve(combined || (code === 0 ? "(no output)" : `exit code ${code}`));
48908
+ });
48909
+ proc.on("error", (err) => {
48910
+ clearTimeout(killTimer);
48911
+ resolve(`Error: ${err.message}`);
48912
+ });
48913
+ });
48914
+ }
48915
+ function openUrl(url) {
48916
+ try {
48917
+ const { execSync: exec5 } = __require("child_process");
48918
+ const platform2 = process.platform;
48919
+ if (platform2 === "win32") {
48920
+ exec5(`start "" "${url}"`, { stdio: "ignore" });
48921
+ } else if (platform2 === "darwin") {
48922
+ exec5(`open "${url}"`, { stdio: "ignore" });
48923
+ } else {
48924
+ exec5(`xdg-open "${url}"`, { stdio: "ignore" });
48925
+ }
48926
+ return `Opened: ${url}`;
48927
+ } catch (err) {
48928
+ return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
48929
+ }
48930
+ }
48931
+ // src/tools/web.ts
48932
+ function stripTags(html) {
48933
+ return html.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#\d+;/g, " ").replace(/\s+/g, " ").trim();
48934
+ }
48935
+ function extractTables(html) {
48936
+ const tables = [];
48937
+ const tableRe = /<table[\s\S]*?<\/table>/gi;
48938
+ let tMatch;
48939
+ while ((tMatch = tableRe.exec(html)) !== null) {
48940
+ const tableHtml = tMatch[0];
48941
+ const rows = [];
48942
+ const rowRe = /<tr[\s\S]*?<\/tr>/gi;
48943
+ let rMatch;
48944
+ while ((rMatch = rowRe.exec(tableHtml)) !== null) {
48945
+ const cells = [];
48946
+ const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
48947
+ let cMatch;
48948
+ while ((cMatch = cellRe.exec(rMatch[0])) !== null) {
48949
+ cells.push(stripTags(cMatch[1] ?? ""));
48950
+ }
48951
+ if (cells.length > 0)
48952
+ rows.push(cells);
48953
+ }
48954
+ if (rows.length < 2)
48955
+ continue;
48956
+ const cols = Math.max(...rows.map((r) => r.length));
48957
+ const padded = rows.map((r) => {
48958
+ while (r.length < cols)
48959
+ r.push("");
48960
+ return r;
48961
+ });
48962
+ const widths = Array.from({ length: cols }, (_, ci) => Math.max(...padded.map((r) => (r[ci] ?? "").length), 3));
48963
+ const fmt = (r) => r.map((c, ci) => c.padEnd(widths[ci] ?? 0)).join(" | ");
48964
+ const header = fmt(padded[0]);
48965
+ const sep = widths.map((w) => "-".repeat(w)).join("-|-");
48966
+ const body = padded.slice(1).map(fmt).join(`
48967
+ `);
48968
+ tables.push(`${header}
48969
+ ${sep}
48970
+ ${body}`);
48971
+ }
48972
+ return tables.length > 0 ? `=== TABLES (${tables.length}) ===
48750
48973
 
48751
- ### Before modifying ANY existing file:
48752
- 1. ALWAYS use read-file on the exact file you plan to change FIRST
48753
- 2. Study the full current content — understand every import, every export, every type, every existing feature
48754
- 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
48755
- 4. If you are unsure what other files import from the file you are editing, use read-folder on the parent directory first to see what exists nearby, then read-file the relevant ones
48974
+ ${tables.join(`
48756
48975
 
48757
- ### Before adding a feature that touches multiple files:
48758
- 1. Use read-folder on the relevant directory to see what files exist
48759
- 2. Use read-file on each file you plan to touch
48760
- 3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
48976
+ ---
48761
48977
 
48762
- ### The golden rule for write-file and changes:
48763
- - The output file must contain EVERYTHING the original had, PLUS your new additions
48764
- - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
48765
- - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
48978
+ `)}` : "";
48979
+ }
48980
+ function extractLists(html) {
48981
+ const lists = [];
48982
+ const listRe = /<[ou]l[\s\S]*?<\/[ou]l>/gi;
48983
+ let lMatch;
48984
+ while ((lMatch = listRe.exec(html)) !== null) {
48985
+ const items = [];
48986
+ const itemRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
48987
+ let iMatch;
48988
+ while ((iMatch = itemRe.exec(lMatch[0])) !== null) {
48989
+ const text = stripTags(iMatch[1] ?? "");
48990
+ if (text.length > 2)
48991
+ items.push(`• ${text}`);
48992
+ }
48993
+ if (items.length > 1)
48994
+ lists.push(items.join(`
48995
+ `));
48996
+ }
48997
+ return lists.length > 0 ? `=== LISTS ===
48766
48998
 
48767
- ## WHEN TO USE read-folder:
48768
- - Before editing files in an unfamiliar directory — list it first to understand the structure
48769
- - When a feature spans multiple files and you are not sure what exists
48770
- - When the user asks you to explore or explain a part of the codebase
48771
-
48772
- ## SCAFFOLDING A NEW PROJECT (follow this exactly)
48773
-
48774
- When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app called list"):
48775
- 1. Create all files first using write-file with paths like \`list/package.json\`, \`list/src/index.tsx\`
48776
- 2. Then run \`cd list && bun install\` (or npm/pnpm) in one shell command
48777
- 3. Then run the project with \`cd list && bun run index.ts\` or whatever the entry point is
48778
- 4. NEVER run \`bun init\` — it is interactive and will hang. Create package.json manually with write-file instead
48779
- 5. TSX files need either tsconfig.json with \`"jsx": "react-jsx"\` or \`/** @jsxImportSource react */\` at the top
48780
-
48781
- ## FETCH → WRITE FLOW (follow this exactly when saving fetched data)
48782
-
48783
- 1. fetch the URL
48784
- 2. Analyze the result — count the rows, identify columns, check completeness
48785
- 3. Tell the user what you found: "Found X rows with columns: A, B, C. Writing now."
48786
- 4. emit write-file with correctly structured, complete content
48787
- 5. After write-file confirms success, emit read-file to verify
48788
- 6. Only after read-file confirms content is correct, tell the user it is done
48789
-
48790
- ## WHEN TO USE TOOLS
48791
-
48792
- - User shares any URL → fetch it immediately
48793
- - User asks to run anything → shell it immediately
48794
- - User asks to open a link, open a URL, or visit a website → open-url it immediately, do NOT use fetch
48795
- - User asks to delete a file → delete-file it immediately (requires approval)
48796
- - User asks to delete a folder or directory → delete-folder it immediately (requires approval)
48797
- - User asks to search for a pattern in files, find usages, find where something is defined → grep it immediately, NEVER use shell grep/findstr/Select-String
48798
- - User asks to read a file → read-file it immediately, NEVER use shell cat/type
48799
- - User asks what files are in a folder, or to explore/list a directory → read-folder it immediately, NEVER use shell ls/dir/find/git ls-files
48800
- - User asks to explore a folder or directory → read-folder it immediately
48801
- - User asks to save/create/write a file → write-file it immediately, then read-file to verify
48802
- - User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
48803
- - User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
48804
- - After clone succeeds, you will see context about the clone in the conversation. Wait for the user to ask a specific question before using any tools. Do NOT auto-read files, do NOT emit any tool tags until the user asks.
48805
- - You are unsure about an API, library, error, concept, or piece of code → search it immediately
48806
- - User asks about something recent or that you might not know → search it immediately
48807
- - You are about to say "I'm not sure" or "I don't know" → search instead of guessing
48808
-
48809
- ## shell IS ONLY FOR:
48810
- - Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
48811
- - Installing packages: \`npm install\`, \`pip install\`
48812
- - Building/testing: \`npm run build\`, \`bun test\`
48813
- - Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
48814
- - Anything that EXECUTES — not reads or lists
48999
+ ${lists.slice(0, 5).join(`
48815
49000
 
48816
- ## CODEBASE
49001
+ `)}` : "";
49002
+ }
49003
+ async function fetchUrl(url) {
49004
+ const res = await fetch(url, {
49005
+ headers: {
49006
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49007
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
49008
+ "Accept-Language": "en-US,en;q=0.5"
49009
+ },
49010
+ signal: AbortSignal.timeout(15000)
49011
+ });
49012
+ if (!res.ok)
49013
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49014
+ const contentType = res.headers.get("content-type") ?? "";
49015
+ if (contentType.includes("application/json")) {
49016
+ const json = await res.json();
49017
+ return JSON.stringify(json, null, 2).slice(0, 8000);
49018
+ }
49019
+ const html = await res.text();
49020
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
49021
+ const title = titleMatch ? stripTags(titleMatch[1]) : "No title";
49022
+ const tables = extractTables(html);
49023
+ const lists = extractLists(html);
49024
+ const bodyText = stripTags(html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "")).replace(/\s{3,}/g, `
48817
49025
 
48818
- ${fileList.length > 0 ? fileList : "(no files indexed)"}
49026
+ `).slice(0, 3000);
49027
+ const parts = [`PAGE: ${title}`, `URL: ${url}`];
49028
+ if (tables)
49029
+ parts.push(tables);
49030
+ if (lists)
49031
+ parts.push(lists);
49032
+ parts.push(`=== TEXT ===
49033
+ ${bodyText}`);
49034
+ return parts.join(`
48819
49035
 
48820
- ${historySummary}`;
49036
+ `);
48821
49037
  }
48822
- var FEW_SHOT_MESSAGES = [
48823
- {
48824
- role: "user",
48825
- content: "delete src/old-component.tsx"
48826
- },
48827
- {
48828
- role: "assistant",
48829
- content: "<delete-file>src/old-component.tsx</delete-file>"
48830
- },
48831
- {
48832
- role: "user",
48833
- content: `Here is the output from delete-file of src/old-component.tsx:
48834
-
48835
- Deleted: /repo/src/old-component.tsx
49038
+ async function searchWeb(query) {
49039
+ const encoded = encodeURIComponent(query);
49040
+ const ddgUrl = `https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1&skip_disambig=1`;
49041
+ try {
49042
+ const res = await fetch(ddgUrl, {
49043
+ headers: { "User-Agent": "Lens/1.0" },
49044
+ signal: AbortSignal.timeout(8000)
49045
+ });
49046
+ if (res.ok) {
49047
+ const data = await res.json();
49048
+ const parts = [`Search: ${query}`];
49049
+ if (data.Answer)
49050
+ parts.push(`Answer: ${data.Answer}`);
49051
+ if (data.AbstractText) {
49052
+ parts.push(`Summary: ${data.AbstractText}`);
49053
+ if (data.AbstractURL)
49054
+ parts.push(`Source: ${data.AbstractURL}`);
49055
+ }
49056
+ if (data.Infobox?.content?.length) {
49057
+ const fields = data.Infobox.content.slice(0, 8).map((f) => ` ${f.label}: ${f.value}`).join(`
49058
+ `);
49059
+ parts.push(`Info:
49060
+ ${fields}`);
49061
+ }
49062
+ if (data.RelatedTopics?.length) {
49063
+ const topics = data.RelatedTopics.filter((t) => t.Text).slice(0, 5).map((t) => ` - ${t.Text}`).join(`
49064
+ `);
49065
+ if (topics)
49066
+ parts.push(`Related:
49067
+ ${topics}`);
49068
+ }
49069
+ const result2 = parts.join(`
48836
49070
 
48837
- Please continue your response based on this output.`
48838
- },
48839
- {
48840
- role: "assistant",
48841
- content: "Done — `src/old-component.tsx` has been deleted."
48842
- },
48843
- {
48844
- role: "user",
48845
- content: "delete the legacy folder"
48846
- },
48847
- {
48848
- role: "assistant",
48849
- content: "<delete-folder>src/legacy</delete-folder>"
48850
- },
48851
- {
48852
- role: "user",
48853
- content: `Here is the output from delete-folder of src/legacy:
49071
+ `);
49072
+ if (result2.length > 60)
49073
+ return result2;
49074
+ }
49075
+ } catch {}
49076
+ try {
49077
+ const htmlUrl = `https://html.duckduckgo.com/html/?q=${encoded}`;
49078
+ const res = await fetch(htmlUrl, {
49079
+ headers: {
49080
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49081
+ Accept: "text/html"
49082
+ },
49083
+ signal: AbortSignal.timeout(1e4)
49084
+ });
49085
+ if (!res.ok)
49086
+ throw new Error(`HTTP ${res.status}`);
49087
+ const html = await res.text();
49088
+ const snippets = [];
49089
+ const snippetRe = /class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
49090
+ let m;
49091
+ while ((m = snippetRe.exec(html)) !== null && snippets.length < 6) {
49092
+ const text = m[1].replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
49093
+ if (text.length > 20)
49094
+ snippets.push(`- ${text}`);
49095
+ }
49096
+ const links = [];
49097
+ const linkRe = /class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
49098
+ while ((m = linkRe.exec(html)) !== null && links.length < 5) {
49099
+ const title = m[2].replace(/<[^>]+>/g, "").trim();
49100
+ const href = m[1];
49101
+ if (title && href)
49102
+ links.push(` ${title} — ${href}`);
49103
+ }
49104
+ if (snippets.length === 0 && links.length === 0)
49105
+ return `No results found for: ${query}`;
49106
+ const parts = [`Search results for: ${query}`];
49107
+ if (snippets.length > 0)
49108
+ parts.push(`Snippets:
49109
+ ${snippets.join(`
49110
+ `)}`);
49111
+ if (links.length > 0)
49112
+ parts.push(`Links:
49113
+ ${links.join(`
49114
+ `)}`);
49115
+ return parts.join(`
48854
49116
 
48855
- Deleted folder: /repo/src/legacy
49117
+ `);
49118
+ } catch (err) {
49119
+ return `Search failed: ${err instanceof Error ? err.message : String(err)}`;
49120
+ }
49121
+ }
49122
+ // src/tools/pdf.ts
49123
+ import path15 from "path";
49124
+ import os6 from "os";
49125
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
49126
+ function generatePdf(filePath, content, repoPath) {
49127
+ const fullPath = path15.isAbsolute(filePath) ? filePath : path15.join(repoPath, filePath);
49128
+ try {
49129
+ const dir = path15.dirname(fullPath);
49130
+ if (!existsSync11(dir))
49131
+ mkdirSync5(dir, { recursive: true });
49132
+ const escaped = content.replace(/\\/g, "\\\\").replace(/"""/g, "\\\"\\\"\\\"").replace(/\r/g, "");
49133
+ const script = `
49134
+ import sys
49135
+ try:
49136
+ from reportlab.lib.pagesizes import letter
49137
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
49138
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
49139
+ from reportlab.lib.units import inch
49140
+ from reportlab.lib import colors
49141
+ except ImportError:
49142
+ import subprocess
49143
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
49144
+ from reportlab.lib.pagesizes import letter
49145
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
49146
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
49147
+ from reportlab.lib.units import inch
49148
+ from reportlab.lib import colors
48856
49149
 
48857
- Please continue your response based on this output.`
48858
- },
48859
- {
48860
- role: "assistant",
48861
- content: "Done — the `src/legacy` folder and all its contents have been deleted."
48862
- },
48863
- {
48864
- role: "user",
48865
- content: "open https://github.com/microsoft/typescript"
48866
- },
48867
- {
48868
- role: "assistant",
48869
- content: "<open-url>https://github.com/microsoft/typescript</open-url>"
48870
- },
48871
- {
48872
- role: "user",
48873
- content: `Here is the output from open-url https://github.com/microsoft/typescript:
49150
+ doc = SimpleDocTemplate(
49151
+ r"""${fullPath}""",
49152
+ pagesize=letter,
49153
+ rightMargin=inch,
49154
+ leftMargin=inch,
49155
+ topMargin=inch,
49156
+ bottomMargin=inch,
49157
+ )
48874
49158
 
48875
- Opened: https://github.com/microsoft/typescript
49159
+ styles = getSampleStyleSheet()
49160
+ styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
49161
+ styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
49162
+ styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
49163
+ styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
49164
+ styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
48876
49165
 
48877
- Please continue your response based on this output.`
48878
- },
48879
- {
48880
- role: "assistant",
48881
- content: "Opened the TypeScript GitHub page in your browser."
48882
- },
48883
- {
48884
- role: "user",
48885
- content: "generate a PDF report about the project and save it to docs/report.pdf"
48886
- },
48887
- {
48888
- role: "assistant",
48889
- content: `<generate-pdf>
48890
- {"path": "docs/report.pdf", "content": "# Project Report\\n\\n## Overview\\n\\nThis document summarizes the project.\\n\\n## Details\\n\\nMore content here."}
48891
- </generate-pdf>`
48892
- },
48893
- {
48894
- role: "user",
48895
- content: `Here is the output from generate-pdf to docs/report.pdf:
49166
+ raw = """${escaped}"""
48896
49167
 
48897
- PDF generated: /repo/docs/report.pdf
49168
+ story = []
49169
+ for line in raw.split("\\n"):
49170
+ s = line.rstrip()
49171
+ if s.startswith("### "):
49172
+ story.append(Paragraph(s[4:], styles["H3"]))
49173
+ elif s.startswith("## "):
49174
+ story.append(Spacer(1, 6))
49175
+ story.append(Paragraph(s[3:], styles["H2"]))
49176
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
49177
+ elif s.startswith("# "):
49178
+ story.append(Paragraph(s[2:], styles["H1"]))
49179
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
49180
+ elif s.startswith("- ") or s.startswith("* "):
49181
+ story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
49182
+ elif s.startswith("---"):
49183
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
49184
+ elif s == "":
49185
+ story.append(Spacer(1, 6))
49186
+ else:
49187
+ import re
49188
+ s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
49189
+ s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
49190
+ s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
49191
+ story.append(Paragraph(s, styles["Body"]))
48898
49192
 
48899
- Please continue your response based on this output.`
48900
- },
48901
- {
48902
- role: "assistant",
48903
- content: "Done — the PDF report has been saved to `docs/report.pdf`."
48904
- },
48905
- {
48906
- role: "user",
48907
- content: 'grep -R "ChatRunner" -n src'
48908
- },
48909
- {
48910
- role: "assistant",
48911
- content: `<grep>
48912
- {"pattern": "ChatRunner", "glob": "src/**/*"}
48913
- </grep>`
48914
- },
48915
- {
48916
- role: "user",
48917
- content: `Here is the output from grep for "ChatRunner":
49193
+ doc.build(story)
49194
+ print("OK")
49195
+ `.replace("${fullPath}", fullPath.replace(/\\/g, "/")).replace("${escaped}", escaped);
49196
+ const tmpFile = path15.join(os6.tmpdir(), `lens_pdf_${Date.now()}.py`);
49197
+ writeFileSync7(tmpFile, script, "utf-8");
49198
+ const { execSync: execSync2 } = __require("child_process");
49199
+ execSync2(`python "${tmpFile}"`, { stdio: "pipe" });
49200
+ try {
49201
+ __require("fs").unlinkSync(tmpFile);
49202
+ } catch {}
49203
+ return `PDF generated: ${fullPath}`;
49204
+ } catch (err) {
49205
+ return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
49206
+ }
49207
+ }
49208
+ // src/prompts/system.ts
49209
+ function buildSystemPrompt(files, memorySummary = "", toolsSection) {
49210
+ const fileList = files.map((f) => `### ${f.path}
49211
+ \`\`\`
49212
+ ${f.content.slice(0, 2000)}
49213
+ \`\`\``).join(`
48918
49214
 
48919
- grep /ChatRunner/ src/**/* — 3 match(es) in 2 file(s)
49215
+ `);
49216
+ const tools = toolsSection ?? BUILTIN_TOOLS_SECTION;
49217
+ return `You are an expert software engineer assistant with access to the user's codebase and tools.
48920
49218
 
48921
- src/index.tsx
48922
- 12: import { ChatRunner } from "./components/chat/ChatRunner";
49219
+ ${tools}
48923
49220
 
48924
- src/components/chat/ChatRunner.tsx
48925
- 1: export const ChatRunner = ...
49221
+ ## MEMORY OPERATIONS
48926
49222
 
48927
- Please continue your response based on this output.`
48928
- },
48929
- {
48930
- role: "assistant",
48931
- content: "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`."
48932
- },
48933
- {
48934
- role: "user",
48935
- content: "find all usages of useInput in the codebase"
48936
- },
48937
- {
48938
- role: "assistant",
48939
- content: `<grep>
48940
- {"pattern": "useInput", "glob": "src/**/*.tsx"}
48941
- </grep>`
48942
- },
48943
- {
48944
- role: "user",
48945
- content: `Here is the output from grep for "useInput":
49223
+ You can save and delete memories at any time by emitting these tags alongside your normal response.
49224
+ They are stripped before display — the user will not see the raw tags.
48946
49225
 
48947
- grep /useInput/ src/**/*.tsx 2 match(es) in 1 file(s)
49226
+ ### memory-addsave something important to long-term memory for this repo
49227
+ <memory-add>User prefers TypeScript strict mode in all new files</memory-add>
48948
49228
 
48949
- src/components/chat/ChatRunner.tsx
48950
- 5: import { useInput } from "ink";
48951
- 210: useInput((input, key) => {
49229
+ ### memory-delete — delete a memory by its ID (shown in brackets like [abc123])
49230
+ <memory-delete>abc123</memory-delete>
48952
49231
 
48953
- Please continue your response based on this output.`
48954
- },
48955
- {
48956
- role: "assistant",
48957
- content: "`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210."
48958
- },
48959
- {
48960
- role: "user",
48961
- content: "show me how to use the read-file tool"
48962
- },
48963
- {
48964
- role: "assistant",
48965
- content: "To read a file, emit the tag alone in your response — nothing else on that turn:\n\n```text\n[read-file]src/components/Header.tsx[/read-file]\n```\n\nThe result comes back as the next user message and you can then analyze it."
48966
- },
48967
- {
48968
- role: "user",
48969
- content: "show me an example of the shell tool"
48970
- },
48971
- {
48972
- role: "assistant",
48973
- content: "To run a shell command, emit only the tag:\n\n```text\n[shell]node -v[/shell]\n```\n\nThe stdout/stderr is returned and you can read it in the next turn."
48974
- },
48975
- {
48976
- role: "user",
48977
- content: "read src folder"
48978
- },
48979
- {
48980
- role: "assistant",
48981
- content: "<read-folder>src</read-folder>"
48982
- },
48983
- {
48984
- role: "user",
48985
- content: `Here is the output from read-folder of src:
49232
+ Use memory-add when:
49233
+ - The user explicitly asks you to remember something ("remember that...", "don't forget...")
49234
+ - You learn something project-specific that would be useful in future sessions
49235
+ (e.g. preferred patterns, architecture decisions, known gotchas, user preferences)
48986
49236
 
48987
- Folder: src (4 entries)
49237
+ Use memory-delete when:
49238
+ - The user asks you to forget something
49239
+ - A memory is outdated or wrong and you are replacing it with a new one
48988
49240
 
48989
- Files:
48990
- index.ts
48991
- App.tsx
49241
+ You may emit multiple memory operations in a single response alongside normal content.
48992
49242
 
48993
- Subfolders:
48994
- components/
48995
- utils/
49243
+ ## RULES
48996
49244
 
48997
- Please continue your response based on this output.`
48998
- },
48999
- {
49000
- role: "assistant",
49001
- content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49002
- },
49245
+ 1. When you need to use a tool, output ONLY the XML tag — nothing before or after it in that response
49246
+ 2. ONE tool per response — emit the tag, then stop completely
49247
+ 3. After the user approves and you get the result, continue your analysis in the next response
49248
+ 4. NEVER print a URL, command, filename, or JSON blob as plain text when you should be using a tool
49249
+ 5. NEVER say "I'll fetch" / "run this command" / "here's the write-file" — just emit the tag
49250
+ 6. NEVER use shell to run git clone — always use the clone tag instead
49251
+ 7. NEVER use shell to list files or folders (no ls, dir, find, git ls-files, tree) — ALWAYS use read-folder instead
49252
+ 8. NEVER use shell to read a file (no cat, type, Get-Content) — ALWAYS use read-file instead
49253
+ 9. NEVER use shell grep, findstr, or Select-String to search file contents — ALWAYS use grep instead
49254
+ 10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
49255
+ 11. write-file content field must be the COMPLETE file content, never empty or placeholder
49256
+ 12. After a write-file succeeds, do NOT repeat it — trust the result and move on
49257
+ 13. After a write-file succeeds, tell the user it is done immediately — do NOT auto-read the file back to verify
49258
+ 14. NEVER apologize and redo a tool call you already made — if write-file or shell ran and returned a result, it worked, do not run it again
49259
+ 15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
49260
+ 16. NEVER second-guess yourself mid-response — commit to your answer
49261
+ 17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
49262
+ 18. If you have already retrieved a result for a path in this conversation, do NOT request it again — use the result you already have
49263
+ 19. Every shell command runs from the repo root — \`cd\` has NO persistent effect. NEVER use \`cd\` alone. Use full paths or combine with && e.g. \`cd list && bun run index.ts\`
49264
+ 20. write-file paths are relative to the repo root — if creating files in a subfolder write the full relative path e.g. \`list/src/index.tsx\` NOT \`src/index.tsx\`
49265
+ 21. When scaffolding a new project in a subfolder, ALL write-file paths must start with that subfolder name e.g. \`list/package.json\`, \`list/src/index.tsx\`
49266
+ 22. When scaffolding a multi-file project, after each write-file succeeds, immediately proceed to writing the NEXT file — NEVER rewrite a file you already wrote in this session. Each file is written ONCE and ONLY ONCE.
49267
+ 23. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
49268
+ 24. When explaining how to use a tool in text, use [tag] bracket notation or a fenced code block — NEVER emit a real XML tool tag as part of an explanation or example
49269
+ 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
49270
+ 26. NEVER use markdown formatting in plain text responses — no **bold**, no *italics*, no # headings, no bullet points with -, *, or +, no numbered lists, no backtick inline code. Write in plain prose. Only use fenced \`\`\` code blocks when showing actual code.
49271
+
49272
+ ## SCAFFOLDING — CHAINING WRITE-FILE CALLS
49273
+
49274
+ When creating multiple files (e.g. scaffolding a project or creating 10 files), emit ALL of them
49275
+ in a single response by chaining the tags back-to-back with no text between them:
49276
+
49277
+ <write-file>
49278
+ {"path": "test/file1.txt", "content": "File 1 content"}
49279
+ </write-file>
49280
+ <write-file>
49281
+ {"path": "test/file2.txt", "content": "File 2 content"}
49282
+ </write-file>
49283
+ <write-file>
49284
+ {"path": "test/file3.txt", "content": "File 3 content"}
49285
+ </write-file>
49286
+
49287
+ The system processes each tag sequentially and automatically continues to the next one.
49288
+ Do NOT wait for a user message between files — emit all tags at once.
49289
+
49290
+ ## CRITICAL: READ BEFORE YOU WRITE
49291
+
49292
+ These rules are mandatory whenever you plan to edit or create a file:
49293
+
49294
+ ### Before modifying ANY existing file:
49295
+ 1. ALWAYS use read-file on the exact file you plan to change FIRST
49296
+ 2. Study the full current content — understand every import, every export, every type, every existing feature
49297
+ 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
49298
+ 4. If you are unsure what other files import from the file you are editing, use read-folder on the parent directory first to see what exists nearby, then read-file the relevant ones
49299
+
49300
+ ### Before adding a feature that touches multiple files:
49301
+ 1. Use read-folder on the relevant directory to see what files exist
49302
+ 2. Use read-file on each file you plan to touch
49303
+ 3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
49304
+
49305
+ ### The golden rule for write-file and changes:
49306
+ - The output file must contain EVERYTHING the original had, PLUS your new additions
49307
+ - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
49308
+ - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
49309
+
49310
+ ## CODEBASE
49311
+
49312
+ ${fileList.length > 0 ? fileList : "(no files indexed)"}
49313
+
49314
+ ${memorySummary}`;
49315
+ }
49316
+ var BUILTIN_TOOLS_SECTION = `## TOOLS
49317
+
49318
+ You have exactly thirteen tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
49319
+
49320
+ ### 1. fetch — load a URL
49321
+ <fetch>https://example.com</fetch>
49322
+
49323
+ ### 2. shell — run a terminal command
49324
+ <shell>node -v</shell>
49325
+
49326
+ ### 3. read-file — read a file from the repo
49327
+ <read-file>src/foo.ts</read-file>
49328
+
49329
+ ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
49330
+ <read-folder>src/components</read-folder>
49331
+
49332
+ ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
49333
+ <grep>
49334
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
49335
+ </grep>
49336
+
49337
+ ### 6. write-file — create or overwrite a file
49338
+ <write-file>
49339
+ {"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}
49340
+ </write-file>
49341
+
49342
+ ### 7. delete-file — permanently delete a single file
49343
+ <delete-file>src/old-component.tsx</delete-file>
49344
+
49345
+ ### 8. delete-folder — permanently delete a folder and all its contents
49346
+ <delete-folder>src/legacy</delete-folder>
49347
+
49348
+ ### 9. open-url — open a URL in the user's default browser
49349
+ <open-url>https://github.com/owner/repo</open-url>
49350
+
49351
+ ### 10. generate-pdf — generate a PDF file from markdown-style content
49352
+ <generate-pdf>
49353
+ {"path": "output/report.pdf", "content": "# Title\\n\\nSome body text.\\n\\n## Section\\n\\nMore content."}
49354
+ </generate-pdf>
49355
+
49356
+ ### 11. search — search the internet for anything you are unsure about
49357
+ <search>how to use React useEffect cleanup function</search>
49358
+
49359
+ ### 12. clone — clone a GitHub repo so you can explore and discuss it
49360
+ <clone>https://github.com/owner/repo</clone>
49361
+
49362
+ ### 13. changes — propose code edits (shown as a diff for user approval)
49363
+ <changes>
49364
+ {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
49365
+ </changes>`;
49366
+ // src/prompts/fewshot.ts
49367
+ var FEW_SHOT_MESSAGES = [
49003
49368
  {
49004
49369
  role: "user",
49005
- content: "list src folder"
49370
+ content: "delete src/old-component.tsx"
49006
49371
  },
49007
49372
  {
49008
49373
  role: "assistant",
49009
- content: "<read-folder>src</read-folder>"
49374
+ content: "<delete-file>src/old-component.tsx</delete-file>"
49010
49375
  },
49011
49376
  {
49012
49377
  role: "user",
49013
- content: `Here is the output from read-folder of src:
49014
-
49015
- Folder: src (4 entries)
49016
-
49017
- Files:
49018
- index.ts
49019
- App.tsx
49378
+ content: `Here is the output from delete-file of src/old-component.tsx:
49020
49379
 
49021
- Subfolders:
49022
- components/
49023
- utils/
49380
+ Deleted: /repo/src/old-component.tsx
49024
49381
 
49025
49382
  Please continue your response based on this output.`
49026
49383
  },
49027
49384
  {
49028
49385
  role: "assistant",
49029
- content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49386
+ content: "Done `src/old-component.tsx` has been deleted."
49030
49387
  },
49031
49388
  {
49032
49389
  role: "user",
49033
- content: "what files are in src/components?"
49390
+ content: "delete the legacy folder"
49034
49391
  },
49035
49392
  {
49036
49393
  role: "assistant",
49037
- content: "<read-folder>src/components</read-folder>"
49394
+ content: "<delete-folder>src/legacy</delete-folder>"
49038
49395
  },
49039
49396
  {
49040
49397
  role: "user",
49041
- content: `Here is the output from read-folder of src/components:
49042
-
49043
- Folder: src/components (5 entries)
49044
-
49045
- Files:
49046
- Header.tsx
49047
- Footer.tsx
49048
- Button.tsx
49398
+ content: `Here is the output from delete-folder of src/legacy:
49049
49399
 
49050
- Subfolders:
49051
- ui/
49052
- forms/
49400
+ Deleted folder: /repo/src/legacy
49053
49401
 
49054
49402
  Please continue your response based on this output.`
49055
49403
  },
49056
49404
  {
49057
49405
  role: "assistant",
49058
- content: "The `src/components` folder has 3 files `Header.tsx`, `Footer.tsx`, `Button.tsx` — plus two subfolders: `ui/` and `forms/`."
49406
+ content: "Done — the `src/legacy` folder and all its contents have been deleted."
49059
49407
  },
49060
49408
  {
49061
49409
  role: "user",
49062
- content: "list the files in src/utils"
49410
+ content: "open https://github.com/microsoft/typescript"
49063
49411
  },
49064
49412
  {
49065
49413
  role: "assistant",
49066
- content: "<read-folder>src/utils</read-folder>"
49414
+ content: "<open-url>https://github.com/microsoft/typescript</open-url>"
49067
49415
  },
49068
49416
  {
49069
49417
  role: "user",
49070
- content: `Here is the output from read-folder of src/utils:
49071
-
49072
- Folder: src/utils (3 entries)
49418
+ content: `Here is the output from open-url https://github.com/microsoft/typescript:
49073
49419
 
49074
- Files:
49075
- api.ts
49076
- helpers.ts
49077
- format.ts
49420
+ Opened: https://github.com/microsoft/typescript
49078
49421
 
49079
49422
  Please continue your response based on this output.`
49080
49423
  },
49081
49424
  {
49082
49425
  role: "assistant",
49083
- content: "The `src/utils` folder contains 3 files: `api.ts`, `helpers.ts`, and `format.ts`."
49426
+ content: "Opened the TypeScript GitHub page in your browser."
49084
49427
  },
49085
49428
  {
49086
49429
  role: "user",
49087
- content: "show me what's in the src directory"
49430
+ content: "generate a PDF report about the project and save it to docs/report.pdf"
49088
49431
  },
49089
49432
  {
49090
49433
  role: "assistant",
49091
- content: "<read-folder>src</read-folder>"
49434
+ content: `<generate-pdf>
49435
+ {"path": "docs/report.pdf", "content": "# Project Report\\n\\n## Overview\\n\\nThis document summarizes the project.\\n\\n## Details\\n\\nMore content here."}
49436
+ </generate-pdf>`
49092
49437
  },
49093
49438
  {
49094
49439
  role: "user",
49095
- content: `Here is the output from read-folder of src:
49096
-
49097
- Folder: src (4 entries)
49098
-
49099
- Files:
49100
- index.ts
49101
- App.tsx
49440
+ content: `Here is the output from generate-pdf to docs/report.pdf:
49102
49441
 
49103
- Subfolders:
49104
- components/
49105
- utils/
49442
+ PDF generated: /repo/docs/report.pdf
49106
49443
 
49107
49444
  Please continue your response based on this output.`
49108
49445
  },
49109
49446
  {
49110
49447
  role: "assistant",
49111
- content: "The `src` directory has 2 files (`index.ts`, `App.tsx`) and 2 subfolders (`components/`, `utils/`)."
49448
+ content: "Done the PDF report has been saved to `docs/report.pdf`."
49112
49449
  },
49113
49450
  {
49114
49451
  role: "user",
49115
- content: "show me the project structure"
49452
+ content: 'grep -R "ChatRunner" -n src'
49116
49453
  },
49117
49454
  {
49118
49455
  role: "assistant",
49119
- content: "<read-folder>.</read-folder>"
49456
+ content: `<grep>
49457
+ {"pattern": "ChatRunner", "glob": "src/**/*"}
49458
+ </grep>`
49120
49459
  },
49121
49460
  {
49122
49461
  role: "user",
49123
- content: `Here is the output from read-folder of .:
49462
+ content: `Here is the output from grep for "ChatRunner":
49124
49463
 
49125
- Folder: . (5 entries)
49464
+ grep /ChatRunner/ src/**/* — 3 match(es) in 2 file(s)
49126
49465
 
49127
- Files:
49128
- package.json
49129
- tsconfig.json
49130
- README.md
49466
+ src/index.tsx
49467
+ 12: import { ChatRunner } from "./components/chat/ChatRunner";
49131
49468
 
49132
- Subfolders:
49133
- src/
49134
- node_modules/
49469
+ src/components/chat/ChatRunner.tsx
49470
+ 1: export const ChatRunner = ...
49135
49471
 
49136
49472
  Please continue your response based on this output.`
49137
49473
  },
49138
49474
  {
49139
49475
  role: "assistant",
49140
- content: "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code."
49476
+ content: "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`."
49141
49477
  },
49142
49478
  {
49143
49479
  role: "user",
49144
- content: "what does the project look like?"
49480
+ content: "find all usages of useInput in the codebase"
49145
49481
  },
49146
49482
  {
49147
49483
  role: "assistant",
49148
- content: "<read-folder>.</read-folder>"
49484
+ content: `<grep>
49485
+ {"pattern": "useInput", "glob": "src/**/*.tsx"}
49486
+ </grep>`
49149
49487
  },
49150
49488
  {
49151
49489
  role: "user",
49152
- content: `Here is the output from read-folder of .:
49153
-
49154
- Folder: . (4 entries)
49490
+ content: `Here is the output from grep for "useInput":
49155
49491
 
49156
- Files:
49157
- package.json
49158
- README.md
49492
+ grep /useInput/ src/**/*.tsx — 2 match(es) in 1 file(s)
49159
49493
 
49160
- Subfolders:
49161
- src/
49162
- dist/
49494
+ src/components/chat/ChatRunner.tsx
49495
+ 5: import { useInput } from "ink";
49496
+ 210: useInput((input, key) => {
49163
49497
 
49164
49498
  Please continue your response based on this output.`
49165
49499
  },
49166
49500
  {
49167
49501
  role: "assistant",
49168
- content: "The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`."
49502
+ content: "`useInput` is used in `src/components/chat/ChatRunner.tsx` imported on line 5 and called on line 210."
49169
49503
  },
49170
49504
  {
49171
49505
  role: "user",
49172
- content: "fetch https://api.github.com/repos/microsoft/typescript"
49506
+ content: "show me how to use the read-file tool"
49173
49507
  },
49174
49508
  {
49175
49509
  role: "assistant",
49176
- content: "<fetch>https://api.github.com/repos/microsoft/typescript</fetch>"
49510
+ content: "To read a file, emit the tag alone in your response — nothing else on that turn:\n\n```text\n[read-file]src/components/Header.tsx[/read-file]\n```\n\nThe result comes back as the next user message and you can then analyze it."
49177
49511
  },
49178
49512
  {
49179
49513
  role: "user",
49180
- content: `Here is the output from fetch of https://api.github.com/repos/microsoft/typescript:
49181
-
49182
- {"name":"TypeScript","stargazers_count":100000}
49183
-
49184
- Please continue your response based on this output.`
49514
+ content: "show me an example of the shell tool"
49185
49515
  },
49186
49516
  {
49187
49517
  role: "assistant",
49188
- content: "Found 1 object with fields: name, stargazers_count. Writing to ts-info.json now."
49518
+ content: "To run a shell command, emit only the tag:\n\n```text\n[shell]node -v[/shell]\n```\n\nThe stdout/stderr is returned and you can read it in the next turn."
49189
49519
  },
49190
49520
  {
49191
49521
  role: "user",
49192
- content: "ok go ahead"
49522
+ content: "read src folder"
49193
49523
  },
49194
49524
  {
49195
49525
  role: "assistant",
49196
- content: `<write-file>
49197
- {"path": "ts-info.json", "content": "{"name":"TypeScript","stars":100000}"}
49198
- </write-file>`
49526
+ content: "<read-folder>src</read-folder>"
49199
49527
  },
49200
49528
  {
49201
49529
  role: "user",
49202
- content: `Here is the output from write-file to ts-info.json:
49530
+ content: `Here is the output from read-folder of src:
49203
49531
 
49204
- Written: /repo/ts-info.json (1 lines, 44 bytes)
49532
+ Folder: src (4 entries)
49533
+
49534
+ Files:
49535
+ index.ts
49536
+ App.tsx
49537
+
49538
+ Subfolders:
49539
+ components/
49540
+ utils/
49205
49541
 
49206
49542
  Please continue your response based on this output.`
49207
49543
  },
49208
49544
  {
49209
49545
  role: "assistant",
49210
- content: "Done `ts-info.json` has been saved."
49546
+ content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49211
49547
  },
49212
49548
  {
49213
49549
  role: "user",
49214
- content: "write it to a file called hello.py"
49550
+ content: "what files are in src/components?"
49215
49551
  },
49216
49552
  {
49217
49553
  role: "assistant",
49218
- content: `<write-file>
49219
- {"path": "hello.py", "content": "# hello.py\\nprint('hello')"}
49220
- </write-file>`
49554
+ content: "<read-folder>src/components</read-folder>"
49555
+ },
49556
+ {
49557
+ role: "user",
49558
+ content: `Here is the output from read-folder of src/components:
49559
+
49560
+ Folder: src/components (5 entries)
49561
+
49562
+ Files:
49563
+ Header.tsx
49564
+ Footer.tsx
49565
+ Button.tsx
49566
+
49567
+ Subfolders:
49568
+ ui/
49569
+ forms/
49570
+
49571
+ Please continue your response based on this output.`
49572
+ },
49573
+ {
49574
+ role: "assistant",
49575
+ content: "The `src/components` folder has 3 files — `Header.tsx`, `Footer.tsx`, `Button.tsx` — plus two subfolders: `ui/` and `forms/`."
49576
+ },
49577
+ {
49578
+ role: "user",
49579
+ content: "show me the project structure"
49580
+ },
49581
+ {
49582
+ role: "assistant",
49583
+ content: "<read-folder>.</read-folder>"
49584
+ },
49585
+ {
49586
+ role: "user",
49587
+ content: `Here is the output from read-folder of .:
49588
+
49589
+ Folder: . (5 entries)
49590
+
49591
+ Files:
49592
+ package.json
49593
+ tsconfig.json
49594
+ README.md
49595
+
49596
+ Subfolders:
49597
+ src/
49598
+ node_modules/
49599
+
49600
+ Please continue your response based on this output.`
49601
+ },
49602
+ {
49603
+ role: "assistant",
49604
+ content: "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code."
49605
+ },
49606
+ {
49607
+ role: "user",
49608
+ content: "fetch https://api.github.com/repos/microsoft/typescript"
49609
+ },
49610
+ {
49611
+ role: "assistant",
49612
+ content: "<fetch>https://api.github.com/repos/microsoft/typescript</fetch>"
49613
+ },
49614
+ {
49615
+ role: "user",
49616
+ content: `Here is the output from fetch of https://api.github.com/repos/microsoft/typescript:
49617
+
49618
+ {"name":"TypeScript","stargazers_count":100000}
49619
+
49620
+ Please continue your response based on this output.`
49621
+ },
49622
+ {
49623
+ role: "assistant",
49624
+ content: "Found 1 object with fields: name, stargazers_count. Writing to ts-info.json now."
49625
+ },
49626
+ {
49627
+ role: "user",
49628
+ content: "ok go ahead"
49629
+ },
49630
+ {
49631
+ role: "assistant",
49632
+ content: `<write-file>
49633
+ {"path": "ts-info.json", "content": "{"name":"TypeScript","stars":100000}"}
49634
+ </write-file>`
49635
+ },
49636
+ {
49637
+ role: "user",
49638
+ content: `Here is the output from write-file to ts-info.json:
49639
+
49640
+ Written: /repo/ts-info.json (1 lines, 44 bytes)
49641
+
49642
+ Please continue your response based on this output.`
49643
+ },
49644
+ {
49645
+ role: "assistant",
49646
+ content: "Done — `ts-info.json` has been saved."
49647
+ },
49648
+ {
49649
+ role: "user",
49650
+ content: "write it to a file called hello.py"
49651
+ },
49652
+ {
49653
+ role: "assistant",
49654
+ content: `<write-file>
49655
+ {"path": "hello.py", "content": "# hello.py\\nprint('hello')"}
49656
+ </write-file>`
49221
49657
  },
49222
49658
  {
49223
49659
  role: "user",
@@ -49314,770 +49750,351 @@ Please continue your response based on this output.`
49314
49750
  },
49315
49751
  {
49316
49752
  role: "user",
49317
- content: "what does this project do?"
49753
+ content: "scaffold a new project called myapp with package.json and src/index.ts"
49318
49754
  },
49319
49755
  {
49320
49756
  role: "assistant",
49321
- content: "This project is a CLI coding assistant that lets you chat with an AI model about your codebase. It can read and write files, run shell commands, search the web, and propose diffs for your approval."
49322
- }
49323
- ];
49324
- function parseResponse(text) {
49325
- const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49326
- const candidates = [];
49327
- const patterns = [
49328
- { kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
49329
- { kind: "shell", re: /<shell>([\s\S]*?)<\/shell>/g },
49330
- { kind: "read-file", re: /<read-file>([\s\S]*?)<\/read-file>/g },
49331
- { kind: "read-folder", re: /<read-folder>([\s\S]*?)<\/read-folder>/g },
49332
- { kind: "grep", re: /<grep>([\s\S]*?)<\/grep>/g },
49333
- { kind: "delete-file", re: /<delete-file>([\s\S]*?)<\/delete-file>/g },
49334
- {
49335
- kind: "delete-folder",
49336
- re: /<delete-folder>([\s\S]*?)<\/delete-folder>/g
49337
- },
49338
- { kind: "open-url", re: /<open-url>([\s\S]*?)<\/open-url>/g },
49339
- { kind: "generate-pdf", re: /<generate-pdf>([\s\S]*?)<\/generate-pdf>/g },
49340
- { kind: "write-file", re: /<write-file>([\s\S]*?)<\/write-file>/g },
49341
- { kind: "search", re: /<search>([\s\S]*?)<\/search>/g },
49342
- { kind: "clone", re: /<clone>([\s\S]*?)<\/clone>/g },
49343
- { kind: "changes", re: /<changes>([\s\S]*?)<\/changes>/g },
49344
- { kind: "fetch", re: /```fetch\r?\n([\s\S]*?)\r?\n```/g },
49345
- { kind: "shell", re: /```shell\r?\n([\s\S]*?)\r?\n```/g },
49346
- { kind: "read-file", re: /```read-file\r?\n([\s\S]*?)\r?\n```/g },
49347
- { kind: "read-folder", re: /```read-folder\r?\n([\s\S]*?)\r?\n```/g },
49348
- { kind: "write-file", re: /```write-file\r?\n([\s\S]*?)\r?\n```/g },
49349
- { kind: "search", re: /```search\r?\n([\s\S]*?)\r?\n```/g },
49350
- { kind: "changes", re: /```changes\r?\n([\s\S]*?)\r?\n```/g }
49351
- ];
49352
- for (const { kind: kind2, re } of patterns) {
49353
- re.lastIndex = 0;
49354
- const m = re.exec(scanText);
49355
- if (m) {
49356
- const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
49357
- const originalMatch = originalRe.exec(text.slice(m.index));
49358
- if (originalMatch) {
49359
- const fakeMatch = Object.assign([
49360
- text.slice(m.index, m.index + originalMatch[0].length),
49361
- originalMatch[1]
49362
- ], { index: m.index, input: text, groups: undefined });
49363
- candidates.push({ index: m.index, kind: kind2, match: fakeMatch });
49364
- }
49365
- }
49366
- }
49367
- if (candidates.length === 0)
49368
- return { kind: "text", content: text.trim() };
49369
- candidates.sort((a, b) => a.index - b.index);
49370
- const { kind, match } = candidates[0];
49371
- const before2 = text.slice(0, match.index).replace(/<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g, "").trim();
49372
- const body = (match[1] ?? "").trim();
49373
- if (kind === "changes") {
49374
- try {
49375
- const parsed = JSON.parse(body);
49376
- const display = [before2, parsed.summary].filter(Boolean).join(`
49377
-
49378
- `);
49379
- return { kind: "changes", content: display, patches: parsed.patches };
49380
- } catch {}
49381
- }
49382
- if (kind === "shell")
49383
- return { kind: "shell", content: before2, command: body };
49384
- if (kind === "fetch") {
49385
- const url = body.replace(/^<|>$/g, "").trim();
49386
- return { kind: "fetch", content: before2, url };
49387
- }
49388
- if (kind === "read-file")
49389
- return { kind: "read-file", content: before2, filePath: body };
49390
- if (kind === "read-folder")
49391
- return { kind: "read-folder", content: before2, folderPath: body };
49392
- if (kind === "delete-file")
49393
- return { kind: "delete-file", content: before2, filePath: body };
49394
- if (kind === "delete-folder")
49395
- return { kind: "delete-folder", content: before2, folderPath: body };
49396
- if (kind === "open-url") {
49397
- const url = body.replace(/^<|>$/g, "").trim();
49398
- return { kind: "open-url", content: before2, url };
49399
- }
49400
- if (kind === "generate-pdf") {
49401
- try {
49402
- const parsed = JSON.parse(body);
49403
- return {
49404
- kind: "generate-pdf",
49405
- content: before2,
49406
- filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
49407
- pdfContent: parsed.content ?? ""
49408
- };
49409
- } catch {
49410
- return { kind: "text", content: text };
49411
- }
49412
- }
49413
- if (kind === "grep") {
49414
- try {
49415
- const parsed = JSON.parse(body);
49416
- return {
49417
- kind: "grep",
49418
- content: before2,
49419
- pattern: parsed.pattern,
49420
- glob: parsed.glob ?? "**/*"
49421
- };
49422
- } catch {
49423
- return { kind: "grep", content: before2, pattern: body, glob: "**/*" };
49424
- }
49425
- }
49426
- if (kind === "write-file") {
49427
- try {
49428
- const parsed = JSON.parse(body);
49429
- return {
49430
- kind: "write-file",
49431
- content: before2,
49432
- filePath: parsed.path,
49433
- fileContent: parsed.content
49434
- };
49435
- } catch {}
49436
- }
49437
- if (kind === "search")
49438
- return { kind: "search", content: before2, query: body };
49439
- if (kind === "clone") {
49440
- const url = body.replace(/^<|>$/g, "").trim();
49441
- return { kind: "clone", content: before2, repoUrl: url };
49442
- }
49443
- return { kind: "text", content: text.trim() };
49444
- }
49445
- function extractGithubUrl(text) {
49446
- const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
49447
- return match ? match[0] : null;
49448
- }
49449
- function toCloneUrl(url) {
49450
- const clean = url.replace(/\/+$/, "");
49451
- return clean.endsWith(".git") ? clean : `${clean}.git`;
49452
- }
49453
- function buildApiMessages(messages) {
49454
- return messages.map((m) => {
49455
- if (m.type === "tool") {
49456
- if (!m.approved) {
49457
- return {
49458
- role: "user",
49459
- content: "The tool call was denied by the user. Please respond without using that tool."
49460
- };
49461
- }
49462
- const label = m.toolName === "shell" ? `shell command \`${m.content}\`` : m.toolName === "fetch" ? `fetch of ${m.content}` : m.toolName === "read-file" ? `read-file of ${m.content}` : m.toolName === "read-folder" ? `read-folder of ${m.content}` : m.toolName === "grep" ? `grep for "${m.content}"` : m.toolName === "delete-file" ? `delete-file of ${m.content}` : m.toolName === "delete-folder" ? `delete-folder of ${m.content}` : m.toolName === "open-url" ? `open-url ${m.content}` : m.toolName === "generate-pdf" ? `generate-pdf to ${m.content}` : m.toolName === "search" ? `web search for "${m.content}"` : `write-file to ${m.content}`;
49463
- return {
49464
- role: "user",
49465
- content: `Here is the output from the ${label}:
49757
+ content: `<write-file>
49758
+ {"path": "myapp/package.json", "content": "{"name":"myapp","version":"1.0.0"}"}
49759
+ </write-file>`
49760
+ },
49761
+ {
49762
+ role: "user",
49763
+ content: `Here is the output from write-file to myapp/package.json:
49466
49764
 
49467
- ${m.result}
49765
+ Written: /repo/myapp/package.json (1 lines, 38 bytes)
49468
49766
 
49469
49767
  Please continue your response based on this output.`
49470
- };
49471
- }
49472
- return { role: m.role, content: m.content };
49473
- });
49474
- }
49475
- async function callChat(provider, systemPrompt, messages, abortSignal) {
49476
- const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
49477
- let url;
49478
- let headers;
49479
- let body;
49480
- if (provider.type === "anthropic") {
49481
- url = "https://api.anthropic.com/v1/messages";
49482
- headers = {
49483
- "Content-Type": "application/json",
49484
- "x-api-key": provider.apiKey ?? "",
49485
- "anthropic-version": "2023-06-01"
49486
- };
49487
- body = {
49488
- model: provider.model,
49489
- max_tokens: 4096,
49490
- system: systemPrompt,
49491
- messages: apiMessages
49492
- };
49493
- } else {
49494
- const base2 = provider.baseUrl ?? "https://api.openai.com/v1";
49495
- url = `${base2}/chat/completions`;
49496
- headers = {
49497
- "Content-Type": "application/json",
49498
- Authorization: `Bearer ${provider.apiKey}`
49499
- };
49500
- body = {
49501
- model: provider.model,
49502
- max_tokens: 4096,
49503
- messages: [{ role: "system", content: systemPrompt }, ...apiMessages]
49504
- };
49505
- }
49506
- const controller = new AbortController;
49507
- const timer = setTimeout(() => controller.abort(), 60000);
49508
- abortSignal?.addEventListener("abort", () => controller.abort());
49509
- const res = await fetch(url, {
49510
- method: "POST",
49511
- headers,
49512
- body: JSON.stringify(body),
49513
- signal: controller.signal
49514
- });
49515
- clearTimeout(timer);
49516
- if (!res.ok)
49517
- throw new Error(`API error ${res.status}: ${await res.text()}`);
49518
- const data = await res.json();
49519
- if (provider.type === "anthropic") {
49520
- const content = data.content;
49521
- return content.filter((b) => b.type === "text").map((b) => b.text).join("");
49522
- } else {
49523
- const choices = data.choices;
49524
- return choices[0]?.message.content ?? "";
49525
- }
49526
- }
49527
- var SKIP_DIRS4 = new Set([
49528
- "node_modules",
49529
- ".git",
49530
- "dist",
49531
- "build",
49532
- ".next",
49533
- "out",
49534
- "coverage",
49535
- "__pycache__",
49536
- ".venv",
49537
- "venv"
49538
- ]);
49539
- function walkDir3(dir, base2 = dir) {
49540
- const results = [];
49541
- let entries;
49542
- try {
49543
- entries = readdirSync3(dir, { encoding: "utf-8" });
49544
- } catch {
49545
- return results;
49546
- }
49547
- for (const entry of entries) {
49548
- if (SKIP_DIRS4.has(entry))
49549
- continue;
49550
- const full = path14.join(dir, entry);
49551
- const rel = path14.relative(base2, full).replace(/\\/g, "/");
49552
- let isDir = false;
49553
- try {
49554
- isDir = statSync4(full).isDirectory();
49555
- } catch {
49556
- continue;
49557
- }
49558
- if (isDir)
49559
- results.push(...walkDir3(full, base2));
49560
- else
49561
- results.push(rel);
49562
- }
49563
- return results;
49564
- }
49565
- function applyPatches3(repoPath, patches) {
49566
- for (const patch of patches) {
49567
- const fullPath = path14.join(repoPath, patch.path);
49568
- const dir = path14.dirname(fullPath);
49569
- if (!existsSync10(dir))
49570
- mkdirSync4(dir, { recursive: true });
49571
- writeFileSync6(fullPath, patch.content, "utf-8");
49572
- }
49573
- }
49574
- async function runShell(command, cwd2) {
49575
- return new Promise((resolve) => {
49576
- const { spawn: spawn2 } = __require("child_process");
49577
- const isWin = process.platform === "win32";
49578
- const shell = isWin ? "cmd.exe" : "/bin/sh";
49579
- const shellFlag = isWin ? "/c" : "-c";
49580
- const proc = spawn2(shell, [shellFlag, command], {
49581
- cwd: cwd2,
49582
- env: process.env,
49583
- stdio: ["ignore", "pipe", "pipe"]
49584
- });
49585
- const chunks = [];
49586
- const errChunks = [];
49587
- proc.stdout.on("data", (d) => chunks.push(d));
49588
- proc.stderr.on("data", (d) => errChunks.push(d));
49589
- const killTimer = setTimeout(() => {
49590
- proc.kill();
49591
- resolve("(command timed out after 5 minutes)");
49592
- }, 5 * 60 * 1000);
49593
- proc.on("close", (code) => {
49594
- clearTimeout(killTimer);
49595
- const stdout = Buffer.concat(chunks).toString("utf-8").trim();
49596
- const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
49597
- const combined = [stdout, stderr].filter(Boolean).join(`
49598
- `);
49599
- resolve(combined || (code === 0 ? "(no output)" : `exit code ${code}`));
49600
- });
49601
- proc.on("error", (err) => {
49602
- clearTimeout(killTimer);
49603
- resolve(`Error: ${err.message}`);
49604
- });
49605
- });
49606
- }
49607
- function stripTags(html) {
49608
- return html.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#\d+;/g, " ").replace(/\s+/g, " ").trim();
49609
- }
49610
- function extractTables(html) {
49611
- const tables = [];
49612
- const tableRe = /<table[\s\S]*?<\/table>/gi;
49613
- let tMatch;
49614
- while ((tMatch = tableRe.exec(html)) !== null) {
49615
- const tableHtml = tMatch[0];
49616
- const rows = [];
49617
- const rowRe = /<tr[\s\S]*?<\/tr>/gi;
49618
- let rMatch;
49619
- while ((rMatch = rowRe.exec(tableHtml)) !== null) {
49620
- const cells = [];
49621
- const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
49622
- let cMatch;
49623
- while ((cMatch = cellRe.exec(rMatch[0])) !== null) {
49624
- cells.push(stripTags(cMatch[1] ?? ""));
49625
- }
49626
- if (cells.length > 0)
49627
- rows.push(cells);
49628
- }
49629
- if (rows.length < 2)
49630
- continue;
49631
- const cols = Math.max(...rows.map((r) => r.length));
49632
- const padded = rows.map((r) => {
49633
- while (r.length < cols)
49634
- r.push("");
49635
- return r;
49636
- });
49637
- const widths = Array.from({ length: cols }, (_, ci) => Math.max(...padded.map((r) => (r[ci] ?? "").length), 3));
49638
- const fmt = (r) => r.map((c, ci) => c.padEnd(widths[ci] ?? 0)).join(" | ");
49639
- const header = fmt(padded[0]);
49640
- const sep = widths.map((w) => "-".repeat(w)).join("-|-");
49641
- const body = padded.slice(1).map(fmt).join(`
49642
- `);
49643
- tables.push(`${header}
49644
- ${sep}
49645
- ${body}`);
49646
- }
49647
- return tables.length > 0 ? `=== TABLES (${tables.length}) ===
49648
-
49649
- ${tables.join(`
49650
-
49651
- ---
49652
-
49653
- `)}` : "";
49654
- }
49655
- function extractLists(html) {
49656
- const lists = [];
49657
- const listRe = /<[ou]l[\s\S]*?<\/[ou]l>/gi;
49658
- let lMatch;
49659
- while ((lMatch = listRe.exec(html)) !== null) {
49660
- const items = [];
49661
- const itemRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
49662
- let iMatch;
49663
- while ((iMatch = itemRe.exec(lMatch[0])) !== null) {
49664
- const text = stripTags(iMatch[1] ?? "");
49665
- if (text.length > 2)
49666
- items.push(`• ${text}`);
49667
- }
49668
- if (items.length > 1)
49669
- lists.push(items.join(`
49670
- `));
49671
- }
49672
- return lists.length > 0 ? `=== LISTS ===
49673
-
49674
- ${lists.slice(0, 5).join(`
49675
-
49676
- `)}` : "";
49677
- }
49678
- async function fetchUrl(url) {
49679
- const res = await fetch(url, {
49680
- headers: {
49681
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49682
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
49683
- "Accept-Language": "en-US,en;q=0.5"
49684
- },
49685
- signal: AbortSignal.timeout(15000)
49686
- });
49687
- if (!res.ok)
49688
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49689
- const contentType = res.headers.get("content-type") ?? "";
49690
- if (contentType.includes("application/json")) {
49691
- const json = await res.json();
49692
- return JSON.stringify(json, null, 2).slice(0, 8000);
49693
- }
49694
- const html = await res.text();
49695
- const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
49696
- const title = titleMatch ? stripTags(titleMatch[1]) : "No title";
49697
- const tables = extractTables(html);
49698
- const lists = extractLists(html);
49699
- const bodyText = stripTags(html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "")).replace(/\s{3,}/g, `
49700
-
49701
- `).slice(0, 3000);
49702
- const parts = [`PAGE: ${title}`, `URL: ${url}`];
49703
- if (tables)
49704
- parts.push(tables);
49705
- if (lists)
49706
- parts.push(lists);
49707
- parts.push(`=== TEXT ===
49708
- ${bodyText}`);
49709
- return parts.join(`
49710
-
49711
- `);
49712
- }
49713
- async function searchWeb(query) {
49714
- const encoded = encodeURIComponent(query);
49715
- const ddgUrl = `https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1&skip_disambig=1`;
49716
- try {
49717
- const res = await fetch(ddgUrl, {
49718
- headers: { "User-Agent": "Lens/1.0" },
49719
- signal: AbortSignal.timeout(8000)
49720
- });
49721
- if (res.ok) {
49722
- const data = await res.json();
49723
- const parts = [`Search: ${query}`];
49724
- if (data.Answer)
49725
- parts.push(`Answer: ${data.Answer}`);
49726
- if (data.AbstractText) {
49727
- parts.push(`Summary: ${data.AbstractText}`);
49728
- if (data.AbstractURL)
49729
- parts.push(`Source: ${data.AbstractURL}`);
49730
- }
49731
- if (data.Infobox?.content?.length) {
49732
- const fields = data.Infobox.content.slice(0, 8).map((f) => ` ${f.label}: ${f.value}`).join(`
49733
- `);
49734
- parts.push(`Info:
49735
- ${fields}`);
49736
- }
49737
- if (data.RelatedTopics?.length) {
49738
- const topics = data.RelatedTopics.filter((t) => t.Text).slice(0, 5).map((t) => ` - ${t.Text}`).join(`
49739
- `);
49740
- if (topics)
49741
- parts.push(`Related:
49742
- ${topics}`);
49743
- }
49744
- const result2 = parts.join(`
49745
-
49746
- `);
49747
- if (result2.length > 60)
49748
- return result2;
49749
- }
49750
- } catch {}
49751
- try {
49752
- const htmlUrl = `https://html.duckduckgo.com/html/?q=${encoded}`;
49753
- const res = await fetch(htmlUrl, {
49754
- headers: {
49755
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49756
- Accept: "text/html"
49757
- },
49758
- signal: AbortSignal.timeout(1e4)
49759
- });
49760
- if (!res.ok)
49761
- throw new Error(`HTTP ${res.status}`);
49762
- const html = await res.text();
49763
- const snippets = [];
49764
- const snippetRe = /class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
49765
- let m;
49766
- while ((m = snippetRe.exec(html)) !== null && snippets.length < 6) {
49767
- const text = m[1].replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
49768
- if (text.length > 20)
49769
- snippets.push(`- ${text}`);
49770
- }
49771
- const links = [];
49772
- const linkRe = /class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
49773
- while ((m = linkRe.exec(html)) !== null && links.length < 5) {
49774
- const title = m[2].replace(/<[^>]+>/g, "").trim();
49775
- const href = m[1];
49776
- if (title && href)
49777
- links.push(` ${title} — ${href}`);
49778
- }
49779
- if (snippets.length === 0 && links.length === 0) {
49780
- return `No results found for: ${query}`;
49781
- }
49782
- const parts = [`Search results for: ${query}`];
49783
- if (snippets.length > 0)
49784
- parts.push(`Snippets:
49785
- ${snippets.join(`
49786
- `)}`);
49787
- if (links.length > 0)
49788
- parts.push(`Links:
49789
- ${links.join(`
49790
- `)}`);
49791
- return parts.join(`
49768
+ },
49769
+ {
49770
+ role: "assistant",
49771
+ content: `<write-file>
49772
+ {"path": "myapp/src/index.ts", "content": "console.log('hello')"}
49773
+ </write-file>`
49774
+ },
49775
+ {
49776
+ role: "user",
49777
+ content: `Here is the output from write-file to myapp/src/index.ts:
49792
49778
 
49793
- `);
49794
- } catch (err) {
49795
- return `Search failed: ${err instanceof Error ? err.message : String(err)}`;
49796
- }
49797
- }
49798
- function readFile(filePath, repoPath) {
49799
- const candidates = path14.isAbsolute(filePath) ? [filePath] : [filePath, path14.join(repoPath, filePath)];
49800
- for (const candidate of candidates) {
49801
- if (existsSync10(candidate)) {
49802
- try {
49803
- const content = readFileSync10(candidate, "utf-8");
49804
- const lines = content.split(`
49805
- `).length;
49806
- return `File: ${candidate} (${lines} lines)
49779
+ Written: /repo/myapp/src/index.ts (1 lines, 20 bytes)
49807
49780
 
49808
- ${content.slice(0, 8000)}${content.length > 8000 ? `
49781
+ Please continue your response based on this output.`
49782
+ },
49783
+ {
49784
+ role: "assistant",
49785
+ content: "Done — scaffolded `myapp/package.json` and `myapp/src/index.ts`. Run `cd myapp && npm install` to get started."
49786
+ },
49787
+ {
49788
+ role: "user",
49789
+ content: "remember that we always use bun in this project"
49790
+ },
49791
+ {
49792
+ role: "assistant",
49793
+ content: `<memory-add>Project uses bun as the package manager and runtime, not npm or node</memory-add>
49809
49794
 
49810
- (truncated)` : ""}`;
49811
- } catch (err) {
49812
- return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
49813
- }
49814
- }
49795
+ Got it I'll always use bun for this project.`
49796
+ },
49797
+ {
49798
+ role: "user",
49799
+ content: "forget the bun preference"
49800
+ },
49801
+ {
49802
+ role: "assistant",
49803
+ content: `<memory-delete>MEMORY_ID</memory-delete>
49804
+
49805
+ Done — removed that memory.`
49815
49806
  }
49816
- return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
49817
- }
49818
- function readFolder(folderPath, repoPath) {
49819
- const sanitized = folderPath.replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "").trim();
49820
- const candidates = path14.isAbsolute(sanitized) ? [sanitized] : [sanitized, path14.join(repoPath, sanitized)];
49821
- for (const candidate of candidates) {
49822
- if (!existsSync10(candidate))
49823
- continue;
49824
- let stat;
49825
- try {
49826
- stat = statSync4(candidate);
49827
- } catch {
49828
- continue;
49829
- }
49830
- if (!stat.isDirectory()) {
49831
- return `Not a directory: ${candidate}. Use read-file to read a file.`;
49832
- }
49833
- let entries;
49834
- try {
49835
- entries = readdirSync3(candidate, { encoding: "utf-8" });
49836
- } catch (err) {
49837
- return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
49838
- }
49839
- const files = [];
49840
- const subfolders = [];
49841
- for (const entry of entries) {
49842
- if (entry.startsWith(".") && entry !== ".env")
49843
- continue;
49844
- const full = path14.join(candidate, entry);
49845
- try {
49846
- if (statSync4(full).isDirectory()) {
49847
- subfolders.push(`${entry}/`);
49848
- } else {
49849
- files.push(entry);
49850
- }
49851
- } catch {}
49852
- }
49853
- const total = files.length + subfolders.length;
49854
- const lines = [`Folder: ${candidate} (${total} entries)`, ""];
49855
- if (files.length > 0) {
49856
- lines.push("Files:");
49857
- files.forEach((f) => lines.push(` ${f}`));
49858
- }
49859
- if (subfolders.length > 0) {
49860
- if (files.length > 0)
49861
- lines.push("");
49862
- lines.push("Subfolders:");
49863
- subfolders.forEach((d) => lines.push(` ${d}`));
49807
+ ];
49808
+ // src/utils/tools/registry.ts
49809
+ class ToolRegistry {
49810
+ tools = new Map;
49811
+ register(tool) {
49812
+ if (this.tools.has(tool.name)) {
49813
+ console.warn(`[ToolRegistry] Overwriting existing tool: "${tool.name}"`);
49864
49814
  }
49865
- if (total === 0) {
49866
- lines.push("(empty folder)");
49815
+ this.tools.set(tool.name, tool);
49816
+ }
49817
+ unregister(name) {
49818
+ this.tools.delete(name);
49819
+ }
49820
+ get(name) {
49821
+ return this.tools.get(name);
49822
+ }
49823
+ all() {
49824
+ return Array.from(this.tools.values());
49825
+ }
49826
+ names() {
49827
+ return Array.from(this.tools.keys());
49828
+ }
49829
+ buildSystemPromptSection() {
49830
+ const lines = [`## TOOLS
49831
+ `];
49832
+ lines.push("You have exactly " + this.tools.size + ` tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
49833
+ `);
49834
+ let i = 1;
49835
+ for (const tool of this.tools.values()) {
49836
+ lines.push(tool.systemPromptEntry(i++));
49867
49837
  }
49868
49838
  return lines.join(`
49869
49839
  `);
49870
49840
  }
49871
- return `Folder not found: ${sanitized}`;
49872
49841
  }
49873
- function grepFiles(pattern, glob, repoPath) {
49874
- let regex2;
49875
- try {
49876
- regex2 = new RegExp(pattern, "i");
49877
- } catch {
49878
- regex2 = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
49879
- }
49880
- const globToFilter = (g) => {
49881
- const cleaned = g.replace(/^\*\*\//, "");
49882
- const parts = cleaned.split("/");
49883
- const ext = parts[parts.length - 1];
49884
- const prefix = parts.slice(0, -1).join("/");
49885
- return (rel) => {
49886
- if (ext?.startsWith("*.")) {
49887
- const extSuffix = ext.slice(1);
49888
- if (!rel.endsWith(extSuffix))
49889
- return false;
49890
- } else if (ext && !ext.includes("*")) {
49891
- if (!rel.endsWith(ext))
49892
- return false;
49842
+ var registry = new ToolRegistry;
49843
+
49844
+ // src/utils/chat.ts
49845
+ function parseResponse(text) {
49846
+ const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49847
+ const candidates = [];
49848
+ for (const toolName2 of registry.names()) {
49849
+ const escaped = toolName2.replace(/[-]/g, "\\-");
49850
+ const xmlRe = new RegExp(`<${escaped}>([\\s\\S]*?)<\\/${escaped}>`, "g");
49851
+ xmlRe.lastIndex = 0;
49852
+ const xmlM = xmlRe.exec(scanText);
49853
+ if (xmlM) {
49854
+ const orig = new RegExp(xmlRe.source);
49855
+ const origM = orig.exec(text.slice(xmlM.index));
49856
+ if (origM) {
49857
+ candidates.push({
49858
+ index: xmlM.index,
49859
+ toolName: toolName2,
49860
+ match: Object.assign([
49861
+ text.slice(xmlM.index, xmlM.index + origM[0].length),
49862
+ origM[1]
49863
+ ], { index: xmlM.index, input: text, groups: undefined })
49864
+ });
49893
49865
  }
49894
- if (prefix && !prefix.includes("*")) {
49895
- if (!rel.startsWith(prefix))
49896
- return false;
49866
+ }
49867
+ const fencedRe = new RegExp(`\`\`\`${escaped}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\``, "g");
49868
+ fencedRe.lastIndex = 0;
49869
+ const fencedM = fencedRe.exec(scanText);
49870
+ if (fencedM) {
49871
+ const orig = new RegExp(fencedRe.source);
49872
+ const origM = orig.exec(text.slice(fencedM.index));
49873
+ if (origM) {
49874
+ candidates.push({
49875
+ index: fencedM.index,
49876
+ toolName: toolName2,
49877
+ match: Object.assign([
49878
+ text.slice(fencedM.index, fencedM.index + origM[0].length),
49879
+ origM[1]
49880
+ ], { index: fencedM.index, input: text, groups: undefined })
49881
+ });
49897
49882
  }
49898
- return true;
49899
- };
49900
- };
49901
- const filter2 = globToFilter(glob);
49902
- const allFiles = walkDir3(repoPath);
49903
- const matchedFiles = allFiles.filter(filter2);
49904
- if (matchedFiles.length === 0) {
49905
- return `No files matched glob: ${glob}`;
49883
+ }
49906
49884
  }
49907
- const results = [];
49908
- let totalMatches = 0;
49909
- for (const relPath of matchedFiles) {
49910
- const fullPath = path14.join(repoPath, relPath);
49911
- let content;
49885
+ if (candidates.length === 0)
49886
+ return { kind: "text", content: text.trim() };
49887
+ candidates.sort((a, b) => a.index - b.index);
49888
+ const { toolName, match } = candidates[0];
49889
+ const before2 = text.slice(0, match.index).trim();
49890
+ const body = (match[1] ?? "").trim();
49891
+ const afterMatch = text.slice(match.index + match[0].length).trim();
49892
+ const remainder = afterMatch.length > 0 ? afterMatch : undefined;
49893
+ if (toolName === "changes") {
49912
49894
  try {
49913
- content = readFileSync10(fullPath, "utf-8");
49914
- } catch {
49915
- continue;
49916
- }
49917
- const lines = content.split(`
49895
+ const parsed = JSON.parse(body);
49896
+ const display = [before2, parsed.summary].filter(Boolean).join(`
49897
+
49918
49898
  `);
49919
- const fileMatches = [];
49920
- lines.forEach((line, i) => {
49921
- if (regex2.test(line)) {
49922
- fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
49923
- totalMatches++;
49924
- }
49925
- });
49926
- if (fileMatches.length > 0) {
49927
- results.push(`${relPath}
49928
- ${fileMatches.join(`
49929
- `)}`);
49930
- }
49931
- if (totalMatches >= 200) {
49932
- results.push("(truncated — too many matches)");
49933
- break;
49899
+ return {
49900
+ kind: "changes",
49901
+ content: display,
49902
+ patches: parsed.patches,
49903
+ remainder
49904
+ };
49905
+ } catch {
49906
+ return { kind: "text", content: text.trim() };
49934
49907
  }
49935
49908
  }
49936
- if (results.length === 0) {
49937
- return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
49909
+ if (toolName === "clone") {
49910
+ return {
49911
+ kind: "clone",
49912
+ content: before2,
49913
+ repoUrl: body.replace(/^<|>$/g, "").trim(),
49914
+ remainder
49915
+ };
49938
49916
  }
49939
- return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)
49917
+ const tool = registry.get(toolName);
49918
+ if (!tool)
49919
+ return { kind: "text", content: text.trim() };
49920
+ const input = tool.parseInput(body);
49921
+ if (input === null)
49922
+ return { kind: "text", content: text.trim() };
49923
+ return {
49924
+ kind: "tool",
49925
+ toolName,
49926
+ input,
49927
+ rawInput: body,
49928
+ content: before2,
49929
+ remainder
49930
+ };
49931
+ }
49932
+ function extractGithubUrl(text) {
49933
+ const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
49934
+ return match ? match[0] : null;
49935
+ }
49936
+ function toCloneUrl(url) {
49937
+ const clean = url.replace(/\/+$/, "");
49938
+ return clean.endsWith(".git") ? clean : `${clean}.git`;
49939
+ }
49940
+ function buildApiMessages(messages) {
49941
+ return messages.map((m) => {
49942
+ if (m.type === "tool") {
49943
+ if (!m.approved) {
49944
+ return {
49945
+ role: "user",
49946
+ content: "The tool call was denied by the user. Please respond without using that tool."
49947
+ };
49948
+ }
49949
+ return {
49950
+ role: "user",
49951
+ content: `Here is the output from the ${m.toolName} of ${m.content}:
49940
49952
 
49941
- ${results.join(`
49953
+ ${m.result}
49942
49954
 
49943
- `)}`;
49955
+ Please continue your response based on this output.`
49956
+ };
49957
+ }
49958
+ return { role: m.role, content: m.content };
49959
+ });
49944
49960
  }
49945
- function writeFile(filePath, content, repoPath) {
49946
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
49947
- try {
49948
- const dir = path14.dirname(fullPath);
49949
- if (!existsSync10(dir))
49950
- mkdirSync4(dir, { recursive: true });
49951
- writeFileSync6(fullPath, content, "utf-8");
49952
- const lines = content.split(`
49953
- `).length;
49954
- return `Written: ${fullPath} (${lines} lines, ${content.length} bytes)`;
49955
- } catch (err) {
49956
- return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
49961
+ async function callChat(provider, systemPrompt, messages, abortSignal) {
49962
+ const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
49963
+ let url;
49964
+ let headers;
49965
+ let body;
49966
+ if (provider.type === "anthropic") {
49967
+ url = "https://api.anthropic.com/v1/messages";
49968
+ headers = {
49969
+ "Content-Type": "application/json",
49970
+ "x-api-key": provider.apiKey ?? "",
49971
+ "anthropic-version": "2023-06-01"
49972
+ };
49973
+ body = {
49974
+ model: provider.model,
49975
+ max_tokens: 4096,
49976
+ system: systemPrompt,
49977
+ messages: apiMessages
49978
+ };
49979
+ } else {
49980
+ const base2 = provider.baseUrl ?? "https://api.openai.com/v1";
49981
+ url = `${base2}/chat/completions`;
49982
+ headers = {
49983
+ "Content-Type": "application/json",
49984
+ Authorization: `Bearer ${provider.apiKey}`
49985
+ };
49986
+ body = {
49987
+ model: provider.model,
49988
+ max_tokens: 4096,
49989
+ messages: [{ role: "system", content: systemPrompt }, ...apiMessages]
49990
+ };
49991
+ }
49992
+ const controller = new AbortController;
49993
+ const timer = setTimeout(() => controller.abort(), 60000);
49994
+ abortSignal?.addEventListener("abort", () => controller.abort());
49995
+ const res = await fetch(url, {
49996
+ method: "POST",
49997
+ headers,
49998
+ body: JSON.stringify(body),
49999
+ signal: controller.signal
50000
+ });
50001
+ clearTimeout(timer);
50002
+ if (!res.ok)
50003
+ throw new Error(`API error ${res.status}: ${await res.text()}`);
50004
+ const data = await res.json();
50005
+ if (provider.type === "anthropic") {
50006
+ const content = data.content;
50007
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("");
50008
+ } else {
50009
+ const choices = data.choices;
50010
+ return choices[0]?.message.content ?? "";
49957
50011
  }
49958
50012
  }
49959
- function deleteFile(filePath, repoPath) {
49960
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
49961
- try {
49962
- if (!existsSync10(fullPath))
49963
- return `File not found: ${fullPath}`;
49964
- const { unlinkSync } = __require("fs");
49965
- unlinkSync(fullPath);
49966
- return `Deleted: ${fullPath}`;
49967
- } catch (err) {
49968
- return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
49969
- }
50013
+
50014
+ // src/utils/chatHistory.ts
50015
+ import {
50016
+ existsSync as existsSync12,
50017
+ mkdirSync as mkdirSync6,
50018
+ readFileSync as readFileSync11,
50019
+ readdirSync as readdirSync4,
50020
+ writeFileSync as writeFileSync8,
50021
+ unlinkSync
50022
+ } from "fs";
50023
+ import path16 from "path";
50024
+ import os7 from "os";
50025
+ var LENS_DIR = path16.join(os7.homedir(), ".lens");
50026
+ var CHATS_DIR = path16.join(LENS_DIR, "chats");
50027
+ function ensureChatsDir() {
50028
+ if (!existsSync12(CHATS_DIR))
50029
+ mkdirSync6(CHATS_DIR, { recursive: true });
49970
50030
  }
49971
- function deleteFolder(folderPath, repoPath) {
49972
- const fullPath = path14.isAbsolute(folderPath) ? folderPath : path14.join(repoPath, folderPath);
49973
- try {
49974
- if (!existsSync10(fullPath))
49975
- return `Folder not found: ${fullPath}`;
49976
- const { rmSync } = __require("fs");
49977
- rmSync(fullPath, { recursive: true, force: true });
49978
- return `Deleted folder: ${fullPath}`;
49979
- } catch (err) {
49980
- return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
49981
- }
50031
+ function chatFilePath(name) {
50032
+ const safe = name.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
50033
+ return path16.join(CHATS_DIR, `${safe}.json`);
49982
50034
  }
49983
- function openUrl(url) {
50035
+ function saveChat(name, repoPath, messages) {
50036
+ ensureChatsDir();
50037
+ const data = {
50038
+ name,
50039
+ repoPath,
50040
+ messages,
50041
+ savedAt: new Date().toISOString(),
50042
+ userMessageCount: messages.filter((m) => m.role === "user").length
50043
+ };
50044
+ writeFileSync8(chatFilePath(name), JSON.stringify(data, null, 2), "utf-8");
50045
+ }
50046
+ function loadChat(name) {
50047
+ const filePath = chatFilePath(name);
50048
+ if (!existsSync12(filePath))
50049
+ return null;
49984
50050
  try {
49985
- const { execSync: execSync2 } = __require("child_process");
49986
- const platform2 = process.platform;
49987
- if (platform2 === "win32") {
49988
- execSync2(`start "" "${url}"`, { stdio: "ignore" });
49989
- } else if (platform2 === "darwin") {
49990
- execSync2(`open "${url}"`, { stdio: "ignore" });
49991
- } else {
49992
- execSync2(`xdg-open "${url}"`, { stdio: "ignore" });
49993
- }
49994
- return `Opened: ${url}`;
49995
- } catch (err) {
49996
- return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
50051
+ return JSON.parse(readFileSync11(filePath, "utf-8"));
50052
+ } catch {
50053
+ return null;
49997
50054
  }
49998
50055
  }
49999
- function generatePdf(filePath, content, repoPath) {
50000
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
50001
- try {
50002
- const dir = path14.dirname(fullPath);
50003
- if (!existsSync10(dir))
50004
- mkdirSync4(dir, { recursive: true });
50005
- const escaped = content.replace(/\\/g, "\\\\").replace(/"""/g, "\\\"\\\"\\\"").replace(/\r/g, "");
50006
- const script = `
50007
- import sys
50008
- try:
50009
- from reportlab.lib.pagesizes import letter
50010
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
50011
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
50012
- from reportlab.lib.units import inch
50013
- from reportlab.lib import colors
50014
- except ImportError:
50015
- import subprocess
50016
- subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
50017
- from reportlab.lib.pagesizes import letter
50018
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
50019
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
50020
- from reportlab.lib.units import inch
50021
- from reportlab.lib import colors
50022
-
50023
- doc = SimpleDocTemplate(
50024
- r"""${fullPath}""",
50025
- pagesize=letter,
50026
- rightMargin=inch,
50027
- leftMargin=inch,
50028
- topMargin=inch,
50029
- bottomMargin=inch,
50030
- )
50031
-
50032
- styles = getSampleStyleSheet()
50033
- styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
50034
- styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
50035
- styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
50036
- styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
50037
- styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
50038
-
50039
- raw = """${escaped}"""
50040
-
50041
- story = []
50042
- for line in raw.split("\\n"):
50043
- s = line.rstrip()
50044
- if s.startswith("### "):
50045
- story.append(Paragraph(s[4:], styles["H3"]))
50046
- elif s.startswith("## "):
50047
- story.append(Spacer(1, 6))
50048
- story.append(Paragraph(s[3:], styles["H2"]))
50049
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
50050
- elif s.startswith("# "):
50051
- story.append(Paragraph(s[2:], styles["H1"]))
50052
- story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
50053
- elif s.startswith("- ") or s.startswith("* "):
50054
- story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
50055
- elif s.startswith("---"):
50056
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
50057
- elif s == "":
50058
- story.append(Spacer(1, 6))
50059
- else:
50060
- import re
50061
- s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
50062
- s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
50063
- s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
50064
- story.append(Paragraph(s, styles["Body"]))
50065
-
50066
- doc.build(story)
50067
- print("OK")
50068
- `.replace("${fullPath}", fullPath.replace(/\\/g, "/")).replace("${escaped}", escaped);
50069
- const os6 = __require("os");
50070
- const tmpFile = path14.join(os6.tmpdir(), `lens_pdf_${Date.now()}.py`);
50071
- writeFileSync6(tmpFile, script, "utf-8");
50072
- const { execSync: execSync2 } = __require("child_process");
50073
- execSync2(`python "${tmpFile}"`, { stdio: "pipe" });
50056
+ function listChats(repoPath) {
50057
+ ensureChatsDir();
50058
+ const files = readdirSync4(CHATS_DIR).filter((f) => f.endsWith(".json"));
50059
+ const chats = [];
50060
+ for (const file of files) {
50074
50061
  try {
50075
- __require("fs").unlinkSync(tmpFile);
50062
+ const data = JSON.parse(readFileSync11(path16.join(CHATS_DIR, file), "utf-8"));
50063
+ if (!repoPath || data.repoPath === repoPath)
50064
+ chats.push(data);
50076
50065
  } catch {}
50077
- return `PDF generated: ${fullPath}`;
50078
- } catch (err) {
50079
- return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
50080
50066
  }
50067
+ return chats.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime());
50068
+ }
50069
+ function deleteChat(name) {
50070
+ const filePath = chatFilePath(name);
50071
+ if (!existsSync12(filePath))
50072
+ return false;
50073
+ try {
50074
+ unlinkSync(filePath);
50075
+ return true;
50076
+ } catch {
50077
+ return false;
50078
+ }
50079
+ }
50080
+ function getChatNameSuggestions(messages) {
50081
+ const userMsgs = messages.filter((m) => m.role === "user").map((m) => m.content.toLowerCase().trim());
50082
+ const date = new Date().toISOString().slice(0, 10);
50083
+ if (userMsgs.length === 0) {
50084
+ return [`chat-${date}`, `session-${date}`, `new-chat`];
50085
+ }
50086
+ const suggestions = [];
50087
+ const toSlug = (s) => s.replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean).slice(0, 4).join("-");
50088
+ const firstSlug = toSlug(userMsgs[0]);
50089
+ if (firstSlug)
50090
+ suggestions.push(firstSlug);
50091
+ if (userMsgs.length > 1) {
50092
+ const lastSlug = toSlug(userMsgs[userMsgs.length - 1]);
50093
+ if (lastSlug && lastSlug !== firstSlug)
50094
+ suggestions.push(lastSlug);
50095
+ }
50096
+ suggestions.push(`session-${date}`);
50097
+ return suggestions.slice(0, 3);
50081
50098
  }
50082
50099
 
50083
50100
  // src/components/chat/ChatMessage.tsx
@@ -50182,6 +50199,19 @@ function MessageBody({ content }) {
50182
50199
  })
50183
50200
  }, undefined, false, undefined, this);
50184
50201
  }
50202
+ function summarizeToolContent(toolName, content) {
50203
+ if (toolName === "write-file" || toolName === "read-file") {
50204
+ const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
50205
+ if (pathMatch)
50206
+ return pathMatch[1];
50207
+ }
50208
+ if (content.includes('"summary"')) {
50209
+ const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
50210
+ if (summaryMatch)
50211
+ return summaryMatch[1];
50212
+ }
50213
+ return content.length > 120 ? content.slice(0, 120) + "…" : content;
50214
+ }
50185
50215
  function StaticMessage({ msg }) {
50186
50216
  if (msg.role === "user") {
50187
50217
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
@@ -50212,7 +50242,7 @@ function StaticMessage({ msg }) {
50212
50242
  search: "?"
50213
50243
  };
50214
50244
  const icon = icons[msg.toolName] ?? "·";
50215
- const label = msg.toolName === "shell" ? msg.content : msg.toolName === "search" ? `"${msg.content}"` : msg.content;
50245
+ const label = msg.toolName === "shell" ? msg.content : msg.toolName === "search" ? `"${msg.content}"` : summarizeToolContent(msg.toolName, msg.content);
50216
50246
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
50217
50247
  flexDirection: "column",
50218
50248
  marginBottom: 1,
@@ -50417,50 +50447,71 @@ function PermissionPrompt({
50417
50447
  let icon;
50418
50448
  let label;
50419
50449
  let value;
50420
- if (tool.type === "shell") {
50421
- icon = "$";
50422
- label = "run";
50423
- value = tool.command;
50424
- } else if (tool.type === "fetch") {
50425
- icon = "~>";
50426
- label = "fetch";
50427
- value = tool.url;
50428
- } else if (tool.type === "read-file") {
50429
- icon = "r";
50430
- label = "read";
50431
- value = tool.filePath;
50432
- } else if (tool.type === "read-folder") {
50433
- icon = "d";
50434
- label = "folder";
50435
- value = tool.folderPath;
50436
- } else if (tool.type === "grep") {
50437
- icon = "/";
50438
- label = "grep";
50439
- value = `${tool.pattern} ${tool.glob}`;
50440
- } else if (tool.type === "delete-file") {
50441
- icon = "x";
50442
- label = "delete";
50443
- value = tool.filePath;
50444
- } else if (tool.type === "delete-folder") {
50445
- icon = "X";
50446
- label = "delete folder";
50447
- value = tool.folderPath;
50448
- } else if (tool.type === "open-url") {
50449
- icon = "↗";
50450
- label = "open";
50451
- value = tool.url;
50452
- } else if (tool.type === "generate-pdf") {
50453
- icon = "P";
50454
- label = "pdf";
50455
- value = tool.filePath;
50456
- } else if (tool.type === "write-file") {
50457
- icon = "w";
50458
- label = "write";
50459
- value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50450
+ if ("_label" in tool) {
50451
+ const iconMap = {
50452
+ run: "$",
50453
+ fetch: "~>",
50454
+ read: "r",
50455
+ write: "w",
50456
+ delete: "x",
50457
+ "delete folder": "X",
50458
+ open: "",
50459
+ pdf: "P",
50460
+ search: "?",
50461
+ folder: "d",
50462
+ grep: "/",
50463
+ clone: "",
50464
+ query: ""
50465
+ };
50466
+ icon = iconMap[tool._label] ?? "·";
50467
+ label = tool._label;
50468
+ value = tool._display;
50460
50469
  } else {
50461
- icon = "?";
50462
- label = "search";
50463
- value = tool.query;
50470
+ if (tool.type === "shell") {
50471
+ icon = "$";
50472
+ label = "run";
50473
+ value = tool.command;
50474
+ } else if (tool.type === "fetch") {
50475
+ icon = "~>";
50476
+ label = "fetch";
50477
+ value = tool.url;
50478
+ } else if (tool.type === "read-file") {
50479
+ icon = "r";
50480
+ label = "read";
50481
+ value = tool.filePath;
50482
+ } else if (tool.type === "read-folder") {
50483
+ icon = "d";
50484
+ label = "folder";
50485
+ value = tool.folderPath;
50486
+ } else if (tool.type === "grep") {
50487
+ icon = "/";
50488
+ label = "grep";
50489
+ value = `${tool.pattern} ${tool.glob}`;
50490
+ } else if (tool.type === "delete-file") {
50491
+ icon = "x";
50492
+ label = "delete";
50493
+ value = tool.filePath;
50494
+ } else if (tool.type === "delete-folder") {
50495
+ icon = "X";
50496
+ label = "delete folder";
50497
+ value = tool.folderPath;
50498
+ } else if (tool.type === "open-url") {
50499
+ icon = "↗";
50500
+ label = "open";
50501
+ value = tool.url;
50502
+ } else if (tool.type === "generate-pdf") {
50503
+ icon = "P";
50504
+ label = "pdf";
50505
+ value = tool.filePath;
50506
+ } else if (tool.type === "write-file") {
50507
+ icon = "w";
50508
+ label = "write";
50509
+ value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50510
+ } else {
50511
+ icon = "?";
50512
+ label = "search";
50513
+ value = tool.query ?? "";
50514
+ }
50464
50515
  }
50465
50516
  return /* @__PURE__ */ jsx_dev_runtime20.jsxDEV(Box_default, {
50466
50517
  flexDirection: "column",
@@ -50497,7 +50548,8 @@ function PermissionPrompt({
50497
50548
  function InputBox({
50498
50549
  value,
50499
50550
  onChange,
50500
- onSubmit
50551
+ onSubmit,
50552
+ inputKey
50501
50553
  }) {
50502
50554
  return /* @__PURE__ */ jsx_dev_runtime20.jsxDEV(Box_default, {
50503
50555
  marginTop: 1,
@@ -50518,7 +50570,7 @@ function InputBox({
50518
50570
  value,
50519
50571
  onChange,
50520
50572
  onSubmit
50521
- }, undefined, false, undefined, this)
50573
+ }, inputKey, false, undefined, this)
50522
50574
  ]
50523
50575
  }, undefined, true, undefined, this)
50524
50576
  }, undefined, false, undefined, this);
@@ -52283,53 +52335,96 @@ function TimelineRunner({
52283
52335
  }, undefined, true, undefined, this);
52284
52336
  }
52285
52337
 
52286
- // src/utils/history.ts
52287
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
52288
- import path15 from "path";
52289
- import os6 from "os";
52290
- var LENS_DIR = path15.join(os6.homedir(), ".lens");
52291
- var HISTORY_PATH = path15.join(LENS_DIR, "history.json");
52292
- function loadHistory() {
52293
- if (!existsSync11(HISTORY_PATH))
52294
- return { entries: [] };
52338
+ // src/utils/memory.ts
52339
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
52340
+ import path17 from "path";
52341
+ import os8 from "os";
52342
+ var LENS_DIR2 = path17.join(os8.homedir(), ".lens");
52343
+ var MEMORY_PATH = path17.join(LENS_DIR2, "memory.json");
52344
+ function loadMemoryFile() {
52345
+ if (!existsSync13(MEMORY_PATH))
52346
+ return { entries: [], memories: [] };
52295
52347
  try {
52296
- return JSON.parse(readFileSync11(HISTORY_PATH, "utf-8"));
52348
+ const data = JSON.parse(readFileSync12(MEMORY_PATH, "utf-8"));
52349
+ return {
52350
+ entries: data.entries ?? [],
52351
+ memories: data.memories ?? []
52352
+ };
52297
52353
  } catch {
52298
- return { entries: [] };
52354
+ return { entries: [], memories: [] };
52299
52355
  }
52300
52356
  }
52301
- function saveHistory(h) {
52302
- if (!existsSync11(LENS_DIR))
52303
- mkdirSync5(LENS_DIR, { recursive: true });
52304
- writeFileSync7(HISTORY_PATH, JSON.stringify(h, null, 2), "utf-8");
52357
+ function saveMemoryFile(m) {
52358
+ if (!existsSync13(LENS_DIR2))
52359
+ mkdirSync7(LENS_DIR2, { recursive: true });
52360
+ writeFileSync9(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
52305
52361
  }
52306
- function appendHistory(entry) {
52307
- const h = loadHistory();
52308
- h.entries.push({ ...entry, timestamp: new Date().toISOString() });
52309
- if (h.entries.length > 500)
52310
- h.entries = h.entries.slice(-500);
52311
- saveHistory(h);
52362
+ function appendMemory(entry) {
52363
+ const m = loadMemoryFile();
52364
+ m.entries.push({ ...entry, timestamp: new Date().toISOString() });
52365
+ if (m.entries.length > 500)
52366
+ m.entries = m.entries.slice(-500);
52367
+ saveMemoryFile(m);
52312
52368
  }
52313
- function buildHistorySummary(repoPath) {
52314
- const h = loadHistory();
52315
- const relevant = h.entries.filter((e) => e.repoPath === repoPath).slice(-50);
52316
- if (relevant.length === 0)
52317
- return "";
52318
- const lines = relevant.map((e) => {
52319
- const ts = new Date(e.timestamp).toLocaleString();
52320
- return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
52321
- });
52322
- return `## WHAT YOU HAVE ALREADY DONE IN THIS REPO
52369
+ function buildMemorySummary(repoPath) {
52370
+ const m = loadMemoryFile();
52371
+ const relevant = m.entries.filter((e) => e.repoPath === repoPath).slice(-50);
52372
+ const memories = m.memories.filter((mem) => mem.repoPath === repoPath);
52373
+ const parts = [];
52374
+ if (memories.length > 0) {
52375
+ parts.push(`## MEMORIES ABOUT THIS REPO
52376
+
52377
+ ${memories.map((mem) => `- [${mem.id}] ${mem.content}`).join(`
52378
+ `)}`);
52379
+ }
52380
+ if (relevant.length > 0) {
52381
+ const lines = relevant.map((e) => {
52382
+ const ts = new Date(e.timestamp).toLocaleString();
52383
+ return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
52384
+ });
52385
+ parts.push(`## WHAT YOU HAVE ALREADY DONE IN THIS REPO
52323
52386
 
52324
52387
  The following actions have already been completed. Do NOT repeat them unless the user explicitly asks you to redo something:
52325
52388
 
52326
52389
  ${lines.join(`
52327
- `)}`;
52390
+ `)}`);
52391
+ }
52392
+ return parts.join(`
52393
+
52394
+ `);
52395
+ }
52396
+ function clearRepoMemory(repoPath) {
52397
+ const m = loadMemoryFile();
52398
+ m.entries = m.entries.filter((e) => e.repoPath !== repoPath);
52399
+ m.memories = m.memories.filter((mem) => mem.repoPath !== repoPath);
52400
+ saveMemoryFile(m);
52328
52401
  }
52329
- function clearRepoHistory(repoPath) {
52330
- const h = loadHistory();
52331
- h.entries = h.entries.filter((e) => e.repoPath !== repoPath);
52332
- saveHistory(h);
52402
+ function generateId() {
52403
+ return Math.random().toString(36).slice(2, 8);
52404
+ }
52405
+ function addMemory(content, repoPath) {
52406
+ const m = loadMemoryFile();
52407
+ const memory = {
52408
+ id: generateId(),
52409
+ content,
52410
+ timestamp: new Date().toISOString(),
52411
+ repoPath
52412
+ };
52413
+ m.memories.push(memory);
52414
+ saveMemoryFile(m);
52415
+ return memory;
52416
+ }
52417
+ function deleteMemory(id, repoPath) {
52418
+ const m = loadMemoryFile();
52419
+ const before2 = m.memories.length;
52420
+ m.memories = m.memories.filter((mem) => !(mem.id === id && mem.repoPath === repoPath));
52421
+ if (m.memories.length === before2)
52422
+ return false;
52423
+ saveMemoryFile(m);
52424
+ return true;
52425
+ }
52426
+ function listMemories(repoPath) {
52427
+ return loadMemoryFile().memories.filter((mem) => mem.repoPath === repoPath);
52333
52428
  }
52334
52429
 
52335
52430
  // src/components/chat/ChatRunner.tsx
@@ -52337,74 +52432,121 @@ var jsx_dev_runtime22 = __toESM(require_jsx_dev_runtime(), 1);
52337
52432
  var COMMANDS = [
52338
52433
  { cmd: "/timeline", desc: "browse commit history" },
52339
52434
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
52340
- { cmd: "/review", desc: "review current codebsae" },
52341
- { cmd: "/auto", desc: "toggle auto-approve for read/search tools" }
52435
+ { cmd: "/review", desc: "review current codebase" },
52436
+ { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
52437
+ { cmd: "/chat", desc: "chat history commands" },
52438
+ { cmd: "/chat list", desc: "list saved chats for this repo" },
52439
+ { cmd: "/chat load", desc: "load a saved chat by name" },
52440
+ { cmd: "/chat rename", desc: "rename the current chat" },
52441
+ { cmd: "/chat delete", desc: "delete a saved chat by name" },
52442
+ { cmd: "/memory", desc: "memory commands" },
52443
+ { cmd: "/memory list", desc: "list all memories for this repo" },
52444
+ { cmd: "/memory add", desc: "add a memory" },
52445
+ { cmd: "/memory delete", desc: "delete a memory by id" },
52446
+ { cmd: "/memory clear", desc: "clear all memories for this repo" }
52342
52447
  ];
52343
52448
  function CommandPalette({
52344
52449
  query,
52345
- onSelect
52450
+ onSelect,
52451
+ recentChats
52346
52452
  }) {
52347
52453
  const q = query.toLowerCase();
52454
+ const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
52455
+ const chatFilter = isChatLoad ? q.startsWith("/chat load") ? q.slice("/chat load".length).trim() : q.slice("/chat delete".length).trim() : "";
52456
+ const filteredChats = chatFilter ? recentChats.filter((n) => n.toLowerCase().includes(chatFilter)) : recentChats;
52348
52457
  const matches2 = COMMANDS.filter((c) => c.cmd.startsWith(q));
52349
- if (!matches2.length)
52458
+ if (!matches2.length && !isChatLoad)
52459
+ return null;
52460
+ if (!matches2.length && isChatLoad && filteredChats.length === 0)
52350
52461
  return null;
52351
52462
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52352
52463
  flexDirection: "column",
52353
52464
  marginBottom: 1,
52354
52465
  marginLeft: 2,
52355
- children: matches2.map((c, i) => {
52356
- const isExact = c.cmd === query;
52357
- return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52358
- gap: 2,
52466
+ children: [
52467
+ matches2.map((c, i) => {
52468
+ const isExact = c.cmd === query;
52469
+ return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52470
+ gap: 2,
52471
+ children: [
52472
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52473
+ color: isExact ? ACCENT : "white",
52474
+ bold: isExact,
52475
+ children: c.cmd
52476
+ }, undefined, false, undefined, this),
52477
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52478
+ color: "gray",
52479
+ dimColor: true,
52480
+ children: c.desc
52481
+ }, undefined, false, undefined, this)
52482
+ ]
52483
+ }, i, true, undefined, this);
52484
+ }),
52485
+ isChatLoad && filteredChats.length > 0 && /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52486
+ flexDirection: "column",
52487
+ marginTop: matches2.length ? 1 : 0,
52359
52488
  children: [
52360
- /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52361
- color: isExact ? ACCENT : "white",
52362
- bold: isExact,
52363
- children: c.cmd
52364
- }, undefined, false, undefined, this),
52365
52489
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52366
52490
  color: "gray",
52367
52491
  dimColor: true,
52368
- children: c.desc
52369
- }, undefined, false, undefined, this)
52492
+ children: chatFilter ? `matching "${chatFilter}":` : "recent chats:"
52493
+ }, undefined, false, undefined, this),
52494
+ filteredChats.map((name, i) => /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52495
+ gap: 1,
52496
+ marginLeft: 2,
52497
+ children: [
52498
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52499
+ color: ACCENT,
52500
+ children: "·"
52501
+ }, undefined, false, undefined, this),
52502
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52503
+ color: "white",
52504
+ children: name
52505
+ }, undefined, false, undefined, this)
52506
+ ]
52507
+ }, i, true, undefined, this))
52370
52508
  ]
52371
- }, i, true, undefined, this);
52372
- })
52373
- }, undefined, false, undefined, this);
52509
+ }, undefined, true, undefined, this)
52510
+ ]
52511
+ }, undefined, true, undefined, this);
52374
52512
  }
52375
52513
  var ChatRunner = ({ repoPath }) => {
52376
- const [stage, setStage] = import_react48.useState({ type: "picking-provider" });
52377
- const [committed, setCommitted] = import_react48.useState([]);
52378
- const [provider, setProvider] = import_react48.useState(null);
52379
- const [systemPrompt, setSystemPrompt] = import_react48.useState("");
52380
- const [inputValue, setInputValue] = import_react48.useState("");
52381
- const [pendingMsgIndex, setPendingMsgIndex] = import_react48.useState(null);
52382
- const [allMessages, setAllMessages] = import_react48.useState([]);
52383
- const [clonedUrls, setClonedUrls] = import_react48.useState(new Set);
52384
- const [showTimeline, setShowTimeline] = import_react48.useState(false);
52385
- const [showReview, setShowReview] = import_react48.useState(false);
52386
- const [autoApprove, setAutoApprove] = import_react48.useState(false);
52387
- const abortControllerRef = import_react48.useRef(null);
52388
- const toolResultCache = import_react48.useRef(new Map);
52389
- const inputBuffer = import_react48.useRef("");
52390
- const flushTimer = import_react48.useRef(null);
52391
- const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
52392
- const flushBuffer = () => {
52393
- const buf = inputBuffer.current;
52394
- if (!buf)
52395
- return;
52396
- inputBuffer.current = "";
52397
- setInputValue((v) => v + buf);
52398
- };
52399
- const scheduleFlush = () => {
52400
- if (flushTimer.current !== null)
52401
- return;
52402
- flushTimer.current = setTimeout(() => {
52403
- flushTimer.current = null;
52404
- flushBuffer();
52405
- }, 16);
52514
+ const [stage, setStage] = import_react49.useState({ type: "picking-provider" });
52515
+ const [committed, setCommitted] = import_react49.useState([]);
52516
+ const [provider, setProvider] = import_react49.useState(null);
52517
+ const [systemPrompt, setSystemPrompt] = import_react49.useState("");
52518
+ const [inputValue, setInputValue] = import_react49.useState("");
52519
+ const [pendingMsgIndex, setPendingMsgIndex] = import_react49.useState(null);
52520
+ const [allMessages, setAllMessages] = import_react49.useState([]);
52521
+ const [clonedUrls, setClonedUrls] = import_react49.useState(new Set);
52522
+ const [showTimeline, setShowTimeline] = import_react49.useState(false);
52523
+ const [showReview, setShowReview] = import_react49.useState(false);
52524
+ const [autoApprove, setAutoApprove] = import_react49.useState(false);
52525
+ const [chatName, setChatName] = import_react49.useState(null);
52526
+ const chatNameRef = import_react49.useRef(null);
52527
+ const [recentChats, setRecentChats] = import_react49.useState([]);
52528
+ const inputHistoryRef = import_react49.useRef([]);
52529
+ const historyIndexRef = import_react49.useRef(-1);
52530
+ const [inputKey, setInputKey] = import_react49.useState(0);
52531
+ const updateChatName = (name) => {
52532
+ chatNameRef.current = name;
52533
+ setChatName(name);
52406
52534
  };
52535
+ const abortControllerRef = import_react49.useRef(null);
52536
+ const toolResultCache = import_react49.useRef(new Map);
52537
+ const batchApprovedRef = import_react49.useRef(false);
52538
+ const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
52539
+ import_react48.default.useEffect(() => {
52540
+ const chats = listChats(repoPath);
52541
+ setRecentChats(chats.slice(0, 10).map((c) => c.name));
52542
+ }, [repoPath]);
52543
+ import_react48.default.useEffect(() => {
52544
+ if (chatNameRef.current && allMessages.length > 1) {
52545
+ saveChat(chatNameRef.current, repoPath, allMessages);
52546
+ }
52547
+ }, [allMessages]);
52407
52548
  const handleError = (currentAll) => (err) => {
52549
+ batchApprovedRef.current = false;
52408
52550
  if (err instanceof Error && err.name === "AbortError") {
52409
52551
  setStage({ type: "idle" });
52410
52552
  return;
@@ -52420,180 +52562,63 @@ var ChatRunner = ({ repoPath }) => {
52420
52562
  };
52421
52563
  const processResponse = (raw, currentAll, signal) => {
52422
52564
  if (signal.aborted) {
52565
+ batchApprovedRef.current = false;
52423
52566
  setStage({ type: "idle" });
52424
52567
  return;
52425
52568
  }
52426
- const parsed = parseResponse(raw);
52569
+ const memAddMatches = [
52570
+ ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g)
52571
+ ];
52572
+ const memDelMatches = [
52573
+ ...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g)
52574
+ ];
52575
+ for (const match of memAddMatches) {
52576
+ const content = match[1].trim();
52577
+ if (content)
52578
+ addMemory(content, repoPath);
52579
+ }
52580
+ for (const match of memDelMatches) {
52581
+ const id = match[1].trim();
52582
+ if (id)
52583
+ deleteMemory(id, repoPath);
52584
+ }
52585
+ const cleanRaw = raw.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "").replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "").trim();
52586
+ const parsed = parseResponse(cleanRaw);
52427
52587
  if (parsed.kind === "changes") {
52588
+ batchApprovedRef.current = false;
52428
52589
  if (parsed.patches.length === 0) {
52429
- const msg2 = {
52430
- role: "assistant",
52431
- content: parsed.content,
52432
- type: "text"
52433
- };
52434
- setAllMessages([...currentAll, msg2]);
52435
- setCommitted((prev) => [...prev, msg2]);
52436
- setStage({ type: "idle" });
52437
- return;
52438
- }
52439
- const assistantMsg = {
52440
- role: "assistant",
52441
- content: parsed.content,
52442
- type: "plan",
52443
- patches: parsed.patches,
52444
- applied: false
52445
- };
52446
- const withAssistant = [...currentAll, assistantMsg];
52447
- setAllMessages(withAssistant);
52448
- setPendingMsgIndex(withAssistant.length - 1);
52449
- const diffLines = buildDiffs(repoPath, parsed.patches);
52450
- setStage({
52451
- type: "preview",
52452
- patches: parsed.patches,
52453
- diffLines,
52454
- scrollOffset: 0,
52455
- pendingMessages: currentAll
52456
- });
52457
- return;
52458
- }
52459
- if (parsed.kind === "shell" || parsed.kind === "fetch" || parsed.kind === "read-file" || parsed.kind === "read-folder" || parsed.kind === "grep" || parsed.kind === "write-file" || parsed.kind === "delete-file" || parsed.kind === "delete-folder" || parsed.kind === "open-url" || parsed.kind === "generate-pdf" || parsed.kind === "search") {
52460
- let tool;
52461
- if (parsed.kind === "shell") {
52462
- tool = { type: "shell", command: parsed.command };
52463
- } else if (parsed.kind === "fetch") {
52464
- tool = { type: "fetch", url: parsed.url };
52465
- } else if (parsed.kind === "read-file") {
52466
- tool = { type: "read-file", filePath: parsed.filePath };
52467
- } else if (parsed.kind === "read-folder") {
52468
- tool = { type: "read-folder", folderPath: parsed.folderPath };
52469
- } else if (parsed.kind === "grep") {
52470
- tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
52471
- } else if (parsed.kind === "delete-file") {
52472
- tool = { type: "delete-file", filePath: parsed.filePath };
52473
- } else if (parsed.kind === "delete-folder") {
52474
- tool = { type: "delete-folder", folderPath: parsed.folderPath };
52475
- } else if (parsed.kind === "open-url") {
52476
- tool = { type: "open-url", url: parsed.url };
52477
- } else if (parsed.kind === "generate-pdf") {
52478
- tool = {
52479
- type: "generate-pdf",
52480
- filePath: parsed.filePath,
52481
- content: parsed.pdfContent
52482
- };
52483
- } else if (parsed.kind === "search") {
52484
- tool = { type: "search", query: parsed.query };
52485
- } else {
52486
- tool = {
52487
- type: "write-file",
52488
- filePath: parsed.filePath,
52489
- fileContent: parsed.fileContent
52490
- };
52491
- }
52492
- if (parsed.content) {
52493
- const preambleMsg = {
52590
+ const msg = {
52494
52591
  role: "assistant",
52495
52592
  content: parsed.content,
52496
52593
  type: "text"
52497
52594
  };
52498
- setAllMessages([...currentAll, preambleMsg]);
52499
- setCommitted((prev) => [...prev, preambleMsg]);
52500
- }
52501
- const isSafeTool = parsed.kind === "read-file" || parsed.kind === "read-folder" || parsed.kind === "grep" || parsed.kind === "fetch" || parsed.kind === "open-url" || parsed.kind === "search";
52502
- const executeAndContinue = async (approved) => {
52503
- let result2 = "(denied by user)";
52504
- if (approved) {
52505
- const cacheKey = parsed.kind === "read-file" ? `read-file:${parsed.filePath}` : parsed.kind === "read-folder" ? `read-folder:${parsed.folderPath}` : parsed.kind === "grep" ? `grep:${parsed.pattern}:${parsed.glob}` : null;
52506
- if (cacheKey && toolResultCache.current.has(cacheKey)) {
52507
- result2 = toolResultCache.current.get(cacheKey) + `
52508
-
52509
- [NOTE: This result was already retrieved earlier. Do not request it again.]`;
52510
- } else {
52511
- try {
52512
- setStage({ type: "thinking" });
52513
- if (parsed.kind === "shell") {
52514
- result2 = await runShell(parsed.command, repoPath);
52515
- } else if (parsed.kind === "fetch") {
52516
- result2 = await fetchUrl(parsed.url);
52517
- } else if (parsed.kind === "read-file") {
52518
- result2 = readFile(parsed.filePath, repoPath);
52519
- } else if (parsed.kind === "read-folder") {
52520
- result2 = readFolder(parsed.folderPath, repoPath);
52521
- } else if (parsed.kind === "grep") {
52522
- result2 = grepFiles(parsed.pattern, parsed.glob, repoPath);
52523
- } else if (parsed.kind === "delete-file") {
52524
- result2 = deleteFile(parsed.filePath, repoPath);
52525
- } else if (parsed.kind === "delete-folder") {
52526
- result2 = deleteFolder(parsed.folderPath, repoPath);
52527
- } else if (parsed.kind === "open-url") {
52528
- result2 = openUrl(parsed.url);
52529
- } else if (parsed.kind === "generate-pdf") {
52530
- result2 = generatePdf(parsed.filePath, parsed.pdfContent, repoPath);
52531
- } else if (parsed.kind === "write-file") {
52532
- result2 = writeFile(parsed.filePath, parsed.fileContent, repoPath);
52533
- } else if (parsed.kind === "search") {
52534
- result2 = await searchWeb(parsed.query);
52535
- }
52536
- if (cacheKey) {
52537
- toolResultCache.current.set(cacheKey, result2);
52538
- }
52539
- } catch (err) {
52540
- result2 = `Error: ${err instanceof Error ? err.message : "failed"}`;
52541
- }
52542
- }
52543
- }
52544
- if (approved && !result2.startsWith("Error:")) {
52545
- const kindMap2 = {
52546
- shell: "shell-run",
52547
- fetch: "url-fetched",
52548
- "read-file": "file-read",
52549
- "read-folder": "file-read",
52550
- grep: "file-read",
52551
- "delete-file": "file-written",
52552
- "delete-folder": "file-written",
52553
- "open-url": "url-fetched",
52554
- "generate-pdf": "file-written",
52555
- "write-file": "file-written",
52556
- search: "url-fetched"
52557
- };
52558
- appendHistory({
52559
- kind: kindMap2[parsed.kind] ?? "shell-run",
52560
- detail: parsed.kind === "shell" ? parsed.command : parsed.kind === "fetch" ? parsed.url : parsed.kind === "search" ? parsed.query : parsed.kind === "read-folder" ? parsed.folderPath : parsed.kind === "grep" ? `${parsed.pattern} ${parsed.glob}` : parsed.kind === "delete-file" ? parsed.filePath : parsed.kind === "delete-folder" ? parsed.folderPath : parsed.kind === "open-url" ? parsed.url : parsed.kind === "generate-pdf" ? parsed.filePath : parsed.filePath,
52561
- summary: result2.split(`
52562
- `)[0]?.slice(0, 120) ?? "",
52563
- repoPath
52564
- });
52565
- }
52566
- const toolName = parsed.kind === "shell" ? "shell" : parsed.kind === "fetch" ? "fetch" : parsed.kind === "read-file" ? "read-file" : parsed.kind === "read-folder" ? "read-folder" : parsed.kind === "grep" ? "grep" : parsed.kind === "delete-file" ? "delete-file" : parsed.kind === "delete-folder" ? "delete-folder" : parsed.kind === "open-url" ? "open-url" : parsed.kind === "generate-pdf" ? "generate-pdf" : parsed.kind === "search" ? "search" : "write-file";
52567
- const toolContent = parsed.kind === "shell" ? parsed.command : parsed.kind === "fetch" ? parsed.url : parsed.kind === "search" ? parsed.query : parsed.kind === "read-folder" ? parsed.folderPath : parsed.kind === "grep" ? `${parsed.pattern} — ${parsed.glob}` : parsed.kind === "delete-file" ? parsed.filePath : parsed.kind === "delete-folder" ? parsed.folderPath : parsed.kind === "open-url" ? parsed.url : parsed.kind === "generate-pdf" ? parsed.filePath : parsed.filePath;
52568
- const toolMsg = {
52569
- role: "assistant",
52570
- type: "tool",
52571
- toolName,
52572
- content: toolContent,
52573
- result: result2,
52574
- approved
52575
- };
52576
- const withTool = [...currentAll, toolMsg];
52577
- setAllMessages(withTool);
52578
- setCommitted((prev) => [...prev, toolMsg]);
52579
- const nextAbort = new AbortController;
52580
- abortControllerRef.current = nextAbort;
52581
- setStage({ type: "thinking" });
52582
- callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52583
- };
52584
- if (autoApprove && isSafeTool) {
52585
- executeAndContinue(true);
52595
+ setAllMessages([...currentAll, msg]);
52596
+ setCommitted((prev) => [...prev, msg]);
52597
+ setStage({ type: "idle" });
52586
52598
  return;
52587
52599
  }
52600
+ const assistantMsg = {
52601
+ role: "assistant",
52602
+ content: parsed.content,
52603
+ type: "plan",
52604
+ patches: parsed.patches,
52605
+ applied: false
52606
+ };
52607
+ const withAssistant = [...currentAll, assistantMsg];
52608
+ setAllMessages(withAssistant);
52609
+ setPendingMsgIndex(withAssistant.length - 1);
52610
+ const diffLines = buildDiffs(repoPath, parsed.patches);
52588
52611
  setStage({
52589
- type: "permission",
52590
- tool,
52591
- pendingMessages: currentAll,
52592
- resolve: executeAndContinue
52612
+ type: "preview",
52613
+ patches: parsed.patches,
52614
+ diffLines,
52615
+ scrollOffset: 0,
52616
+ pendingMessages: currentAll
52593
52617
  });
52594
52618
  return;
52595
52619
  }
52596
52620
  if (parsed.kind === "clone") {
52621
+ batchApprovedRef.current = false;
52597
52622
  if (parsed.content) {
52598
52623
  const preambleMsg = {
52599
52624
  role: "assistant",
@@ -52610,23 +52635,116 @@ var ChatRunner = ({ repoPath }) => {
52610
52635
  });
52611
52636
  return;
52612
52637
  }
52613
- const msg = {
52614
- role: "assistant",
52615
- content: parsed.content,
52616
- type: "text"
52617
- };
52618
- const withMsg = [...currentAll, msg];
52619
- setAllMessages(withMsg);
52620
- setCommitted((prev) => [...prev, msg]);
52621
- const lastUserMsg = [...currentAll].reverse().find((m) => m.role === "user");
52622
- const githubUrl = lastUserMsg ? extractGithubUrl(lastUserMsg.content) : null;
52623
- if (githubUrl && !clonedUrls.has(githubUrl)) {
52624
- setTimeout(() => {
52625
- setStage({ type: "clone-offer", repoUrl: githubUrl });
52626
- }, 80);
52627
- } else {
52638
+ if (parsed.kind === "text") {
52639
+ batchApprovedRef.current = false;
52640
+ const msg = {
52641
+ role: "assistant",
52642
+ content: parsed.content,
52643
+ type: "text"
52644
+ };
52645
+ const withMsg = [...currentAll, msg];
52646
+ setAllMessages(withMsg);
52647
+ setCommitted((prev) => [...prev, msg]);
52648
+ const lastUserMsg = [...currentAll].reverse().find((m) => m.role === "user");
52649
+ const githubUrl = lastUserMsg ? extractGithubUrl(lastUserMsg.content) : null;
52650
+ if (githubUrl && !clonedUrls.has(githubUrl)) {
52651
+ setTimeout(() => setStage({ type: "clone-offer", repoUrl: githubUrl }), 80);
52652
+ } else {
52653
+ setStage({ type: "idle" });
52654
+ }
52655
+ return;
52656
+ }
52657
+ const tool = registry.get(parsed.toolName);
52658
+ if (!tool) {
52659
+ batchApprovedRef.current = false;
52628
52660
  setStage({ type: "idle" });
52661
+ return;
52662
+ }
52663
+ if (parsed.content) {
52664
+ const preambleMsg = {
52665
+ role: "assistant",
52666
+ content: parsed.content,
52667
+ type: "text"
52668
+ };
52669
+ setAllMessages([...currentAll, preambleMsg]);
52670
+ setCommitted((prev) => [...prev, preambleMsg]);
52671
+ }
52672
+ const remainder = parsed.remainder;
52673
+ const isSafe = tool.safe ?? false;
52674
+ const executeAndContinue = async (approved) => {
52675
+ if (approved && remainder) {
52676
+ batchApprovedRef.current = true;
52677
+ }
52678
+ let result2 = "(denied by user)";
52679
+ if (approved) {
52680
+ const cacheKey = isSafe ? `${parsed.toolName}:${parsed.rawInput}` : null;
52681
+ if (cacheKey && toolResultCache.current.has(cacheKey)) {
52682
+ result2 = toolResultCache.current.get(cacheKey) + `
52683
+
52684
+ [NOTE: This result was already retrieved earlier. Do not request it again.]`;
52685
+ } else {
52686
+ try {
52687
+ setStage({ type: "thinking" });
52688
+ const toolResult = await tool.execute(parsed.input, {
52689
+ repoPath,
52690
+ messages: currentAll
52691
+ });
52692
+ result2 = toolResult.value;
52693
+ if (cacheKey && toolResult.kind === "text") {
52694
+ toolResultCache.current.set(cacheKey, result2);
52695
+ }
52696
+ } catch (err) {
52697
+ result2 = `Error: ${err instanceof Error ? err.message : "failed"}`;
52698
+ }
52699
+ }
52700
+ }
52701
+ if (approved && !result2.startsWith("Error:")) {
52702
+ appendMemory({
52703
+ kind: "shell-run",
52704
+ detail: tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput,
52705
+ summary: result2.split(`
52706
+ `)[0]?.slice(0, 120) ?? "",
52707
+ repoPath
52708
+ });
52709
+ }
52710
+ const displayContent = tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput;
52711
+ const toolMsg = {
52712
+ role: "assistant",
52713
+ type: "tool",
52714
+ toolName: parsed.toolName,
52715
+ content: displayContent,
52716
+ result: result2,
52717
+ approved
52718
+ };
52719
+ const withTool = [...currentAll, toolMsg];
52720
+ setAllMessages(withTool);
52721
+ setCommitted((prev) => [...prev, toolMsg]);
52722
+ if (approved && remainder && remainder.length > 0) {
52723
+ processResponse(remainder, withTool, signal);
52724
+ return;
52725
+ }
52726
+ batchApprovedRef.current = false;
52727
+ const nextAbort = new AbortController;
52728
+ abortControllerRef.current = nextAbort;
52729
+ setStage({ type: "thinking" });
52730
+ callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52731
+ };
52732
+ if (autoApprove && isSafe || batchApprovedRef.current) {
52733
+ executeAndContinue(true);
52734
+ return;
52629
52735
  }
52736
+ const permLabel = tool.permissionLabel ?? tool.name;
52737
+ const permValue = tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput;
52738
+ setStage({
52739
+ type: "permission",
52740
+ tool: {
52741
+ type: parsed.toolName,
52742
+ _display: permValue,
52743
+ _label: permLabel
52744
+ },
52745
+ pendingMessages: currentAll,
52746
+ resolve: executeAndContinue
52747
+ });
52630
52748
  };
52631
52749
  const sendMessage = (text) => {
52632
52750
  if (!provider)
@@ -52644,7 +52762,7 @@ var ChatRunner = ({ repoPath }) => {
52644
52762
  setAutoApprove(next);
52645
52763
  const msg = {
52646
52764
  role: "assistant",
52647
- content: next ? "Auto-approve ON — read, search, grep and folder tools will run without asking. Write and code changes still require approval." : "Auto-approve OFF — all tools will ask for permission.",
52765
+ content: next ? "Auto-approve ON — safe tools (read, search, fetch) will run without asking." : "Auto-approve OFF — all tools will ask for permission.",
52648
52766
  type: "text"
52649
52767
  };
52650
52768
  setCommitted((prev) => [...prev, msg]);
@@ -52652,14 +52770,204 @@ var ChatRunner = ({ repoPath }) => {
52652
52770
  return;
52653
52771
  }
52654
52772
  if (text.trim().toLowerCase() === "/clear history") {
52655
- clearRepoHistory(repoPath);
52656
- const clearedMsg = {
52773
+ clearRepoMemory(repoPath);
52774
+ const msg = {
52657
52775
  role: "assistant",
52658
52776
  content: "History cleared for this repo.",
52659
52777
  type: "text"
52660
52778
  };
52661
- setCommitted((prev) => [...prev, clearedMsg]);
52662
- setAllMessages((prev) => [...prev, clearedMsg]);
52779
+ setCommitted((prev) => [...prev, msg]);
52780
+ setAllMessages((prev) => [...prev, msg]);
52781
+ return;
52782
+ }
52783
+ if (text.trim().toLowerCase() === "/chat") {
52784
+ const msg = {
52785
+ role: "assistant",
52786
+ content: "Chat commands: `/chat list` · `/chat load <n>` · `/chat rename <n>` · `/chat delete <n>`",
52787
+ type: "text"
52788
+ };
52789
+ setCommitted((prev) => [...prev, msg]);
52790
+ setAllMessages((prev) => [...prev, msg]);
52791
+ return;
52792
+ }
52793
+ if (text.trim().toLowerCase().startsWith("/chat rename")) {
52794
+ const parts = text.trim().split(/\s+/);
52795
+ const newName = parts.slice(2).join("-");
52796
+ if (!newName) {
52797
+ const msg2 = {
52798
+ role: "assistant",
52799
+ content: "Usage: `/chat rename <new-name>`",
52800
+ type: "text"
52801
+ };
52802
+ setCommitted((prev) => [...prev, msg2]);
52803
+ setAllMessages((prev) => [...prev, msg2]);
52804
+ return;
52805
+ }
52806
+ const oldName = chatNameRef.current;
52807
+ if (oldName)
52808
+ deleteChat(oldName);
52809
+ updateChatName(newName);
52810
+ saveChat(newName, repoPath, allMessages);
52811
+ setRecentChats((prev) => [newName, ...prev.filter((n) => n !== newName && n !== oldName)].slice(0, 10));
52812
+ const msg = {
52813
+ role: "assistant",
52814
+ content: `Chat renamed to **${newName}**.`,
52815
+ type: "text"
52816
+ };
52817
+ setCommitted((prev) => [...prev, msg]);
52818
+ setAllMessages((prev) => [...prev, msg]);
52819
+ return;
52820
+ }
52821
+ if (text.trim().toLowerCase().startsWith("/chat delete")) {
52822
+ const parts = text.trim().split(/\s+/);
52823
+ const name = parts.slice(2).join("-");
52824
+ if (!name) {
52825
+ const msg2 = {
52826
+ role: "assistant",
52827
+ content: "Usage: `/chat delete <n>`",
52828
+ type: "text"
52829
+ };
52830
+ setCommitted((prev) => [...prev, msg2]);
52831
+ setAllMessages((prev) => [...prev, msg2]);
52832
+ return;
52833
+ }
52834
+ const deleted = deleteChat(name);
52835
+ if (!deleted) {
52836
+ const msg2 = {
52837
+ role: "assistant",
52838
+ content: `Chat **${name}** not found.`,
52839
+ type: "text"
52840
+ };
52841
+ setCommitted((prev) => [...prev, msg2]);
52842
+ setAllMessages((prev) => [...prev, msg2]);
52843
+ return;
52844
+ }
52845
+ if (chatNameRef.current === name) {
52846
+ chatNameRef.current = null;
52847
+ setChatName(null);
52848
+ }
52849
+ setRecentChats((prev) => prev.filter((n) => n !== name));
52850
+ const msg = {
52851
+ role: "assistant",
52852
+ content: `Chat **${name}** deleted.`,
52853
+ type: "text"
52854
+ };
52855
+ setCommitted((prev) => [...prev, msg]);
52856
+ setAllMessages((prev) => [...prev, msg]);
52857
+ return;
52858
+ }
52859
+ if (text.trim().toLowerCase() === "/chat list") {
52860
+ const chats = listChats(repoPath);
52861
+ const content = chats.length === 0 ? "No saved chats for this repo yet." : `Saved chats:
52862
+
52863
+ ${chats.map((c) => `- **${c.name}** · ${c.userMessageCount} messages · ${new Date(c.savedAt).toLocaleString()}`).join(`
52864
+ `)}`;
52865
+ const msg = { role: "assistant", content, type: "text" };
52866
+ setCommitted((prev) => [...prev, msg]);
52867
+ setAllMessages((prev) => [...prev, msg]);
52868
+ return;
52869
+ }
52870
+ if (text.trim().toLowerCase().startsWith("/chat load")) {
52871
+ const parts = text.trim().split(/\s+/);
52872
+ const name = parts.slice(2).join("-");
52873
+ if (!name) {
52874
+ const chats = listChats(repoPath);
52875
+ const content = chats.length === 0 ? "No saved chats found." : `Specify a chat name. Recent chats:
52876
+
52877
+ ${chats.slice(0, 10).map((c) => `- **${c.name}**`).join(`
52878
+ `)}`;
52879
+ const msg = { role: "assistant", content, type: "text" };
52880
+ setCommitted((prev) => [...prev, msg]);
52881
+ setAllMessages((prev) => [...prev, msg]);
52882
+ return;
52883
+ }
52884
+ const saved = loadChat(name);
52885
+ if (!saved) {
52886
+ const msg = {
52887
+ role: "assistant",
52888
+ content: `Chat **${name}** not found. Use \`/chat list\` to see saved chats.`,
52889
+ type: "text"
52890
+ };
52891
+ setCommitted((prev) => [...prev, msg]);
52892
+ setAllMessages((prev) => [...prev, msg]);
52893
+ return;
52894
+ }
52895
+ updateChatName(name);
52896
+ setAllMessages(saved.messages);
52897
+ setCommitted(saved.messages);
52898
+ const notice = {
52899
+ role: "assistant",
52900
+ content: `Loaded chat **${name}** · ${saved.userMessageCount} messages · saved ${new Date(saved.savedAt).toLocaleString()}`,
52901
+ type: "text"
52902
+ };
52903
+ setCommitted((prev) => [...prev, notice]);
52904
+ setAllMessages((prev) => [...prev, notice]);
52905
+ return;
52906
+ }
52907
+ if (text.trim().toLowerCase() === "/memory list" || text.trim().toLowerCase() === "/memory") {
52908
+ const mems = listMemories(repoPath);
52909
+ const content = mems.length === 0 ? "No memories stored for this repo yet." : `Memories for this repo:
52910
+
52911
+ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
52912
+ `)}`;
52913
+ const msg = { role: "assistant", content, type: "text" };
52914
+ setCommitted((prev) => [...prev, msg]);
52915
+ setAllMessages((prev) => [...prev, msg]);
52916
+ return;
52917
+ }
52918
+ if (text.trim().toLowerCase().startsWith("/memory add")) {
52919
+ const content = text.trim().slice("/memory add".length).trim();
52920
+ if (!content) {
52921
+ const msg2 = {
52922
+ role: "assistant",
52923
+ content: "Usage: `/memory add <content>`",
52924
+ type: "text"
52925
+ };
52926
+ setCommitted((prev) => [...prev, msg2]);
52927
+ setAllMessages((prev) => [...prev, msg2]);
52928
+ return;
52929
+ }
52930
+ const mem = addMemory(content, repoPath);
52931
+ const msg = {
52932
+ role: "assistant",
52933
+ content: `Memory saved **[${mem.id}]**: ${mem.content}`,
52934
+ type: "text"
52935
+ };
52936
+ setCommitted((prev) => [...prev, msg]);
52937
+ setAllMessages((prev) => [...prev, msg]);
52938
+ return;
52939
+ }
52940
+ if (text.trim().toLowerCase().startsWith("/memory delete")) {
52941
+ const id = text.trim().split(/\s+/)[2];
52942
+ if (!id) {
52943
+ const msg2 = {
52944
+ role: "assistant",
52945
+ content: "Usage: `/memory delete <id>`",
52946
+ type: "text"
52947
+ };
52948
+ setCommitted((prev) => [...prev, msg2]);
52949
+ setAllMessages((prev) => [...prev, msg2]);
52950
+ return;
52951
+ }
52952
+ const deleted = deleteMemory(id, repoPath);
52953
+ const msg = {
52954
+ role: "assistant",
52955
+ content: deleted ? `Memory **[${id}]** deleted.` : `Memory **[${id}]** not found.`,
52956
+ type: "text"
52957
+ };
52958
+ setCommitted((prev) => [...prev, msg]);
52959
+ setAllMessages((prev) => [...prev, msg]);
52960
+ return;
52961
+ }
52962
+ if (text.trim().toLowerCase() === "/memory clear") {
52963
+ clearRepoMemory(repoPath);
52964
+ const msg = {
52965
+ role: "assistant",
52966
+ content: "All memories cleared for this repo.",
52967
+ type: "text"
52968
+ };
52969
+ setCommitted((prev) => [...prev, msg]);
52970
+ setAllMessages((prev) => [...prev, msg]);
52663
52971
  return;
52664
52972
  }
52665
52973
  const userMsg = { role: "user", content: text, type: "text" };
@@ -52667,6 +52975,18 @@ var ChatRunner = ({ repoPath }) => {
52667
52975
  setCommitted((prev) => [...prev, userMsg]);
52668
52976
  setAllMessages(nextAll);
52669
52977
  toolResultCache.current.clear();
52978
+ batchApprovedRef.current = false;
52979
+ inputHistoryRef.current = [
52980
+ text,
52981
+ ...inputHistoryRef.current.filter((m) => m !== text)
52982
+ ].slice(0, 50);
52983
+ historyIndexRef.current = -1;
52984
+ if (!chatName) {
52985
+ const name = getChatNameSuggestions(nextAll)[0] ?? `chat-${new Date().toISOString().slice(0, 10)}`;
52986
+ updateChatName(name);
52987
+ setRecentChats((prev) => [name, ...prev.filter((n) => n !== name)].slice(0, 10));
52988
+ saveChat(name, repoPath, nextAll);
52989
+ }
52670
52990
  const abort = new AbortController;
52671
52991
  abortControllerRef.current = abort;
52672
52992
  setStage({ type: "thinking" });
@@ -52678,6 +52998,7 @@ var ChatRunner = ({ repoPath }) => {
52678
52998
  if (stage.type === "thinking" && key.escape) {
52679
52999
  abortControllerRef.current?.abort();
52680
53000
  abortControllerRef.current = null;
53001
+ batchApprovedRef.current = false;
52681
53002
  setStage({ type: "idle" });
52682
53003
  return;
52683
53004
  }
@@ -52686,6 +53007,20 @@ var ChatRunner = ({ repoPath }) => {
52686
53007
  process.exit(0);
52687
53008
  return;
52688
53009
  }
53010
+ if (key.upArrow && inputHistoryRef.current.length > 0) {
53011
+ const next = Math.min(historyIndexRef.current + 1, inputHistoryRef.current.length - 1);
53012
+ historyIndexRef.current = next;
53013
+ setInputValue(inputHistoryRef.current[next]);
53014
+ setInputKey((k) => k + 1);
53015
+ return;
53016
+ }
53017
+ if (key.downArrow) {
53018
+ const next = historyIndexRef.current - 1;
53019
+ historyIndexRef.current = next;
53020
+ setInputValue(next < 0 ? "" : inputHistoryRef.current[next]);
53021
+ setInputKey((k) => k + 1);
53022
+ return;
53023
+ }
52689
53024
  if (key.tab && inputValue.startsWith("/")) {
52690
53025
  const q = inputValue.toLowerCase();
52691
53026
  const match = COMMANDS.find((c) => c.cmd.startsWith(q));
@@ -52704,9 +53039,9 @@ var ChatRunner = ({ repoPath }) => {
52704
53039
  startCloneRepo(cloneUrl).then((result2) => {
52705
53040
  if (result2.done) {
52706
53041
  const repoName = cloneUrl.split("/").pop()?.replace(/\.git$/, "") ?? "repo";
52707
- const destPath = path16.join(os7.tmpdir(), repoName);
53042
+ const destPath = path18.join(os9.tmpdir(), repoName);
52708
53043
  const fileCount = walkDir3(destPath).length;
52709
- appendHistory({
53044
+ appendMemory({
52710
53045
  kind: "url-fetched",
52711
53046
  detail: repoUrl,
52712
53047
  summary: `Cloned ${repoName} — ${fileCount} files`,
@@ -52742,16 +53077,14 @@ var ChatRunner = ({ repoPath }) => {
52742
53077
  if (stage.type === "clone-exists") {
52743
53078
  if (input === "y" || input === "Y") {
52744
53079
  const { repoUrl, repoPath: existingPath } = stage;
52745
- const cloneUrl = toCloneUrl(repoUrl);
52746
53080
  setStage({ type: "cloning", repoUrl });
52747
- startCloneRepo(cloneUrl, { forceReclone: true }).then((result2) => {
53081
+ startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then((result2) => {
52748
53082
  if (result2.done) {
52749
- const fileCount = walkDir3(existingPath).length;
52750
53083
  setStage({
52751
53084
  type: "clone-done",
52752
53085
  repoUrl,
52753
53086
  destPath: existingPath,
52754
- fileCount
53087
+ fileCount: walkDir3(existingPath).length
52755
53088
  });
52756
53089
  } else {
52757
53090
  setStage({
@@ -52764,12 +53097,11 @@ var ChatRunner = ({ repoPath }) => {
52764
53097
  }
52765
53098
  if (input === "n" || input === "N") {
52766
53099
  const { repoUrl, repoPath: existingPath } = stage;
52767
- const fileCount = walkDir3(existingPath).length;
52768
53100
  setStage({
52769
53101
  type: "clone-done",
52770
53102
  repoUrl,
52771
53103
  destPath: existingPath,
52772
- fileCount
53104
+ fileCount: walkDir3(existingPath).length
52773
53105
  });
52774
53106
  return;
52775
53107
  }
@@ -52791,7 +53123,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52791
53123
  type: "tool",
52792
53124
  toolName: "fetch",
52793
53125
  content: stage.repoUrl,
52794
- result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files. Use read-file with full path e.g. read-file ${stage.destPath}/README.md`,
53126
+ result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files.`,
52795
53127
  approved: true
52796
53128
  };
52797
53129
  const withClone = [...allMessages, contextMsg, summaryMsg];
@@ -52812,6 +53144,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52812
53144
  return;
52813
53145
  }
52814
53146
  if (input === "n" || input === "N" || key.escape) {
53147
+ batchApprovedRef.current = false;
52815
53148
  stage.resolve(false);
52816
53149
  return;
52817
53150
  }
@@ -52834,7 +53167,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52834
53167
  const msg = allMessages[pendingMsgIndex];
52835
53168
  if (msg?.type === "plan") {
52836
53169
  setCommitted((prev) => [...prev, { ...msg, applied: false }]);
52837
- appendHistory({
53170
+ appendMemory({
52838
53171
  kind: "code-skipped",
52839
53172
  detail: msg.patches.map((p) => p.path).join(", "),
52840
53173
  summary: `Skipped changes to ${msg.patches.length} file(s)`,
@@ -52849,7 +53182,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52849
53182
  if (key.return || input === "a" || input === "A") {
52850
53183
  try {
52851
53184
  applyPatches3(repoPath, stage.patches);
52852
- appendHistory({
53185
+ appendMemory({
52853
53186
  kind: "code-applied",
52854
53187
  detail: stage.patches.map((p) => p.path).join(", "),
52855
53188
  summary: `Applied changes to ${stage.patches.length} file(s)`,
@@ -52892,7 +53225,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52892
53225
  setStage({ type: "loading" });
52893
53226
  fetchFileTree(repoPath).catch(() => walkDir3(repoPath)).then((fileTree) => {
52894
53227
  const importantFiles = readImportantFiles(repoPath, fileTree);
52895
- const historySummary = buildHistorySummary(repoPath);
53228
+ const historySummary = buildMemorySummary(repoPath);
52896
53229
  const lensFile = readLensFile(repoPath);
52897
53230
  const lensContext = lensFile ? `
52898
53231
 
@@ -52901,17 +53234,16 @@ ${lensFile.overview}
52901
53234
 
52902
53235
  Important folders: ${lensFile.importantFolders.join(", ")}
52903
53236
  Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}` : "";
52904
- setSystemPrompt(buildSystemPrompt(importantFiles, historySummary) + lensContext);
52905
- const historyNote = historySummary ? `
52906
-
52907
- I have memory of previous actions in this repo.` : "";
52908
- const lensGreetNote = lensFile ? `
52909
-
52910
- Found LENS.md — I have context from a previous analysis of this repo.` : "";
53237
+ const toolsSection = registry.buildSystemPromptSection();
53238
+ setSystemPrompt(buildSystemPrompt(importantFiles, historySummary, toolsSection) + lensContext);
52911
53239
  const greeting = {
52912
53240
  role: "assistant",
52913
- content: `Welcome to Lens
52914
- Codebase loaded — ${importantFiles.length} files indexed.${historyNote}${lensGreetNote}
53241
+ content: `Welcome to Lens
53242
+ Codebase loaded — ${importantFiles.length} files indexed.${historySummary ? `
53243
+
53244
+ I have memory of previous actions in this repo.` : ""}${lensFile ? `
53245
+
53246
+ Found LENS.md — I have context from a previous analysis of this repo.` : ""}
52915
53247
  Ask me anything, tell me what to build, share a URL, or ask me to read/write files.
52916
53248
 
52917
53249
  Tip: type /timeline to browse commit history.`,
@@ -52926,7 +53258,7 @@ Tip: type /timeline to browse commit history.`,
52926
53258
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ProviderPicker, {
52927
53259
  onDone: handleProviderDone
52928
53260
  }, undefined, false, undefined, this);
52929
- if (stage.type === "loading") {
53261
+ if (stage.type === "loading")
52930
53262
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52931
53263
  gap: 1,
52932
53264
  marginTop: 1,
@@ -52946,19 +53278,16 @@ Tip: type /timeline to browse commit history.`,
52946
53278
  }, undefined, false, undefined, this)
52947
53279
  ]
52948
53280
  }, undefined, true, undefined, this);
52949
- }
52950
- if (showTimeline) {
53281
+ if (showTimeline)
52951
53282
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(TimelineRunner, {
52952
53283
  repoPath,
52953
53284
  onExit: () => setShowTimeline(false)
52954
53285
  }, undefined, false, undefined, this);
52955
- }
52956
- if (showReview) {
53286
+ if (showReview)
52957
53287
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ReviewCommand, {
52958
53288
  path: repoPath,
52959
53289
  onExit: () => setShowReview(false)
52960
53290
  }, undefined, false, undefined, this);
52961
- }
52962
53291
  if (stage.type === "clone-offer")
52963
53292
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(CloneOfferView, {
52964
53293
  stage,
@@ -53029,18 +53358,21 @@ Tip: type /timeline to browse commit history.`,
53029
53358
  children: [
53030
53359
  inputValue.startsWith("/") && /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(CommandPalette, {
53031
53360
  query: inputValue,
53032
- onSelect: (cmd) => {
53033
- setInputValue(cmd);
53034
- }
53361
+ onSelect: (cmd) => setInputValue(cmd),
53362
+ recentChats
53035
53363
  }, undefined, false, undefined, this),
53036
53364
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(InputBox, {
53037
53365
  value: inputValue,
53038
- onChange: setInputValue,
53366
+ onChange: (v) => {
53367
+ historyIndexRef.current = -1;
53368
+ setInputValue(v);
53369
+ },
53039
53370
  onSubmit: (val) => {
53040
53371
  if (val.trim())
53041
53372
  sendMessage(val.trim());
53042
53373
  setInputValue("");
53043
- }
53374
+ },
53375
+ inputKey
53044
53376
  }, undefined, false, undefined, this),
53045
53377
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ShortcutBar, {
53046
53378
  autoApprove
@@ -53054,8 +53386,8 @@ Tip: type /timeline to browse commit history.`,
53054
53386
  // src/commands/chat.tsx
53055
53387
  var jsx_dev_runtime23 = __toESM(require_jsx_dev_runtime(), 1);
53056
53388
  var ChatCommand = ({ path: inputPath }) => {
53057
- const resolvedPath = path17.resolve(inputPath);
53058
- if (!existsSync12(resolvedPath)) {
53389
+ const resolvedPath = path19.resolve(inputPath);
53390
+ if (!existsSync14(resolvedPath)) {
53059
53391
  return /* @__PURE__ */ jsx_dev_runtime23.jsxDEV(Box_default, {
53060
53392
  marginTop: 1,
53061
53393
  children: /* @__PURE__ */ jsx_dev_runtime23.jsxDEV(Text, {
@@ -53074,12 +53406,12 @@ var ChatCommand = ({ path: inputPath }) => {
53074
53406
  };
53075
53407
 
53076
53408
  // src/commands/timeline.tsx
53077
- import { existsSync as existsSync13 } from "fs";
53078
- import path18 from "path";
53409
+ import { existsSync as existsSync15 } from "fs";
53410
+ import path20 from "path";
53079
53411
  var jsx_dev_runtime24 = __toESM(require_jsx_dev_runtime(), 1);
53080
53412
  var TimelineCommand = ({ path: inputPath }) => {
53081
- const resolvedPath = path18.resolve(inputPath);
53082
- if (!existsSync13(resolvedPath)) {
53413
+ const resolvedPath = path20.resolve(inputPath);
53414
+ if (!existsSync15(resolvedPath)) {
53083
53415
  return /* @__PURE__ */ jsx_dev_runtime24.jsxDEV(Box_default, {
53084
53416
  marginTop: 1,
53085
53417
  children: /* @__PURE__ */ jsx_dev_runtime24.jsxDEV(Text, {
@@ -53096,9 +53428,259 @@ var TimelineCommand = ({ path: inputPath }) => {
53096
53428
  repoPath: resolvedPath
53097
53429
  }, undefined, false, undefined, this);
53098
53430
  };
53431
+ // src/utils/tools/builtins.ts
53432
+ var fetchTool = {
53433
+ name: "fetch",
53434
+ description: "load a URL",
53435
+ safe: true,
53436
+ permissionLabel: "fetch",
53437
+ systemPromptEntry: (i) => `### ${i}. fetch — load a URL
53438
+ <fetch>https://example.com</fetch>`,
53439
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53440
+ summariseInput: (url) => url,
53441
+ execute: async (url) => {
53442
+ try {
53443
+ const value = await fetchUrl(url);
53444
+ return { kind: "text", value };
53445
+ } catch (err) {
53446
+ return {
53447
+ kind: "error",
53448
+ value: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`
53449
+ };
53450
+ }
53451
+ }
53452
+ };
53453
+ var shellTool = {
53454
+ name: "shell",
53455
+ description: "run a terminal command",
53456
+ safe: false,
53457
+ permissionLabel: "run",
53458
+ systemPromptEntry: (i) => `### ${i}. shell — run a terminal command
53459
+ <shell>node -v</shell>`,
53460
+ parseInput: (body) => body || null,
53461
+ summariseInput: (cmd) => cmd,
53462
+ execute: async (cmd, ctx) => {
53463
+ const value = await runShell(cmd, ctx.repoPath);
53464
+ return { kind: "text", value };
53465
+ }
53466
+ };
53467
+ var readFileTool = {
53468
+ name: "read-file",
53469
+ description: "read a file from the repo",
53470
+ safe: true,
53471
+ permissionLabel: "read",
53472
+ systemPromptEntry: (i) => `### ${i}. read-file — read a file from the repo
53473
+ <read-file>src/foo.ts</read-file>`,
53474
+ parseInput: (body) => body || null,
53475
+ summariseInput: (p) => p,
53476
+ execute: (filePath, ctx) => ({
53477
+ kind: "text",
53478
+ value: readFile(filePath, ctx.repoPath)
53479
+ })
53480
+ };
53481
+ var readFolderTool = {
53482
+ name: "read-folder",
53483
+ description: "list contents of a folder (files + subfolders, one level deep)",
53484
+ safe: true,
53485
+ permissionLabel: "folder",
53486
+ systemPromptEntry: (i) => `### ${i}. read-folder — list contents of a folder (files + subfolders, one level deep)
53487
+ <read-folder>src/components</read-folder>`,
53488
+ parseInput: (body) => body || null,
53489
+ summariseInput: (p) => p,
53490
+ execute: (folderPath, ctx) => ({
53491
+ kind: "text",
53492
+ value: readFolder(folderPath, ctx.repoPath)
53493
+ })
53494
+ };
53495
+ var grepTool = {
53496
+ name: "grep",
53497
+ description: "search for a pattern across files in the repo",
53498
+ safe: true,
53499
+ permissionLabel: "grep",
53500
+ systemPromptEntry: (i) => `### ${i}. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
53501
+ <grep>
53502
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
53503
+ </grep>`,
53504
+ parseInput: (body) => {
53505
+ try {
53506
+ const parsed = JSON.parse(body);
53507
+ return { pattern: parsed.pattern, glob: parsed.glob ?? "**/*" };
53508
+ } catch {
53509
+ return { pattern: body, glob: "**/*" };
53510
+ }
53511
+ },
53512
+ summariseInput: ({ pattern, glob }) => `${pattern} — ${glob}`,
53513
+ execute: ({ pattern, glob }, ctx) => ({
53514
+ kind: "text",
53515
+ value: grepFiles(pattern, glob, ctx.repoPath)
53516
+ })
53517
+ };
53518
+ var writeFileTool = {
53519
+ name: "write-file",
53520
+ description: "create or overwrite a file",
53521
+ safe: false,
53522
+ permissionLabel: "write",
53523
+ systemPromptEntry: (i) => `### ${i}. write-file — create or overwrite a file
53524
+ <write-file>
53525
+ {"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}
53526
+ </write-file>`,
53527
+ parseInput: (body) => {
53528
+ try {
53529
+ const parsed = JSON.parse(body);
53530
+ if (!parsed.path)
53531
+ return null;
53532
+ return parsed;
53533
+ } catch {
53534
+ return null;
53535
+ }
53536
+ },
53537
+ summariseInput: ({ path: path21, content }) => `${path21} (${content.length} bytes)`,
53538
+ execute: ({ path: filePath, content }, ctx) => ({
53539
+ kind: "text",
53540
+ value: writeFile(filePath, content, ctx.repoPath)
53541
+ })
53542
+ };
53543
+ var deleteFileTool = {
53544
+ name: "delete-file",
53545
+ description: "permanently delete a single file",
53546
+ safe: false,
53547
+ permissionLabel: "delete",
53548
+ systemPromptEntry: (i) => `### ${i}. delete-file — permanently delete a single file
53549
+ <delete-file>src/old-component.tsx</delete-file>`,
53550
+ parseInput: (body) => body || null,
53551
+ summariseInput: (p) => p,
53552
+ execute: (filePath, ctx) => ({
53553
+ kind: "text",
53554
+ value: deleteFile(filePath, ctx.repoPath)
53555
+ })
53556
+ };
53557
+ var deleteFolderTool = {
53558
+ name: "delete-folder",
53559
+ description: "permanently delete a folder and all its contents",
53560
+ safe: false,
53561
+ permissionLabel: "delete folder",
53562
+ systemPromptEntry: (i) => `### ${i}. delete-folder — permanently delete a folder and all its contents
53563
+ <delete-folder>src/legacy</delete-folder>`,
53564
+ parseInput: (body) => body || null,
53565
+ summariseInput: (p) => p,
53566
+ execute: (folderPath, ctx) => ({
53567
+ kind: "text",
53568
+ value: deleteFolder(folderPath, ctx.repoPath)
53569
+ })
53570
+ };
53571
+ var openUrlTool = {
53572
+ name: "open-url",
53573
+ description: "open a URL in the user's default browser",
53574
+ safe: true,
53575
+ permissionLabel: "open",
53576
+ systemPromptEntry: (i) => `### ${i}. open-url — open a URL in the user's default browser
53577
+ <open-url>https://github.com/owner/repo</open-url>`,
53578
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53579
+ summariseInput: (url) => url,
53580
+ execute: (url) => ({ kind: "text", value: openUrl(url) })
53581
+ };
53582
+ var generatePdfTool = {
53583
+ name: "generate-pdf",
53584
+ description: "generate a PDF file from markdown-style content",
53585
+ safe: false,
53586
+ permissionLabel: "pdf",
53587
+ systemPromptEntry: (i) => `### ${i}. generate-pdf — generate a PDF file from markdown-style content
53588
+ <generate-pdf>
53589
+ {"path": "output/report.pdf", "content": "# Title\\n\\nSome body text."}
53590
+ </generate-pdf>`,
53591
+ parseInput: (body) => {
53592
+ try {
53593
+ const parsed = JSON.parse(body);
53594
+ return {
53595
+ filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
53596
+ content: parsed.content ?? ""
53597
+ };
53598
+ } catch {
53599
+ return null;
53600
+ }
53601
+ },
53602
+ summariseInput: ({ filePath }) => filePath,
53603
+ execute: ({ filePath, content }, ctx) => ({
53604
+ kind: "text",
53605
+ value: generatePdf(filePath, content, ctx.repoPath)
53606
+ })
53607
+ };
53608
+ var searchTool = {
53609
+ name: "search",
53610
+ description: "search the internet for anything you are unsure about",
53611
+ safe: true,
53612
+ permissionLabel: "search",
53613
+ systemPromptEntry: (i) => `### ${i}. search — search the internet for anything you are unsure about
53614
+ <search>how to use React useEffect cleanup function</search>`,
53615
+ parseInput: (body) => body || null,
53616
+ summariseInput: (q) => `"${q}"`,
53617
+ execute: async (query) => {
53618
+ try {
53619
+ const value = await searchWeb(query);
53620
+ return { kind: "text", value };
53621
+ } catch (err) {
53622
+ return {
53623
+ kind: "error",
53624
+ value: `Search failed: ${err instanceof Error ? err.message : String(err)}`
53625
+ };
53626
+ }
53627
+ }
53628
+ };
53629
+ var cloneTool = {
53630
+ name: "clone",
53631
+ description: "clone a GitHub repo so you can explore and discuss it",
53632
+ safe: false,
53633
+ permissionLabel: "clone",
53634
+ systemPromptEntry: (i) => `### ${i}. clone — clone a GitHub repo so you can explore and discuss it
53635
+ <clone>https://github.com/owner/repo</clone>`,
53636
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53637
+ summariseInput: (url) => url,
53638
+ execute: (repoUrl) => ({
53639
+ kind: "text",
53640
+ value: `Clone of ${repoUrl} was handled by the UI.`
53641
+ })
53642
+ };
53643
+ var changesTool = {
53644
+ name: "changes",
53645
+ description: "propose code edits (shown as a diff for user approval)",
53646
+ safe: false,
53647
+ permissionLabel: "changes",
53648
+ systemPromptEntry: (i) => `### ${i}. changes — propose code edits (shown as a diff for user approval)
53649
+ <changes>
53650
+ {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
53651
+ </changes>`,
53652
+ parseInput: (body) => {
53653
+ try {
53654
+ return JSON.parse(body);
53655
+ } catch {
53656
+ return null;
53657
+ }
53658
+ },
53659
+ summariseInput: ({ summary }) => summary,
53660
+ execute: ({ summary }) => ({
53661
+ kind: "text",
53662
+ value: `Changes proposed: ${summary}`
53663
+ })
53664
+ };
53665
+ function registerBuiltins() {
53666
+ registry.register(fetchTool);
53667
+ registry.register(shellTool);
53668
+ registry.register(readFileTool);
53669
+ registry.register(readFolderTool);
53670
+ registry.register(grepTool);
53671
+ registry.register(writeFileTool);
53672
+ registry.register(deleteFileTool);
53673
+ registry.register(deleteFolderTool);
53674
+ registry.register(openUrlTool);
53675
+ registry.register(generatePdfTool);
53676
+ registry.register(searchTool);
53677
+ registry.register(cloneTool);
53678
+ registry.register(changesTool);
53679
+ }
53099
53680
 
53100
53681
  // src/index.tsx
53101
53682
  var jsx_dev_runtime25 = __toESM(require_jsx_dev_runtime(), 1);
53683
+ registerBuiltins();
53102
53684
  var program2 = new Command;
53103
53685
  program2.command("repo <url>").description("Analyze a remote repository").action((url) => {
53104
53686
  render_default(/* @__PURE__ */ jsx_dev_runtime25.jsxDEV(RepoCommand, {