# Snow CLI User Guide - Custom StatusLine ## Overview Snow CLI supports loading custom StatusLine plugins from your user directory. You can place one or more JavaScript files in `~/.snow/plugin/statusline/`, and Snow CLI will load them on startup. Use this feature when you want to: - Show your own environment status - Display project-specific hints - Add time, directory, branch, service, or local machine indicators - Switch status text by Simplified Chinese, Traditional Chinese, and English - Override a built-in StatusLine plugin with your own implementation ## Plugin Directory Snow CLI currently loads StatusLine plugins from: ```bash ~/.snow/plugin/statusline/ ``` Supported file extensions: - `.js` - `.mjs` - `.cjs` Notes: - Plugins are loaded from the user directory only - Snow CLI sorts plugin files by filename before loading - Adding, modifying, or deleting plugin files hot-reloads automatically — no Snow CLI restart required ## Export Formats A plugin module can export in any of these forms: ```js export default { ... } ``` ```js export const statusLineHook = { ... } ``` ```js export const statusLineHooks = [{ ... }, { ... }] ``` If multiple plugins use the same hook `id`, the later loaded plugin overrides the earlier one. ## Hook Structure Each StatusLine hook uses this structure: ```js export default { id: 'custom.example', refreshIntervalMs: 60000, getItems(context) { return { id: 'custom-example-item', text: 'Hello', detailedText: 'Hello from custom status line', color: 'cyan', priority: 200, }; }, }; ``` Field description: - `id`: unique hook id, used for merging and override behavior - `refreshIntervalMs`: optional refresh interval in milliseconds; minimum effective interval is 1000 ms - `enable`: optional, whether to enable this hook, defaults to `true`, set to `false` to temporarily disable - `getItems(context)`: returns one item, multiple items, or `undefined` The `getItems` result supports: - single item object - array of item objects - `undefined` or `null` to render nothing - async return values via `async getItems()` ## Render Item Fields Each render item supports the following fields: - `id`: optional item id; Snow CLI auto-generates one if omitted - `text`: short text used in simple mode - `detailedText`: optional text used in normal mode; falls back to `text` - `color`: optional Ink color string or hex color - `gradient`: optional gradient color array, containing two or more hex colors or named colors (e.g. `['#10B981', '#60A5FA']`). When provided, the text is rendered character-by-character with interpolated gradient colors; falls back to `color` when not provided or invalid - `priority`: optional sort priority; lower values render first ### Gradient Colors Terminals do not support pixel-level gradients. Snow CLI simulates gradients by interpolating colors character by character. When you provide a `gradient` array, Snow CLI generates a linear interpolation color sequence based on the text length, assigning one color per character. Using 2 to 5 colors is recommended: - 2 colors: left-right two-tone, suitable for very short text - 3-4 colors: natural gradient transition, recommended for 6-15 character text - 5 colors: the upper limit, best for longer text `gradient` takes priority over `color`: when both are provided, `gradient` is used; if `gradient` is invalid, it falls back to `color`. ## Context Object `getItems(context)` receives this context object: ```js { cwd: '/absolute/current/working/directory', platform: 'darwin', language: 'en', simpleMode: false, labels: { gitBranch: 'Git Branch', }, 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: 'Input copied', 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', responsesServiceTier: 'fast', 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, }, }, } ``` Field description: - `cwd`: current Snow CLI working directory - `platform`: current Node.js platform value, such as `darwin`, `linux`, `win32` - `language`: current Snow CLI language, one of `en`, `zh`, `zh-TW` - `simpleMode`: whether Snow CLI is in simple theme mode - `labels`: localized labels that built-in plugins may reuse - `system`: a ready-to-use snapshot of current StatusLine system state Available fields under `system`: - `system.memory`: current Snow CLI process memory, including `usageMb` and `formattedUsage` - `system.modes`: current mode flags, including `yolo`, `plan`, `vulnerabilityHunting`, `toolSearchEnabled`, `hybridCompress`, `team`, `ultraTodo`, `telemetry`, `simple` - `system.ide`: IDE connection state, including `connectionStatus`, `editorContext`, `selectedTextLength` - `system.backend`: backend connection state, including `connectionStatus`, `instanceName` - `system.contextWindow`: context window state; when present it includes token metrics, cache metrics, `percentage`, and `totalInputTokens` - `system.codebase`: codebase indexing state, including `indexing` and `progress` - `system.watcher`: file watcher state, including `enabled` and `fileUpdateNotification` - `system.clipboard`: most recent copy feedback, including `text`, `isError`, `timestamp` - `system.privacy`: privacy filter configuration snapshot, including `configured`, `enabled`, `mode`, `apiUrlConfigured`, `apiUrl`, `model`, and `toolResultTools`. This state is exposed only for plugins; Snow CLI itself does not render a default Privacy item in StatusLine. - `apiUrlConfigured` is a compatibility boolean that only indicates whether an API endpoint is configured. - `apiUrl` is the actual privacy filter API request URL; it is returned only when `mode` is `api` and the endpoint is non-empty. In `local` mode or without an endpoint, it is `undefined`. - `system.profile`: current profile full configuration, including `currentName`, `baseUrl`, `requestMethod`, `advancedModel`, `basicModel`, `maxContextTokens`, `maxTokens`, `anthropicBeta`, `anthropicCacheTTL`, `thinkingEnabled`, `thinkingType`, `thinkingBudgetTokens`, `thinkingEffort`, `geminiThinkingEnabled`, `geminiThinkingLevel`, `responsesReasoningEnabled`, `responsesReasoningEffort`, `chatThinkingEnabled`, `chatReasoningEffort`, `responsesServiceTier`, `responsesVerbosity`, `anthropicSpeed`, `enablePromptOptimization`, `enableAutoCompress`, `autoCompressThreshold`, `showThinking`, `streamIdleTimeoutSec`, `systemPromptId`, `customHeadersSchemeId`, `toolResultTokenLimit`, `streamingDisplay` (excluding `apiKey`) How thinking / reasoning fields map to `requestMethod`: - `anthropic`: `thinkingEnabled` / `thinkingType` (`'enabled'` or `'adaptive'`) / `thinkingEffort` / `thinkingBudgetTokens` - `gemini`: `geminiThinkingEnabled` / `geminiThinkingLevel` - `responses`: `responsesReasoningEnabled` / `responsesReasoningEffort` - `chat` (OpenAI Chat Completions compatible APIs such as DeepSeek): `chatThinkingEnabled` / `chatReasoningEffort` - `system.compression`: auto-compression state, including `blockToast` - `system.speedometer`: real-time speedometer state, including `enabled` (whether enabled), `tps` (current tokens/s), `peakTps` (peak tokens/s), `ttftMs` (Time To First Token in milliseconds; `null` means no first token received yet in the current session). `enabled` is `true` only when the `/speedometer` command is active. ## Example 1: Real Clock Plugin A real working example file already exists in your user directory: ````bash ~/.snow/plugin/statusline/example-clock.js Its content is: ```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, }; }, }; ```` ## Example 2: Show Current Directory Name ```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: `Current Folder: ${folderName}`, color: 'green', priority: 150, }; }, }; ``` ## Example 3: Use System State ```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 connected · Active file: ${activeFile}` : 'IDE connected', color: '#22C55E', priority: 120, }); } if (context.system.contextWindow) { items.push({ id: 'custom-system-context', text: `CTX ${context.system.contextWindow.percentage.toFixed(1)}%`, detailedText: `Context used: ${context.system.contextWindow.totalInputTokens} tokens`, color: 'cyan', priority: 130, }); } items.push({ id: 'custom-system-memory', text: `MEM ${context.system.memory.formattedUsage}`, detailedText: `Current memory usage: ${context.system.memory.formattedUsage}`, color: 'yellow', priority: 140, }); return items; }, }; ``` ## Example 4: Return Multiple Status Items ```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: 'Environment: Development', color: 'yellow', priority: 110, }, ]; }, }; ``` ## Example 5: Gradient Colors Use the `gradient` field to apply gradient colors to a status item. Snow CLI automatically interpolates a per-character color sequence based on the text length: ```js export default { id: 'custom.gradient-status', refreshIntervalMs: 5000, getItems(context) { const items = []; // Two-color gradient: green to blue items.push({ id: 'gradient-green-blue', text: `⚑ ${context.system.profile?.currentName || 'default'}`, detailedText: `Current Profile: ${ context.system.profile?.currentName || 'default' }`, gradient: ['#10B981', '#60A5FA'], priority: 100, }); // Three-color gradient: purple to pink to orange items.push({ id: 'gradient-purple-pink-orange', text: '★ PREMIUM', detailedText: 'Premium membership status', gradient: ['#A78BFA', '#F472B6', '#FB923C'], priority: 110, }); return items; }, }; ``` ## Built-in Git Branch Example Snow CLI already includes a built-in Git branch StatusLine plugin. Reference implementation: - `source/ui/components/common/statusline/gitBranch.ts` This built-in hook: - uses hook id `builtin.git-branch` - refreshes every 10 seconds - reads the current Git branch from `context.cwd` - renders short and detailed text separately If you create another plugin with the same hook id, you can override the built-in behavior. ## Built-in Hook IDs (Overridable) In addition to `builtin.git-branch`, Snow CLI reserves stable hook ids for all other built-in status items. If your plugin registers a hook with one of these ids, Snow CLI will skip the hard-coded rendering for that item and use your hook's output instead: | Hook ID | Default Rendering | Trigger Condition | | ---------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `builtin.profile` | `§ {profileName}` | A current profile is active | | `builtin.mode-yolo` | `⧴ YOLO` | YOLO mode is enabled | | `builtin.mode-plan` | `⚐ Plan` | Plan mode is enabled | | `builtin.mode-hunt` | `⍨ Vuln Hunt` | Vulnerability hunting mode is enabled | | `builtin.mode-team` | `⚑ Team` | Team mode is enabled | | `builtin.mode-ultra-todo` | `◈ Ultra TODO` | Ultra TODO mode is enabled | | `builtin.tool-search` | `♾︎ ToolSearch ON` | On-demand tool search is enabled | | `builtin.hybrid-compress` | `⇌ Hybrid Compress` | Hybrid compression is enabled | | `builtin.telemetry` | `⌁ OTel` | Telemetry is enabled | | `builtin.privacy` | No default rendering; plugin-only | Readable whether privacy is configured; API mode exposes `system.privacy.apiUrl` when configured | | `builtin.ide-connection` | `◐/●/○ IDE` | VSCode connection is not disconnected | | `builtin.backend-connection` | `◐/↻/● Backend` | Backend connection is not disconnected | | `builtin.codebase-indexing` | `◐ Indexing {processed}/{total}` or error | Indexing is running or has errored | | `builtin.watcher` | `☉ Watcher` | Watcher is enabled and not indexing | | `builtin.file-update` | `⛁ Updated` | A file update notification arrived | | `builtin.copy-status` | Clipboard success / failure toast | A clipboard message exists | | `builtin.compress-block` | Auto-compression block toast | Auto compression was blocked | | `builtin.memory` | `⛁ {memoryUsage}` | Always rendered | | `builtin.speedometer` | `⏱ {tps} tok/s · peak {peakTps} · ttft {ttft}s` | Speedometer is enabled (via `/speedometer` command) | | `builtin.git-branch` | `⑂ {branch}` | The current directory is a Git repo | Notes: - Once a plugin registers a hook with the same id, Snow CLI completely skips the corresponding built-in render. Icons, colors, thresholds, etc. are then fully controlled by your hook. - Whether the built-in item is visible at all (e.g. whether YOLO mode is on) is still determined by Snow CLI. Your plugin can read the same flags via `context.system.modes`, `context.system.ide`, `context.system.contextWindow`, etc. to decide what to return. - Overriding `builtin.memory` removes the default `⛁ 232 MB` block, so make sure your hook renders memory information (you can read `context.system.memory.usageMb`). ## Override Example ```js export default { id: 'builtin.git-branch', refreshIntervalMs: 15000, async getItems(context) { return { text: '⑂ custom-branch', detailedText: `⑂ Custom Git Branch (${context.cwd})`, color: 'magenta', priority: 100, }; }, }; ``` ## Error Handling If a plugin fails: - Snow CLI skips the broken result for that refresh cycle - the error is written to the Snow CLI log - other plugins continue to run Common problems: - file is not in `~/.snow/plugin/statusline/` - file extension is not supported - exported value is not a valid hook object - `text` is missing or empty - plugin code throws at runtime ## Best Practices - Keep `getItems()` fast and lightweight - Use a reasonable refresh interval - Return `undefined` when the status should be hidden - Use stable `id` values for predictable ordering and override behavior - Prefer `detailedText` for verbose mode and `text` for compact mode - Plugin file changes hot-reload automatically — no restart needed ## Troubleshooting ### Plugin does not appear Check: 1. File path is `~/.snow/plugin/statusline/*.js` 2. Wait a moment for hot-reload (or save the file again) 3. Export format is valid 4. `text` is not empty 5. The plugin does not throw errors during execution ### Status order is unexpected Check `priority` values. - smaller number = earlier render - larger number = later render ### My plugin does not override built-in Git branch Make sure the hook `id` exactly matches: ```js id: 'builtin.git-branch'; ``` ## Related Files - `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`