Repository navigation
fix(templates): make the ai-agent starter pass its own eval #3391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
39e879a
fix(templates): render maths in the chat starters
kojiwakayama 0ddfd5b
fix(templates): carry maths in a code span, not dollar delimiters
kojiwakayama bfa2184
fix(templates): tell the assistant to write arithmetic in plain text
kojiwakayama 388438a
fix(templates): keep the calculator instruction from contradicting it…
kojiwakayama aa58d48
fix(templates): offer the calculator to the ai-agent starter
kojiwakayama File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| /** | ||
| * Normalise LaTeX delimiters before Markdown parsing. | ||
| * | ||
| * Two problems make this a pre-parse pass rather than a plugin. | ||
| * | ||
| * First, `\(` and `\[` cannot survive the Markdown parser. CommonMark treats a | ||
| * backslash before ASCII punctuation as a character escape, so `\(x\)` reaches | ||
| * the syntax tree as plain `(x)`. By the time any remark or rehype plugin runs, | ||
| * the delimiters are already gone, and a KaTeX DOM pass has nothing to match. | ||
| * | ||
| * Second, `$` cannot be trusted as a delimiter in this content. Assistant | ||
| * answers are full of currency: "18% tip on $84.50 ... totals $99.71". Enabling | ||
| * single dollar math against that text turns the prose between two amounts into | ||
| * a formula. So every dollar sign that the author wrote is escaped first, and | ||
| * the only unescaped dollars left in the document are the delimiters introduced | ||
| * here. That is what makes `singleDollarTextMath` safe downstream. | ||
| * | ||
| * Code is never rewritten. Fenced blocks and inline spans are copied through | ||
| * untouched, so a snippet containing `$` or `\(` keeps its exact source. | ||
| */ | ||
|
|
||
| /** A half-open source range that must be copied through verbatim. */ | ||
| interface CodeSpan { | ||
| readonly start: number; | ||
| readonly end: number; | ||
| } | ||
|
|
||
| const FENCE_RE = /^[ \t]*(`{3,}|~{3,})/; | ||
|
|
||
| /** | ||
| * Locate fenced code blocks. | ||
| * | ||
| * A fence closes on the first later fence of the same character that is at | ||
| * least as long as the opener. An unterminated fence runs to end of input, | ||
| * matching how CommonMark treats it, so trailing text is not rewritten as if it | ||
| * were prose. | ||
| */ | ||
| function findFencedSpans(source: string): CodeSpan[] { | ||
| const spans: CodeSpan[] = []; | ||
| const lines = source.split("\n"); | ||
| let offset = 0; | ||
| let open: { marker: string; start: number } | undefined; | ||
|
|
||
| for (const line of lines) { | ||
| const match = FENCE_RE.exec(line); | ||
| if (open === undefined) { | ||
| if (match?.[1]) open = { marker: match[1], start: offset }; | ||
| } else if ( | ||
| match?.[1] && | ||
| match[1][0] === open.marker[0] && | ||
| match[1].length >= open.marker.length | ||
| ) { | ||
| spans.push({ start: open.start, end: offset + line.length }); | ||
| open = undefined; | ||
| } | ||
| offset += line.length + 1; | ||
| } | ||
|
|
||
| if (open !== undefined) spans.push({ start: open.start, end: source.length }); | ||
| return spans; | ||
| } | ||
|
|
||
| /** Locate inline code spans, skipping any that fall inside a fenced block. */ | ||
| function findInlineSpans(source: string, fenced: readonly CodeSpan[]): CodeSpan[] { | ||
| const spans: CodeSpan[] = []; | ||
| const inFence = (index: number) => fenced.some((s) => index >= s.start && index < s.end); | ||
|
|
||
| let index = 0; | ||
| while (index < source.length) { | ||
| if (source[index] !== "`" || inFence(index)) { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| let run = 0; | ||
| while (source[index + run] === "`") run += 1; | ||
| const marker = "`".repeat(run); | ||
| const close = source.indexOf(marker, index + run); | ||
| // An unclosed run is literal text, not a span, so leave it to be rewritten. | ||
| if (close === -1) { | ||
| index += run; | ||
| continue; | ||
| } | ||
| spans.push({ start: index, end: close + run }); | ||
| index = close + run; | ||
| } | ||
|
|
||
| return spans; | ||
| } | ||
|
|
||
| const MATH_RE = /\\\[([\s\S]*?)\\\]|\\\(([\s\S]*?)\\\)/g; | ||
|
|
||
| /** Outside maths a dollar is currency, so hide it from the math tokenizer. */ | ||
| function escapeProseDollars(text: string): string { | ||
| return text.replace(/\$/g, "\\$"); | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * Inside maths a dollar has to stop being a dollar character entirely. | ||
| * | ||
| * `remark-math` closes an inline expression at the first `$` it meets and does | ||
| * not honour a backslash escape, so `\textbf{ \$99.71 }` silently fails to | ||
| * render rather than producing a dollar sign. `\textdollar` is the TeX spelling | ||
| * that survives, and the braces keep it from swallowing a following digit. | ||
| */ | ||
| function convertMathDollars(body: string): string { | ||
| return body.replace(/\$/g, "{\\textdollar}"); | ||
| } | ||
|
|
||
| /** Promote LaTeX delimiters to remark-math syntax, per region. */ | ||
| function convertProse(text: string): string { | ||
| let result = ""; | ||
| let cursor = 0; | ||
|
|
||
| for (const match of text.matchAll(MATH_RE)) { | ||
| const index = match.index ?? 0; | ||
| result += escapeProseDollars(text.slice(cursor, index)); | ||
| const display = match[1] !== undefined; | ||
| const body = convertMathDollars(display ? match[1] : (match[2] ?? "")); | ||
| result += display ? `$$${body}$$` : `$${body}$`; | ||
| cursor = index + match[0].length; | ||
| } | ||
|
|
||
| result += escapeProseDollars(text.slice(cursor)); | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Rewrite `\(inline\)` and `\[display\]` into `$inline$` and `$$display$$`, | ||
| * leaving every code region and every author-written `$` untouched. | ||
| */ | ||
| export function normalizeLatexDelimiters(source: string): string { | ||
| const fenced = findFencedSpans(source); | ||
| const protectedSpans = [...fenced, ...findInlineSpans(source, fenced)] | ||
| .sort((a, b) => a.start - b.start); | ||
|
|
||
| let result = ""; | ||
| let cursor = 0; | ||
| for (const span of protectedSpans) { | ||
| if (span.start < cursor) continue; | ||
| result += convertProse(source.slice(cursor, span.start)); | ||
| result += source.slice(span.start, span.end); | ||
| cursor = span.end; | ||
| } | ||
| result += convertProse(source.slice(cursor)); | ||
| return result; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
cli/templates/files/coding-agent/app/latex-delimiters.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| /** | ||
| * Normalise LaTeX delimiters before Markdown parsing. | ||
| * | ||
| * Two problems make this a pre-parse pass rather than a plugin. | ||
| * | ||
| * First, `\(` and `\[` cannot survive the Markdown parser. CommonMark treats a | ||
| * backslash before ASCII punctuation as a character escape, so `\(x\)` reaches | ||
| * the syntax tree as plain `(x)`. By the time any remark or rehype plugin runs, | ||
| * the delimiters are already gone, and a KaTeX DOM pass has nothing to match. | ||
| * | ||
| * Second, `$` cannot be trusted as a delimiter in this content. Assistant | ||
| * answers are full of currency: "18% tip on $84.50 ... totals $99.71". Enabling | ||
| * single dollar math against that text turns the prose between two amounts into | ||
| * a formula. So every dollar sign that the author wrote is escaped first, and | ||
| * the only unescaped dollars left in the document are the delimiters introduced | ||
| * here. That is what makes `singleDollarTextMath` safe downstream. | ||
| * | ||
| * Code is never rewritten. Fenced blocks and inline spans are copied through | ||
| * untouched, so a snippet containing `$` or `\(` keeps its exact source. | ||
| */ | ||
|
|
||
| /** A half-open source range that must be copied through verbatim. */ | ||
| interface CodeSpan { | ||
| readonly start: number; | ||
| readonly end: number; | ||
| } | ||
|
|
||
| const FENCE_RE = /^[ \t]*(`{3,}|~{3,})/; | ||
|
|
||
| /** | ||
| * Locate fenced code blocks. | ||
| * | ||
| * A fence closes on the first later fence of the same character that is at | ||
| * least as long as the opener. An unterminated fence runs to end of input, | ||
| * matching how CommonMark treats it, so trailing text is not rewritten as if it | ||
| * were prose. | ||
| */ | ||
| function findFencedSpans(source: string): CodeSpan[] { | ||
| const spans: CodeSpan[] = []; | ||
| const lines = source.split("\n"); | ||
| let offset = 0; | ||
| let open: { marker: string; start: number } | undefined; | ||
|
|
||
| for (const line of lines) { | ||
| const match = FENCE_RE.exec(line); | ||
| if (open === undefined) { | ||
| if (match?.[1]) open = { marker: match[1], start: offset }; | ||
| } else if ( | ||
| match?.[1] && | ||
| match[1][0] === open.marker[0] && | ||
| match[1].length >= open.marker.length | ||
| ) { | ||
| spans.push({ start: open.start, end: offset + line.length }); | ||
| open = undefined; | ||
| } | ||
| offset += line.length + 1; | ||
| } | ||
|
|
||
| if (open !== undefined) spans.push({ start: open.start, end: source.length }); | ||
| return spans; | ||
| } | ||
|
|
||
| /** Locate inline code spans, skipping any that fall inside a fenced block. */ | ||
| function findInlineSpans(source: string, fenced: readonly CodeSpan[]): CodeSpan[] { | ||
| const spans: CodeSpan[] = []; | ||
| const inFence = (index: number) => fenced.some((s) => index >= s.start && index < s.end); | ||
|
|
||
| let index = 0; | ||
| while (index < source.length) { | ||
| if (source[index] !== "`" || inFence(index)) { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| let run = 0; | ||
| while (source[index + run] === "`") run += 1; | ||
| const marker = "`".repeat(run); | ||
| const close = source.indexOf(marker, index + run); | ||
| // An unclosed run is literal text, not a span, so leave it to be rewritten. | ||
| if (close === -1) { | ||
| index += run; | ||
| continue; | ||
| } | ||
| spans.push({ start: index, end: close + run }); | ||
| index = close + run; | ||
| } | ||
|
|
||
| return spans; | ||
| } | ||
|
|
||
| const MATH_RE = /\\\[([\s\S]*?)\\\]|\\\(([\s\S]*?)\\\)/g; | ||
|
|
||
| /** Outside maths a dollar is currency, so hide it from the math tokenizer. */ | ||
| function escapeProseDollars(text: string): string { | ||
| return text.replace(/\$/g, "\\$"); | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| } | ||
|
|
||
| /** | ||
| * Inside maths a dollar has to stop being a dollar character entirely. | ||
| * | ||
| * `remark-math` closes an inline expression at the first `$` it meets and does | ||
| * not honour a backslash escape, so `\textbf{ \$99.71 }` silently fails to | ||
| * render rather than producing a dollar sign. `\textdollar` is the TeX spelling | ||
| * that survives, and the braces keep it from swallowing a following digit. | ||
| */ | ||
| function convertMathDollars(body: string): string { | ||
| return body.replace(/\$/g, "{\\textdollar}"); | ||
| } | ||
|
|
||
| /** Promote LaTeX delimiters to remark-math syntax, per region. */ | ||
| function convertProse(text: string): string { | ||
| let result = ""; | ||
| let cursor = 0; | ||
|
|
||
| for (const match of text.matchAll(MATH_RE)) { | ||
| const index = match.index ?? 0; | ||
| result += escapeProseDollars(text.slice(cursor, index)); | ||
| const display = match[1] !== undefined; | ||
| const body = convertMathDollars(display ? match[1] : (match[2] ?? "")); | ||
| result += display ? `$$${body}$$` : `$${body}$`; | ||
| cursor = index + match[0].length; | ||
| } | ||
|
|
||
| result += escapeProseDollars(text.slice(cursor)); | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Rewrite `\(inline\)` and `\[display\]` into `$inline$` and `$$display$$`, | ||
| * leaving every code region and every author-written `$` untouched. | ||
| */ | ||
| export function normalizeLatexDelimiters(source: string): string { | ||
| const fenced = findFencedSpans(source); | ||
| const protectedSpans = [...fenced, ...findInlineSpans(source, fenced)] | ||
| .sort((a, b) => a.start - b.start); | ||
|
|
||
| let result = ""; | ||
| let cursor = 0; | ||
| for (const span of protectedSpans) { | ||
| if (span.start < cursor) continue; | ||
| result += convertProse(source.slice(cursor, span.start)); | ||
| result += source.slice(span.start, span.end); | ||
| cursor = span.end; | ||
| } | ||
| result += convertProse(source.slice(cursor)); | ||
| return result; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.