diff --git a/docs/users/features/status-line.md b/docs/users/features/status-line.md index 79b8c81f2..9ef227217 100644 --- a/docs/users/features/status-line.md +++ b/docs/users/features/status-line.md @@ -40,7 +40,7 @@ Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`: "ui": { "statusLine": { "type": "command", - "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.id'); tokens=$(echo \"$input\" | jq -r '.context_window.last_prompt_token_count'); echo \"$model ctx:$tokens\"", + "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model ctx:${pct}%\"", "padding": 0 } } @@ -60,13 +60,45 @@ The command receives a JSON object via stdin with the following fields: ```json { "session_id": "abc-123", - "cwd": "/home/user/project", + "version": "0.14.1", "model": { - "id": "qwen-3-235b" + "display_name": "qwen-3-235b" }, "context_window": { "context_window_size": 131072, - "last_prompt_token_count": 45000 + "used_percentage": 34.3, + "remaining_percentage": 65.7, + "current_usage": 45000, + "total_input_tokens": 30000, + "total_output_tokens": 5000 + }, + "workspace": { + "current_dir": "/home/user/project" + }, + "git": { + "branch": "main" + }, + "metrics": { + "models": { + "qwen-3-235b": { + "api": { + "total_requests": 10, + "total_errors": 0, + "total_latency_ms": 5000 + }, + "tokens": { + "prompt": 30000, + "completion": 5000, + "total": 35000, + "cached": 10000, + "thoughts": 2000 + } + } + }, + "files": { + "total_lines_added": 120, + "total_lines_removed": 30 + } }, "vim": { "mode": "INSERT" @@ -74,14 +106,24 @@ The command receives a JSON object via stdin with the following fields: } ``` -| Field | Type | Description | -| ---------------------------------------- | ---------------- | ---------------------------------------------------------------------------------- | -| `session_id` | string | Unique session identifier | -| `cwd` | string | Current working directory | -| `model.id` | string | Current model identifier | -| `context_window.context_window_size` | number | Total context window size | -| `context_window.last_prompt_token_count` | number | Tokens used in the last prompt | -| `vim` | object \| absent | Present only when vim mode is enabled. Contains `mode` (`"INSERT"` or `"NORMAL"`). | +| Field | Type | Description | +| ------------------------------------- | ---------------- | ---------------------------------------------------------------------------------- | +| `session_id` | string | Unique session identifier | +| `version` | string | Qwen Code version | +| `model.display_name` | string | Current model name | +| `context_window.context_window_size` | number | Total context window size in tokens | +| `context_window.used_percentage` | number | Context window usage as percentage (0–100) | +| `context_window.remaining_percentage` | number | Context window remaining as percentage (0–100) | +| `context_window.current_usage` | number | Token count from the last API call (current context size) | +| `context_window.total_input_tokens` | number | Total input tokens consumed this session | +| `context_window.total_output_tokens` | number | Total output tokens consumed this session | +| `workspace.current_dir` | string | Current working directory | +| `git` | object \| absent | Present only inside a git repository. | +| `git.branch` | string | Current branch name | +| `metrics.models..api` | object | Per-model API stats: `total_requests`, `total_errors`, `total_latency_ms` | +| `metrics.models..tokens` | object | Per-model token usage: `prompt`, `completion`, `total`, `cached`, `thoughts` | +| `metrics.files` | object | File change stats: `total_lines_added`, `total_lines_removed` | +| `vim` | object \| absent | Present only when vim mode is enabled. Contains `mode` (`"INSERT"` or `"NORMAL"`). | > **Important:** stdin can only be read once. Always store it in a variable first: `input=$(cat)`. @@ -94,7 +136,7 @@ The command receives a JSON object via stdin with the following fields: "ui": { "statusLine": { "type": "command", - "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.id'); tokens=$(echo \"$input\" | jq -r '.context_window.last_prompt_token_count'); size=$(echo \"$input\" | jq -r '.context_window.context_window_size'); pct=$((tokens * 100 / (size > 0 ? size : 1))); echo \"$model ctx:${pct}%\"" + "command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model ctx:${pct}%\"" } } } @@ -109,7 +151,7 @@ Output: `qwen-3-235b ctx:34%` "ui": { "statusLine": { "type": "command", - "command": "branch=$(git branch --show-current 2>/dev/null); dir=$(basename \"$PWD\"); echo \"$dir${branch:+ ($branch)}\"" + "command": "input=$(cat); branch=$(echo \"$input\" | jq -r '.git.branch // empty'); dir=$(basename \"$(echo \"$input\" | jq -r '.workspace.current_dir')\"); echo \"$dir${branch:+ ($branch)}\"" } } } @@ -117,7 +159,22 @@ Output: `qwen-3-235b ctx:34%` Output: `my-project (main)` -> Note: `git` and `pwd` run in the workspace directory automatically. +> Note: The `git.branch` field is provided directly in the JSON input — no need to shell out to `git`. + +### File change stats + +```json +{ + "ui": { + "statusLine": { + "type": "command", + "command": "input=$(cat); added=$(echo \"$input\" | jq -r '.metrics.files.total_lines_added'); removed=$(echo \"$input\" | jq -r '.metrics.files.total_lines_removed'); echo \"+$added/-$removed lines\"" + } + } +} +``` + +Output: `+120/-30 lines` ### Script file for complex commands @@ -126,18 +183,17 @@ For longer commands, save a script file at `~/.qwen/statusline-command.sh`: ```bash #!/bin/bash input=$(cat) -model=$(echo "$input" | jq -r '.model.id') -tokens=$(echo "$input" | jq -r '.context_window.last_prompt_token_count') -size=$(echo "$input" | jq -r '.context_window.context_window_size') -branch=$(git branch --show-current 2>/dev/null) +model=$(echo "$input" | jq -r '.model.display_name') +pct=$(echo "$input" | jq -r '.context_window.used_percentage') +branch=$(echo "$input" | jq -r '.git.branch // empty') +added=$(echo "$input" | jq -r '.metrics.files.total_lines_added') +removed=$(echo "$input" | jq -r '.metrics.files.total_lines_removed') parts=() [ -n "$model" ] && parts+=("$model") [ -n "$branch" ] && parts+=("($branch)") -if [ "$tokens" -gt 0 ] && [ "$size" -gt 0 ] 2>/dev/null; then - pct=$((tokens * 100 / size)) - parts+=("ctx:${pct}%") -fi +[ "$pct" != "0" ] 2>/dev/null && parts+=("ctx:${pct}%") +([ "$added" -gt 0 ] || [ "$removed" -gt 0 ]) 2>/dev/null && parts+=("+${added}/-${removed}") echo "${parts[*]}" ``` @@ -157,7 +213,7 @@ Then reference it in settings: ## Behavior -- **Update triggers**: The status line updates when the model changes, a new message is sent (token count changes), or vim mode is toggled. Updates are debounced (300ms). +- **Update triggers**: The status line updates when the model changes, a new message is sent (token count changes), vim mode is toggled, git branch changes, tool calls complete, or file changes occur. Updates are debounced (300ms). - **Timeout**: Commands that take longer than 5 seconds are killed. The status line clears on failure. - **Output**: Only the first line of stdout is used. The text is rendered with dimmed colors and truncated to terminal width. - **Hot reload**: Changes to `ui.statusLine` in settings take effect immediately — no restart required. @@ -166,9 +222,9 @@ Then reference it in settings: ## Troubleshooting -| Problem | Cause | Fix | -| ----------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Status line not showing | Config at wrong path | Must be under `ui.statusLine`, not root-level `statusLine` | -| Empty output | Command fails silently | Test manually: `echo '{"model":{"id":"test"},"cwd":"/tmp","context_window":{"context_window_size":1,"last_prompt_token_count":0}}' \| sh -c 'your_command'` | -| Stale data | No trigger fired | Send a message or switch models to trigger an update | -| Command too slow | Complex script | Optimize the script or move heavy work to a background cache | +| Problem | Cause | Fix | +| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Status line not showing | Config at wrong path | Must be under `ui.statusLine`, not root-level `statusLine` | +| Empty output | Command fails silently | Test manually: `echo '{"session_id":"test","version":"0.14.1","model":{"display_name":"test"},"context_window":{"context_window_size":0,"used_percentage":0,"remaining_percentage":100,"current_usage":0,"total_input_tokens":0,"total_output_tokens":0},"workspace":{"current_dir":"/tmp"},"metrics":{"models":{},"files":{"total_lines_added":0,"total_lines_removed":0}}}' \| sh -c 'your_command'` | +| Stale data | No trigger fired | Send a message or switch models to trigger an update | +| Command too slow | Complex script | Optimize the script or move heavy work to a background cache | diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 34c6af341..29ea1c756 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -287,7 +287,6 @@ export const AppContainer = (props: AppContainerProps) => { const { stats: sessionStats, startNewSession } = useSessionStats(); const logger = useLogger(config.storage, sessionStats.sessionId); const branchName = useGitBranchName(config.getTargetDir()); - // Layout measurements const mainControlsRef = useRef(null); const originalTitleRef = useRef( diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts index cec5686a3..1f5ede315 100644 --- a/packages/cli/src/ui/hooks/useStatusLine.ts +++ b/packages/cli/src/ui/hooks/useStatusLine.ts @@ -10,6 +10,7 @@ import { useSettings } from '../contexts/SettingsContext.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { useVimMode } from '../contexts/VimModeContext.js'; +import type { SessionMetrics } from '../contexts/SessionContext.js'; /** * Structured JSON input passed to the status line command via stdin. @@ -18,13 +19,46 @@ import { useVimMode } from '../contexts/VimModeContext.js'; */ export interface StatusLineCommandInput { session_id: string; - cwd: string; + version: string; model: { - id: string; + display_name: string; }; context_window: { context_window_size: number; - last_prompt_token_count: number; + used_percentage: number; + remaining_percentage: number; + current_usage: number; + total_input_tokens: number; + total_output_tokens: number; + }; + workspace: { + current_dir: string; + }; + git?: { + branch: string; + }; + metrics: { + models: Record< + string, + { + api: { + total_requests: number; + total_errors: number; + total_latency_ms: number; + }; + tokens: { + prompt: number; + completion: number; + total: number; + cached: number; + thoughts: number; + }; + } + >; + files: { + total_lines_added: number; + total_lines_removed: number; + }; }; vim?: { mode: string; @@ -65,6 +99,35 @@ function getStatusLineConfig( return undefined; } +function buildMetricsPayload( + m: SessionMetrics, +): StatusLineCommandInput['metrics'] { + const models: StatusLineCommandInput['metrics']['models'] = {}; + for (const [id, mm] of Object.entries(m.models)) { + models[id] = { + api: { + total_requests: mm.api.totalRequests, + total_errors: mm.api.totalErrors, + total_latency_ms: mm.api.totalLatencyMs, + }, + tokens: { + prompt: mm.tokens.prompt, + completion: mm.tokens.candidates, + total: mm.tokens.total, + cached: mm.tokens.cached, + thoughts: mm.tokens.thoughts, + }, + }; + } + return { + models, + files: { + total_lines_added: m.files.totalLinesAdded, + total_lines_removed: m.files.totalLinesRemoved, + }, + }; +} + /** * Hook that executes a user-configured shell command and returns its output * for display in the status line. The command receives structured JSON context @@ -109,16 +172,24 @@ export function useStatusLine(): { // Initialized with current values so the state-change effect // does not fire redundantly on mount. const { lastPromptTokenCount } = uiState.sessionStats; - const { currentModel } = uiState; + const { currentModel, branchName } = uiState; + const totalToolCalls = uiState.sessionStats.metrics.tools.totalCalls; + const totalLinesAdded = uiState.sessionStats.metrics.files.totalLinesAdded; const effectiveVim = vimEnabled ? vimMode : undefined; const prevStateRef = useRef<{ promptTokenCount: number; currentModel: string; effectiveVim: string | undefined; + branchName: string | undefined; + totalToolCalls: number; + totalLinesAdded: number; }>({ promptTokenCount: lastPromptTokenCount, currentModel, effectiveVim, + branchName, + totalToolCalls, + totalLinesAdded, }); // Guard: when true, the mount effect has already called doUpdate so the @@ -138,18 +209,52 @@ export function useStatusLine(): { const ui = uiStateRef.current; const cfg = configRef.current; + const stats = ui.sessionStats; + const m = stats.metrics; + + const contextWindowSize = + cfg.getContentGeneratorConfig()?.contextWindowSize || 0; + const usedPercentage = + contextWindowSize > 0 + ? Math.round((stats.lastPromptTokenCount / contextWindowSize) * 1000) / + 10 + : 0; + + let totalInputTokens = 0; + let totalOutputTokens = 0; + for (const mm of Object.values(m.models)) { + totalInputTokens += mm.tokens.prompt; + totalOutputTokens += mm.tokens.candidates; + } const input: StatusLineCommandInput = { - session_id: ui.sessionStats.sessionId, - cwd: cfg.getTargetDir(), + session_id: stats.sessionId, + version: cfg.getCliVersion() || 'unknown', model: { - id: ui.currentModel || cfg.getModel() || 'unknown', + display_name: ui.currentModel || cfg.getModel() || 'unknown', }, context_window: { - context_window_size: - cfg.getContentGeneratorConfig()?.contextWindowSize || 0, - last_prompt_token_count: ui.sessionStats.lastPromptTokenCount, + context_window_size: contextWindowSize, + used_percentage: usedPercentage, + remaining_percentage: + contextWindowSize > 0 + ? Math.round( + (1 - stats.lastPromptTokenCount / contextWindowSize) * 1000, + ) / 10 + : 100, + current_usage: stats.lastPromptTokenCount, + total_input_tokens: totalInputTokens, + total_output_tokens: totalOutputTokens, }, + workspace: { + current_dir: cfg.getTargetDir(), + }, + ...(ui.branchName && { + git: { + branch: ui.branchName, + }, + }), + metrics: buildMetricsPayload(m), ...(vimEnabledRef.current && { vim: { mode: vimModeRef.current ?? 'INSERT' }, }), @@ -220,11 +325,17 @@ export function useStatusLine(): { if ( lastPromptTokenCount !== prev.promptTokenCount || currentModel !== prev.currentModel || - effectiveVim !== prev.effectiveVim + effectiveVim !== prev.effectiveVim || + branchName !== prev.branchName || + totalToolCalls !== prev.totalToolCalls || + totalLinesAdded !== prev.totalLinesAdded ) { prev.promptTokenCount = lastPromptTokenCount; prev.currentModel = currentModel; prev.effectiveVim = effectiveVim; + prev.branchName = branchName; + prev.totalToolCalls = totalToolCalls; + prev.totalLinesAdded = totalLinesAdded; scheduleUpdate(); } }, [ @@ -232,6 +343,9 @@ export function useStatusLine(): { lastPromptTokenCount, currentModel, effectiveVim, + branchName, + totalToolCalls, + totalLinesAdded, scheduleUpdate, ]); diff --git a/packages/core/src/subagents/builtin-agents.ts b/packages/core/src/subagents/builtin-agents.ts index ed0110351..a97ca24eb 100644 --- a/packages/core/src/subagents/builtin-agents.ts +++ b/packages/core/src/subagents/builtin-agents.ts @@ -141,13 +141,34 @@ How to use the statusLine command: 1. The statusLine command will receive the following JSON input via stdin: { "session_id": "string", - "cwd": "string", + "version": "string", "model": { - "id": "string" + "display_name": "string" }, "context_window": { "context_window_size": number, - "last_prompt_token_count": number + "used_percentage": number, + "remaining_percentage": number, + "current_usage": number, + "total_input_tokens": number, + "total_output_tokens": number + }, + "workspace": { + "current_dir": "string" + }, + "git": { // Optional, only present when inside a git repo + "branch": "string" + }, + "metrics": { + "models": { + "": { + "api": { "total_requests": number, "total_errors": number, "total_latency_ms": number }, + "tokens": { "prompt": number, "completion": number, "total": number, "cached": number, "thoughts": number } + } + }, + "files": { + "total_lines_added": number, "total_lines_removed": number + } }, "vim": { // Optional, only present when vim mode is enabled "mode": "INSERT" | "NORMAL" @@ -155,10 +176,13 @@ How to use the statusLine command: } IMPORTANT: stdin can only be consumed once. Always read it into a variable first: - - input=$(cat); echo "$(echo "$input" | jq -r '.model.id') in $(echo "$input" | jq -r '.cwd')" + - input=$(cat); echo "$(echo "$input" | jq -r '.model.display_name') in $(echo "$input" | jq -r '.workspace.current_dir')" To display context usage: - - input=$(cat); tokens=$(echo "$input" | jq -r '.context_window.last_prompt_token_count'); size=$(echo "$input" | jq -r '.context_window.context_window_size'); [ "$tokens" -gt 0 ] 2>/dev/null && echo "Context: $((tokens * 100 / size))% used" + - input=$(cat); pct=$(echo "$input" | jq -r '.context_window.used_percentage'); echo "Context: $pct% used" + + To display git branch: + - input=$(cat); branch=$(echo "$input" | jq -r '.git.branch // empty'); echo "\${branch:-no branch}" 2. For longer commands, you can save a new file in the user's ~/.qwen directory, e.g.: - ~/.qwen/statusline-command.sh and reference that file in the settings.