@ridit/lens 0.2.1 → 0.2.4

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
@@ -35841,6 +35841,43 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
35841
35841
  }
35842
35842
  });
35843
35843
 
35844
+ // src/utils/tools/registry.ts
35845
+ class ToolRegistry {
35846
+ tools = new Map;
35847
+ register(tool) {
35848
+ if (this.tools.has(tool.name)) {
35849
+ console.warn(`[ToolRegistry] Overwriting existing tool: "${tool.name}"`);
35850
+ }
35851
+ this.tools.set(tool.name, tool);
35852
+ }
35853
+ unregister(name) {
35854
+ this.tools.delete(name);
35855
+ }
35856
+ get(name) {
35857
+ return this.tools.get(name);
35858
+ }
35859
+ all() {
35860
+ return Array.from(this.tools.values());
35861
+ }
35862
+ names() {
35863
+ return Array.from(this.tools.keys());
35864
+ }
35865
+ buildSystemPromptSection() {
35866
+ const lines = [`## TOOLS
35867
+ `];
35868
+ 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.
35869
+ `);
35870
+ let i = 1;
35871
+ for (const tool of this.tools.values()) {
35872
+ lines.push(tool.systemPromptEntry(i++));
35873
+ }
35874
+ return lines.join(`
35875
+ `);
35876
+ }
35877
+ }
35878
+ var registry = new ToolRegistry;
35879
+ globalThis.__lens_registry = registry;
35880
+
35844
35881
  // node_modules/ink/build/render.js
35845
35882
  import { Stream } from "node:stream";
35846
35883
  import process13 from "node:process";
@@ -49206,72 +49243,17 @@ print("OK")
49206
49243
  }
49207
49244
  }
49208
49245
  // src/prompts/system.ts
49209
- function buildSystemPrompt(files, memorySummary = "") {
49246
+ function buildSystemPrompt(files, memorySummary = "", toolsSection) {
49210
49247
  const fileList = files.map((f) => `### ${f.path}
49211
49248
  \`\`\`
49212
49249
  ${f.content.slice(0, 2000)}
49213
49250
  \`\`\``).join(`
49214
49251
 
49215
49252
  `);
49253
+ const tools = toolsSection ?? BUILTIN_TOOLS_SECTION;
49216
49254
  return `You are an expert software engineer assistant with access to the user's codebase and tools.
49217
49255
 
49218
- ## TOOLS
49219
-
49220
- 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.
49221
-
49222
- ### 1. fetch — load a URL
49223
- <fetch>https://example.com</fetch>
49224
-
49225
- ### 2. shell — run a terminal command
49226
- <shell>node -v</shell>
49227
-
49228
- ### 3. read-file — read a file from the repo
49229
- <read-file>src/foo.ts</read-file>
49230
-
49231
- ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
49232
- <read-folder>src/components</read-folder>
49233
-
49234
- ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
49235
- <grep>
49236
- {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
49237
- </grep>
49238
-
49239
- ### 6. write-file — create or overwrite a file
49240
- <write-file>
49241
- {"path": "data/output.csv", "content": "col1,col2
49242
- val1,val2"}
49243
- </write-file>
49244
-
49245
- ### 7. delete-file — permanently delete a single file
49246
- <delete-file>src/old-component.tsx</delete-file>
49247
-
49248
- ### 8. delete-folder — permanently delete a folder and all its contents
49249
- <delete-folder>src/legacy</delete-folder>
49250
-
49251
- ### 9. open-url — open a URL in the user's default browser
49252
- <open-url>https://github.com/owner/repo</open-url>
49253
-
49254
- ### 10. generate-pdf — generate a PDF file from markdown-style content
49255
- <generate-pdf>
49256
- {"path": "output/report.pdf", "content": "# Title
49257
-
49258
- Some body text.
49259
-
49260
- ## Section
49261
-
49262
- More content."}
49263
- </generate-pdf>
49264
-
49265
- ### 11. search — search the internet for anything you are unsure about
49266
- <search>how to use React useEffect cleanup function</search>
49267
-
49268
- ### 12. clone — clone a GitHub repo so you can explore and discuss it
49269
- <clone>https://github.com/owner/repo</clone>
49270
-
49271
- ### 13. changes — propose code edits (shown as a diff for user approval)
49272
- <changes>
49273
- {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
49274
- </changes>
49256
+ ${tools}
49275
49257
 
49276
49258
  ## MEMORY OPERATIONS
49277
49259
 
@@ -49321,9 +49303,26 @@ You may emit multiple memory operations in a single response alongside normal co
49321
49303
  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.
49322
49304
  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
49323
49305
  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
49324
- 25. NEVER chain tool calls unless the user's request explicitly requires multiple steps
49325
- 26. NEVER read files, list folders, or run tools that were not asked for in the current user message
49326
- 27. 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.
49306
+ 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
49307
+ 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.
49308
+
49309
+ ## SCAFFOLDING — CHAINING WRITE-FILE CALLS
49310
+
49311
+ When creating multiple files (e.g. scaffolding a project or creating 10 files), emit ALL of them
49312
+ in a single response by chaining the tags back-to-back with no text between them:
49313
+
49314
+ <write-file>
49315
+ {"path": "test/file1.txt", "content": "File 1 content"}
49316
+ </write-file>
49317
+ <write-file>
49318
+ {"path": "test/file2.txt", "content": "File 2 content"}
49319
+ </write-file>
49320
+ <write-file>
49321
+ {"path": "test/file3.txt", "content": "File 3 content"}
49322
+ </write-file>
49323
+
49324
+ The system processes each tag sequentially and automatically continues to the next one.
49325
+ Do NOT wait for a user message between files — emit all tags at once.
49327
49326
 
49328
49327
  ## CRITICAL: READ BEFORE YOU WRITE
49329
49328
 
@@ -49345,61 +49344,62 @@ These rules are mandatory whenever you plan to edit or create a file:
49345
49344
  - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
49346
49345
  - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
49347
49346
 
49348
- ## WHEN TO USE read-folder:
49349
- - Before editing files in an unfamiliar directory — list it first to understand the structure
49350
- - When a feature spans multiple files and you are not sure what exists
49351
- - When the user asks you to explore or explain a part of the codebase
49352
-
49353
- ## SCAFFOLDING A NEW PROJECT (follow this exactly)
49354
-
49355
- When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app called list"):
49356
- 1. Create all files first using write-file with paths like \`list/package.json\`, \`list/src/index.tsx\`
49357
- 2. Then run \`cd list && bun install\` (or npm/pnpm) in one shell command
49358
- 3. Then run the project with \`cd list && bun run index.ts\` or whatever the entry point is
49359
- 4. NEVER run \`bun init\` — it is interactive and will hang. Create package.json manually with write-file instead
49360
- 5. TSX files need either tsconfig.json with \`"jsx": "react-jsx"\` or \`/** @jsxImportSource react */\` at the top
49361
-
49362
- ## FETCH → WRITE FLOW (follow this exactly when saving fetched data)
49363
-
49364
- 1. fetch the URL
49365
- 2. Analyze the result — count the rows, identify columns, check completeness
49366
- 3. Tell the user what you found: "Found X rows with columns: A, B, C. Writing now."
49367
- 4. emit write-file with correctly structured, complete content
49368
- 5. After write-file confirms success, emit read-file to verify
49369
- 6. Only after read-file confirms content is correct, tell the user it is done
49370
-
49371
- ## WHEN TO USE TOOLS
49372
-
49373
- - User shares any URL → fetch it immediately
49374
- - User asks to run anything → shell it immediately
49375
- - User asks to open a link, open a URL, or visit a website → open-url it immediately, do NOT use fetch
49376
- - User asks to delete a file → delete-file it immediately (requires approval)
49377
- - User asks to delete a folder or directory → delete-folder it immediately (requires approval)
49378
- - 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
49379
- - User asks to read a file → read-file it immediately, NEVER use shell cat/type
49380
- - 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
49381
- - User asks to explore a folder or directory → read-folder it immediately
49382
- - User asks to save/create/write a file → write-file it immediately, then read-file to verify
49383
- - User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
49384
- - User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
49385
- - 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.
49386
- - You are unsure about an API, library, error, concept, or piece of code → search it immediately
49387
- - User asks about something recent or that you might not know → search it immediately
49388
- - You are about to say "I'm not sure" or "I don't know" → search instead of guessing
49389
-
49390
- ## shell IS ONLY FOR:
49391
- - Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
49392
- - Installing packages: \`npm install\`, \`pip install\`
49393
- - Building/testing: \`npm run build\`, \`bun test\`
49394
- - Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
49395
- - Anything that EXECUTES — not reads or lists
49396
-
49397
49347
  ## CODEBASE
49398
49348
 
49399
49349
  ${fileList.length > 0 ? fileList : "(no files indexed)"}
49400
49350
 
49401
49351
  ${memorySummary}`;
49402
49352
  }
49353
+ var BUILTIN_TOOLS_SECTION = `## TOOLS
49354
+
49355
+ 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.
49356
+
49357
+ ### 1. fetch — load a URL
49358
+ <fetch>https://example.com</fetch>
49359
+
49360
+ ### 2. shell — run a terminal command
49361
+ <shell>node -v</shell>
49362
+
49363
+ ### 3. read-file — read a file from the repo
49364
+ <read-file>src/foo.ts</read-file>
49365
+
49366
+ ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
49367
+ <read-folder>src/components</read-folder>
49368
+
49369
+ ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
49370
+ <grep>
49371
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
49372
+ </grep>
49373
+
49374
+ ### 6. write-file — create or overwrite a file
49375
+ <write-file>
49376
+ {"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}
49377
+ </write-file>
49378
+
49379
+ ### 7. delete-file — permanently delete a single file
49380
+ <delete-file>src/old-component.tsx</delete-file>
49381
+
49382
+ ### 8. delete-folder — permanently delete a folder and all its contents
49383
+ <delete-folder>src/legacy</delete-folder>
49384
+
49385
+ ### 9. open-url — open a URL in the user's default browser
49386
+ <open-url>https://github.com/owner/repo</open-url>
49387
+
49388
+ ### 10. generate-pdf — generate a PDF file from markdown-style content
49389
+ <generate-pdf>
49390
+ {"path": "output/report.pdf", "content": "# Title\\n\\nSome body text.\\n\\n## Section\\n\\nMore content."}
49391
+ </generate-pdf>
49392
+
49393
+ ### 11. search — search the internet for anything you are unsure about
49394
+ <search>how to use React useEffect cleanup function</search>
49395
+
49396
+ ### 12. clone — clone a GitHub repo so you can explore and discuss it
49397
+ <clone>https://github.com/owner/repo</clone>
49398
+
49399
+ ### 13. changes — propose code edits (shown as a diff for user approval)
49400
+ <changes>
49401
+ {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
49402
+ </changes>`;
49403
49403
  // src/prompts/fewshot.ts
49404
49404
  var FEW_SHOT_MESSAGES = [
49405
49405
  {
@@ -49846,129 +49846,89 @@ Done — removed that memory.`
49846
49846
  function parseResponse(text) {
49847
49847
  const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49848
49848
  const candidates = [];
49849
- const patterns = [
49850
- { kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
49851
- { kind: "shell", re: /<shell>([\s\S]*?)<\/shell>/g },
49852
- { kind: "read-file", re: /<read-file>([\s\S]*?)<\/read-file>/g },
49853
- { kind: "read-folder", re: /<read-folder>([\s\S]*?)<\/read-folder>/g },
49854
- { kind: "grep", re: /<grep>([\s\S]*?)<\/grep>/g },
49855
- { kind: "delete-file", re: /<delete-file>([\s\S]*?)<\/delete-file>/g },
49856
- {
49857
- kind: "delete-folder",
49858
- re: /<delete-folder>([\s\S]*?)<\/delete-folder>/g
49859
- },
49860
- { kind: "open-url", re: /<open-url>([\s\S]*?)<\/open-url>/g },
49861
- { kind: "generate-pdf", re: /<generate-pdf>([\s\S]*?)<\/generate-pdf>/g },
49862
- { kind: "write-file", re: /<write-file>([\s\S]*?)<\/write-file>/g },
49863
- { kind: "search", re: /<search>([\s\S]*?)<\/search>/g },
49864
- { kind: "clone", re: /<clone>([\s\S]*?)<\/clone>/g },
49865
- { kind: "changes", re: /<changes>([\s\S]*?)<\/changes>/g },
49866
- { kind: "fetch", re: /```fetch\r?\n([\s\S]*?)\r?\n```/g },
49867
- { kind: "shell", re: /```shell\r?\n([\s\S]*?)\r?\n```/g },
49868
- { kind: "read-file", re: /```read-file\r?\n([\s\S]*?)\r?\n```/g },
49869
- { kind: "read-folder", re: /```read-folder\r?\n([\s\S]*?)\r?\n```/g },
49870
- { kind: "write-file", re: /```write-file\r?\n([\s\S]*?)\r?\n```/g },
49871
- { kind: "search", re: /```search\r?\n([\s\S]*?)\r?\n```/g },
49872
- { kind: "changes", re: /```changes\r?\n([\s\S]*?)\r?\n```/g }
49873
- ];
49874
- for (const { kind: kind2, re } of patterns) {
49875
- re.lastIndex = 0;
49876
- const m = re.exec(scanText);
49877
- if (m) {
49878
- const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
49879
- const originalMatch = originalRe.exec(text.slice(m.index));
49880
- if (originalMatch) {
49881
- const fakeMatch = Object.assign([
49882
- text.slice(m.index, m.index + originalMatch[0].length),
49883
- originalMatch[1]
49884
- ], { index: m.index, input: text, groups: undefined });
49885
- candidates.push({ index: m.index, kind: kind2, match: fakeMatch });
49849
+ for (const toolName2 of registry.names()) {
49850
+ const escaped = toolName2.replace(/[-]/g, "\\-");
49851
+ const xmlRe = new RegExp(`<${escaped}>([\\s\\S]*?)<\\/${escaped}>`, "g");
49852
+ xmlRe.lastIndex = 0;
49853
+ const xmlM = xmlRe.exec(scanText);
49854
+ if (xmlM) {
49855
+ const orig = new RegExp(xmlRe.source);
49856
+ const origM = orig.exec(text.slice(xmlM.index));
49857
+ if (origM) {
49858
+ candidates.push({
49859
+ index: xmlM.index,
49860
+ toolName: toolName2,
49861
+ match: Object.assign([
49862
+ text.slice(xmlM.index, xmlM.index + origM[0].length),
49863
+ origM[1]
49864
+ ], { index: xmlM.index, input: text, groups: undefined })
49865
+ });
49866
+ }
49867
+ }
49868
+ const fencedRe = new RegExp(`\`\`\`${escaped}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\``, "g");
49869
+ fencedRe.lastIndex = 0;
49870
+ const fencedM = fencedRe.exec(scanText);
49871
+ if (fencedM) {
49872
+ const orig = new RegExp(fencedRe.source);
49873
+ const origM = orig.exec(text.slice(fencedM.index));
49874
+ if (origM) {
49875
+ candidates.push({
49876
+ index: fencedM.index,
49877
+ toolName: toolName2,
49878
+ match: Object.assign([
49879
+ text.slice(fencedM.index, fencedM.index + origM[0].length),
49880
+ origM[1]
49881
+ ], { index: fencedM.index, input: text, groups: undefined })
49882
+ });
49886
49883
  }
49887
49884
  }
49888
49885
  }
49889
49886
  if (candidates.length === 0)
49890
49887
  return { kind: "text", content: text.trim() };
49891
49888
  candidates.sort((a, b) => a.index - b.index);
49892
- const { kind, match } = candidates[0];
49893
- const before2 = text.slice(0, match.index).replace(/<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g, "").trim();
49889
+ const { toolName, match } = candidates[0];
49890
+ const before2 = text.slice(0, match.index).trim();
49894
49891
  const body = (match[1] ?? "").trim();
49895
- if (kind === "changes") {
49892
+ const afterMatch = text.slice(match.index + match[0].length).trim();
49893
+ const remainder = afterMatch.length > 0 ? afterMatch : undefined;
49894
+ if (toolName === "changes") {
49896
49895
  try {
49897
49896
  const parsed = JSON.parse(body);
49898
49897
  const display = [before2, parsed.summary].filter(Boolean).join(`
49899
49898
 
49900
49899
  `);
49901
- return { kind: "changes", content: display, patches: parsed.patches };
49902
- } catch {}
49903
- }
49904
- if (kind === "shell")
49905
- return { kind: "shell", content: before2, command: body };
49906
- if (kind === "fetch")
49907
- return {
49908
- kind: "fetch",
49909
- content: before2,
49910
- url: body.replace(/^<|>$/g, "").trim()
49911
- };
49912
- if (kind === "read-file")
49913
- return { kind: "read-file", content: before2, filePath: body };
49914
- if (kind === "read-folder")
49915
- return { kind: "read-folder", content: before2, folderPath: body };
49916
- if (kind === "delete-file")
49917
- return { kind: "delete-file", content: before2, filePath: body };
49918
- if (kind === "delete-folder")
49919
- return { kind: "delete-folder", content: before2, folderPath: body };
49920
- if (kind === "open-url")
49921
- return {
49922
- kind: "open-url",
49923
- content: before2,
49924
- url: body.replace(/^<|>$/g, "").trim()
49925
- };
49926
- if (kind === "search")
49927
- return { kind: "search", content: before2, query: body };
49928
- if (kind === "clone")
49929
- return {
49930
- kind: "clone",
49931
- content: before2,
49932
- repoUrl: body.replace(/^<|>$/g, "").trim()
49933
- };
49934
- if (kind === "generate-pdf") {
49935
- try {
49936
- const parsed = JSON.parse(body);
49937
49900
  return {
49938
- kind: "generate-pdf",
49939
- content: before2,
49940
- filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
49941
- pdfContent: parsed.content ?? ""
49942
- };
49943
- } catch {
49944
- return { kind: "text", content: text };
49945
- }
49946
- }
49947
- if (kind === "grep") {
49948
- try {
49949
- const parsed = JSON.parse(body);
49950
- return {
49951
- kind: "grep",
49952
- content: before2,
49953
- pattern: parsed.pattern,
49954
- glob: parsed.glob ?? "**/*"
49901
+ kind: "changes",
49902
+ content: display,
49903
+ patches: parsed.patches,
49904
+ remainder
49955
49905
  };
49956
49906
  } catch {
49957
- return { kind: "grep", content: before2, pattern: body, glob: "**/*" };
49907
+ return { kind: "text", content: text.trim() };
49958
49908
  }
49959
49909
  }
49960
- if (kind === "write-file") {
49961
- try {
49962
- const parsed = JSON.parse(body);
49963
- return {
49964
- kind: "write-file",
49965
- content: before2,
49966
- filePath: parsed.path,
49967
- fileContent: parsed.content
49968
- };
49969
- } catch {}
49910
+ if (toolName === "clone") {
49911
+ return {
49912
+ kind: "clone",
49913
+ content: before2,
49914
+ repoUrl: body.replace(/^<|>$/g, "").trim(),
49915
+ remainder
49916
+ };
49970
49917
  }
49971
- return { kind: "text", content: text.trim() };
49918
+ const tool = registry.get(toolName);
49919
+ if (!tool)
49920
+ return { kind: "text", content: text.trim() };
49921
+ const input = tool.parseInput(body);
49922
+ if (input === null)
49923
+ return { kind: "text", content: text.trim() };
49924
+ return {
49925
+ kind: "tool",
49926
+ toolName,
49927
+ input,
49928
+ rawInput: body,
49929
+ content: before2,
49930
+ remainder
49931
+ };
49972
49932
  }
49973
49933
  function extractGithubUrl(text) {
49974
49934
  const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
@@ -49987,10 +49947,9 @@ function buildApiMessages(messages) {
49987
49947
  content: "The tool call was denied by the user. Please respond without using that tool."
49988
49948
  };
49989
49949
  }
49990
- 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}`;
49991
49950
  return {
49992
49951
  role: "user",
49993
- content: `Here is the output from the ${label}:
49952
+ content: `Here is the output from the ${m.toolName} of ${m.content}:
49994
49953
 
49995
49954
  ${m.result}
49996
49955
 
@@ -50489,50 +50448,71 @@ function PermissionPrompt({
50489
50448
  let icon;
50490
50449
  let label;
50491
50450
  let value;
50492
- if (tool.type === "shell") {
50493
- icon = "$";
50494
- label = "run";
50495
- value = tool.command;
50496
- } else if (tool.type === "fetch") {
50497
- icon = "~>";
50498
- label = "fetch";
50499
- value = tool.url;
50500
- } else if (tool.type === "read-file") {
50501
- icon = "r";
50502
- label = "read";
50503
- value = tool.filePath;
50504
- } else if (tool.type === "read-folder") {
50505
- icon = "d";
50506
- label = "folder";
50507
- value = tool.folderPath;
50508
- } else if (tool.type === "grep") {
50509
- icon = "/";
50510
- label = "grep";
50511
- value = `${tool.pattern} ${tool.glob}`;
50512
- } else if (tool.type === "delete-file") {
50513
- icon = "x";
50514
- label = "delete";
50515
- value = tool.filePath;
50516
- } else if (tool.type === "delete-folder") {
50517
- icon = "X";
50518
- label = "delete folder";
50519
- value = tool.folderPath;
50520
- } else if (tool.type === "open-url") {
50521
- icon = "↗";
50522
- label = "open";
50523
- value = tool.url;
50524
- } else if (tool.type === "generate-pdf") {
50525
- icon = "P";
50526
- label = "pdf";
50527
- value = tool.filePath;
50528
- } else if (tool.type === "write-file") {
50529
- icon = "w";
50530
- label = "write";
50531
- value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50451
+ if ("_label" in tool) {
50452
+ const iconMap = {
50453
+ run: "$",
50454
+ fetch: "~>",
50455
+ read: "r",
50456
+ write: "w",
50457
+ delete: "x",
50458
+ "delete folder": "X",
50459
+ open: "",
50460
+ pdf: "P",
50461
+ search: "?",
50462
+ folder: "d",
50463
+ grep: "/",
50464
+ clone: "",
50465
+ query: ""
50466
+ };
50467
+ icon = iconMap[tool._label] ?? "·";
50468
+ label = tool._label;
50469
+ value = tool._display;
50532
50470
  } else {
50533
- icon = "?";
50534
- label = "search";
50535
- value = tool.query;
50471
+ if (tool.type === "shell") {
50472
+ icon = "$";
50473
+ label = "run";
50474
+ value = tool.command;
50475
+ } else if (tool.type === "fetch") {
50476
+ icon = "~>";
50477
+ label = "fetch";
50478
+ value = tool.url;
50479
+ } else if (tool.type === "read-file") {
50480
+ icon = "r";
50481
+ label = "read";
50482
+ value = tool.filePath;
50483
+ } else if (tool.type === "read-folder") {
50484
+ icon = "d";
50485
+ label = "folder";
50486
+ value = tool.folderPath;
50487
+ } else if (tool.type === "grep") {
50488
+ icon = "/";
50489
+ label = "grep";
50490
+ value = `${tool.pattern} ${tool.glob}`;
50491
+ } else if (tool.type === "delete-file") {
50492
+ icon = "x";
50493
+ label = "delete";
50494
+ value = tool.filePath;
50495
+ } else if (tool.type === "delete-folder") {
50496
+ icon = "X";
50497
+ label = "delete folder";
50498
+ value = tool.folderPath;
50499
+ } else if (tool.type === "open-url") {
50500
+ icon = "↗";
50501
+ label = "open";
50502
+ value = tool.url;
50503
+ } else if (tool.type === "generate-pdf") {
50504
+ icon = "P";
50505
+ label = "pdf";
50506
+ value = tool.filePath;
50507
+ } else if (tool.type === "write-file") {
50508
+ icon = "w";
50509
+ label = "write";
50510
+ value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50511
+ } else {
50512
+ icon = "?";
50513
+ label = "search";
50514
+ value = tool.query ?? "";
50515
+ }
50536
50516
  }
50537
50517
  return /* @__PURE__ */ jsx_dev_runtime20.jsxDEV(Box_default, {
50538
50518
  flexDirection: "column",
@@ -52389,8 +52369,8 @@ function appendMemory(entry) {
52389
52369
  }
52390
52370
  function buildMemorySummary(repoPath) {
52391
52371
  const m = loadMemoryFile();
52392
- const relevant = m.entries.filter((e) => e.repoPath === repoPath).slice(-50);
52393
- const memories = m.memories.filter((mem) => mem.repoPath === repoPath);
52372
+ const relevant = m.entries.slice(-50);
52373
+ const memories = m.memories;
52394
52374
  const parts = [];
52395
52375
  if (memories.length > 0) {
52396
52376
  parts.push(`## MEMORIES ABOUT THIS REPO
@@ -52416,8 +52396,8 @@ ${lines.join(`
52416
52396
  }
52417
52397
  function clearRepoMemory(repoPath) {
52418
52398
  const m = loadMemoryFile();
52419
- m.entries = m.entries.filter((e) => e.repoPath !== repoPath);
52420
- m.memories = m.memories.filter((mem) => mem.repoPath !== repoPath);
52399
+ m.entries = m.entries = [];
52400
+ m.memories = m.memories = [];
52421
52401
  saveMemoryFile(m);
52422
52402
  }
52423
52403
  function generateId() {
@@ -52428,8 +52408,7 @@ function addMemory(content, repoPath) {
52428
52408
  const memory = {
52429
52409
  id: generateId(),
52430
52410
  content,
52431
- timestamp: new Date().toISOString(),
52432
- repoPath
52411
+ timestamp: new Date().toISOString()
52433
52412
  };
52434
52413
  m.memories.push(memory);
52435
52414
  saveMemoryFile(m);
@@ -52438,14 +52417,14 @@ function addMemory(content, repoPath) {
52438
52417
  function deleteMemory(id, repoPath) {
52439
52418
  const m = loadMemoryFile();
52440
52419
  const before2 = m.memories.length;
52441
- m.memories = m.memories.filter((mem) => !(mem.id === id && mem.repoPath === repoPath));
52420
+ m.memories = m.memories.filter((mem) => !(mem.id === id));
52442
52421
  if (m.memories.length === before2)
52443
52422
  return false;
52444
52423
  saveMemoryFile(m);
52445
52424
  return true;
52446
52425
  }
52447
52426
  function listMemories(repoPath) {
52448
- return loadMemoryFile().memories.filter((mem) => mem.repoPath === repoPath);
52427
+ return loadMemoryFile().memories;
52449
52428
  }
52450
52429
 
52451
52430
  // src/components/chat/ChatRunner.tsx
@@ -52555,8 +52534,7 @@ var ChatRunner = ({ repoPath }) => {
52555
52534
  };
52556
52535
  const abortControllerRef = import_react49.useRef(null);
52557
52536
  const toolResultCache = import_react49.useRef(new Map);
52558
- const inputBuffer = import_react49.useRef("");
52559
- const flushTimer = import_react49.useRef(null);
52537
+ const batchApprovedRef = import_react49.useRef(false);
52560
52538
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
52561
52539
  import_react48.default.useEffect(() => {
52562
52540
  const chats = listChats(repoPath);
@@ -52567,22 +52545,8 @@ var ChatRunner = ({ repoPath }) => {
52567
52545
  saveChat(chatNameRef.current, repoPath, allMessages);
52568
52546
  }
52569
52547
  }, [allMessages]);
52570
- const flushBuffer = () => {
52571
- const buf = inputBuffer.current;
52572
- if (!buf)
52573
- return;
52574
- inputBuffer.current = "";
52575
- setInputValue((v) => v + buf);
52576
- };
52577
- const scheduleFlush = () => {
52578
- if (flushTimer.current !== null)
52579
- return;
52580
- flushTimer.current = setTimeout(() => {
52581
- flushTimer.current = null;
52582
- flushBuffer();
52583
- }, 16);
52584
- };
52585
52548
  const handleError = (currentAll) => (err) => {
52549
+ batchApprovedRef.current = false;
52586
52550
  if (err instanceof Error && err.name === "AbortError") {
52587
52551
  setStage({ type: "idle" });
52588
52552
  return;
@@ -52598,6 +52562,7 @@ var ChatRunner = ({ repoPath }) => {
52598
52562
  };
52599
52563
  const processResponse = (raw, currentAll, signal) => {
52600
52564
  if (signal.aborted) {
52565
+ batchApprovedRef.current = false;
52601
52566
  setStage({ type: "idle" });
52602
52567
  return;
52603
52568
  }
@@ -52620,14 +52585,15 @@ var ChatRunner = ({ repoPath }) => {
52620
52585
  const cleanRaw = raw.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "").replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "").trim();
52621
52586
  const parsed = parseResponse(cleanRaw);
52622
52587
  if (parsed.kind === "changes") {
52588
+ batchApprovedRef.current = false;
52623
52589
  if (parsed.patches.length === 0) {
52624
- const msg2 = {
52590
+ const msg = {
52625
52591
  role: "assistant",
52626
52592
  content: parsed.content,
52627
52593
  type: "text"
52628
52594
  };
52629
- setAllMessages([...currentAll, msg2]);
52630
- setCommitted((prev) => [...prev, msg2]);
52595
+ setAllMessages([...currentAll, msg]);
52596
+ setCommitted((prev) => [...prev, msg]);
52631
52597
  setStage({ type: "idle" });
52632
52598
  return;
52633
52599
  }
@@ -52651,144 +52617,8 @@ var ChatRunner = ({ repoPath }) => {
52651
52617
  });
52652
52618
  return;
52653
52619
  }
52654
- 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") {
52655
- let tool;
52656
- if (parsed.kind === "shell") {
52657
- tool = { type: "shell", command: parsed.command };
52658
- } else if (parsed.kind === "fetch") {
52659
- tool = { type: "fetch", url: parsed.url };
52660
- } else if (parsed.kind === "read-file") {
52661
- tool = { type: "read-file", filePath: parsed.filePath };
52662
- } else if (parsed.kind === "read-folder") {
52663
- tool = { type: "read-folder", folderPath: parsed.folderPath };
52664
- } else if (parsed.kind === "grep") {
52665
- tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
52666
- } else if (parsed.kind === "delete-file") {
52667
- tool = { type: "delete-file", filePath: parsed.filePath };
52668
- } else if (parsed.kind === "delete-folder") {
52669
- tool = { type: "delete-folder", folderPath: parsed.folderPath };
52670
- } else if (parsed.kind === "open-url") {
52671
- tool = { type: "open-url", url: parsed.url };
52672
- } else if (parsed.kind === "generate-pdf") {
52673
- tool = {
52674
- type: "generate-pdf",
52675
- filePath: parsed.filePath,
52676
- content: parsed.pdfContent
52677
- };
52678
- } else if (parsed.kind === "search") {
52679
- tool = { type: "search", query: parsed.query };
52680
- } else {
52681
- tool = {
52682
- type: "write-file",
52683
- filePath: parsed.filePath,
52684
- fileContent: parsed.fileContent
52685
- };
52686
- }
52687
- if (parsed.content) {
52688
- const preambleMsg = {
52689
- role: "assistant",
52690
- content: parsed.content,
52691
- type: "text"
52692
- };
52693
- setAllMessages([...currentAll, preambleMsg]);
52694
- setCommitted((prev) => [...prev, preambleMsg]);
52695
- }
52696
- const isSafeTool = parsed.kind === "read-file" || parsed.kind === "read-folder" || parsed.kind === "grep" || parsed.kind === "fetch" || parsed.kind === "open-url" || parsed.kind === "search";
52697
- const executeAndContinue = async (approved) => {
52698
- let result2 = "(denied by user)";
52699
- if (approved) {
52700
- 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;
52701
- if (cacheKey && toolResultCache.current.has(cacheKey)) {
52702
- result2 = toolResultCache.current.get(cacheKey) + `
52703
-
52704
- [NOTE: This result was already retrieved earlier. Do not request it again.]`;
52705
- } else {
52706
- try {
52707
- setStage({ type: "thinking" });
52708
- if (parsed.kind === "shell") {
52709
- result2 = await runShell(parsed.command, repoPath);
52710
- } else if (parsed.kind === "fetch") {
52711
- result2 = await fetchUrl(parsed.url);
52712
- } else if (parsed.kind === "read-file") {
52713
- result2 = readFile(parsed.filePath, repoPath);
52714
- } else if (parsed.kind === "read-folder") {
52715
- result2 = readFolder(parsed.folderPath, repoPath);
52716
- } else if (parsed.kind === "grep") {
52717
- result2 = grepFiles(parsed.pattern, parsed.glob, repoPath);
52718
- } else if (parsed.kind === "delete-file") {
52719
- result2 = deleteFile(parsed.filePath, repoPath);
52720
- } else if (parsed.kind === "delete-folder") {
52721
- result2 = deleteFolder(parsed.folderPath, repoPath);
52722
- } else if (parsed.kind === "open-url") {
52723
- result2 = openUrl(parsed.url);
52724
- } else if (parsed.kind === "generate-pdf") {
52725
- result2 = generatePdf(parsed.filePath, parsed.pdfContent, repoPath);
52726
- } else if (parsed.kind === "write-file") {
52727
- result2 = writeFile(parsed.filePath, parsed.fileContent, repoPath);
52728
- } else if (parsed.kind === "search") {
52729
- result2 = await searchWeb(parsed.query);
52730
- }
52731
- if (cacheKey) {
52732
- toolResultCache.current.set(cacheKey, result2);
52733
- }
52734
- } catch (err) {
52735
- result2 = `Error: ${err instanceof Error ? err.message : "failed"}`;
52736
- }
52737
- }
52738
- }
52739
- if (approved && !result2.startsWith("Error:")) {
52740
- const kindMap2 = {
52741
- shell: "shell-run",
52742
- fetch: "url-fetched",
52743
- "read-file": "file-read",
52744
- "read-folder": "file-read",
52745
- grep: "file-read",
52746
- "delete-file": "file-written",
52747
- "delete-folder": "file-written",
52748
- "open-url": "url-fetched",
52749
- "generate-pdf": "file-written",
52750
- "write-file": "file-written",
52751
- search: "url-fetched"
52752
- };
52753
- appendMemory({
52754
- kind: kindMap2[parsed.kind] ?? "shell-run",
52755
- 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,
52756
- summary: result2.split(`
52757
- `)[0]?.slice(0, 120) ?? "",
52758
- repoPath
52759
- });
52760
- }
52761
- 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";
52762
- 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;
52763
- const toolMsg = {
52764
- role: "assistant",
52765
- type: "tool",
52766
- toolName,
52767
- content: toolContent,
52768
- result: result2,
52769
- approved
52770
- };
52771
- const withTool = [...currentAll, toolMsg];
52772
- setAllMessages(withTool);
52773
- setCommitted((prev) => [...prev, toolMsg]);
52774
- const nextAbort = new AbortController;
52775
- abortControllerRef.current = nextAbort;
52776
- setStage({ type: "thinking" });
52777
- callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52778
- };
52779
- if (autoApprove && isSafeTool) {
52780
- executeAndContinue(true);
52781
- return;
52782
- }
52783
- setStage({
52784
- type: "permission",
52785
- tool,
52786
- pendingMessages: currentAll,
52787
- resolve: executeAndContinue
52788
- });
52789
- return;
52790
- }
52791
52620
  if (parsed.kind === "clone") {
52621
+ batchApprovedRef.current = false;
52792
52622
  if (parsed.content) {
52793
52623
  const preambleMsg = {
52794
52624
  role: "assistant",
@@ -52805,23 +52635,116 @@ var ChatRunner = ({ repoPath }) => {
52805
52635
  });
52806
52636
  return;
52807
52637
  }
52808
- const msg = {
52809
- role: "assistant",
52810
- content: parsed.content,
52811
- type: "text"
52812
- };
52813
- const withMsg = [...currentAll, msg];
52814
- setAllMessages(withMsg);
52815
- setCommitted((prev) => [...prev, msg]);
52816
- const lastUserMsg = [...currentAll].reverse().find((m) => m.role === "user");
52817
- const githubUrl = lastUserMsg ? extractGithubUrl(lastUserMsg.content) : null;
52818
- if (githubUrl && !clonedUrls.has(githubUrl)) {
52819
- setTimeout(() => {
52820
- setStage({ type: "clone-offer", repoUrl: githubUrl });
52821
- }, 80);
52822
- } 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;
52823
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]);
52824
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;
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
+ });
52825
52748
  };
52826
52749
  const sendMessage = (text) => {
52827
52750
  if (!provider)
@@ -52839,7 +52762,7 @@ var ChatRunner = ({ repoPath }) => {
52839
52762
  setAutoApprove(next);
52840
52763
  const msg = {
52841
52764
  role: "assistant",
52842
- 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.",
52843
52766
  type: "text"
52844
52767
  };
52845
52768
  setCommitted((prev) => [...prev, msg]);
@@ -52848,13 +52771,13 @@ var ChatRunner = ({ repoPath }) => {
52848
52771
  }
52849
52772
  if (text.trim().toLowerCase() === "/clear history") {
52850
52773
  clearRepoMemory(repoPath);
52851
- const clearedMsg = {
52774
+ const msg = {
52852
52775
  role: "assistant",
52853
52776
  content: "History cleared for this repo.",
52854
52777
  type: "text"
52855
52778
  };
52856
- setCommitted((prev) => [...prev, clearedMsg]);
52857
- setAllMessages((prev) => [...prev, clearedMsg]);
52779
+ setCommitted((prev) => [...prev, msg]);
52780
+ setAllMessages((prev) => [...prev, msg]);
52858
52781
  return;
52859
52782
  }
52860
52783
  if (text.trim().toLowerCase() === "/chat") {
@@ -52901,7 +52824,7 @@ var ChatRunner = ({ repoPath }) => {
52901
52824
  if (!name) {
52902
52825
  const msg2 = {
52903
52826
  role: "assistant",
52904
- content: "Usage: `/chat delete <name>`",
52827
+ content: "Usage: `/chat delete <n>`",
52905
52828
  type: "text"
52906
52829
  };
52907
52830
  setCommitted((prev) => [...prev, msg2]);
@@ -53052,6 +52975,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53052
52975
  setCommitted((prev) => [...prev, userMsg]);
53053
52976
  setAllMessages(nextAll);
53054
52977
  toolResultCache.current.clear();
52978
+ batchApprovedRef.current = false;
53055
52979
  inputHistoryRef.current = [
53056
52980
  text,
53057
52981
  ...inputHistoryRef.current.filter((m) => m !== text)
@@ -53074,6 +52998,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53074
52998
  if (stage.type === "thinking" && key.escape) {
53075
52999
  abortControllerRef.current?.abort();
53076
53000
  abortControllerRef.current = null;
53001
+ batchApprovedRef.current = false;
53077
53002
  setStage({ type: "idle" });
53078
53003
  return;
53079
53004
  }
@@ -53092,8 +53017,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53092
53017
  if (key.downArrow) {
53093
53018
  const next = historyIndexRef.current - 1;
53094
53019
  historyIndexRef.current = next;
53095
- const val = next < 0 ? "" : inputHistoryRef.current[next];
53096
- setInputValue(val);
53020
+ setInputValue(next < 0 ? "" : inputHistoryRef.current[next]);
53097
53021
  setInputKey((k) => k + 1);
53098
53022
  return;
53099
53023
  }
@@ -53153,16 +53077,14 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53153
53077
  if (stage.type === "clone-exists") {
53154
53078
  if (input === "y" || input === "Y") {
53155
53079
  const { repoUrl, repoPath: existingPath } = stage;
53156
- const cloneUrl = toCloneUrl(repoUrl);
53157
53080
  setStage({ type: "cloning", repoUrl });
53158
- startCloneRepo(cloneUrl, { forceReclone: true }).then((result2) => {
53081
+ startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then((result2) => {
53159
53082
  if (result2.done) {
53160
- const fileCount = walkDir3(existingPath).length;
53161
53083
  setStage({
53162
53084
  type: "clone-done",
53163
53085
  repoUrl,
53164
53086
  destPath: existingPath,
53165
- fileCount
53087
+ fileCount: walkDir3(existingPath).length
53166
53088
  });
53167
53089
  } else {
53168
53090
  setStage({
@@ -53175,12 +53097,11 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53175
53097
  }
53176
53098
  if (input === "n" || input === "N") {
53177
53099
  const { repoUrl, repoPath: existingPath } = stage;
53178
- const fileCount = walkDir3(existingPath).length;
53179
53100
  setStage({
53180
53101
  type: "clone-done",
53181
53102
  repoUrl,
53182
53103
  destPath: existingPath,
53183
- fileCount
53104
+ fileCount: walkDir3(existingPath).length
53184
53105
  });
53185
53106
  return;
53186
53107
  }
@@ -53202,7 +53123,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
53202
53123
  type: "tool",
53203
53124
  toolName: "fetch",
53204
53125
  content: stage.repoUrl,
53205
- 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.`,
53206
53127
  approved: true
53207
53128
  };
53208
53129
  const withClone = [...allMessages, contextMsg, summaryMsg];
@@ -53223,6 +53144,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
53223
53144
  return;
53224
53145
  }
53225
53146
  if (input === "n" || input === "N" || key.escape) {
53147
+ batchApprovedRef.current = false;
53226
53148
  stage.resolve(false);
53227
53149
  return;
53228
53150
  }
@@ -53312,17 +53234,16 @@ ${lensFile.overview}
53312
53234
 
53313
53235
  Important folders: ${lensFile.importantFolders.join(", ")}
53314
53236
  Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}` : "";
53315
- setSystemPrompt(buildSystemPrompt(importantFiles, historySummary) + lensContext);
53316
- const historyNote = historySummary ? `
53317
-
53318
- I have memory of previous actions in this repo.` : "";
53319
- const lensGreetNote = lensFile ? `
53320
-
53321
- 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);
53322
53239
  const greeting = {
53323
53240
  role: "assistant",
53324
- content: `Welcome to Lens
53325
- 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.` : ""}
53326
53247
  Ask me anything, tell me what to build, share a URL, or ask me to read/write files.
53327
53248
 
53328
53249
  Tip: type /timeline to browse commit history.`,
@@ -53337,7 +53258,7 @@ Tip: type /timeline to browse commit history.`,
53337
53258
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ProviderPicker, {
53338
53259
  onDone: handleProviderDone
53339
53260
  }, undefined, false, undefined, this);
53340
- if (stage.type === "loading") {
53261
+ if (stage.type === "loading")
53341
53262
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
53342
53263
  gap: 1,
53343
53264
  marginTop: 1,
@@ -53357,19 +53278,16 @@ Tip: type /timeline to browse commit history.`,
53357
53278
  }, undefined, false, undefined, this)
53358
53279
  ]
53359
53280
  }, undefined, true, undefined, this);
53360
- }
53361
- if (showTimeline) {
53281
+ if (showTimeline)
53362
53282
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(TimelineRunner, {
53363
53283
  repoPath,
53364
53284
  onExit: () => setShowTimeline(false)
53365
53285
  }, undefined, false, undefined, this);
53366
- }
53367
- if (showReview) {
53286
+ if (showReview)
53368
53287
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ReviewCommand, {
53369
53288
  path: repoPath,
53370
53289
  onExit: () => setShowReview(false)
53371
53290
  }, undefined, false, undefined, this);
53372
- }
53373
53291
  if (stage.type === "clone-offer")
53374
53292
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(CloneOfferView, {
53375
53293
  stage,
@@ -53510,9 +53428,282 @@ var TimelineCommand = ({ path: inputPath }) => {
53510
53428
  repoPath: resolvedPath
53511
53429
  }, undefined, false, undefined, this);
53512
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
+ }
53680
+
53681
+ // src/utils/addons/loadAddons.ts
53682
+ import path21 from "path";
53683
+ import os10 from "os";
53684
+ import { existsSync as existsSync16, readdirSync as readdirSync5 } from "fs";
53685
+ var ADDONS_DIR = path21.join(os10.homedir(), ".lens", "addons");
53686
+ async function loadAddons() {
53687
+ if (!existsSync16(ADDONS_DIR)) {
53688
+ return;
53689
+ }
53690
+ const files = readdirSync5(ADDONS_DIR).filter((f) => f.endsWith(".js") && !f.startsWith("_"));
53691
+ for (const file of files) {
53692
+ const fullPath = path21.join(ADDONS_DIR, file);
53693
+ try {
53694
+ await import(fullPath);
53695
+ console.log(`[addons] loaded: ${file}
53696
+ `);
53697
+ } catch (err) {
53698
+ console.error(`[addons] failed to load ${file}:`, err instanceof Error ? err.message : String(err));
53699
+ }
53700
+ }
53701
+ }
53513
53702
 
53514
53703
  // src/index.tsx
53515
53704
  var jsx_dev_runtime25 = __toESM(require_jsx_dev_runtime(), 1);
53705
+ registerBuiltins();
53706
+ await loadAddons();
53516
53707
  var program2 = new Command;
53517
53708
  program2.command("repo <url>").description("Analyze a remote repository").action((url) => {
53518
53709
  render_default(/* @__PURE__ */ jsx_dev_runtime25.jsxDEV(RepoCommand, {