agent-Specialization/cli/src/commands.ts
JOJO cc9df7959a feat(cli): 新增 React/Ink CLI 端,修复全屏布局与光标定位
## 新增:基于 React/Ink 的 CLI 终端(cli/)

使用 Ink 框架构建完整的终端 UI,取代原有的纯 print 交互方式:

- App.tsx:主应用组件,管理对话状态、任务轮询、键盘输入、slash 命令
- components.tsx:Timeline、Composer、StatusLine、Picker、SlashMenu 等全部 UI 组件
- eventMapper.ts:将后端 SSE 事件流映射为 Timeline 条目,支持 thinking/tool/assistant/sub_agent 等类型
- api.ts:封装所有后端 REST 接口(任务、对话、模型、权限、工作区等)
- commands.ts:slash 命令定义(/new /resume /model /permission /execution /workspace /compact /tools 等)
- workspaces.ts:本地工作区目录的持久化管理
- types.ts:所有共享类型定义
- format.ts:context 格式化、路径缩写等工具函数

## 重大修复:去除固定高度全屏模式,恢复滚轮滚动

**问题根因**:根 Box 设置了 `height={stdout.rows}` 使 Ink 进入原地重绘模式
(类似 vim/htop),所有内容在同一块屏幕区域反复擦写,永远不会真正
"滚出"屏幕顶部,导致 terminal 的 scrollback buffer 始终为空,鼠标
滚轮无任何响应。

**修复方案**:
- 移除根 Box 的 `height` 约束和 `overflow="hidden"`
- 移除内层 Box 的 `flexGrow`/`justifyContent="flex-end"` 全屏布局
- Timeline 不再做行数截断,直接渲染全部条目
- 内容自然流入 terminal scrollback buffer,鼠标滚轮恢复正常

**同步清理**:删除 `getTimelineMaxRows`、`estimateTimelineRows`、
`scrollOffset` 状态及方向键滚动逻辑(这些都是全屏模式下的补丁,
去除固定高度后不再需要)

## 修复:Composer 光标位置偏移一行

`setCursorPosition` 中 y 轴使用 `lines.length` 导致单行时偏移 +1。
改为 `lines.length - 1`,与实际最后一行渲染位置对齐。

## 其他

- AGENTS.md / README.md:补充 CLI 启动方式与架构说明
- server/chat.py:补充任务轮询接口所需字段
- package.json:新增 cli 相关脚本入口
- docs/cli_slash_commands_spec.md、docs/cli_ui_display_spec.md:CLI 设计文档

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:06:51 +08:00

29 lines
1.7 KiB
TypeScript

import type { SlashCommand } from './types.js';
export const slashCommands: SlashCommand[] = [
{ name: '/new', description: '新建对话', kind: 'immediate' },
{ name: '/resume', description: '查看对话列表并切换对话', kind: 'picker' },
{ name: '/model', description: '切换模型与思考模式', kind: 'picker' },
{ name: '/versioning', description: '管理版本控制', aliases: ['/version'], kind: 'picker' },
{ name: '/permission', description: '切换权限模式', aliases: ['/perm'], kind: 'picker' },
{ name: '/execution', description: '切换执行环境', aliases: ['/exec', '/env'], kind: 'picker' },
{ name: '/workspace', description: '选择或切换工作区', kind: 'picker' },
{ name: '/compact', description: '压缩当前对话上下文', kind: 'immediate' },
{ name: '/tools', description: '启用或禁用工具', aliases: ['/tool'], kind: 'picker' },
{ name: '/path', description: '管理路径授权', kind: 'form' },
{ name: '/skill', description: '选择本轮要使用的 skill', kind: 'picker' },
{ name: '/status', description: '查看当前状态', kind: 'panel' },
{ name: '/help', description: '查看命令帮助', kind: 'panel' },
{ name: '/clear', description: '清空当前 CLI 显示', kind: 'immediate' },
{ name: '/exit', description: '退出 CLI', kind: 'immediate' },
];
export function filterCommands(query: string): SlashCommand[] {
const normalized = query.trim().toLowerCase();
if (!normalized || normalized === '/') return slashCommands;
return slashCommands.filter((command) => {
const candidates = [command.name, ...(command.aliases || [])];
return candidates.some((name) => name.toLowerCase().startsWith(normalized) || name.toLowerCase().includes(normalized));
});
}