@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 +103 -44
- package/hello.py +51 -0
- package/package.json +3 -3
- package/src/colors.ts +9 -0
- package/src/components/chat/ChatMessage.tsx +8 -9
- package/src/components/chat/ChatRunner.tsx +44 -8
- package/src/utils/chat.ts +99 -29
- package/skills.json +0 -7
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
|
-
###
|
|
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
|
-
###
|
|
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,
|
|
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
|
-
|
|
48739
|
-
|
|
48740
|
-
|
|
48741
|
-
|
|
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: "
|
|
49210
|
+
content: "Done — `ts-info.json` has been saved."
|
|
49191
49211
|
},
|
|
49192
49212
|
{
|
|
49193
49213
|
role: "user",
|
|
49194
|
-
content:
|
|
49195
|
-
|
|
49196
|
-
|
|
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
|
-
|
|
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 —
|
|
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(
|
|
49318
|
-
if (m)
|
|
49319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50076
|
-
|
|
50077
|
-
|
|
50078
|
-
|
|
50079
|
-
|
|
50080
|
-
|
|
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.
|
|
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"
|
|
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
|
|
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 = (
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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,
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
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: "
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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]
|
|
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;
|
|
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);
|
|
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)
|