@ridit/lens 0.1.7 → 0.1.9

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
@@ -48707,10 +48707,10 @@ More content."}
48707
48707
  ### 11. search — search the internet for anything you are unsure about
48708
48708
  <search>how to use React useEffect cleanup function</search>
48709
48709
 
48710
- ### 11. clone — clone a GitHub repo so you can explore and discuss it
48710
+ ### 12. clone — clone a GitHub repo so you can explore and discuss it
48711
48711
  <clone>https://github.com/owner/repo</clone>
48712
48712
 
48713
- ### 12. changes — propose code edits (shown as a diff for user approval)
48713
+ ### 13. changes — propose code edits (shown as a diff for user approval)
48714
48714
  <changes>
48715
48715
  {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
48716
48716
  </changes>
@@ -48729,16 +48729,20 @@ More content."}
48729
48729
  10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
48730
48730
  11. write-file content field must be the COMPLETE file content, never empty or placeholder
48731
48731
  12. After a write-file succeeds, do NOT repeat it — trust the result and move on
48732
- 13. After a write-file succeeds, use read-file to verify the content before telling the user it is done
48732
+ 13. After a write-file succeeds, tell the user it is done immediately do NOT auto-read the file back to verify
48733
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
48734
  15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
48735
48735
  16. NEVER second-guess yourself mid-response — commit to your answer
48736
48736
  17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
48737
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
- 17. 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
- 18. 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
- 19. 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
- 20. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
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.
48742
48746
 
48743
48747
  ## CRITICAL: READ BEFORE YOU WRITE
48744
48748
 
@@ -48952,6 +48956,22 @@ Please continue your response based on this output.`
48952
48956
  role: "assistant",
48953
48957
  content: "`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210."
48954
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
+ },
48955
48975
  {
48956
48976
  role: "user",
48957
48977
  content: "read src folder"
@@ -49187,21 +49207,29 @@ Please continue your response based on this output.`
49187
49207
  },
49188
49208
  {
49189
49209
  role: "assistant",
49190
- content: "<read-file>ts-info.json</read-file>"
49210
+ content: "Done — `ts-info.json` has been saved."
49191
49211
  },
49192
49212
  {
49193
49213
  role: "user",
49194
- content: `Here is the output from read-file of ts-info.json:
49195
-
49196
- File: ts-info.json (1 lines)
49214
+ content: "write it to a file called hello.py"
49215
+ },
49216
+ {
49217
+ role: "assistant",
49218
+ content: `<write-file>
49219
+ {"path": "hello.py", "content": "# hello.py\\nprint('hello')"}
49220
+ </write-file>`
49221
+ },
49222
+ {
49223
+ role: "user",
49224
+ content: `Here is the output from write-file to hello.py:
49197
49225
 
49198
- {"name":"TypeScript","stars":100000}
49226
+ Written: /repo/hello.py (2 lines, 32 bytes)
49199
49227
 
49200
49228
  Please continue your response based on this output.`
49201
49229
  },
49202
49230
  {
49203
49231
  role: "assistant",
49204
- content: "Done — saved and verified `ts-info.json`. Data looks correct."
49232
+ content: "Done — `hello.py` has been written."
49205
49233
  },
49206
49234
  {
49207
49235
  role: "user",
@@ -49283,9 +49311,18 @@ Please continue your response based on this output.`
49283
49311
  {
49284
49312
  role: "assistant",
49285
49313
  content: "The `??` operator is the nullish coalescing operator. It returns the right side only when the left side is `null` or `undefined`."
49314
+ },
49315
+ {
49316
+ role: "user",
49317
+ content: "what does this project do?"
49318
+ },
49319
+ {
49320
+ 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."
49286
49322
  }
49287
49323
  ];
49288
49324
  function parseResponse(text) {
49325
+ const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49289
49326
  const candidates = [];
49290
49327
  const patterns = [
49291
49328
  { kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
@@ -49314,16 +49351,25 @@ function parseResponse(text) {
49314
49351
  ];
49315
49352
  for (const { kind: kind2, re } of patterns) {
49316
49353
  re.lastIndex = 0;
49317
- const m = re.exec(text);
49318
- if (m)
49319
- candidates.push({ index: m.index, kind: kind2, match: m });
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
+ }
49320
49366
  }
49321
49367
  if (candidates.length === 0)
49322
49368
  return { kind: "text", content: text.trim() };
49323
49369
  candidates.sort((a, b) => a.index - b.index);
49324
49370
  const { kind, match } = candidates[0];
49325
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();
49326
- const body = match[1].trim();
49372
+ const body = (match[1] ?? "").trim();
49327
49373
  if (kind === "changes") {
49328
49374
  try {
49329
49375
  const parsed = JSON.parse(body);
@@ -49426,7 +49472,7 @@ Please continue your response based on this output.`
49426
49472
  return { role: m.role, content: m.content };
49427
49473
  });
49428
49474
  }
49429
- async function callChat(provider, systemPrompt, messages) {
49475
+ async function callChat(provider, systemPrompt, messages, abortSignal) {
49430
49476
  const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
49431
49477
  let url;
49432
49478
  let headers;
@@ -49459,6 +49505,7 @@ async function callChat(provider, systemPrompt, messages) {
49459
49505
  }
49460
49506
  const controller = new AbortController;
49461
49507
  const timer = setTimeout(() => controller.abort(), 60000);
49508
+ abortSignal?.addEventListener("abort", () => controller.abort());
49462
49509
  const res = await fetch(url, {
49463
49510
  method: "POST",
49464
49511
  headers,
@@ -50010,7 +50057,6 @@ for line in raw.split("\\n"):
50010
50057
  elif s == "":
50011
50058
  story.append(Spacer(1, 6))
50012
50059
  else:
50013
- # handle **bold** inline
50014
50060
  import re
50015
50061
  s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
50016
50062
  s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
@@ -50063,23 +50109,15 @@ function InlineText({ text }) {
50063
50109
  function CodeBlock({ lang, code }) {
50064
50110
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
50065
50111
  flexDirection: "column",
50066
- marginY: 1,
50067
- marginLeft: 2,
50068
- children: [
50069
- lang && /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Text, {
50070
- color: "gray",
50071
- children: lang
50072
- }, undefined, false, undefined, this),
50073
- code.split(`
50112
+ children: code.split(`
50074
50113
  `).map((line, i) => /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Text, {
50075
- color: ACCENT,
50076
- children: [
50077
- " ",
50078
- line
50079
- ]
50080
- }, i, true, undefined, this))
50081
- ]
50082
- }, undefined, true, undefined, this);
50114
+ color: ACCENT,
50115
+ children: [
50116
+ " ",
50117
+ line
50118
+ ]
50119
+ }, i, true, undefined, this))
50120
+ }, undefined, false, undefined, this);
50083
50121
  }
50084
50122
  function MessageBody({ content }) {
50085
50123
  const segments = content.split(/(```[\s\S]*?```)/g);
@@ -50149,6 +50187,9 @@ function StaticMessage({ msg }) {
50149
50187
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
50150
50188
  marginBottom: 1,
50151
50189
  gap: 1,
50190
+ backgroundColor: "#1a1a1a",
50191
+ paddingLeft: 1,
50192
+ paddingRight: 2,
50152
50193
  children: [
50153
50194
  /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Text, {
50154
50195
  color: "gray",
@@ -50167,12 +50208,6 @@ function StaticMessage({ msg }) {
50167
50208
  shell: "$",
50168
50209
  fetch: "~>",
50169
50210
  "read-file": "r",
50170
- "read-folder": "d",
50171
- grep: "/",
50172
- "delete-file": "x",
50173
- "delete-folder": "X",
50174
- "open-url": "↗",
50175
- "generate-pdf": "P",
50176
50211
  "write-file": "w",
50177
50212
  search: "?"
50178
50213
  };
@@ -52349,6 +52384,7 @@ var ChatRunner = ({ repoPath }) => {
52349
52384
  const [showTimeline, setShowTimeline] = import_react48.useState(false);
52350
52385
  const [showReview, setShowReview] = import_react48.useState(false);
52351
52386
  const [autoApprove, setAutoApprove] = import_react48.useState(false);
52387
+ const abortControllerRef = import_react48.useRef(null);
52352
52388
  const toolResultCache = import_react48.useRef(new Map);
52353
52389
  const inputBuffer = import_react48.useRef("");
52354
52390
  const flushTimer = import_react48.useRef(null);
@@ -52369,6 +52405,10 @@ var ChatRunner = ({ repoPath }) => {
52369
52405
  }, 16);
52370
52406
  };
52371
52407
  const handleError = (currentAll) => (err) => {
52408
+ if (err instanceof Error && err.name === "AbortError") {
52409
+ setStage({ type: "idle" });
52410
+ return;
52411
+ }
52372
52412
  const errMsg = {
52373
52413
  role: "assistant",
52374
52414
  content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
@@ -52378,7 +52418,11 @@ var ChatRunner = ({ repoPath }) => {
52378
52418
  setCommitted((prev) => [...prev, errMsg]);
52379
52419
  setStage({ type: "idle" });
52380
52420
  };
52381
- const processResponse = (raw, currentAll) => {
52421
+ const processResponse = (raw, currentAll, signal) => {
52422
+ if (signal.aborted) {
52423
+ setStage({ type: "idle" });
52424
+ return;
52425
+ }
52382
52426
  const parsed = parseResponse(raw);
52383
52427
  if (parsed.kind === "changes") {
52384
52428
  if (parsed.patches.length === 0) {
@@ -52532,8 +52576,10 @@ var ChatRunner = ({ repoPath }) => {
52532
52576
  const withTool = [...currentAll, toolMsg];
52533
52577
  setAllMessages(withTool);
52534
52578
  setCommitted((prev) => [...prev, toolMsg]);
52579
+ const nextAbort = new AbortController;
52580
+ abortControllerRef.current = nextAbort;
52535
52581
  setStage({ type: "thinking" });
52536
- callChat(provider, systemPrompt, withTool).then((r) => processResponse(r, withTool)).catch(handleError(withTool));
52582
+ callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52537
52583
  };
52538
52584
  if (autoApprove && isSafeTool) {
52539
52585
  executeAndContinue(true);
@@ -52621,12 +52667,20 @@ var ChatRunner = ({ repoPath }) => {
52621
52667
  setCommitted((prev) => [...prev, userMsg]);
52622
52668
  setAllMessages(nextAll);
52623
52669
  toolResultCache.current.clear();
52670
+ const abort = new AbortController;
52671
+ abortControllerRef.current = abort;
52624
52672
  setStage({ type: "thinking" });
52625
- callChat(provider, systemPrompt, nextAll).then((raw) => processResponse(raw, nextAll)).catch(handleError(nextAll));
52673
+ callChat(provider, systemPrompt, nextAll, abort.signal).then((raw) => processResponse(raw, nextAll, abort.signal)).catch(handleError(nextAll));
52626
52674
  };
52627
52675
  use_input_default((input, key) => {
52628
52676
  if (showTimeline)
52629
52677
  return;
52678
+ if (stage.type === "thinking" && key.escape) {
52679
+ abortControllerRef.current?.abort();
52680
+ abortControllerRef.current = null;
52681
+ setStage({ type: "idle" });
52682
+ return;
52683
+ }
52630
52684
  if (stage.type === "idle") {
52631
52685
  if (key.ctrl && input === "c") {
52632
52686
  process.exit(0);
@@ -52958,6 +53012,11 @@ Tip: type /timeline to browse commit history.`,
52958
53012
  }, undefined, false, undefined, this),
52959
53013
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(TypewriterText, {
52960
53014
  text: thinkingPhrase
53015
+ }, undefined, false, undefined, this),
53016
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
53017
+ color: "gray",
53018
+ dimColor: true,
53019
+ children: "· esc cancel"
52961
53020
  }, undefined, false, undefined, this)
52962
53021
  ]
52963
53022
  }, undefined, true, undefined, this),
package/hello.py ADDED
@@ -0,0 +1,51 @@
1
+ # hello.py
2
+ # A simple script that reads a text file, makes an HTTP request,
3
+ # and prints the result in a friendly way.
4
+
5
+ import json
6
+ import pathlib
7
+ import sys
8
+ from urllib.request import urlopen
9
+
10
+ def read_local_file(path: str) -> str:
11
+ """Read the contents of a file and return it as a string."""
12
+ file_path = pathlib.Path(path)
13
+ if not file_path.is_file():
14
+ raise FileNotFoundError(f"❌ File not found: {path}")
15
+ return file_path.read_text(encoding="utf-8")
16
+
17
+ def fetch_json(url: str) -> dict:
18
+ """Fetch JSON from a URL and decode it into a Python dict."""
19
+ with urlopen(url) as response:
20
+ if response.status != 200:
21
+ raise RuntimeError(f"❌ HTTP {response.status} from {url}")
22
+ data = response.read()
23
+ return json.loads(data)
24
+
25
+ def main():
26
+ # 1️⃣ Read a local file (optional – you can comment this out)
27
+ try:
28
+ local_content = read_local_file("example.txt")
29
+ print("📄 Contents of example.txt:")
30
+ print(local_content)
31
+ except FileNotFoundError:
32
+ print("⚠️ example.txt not found – skipping that step.")
33
+
34
+ # 2️⃣ Fetch some JSON data from a public API
35
+ url = "https://api.github.com/repos/python/cpython"
36
+ print(f"\n🌐 Fetching data from {url} …")
37
+ repo_info = fetch_json(url)
38
+
39
+ # 3️⃣ Extract a couple of useful fields and display them
40
+ name = repo_info.get("name")
41
+ stars = repo_info.get("stargazers_count")
42
+ description = repo_info.get("description")
43
+ print("\n🔎 Repository info:")
44
+ print(f"• Name: {name}")
45
+ print(f"• Stars: {stars:,}") # adds commas for readability
46
+ print(f"• Description: {description}")
47
+
48
+ return 0
49
+
50
+ if __name__ == "__main__":
51
+ sys.exit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridit/lens",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Know Your Codebase.",
5
5
  "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
6
  "license": "MIT",
@@ -18,7 +18,6 @@
18
18
  "prepublishOnly": "npm run build"
19
19
  },
20
20
  "dependencies": {
21
- "chalk": "^5.6.2",
22
21
  "commander": "^14.0.3",
23
22
  "figures": "^6.1.0",
24
23
  "ink": "^6.8.0",
@@ -26,7 +25,8 @@
26
25
  "ink-text-input": "^6.0.0",
27
26
  "nanoid": "^5.1.6",
28
27
  "react": "^19.2.4",
29
- "react-devtools-core": "^7.0.1"
28
+ "react-devtools-core": "^7.0.1",
29
+ "sugar-high": "^0.9.5"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/bun": "latest",
package/src/colors.ts CHANGED
@@ -1 +1,10 @@
1
1
  export const ACCENT = "#DA7758";
2
+
3
+ export const TOKEN_KEYWORD = "#B385C9";
4
+ export const TOKEN_STRING = "#85C98A";
5
+ export const TOKEN_NUMBER = "#E8C170";
6
+ export const TOKEN_PROPERTY = "#79C5D4";
7
+ export const TOKEN_ENTITY = "#7AABDB";
8
+ export const TOKEN_TEXT = "#E8E8E8";
9
+ export const TOKEN_MUTED = "#888888";
10
+ export const TOKEN_COMMENT = "#777777";
@@ -34,8 +34,7 @@ function InlineText({ text }: { text: string }) {
34
34
 
35
35
  function CodeBlock({ lang, code }: { lang: string; code: string }) {
36
36
  return (
37
- <Box flexDirection="column" marginY={1} marginLeft={2}>
38
- {lang && <Text color="gray">{lang}</Text>}
37
+ <Box flexDirection="column">
39
38
  {code.split("\n").map((line, i) => (
40
39
  <Text key={i} color={ACCENT}>
41
40
  {" "}
@@ -102,7 +101,13 @@ function MessageBody({ content }: { content: string }) {
102
101
  export function StaticMessage({ msg }: { msg: Message }) {
103
102
  if (msg.role === "user") {
104
103
  return (
105
- <Box marginBottom={1} gap={1}>
104
+ <Box
105
+ marginBottom={1}
106
+ gap={1}
107
+ backgroundColor={"#1a1a1a"}
108
+ paddingLeft={1}
109
+ paddingRight={2}
110
+ >
106
111
  <Text color="gray">{">"}</Text>
107
112
  <Text color="white" bold>
108
113
  {msg.content}
@@ -116,12 +121,6 @@ export function StaticMessage({ msg }: { msg: Message }) {
116
121
  shell: "$",
117
122
  fetch: "~>",
118
123
  "read-file": "r",
119
- "read-folder": "d",
120
- grep: "/",
121
- "delete-file": "x",
122
- "delete-folder": "X",
123
- "open-url": "↗",
124
- "generate-pdf": "P",
125
124
  "write-file": "w",
126
125
  search: "?",
127
126
  };
@@ -107,6 +107,10 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
107
107
  const [showReview, setShowReview] = useState(false);
108
108
  const [autoApprove, setAutoApprove] = useState(false);
109
109
 
110
+ // Abort controller for the currently in-flight API call.
111
+ // Pressing ESC while thinking aborts the request and drops the response.
112
+ const abortControllerRef = useRef<AbortController | null>(null);
113
+
110
114
  // Cache of tool results within a single conversation turn to prevent
111
115
  // the model from re-calling tools it already ran with the same args
112
116
  const toolResultCache = useRef<Map<string, string>>(new Map());
@@ -131,6 +135,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
131
135
  };
132
136
 
133
137
  const handleError = (currentAll: Message[]) => (err: unknown) => {
138
+ // Silently drop aborted requests — user pressed ESC intentionally
139
+ if (err instanceof Error && err.name === "AbortError") {
140
+ setStage({ type: "idle" });
141
+ return;
142
+ }
134
143
  const errMsg: Message = {
135
144
  role: "assistant",
136
145
  content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
@@ -141,7 +150,17 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
141
150
  setStage({ type: "idle" });
142
151
  };
143
152
 
144
- const processResponse = (raw: string, currentAll: Message[]) => {
153
+ const processResponse = (
154
+ raw: string,
155
+ currentAll: Message[],
156
+ signal: AbortSignal,
157
+ ) => {
158
+ // If ESC was pressed before we got here, silently drop the response
159
+ if (signal.aborted) {
160
+ setStage({ type: "idle" });
161
+ return;
162
+ }
163
+
145
164
  const parsed = parseResponse(raw);
146
165
 
147
166
  if (parsed.kind === "changes") {
@@ -245,7 +264,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
245
264
  const executeAndContinue = async (approved: boolean) => {
246
265
  let result = "(denied by user)";
247
266
  if (approved) {
248
- // Build a cache key for idempotent read-only tools
249
267
  const cacheKey =
250
268
  parsed.kind === "read-file"
251
269
  ? `read-file:${parsed.filePath}`
@@ -256,7 +274,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
256
274
  : null;
257
275
 
258
276
  if (cacheKey && toolResultCache.current.has(cacheKey)) {
259
- // Return cached result with a note so the model stops retrying
260
277
  result =
261
278
  toolResultCache.current.get(cacheKey)! +
262
279
  "\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
@@ -294,7 +311,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
294
311
  } else if (parsed.kind === "search") {
295
312
  result = await searchWeb(parsed.query);
296
313
  }
297
- // Store result in cache for cacheable tools
298
314
  if (cacheKey) {
299
315
  toolResultCache.current.set(cacheKey, result);
300
316
  }
@@ -402,9 +418,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
402
418
  setAllMessages(withTool);
403
419
  setCommitted((prev) => [...prev, toolMsg]);
404
420
 
421
+ // Create a fresh abort controller for the follow-up call
422
+ const nextAbort = new AbortController();
423
+ abortControllerRef.current = nextAbort;
424
+
405
425
  setStage({ type: "thinking" });
406
- callChat(provider!, systemPrompt, withTool)
407
- .then((r: string) => processResponse(r, withTool))
426
+ callChat(provider!, systemPrompt, withTool, nextAbort.signal)
427
+ .then((r: string) => processResponse(r, withTool, nextAbort.signal))
408
428
  .catch(handleError(withTool));
409
429
  };
410
430
 
@@ -510,15 +530,28 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
510
530
  setCommitted((prev) => [...prev, userMsg]);
511
531
  setAllMessages(nextAll);
512
532
  toolResultCache.current.clear();
533
+
534
+ // Create a fresh abort controller for this request
535
+ const abort = new AbortController();
536
+ abortControllerRef.current = abort;
537
+
513
538
  setStage({ type: "thinking" });
514
- callChat(provider, systemPrompt, nextAll)
515
- .then((raw: string) => processResponse(raw, nextAll))
539
+ callChat(provider, systemPrompt, nextAll, abort.signal)
540
+ .then((raw: string) => processResponse(raw, nextAll, abort.signal))
516
541
  .catch(handleError(nextAll));
517
542
  };
518
543
 
519
544
  useInput((input, key) => {
520
545
  if (showTimeline) return;
521
546
 
547
+ // ESC while thinking → abort the in-flight request and go idle
548
+ if (stage.type === "thinking" && key.escape) {
549
+ abortControllerRef.current?.abort();
550
+ abortControllerRef.current = null;
551
+ setStage({ type: "idle" });
552
+ return;
553
+ }
554
+
522
555
  if (stage.type === "idle") {
523
556
  if (key.ctrl && input === "c") {
524
557
  process.exit(0);
@@ -844,6 +877,9 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
844
877
  <Box gap={1}>
845
878
  <Text color={ACCENT}>●</Text>
846
879
  <TypewriterText text={thinkingPhrase} />
880
+ <Text color="gray" dimColor>
881
+ · esc cancel
882
+ </Text>
847
883
  </Box>
848
884
  )}
849
885
 
package/src/utils/chat.ts CHANGED
@@ -69,10 +69,10 @@ You have exactly eleven tools. To use a tool you MUST wrap it in the exact XML t
69
69
  ### 11. search — search the internet for anything you are unsure about
70
70
  <search>how to use React useEffect cleanup function</search>
71
71
 
72
- ### 11. clone — clone a GitHub repo so you can explore and discuss it
72
+ ### 12. clone — clone a GitHub repo so you can explore and discuss it
73
73
  <clone>https://github.com/owner/repo</clone>
74
74
 
75
- ### 12. changes — propose code edits (shown as a diff for user approval)
75
+ ### 13. changes — propose code edits (shown as a diff for user approval)
76
76
  <changes>
77
77
  {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
78
78
  </changes>
@@ -91,16 +91,20 @@ You have exactly eleven tools. To use a tool you MUST wrap it in the exact XML t
91
91
  10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
92
92
  11. write-file content field must be the COMPLETE file content, never empty or placeholder
93
93
  12. After a write-file succeeds, do NOT repeat it — trust the result and move on
94
- 13. After a write-file succeeds, use read-file to verify the content before telling the user it is done
94
+ 13. After a write-file succeeds, tell the user it is done immediately do NOT auto-read the file back to verify
95
95
  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
96
96
  15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
97
97
  16. NEVER second-guess yourself mid-response — commit to your answer
98
98
  17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
99
99
  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
100
- 17. 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\`
101
- 18. 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\`
102
- 19. 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\`
103
- 20. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
100
+ 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\`
101
+ 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\`
102
+ 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\`
103
+ 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
104
+ 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
105
+ 24. NEVER chain tool calls unless the user's request explicitly requires multiple steps
106
+ 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
107
+ 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.
104
108
 
105
109
  ## CRITICAL: READ BEFORE YOU WRITE
106
110
 
@@ -179,9 +183,8 @@ ${historySummary}`;
179
183
  }
180
184
 
181
185
  // ── Few-shot examples ─────────────────────────────────────────────────────────
182
-
183
186
  export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
184
- // read-folder examples FIRST highest priority pattern to establish
187
+ // ── delete / open / pdf ───────────────────────────────────────────────────
185
188
  {
186
189
  role: "user",
187
190
  content: "delete src/old-component.tsx",
@@ -253,6 +256,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
253
256
  role: "assistant",
254
257
  content: "Done — the PDF report has been saved to `docs/report.pdf`.",
255
258
  },
259
+
260
+ // ── grep ──────────────────────────────────────────────────────────────────
256
261
  {
257
262
  role: "user",
258
263
  content: 'grep -R "ChatRunner" -n src',
@@ -289,6 +294,28 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
289
294
  content:
290
295
  "`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210.",
291
296
  },
297
+
298
+ // ── showing tool usage as an example (bracket notation, NOT real tags) ────
299
+ {
300
+ role: "user",
301
+ content: "show me how to use the read-file tool",
302
+ },
303
+ {
304
+ role: "assistant",
305
+ content:
306
+ "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.",
307
+ },
308
+ {
309
+ role: "user",
310
+ content: "show me an example of the shell tool",
311
+ },
312
+ {
313
+ role: "assistant",
314
+ content:
315
+ "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.",
316
+ },
317
+
318
+ // ── read-folder ───────────────────────────────────────────────────────────
292
319
  {
293
320
  role: "user",
294
321
  content: "read src folder",
@@ -415,6 +442,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
415
442
  content:
416
443
  "The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`.",
417
444
  },
445
+
446
+ // ── fetch → write → done (no auto-verify) ────────────────────────────────
418
447
  {
419
448
  role: "user",
420
449
  content: "fetch https://api.github.com/repos/microsoft/typescript",
@@ -449,17 +478,30 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
449
478
  },
450
479
  {
451
480
  role: "assistant",
452
- content: "<read-file>ts-info.json</read-file>",
481
+ content: "Done — `ts-info.json` has been saved.",
482
+ },
483
+
484
+ // ── write it to a file → done, nothing else ───────────────────────────────
485
+ {
486
+ role: "user",
487
+ content: "write it to a file called hello.py",
488
+ },
489
+ {
490
+ role: "assistant",
491
+ content:
492
+ '<write-file>\n{"path": "hello.py", "content": "# hello.py\\nprint(\'hello\')"}\n</write-file>',
453
493
  },
454
494
  {
455
495
  role: "user",
456
496
  content:
457
- 'Here is the output from read-file of ts-info.json:\n\nFile: ts-info.json (1 lines)\n\n{"name":"TypeScript","stars":100000}\n\nPlease continue your response based on this output.',
497
+ "Here is the output from write-file to hello.py:\n\nWritten: /repo/hello.py (2 lines, 32 bytes)\n\nPlease continue your response based on this output.",
458
498
  },
459
499
  {
460
500
  role: "assistant",
461
- content: "Done — saved and verified `ts-info.json`. Data looks correct.",
501
+ content: "Done — `hello.py` has been written.",
462
502
  },
503
+
504
+ // ── read before write ─────────────────────────────────────────────────────
463
505
  {
464
506
  role: "user",
465
507
  content: "add a logout button to src/components/Header.tsx",
@@ -478,6 +520,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
478
520
  content:
479
521
  '<changes>\n{"summary": "Add logout button to Header — preserves all existing nav items and imports", "patches": [{"path": "src/components/Header.tsx", "content": "// complete file with logout button added", "isNew": false}]}\n</changes>',
480
522
  },
523
+
524
+ // ── shell ─────────────────────────────────────────────────────────────────
481
525
  {
482
526
  role: "user",
483
527
  content: "what node version am I on",
@@ -495,6 +539,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
495
539
  role: "assistant",
496
540
  content: "You're running Node.js v20.11.0.",
497
541
  },
542
+
543
+ // ── clone ─────────────────────────────────────────────────────────────────
498
544
  {
499
545
  role: "user",
500
546
  content: "clone https://github.com/facebook/react",
@@ -513,6 +559,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
513
559
  content:
514
560
  "Cloned! The React repo has 2847 files. I can read source files, explain how it works, or suggest improvements — just ask.",
515
561
  },
562
+
563
+ // ── search ────────────────────────────────────────────────────────────────
516
564
  {
517
565
  role: "user",
518
566
  content: "what does the ?? operator do in typescript",
@@ -531,8 +579,16 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
531
579
  content:
532
580
  "The `??` operator is the nullish coalescing operator. It returns the right side only when the left side is `null` or `undefined`.",
533
581
  },
582
+ {
583
+ role: "user",
584
+ content: "what does this project do?",
585
+ },
586
+ {
587
+ role: "assistant",
588
+ content:
589
+ "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.",
590
+ },
534
591
  ];
535
-
536
592
  // ── Response parser ───────────────────────────────────────────────────────────
537
593
 
538
594
  export type ParsedResponse =
@@ -562,6 +618,10 @@ export type ParsedResponse =
562
618
  | { kind: "clone"; content: string; repoUrl: string };
563
619
 
564
620
  export function parseResponse(text: string): ParsedResponse {
621
+ // Strip fenced code blocks before scanning for tool tags so that tags shown
622
+ // as examples inside ``` ... ``` blocks are never executed.
623
+ const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
624
+
565
625
  type Candidate = {
566
626
  index: number;
567
627
  kind:
@@ -610,15 +670,32 @@ export function parseResponse(text: string): ParsedResponse {
610
670
 
611
671
  for (const { kind, re } of patterns) {
612
672
  re.lastIndex = 0;
613
- const m = re.exec(text);
614
- if (m) candidates.push({ index: m.index, kind, match: m });
673
+ // Scan against the code-block-stripped text so tags inside ``` are ignored
674
+ const m = re.exec(scanText);
675
+ if (m) {
676
+ // Re-extract the match body from the original text at the same position
677
+ const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
678
+ const originalMatch = originalRe.exec(text.slice(m.index));
679
+ if (originalMatch) {
680
+ // Reconstruct a match-like object with the correct index
681
+ const fakeMatch = Object.assign(
682
+ [
683
+ text.slice(m.index, m.index + originalMatch[0].length),
684
+ originalMatch[1],
685
+ ] as unknown as RegExpExecArray,
686
+ { index: m.index, input: text, groups: undefined },
687
+ );
688
+ candidates.push({ index: m.index, kind, match: fakeMatch });
689
+ }
690
+ }
615
691
  }
616
692
 
617
693
  if (candidates.length === 0) return { kind: "text", content: text.trim() };
618
694
 
619
695
  candidates.sort((a, b) => a.index - b.index);
620
696
  const { kind, match } = candidates[0]!;
621
- // Strip any leaked tool tags from preamble (e.g. model emits tag twice or mid-sentence)
697
+
698
+ // Strip any leaked tool tags from preamble text
622
699
  const before = text
623
700
  .slice(0, match.index)
624
701
  .replace(
@@ -626,7 +703,7 @@ export function parseResponse(text: string): ParsedResponse {
626
703
  "",
627
704
  )
628
705
  .trim();
629
- const body = match[1]!.trim();
706
+ const body = (match[1] ?? "").trim();
630
707
 
631
708
  if (kind === "changes") {
632
709
  try {
@@ -690,7 +767,6 @@ export function parseResponse(text: string): ParsedResponse {
690
767
  glob: parsed.glob ?? "**/*",
691
768
  };
692
769
  } catch {
693
- // treat body as plain pattern with no glob
694
770
  return { kind: "grep", content: before, pattern: body, glob: "**/*" };
695
771
  }
696
772
  }
@@ -788,6 +864,7 @@ export async function callChat(
788
864
  provider: Provider,
789
865
  systemPrompt: string,
790
866
  messages: Message[],
867
+ abortSignal?: AbortSignal,
791
868
  ): Promise<string> {
792
869
  const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
793
870
 
@@ -825,6 +902,8 @@ export async function callChat(
825
902
  const controller = new AbortController();
826
903
  const timer = setTimeout(() => controller.abort(), 60_000);
827
904
 
905
+ abortSignal?.addEventListener("abort", () => controller.abort());
906
+
828
907
  const res = await fetch(url, {
829
908
  method: "POST",
830
909
  headers,
@@ -1251,7 +1330,7 @@ export function readFolder(folderPath: string, repoPath: string): string {
1251
1330
  const subfolders: string[] = [];
1252
1331
 
1253
1332
  for (const entry of entries) {
1254
- if (entry.startsWith(".") && entry !== ".env") continue; // skip hidden except .env hint
1333
+ if (entry.startsWith(".") && entry !== ".env") continue;
1255
1334
  const full = path.join(candidate, entry);
1256
1335
  try {
1257
1336
  if (statSync(full).isDirectory()) {
@@ -1297,29 +1376,22 @@ export function grepFiles(
1297
1376
  try {
1298
1377
  regex = new RegExp(pattern, "i");
1299
1378
  } catch {
1300
- // fall back to literal string match if pattern is not valid regex
1301
1379
  regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
1302
1380
  }
1303
1381
 
1304
- // Convert glob to a simple path prefix/suffix filter
1305
- // Supports patterns like: src/**/*.tsx, **/*.ts, src/utils/*
1306
1382
  const globToFilter = (g: string): ((rel: string) => boolean) => {
1307
- // strip leading **/
1308
1383
  const cleaned = g.replace(/^\*\*\//, "");
1309
1384
  const parts = cleaned.split("/");
1310
1385
  const ext = parts[parts.length - 1];
1311
1386
  const prefix = parts.slice(0, -1).join("/");
1312
1387
 
1313
1388
  return (rel: string) => {
1314
- // extension match (e.g. *.tsx)
1315
1389
  if (ext?.startsWith("*.")) {
1316
- const extSuffix = ext.slice(1); // e.g. .tsx
1390
+ const extSuffix = ext.slice(1);
1317
1391
  if (!rel.endsWith(extSuffix)) return false;
1318
1392
  } else if (ext && !ext.includes("*")) {
1319
- // exact filename
1320
1393
  if (!rel.endsWith(ext)) return false;
1321
1394
  }
1322
- // prefix match
1323
1395
  if (prefix && !prefix.includes("*")) {
1324
1396
  if (!rel.startsWith(prefix)) return false;
1325
1397
  }
@@ -1452,7 +1524,6 @@ export function generatePdf(
1452
1524
  const dir = path.dirname(fullPath);
1453
1525
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1454
1526
 
1455
- // Escape content for embedding in a Python string literal
1456
1527
  const escaped = content
1457
1528
  .replace(/\\/g, "\\\\")
1458
1529
  .replace(/"""/g, '\\"\\"\\"')
@@ -1512,7 +1583,6 @@ for line in raw.split("\\n"):
1512
1583
  elif s == "":
1513
1584
  story.append(Spacer(1, 6))
1514
1585
  else:
1515
- # handle **bold** inline
1516
1586
  import re
1517
1587
  s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
1518
1588
  s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
package/skills.json DELETED
@@ -1,7 +0,0 @@
1
- [
2
- {
3
- "id": "1773310757090-o6sri",
4
- "name": "this is a skill",
5
- "description": "this is a skill"
6
- }
7
- ]