关于 create-agent-tui
create-agent-tui 是一项用于生成 TypeScript Agent 终端界面的技能,面向从头搭建 Agent 的开发者。它通过可选工具、模块和显示样式来定制项目,并以 OpenRouter 作为目标模型服务。
主要功能
- 01生成 TypeScript Agent TUI 项目
- 02按清单选择工具与架构模块
- 03配置会话、权限和日志功能
- 04定制输入、工具显示与加载动画
- 05提供 OpenRouter 模型切换命令
适用场景
- 01从零创建终端 Agent 项目
- 02为现有 Agent 添加工具
- 03为 Agent 增加会话保存或审批
- 04定制 Agent 的终端交互方式
MCP 相关
README 提及可扩展的 MCP 接入点,但未说明具体的 MCP 配置或能力。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openrouterteam/ skills --skill create-agent-tui安装后在 Claude Code 中输入/create-agent-tui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.jsonREADME.mdreferences/input-styles.mdloader.mdmodules.mdserver-entry-points.mdslash-commands.mdsystem-prompt.mdtool-display.mdtools.mdtui.mdsample/.gitignorepackage-lock.jsonpackage.jsonsample/src/agent.tsbanner.tscli.tscommands.tsconfig.tsloader.tsrenderer.tsscreenshot-demos.tssession.tsterminal-bg.tssample/src/tools/custom.tsfile-edit.tsfile-read.tsfile-write.tsglob.tsgrep.ts详细介绍
创建 Agent TUI
搭建一个使用 TypeScript 编写、面向 OpenRouter 的完整 Agent TUI。生成的项目使用 @openrouter/agent 处理内层循环(模型调用、工具执行、停止条件),并提供外层框架:可自定义的终端界面、配置、会话管理、工具定义和入口点。
架构借鉴了三个生产级 Agent 系统:
- pi-mono/coding-agent — 三层分离、JSONL 会话、可插拔的工具操作
- Claude Code — 工具元数据(只读、破坏性操作、审批)、系统提示词组合
- Codex CLI — 分层配置、带会话缓存的审批流程、结构化日志记录
先决条件
- Node.js 18+
- 从 openrouter.ai/settings/keys 获取的
OPENROUTER_API_KEY - 如需完整的 SDK 参考,请参阅
openrouter-typescript-sdk技能
决策树
| 用户想要…… | 操作 |
|---|---|
| 从头构建新的 Agent | 展示下方清单 → 按照生成工作流操作 |
| 为现有框架添加工具 | 阅读 references/tools.md,仅展示工具清单 |
| 添加框架模块 | 阅读 references/modules.md,生成该模块 |
| 添加 API 服务入口点 | 阅读 references/server-entry-points.md |
交互式工具清单
将此清单作为多选列表展示。标记为 ON 的项目为预选默认项。
OpenRouter 服务端工具(服务端执行,无需实现)
| 工具 | 类型字符串 | 默认值 | 配置 |
|---|---|---|---|
| 网页搜索 | openrouter:web_search | ON | engine、max_results、域名过滤 |
| 日期时间 | openrouter:datetime | ON | timezone |
| 图像生成 | openrouter:image_generation | OFF | model、quality、size、format |
服务端工具与用户定义的工具一起放入 tools 数组。无需客户端代码 — OpenRouter 会执行这些工具。
用户定义的工具(客户端执行,生成到 src/tools/)
| 工具 | 默认值 | 描述 |
|---|---|---|
| 文件读取 | ON | 读取文件,支持 offset/limit,检测图像 |
| 文件写入 | ON | 写入/创建文件,自动创建目录 |
| 文件编辑 | ON | 搜索并替换,带差异验证 |
| Glob/查找 | ON | 按 glob 模式查找文件 |
| Grep/搜索 | ON | 使用正则表达式搜索内容 |
| 目录列表 | ON | 列出目录内容 |
| Shell/Bash | ON | 执行命令,支持超时和输出捕获 |
| JS REPL | OFF | 持久化 Node.js 环境 |
| 子 Agent 生成 | OFF | 将任务委派给子 Agent |
| 计划/待办 | OFF | 跟踪多步骤任务进度 |
| 请求用户输入 | OFF | 结构化多选问题 |
| 网页抓取 | OFF | 获取网页并提取文本 |
| 查看图像 | OFF | 将本地图像读取为 base64 |
| 自定义工具模板 | ON | 用于特定领域工具的空白骨架 |
框架模块(架构组件)
| 模块 | 默认值 | 描述 |
|---|---|---|
| 会话持久化 | ON | 仅追加的 JSONL 对话日志 |
| ASCII Logo 横幅 | OFF | 启动时显示自定义 ASCII 艺术横幅 — 询问项目名称 |
| 上下文压缩 | OFF | 上下文较长时总结较早的消息 |
| 系统提示词组合 | OFF | 从静态和动态上下文组装指令 |
| 工具权限/审批 | OFF | 对危险工具的使用要求用户确认 |
| 结构化事件日志记录 | OFF | 为工具调用、API 请求和错误生成事件 |
@-文件引用 | OFF | 使用 @filename 将文件内容附加到下一条消息 |
! Shell 快捷方式 | OFF | 使用 !command 运行 Shell,并将输出注入上下文 |
| 多行输入 | OFF | 使用 Shift+Enter 输入多行内容(需要原始终端模式) |
斜杠命令(面向用户的 REPL 命令)
| 命令 | 默认值 | 描述 |
|---|---|---|
/model | ON | 通过 OpenRouter API 切换模型 |
/new | ON | 开始新的对话 |
/help | ON | 列出可用命令 |
/compact | OFF | 手动压缩上下文 |
/session | OFF | 显示会话元数据和令牌用量 |
/export | OFF | 将对话保存为 Markdown |
启用斜杠命令后,生成带有命令注册表的 src/commands.ts。规范请参阅 references/slash-commands.md。
视觉自定义(每项均为单选)
输入样式 — 提示词的外观。请参阅 references/input-styles.md:
| 样式 | 默认值 | 描述 |
|---|---|---|
block | ON | 全宽背景框,带有 › 提示符,并适配终端主题 |
bordered | 输入区域上下带有水平 ─ 线 | |
plain | 简单的 > readline 提示符,不使用转义序列 | |
| Other | 用户描述所需样式 — 实现自定义输入样式 |
工具显示 — 执行期间工具调用的显示方式。请参阅 references/tool-display.md:
| 样式 | 默认值 | 描述 |
|---|---|---|
grouped | ON | 粗体操作标签与树枝状输出 |
emoji | 每次调用使用 ⚡/✓ 标记,并显示参数和耗时 | |
minimal | 汇总为单行摘要 | |
hidden | 不显示工具输出 | |
| Other | 用户描述所需样式 — 实现自定义显示方式 |
加载动画 — 等待模型响应时显示。请参阅 references/loader.md:
| 样式 | 默认值 | 描述 |
|---|---|---|
spinner | ON | 文本左侧显示 Braille 点状旋转动画(⠋⠙⠹…) |
gradient | 加载文本上滚动显示彩色渐变光泽 | |
minimal | 末尾显示点号(Working···) | |
| Other | 用户描述所需样式 — 实现自定义动画 |
还要询问加载文本(默认值:"Working")。
生成工作流
获取清单选项后,按以下工作流操作:
- [ ] Generate package.json with dependencies
- [ ] Generate src/config.ts (add showBanner field if ASCII Logo Banner is ON)
- [ ] Generate src/tools/index.ts wiring selected tools + server tools
- [ ] Generate selected tool files in src/tools/ (see Tool Pattern below, specs in references/tools.md)
- [ ] Generate src/agent.ts (core runner)
- [ ] Generate selected harness modules (specs in references/modules.md)
- [ ] Generate src/terminal-bg.ts (adaptive input background — see references/tui.md)
- [ ] Generate input style functions in src/cli.ts (block/bordered/plain — see references/input-styles.md)
- [ ] Generate src/renderer.ts (tool display — see references/tool-display.md)
- [ ] Generate src/loader.ts (loader animation — see references/loader.md)
- [ ] If slash commands selected: generate src/commands.ts (see references/slash-commands.md)
- [ ] If ASCII Logo Banner is ON: generate src/banner.ts (see ASCII Logo Banner section below)
- [ ] Generate src/cli.ts entry point (or src/server.ts — see references/server-entry-points.md)
- [ ] Generate .env.example with OPENROUTER_API_KEY=
- [ ] Verify: run npx tsc --noEmit to check types
工具模式
所有用户自定义工具都遵循此模式,并使用 @openrouter/agent/tool。以下是一个完整示例 — references/tools.md 中的所有其他工具也遵循相同结构:
import { tool } from '@openrouter/agent/tool';
import { z } from 'zod';
import { readFile, stat } from 'fs/promises';
const DEFAULT_LINE_LIMIT = 2000;
const MAX_LINE_CHARS = 2000;
export const fileReadTool = tool({
name: 'file_read',
description:
'Read the contents of a file. Output is capped at 2000 lines by default (use offset/limit to paginate) and any line longer than 2000 characters is truncated. When the response is truncated, the hint field tells you how to continue.',
inputSchema: z.object({
path: z.string().describe('Absolute path to the file'),
offset: z.number().optional().describe('Start reading from this line (1-indexed)'),
limit: z.number().optional().describe(`Maximum lines to return (default ${DEFAULT_LINE_LIMIT})`),
}),
execute: async ({ path, offset, limit }) => {
try {
const content = await readFile(path, 'utf-8');
const lines = content.split('\n');
const start = offset ? offset - 1 : 0;
const end = Math.min(start + (limit ?? DEFAULT_LINE_LIMIT), lines.length);
let longLines = 0;
const slice = lines.slice(start, end).map((line) => {
if (line.length <= MAX_LINE_CHARS) return line;
longLines++;
return line.slice(0, MAX_LINE_CHARS) + `… [line truncated, ${line.length - MAX_LINE_CHARS} chars dropped]`;
});
const tailTruncated = end < lines.length;
const truncated = tailTruncated || longLines > 0;
const hintParts: string[] = [`Showing lines ${start + 1}-${end} of ${lines.length}.`];
if (tailTruncated) hintParts.push(`Use offset=${end + 1} to continue.`);
if (longLines > 0) hintParts.push(`${longLines} line(s) exceeded ${MAX_LINE_CHARS} chars and were per-line truncated; use grep to fetch content from those lines.`);
return {
content: slice.join('\n'),
totalLines: lines.length,
...(truncated && {
truncated: true,
...(tailTruncated && { nextOffset: end + 1 }),
hint: hintParts.join(' '),
}),
};
} catch (err: any) {
if (err.code === 'ENOENT') return { error: `File not found: ${path}` };
if (err.code === 'EACCES') return { error: `Permission denied: ${path}` };
return { error: err.message };
}
},
});
有关所有其他工具的规范,请参阅 references/tools.md。
核心文件
以下文件始终会生成。Agent 会根据清单选项对其进行调整。
package.json
初始化项目并安装最新版本的依赖项:
npm init -y
npm pkg set type=module
npm pkg set scripts.start="tsx src/cli.ts"
npm pkg set scripts.dev="tsx watch src/cli.ts"
npm install @openrouter/agent glob zod
npm install -D tsx typescript @types/node
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
src/config.ts
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
export interface DisplayConfig {
toolDisplay: 'emoji' | 'grouped' | 'minimal' | 'hidden';
reasoning: boolean;
inputStyle: 'block' | 'bordered' | 'plain';
}
export interface AgentConfig {
apiKey: string;
model: string;
systemPrompt: string;
maxSteps: number;
maxCost: number;
sessionDir: string;
showBanner: boolean;
display: DisplayConfig;
slashCommands: boolean;
}
const DEFAULTS: AgentConfig = {
apiKey: '',
model: 'anthropic/claude-opus-4.7',
systemPrompt: [
'You are a coding assistant with access to tools for reading, writing, editing, and searching files, and running shell commands.',
'',
'Current working directory: {cwd}',
'',
'Guidelines:',
'- Use your tools proactively. Explore the codebase to find answers instead of asking the user.',
'- Keep working until the task is fully resolved before responding.',
'- Do not guess or make up information — use your tools to verify.',
'- Be concise and direct.',
'- Show file paths clearly when working with files.',
'- Prefer grep and glob tools over shell commands for file search.',
'- When editing code, make minimal targeted changes consistent with the existing style.',
].join('\n'),
maxSteps: 20,
maxCost: 1.0,
sessionDir: '.sessions',
showBanner: false,
display: { toolDisplay: 'grouped', reasoning: false, inputStyle: 'block' },
slashCommands: true,
};
export function loadConfig(overrides: Partial<AgentConfig> = {}): AgentConfig {
let config = { ...DEFAULTS };
const configPath = resolve('agent.config.json');
if (existsSync(configPath)) {
const file = JSON.parse(readFileSync(configPath, 'utf-8'));
if (file.display) {
config.display = { ...config.display, ...file.display };
}
config = { ...config, ...file, display: config.display };
}
if (process.env.OPENROUTER_API_KEY) config.apiKey = process.env.OPENROUTER_API_KEY;
if (process.env.AGENT_MODEL) config.model = process.env.AGENT_MODEL;
if (process.env.AGENT_MAX_STEPS) config.maxSteps = Number(process.env.AGENT_MAX_STEPS);
if (process.env.AGENT_MAX_COST) config.maxCost = Number(process.env.AGENT_MAX_COST);
if (overrides.display) {
config.display = { ...config.display, ...overrides.display };
}
config = { ...config, ...overrides, display: config.display };
if (!config.apiKey) throw new Error('OPENROUTER_API_KEY is required.');
return config;
}
src/tools/index.ts
根据清单选项调整导入内容。此示例包含所有默认启用的工具:
import { serverTool } from '@openrouter/agent';
import { fileReadTool } from './file-read.js';
import { fileWriteTool } from './file-write.js';
import { fileEditTool } from './file-edit.js';
import { globTool } from './glob.js';
import { grepTool } from './grep.js';
import { listDirTool } from './list-dir.js';
import { shellTool } from './shell.js';
export const tools = [
// User-defined tools — executed client-side
fileReadTool,
fileWriteTool,
fileEditTool,
globTool,
grepTool,
listDirTool,
shellTool,
// Server tools — executed by OpenRouter, no client implementation needed
serverTool({ type: 'openrouter:web_search' }),
serverTool({ type: 'openrouter:datetime', parameters: { timezone: 'UTC' } }),
];
src/agent.ts
import { OpenRouter } from '@openrouter/agent';
import type { Item } from '@openrouter/agent';
import { stepCountIs, maxCost } from '@openrouter/agent/stop-conditions';
import type { AgentConfig } from './config.js';
import { tools } from './tools/index.js';
export type ChatMessage = { role: 'user' | 'assistant' | 'system'; content: string };
export type AgentEvent =
| { type: 'text'; delta: string }
| { type: 'tool_call'; name: string; callId: string; args: Record<string, unknown> }
| { type: 'tool_result'; name: string; callId: string; output: string }
| { type: 'reasoning'; delta: string };
export async function runAgent(
config: AgentConfig,
input: string | ChatMessage[],
options?: { onEvent?: (event: AgentEvent) => void; signal?: AbortSignal },
) {
const client = new OpenRouter({ apiKey: config.apiKey });
const result = client.callModel({
model: config.model,
instructions: config.systemPrompt.replace('{cwd}', process.cwd()),
input: input as string | Item[],
tools,
stopWhen: [stepCountIs(config.maxSteps), maxCost(config.maxCost)],
});
if (options?.onEvent) {
// Track text length PER message item by id. A multi-step agent emits
// multiple OutputMessage items over the course of a single run (one per
// assistant turn between tool calls), and each one grows from 0 to its
// final length. A single global cursor breaks on the second message:
// when its length is smaller than the cursor from the first, the slice
// cuts mid-string and drops the start of the new message's text.
const textByItem = new Map<string, number>();
const callNames = new Map<string, string>();
for await (const item of result.getItemsStream()) {
if (options?.signal?.aborted) break;
if (item.type === 'message') {
const text = item.content
?.filter((c): c is { type: 'output_text'; text: string } => 'text' in c)
.map((c) => c.text)
.join('') ?? '';
const prev = textByItem.get(item.id) ?? 0;
if (text.length > prev) {
options.onEvent({ type: 'text', delta: text.slice(prev) });
textByItem.set(item.id, text.length);
}
} else if (item.type === 'function_call') {
callNames.set(item.callId, item.name);
if (item.status === 'completed') {
const args = (() => { try { return item.arguments ? JSON.parse(item.arguments) : {}; } catch { return {}; } })();
options.onEvent({ type: 'tool_call', name: item.name, callId: item.callId, args });
}
} else if (item.type === 'function_call_output') {
const out = typeof item.output === 'string' ? item.output : JSON.stringify(item.output);
options.onEvent({
type: 'tool_result',
name: callNames.get(item.callId) ?? 'unknown',
callId: item.callId,
output: out.length > 200 ? out.slice(0, 200) + '…' : out,
});
} else if (item.type === 'reasoning') {
const text = item.summary?.map((s: { text: string }) => s.text).join('') ?? '';
if (text) options.onEvent({ type: 'reasoning', delta: text });
}
}
}
const response = await result.getResponse();
return { text: response.outputText ?? '', usage: response.usage, output: response.output };
}
export async function runAgentWithRetry(
config: AgentConfig,
input: string | ChatMessage[],
options?: { onEvent?: (event: AgentEvent) => void; signal?: AbortSignal; maxRetries?: number },
) {
for (let attempt = 0, max = options?.maxRetries ?? 3; attempt <= max; attempt++) {
try { return await runAgent(config, input, options); }
catch (err: any) {
const s = err?.status ?? err?.statusCode;
if (!(s === 429 || (s >= 500 && s < 600)) || attempt === max) throw err;
await new Promise((r) => setTimeout(r, Math.min(1000 * 2 ** attempt, 30000)));
}
}
throw new Error('Unreachable');
}
src/cli.ts
支持三种输入样式:block(背景框)、bordered(水平线)和 plain(简单插入符)。完整实现请参阅 references/input-styles.md 中的 styledReadLine()、borderedReadLine() 和 getInput() 分派器。
import { createInterface } from 'readline';
import { loadConfig } from './config.js';
import { runAgentWithRetry, type AgentEvent } from './agent.js';
import { detectBg } from './terminal-bg.js';
// import { styledReadLine, borderedReadLine } from ... — see references/input-styles.md
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const GRAY = '\x1b[90m';
function formatTokens(n: number): string {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
}
function summarizeArgs(name: string, args: Record<string, unknown>): string {
const key = { shell: 'command', file_read: 'path', file_write: 'path',
file_edit: 'path', glob: 'pattern', grep: 'pattern', web_search: 'query',
}[name] ?? Object.keys(args)[0];
if (!key || !(key in args)) return '';
const val = String(args[key]);
return `${key}=${val.length > 40 ? val.slice(0, 40) + '…' : val}`;
}
async function main() {
const config = loadConfig();
const BG_INPUT = config.display.inputStyle === 'block' ? await detectBg() : '';
// Banner
const width = Math.min(process.stdout.columns || 60, 60);
const line = GRAY + '─'.repeat(width) + RESET;
console.log(`\n${line}`);
console.log(` ${BOLD}My Agent${RESET} ${DIM}v0.1.0${RESET}`);
console.log(` ${DIM}model${RESET} ${CYAN}${config.model}${RESET}`);
if (config.slashCommands) console.log(` ${DIM}/model to change${RESET}`);
console.log(`${line}\n`);
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: `${GREEN}>${RESET} ` });
async function getInput(): Promise<string> {
switch (config.display.inputStyle) {
case 'block': return styledReadLine(BG_INPUT);
case 'bordered': return borderedReadLine();
case 'plain':
default:
return new Promise((r) => { rl.prompt(); rl.once('line', r); });
}
}
while (true) {
const input = await getInput();
const trimmed = input.trim();
if (!trimmed) continue;
if (config.display.inputStyle !== 'plain') {
const cwd = process.cwd().replace(process.env.HOME ?? '', '~');
process.stdout.write(`\x1b[K ${DIM}${cwd}${RESET}\n`);
}
if (trimmed.toLowerCase() === 'exit') { process.exit(0); }
console.log();
let streaming = false, started = false;
const toolStart = new Map<string, number>();
const dots = ['·', '··', '···'];
let di = 0;
const spin = setInterval(() => {
if (!started) process.stdout.write(`\r${DIM}${dots[di++ % 3]}${RESET}`);
}, 300);
const handleEvent = (event: AgentEvent) => {
if (!started) { started = true; process.stdout.write('\r\x1b[K'); }
if (event.type === 'text') { streaming = true; process.stdout.write(event.delta); }
else if (event.type === 'tool_call') {
if (streaming) { process.stdout.write('\n'); streaming = false; }
toolStart.set(event.callId, Date.now());
const args = summarizeArgs(event.name, event.args);
console.log(` ${YELLOW}⚡${RESET} ${DIM}${event.name}${args ? ' ' + args : ''}${RESET}`);
} else if (event.type === 'tool_result') {
const ms = Date.now() - (toolStart.get(event.callId) ?? Date.now());
console.log(` ${GREEN}✓${RESET} ${DIM}${event.name} (${(ms / 1000).toFixed(1)}s)${RESET}`);
started = false;
}
};
try {
const result = await runAgentWithRetry(config, trimmed, { onEvent: handleEvent });
clearInterval(spin);
if (streaming) process.stdout.write(RESET);
const inT = result.usage?.inputTokens ?? 0;
const outT = result.usage?.outputTokens ?? 0;
console.log(`\n${GRAY} ${formatTokens(inT)} in · ${formatTokens(outT)} out${RESET}\n`);
} catch (err: any) {
clearInterval(spin);
if (streaming) process.stdout.write(RESET);
console.log(`\n${YELLOW} Error: ${err.message}${RESET}\n`);
}
}
}
main();
ASCII Logo 横幅
选择 ASCII Logo Banner 后,询问用户的项目名称,然后生成包含该名称 ASCII 艺术字的 src/banner.ts。使用 █ 字符绘制块状字母样式。横幅应适合 60 列宽的终端。
src/banner.ts
为用户的项目名称生成 ASCII 艺术字。以下是项目名称为“ACME”时的示例:
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const CYAN = '\x1b[36m';
const LOGO = `
█████╗ ██████╗███╗ ███╗███████╗
██╔══██╗██╔════╝████╗ ████║██╔════╝
███████║██║ ██╔████╔██║█████╗
██╔══██║██║ ██║╚██╔╝██║██╔══╝
██║ ██║╚██████╗██║ ╚═╝ ██║███████╗
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝`;
export function printBanner(model: string): void {
console.log(CYAN + BOLD + LOGO + RESET);
console.log(` ${DIM}model ${RESET}${model}\n`);
}
根据用户的实际项目名称调整 ASCII 艺术字。保持名称为一到两个简短单词,并确保不超过 60 列。
接入 src/cli.ts
选择 showBanner 后,在 main() 开头的文本横幅之前添加:
import { printBanner } from './banner.js';
// In main(), replace the text banner with:
if (config.showBanner) {
printBanner(config.model);
} else {
// fall back to the text banner from the cli.ts template above
}
向 AgentConfig 添加 showBanner: boolean(默认值为 false)。通过 agent.config.json 或 loadConfig({ showBanner: true }) 启用。
参考文件
如需查阅核心文件之外的内容:
- references/tools.md — 所有用户自定义工具的规格:file-read、file-write、file-edit、glob、grep、list-dir、shell、js-repl、sub-agent、plan、request-input、web-fetch、view-image、custom template
- references/modules.md — Harness 模块:会话持久化、上下文压缩、系统提示词组合、工具审批、结构化日志记录
- references/tui.md — 终端后台检测、自适应输入背景
- references/tool-display.md — 工具显示样式:emoji、grouped、minimal;TuiRenderer 类、各工具的颜色、格式化器
- references/input-styles.md — 输入样式:block(背景框)、bordered(水平线)、plain(简单插入符)
- references/loader.md — 加载动画:gradient(滚动闪光)、spinner(盲文点)、minimal(尾随点)
- references/slash-commands.md — 斜杠命令注册表:/model、/new、/help、/compact、/session、/export
- references/system-prompt.md — 默认系统提示词、buildSystemPrompt()、自定义指南
- references/server-entry-points.md — 支持 SSE 流式传输的 Express/Hono API 服务入口,以及扩展点(MCP、WebSocket、动态模型)