fix: fence lone commands and give the prompt some warmth (#12)

This commit was merged in pull request #12.
This commit is contained in:
2026-10-03 22:00:38 +00:00
parent 75fc914eca
commit 95edbcd20b
4 changed files with 199 additions and 18 deletions
+94 -8
View File
@@ -1,5 +1,5 @@
/**
* Labels fenced code blocks the model left unlabelled.
* Fixups for code the model wrote as markdown.
*
* Vicinae renders fences through `MdCodeBlock.qml`, which gives a dark panel, a
* header showing the language, a Copy button, and syntax colouring resolved by
@@ -9,8 +9,7 @@
* so `ts` resolves through `file.ts` and `bash` through its definition name,
* while an unrecognised string such as `terminal` still renders as a label but
* produces no highlighting at all. A wrong label is therefore worse than none,
* and this stays deliberately conservative: it only labels a block when there is
* a clear signal, and leaves anything ambiguous alone.
* and inference stays deliberately conservative.
*/
/** First words that mark a block as shell rather than a programming language. */
@@ -75,6 +74,20 @@ const SHELL_COMMANDS = new Set([
"zip",
]);
const FENCE = "```";
/**
* An opening fence, with an optional language.
*
* The language has to be part of this pattern. A closing ``` and an opening
* bare ``` are the same two lines apart from context, so a pattern that only
* matched bare fences treated every closing fence as a new opening one and
* swallowed the rest of the answer.
*/
const OPENING_FENCE = /^(\s*)(`{3,}|~{3,})\s*([^\s`~]*)\s*$/;
/** A closing fence is the marker and nothing else. */
const CLOSING_FENCE = /^\s*(`{3,}|~{3,})\s*$/;
const STANDALONE_CODE_SPAN = /^\s*`([^`\n]+)`\s*[.!]?\s*$/;
function significantLines(code: string): string[] {
return code
.split("\n")
@@ -153,7 +166,65 @@ export function inferLanguage(code: string): string | undefined {
return undefined;
}
const OPENING_FENCE = /^(\s*)(`{3,}|~{3,})\s*$/;
/** Copies a fenced block through verbatim, returning the index just past it. */
function copyFence(lines: string[], out: string[], start: number): number {
let index = start;
out.push(lines[index]);
index += 1;
while (index < lines.length && !CLOSING_FENCE.test(lines[index])) {
out.push(lines[index]);
index += 1;
}
if (index < lines.length) {
out.push(lines[index]);
index += 1;
}
return index;
}
/**
* Turns a paragraph that is nothing but one inline code span into a fence.
*
* Models reliably fence multi-line code and just as reliably answer "what is the
* command for X" with a lone `` `pwd` ``. A single inline chip is the worst
* presentation for that answer: no Copy button, no language, no colouring. The
* rule is narrow on purpose, a whole line that is exactly one span, so
* ``run `pwd` to check`` is left alone while a standalone command is promoted.
*
* Existing fences are copied through untouched, so a span-like line inside a
* code block is never rewritten.
*/
export function promoteStandaloneCodeSpans(markdown: string): string {
const lines = markdown.split("\n");
const out: string[] = [];
let index = 0;
while (index < lines.length) {
const fence = OPENING_FENCE.exec(lines[index]);
if (fence) {
index = copyFence(lines, out, index);
continue;
}
const span = STANDALONE_CODE_SPAN.exec(lines[index]);
if (span) {
const code = span[1].trim();
out.push(FENCE + (inferLanguage(code) ?? ""), code, FENCE);
index += 1;
continue;
}
out.push(lines[index]);
index += 1;
}
return out.join("\n");
}
/**
* Adds a language to fences that have none, leaving labelled fences untouched.
@@ -175,11 +246,11 @@ export function labelCodeFences(markdown: string): string {
continue;
}
const [, indent, fence] = opener;
const [, indent, marker, existing] = opener;
const body: string[] = [];
index += 1;
while (index < lines.length && !lines[index].trimStart().startsWith(fence)) {
while (index < lines.length && !CLOSING_FENCE.test(lines[index])) {
body.push(lines[index]);
index += 1;
}
@@ -187,11 +258,26 @@ export function labelCodeFences(markdown: string): string {
const closing = index < lines.length ? lines[index] : "";
if (closing) index += 1;
const language = inferLanguage(body.join("\n"));
out.push(language ? `${indent}${fence}${language}` : openingLine);
// A fence the model already labelled is left exactly as written.
if (existing) out.push(openingLine);
else {
const language = inferLanguage(body.join("\n"));
out.push(language ? `${indent}${marker}${language}` : openingLine);
}
out.push(...body);
if (closing) out.push(closing);
}
return out.join("\n");
}
/**
* The full pre-render pass, in order.
*
* Promotion runs first so a promoted block is seen as an ordinary fence by the
* labeller, which is also harmless when promotion already inferred a language.
*/
export function repairCodeBlocks(markdown: string): string {
return labelCodeFences(promoteStandaloneCodeSpans(markdown));
}