Skip to content

Feat markdown rendering#135

Merged
simongdavies merged 7 commits into
hyperlight-dev:mainfrom
simongdavies:feat-markdown-rendering
May 14, 2026
Merged

Feat markdown rendering#135
simongdavies merged 7 commits into
hyperlight-dev:mainfrom
simongdavies:feat-markdown-rendering

Conversation

@simongdavies
Copy link
Copy Markdown
Member

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 marked and marked-terminal libraries 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:

  • Added marked and marked-terminal dependencies and integrated a new markdown-renderer.ts module 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.
  • Introduced new CLI flags (--markdown, --no-markdown, --md, --no-md) to toggle markdown rendering, with the feature enabled by default. Updated CLI config and help text accordingly.
  • Added a /markdown command to toggle markdown rendering at runtime, with detailed help text explaining the effects and defaults.

File Link and Session File Management:

  • Implemented a post-processor to convert [[file:path]] markers in LLM output into clickable OSC 8 terminal hyperlinks and registered file references for session management.
  • Added /files and /open slash commands to list and open files produced during the session, with corresponding help entries.

Tool Execution and Output Improvements:

  • Improved tool execution feedback: tool names are now shown explicitly, and for sandbox tools (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.ts

Other Notable Changes:

  • Added terminal hyperlink and file path formatting utilities to the ANSI helper (src/agent/ansi.ts
  • Added a utility to format configuration summaries as markdown tables for improved display in markdown mode. (src/agent/slash-commands.ts

Build and Bash Interpreter Updates:

  • Added a stub for node:module to support createRequire() in the bash interpreter sandbox, and updated the bash bundle build process and metadata.
  • Improved error handling in the bash bundle build script to fail hard in CI if the build fails, preventing stale bundles.

- 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>
Copilot AI review requested due to automatic review settings May 14, 2026 14:56
@simongdavies simongdavies added the enhancement New feature or request label May 14, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/--md CLI flags, /markdown slash command, and markdown rendering of LLM responses, code blocks, and selected tool/config output.
  • New state.producedFiles tracking, [[file:path]] linkification, and /files + /open commands; refactored execute_javascript/execute_bash large-output paths around a new wrapToolResult helper and shared DISK_SAVE_THRESHOLD_BYTES / MAX_LLM_RESULT_CHARS.
  • Bash bundle build now adds a node:module stub 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

  • linkifyFiles is applied to the post-rendered markdown string. renderMarkdown may 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 running linkifyFiles on the raw markdown text first (replacing markers with a stable plain-text placeholder), then passing the result through renderMarkdown, 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);

Comment thread src/agent/slash-commands.ts
Comment thread src/agent/commands.ts Outdated
Comment thread src/agent/slash-commands.ts Outdated
Comment thread src/agent/event-handler.ts
Comment thread src/agent/index.ts
Comment thread src/agent/slash-commands.ts Outdated
Comment thread src/agent/event-handler.ts Outdated
Comment thread src/agent/index.ts
Comment thread src/agent/index.ts
Comment thread src/agent/event-handler.ts
- /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 simongdavies merged commit c01ab93 into hyperlight-dev:main May 14, 2026
12 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants