Skip to content

feat(tui): 工具详情页支持参数填写与运行,提升参数/描述可读性 - #9

Open
kugouming wants to merge 20 commits into
mainfrom
feat/tui-tool-params-run-panel
Open

feat(tui): 工具详情页支持参数填写与运行,提升参数/描述可读性#9
kugouming wants to merge 20 commits into
mainfrom
feat/tui-tool-params-run-panel

Conversation

@kugouming

Copy link
Copy Markdown
Contributor

概述

把工具详情页从「只读工具列表」扩展为「参数填写 → 运行 → 结果查看」的一体化交互面板,并针对长描述、多参数的阅读体验做了收敛。

Closes #8

主要改动

  • src/tui/components/ServiceTools.tsx
    • 四区交互(list / params / result / json):Tab 切区、Ctrl+R 运行、Ctrl+J 表单↔原始 JSON、Ctrl+Y 复制、Ctrl+O 落盘、f 全宽视图
    • 结果区:格式化/原始切换、行选区(v 锚定在光标处、↑/↓ 扩展、Esc 取消)、可见行光标 ↑/↓ 移动、←/→ 翻页同步)
    • 描述区:超长默认折叠,Ctrl+E 展开/折叠,且为临时状态——切到另一个工具恢复默认
    • 每个工具的表单值按工具名缓存,切走再切回不丢
  • src/tui/tool-param-schema.ts:按 JSON Schema 构建参数行——非聚焦参数保留一行截断描述( 作为"还有更多"的提示),聚焦时展开全部描述 + enum: / default: + 编辑行;提供参数校验与取值汇总
  • src/tui/text-layout.ts:CJK 双宽感知的截断 / 换行 / 手绘边框 / section 竖线(SECTION_BAR,焦点只体现在颜色上)
  • src/tui/clipboard.tscomponents/SingleLineInput.tsxcomponents/JsonTextArea.tsx:跨平台复制与受控输入组件
  • src/tui/discovery-worker.ts:把 stdio/HTTP/SSE 会话管理抽为泛型复用,新增 callServiceTooltools/call),后端会话过期(-32001 / HTTP 404)透明重建并重试,工具级错误不重试
  • src/tui/app-optimized.tsxcomponents/HelpDialog.tsx:接入与快捷键说明
  • 依赖:新增 string-widthwrap-ansi

测试

  • 新增 tests/integration/tui-service-tools-run.test.ts(19 例,真实组件 + 假 TTY 驱动按键)
  • 新增 tests/unit/tui/{tool-param-schema,text-layout,clipboard,tool-call-result}.test.ts
  • 扩展 tests/integration/{discovery-worker-session-expiry,tui-service-tools-scroll}.test.ts

测试计划

  • npm run typecheck / npm run lint
  • npm test — 62 文件 / 1090 用例全绿
  • npm run verify:local — E2E 15/15 通过
  • TUI 手工确认:参数填写并运行;参数区一行说明、聚焦后完整展开;Ctrl+E 展开描述且切工具后复位;结果区 ↑/↓ 移动、v 从光标起选、Ctrl+Y 只复制选中行

- ServiceTools 从只读工具列表扩展为「参数填写 → 运行 → 结果查看」四区交互面板(Tab 切区、Ctrl+R 运行、Ctrl+J 表单/JSON 切换、Ctrl+Y 复制、Ctrl+O 存文件)
- discovery-worker 将 stdio/HTTP/SSE 会话抽为泛型复用,新增 callServiceTool 及 tools/call,会话过期(-32001/404)自动重试且不重试工具级错误
- 新增 tool-param-schema(按 JSON Schema 构建参数行与校验)、text-layout(CJK 双宽截断/换行/边框)、clipboard(跨平台复制)及其单测
- 新增 JsonTextArea 与 SingleLineInput 受控输入组件,HelpDialog 补充新快捷键说明
- 补充 TUI 运行面板端到端测试与 callServiceTool 会话过期集成测试
- 新增 string-width、wrap-ansi 依赖以支持宽字符换行计算
对 TUI 全操作面做了一轮自动化走查(真实终端逐屏操作 + 断言),修复其中确认的问题:

- 帧溢出导致整屏错乱(根因):服务列表按「每服务 1 行」估算高度、端点列仅 20 列而实际折行 2-3 行,
  表单同样按「每字段 1 行」估算;内容高于视口后 ink 的绝对定位写入落到错误行,表现为游离字符、
  丢行、边框被覆盖。改为每服务严格一行(端点/标签省略号截断)、表单按真实字段行高计算窗口、
  app 层下传正确的 height 预算
- 窄终端(<90 列)布局崩坏:改为自适应列宽,依次丢弃 tags 列 → tools 列 → 收窄 name 列,端点保底 10 列
- Ctrl+S 会把 's' 敲进当前输入框(连按两次可把 's' 当 command 落库):表单文本框改用 SingleLineInput,
  组合键不再泄漏到字段
- 统一表单校验失败无任何提示:新增提交错误框,并显示首个出错字段与原因
- 粘贴/多字符输入被静默丢弃(参数、原始 JSON、搜索框):新增 input-text 判定并接受整块输入
- Ctrl+C 不退出(有服务配置时进程被连接池/定时器/stdio 子进程挂住):ink 在 exitOnCtrlC=true 时
  根本不把 Ctrl+C 交给 useInput,改为 exitOnCtrlC:false + 应用内 unmount 后显式 process.exit
- 删除、重名覆盖直接改配置:新增 y/n 确认框(仅显式 y 生效,避免误按 Enter)
- 自身写盘触发「外部变更」误导提示、footer 配置路径为空:分别用自写标记与 resolved configDir 修正
- 结果框左边框被光标覆盖、JSON 模式以 {} 存根导致 {}{...}、存档路径被截断无法定位:
  光标移入框内、空参数时以空缓冲起手、存档同时复制完整路径

新增回归测试:input-text 单测、ServiceList 布局(单行/窄屏降级/分页/帧高不溢出)、
统一表单输入(Ctrl+S 不污染/校验可见/粘贴),以及共享的伪终端 harness。
- scripts/tui-e2e.mjs(npm run verify:tui):用 tmux 充当真实终端(私有 socket + send-keys +
  capture-pane 读屏),覆盖 T1-T8:列表一屏渲染、窄终端降级、删除二次确认、重名覆盖确认、
  Ctrl+S 不污染、Ctrl+C 退出、参数粘贴运行、工具视图搜索过滤
- 隔离是硬性要求:每场景 mkdtemp 独立配置目录、不绑定端口、不触碰 ~/.onemcp 与运行中的 daemon;
  配置由 onemcp --init 生成后打补丁(不手写模板,避免 schema 变更后应用校验失败静默退出)
- 新增 tui-mock-mcp fixture(与 e2e-local 断言用的 mock-stdio-mcp 分开,避免互相影响)
- CLAUDE.md 新增「TUI 场景回归规则」、命令表、验证链与目录结构;README 补充 TUI 端到端小节
本机 :5625 由 LaunchAgent(site.iskill.onemcp,RunAtLoad + KeepAlive)托管,这类 daemon
不写 ~/.onemcp/server.pid,原先只认 pidfile 的 stopDaemon 只能报错拒绝,导致 deploy:local
永远停在最后一步(构建/打包/全局安装其实已成功),也让人误以为代码有问题。

- 新增 launchd 识别:从监听端口的 PID 出发沿进程祖先链在 `launchctl list` 中找托管 label;
  命中则用 `launchctl kickstart -k gui/<uid>/<label>` 重启(含旧版 stop/start 兜底),
  重启后仍由守护进程监督,随后照旧做 initialize 就绪冒烟
- 未命中(含非 macOS)时保持原 pidfile 路径:读 pidfile → SIGTERM → 必要时 SIGKILL →
  `onemcp -m server -d` 启动
- 文档:CLAUDE.md 增加本机守护方式注记与切版命令,README 说明两种托管方式的自适应重启

验证:launchd 分支 `npm run deploy:local` 输出 "port 5625 is served by launchd service
'site.iskill.onemcp' — restarting it via launchctl" 并 PASSED;pidfile 分支在空闲端口
(--port 5699)同样 PASSED,测试实例与 pidfile 已清理。
配置校验只在「两个名字归一化后相同」时报冲突,而归一化会剥掉所有非 ASCII 字符,
于是:单个纯中文名(如「服务端-甲」)静默通过,它的命名空间前缀退化成 "-";两个
不同的中文名则被判为冲突,报错信息("collides with … after namespace normalization")
对用户毫无可操作性。

改为在归一化后不含 [a-z0-9] 时直接给出可读错误(名字即工具命名空间前缀),
单个与多个非 ASCII 名都拦得住;同时保留原有冲突检查(大小写/空格差异仍会被发现)。
带空格的名字(如 "yapi product")不受影响 —— 归一化后含字母数字。

新增单测覆盖「纯非 ASCII 名被拒」「带空格名仍合法」,并把属性测试的服务名生成器
约束到该契约(原先 fc.string() 会生成被规则拒绝的名字)。
审查发现的三类真实问题:

- 工具调用的重试只覆盖「会话过期」,注释却称与 ToolRouter 一致。路由层用的是
  isSessionExpiryError || isRecoverableConnectionError(会话过期 + 可重连的传输死亡),
  且共享模块的文件头就写明同时服务 TUI。现改为同一判定,并给「一次性会话在应答前
  结束」引入可机读的 SessionClosedError(此前只能靠消息文本判断,包装成 DiscoveryError
  后更是彻底识别不到)—— stdio 后端崩在调用中途时,工具面板现在会重连重放。
- 旧 UI(ONEMCP_USE_LEGACY_UI=true)给 ServiceList 传整个终端高度、给 ServiceFormUnified
  不传高度,而组件语义已经变成「传入值即实际可用高度」,于是帧溢出那类错乱在旧 UI 仍可
  复现;改为传 contentHeight。顺带修掉按键劫持:`t` 被前面的分支当成「打开工具视图」,
  与文档「Space/t 切换启用」不符(后面那条正确分支永远不可达)。
- SingleLineInput 的游标开窗按 UTF-16 码元切片,中文值会超出它承诺的单行(光标也可能
  移出可视窗口);改为按显示宽度(text-layout.displayWidth)计算。

清理:删掉空转的 showDetails/globalToolStats props 与重复的 JSON 包装(统一到
src/utils/safe-json.ts);发现结果被 200K 字符截断时给出提示(此前静默丢尾部),
并说明 per-tool 重置 effect 为何只以工具名为依赖(改为依赖 schema 会在每次列表刷新时
清空正在输入的参数)。旧 UI 同时补齐了 lint 要求(去掉多余 await、浮空 promise、
非空断言、死分支)。
TUI E2E 新增 5 个场景,并把 T8 的「工具总数」断言从 text.includes('4') 收紧为精确表头:

- T9 结果区操作:Ctrl+P 原始输出、v 选行 + Ctrl+Y 复制选区、f 全宽
- T10 大输出分页:PageDown/PageUp 必须改变可见行区间
- T11 CJK:中文标签在列表里每服务恰好一行;中文参数值运行后原样回显
- T12 配置路径与自身写盘提示:footer 显示真实 configDir;保存后是成功提示而非
  「外部变更」(后者是此前修掉的误导提示)
- T13 结果存档:Ctrl+O 给出路径并复制完整路径

场景总数 31 → 49 条断言。另修两处脚本问题:缺 tmux 时返回 2(此前返回 0,把「跳过」
伪装成「通过」);T12 之类的路径断言依赖短配置目录(/var/folders 的长路径会被 footer
截断)。

新增集成测试(覆盖 E2E 到不了的路径):
- tui-single-line-input:CJK 值单行不溢出、整块粘贴、组合键不落字
- tui-call-dead-transport-recovery:真实子进程首调中途死亡 → 重连重放成功(P0 行为回归)
- tui-service-tools-json-cache:Ctrl+J 表单↔JSON 投影、非法 JSON 拒绝执行且给错、
  按工具保留已输入参数

顺带把两个既有测试内联的 ANSI 终端仿真器换成共享 helper(新增的第三份拷贝不再扩散)。
TUI 全部是 .tsx,而 lint/format 脚本只覆盖 .ts:新写的 TUI 代码可以带 any/未处理 promise
而无人发现(仓库里已积累 76 个此类错误,其中 38 个在死组件里)。现把 .tsx 一并纳入,
并修掉全部错误后保持零告警:

- package.json:lint/lint:fix 用 --ext .ts,.tsx;format/format:check 加上 src/**/*.tsx
- 删除死代码:ServiceJsonEditor(组件无任何入口渲染,仅被自身测试引用;其 38 个错误随之消失,
  死代码测试用例一并删除)、FileImportDialog、Footer(均零引用)
- 修掉旧 UI 的 lint 问题(浮空 promise、多余 await、非空断言、死分支)
- CI 新增 tui-e2e 任务(ubuntu + 安装 tmux + build + verify:tui):此前 CLAUDE.md 声称
  「场景退出码供 CI 使用」但 CI 从未跑过任何 E2E

文档同步:CLAUDE.md/README 更新 T 场景清单(T1-T13)、说明 CI 跑哪些门禁
(verify:local 需全局安装,仍是本地门禁);docs/TUI_JSON_MODE.md 顶部标注其所描述的
独立 JSON 视图已不再挂载,当前 JSON 编辑是工具详情页的 Ctrl+J。
- launchd 分支跳过 stopDaemon 的 pidfile 清理:残留 pidfile 里的 PID 可能已被别的进程
  复用,将来走 pidfile 路径时会去 signal 无关进程。改为进入该分支即清掉。
- fallback 的 `launchctl stop` → `start` 不再以退出码判成败:KeepAlive 的服务在 stop 后
  会自动重启,随后的 start 报「已在运行」时其实已经成功。现在只记录一行说明,是否恢复
  交由后续的 initialize 就绪轮询判定。

(说明该分支如何被触发的注释也一并补充:这类守护进程不写 ~/.onemcp/server.pid,
pidfile 路径天然无法接管。)
上一次提交里这份文件是未格式化版本:pre-commit 钩子的 `lint:fix` 把格式修在工作区,
而 git 提交的是已经暂存好的内容,两者不一致 —— 本地 `format:check` 检查的是工作区
(已修好)因而通过,CI 检查的是提交内容因而报了 61 个 prettier 错误。本提交把工作区
里那份修好的内容真正入库。

(钩子的这个陷阱在下一次提交里一并堵上。)
`lint:fix` 改的是工作区,提交记录的是暂存内容:修复若发生在 `git add` 之后,
提交里就仍是旧版本,而紧随其后的 `format:check` 检查的是工作区(已修好)因而通过 ——
CI 于是挂在"你刚修过的那个文件"上(本次 CI 的 Build & Lint 就是这么失败的)。

钩子现在比较 lint:fix 前后已暂存文件的工作区差异,若 lint:fix 引入了新改动则中止提交
并列出受影响文件,要求重新 git add。(已实测:注入一处格式偏差后钩子退出码 1 并指名文件。)
在 ubuntu runner 上连最小 Ink 程序都渲染不出帧(原始 PTY 抓包只有 \x1b[?25l,6 字节),
所有场景都会以「未就绪」失败 —— 属环境限制而非代码问题。改为在 macos-latest 上跑,
并在工作流里写明原因,避免以后被顺手改回 ubuntu。
CI 上 0/13 全败的根因不是 runner 平台(macOS runner 同样失败、本机 CI=true 可复现),
而是 Ink 依赖 is-in-ci:检测到 CI / CONTINUOUS_INTEGRATION / CI_* 时只在退出时绘制最后
一帧,驱动脚本因此全程看到空屏、所有场景以「未就绪」失败。

脚本现在为 tmux 会话剥掉这些标记(等价于开发者终端),CI 任务恢复为 ubuntu runner。
本机验证:CI=true GITHUB_ACTIONS=true node scripts/tui-e2e.mjs → 49/49 通过。
同步订正 README/CLAUDE.md 里先前「ubuntu 渲染不出帧」的错误结论。
tmux 客户端环境过滤在 CI 的 tmux 3.4 上没能阻止 CI 标记进入 pane,改为在启动命令上
显式 `env -u CI -u CONTINUOUS_INTEGRATION`(不依赖 tmux 的 server 环境语义)。
同时临时加 4 组对照探针(我们的应用/最小 Ink × 默认/剥离)与面板尺寸,一次定位。
CI(ubuntu, tmux 3.4)复验 45/49,剩 4 条均为环境/时序特性:
- Tab/Enter 后焦点尚未提交时就把文本送进了上一个字段 → 改为等待焦点标记
  (▶ Service Name / ▶ Command / ▶ 参数行)再输入,场景在慢 runner 上确定化;
- ubuntu runner 没有 pbcopy/xclip,应用正确地报「未找到剪贴板工具」→ 剪贴板两条断言
  接受「已复制」或「明确提示无工具」两种正确结果(复制链路本身由集成测试覆盖)。

同时删除临时诊断步骤(已证实根因是 Ink 的 CI 模式:CI=true 时只在退出时画最后一帧)。
CI(慢 runner)上失败集合每轮不同(T4 覆盖确认、T10 分页、T12 保存、T13 存档),
典型症状是「下一次按键打到旧焦点上」:连续 send-keys 之间没有让应用重渲染,
Down/Down 会并成一次,Tab 后的文本会落进上一个字段。

- sendText/sendKey 改为异步并在每次按键后留出 settle 间隔(默认 140ms,
  TUI_E2E_KEY_DELAY 可覆盖;CI 任务显式设为 250ms)
- T10 在 Down/Down 之后等待选中项真的落到 big_output 再 Ctrl+R
- 本机(含 CI=true 模拟)复验 49/49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TUI] 工具详情页支持参数填写与运行,并提升参数/描述可读性

1 participant