feat(cli): render assistant output as markdown - #601
Conversation
|
Thanks for the PR @doahc — markdown rendering in the CLI is definitely something we want. However, we're going to close this one for now because of a fundamental compatibility issue. Our CLI runs inside We have this scoped as Issue #504 (Enhanced CLI TUI) — it's planned as a phased core codebase change because getting markdown rendering right under prompt_toolkit requires careful integration with the streaming output, terminal compatibility handling (SSH, tmux, mosh), and performance testing. It's a harder problem than it looks on the surface. There's also a merge conflict with the skin engine work that landed on main since this PR was opened, which customizes the response box branding/colors. The concept is solid though — when we do tackle #504, the approach of rendering Rich Markdown into a Panel and routing through |
…tput Add a lightweight, skin-aware streaming markdown renderer that formats model responses in the terminal. Addresses the core rendering goal from issue NousResearch#504 and builds on the approach discussed in PR NousResearch#601. Streaming path: each completed line passes through a stateful StreamingMarkdownRenderer that produces simple ANSI SGR codes compatible with prompt_toolkit's ANSI() parser and patch_stdout pipeline. Non-streaming path: responses rendered via Rich Markdown through the existing ChatConsole adapter. Supported elements: - Headers (H1-H6), bold, italic, strikethrough, inline code, links - Fenced code blocks with language labels - Ordered and unordered lists (nested) - Tables with full-table buffering for column alignment - Blockquotes with nesting, including tables and code blocks inside - Horizontal rules, HTML stripping, backslash escapes Colors are pulled from the active skin engine (banner_accent for headers, ui_label for code, banner_dim for chrome) so rendering adapts to all built-in and user-defined skins. No new dependencies — uses only stdlib (re, shutil, unicodedata) plus the existing Rich library for the non-streaming fallback.
A2A epic 的桌面侧龙骨:云侧编排腿 hc-523(cloud NousResearch#601)可把任务派进桥接队列 (bridge_type=desktop_daemon)等用户设备来领;本票补上 Desktop 内的 daemon—— 注册设备→拿 device token→心跳→poll 任务→用 hc-524 AcpHarness(fork #107)驱动 本机 coding agent(Claude Code 优先)→回传结果。这是「飞书打字调用家里的 Codex」端到端跑通的最后一块。 契约对齐(逐条对 cloud origin/main 实读) - 注册 POST /api/v1/a2a/daemon/device/register(用户 JWT)→ 201 {device, token:"abr-…"} raw token 只回显一次,本地加密持久化;重注册轮换 token。 - 心跳 POST …/device/heartbeat(abr-token),~30s 一拍,90s 在线窗口。 - 领任务 POST /api/v1/a2a/bridge/tasks/poll(hc-526 端点,同 abr-token)→ {task|null}。 - payload {kind:"local_agent_run", agent_family, prompt, cwd?, …};缺省字段不补默认值。 - 交结果 POST /api/v1/a2a/bridge/tasks/{id}/result;permission_required 按契约上抛 (云侧转飞书审批卡),daemon 绝不自动批准。fork #108 的 401/409 错误面一并消费。 v1 范围拍板(照 PD) - daemon 跑在 Desktop 主进程内(Electron main),Desktop 在线即可被调度;独立常驻 进程留给 hc-535 簇。核心逻辑放独立模块(apex-daemon.cjs 纯函数 + run_once.py)便于 日后抽出。单实例由 Electron requestSingleInstanceLock + started 守卫保证(非锁文件)。 - agent_family:claude 打通;codex/cursor 经 AcpHarness 顺手接上;codebuddy 等未接入 返回明确 agent_not_available/invalid_task,不硬造。 - UI 最小面:providers 设置页「本机 Agent 调度」区块——开关(默认关/dormant)、设备名 输入、连接状态一行、注销按钮;无设备管理面板。i18n 4 语言(zh/zh-TW/en/ja)。 - 凭据只进 safeStorage(device token 加密落盘,同 relay key 待遇),不落明文、不进日志; runner 用本机自有凭据驱动 agent,凭据不上云。日志走 rememberLog→desktop.log。 改动 - agent/coding_agents/run_once.py:一次性 AcpHarness runner(stdin job→stdout result), 纯 reduce_events 折叠事件流 + parse_job 校验;Node daemon shell 出它执行。 - apps/desktop/electron/apex-daemon.cjs:纯 electron-free 契约层——host allowlist、端点 解析、register/heartbeat/poll/task/payload 解析、结果体组装、指数退避、状态派生。 - main.cjs/preload.cjs:加密 token 存储、双自调度循环(心跳+poll,断线退避重连)、 venv-python runner spawn、IPC(status/setEnabled/setDeviceName/unregister)+ boot 门控。 - UI:local-agent-settings.tsx + providers-settings 挂载 + global.d.ts 类型 + 5 i18n 文件。 测试证据(确定性,无假测试) - tests/agent/coding_agents/test_run_once.py:19 例(事件折叠表驱动/权限闸不自动批准/ job 校验拒绝表/scripted channel 端到端 claude 跑通/缺二进制→agent_not_available)。 - apps/desktop/electron/apex-daemon.test.cjs:24 例(allowlist/端点/解析器/结果体/退避/ 状态派生)。已注册进 test:desktop:platforms。 - 门禁:ruff clean、ty clean、coding_agents 全量 64 pass;desktop tsc/tsc -b/vite build 全过、 eslint 我方文件 0 error、vitest i18n parity+feishu 29 pass。 真机验收项(非本机可验):真实 claude/codex/cursor 二进制驱动一条任务、飞书审批卡在 permission_required 时弹出、跨设备定向 poll 只领到本机任务、断网重连退避行为。 Refs: cloud NousResearch#601 (hc-523), fork #107 (hc-524 AcpHarness), fork #108 (契约对齐). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What changed
Why
Hermes CLI responses were displayed as plain text even when the model returned Markdown. This makes headings, emphasis, lists, and fenced code blocks much harder to read. Rich Markdown gives the CLI a clearer final-response surface without changing the agent loop.
How to test
source venv/bin/activate && pytest tests/test_cli_markdown.py tests/test_cli_init.py -qhermesPlatforms tested