Feat markdown rendering#135
Merged
simongdavies merged 7 commits intoMay 14, 2026
Merged
Conversation
- Add marked + marked-terminal for ANSI-formatted markdown output - New --markdown CLI flag and /markdown (/md) toggle command - When enabled: LLM output is buffered (not streamed) and rendered with proper headings, bold, code blocks, lists, tables, links - When disabled (default): raw streaming as before - markdownEnabled state field, wired through event handler to suppress character-by-character streaming in markdown mode - processMessage renders buffered output through marked-terminal before displaying to user - All 2342 tests pass Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
All error messages previously had only the label (e.g. '❌ Error:') styled red, while the actual error text appeared unstyled after the ANSI reset. Now the entire message including the error detail is wrapped in C.err(). - event-handler.ts: SDK-level tool failure error display - slash-commands.ts: 11 error paths (models, sessions, history, audit, modules) all consistently fully red Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
- Tool result strings: render via marked-terminal when markdown mode is enabled and text contains markdown patterns (headings, code blocks, etc.) - register_handler show-code: wrap in ```javascript fence for syntax hl - execute_bash show-code: wrap in ```bash fence for syntax hl - Errors/warnings left untouched — keep C.err()/C.warn() ANSI coloring - JSON objects left untouched — dim pretty-print is fine for structured data - Bash stdout left untouched — too risky for false-positive markdown matches Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
- Add wrapToolResult() helper returning proper ToolResultObject with skipLargeOutputProcessing to bypass SDK VB() /tmp truncation - Restructure execute_javascript/execute_bash thresholds: disk save (20KB) and LLM context limit (50K chars) as independent concerns - Add 50K char guards to read_input/read_output with sandbox guidance - Render plugin config and startup/slash-command config as markdown tables when markdown mode is enabled - Render tool result strings with markdown patterns via renderMarkdown Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
… tracking - Markdown rendering enabled by default (--no-markdown to disable) - HYPERAGENT_MARKDOWN env default flipped (disable with =0) - Startup config banner as markdown tables - Plugin and sandbox config confirmations as markdown tables - Tool execution: show tool name for all tools, not just sandbox - Tool results gated behind verbose mode (non-verbose shows ✅ Done) - Errors always shown regardless of verbose mode - [plugins] and [mcp] discovery output gated behind --verbose - File tracking: write_output and auto-save register produced files - [[file:path]] markers in LLM output resolved via linkifyFiles() - /files command: lists all produced files with numbered refs - /open command: opens file by number (WSL/macOS/Linux) - Dedup produced files by absPath to avoid double entries - System message: conditional markdown/plain output instructions - C.fileLink() returns raw paths for terminal auto-detection Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds opt-in (default-on) terminal markdown rendering for LLM output via marked + marked-terminal, plus session file tracking (/files, /open, [[file:...]] markers), a /markdown toggle, refactored tool-output handling that splits "what gets saved to disk" from "what the LLM sees", and a few build/CLI/event-handler tweaks.
Changes:
- New
markdown-renderer.ts,--markdown/--mdCLI flags,/markdownslash command, and markdown rendering of LLM responses, code blocks, and selected tool/config output. - New
state.producedFilestracking,[[file:path]]linkification, and/files+/opencommands; refactoredexecute_javascript/execute_bashlarge-output paths around a newwrapToolResulthelper and sharedDISK_SAVE_THRESHOLD_BYTES/MAX_LLM_RESULT_CHARS. - Bash bundle build now adds a
node:modulestub and fails CI on bundle errors; CI startup logs gated behind--verbose.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| src/agent/markdown-renderer.ts | New module: marked + marked-terminal wrapper, markdown heuristic, and [[file:...]] linkifier. |
| src/agent/index.ts | Tool-output refactor, wrapToolResult, file tracking, markdown rendering in main loop and banner, verbose-gated startup logs. |
| src/agent/event-handler.ts | Tool execution UX changes: unified tool label, non-verbose path prints only ✅ Done, markdown rendering of string/JSON results. |
| src/agent/slash-commands.ts | New /markdown, /files, /open; table-based /config; markdown tables in plugin configure flows; some error-string concatenation tweaks. |
| src/agent/state.ts | Adds markdownEnabled and producedFiles to AgentState. |
| src/agent/cli-parser.ts | Adds markdown CLI flag with env default and --markdown/--no-markdown/--md/--no-md. |
| src/agent/commands.ts | Help entries for /markdown, /files, /open. |
| src/agent/system-message.ts | Adds markdownEnabled to system prompt; injects OUTPUT mode and FILE REFERENCES instructions. |
| src/agent/ansi.ts | Adds C.link (OSC 8) and C.fileLink helpers. |
| scripts/build-modules.js | Fail-hard in CI on bash bundle build failure. |
| scripts/bash-bundle/build.mjs | Adds node:module alias to module-stub. |
| scripts/bash-bundle/module-stub.mjs | New stub exporting a fake createRequire. |
| builtin-modules/bash.json | Updated sourceHash for the rebuilt bundle. |
| package.json | Adds marked, marked-terminal, @types/marked-terminal. |
| package-lock.json | Adds new deps; also contains many unrelated upgrades/downgrades and missing-integrity entries. |
| .gitignore | Removes a stray malformed entry; keeps _tmp_bundle.js ignore. |
Comments suppressed due to low confidence (1)
src/agent/index.ts:5990
linkifyFilesis applied to the post-rendered markdown string.renderMarkdownmay surround text with ANSI reset/styling escape sequences (e.g. inside list items, headings, or paragraphs), which can split a[[file:path]]literal if marked decides any character within is a markdown token (e.g. a*or backtick inside the path). Consider runninglinkifyFileson the raw markdown text first (replacing markers with a stable plain-text placeholder), then passing the result throughrenderMarkdown, or vice-versa with a more tolerant regex.
if (state.markdownEnabled && state.streamedText) {
// Markdown mode: output was buffered (not streamed). Render now.
let rendered = renderMarkdown(state.streamedText);
rendered = linkifyFiles(rendered, fsWriteBase, trackFile);
console.log(rendered);
- /markdown toggle: set sessionNeedsRebuild so system prompt updates - Help text: fix wrong '(default)' label (ON is default, not OFF) - /open: use spawnSync with argv arrays instead of shell interpolation (prevents shell injection with special characters in paths) - /open: validate input with /^\d+$/ regex (reject '1abc' etc.) - Spinner: restart after tool name display so user sees activity - Non-verbose errors: always show parsed.error even in non-verbose mode (previously hidden behind '✅ Done' — regression) - Gate 1 config: restore requested config display before plugin approval - Truncated preview: skip markdown rendering on 300-char truncated content (truncation can break mid-token producing garbled output) - _userDisplayed: verified path through wrapToolResult JSON serialisation Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
simongdavies
added a commit
that referenced
this pull request
May 14, 2026
- /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>
simongdavies
added a commit
that referenced
this pull request
May 14, 2026
) marked v15's use() iterates 'for (prop in pack.renderer)' and validates every enumerable key against its known renderer method list, throwing "renderer 'o' does not exist" at module init. The legacy 'new TerminalRenderer(opts)' route assigns config to own enumerable properties (this.o, this.tab, ...), so the first iteration hits an unknown key and crashes. This broke the agent on every 'just start' since PR #135 landed; CI never noticed because no test imports the module. Switch to the modern markedTerminal() factory which returns a clean MarkedExtension containing only renderer method keys, and add a regression test that import-loads the module and smoke-tests rendering so a future bump can't reintroduce this class of crash. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
simongdavies
added a commit
that referenced
this pull request
May 15, 2026
* fix: marked v15 + marked-terminal v7 incompat in markdown-renderer marked v15's use() iterates 'for (prop in pack.renderer)' and validates every enumerable key against its known renderer method list, throwing "renderer 'o' does not exist" at module init. The legacy 'new TerminalRenderer(opts)' route assigns config to own enumerable properties (this.o, this.tab, ...), so the first iteration hits an unknown key and crashes. This broke the agent on every 'just start' since PR #135 landed; CI never noticed because no test imports the module. Switch to the modern markedTerminal() factory which returns a clean MarkedExtension containing only renderer method keys, and add a regression test that import-loads the module and smoke-tests rendering so a future bump can't reintroduce this class of crash. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * feat: user-generated skills from session learnings Lets users persist what the agent learned in a session as a reusable skill at ~/.hyperagent/skills/<name>/SKILL.md, surviving upgrades and overriding system skills with the same name. Triggered via: - /save-skill [name] - slash command that builds a synthetic prompt from session context (tool history, MCP servers, modules registered, recent errors) and asks the LLM to call generate_skill() - 'save this as a skill' (natural language) - system message documents the generate_skill tool so the LLM can call it directly Components added: - src/agent/skill-writer.ts: validation + CRUD for user skills, with HYPERAGENT_USER_SKILLS_DIR env override for tests - src/agent/session-context.ts: pure extractor that rolls up tool history, MCP servers, modules registered, and recent errors into a prompt-ready string - generate_skill tool: registered in all three gating points (tools[], ALLOWED_TOOLS, availableTools[]) with interactive approval - /skills enhanced with 'info <name>', 'edit <name>', 'delete <name>', override-detection badge for user skills - skill-loader now supports loading from multiple directories with override semantics (later dirs win) - state.ts tracks toolCallHistory (capped FIFO), mcpServersUsed, modulesRegistered, pendingPrompt; populated by onPostToolUse hook and registerModuleImpl - system-message.ts documents the saving workflow for the LLM - docs/SKILLS.md adds 'User Skills (Persist What You Learn)' section Tests: 39 new (skill-writer 22, session-context 9, skill-loader +8). All 2443 TS tests pass; 124 Rust tests pass; lint clean. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * docs: add hand-off test plan for user-generated skills Standalone walkthrough at docs/TESTING-USER-SKILLS.md covering smoke test, full workout, override behaviour, boundary cases, and likely failure modes. Intended to be passed to reviewers / testers who want to exercise the feature without reading the implementation. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * fix: address PR #139 review feedback (18 issues) Security & correctness - skill-writer: cap on UTF-8 byte length (not String.length) so a multi-byte payload can't bypass the 64 KB limit - skill-writer: reject reserved /skills subcommand names (info, edit, delete, list) to prevent shadowing the CLI surface - skill-writer: reject description/triggers containing newlines or a bare '---' line so they can't break out of YAML frontmatter - slash-commands /skills info|edit|delete: validate <name> via validateSkillName before any filesystem join — closes the path traversal vector pointed out by the reviewer UX correctness - index.ts generate_skill: surface an 'Overwrite existing user skill?' confirmation when overwrite=true and the file already exists - slash-commands /save-skill: pass skipAutoSuggest=true so the synthetic prompt's scaffolding terms don't trigger unrelated skills via runSuggestApproach - slash-commands /new: also reset currentUserPrompt + lastGuidance - slash-commands /resume: reset toolCallHistory, mcpServersUsed, modulesRegistered, currentUserPrompt, lastGuidance — local session-learning state can't be reconstructed from a resumed remote session - slash-commands /save-skill: fix 'distinct tools' status line to count the full tool history, not the bounded topTools view - session-context: truncate currentUserPrompt to 2000 chars with an ellipsis so a giant paste can't dominate the prompt MCP session-learning correctness - mcp/plugin-adapter: add optional onCall observer; agent wires it to state.mcpServersUsed so calls made from inside execute_javascript via host:mcp-<name> imports are now tracked - state.ts: add skipNextAutoSuggest flag (consumed in onUserPromptSubmitted) Documentation - docs/TESTING-USER-SKILLS.md: drop branch-name reference, switch override example from non-existent 'code-review' to bundled 'kql-expert', clarify '/skills edit' prints a path (no $EDITOR), describe the now-correct overwrite confirmation flow, note that the override badge surfaces in '/skills' list view, fix approval prompt wording (summary, not full content) Tests - Reserved-name rejection - YAML-unsafe newline rejection (description + trigger) - UTF-8 byte-length cap (32 KB of 4-byte chars) - User-prompt truncation contract Quality gate: 2448 TS tests pass (+5), 124 Rust tests pass. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> --------- Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
simongdavies
added a commit
that referenced
this pull request
May 15, 2026
* docs: changelog for v0.6.0 Move [Unreleased] section to [v0.6.0] - 2026-05-15 covering the 23 commits landed on upstream/main since v0.5.0: Added: - User-generated skills from session learnings (#139) - KQL expert skill with requires-mcp frontmatter and Kusto highlighting (#137) - Terminal markdown rendering via marked + marked-terminal (#135, #136) - Verbose/debug gating for diagnostic output (#137) - execute_bash large output interception (#134) Fixed: - marked v15 + marked-terminal v7 incompat in markdown-renderer (#138) - HybridFs sandbox /tmp path mapping + adapter refactor (#134) - Prettier mangling of nested template literals in styled output (#134) - Full error text now wrapped in C.err() across 11 paths (#135) - /markdown toggle now flips sessionNeedsRebuild (#136) - looksLikeMarkdown false positives on bold/unordered-list (#136) Changed: - Dependency bumps: msal-node 5.2.1, tsx 4.22.0, @types/node 25.8.0, tokio in code-validator guest (#142, #143, #146, #147) Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * fix: address PR #148 review feedback (3 issues) CHANGELOG.md: - Fabric RTI CLI flag: `--mcp setup-fabric-rti` was wrong, actual flag in src/agent/cli-parser.ts is `--mcp-setup-fabric-rti` (single hyphenated token). Users copy-pasting the old form would hit "unknown argument" - Terminal markdown rendering default: said "raw streaming remains the default" but cli-parser.ts sets `markdown: process.env.HYPERAGENT_MARKDOWN !== "0"` (default ON) and `--[no-]markdown` help text confirms default: on. Reworded to make default-on explicit and document the three opt-out paths - Missing link reference: added `[v0.6.0]: https://github.com/hyperlight-dev/hyperagent/releases/tag/v0.6.0` to match the existing reference-style link section No code changes. Prettier clean. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> --------- Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
This pull request adds support for terminal markdown rendering of LLM output, improves file link handling, and enhances CLI and command features. The main improvements include integrating the
markedandmarked-terminallibraries to render markdown as ANSI-formatted output, introducing new CLI flags and commands for markdown and file management, and refining tool execution feedback. These changes significantly improve the readability and usability of LLM responses in the terminal.Markdown Rendering and CLI Enhancements:
markedandmarked-terminaldependencies and integrated a newmarkdown-renderer.tsmodule to render LLM markdown output as ANSI-formatted terminal output, improving readability for structured responses. Also included a heuristic to detect markdown content and a post-processor to convert file references into clickable links.--markdown,--no-markdown,--md,--no-md) to toggle markdown rendering, with the feature enabled by default. Updated CLI config and help text accordingly./markdowncommand to toggle markdown rendering at runtime, with detailed help text explaining the effects and defaults.File Link and Session File Management:
[[file:path]]markers in LLM output into clickable OSC 8 terminal hyperlinks and registered file references for session management./filesand/openslash commands to list and open files produced during the session, with corresponding help entries.Tool Execution and Output Improvements:
execute_javascript,execute_bash), results are rendered as markdown if enabled, or dimmed otherwise. Large JSON outputs are wrapped in code blocks for better formatting. (src/agent/event-handler.tsOther Notable Changes:
src/agent/ansi.tssrc/agent/slash-commands.tsBuild and Bash Interpreter Updates:
node:moduleto supportcreateRequire()in the bash interpreter sandbox, and updated the bash bundle build process and metadata.