@preprocess-cn/nano-code-web
v0.1.7
Published
Web-based display plugin for nano-code
Downloads
826
Readme
nano-code-web
nano-code 的 Web 显示插件(v0.1.3)。通过 SSE(Server-Sent Events)将 nano-code 的 DisplayPlugin 事件推送到浏览器前端,提供图形化交互界面。
Architecture
nano-code 核心 → DisplayPlugin 事件 → display.ts → SSE → public/index.html三层结构:
src/server.ts—NanoCodeWebServer: HTTP + SSE 服务器GET /events— SSE 端点,推送实时事件POST /input— 接收用户输入POST /cancel— 取消当前请求POST /confirm— 处理前端授权确认(Allow/Deny)POST /question-answer— 处理前端问询对话框答案提交POST /mode-toggle— 切换 plan/normal 模式(Shift+Tab)GET /— 前端页面(先查找public/或dist/下同路径文件,存在即返回,否则返回index.html)GET /vendor/*— 第三方 JS/CSS 库GET /health— 健康检查GET /web-files/*— 临时文件服务
src/display.ts— DisplayPlugin 实现- 将 nano-code 的
onStart/onStreamChunk/onToolCall/onStatus等事件广播为 SSE - 通过 NanoPlugin
onBeforeToolCall/onAfterToolCall追踪工具调用 ID
- 将 nano-code 的
public/index.html+public/app.js+public/dialog.js+public/style.css— 单页前端(v0.1.2 将 CSS/JS 从内联提取为独立文件)- 思考内容独立样式(
.msg.think)— 灰色斜体、左侧淡灰边框,视觉区分思考过程与回答 - 调试消息独立样式(
.msg.debug)— 等宽字体、虚线边框,与系统消息样式分离 - Markdown 渲染(
markdown-it+highlight.js语法高亮 +DOMPurify安全消毒) - 流式输出优化:debounce(150ms)+ 代码块闭合检测(``` 成对时立即渲染)
- 工具卡片点击展开/收拢(参数 + 返回结果)
- 问询对话框(
dialog.js模块,三屏:选择 → 自定义输入 → 确认,Esc 取消) - 后台任务状态条(
background:task事件驱动,自动消隐) - Plan mode 指示器(
● PLAN/○ normal,Shift+Tab 切换) - 状态栏(
status:bar事件驱动,显示 mode/tasks 等段落) - 浮动 Toast 通知(右上角,自动消失,支持堆叠与点击关闭)
- 纯 vanilla JS,无框架依赖;第三方库(markdown-it、DOMPurify、highlight.js)通过构建脚本复制到
dist/vendor/本地加载,无需 CDN
- 思考内容独立样式(
Quick Start
构建
npm install
npm run build构建输出 dist/display.js + dist/index.html + dist/app.js + dist/dialog.js + dist/style.css + dist/vendor/(markdown-it、DOMPurify、highlight.js 运行时库)。
配置 nano-code
在 .nano-code.yaml 中配置:
display:
plugin: ./dist/display.js启动
nano-code终端会显示 Web UI 地址(默认 http://localhost:3030)。启动时可配合以下参数:
nano-code --think # 显示思考过程
nano-code --debug # 显示调试信息配置选项
nano-code 通过 session:start 事件的 config 对象传递配置:
| 字段 | 类型 | 说明 |
|------|------|------|
| showThink | boolean | 是否显示 <think>...</think> 内容 |
| debug | boolean | 是否显示调试事件 |
| greeting | string | 会话欢迎语 |
| hasTools | boolean | 是否有可用工具 |
多标签页同步
多个浏览器标签页连接同一后端时,所有页面的消息和状态保持同步:
- 消息同步:
user:input、stream:chunk等事件通过broadcast()发送给所有连接的 SSE 客户端 - Plan mode 同步:切换 mode 时广播
status:bar事件给所有在线客户端;新客户端连入时查询后端当前 mode 并直接推送 - 广播健壮性:
broadcast()使用倒序遍历 + try-catch 保护,单个客户端写失败不影响其他客户端,失效客户端自动移除
事件流
| SSE 事件 | 方向 | 说明 |
|----------|------|------|
| session:start | 后端 → 前端 | 新会话开始,包含配置 |
| session:ready | 后端 → 前端 | 等待用户输入 |
| session:stop | 后端 → 前端 | 会话结束 |
| stream:chunk | 后端 → 前端 | LLM 文本输出流;showThink=true 时前端解析 <think> 标签并分离为独立样式 |
| tool:call | 后端 → 前端 | 工具调用(显示工具卡片) |
| tool:result | 后端 → 前端 | 工具调用结果(更新卡片状态) |
| status | 后端 → 前端 | 状态更新(thinking/end/信息) |
| error | 后端 → 前端 | 错误信息 |
| debug | 后端 → 前端 | 调试信息(.msg.debug 等宽虚线边框样式) |
| agent:turn_start | 后端 → 前端 | Agent 轮次开始 |
| agent:turn_end | 后端 → 前端 | Agent 轮次结束 |
| confirmation:request | 后端 → 前端 | 授权确认请求(Allow/Deny) |
| question:dialog | 后端 → 前端 | 问询对话框(LLM 向用户提问) |
| notify | 后端 → 前端 | 通知消息(浮动 Toast) |
| notify:clear | 后端 → 前端 | 清除所有通知 |
| background:task | 后端 → 前端 | 后台任务状态(started/completed/error) |
| status:bar | 后端 → 前端 | 状态栏段落(mode/tasks 等) |
| user:input | 后端 → 前端 | 用户输入(全局广播,用于历史重放) |
| cancel | 前端 → 后端 | 取消请求(HTTP POST) |
| confirm | 前端 → 后端 | 授权确认结果(HTTP POST) |
| question-answer | 前端 → 后端 | 问询对话框答案提交(HTTP POST) |
| mode-toggle | 前端 → 后端 | Plan mode 切换(HTTP POST) |
关键实现细节
前端重连与历史消息
前端断开后重新打开时,后端会将已广播的历史事件(stream:chunk、user:input、tool:call/result、status、agent:turn_start/end、error)通过 onConnect 回调重新发送给新客户端。缓冲区上限 500 条,/clear 或新 session 时自动清空。
配合 nano-code 的 --continue / -c 标志使用时,restoreSession() 会通过 DisplayPlugin 回放已保存的对话消息,nano-code-web 将其广播为 SSE 事件并同时写入历史缓冲区,前端刷新后仍可看到历史。
广播容错
broadcast() 使用倒序遍历 + try-catch:
- 单个客户端写失败(连接已关闭但尚未清理)不会阻塞其他客户端
- 失效客户端自动从广播列表移除
工具调用双向广播
工具调用事件有两条路径到达 SSE:
- NanoPlugin 路径(
onBeforeToolCall/onAfterToolCall):使用 OpenAI 格式(toolCall.function.name、toolCall.id),是工具卡片的主要来源 - DisplayPlugin 路径(
onToolCall/onToolResult):DisplayPlugin 标准事件路径,作为 NanoPlugin 的补充
ToolCallBroadcaster 通过 ID 去重,确保同一工具调用只广播一次。
前端渲染
- 消息气泡使用
white-space: pre-wrap保留换行和空白 - 工具卡片使用
flex-shrink: 0避免在长对话滚动后被 flex 布局压缩 stream:chunk遇到纯空白块时不创建新气泡(避免工具卡片旁的空白气泡)
开发
npm run dev # tsc --watch 增量编译
npm test # 运行所有测试(70 用例)测试
# 全部测试(单元测试 + 前端集成测试)
npm test
# Playwright 前端集成测试
npx tsx --test tests/frontend.test.ts
# 工具卡片滚动高度测试
npx tsx --test tests/tool-card-scroll.test.ts测试覆盖:
tests/display.test.ts— ThinkFilter(12 用例)+ ToolCallBroadcaster(12 用例,含历史回调)+ 环形缓冲区(9 用例)+ SSE 重放(4 用例)+ background:task(2 用例)+ question:dialog(1 用例)tests/server.test.ts— NanoCodeWebServer(20 用例,含 question-answer / mode-toggle)tests/frontend.test.ts— Playwright 前端渲染(9 用例)tests/tool-card-scroll.test.ts— Playwright 滚动高度(1 用例)
项目文件结构
├── src/
│ ├── display.ts # DisplayPlugin 实现(ThinkFilter、ToolCallBroadcaster、ask_user_question handler)
│ ├── server.ts # HTTP/SSE 服务器
│ └── tool-display.ts # 工具名/参数格式化
├── public/
│ ├── index.html # 前端页面骨架
│ ├── app.js # 前端主逻辑(消息渲染、事件处理)
│ ├── dialog.js # 问询对话框模块(三屏:选择→输入→确认)
│ └── style.css # 前端样式表
├── scripts/
│ └── copy-vendor.mjs # 构建时将 markdown-it/DOMPurify/highlight.js 复制到 dist/vendor/
├── tests/
│ ├── display.test.ts
│ ├── server.test.ts
│ ├── frontend.test.ts
│ └── tool-card-scroll.test.ts
├── dist/ # 构建输出(含 vendor/ 第三方库)
├── vendor/ # highlight.js CDN 缓存(可选,构建时自动下载)
├── .nano-code.yaml # nano-code 插件配置
├── README.md
└── ROADMAP.mdLicense
MIT
