# Snow CLI 使用文档——自定义 StatusLine 指南

## 概述

Snow CLI 支持从用户目录加载自定义 StatusLine 插件。你只需要把一个或多个 JavaScript 文件放到 `~/.snow/plugin/statusline/`，Snow CLI 启动时就会自动加载。

适合这些场景：

- 显示你自己的环境状态
- 显示项目特定提示
- 添加时间、目录、分支、服务、本机状态等信息
- 按简体中文、繁体中文、英语切换状态文本
- 用自己的实现覆盖内置 StatusLine 插件

## 插件目录

当前 Snow CLI 只会从这里加载 StatusLine 插件：

```bash
~/.snow/plugin/statusline/
```

支持的文件扩展名：

- `.js`
- `.mjs`
- `.cjs`

说明：

- 目前只支持用户目录插件
- Snow CLI 会按文件名排序后加载插件文件
- 新增、修改或删除插件文件后会自动热重载，无需重启 Snow CLI

## 支持的导出形式

一个插件模块可以使用以下任意一种导出形式：

```js
export default { ... }
```

```js
export const statusLineHook = { ... }
```

```js
export const statusLineHooks = [{ ... }, { ... }]
```

如果多个插件使用相同的 hook `id`，后加载的插件会覆盖先加载的插件。

## Hook 结构

每个 StatusLine hook 都使用下面这种结构：

```js
export default {
	id: 'custom.example',
	refreshIntervalMs: 60000,
	getItems(context) {
		return {
			id: 'custom-example-item',
			text: 'Hello',
			detailedText: '来自自定义状态栏的问候',
			color: 'cyan',
			priority: 200,
		};
	},
};
```

字段说明：

- `id`：hook 的唯一标识，用于合并和覆盖
- `refreshIntervalMs`：可选，刷新间隔，单位毫秒；系统最小生效值为 1000 ms
- `enable`：可选，是否启用该 hook，默认为 `true`，设为 `false` 可临时禁用
- `getItems(context)`：返回一个状态项、多个状态项，或 `undefined`

`getItems` 支持返回：

- 单个对象
- 对象数组
- `undefined` 或 `null`，表示本次不显示
- `async getItems()` 的异步返回

## 状态项字段

每个渲染项支持这些字段：

- `id`：可选，状态项 id；如果不传，Snow CLI 会自动补全
- `text`：简洁模式下显示的短文本
- `detailedText`：普通模式下优先显示的详细文本；没有则回退到 `text`
- `color`：可选，Ink 颜色字符串或十六进制颜色
- `gradient`：可选，渐变色数组，包含两个或更多十六进制颜色或命名颜色（如 `['#10B981', '#60A5FA']`）。提供时文本会逐字符渲染为渐变色；未提供或无效时回退到 `color` 单色渲染
- `priority`：可选，排序优先级；值越小越靠前

### 渐变色说明

终端不支持像素级渐变，Snow CLI 通过逐字符插值模拟渐变效果。提供 `gradient` 数组后，Snow CLI 会按文本字符数自动生成线性插值色序列，每个字符对应一个颜色。

推荐使用 2 到 5 个颜色：

- 2 色：左右双色拼接，适合极短文本
- 3-4 色：过渡自然的渐变，推荐用于 6-15 字符的文本
- 5 色：上限，文本足够长时效果最佳

`gradient` 优先级高于 `color`：同时提供两者时使用 `gradient`，`gradient` 无效时回退到 `color`。

## context 对象

`getItems(context)` 会收到下面这个上下文对象：

```js
{
	cwd: '/absolute/current/working/directory',
	platform: 'darwin',
	language: 'zh',
	simpleMode: false,
	labels: {
		gitBranch: 'Git分支',
	},
	system: {
		memory: {
			usageMb: 186,
			formattedUsage: '186 MB',
		},
		modes: {
			yolo: false,
			plan: true,
			vulnerabilityHunting: false,
			toolSearchEnabled: true,
			hybridCompress: false,
			team: false,
			ultraTodo: false,
			telemetry: true,
			simple: false,
		},
		ide: {
			connectionStatus: 'connected',
			editorContext: {
				activeFile: '/path/to/file.ts',
				selectedText: 'const answer = 42;',
				cursorPosition: {line: 10, character: 5},
				workspaceFolder: '/path/to/workspace',
			},
			selectedTextLength: 18,
		},
		backend: {
			connectionStatus: 'connected',
			instanceName: 'default',
		},
		contextWindow: {
			inputTokens: 18234,
			maxContextTokens: 128000,
			cacheCreationTokens: 2048,
			cacheReadTokens: 8192,
			percentage: 22.3,
			totalInputTokens: 28474,
			hasAnthropicCache: true,
			hasOpenAICache: false,
			hasAnyCache: true,
		},
		codebase: {
			indexing: true,
			progress: {
				totalFiles: 100,
				processedFiles: 42,
				totalChunks: 320,
				currentFile: 'source/app.ts',
				status: 'indexing',
			},
		},
		watcher: {
			enabled: true,
			fileUpdateNotification: {
				file: 'source/app.ts',
				timestamp: 1710000000000,
			},
		},
		clipboard: {
			text: '已复制输入内容',
			isError: false,
			timestamp: 1710000000000,
		},
		privacy: {
			configured: true,
			enabled: true,
			mode: 'api',
			apiUrlConfigured: true,
			apiUrl: 'https://privacy.example.com/v1/filter',
			model: 'openai/privacy-filter',
			toolResultTools: ['filesystem-read', 'terminal-execute'],
		},
		profile: {
			currentName: 'default',
			baseUrl: 'https://api.openai.com/v1',
			requestMethod: 'chat',
			advancedModel: 'gpt-4o',
			basicModel: 'gpt-4o-mini',
			maxContextTokens: 128000,
			maxTokens: 4096,
			anthropicBeta: false,
			anthropicCacheTTL: '5m',
			thinkingEnabled: false,
			thinkingType: 'adaptive',
			thinkingBudgetTokens: 4096,
			thinkingEffort: 'medium',
			geminiThinkingEnabled: false,
			geminiThinkingLevel: 'high',
			responsesReasoningEnabled: false,
			responsesReasoningEffort: 'medium',
			chatThinkingEnabled: false,
			chatReasoningEffort: 'high',
			responsesFastMode: false,
			responsesVerbosity: 'medium',
			anthropicSpeed: 'standard',
			enablePromptOptimization: true,
			enableAutoCompress: true,
			autoCompressThreshold: 80,
			showThinking: true,
			streamIdleTimeoutSec: 180,
			systemPromptId: ['default'],
			customHeadersSchemeId: 'default',
			toolResultTokenLimit: 100000,
			streamingDisplay: false,
		},
		compression: {
			blockToast: null,
		},
		speedometer: {
			enabled: true,
			tps: 42,
			peakTps: 58,
			ttftMs: 1234,
		},
	},
}
```

字段说明：

- `cwd`：当前 Snow CLI 工作目录
- `platform`：当前 Node.js 平台值，例如 `darwin`、`linux`、`win32`
- `language`：当前 Snow CLI 语言，可能是 `en`、`zh`、`zh-TW`
- `simpleMode`：当前是否为简洁主题模式
- `labels`：内置插件可复用的本地化标签
- `system`：当前状态栏可直接复用的系统状态快照

`system` 下可用字段：

- `system.memory`：当前 Snow CLI 进程内存，包含 `usageMb` 和 `formattedUsage`
- `system.modes`：当前模式状态，包含 `yolo`、`plan`、`vulnerabilityHunting`、`toolSearchEnabled`、`hybridCompress`、`team`、`ultraTodo`、`telemetry`、`simple`
- `system.ide`：IDE 连接状态，包含 `connectionStatus`、`editorContext`、`selectedTextLength`
- `system.backend`：后端连接状态，包含 `connectionStatus`、`instanceName`
- `system.contextWindow`：上下文窗口状态；存在时包含 token 统计、缓存命中以及 `percentage`、`totalInputTokens`
- `system.codebase`：代码库索引状态，包含 `indexing` 和 `progress`
- `system.watcher`：文件监视器状态，包含 `enabled` 和 `fileUpdateNotification`
- `system.clipboard`：最近一次复制提示，包含 `text`、`isError`、`timestamp`
- `system.privacy`：隐私过滤配置快照，包含 `configured`、`enabled`、`mode`、`apiUrlConfigured`、`apiUrl`、`model`、`toolResultTools`。该状态只提供给插件使用，Snow CLI 本体默认不会在 StatusLine 中渲染隐私状态。
  - `apiUrlConfigured` 是兼容性布尔字段，仅表示是否配置了 API 端点。
  - `apiUrl` 是实际的隐私过滤 API 请求地址；仅当 `mode` 为 `api` 且端点非空时返回，本地模式或未配置地址时为 `undefined`。
- `system.profile`：当前 Profile 完整配置信息，包含 `currentName`、`baseUrl`、`requestMethod`、`advancedModel`、`basicModel`、`maxContextTokens`、`maxTokens`、`anthropicBeta`、`anthropicCacheTTL`、`thinkingEnabled`、`thinkingType`、`thinkingBudgetTokens`、`thinkingEffort`、`geminiThinkingEnabled`、`geminiThinkingLevel`、`responsesReasoningEnabled`、`responsesReasoningEffort`、`chatThinkingEnabled`、`chatReasoningEffort`、`responsesFastMode`、`responsesVerbosity`、`anthropicSpeed`、`enablePromptOptimization`、`enableAutoCompress`、`autoCompressThreshold`、`showThinking`、`streamIdleTimeoutSec`、`systemPromptId`、`customHeadersSchemeId`、`toolResultTokenLimit`、`streamingDisplay`（不含 `apiKey`）

  其中思考 / 推理相关字段与 `requestMethod` 的对应关系：

  - `anthropic`：`thinkingEnabled` / `thinkingType`（`'enabled'` 或 `'adaptive'`） / `thinkingEffort` / `thinkingBudgetTokens`
  - `gemini`：`geminiThinkingEnabled` / `geminiThinkingLevel`
  - `responses`：`responsesReasoningEnabled` / `responsesReasoningEffort`
  - `chat`（DeepSeek 等 OpenAI Chat Completions 兼容接口）：`chatThinkingEnabled` / `chatReasoningEffort`

- `system.compression`：自动压缩提示，包含 `blockToast`
- `system.speedometer`：实时测速仪状态，包含 `enabled`（是否启用）、`tps`（当前 token/s）、`peakTps`（峰值 token/s）、`ttftMs`（首字延迟，即 Time To First Token，单位毫秒；`null` 表示当前会话尚未收到首字）。仅当 `/speedometer` 命令启用时 `enabled` 为 `true`。

## 示例 1：真实可用的时钟插件

现在你的用户目录里已经有一个真实可用的插件文件：

````bash
~/.snow/plugin/statusline/example-clock.js


内容如下：

```js
const messages = {
	en: {
		label: 'Current Time',
		directory: 'Directory',
	},
	zh: {
		label: '当前时间',
		directory: '目录',
	},
	'zh-TW': {
		label: '當前時間',
		directory: '目錄',
	},
};

export default {
	id: 'custom.example-clock',
	refreshIntervalMs: 60_000,
	getItems(context) {
		const now = new Date();
		const hours = String(now.getHours()).padStart(2, '0');
		const minutes = String(now.getMinutes()).padStart(2, '0');
		const clock = `${hours}:${minutes}`;
		const message = messages[context.language] || messages.en;

		return {
			id: 'custom-example-clock',
			text: `◷ ${clock}`,
			detailedText: `◷ ${message.label}: ${clock} · ${message.directory}: ${context.cwd}`,
			color: '#A78BFA',
			priority: 200,
		};
	},
};
````

## 示例 2：显示当前目录名

```js
import path from 'node:path';

export default {
	id: 'custom.cwd-name',
	refreshIntervalMs: 5000,
	getItems(context) {
		const folderName = path.basename(context.cwd);
		return {
			text: `DIR ${folderName}`,
			detailedText: `当前目录名: ${folderName}`,
			color: 'green',
			priority: 150,
		};
	},
};
```

## 示例 3：使用系统状态

```js
export default {
	id: 'custom.system-status',
	refreshIntervalMs: 3000,
	getItems(context) {
		const items = [];

		if (context.system.ide.connectionStatus === 'connected') {
			const activeFile = context.system.ide.editorContext?.activeFile;
			items.push({
				id: 'custom-system-ide',
				text: activeFile ? 'IDE ON' : 'IDE READY',
				detailedText: activeFile
					? `IDE 已连接 · 当前文件: ${activeFile}`
					: 'IDE 已连接',
				color: '#22C55E',
				priority: 120,
			});
		}

		if (context.system.contextWindow) {
			items.push({
				id: 'custom-system-context',
				text: `CTX ${context.system.contextWindow.percentage.toFixed(1)}%`,
				detailedText: `上下文已使用 ${context.system.contextWindow.totalInputTokens} tokens`,
				color: 'cyan',
				priority: 130,
			});
		}

		items.push({
			id: 'custom-system-memory',
			text: `MEM ${context.system.memory.formattedUsage}`,
			detailedText: `当前内存占用: ${context.system.memory.formattedUsage}`,
			color: 'yellow',
			priority: 140,
		});

		return items;
	},
};
```

## 示例 4：一次返回多个状态

```js
export default {
	id: 'custom.multi-status',
	refreshIntervalMs: 30000,
	getItems() {
		const now = new Date();
		return [
			{
				text: `T ${String(now.getHours()).padStart(2, '0')}:${String(
					now.getMinutes(),
				).padStart(2, '0')}`,
				color: 'cyan',
				priority: 100,
			},
			{
				text: 'ENV DEV',
				detailedText: '运行环境: Development',
				color: 'yellow',
				priority: 110,
			},
		];
	},
};
```

## 示例 5：渐变色状态

通过 `gradient` 字段可以为状态项设置渐变色。Snow CLI 会根据文本字符数自动插值生成逐字符颜色序列：

```js
export default {
	id: 'custom.gradient-status',
	refreshIntervalMs: 5000,
	getItems(context) {
		const items = [];

		// 双色渐变：从绿到蓝
		items.push({
			id: 'gradient-green-blue',
			text: `⚑ ${context.system.profile?.currentName || 'default'}`,
			detailedText: `当前 Profile: ${
				context.system.profile?.currentName || 'default'
			}`,
			gradient: ['#10B981', '#60A5FA'],
			priority: 100,
		});

		// 三色渐变：紫到粉到橙
		items.push({
			id: 'gradient-purple-pink-orange',
			text: '★ PREMIUM',
			detailedText: '高级会员状态',
			gradient: ['#A78BFA', '#F472B6', '#FB923C'],
			priority: 110,
		});

		return items;
	},
};
```

## 内置 Git 分支插件示例

Snow CLI 已经内置了一个 Git 分支 StatusLine 插件。

参考实现：

- `source/ui/components/common/statusline/gitBranch.ts`

这个内置 hook：

- hook id 是 `builtin.git-branch`
- 每 10 秒刷新一次
- 从 `context.cwd` 读取当前 Git 分支
- 分别输出短文本和详细文本

如果你写一个相同 hook id 的插件，就可以覆盖内置 Git 分支行为。

## 内置 Hook 列表（可被覆盖的 id）

除 `builtin.git-branch` 外，Snow CLI 还为其他内置状态项预留了稳定 hook id。
只要你的插件返回一个相同 id 的 hook，对应的内置状态项就会被你的实现替换：

| Hook ID                      | 默认渲染内容                                    | 触发条件                                                                       |
| ---------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ |
| `builtin.profile`            | `§ {profileName}`                               | 存在当前 Profile                                                               |
| `builtin.mode-yolo`          | `⧴ YOLO`                                        | YOLO 模式开启                                                                  |
| `builtin.mode-plan`          | `⚐ Plan`                                        | Plan 模式开启                                                                  |
| `builtin.mode-hunt`          | `⍨ Vuln Hunt`                                   | 漏洞挖掘模式开启                                                               |
| `builtin.mode-team`          | `⚑ Team`                                        | Team 模式开启                                                                  |
| `builtin.mode-ultra-todo`    | `◈ Ultra TODO`                                  | Ultra TODO 模式开启                                                            |
| `builtin.tool-search`        | `♾︎ ToolSearch ON`                              | 工具按需搜索启用                                                               |
| `builtin.hybrid-compress`    | `⇌ Hybrid Compress`                             | 混合压缩开启                                                                   |
| `builtin.telemetry`          | `⌁ OTel`                                        | 遥测开启                                                                       |
| `builtin.privacy`            | 无默认渲染，仅提供给插件使用                    | 隐私配置存在或未配置均可读取；API 模式配置地址时会暴露 `system.privacy.apiUrl` |
| `builtin.ide-connection`     | `◐/●/○ IDE`                                     | VSCode 连接状态非 disconnected                                                 |
| `builtin.backend-connection` | `◐/↻/● Backend`                                 | 后端连接状态非 disconnected                                                    |
| `builtin.codebase-indexing`  | `◐ 索引 {processed}/{total}` 或错误提示         | 正在索引或索引出错                                                             |
| `builtin.watcher`            | `☉ 文件监视`                                    | 监视器启用且未在索引                                                           |
| `builtin.file-update`        | `⛁ 已更新`                                      | 收到文件更新通知                                                               |
| `builtin.copy-status`        | 复制成功 / 失败提示文案                         | 收到剪贴板提示                                                                 |
| `builtin.compress-block`     | 自动压缩被中断提示文案                          | 自动压缩被阻断                                                                 |
| `builtin.memory`             | `⛁ {memoryUsage}`                               | 始终显示当前进程内存                                                           |
| `builtin.speedometer`        | `⏱ {tps} tok/s · peak {peakTps} · ttft {ttft}s` | 测速仪已启用（通过 `/speedometer` 命令开启）                                   |
| `builtin.git-branch`         | `⑂ {branch}`                                    | 当前目录在 Git 仓库中                                                          |

注意事项：

- 一旦插件以相同 id 注册了 hook，Snow CLI 就会**完全**跳过对应内置项的硬编码渲染。
  这意味着原本的徽章、图标、颜色、阈值等全部交由你的 hook 控制。
- 内置项的"是否显示"条件（例如 YOLO 模式是否开启）由 Snow CLI 主程序决定。
  插件可以通过 `context.system.modes`、`context.system.ide`、`context.system.contextWindow` 等字段读取相同的状态信息，自行决定是否返回内容。
- 覆盖 `builtin.memory` 后默认的"⛁ 232 MB"将不再渲染，请确保你的 hook 能合理展示内存信息（可以读取 `context.system.memory.usageMb`）。

## 覆盖内置插件示例

```js
export default {
	id: 'builtin.git-branch',
	refreshIntervalMs: 15000,
	async getItems(context) {
		return {
			text: '⑂ custom-branch',
			detailedText: `⑂ 自定义 Git 分支 (${context.cwd})`,
			color: 'magenta',
			priority: 100,
		};
	},
};
```

## 错误处理

如果某个插件出错：

- Snow CLI 会跳过这次刷新结果
- 错误会写入 Snow CLI 日志
- 其他插件仍会继续执行

常见问题：

- 文件不在 `~/.snow/plugin/statusline/`

- 扩展名不受支持
- 导出的值不是合法 hook 对象
- `text` 缺失或为空
- 插件代码运行时报错

## 最佳实践

- 保持 `getItems()` 足够轻量
- 设置合理刷新间隔
- 不需要显示时返回 `undefined`
- 使用稳定的 `id` 方便排序和覆盖
- 普通模式优先写 `detailedText`，简洁模式写 `text`
- 修改插件文件后记得重启 Snow CLI

## 故障排查

### 插件没有显示

请检查：

1. 文件路径是否为 `~/.snow/plugin/statusline/*.js`

2. 是否已经重启 Snow CLI
3. 导出格式是否正确
4. `text` 是否为空
5. 插件执行时是否抛错

### 状态顺序不对

检查 `priority`：

- 数值越小越靠前
- 数值越大越靠后

### 为什么没有覆盖内置 Git 分支

请确认 hook `id` 完全一致：

```js
id: 'builtin.git-branch';
```

## 相关文件

- `source/ui/components/common/statusline/useStatusLineHooks.ts`
- `source/ui/components/common/statusline/types.ts`
- `source/ui/components/common/statusline/gitBranch.ts`
- `~/.snow/plugin/statusline/example-clock.js`
