Skip to content

Commit 17134a1

Browse files
authored
fix: address remaining PR #135 review feedback (#136)
- /markdown toggle: set sessionNeedsRebuild so system prompt updates (#1) - CLI help: document --[no-]markdown, --md/--no-md aliases, HYPERAGENT_MARKDOWN (#6) - Streamed output: gate renderMarkdown on looksLikeMarkdown consistently (#7) - markdown-renderer: use local Marked instance instead of global setOptions (#9) - looksLikeMarkdown: remove over-eager bold and unordered-list patterns (#10) - unescape: verified valid marked-terminal option (comment was wrong) (#8) - linkifyFiles order: verified safe — [[file:]] not a markdown token (#16) Verified: diff matches this message. 40 test files, 2350 tests pass. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent c01ab93 commit 17134a1

4 files changed

Lines changed: 36 additions & 22 deletions

File tree

src/agent/cli-parser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ Options:
104104
--show-timing Log timing breakdown to ~/.hyperagent/logs/
105105
--show-reasoning [level] Set reasoning effort (low|medium|high|xhigh, default: high)
106106
--verbose Verbose output mode (scrolling reasoning, turn details)
107-
--no-markdown Disable markdown rendering (use raw streaming instead)
107+
--[no-]markdown Toggle markdown rendering (default: on, env: HYPERAGENT_MARKDOWN)
108+
Aliases: --md, --no-md
108109
--transcript Record session transcript to ~/.hyperagent/logs/
109110
--list-models List available models and exit
110111
--resume [id] Resume previous session (last if no ID given)

src/agent/index.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5993,10 +5993,19 @@ async function processMessage(
59935993
};
59945994

59955995
if (state.markdownEnabled && state.streamedText) {
5996-
// Markdown mode: output was buffered (not streamed). Render now.
5997-
let rendered = renderMarkdown(state.streamedText);
5998-
rendered = linkifyFiles(rendered, fsWriteBase, trackFile);
5999-
console.log(rendered);
5996+
// Markdown mode: output was buffered (not streamed). Render now
5997+
// if it looks like markdown; otherwise print as-is to avoid
5998+
// mangling plain prose with ANSI escapes.
5999+
let output: string;
6000+
if (looksLikeMarkdown(state.streamedText)) {
6001+
output = renderMarkdown(state.streamedText);
6002+
} else {
6003+
output = state.streamedText;
6004+
}
6005+
// Replace [[file:path]] markers before printing so ANSI codes
6006+
// from renderMarkdown don't split the markers.
6007+
output = linkifyFiles(output, fsWriteBase, trackFile);
6008+
console.log(output);
60006009
} else if (!state.streamedContent && content) {
60016010
// Non-streamed fallback (rare) — render through markdown if enabled
60026011
if (state.markdownEnabled && looksLikeMarkdown(content)) {

src/agent/markdown-renderer.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,26 @@
88
// import { renderMarkdown } from "./markdown-renderer.js";
99
// console.log(renderMarkdown("# Hello\n**bold** and `code`"));
1010

11-
import { marked, type MarkedOptions } from "marked";
11+
import { Marked, type MarkedOptions } from "marked";
1212
import TerminalRenderer from "marked-terminal";
1313
import { resolve } from "node:path";
1414

15-
// Configure marked with the terminal renderer once at import time.
16-
// marked-terminal handles: headings, bold/italic, code blocks with
17-
// syntax highlighting, lists, tables, links, blockquotes, and hr.
18-
const renderer = new TerminalRenderer({
15+
// Use a local Marked instance so we don't mutate the global marked
16+
// singleton — other code importing marked won't accidentally get
17+
// terminal-rendered output instead of HTML.
18+
const terminalRenderer = new TerminalRenderer({
1919
// Indent code blocks for visual separation
2020
tab: 2,
2121
// Show URLs inline rather than as footnotes
2222
showSectionPrefix: true,
23-
// Use unicode bullets
23+
// Convert HTML entities back to characters
2424
unescape: true,
2525
});
26+
2627
// marked-terminal's renderer type doesn't match marked v15's _Renderer
2728
// exactly, but it works at runtime. Cast to satisfy the type checker.
28-
marked.setOptions({
29-
renderer: renderer as unknown as MarkedOptions["renderer"],
29+
const localMarked = new Marked({
30+
renderer: terminalRenderer as unknown as MarkedOptions["renderer"],
3031
});
3132

3233
/**
@@ -41,9 +42,9 @@ marked.setOptions({
4142
* @returns ANSI-formatted string ready for console output
4243
*/
4344
export function renderMarkdown(text: string): string {
44-
// marked.parse() can return string | Promise<string> depending on
45+
// localMarked.parse() can return string | Promise<string> depending on
4546
// config. With our sync renderer it always returns string.
46-
const rendered = marked.parse(text) as string;
47+
const rendered = localMarked.parse(text) as string;
4748
// Trim trailing newlines that marked adds (avoids double-spacing)
4849
return rendered.replace(/\n+$/, "");
4950
}
@@ -54,14 +55,14 @@ export function renderMarkdown(text: string): string {
5455
* doesn't benefit from being passed through the renderer.
5556
*/
5657
export function looksLikeMarkdown(text: string): boolean {
57-
// Quick heuristics — check for common markdown patterns
58+
// Require a strong signal that this is markdown rather than plain text.
59+
// Weak patterns like bold (**word**) or list bullets (- item) match
60+
// too many false positives (git branch output, log lines, etc.).
5861
return (
59-
/^#{1,6}\s/m.test(text) || // headings
60-
/\*\*[^*]+\*\*/m.test(text) || // bold
61-
/```[\s\S]*?```/m.test(text) || // code blocks
62-
/^\s*[-*+]\s/m.test(text) || // unordered lists
63-
/^\s*\d+\.\s/m.test(text) || // ordered lists
64-
/\|.*\|.*\|/m.test(text) // tables
62+
/^#{1,6}\s/m.test(text) || // headings (strong signal)
63+
/```[\s\S]*?```/m.test(text) || // code fences (strong signal)
64+
/^\|\s*.+\s*\|\s*.+\s*\|/m.test(text) || // table rows (strong signal)
65+
/^\s*\d+\.\s/m.test(text) // ordered lists (moderate signal)
6566
);
6667
}
6768

src/agent/slash-commands.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ export async function handleSlashCommand(
298298
// Toggle markdown rendering — buffers output instead of streaming
299299
// and renders through marked-terminal for proper formatting.
300300
state.markdownEnabled = !state.markdownEnabled;
301+
// System prompt includes markdown-specific instructions (OUTPUT mode,
302+
// FILE REFERENCES). Rebuild the session so the LLM gets the update.
303+
state.sessionNeedsRebuild = true;
301304
console.log(
302305
` 📝 Markdown rendering: ${state.markdownEnabled ? C.ok("ON") + C.dim(" (output buffered, not streamed)") : C.err("OFF") + C.dim(" (raw streaming)")}`,
303306
);

0 commit comments

Comments
 (0)