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

Merged
adrianbonpin merged 1 commits from feat/promote-inline-commands into prod 2026-10-03 22:00:39 +00:00
4 changed files with 199 additions and 18 deletions
Showing only changes of commit d5382cfdac - Show all commits
+92 -1
View File
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { inferLanguage, labelCodeFences } from "../lib/code"; import {
inferLanguage,
labelCodeFences,
promoteStandaloneCodeSpans,
repairCodeBlocks,
} from "../lib/code";
describe("inferLanguage", () => { describe("inferLanguage", () => {
it("returns nothing for empty or whitespace-only code", () => { it("returns nothing for empty or whitespace-only code", () => {
@@ -79,6 +84,71 @@ describe("inferLanguage", () => {
}); });
}); });
describe("promoteStandaloneCodeSpans", () => {
it("promotes a paragraph that is one inline span", () => {
// The reported case: asking for a command and getting a lone chip back.
expect(promoteStandaloneCodeSpans("`pwd`")).toBe("```bash\npwd\n```");
});
it("promotes the same span on its own line within a larger answer", () => {
const promoted = promoteStandaloneCodeSpans("Here you go:\n\n`pwd`\n\nThat prints the path.");
expect(promoted).toContain("```bash\npwd\n```");
expect(promoted).toContain("Here you go:");
expect(promoted).toContain("That prints the path.");
});
it("tolerates a sentence-ending period", () => {
expect(promoteStandaloneCodeSpans("`pwd`.")).toBe("```bash\npwd\n```");
});
it("leaves a span embedded in a sentence alone", () => {
const input = "Run `pwd` to see where you are.";
expect(promoteStandaloneCodeSpans(input)).toBe(input);
});
it("leaves a bulleted span alone", () => {
const input = "- `pwd -P` resolves symlinks";
expect(promoteStandaloneCodeSpans(input)).toBe(input);
});
it("never rewrites a span-like line inside an existing fence", () => {
const input = "```bash\n`not-a-span`\n```";
expect(promoteStandaloneCodeSpans(input)).toBe(input);
});
it("fences a span whose language cannot be inferred, without guessing one", () => {
expect(promoteStandaloneCodeSpans("`Get-Location`")).toBe("```\nGet-Location\n```");
});
it("is idempotent", () => {
const once = promoteStandaloneCodeSpans("`pwd`");
expect(promoteStandaloneCodeSpans(once)).toBe(once);
});
it("is a no-op on ordinary prose", () => {
const input = "There is no code here at all.";
expect(promoteStandaloneCodeSpans(input)).toBe(input);
});
});
describe("repairCodeBlocks", () => {
it("promotes a lone command and labels it in one pass", () => {
expect(repairCodeBlocks("`pwd`")).toBe("```bash\npwd\n```");
});
it("still labels a bare multi-line fence", () => {
expect(repairCodeBlocks("```\ngit status\n```")).toBe("```bash\ngit status\n```");
});
it("handles a promoted block and a plain fence in the same answer", () => {
const repaired = repairCodeBlocks("First:\n\n`pwd`\n\nThen:\n\n```\nls -la\n```");
expect(repaired).toContain("```bash\npwd\n```");
expect(repaired).toContain("```bash\nls -la\n```");
});
});
describe("labelCodeFences", () => { describe("labelCodeFences", () => {
it("labels a bare fence", () => { it("labels a bare fence", () => {
const labelled = labelCodeFences("```\ngit status\n```"); const labelled = labelCodeFences("```\ngit status\n```");
@@ -123,6 +193,27 @@ describe("labelCodeFences", () => {
expect(labelCodeFences(once)).toBe(once); expect(labelCodeFences(once)).toBe(once);
}); });
it("does not mistake a closing fence for a new opening one", () => {
// A closing ``` and an opening bare ``` are identical lines. Treating the
// closing one as an opening fence swallowed everything after it, so the
// second block never got labelled.
const input = "```bash\npwd\n```\n\n```\nls -la\n```";
expect(labelCodeFences(input)).toContain("```bash\nls -la\n```");
});
it("labels a later block even when an earlier one is already labelled", () => {
const input = "```rust\nfn main() {}\n```\n\n```\npwd\n```";
const labelled = labelCodeFences(input);
expect(labelled).toContain("```rust");
expect(labelled).toContain("```bash\npwd\n```");
});
it("preserves a language the model chose, including multi-word freeform tags", () => {
expect(labelCodeFences("```c++\nint main() {}\n```")).toContain("```c++");
expect(labelCodeFences("```objective-c\nint x;\n```")).toContain("```objective-c");
});
it("preserves indentation of a nested fence", () => { it("preserves indentation of a nested fence", () => {
expect(labelCodeFences(" ```\n ls\n ```")).toContain(" ```bash"); expect(labelCodeFences(" ```\n ls\n ```")).toContain(" ```bash");
}); });
+93 -7
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 * Vicinae renders fences through `MdCodeBlock.qml`, which gives a dark panel, a
* header showing the language, a Copy button, and syntax colouring resolved by * 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, * 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 * 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, * 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 * and inference stays deliberately conservative.
* a clear signal, and leaves anything ambiguous alone.
*/ */
/** First words that mark a block as shell rather than a programming language. */ /** First words that mark a block as shell rather than a programming language. */
@@ -75,6 +74,20 @@ const SHELL_COMMANDS = new Set([
"zip", "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[] { function significantLines(code: string): string[] {
return code return code
.split("\n") .split("\n")
@@ -153,7 +166,65 @@ export function inferLanguage(code: string): string | undefined {
return 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. * Adds a language to fences that have none, leaving labelled fences untouched.
@@ -175,11 +246,11 @@ export function labelCodeFences(markdown: string): string {
continue; continue;
} }
const [, indent, fence] = opener; const [, indent, marker, existing] = opener;
const body: string[] = []; const body: string[] = [];
index += 1; index += 1;
while (index < lines.length && !lines[index].trimStart().startsWith(fence)) { while (index < lines.length && !CLOSING_FENCE.test(lines[index])) {
body.push(lines[index]); body.push(lines[index]);
index += 1; index += 1;
} }
@@ -187,11 +258,26 @@ export function labelCodeFences(markdown: string): string {
const closing = index < lines.length ? lines[index] : ""; const closing = index < lines.length ? lines[index] : "";
if (closing) index += 1; if (closing) index += 1;
// A fence the model already labelled is left exactly as written.
if (existing) out.push(openingLine);
else {
const language = inferLanguage(body.join("\n")); const language = inferLanguage(body.join("\n"));
out.push(language ? `${indent}${fence}${language}` : openingLine); out.push(language ? `${indent}${marker}${language}` : openingLine);
}
out.push(...body); out.push(...body);
if (closing) out.push(closing); if (closing) out.push(closing);
} }
return out.join("\n"); 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));
}
+8 -5
View File
@@ -20,20 +20,23 @@ export const MAX_STORED_CHARS_PER_TURN = 20_000;
/** How much of a prompt becomes the session title in the session list. */ /** How much of a prompt becomes the session title in the session list. */
export const TITLE_MAX_LENGTH = 60; export const TITLE_MAX_LENGTH = 60;
export const DEFAULT_SYSTEM_PROMPT = `You are Lam, a concise assistant in the Vicinae launcher on the user's desktop. export const DEFAULT_SYSTEM_PROMPT = `You are Lam, a friendly assistant in the Vicinae launcher on the user's desktop.
Answer first, detail after. No preamble. Sound like a person, not a document. Use contractions, talk about "you" and "I", and a short natural remark is welcome ("Sure, easy one", "Ah, that trips people up"). Performed enthusiasm and filler added to seem helpful are not.
Lead with the answer, then the detail. This renders in a narrow window, so stay brief: a tight sentence or two, or a short list, then stop. Keep paragraphs short and skip large headings and wide tables.
Write plainly: Write plainly:
- Never use an em dash. Use a comma, a colon, or a full stop. - Never use an em dash. Use a comma, a colon, or a full stop.
- No generated-text tells: "it's not just X, it's Y", "not only... but also", "the real question is". No openers like "Great question" or "Certainly". No "I hope this helps". - Skip stock service-desk phrasing: "Certainly", "I'd be happy to help", "Great question", "I hope this helps", "Let me know if you need anything else". Warmth that means something beats politeness that means nothing.
- No generated-text tells: "it's not just X, it's Y", "not only... but also", "the real question is".
- Avoid delve, tapestry, landscape, leverage, robust, seamless, realm, testament. - Avoid delve, tapestry, landscape, leverage, robust, seamless, realm, testament.
- Do not restate the question or narrate your process. - Do not restate the question or narrate your process.
- Bold only the direct answer to a factual question, never for emphasis in prose. - Bold only the direct answer to a factual question, never for emphasis in prose.
You render in a narrow window: keep paragraphs short, prefer compact lists, and skip large headings and wide tables. Fence every command and every snippet, and always name the language: bash for shell and terminal commands, then json, sql, yaml, or the language for code (ts, js, python, rust, go). Never leave code inline in a sentence, and never open a fence without a language. Put no $ or shell prompt inside the block so it can be copied and run as-is. Ignore the formatting of earlier answers in this conversation: if one put a command in inline code, fence it anyway.
Fence every command and every snippet, and always give the fence a language: bash for shell and terminal commands, then json, sql, yaml, or the language name for code (ts, js, python, rust, go). Never leave code inline in a sentence, and never open a fence without a language. Put commands on their own lines, with no leading $ or shell prompt, so they can be copied and run as-is. A fenced block is never a whole answer on its own. Say in one plain sentence what the code does or when to reach for it, so the reply reads like something a person wrote rather than a bare snippet.
Call a tool when it makes the answer more accurate. Anything time-sensitive or past your cutoff should be checked rather than guessed, and never invent tool output. If a tool fails, say so and answer with what you know. Call a tool when it makes the answer more accurate. Anything time-sensitive or past your cutoff should be checked rather than guessed, and never invent tool output. If a tool fails, say so and answer with what you know.
+5 -4
View File
@@ -1,4 +1,4 @@
import { labelCodeFences } from "./code"; import { repairCodeBlocks } from "./code";
import type { ChatTurn, ThreadMessage } from "./types"; import type { ChatTurn, ThreadMessage } from "./types";
/** /**
@@ -55,9 +55,10 @@ function renderMessage(message: ThreadMessage, options: TranscriptOptions): stri
// stands in and the user can see what Lam is actually doing. // stands in and the user can see what Lam is actually doing.
const placeholder = message.status ? `_${message.status}…_` : "_Thinking…_"; const placeholder = message.status ? `_${message.status}…_` : "_Thinking…_";
const body = message.text.trim() const body = message.text.trim()
? // Fences without a language render as a plain dark panel; labelling them ? // Fences without a language render as a plain dark panel, and a lone
// lets Vicinae colour the code (`MdCodeBlock.qml` + KSyntaxHighlighting). // inline command renders as a chip with no Copy button at all. Both are
labelCodeFences(message.text.trim()) // repaired before the markdown reaches the renderer.
repairCodeBlocks(message.text.trim())
: message.isStreaming : message.isStreaming
? placeholder ? placeholder
: "_No response._"; : "_No response._";