@ridit/lens 0.1.7 → 0.1.8
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 +1073 -103
- package/hello.py +51 -0
- package/package.json +3 -3
- package/src/colors.ts +9 -0
- package/src/components/chat/ChatMessage.tsx +567 -66
- package/src/components/chat/ChatRunner.tsx +44 -8
- package/src/utils/chat.ts +89 -29
- package/skills.json +0 -7
|
@@ -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,19 @@ 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
|
|
104
107
|
|
|
105
108
|
## CRITICAL: READ BEFORE YOU WRITE
|
|
106
109
|
|
|
@@ -179,9 +182,8 @@ ${historySummary}`;
|
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
// ── Few-shot examples ─────────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
185
|
export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
184
|
-
//
|
|
186
|
+
// ── delete / open / pdf ───────────────────────────────────────────────────
|
|
185
187
|
{
|
|
186
188
|
role: "user",
|
|
187
189
|
content: "delete src/old-component.tsx",
|
|
@@ -253,6 +255,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
253
255
|
role: "assistant",
|
|
254
256
|
content: "Done — the PDF report has been saved to `docs/report.pdf`.",
|
|
255
257
|
},
|
|
258
|
+
|
|
259
|
+
// ── grep ──────────────────────────────────────────────────────────────────
|
|
256
260
|
{
|
|
257
261
|
role: "user",
|
|
258
262
|
content: 'grep -R "ChatRunner" -n src',
|
|
@@ -289,6 +293,28 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
289
293
|
content:
|
|
290
294
|
"`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210.",
|
|
291
295
|
},
|
|
296
|
+
|
|
297
|
+
// ── showing tool usage as an example (bracket notation, NOT real tags) ────
|
|
298
|
+
{
|
|
299
|
+
role: "user",
|
|
300
|
+
content: "show me how to use the read-file tool",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
role: "assistant",
|
|
304
|
+
content:
|
|
305
|
+
"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.",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
role: "user",
|
|
309
|
+
content: "show me an example of the shell tool",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
role: "assistant",
|
|
313
|
+
content:
|
|
314
|
+
"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.",
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// ── read-folder ───────────────────────────────────────────────────────────
|
|
292
318
|
{
|
|
293
319
|
role: "user",
|
|
294
320
|
content: "read src folder",
|
|
@@ -415,6 +441,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
415
441
|
content:
|
|
416
442
|
"The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`.",
|
|
417
443
|
},
|
|
444
|
+
|
|
445
|
+
// ── fetch → write → done (no auto-verify) ────────────────────────────────
|
|
418
446
|
{
|
|
419
447
|
role: "user",
|
|
420
448
|
content: "fetch https://api.github.com/repos/microsoft/typescript",
|
|
@@ -449,17 +477,30 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
449
477
|
},
|
|
450
478
|
{
|
|
451
479
|
role: "assistant",
|
|
452
|
-
content: "
|
|
480
|
+
content: "Done — `ts-info.json` has been saved.",
|
|
453
481
|
},
|
|
482
|
+
|
|
483
|
+
// ── write it to a file → done, nothing else ───────────────────────────────
|
|
454
484
|
{
|
|
455
485
|
role: "user",
|
|
486
|
+
content: "write it to a file called hello.py",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
role: "assistant",
|
|
456
490
|
content:
|
|
457
|
-
'
|
|
491
|
+
'<write-file>\n{"path": "hello.py", "content": "# hello.py\\nprint(\'hello\')"}\n</write-file>',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
role: "user",
|
|
495
|
+
content:
|
|
496
|
+
"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
497
|
},
|
|
459
498
|
{
|
|
460
499
|
role: "assistant",
|
|
461
|
-
content: "Done —
|
|
500
|
+
content: "Done — `hello.py` has been written.",
|
|
462
501
|
},
|
|
502
|
+
|
|
503
|
+
// ── read before write ─────────────────────────────────────────────────────
|
|
463
504
|
{
|
|
464
505
|
role: "user",
|
|
465
506
|
content: "add a logout button to src/components/Header.tsx",
|
|
@@ -478,6 +519,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
478
519
|
content:
|
|
479
520
|
'<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
521
|
},
|
|
522
|
+
|
|
523
|
+
// ── shell ─────────────────────────────────────────────────────────────────
|
|
481
524
|
{
|
|
482
525
|
role: "user",
|
|
483
526
|
content: "what node version am I on",
|
|
@@ -495,6 +538,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
495
538
|
role: "assistant",
|
|
496
539
|
content: "You're running Node.js v20.11.0.",
|
|
497
540
|
},
|
|
541
|
+
|
|
542
|
+
// ── clone ─────────────────────────────────────────────────────────────────
|
|
498
543
|
{
|
|
499
544
|
role: "user",
|
|
500
545
|
content: "clone https://github.com/facebook/react",
|
|
@@ -513,6 +558,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
513
558
|
content:
|
|
514
559
|
"Cloned! The React repo has 2847 files. I can read source files, explain how it works, or suggest improvements — just ask.",
|
|
515
560
|
},
|
|
561
|
+
|
|
562
|
+
// ── search ────────────────────────────────────────────────────────────────
|
|
516
563
|
{
|
|
517
564
|
role: "user",
|
|
518
565
|
content: "what does the ?? operator do in typescript",
|
|
@@ -532,7 +579,6 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
532
579
|
"The `??` operator is the nullish coalescing operator. It returns the right side only when the left side is `null` or `undefined`.",
|
|
533
580
|
},
|
|
534
581
|
];
|
|
535
|
-
|
|
536
582
|
// ── Response parser ───────────────────────────────────────────────────────────
|
|
537
583
|
|
|
538
584
|
export type ParsedResponse =
|
|
@@ -562,6 +608,10 @@ export type ParsedResponse =
|
|
|
562
608
|
| { kind: "clone"; content: string; repoUrl: string };
|
|
563
609
|
|
|
564
610
|
export function parseResponse(text: string): ParsedResponse {
|
|
611
|
+
// Strip fenced code blocks before scanning for tool tags so that tags shown
|
|
612
|
+
// as examples inside ``` ... ``` blocks are never executed.
|
|
613
|
+
const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
|
|
614
|
+
|
|
565
615
|
type Candidate = {
|
|
566
616
|
index: number;
|
|
567
617
|
kind:
|
|
@@ -610,15 +660,32 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
610
660
|
|
|
611
661
|
for (const { kind, re } of patterns) {
|
|
612
662
|
re.lastIndex = 0;
|
|
613
|
-
|
|
614
|
-
|
|
663
|
+
// Scan against the code-block-stripped text so tags inside ``` are ignored
|
|
664
|
+
const m = re.exec(scanText);
|
|
665
|
+
if (m) {
|
|
666
|
+
// Re-extract the match body from the original text at the same position
|
|
667
|
+
const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
|
|
668
|
+
const originalMatch = originalRe.exec(text.slice(m.index));
|
|
669
|
+
if (originalMatch) {
|
|
670
|
+
// Reconstruct a match-like object with the correct index
|
|
671
|
+
const fakeMatch = Object.assign(
|
|
672
|
+
[
|
|
673
|
+
text.slice(m.index, m.index + originalMatch[0].length),
|
|
674
|
+
originalMatch[1],
|
|
675
|
+
] as unknown as RegExpExecArray,
|
|
676
|
+
{ index: m.index, input: text, groups: undefined },
|
|
677
|
+
);
|
|
678
|
+
candidates.push({ index: m.index, kind, match: fakeMatch });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
615
681
|
}
|
|
616
682
|
|
|
617
683
|
if (candidates.length === 0) return { kind: "text", content: text.trim() };
|
|
618
684
|
|
|
619
685
|
candidates.sort((a, b) => a.index - b.index);
|
|
620
686
|
const { kind, match } = candidates[0]!;
|
|
621
|
-
|
|
687
|
+
|
|
688
|
+
// Strip any leaked tool tags from preamble text
|
|
622
689
|
const before = text
|
|
623
690
|
.slice(0, match.index)
|
|
624
691
|
.replace(
|
|
@@ -626,7 +693,7 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
626
693
|
"",
|
|
627
694
|
)
|
|
628
695
|
.trim();
|
|
629
|
-
const body = match[1]
|
|
696
|
+
const body = (match[1] ?? "").trim();
|
|
630
697
|
|
|
631
698
|
if (kind === "changes") {
|
|
632
699
|
try {
|
|
@@ -690,7 +757,6 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
690
757
|
glob: parsed.glob ?? "**/*",
|
|
691
758
|
};
|
|
692
759
|
} catch {
|
|
693
|
-
// treat body as plain pattern with no glob
|
|
694
760
|
return { kind: "grep", content: before, pattern: body, glob: "**/*" };
|
|
695
761
|
}
|
|
696
762
|
}
|
|
@@ -788,6 +854,7 @@ export async function callChat(
|
|
|
788
854
|
provider: Provider,
|
|
789
855
|
systemPrompt: string,
|
|
790
856
|
messages: Message[],
|
|
857
|
+
abortSignal?: AbortSignal,
|
|
791
858
|
): Promise<string> {
|
|
792
859
|
const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
|
|
793
860
|
|
|
@@ -825,6 +892,8 @@ export async function callChat(
|
|
|
825
892
|
const controller = new AbortController();
|
|
826
893
|
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
827
894
|
|
|
895
|
+
abortSignal?.addEventListener("abort", () => controller.abort());
|
|
896
|
+
|
|
828
897
|
const res = await fetch(url, {
|
|
829
898
|
method: "POST",
|
|
830
899
|
headers,
|
|
@@ -1251,7 +1320,7 @@ export function readFolder(folderPath: string, repoPath: string): string {
|
|
|
1251
1320
|
const subfolders: string[] = [];
|
|
1252
1321
|
|
|
1253
1322
|
for (const entry of entries) {
|
|
1254
|
-
if (entry.startsWith(".") && entry !== ".env") continue;
|
|
1323
|
+
if (entry.startsWith(".") && entry !== ".env") continue;
|
|
1255
1324
|
const full = path.join(candidate, entry);
|
|
1256
1325
|
try {
|
|
1257
1326
|
if (statSync(full).isDirectory()) {
|
|
@@ -1297,29 +1366,22 @@ export function grepFiles(
|
|
|
1297
1366
|
try {
|
|
1298
1367
|
regex = new RegExp(pattern, "i");
|
|
1299
1368
|
} catch {
|
|
1300
|
-
// fall back to literal string match if pattern is not valid regex
|
|
1301
1369
|
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
1302
1370
|
}
|
|
1303
1371
|
|
|
1304
|
-
// Convert glob to a simple path prefix/suffix filter
|
|
1305
|
-
// Supports patterns like: src/**/*.tsx, **/*.ts, src/utils/*
|
|
1306
1372
|
const globToFilter = (g: string): ((rel: string) => boolean) => {
|
|
1307
|
-
// strip leading **/
|
|
1308
1373
|
const cleaned = g.replace(/^\*\*\//, "");
|
|
1309
1374
|
const parts = cleaned.split("/");
|
|
1310
1375
|
const ext = parts[parts.length - 1];
|
|
1311
1376
|
const prefix = parts.slice(0, -1).join("/");
|
|
1312
1377
|
|
|
1313
1378
|
return (rel: string) => {
|
|
1314
|
-
// extension match (e.g. *.tsx)
|
|
1315
1379
|
if (ext?.startsWith("*.")) {
|
|
1316
|
-
const extSuffix = ext.slice(1);
|
|
1380
|
+
const extSuffix = ext.slice(1);
|
|
1317
1381
|
if (!rel.endsWith(extSuffix)) return false;
|
|
1318
1382
|
} else if (ext && !ext.includes("*")) {
|
|
1319
|
-
// exact filename
|
|
1320
1383
|
if (!rel.endsWith(ext)) return false;
|
|
1321
1384
|
}
|
|
1322
|
-
// prefix match
|
|
1323
1385
|
if (prefix && !prefix.includes("*")) {
|
|
1324
1386
|
if (!rel.startsWith(prefix)) return false;
|
|
1325
1387
|
}
|
|
@@ -1452,7 +1514,6 @@ export function generatePdf(
|
|
|
1452
1514
|
const dir = path.dirname(fullPath);
|
|
1453
1515
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1454
1516
|
|
|
1455
|
-
// Escape content for embedding in a Python string literal
|
|
1456
1517
|
const escaped = content
|
|
1457
1518
|
.replace(/\\/g, "\\\\")
|
|
1458
1519
|
.replace(/"""/g, '\\"\\"\\"')
|
|
@@ -1512,7 +1573,6 @@ for line in raw.split("\\n"):
|
|
|
1512
1573
|
elif s == "":
|
|
1513
1574
|
story.append(Spacer(1, 6))
|
|
1514
1575
|
else:
|
|
1515
|
-
# handle **bold** inline
|
|
1516
1576
|
import re
|
|
1517
1577
|
s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
|
|
1518
1578
|
s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
|