mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
* feat(cli): support refreshInterval in statusLine for periodic refresh The statusLine (#3311) re-runs only when Agent state changes (token count, model, git branch, etc.). Commands that display *external* data — a clock, rate-limit counters, CI build status — have no Agent event to hook into and go stale between messages. Add an optional `ui.statusLine.refreshInterval` field (seconds, minimum 1) that schedules a setInterval alongside the existing event-driven updates. Overlap with state-change debounce is safe: `doUpdate` kills any in-flight child and bumps the generation counter, so only the most recent output reaches the footer. Validation lives in `getStatusLineConfig`: - Must be `number`, `Number.isFinite(...)`, `>= 1` - Anything else is silently dropped (no interval scheduled) No changes to the default behavior — configs without `refreshInterval` behave exactly as before. * fix(cli): yield periodic statusLine tick when previous exec is in flight Review feedback on #3383: with `refreshInterval: 1` and a command whose real exec time exceeds 1s, each tick was unconditionally calling `doUpdate()` — which kills the in-flight child and bumps the generation counter — so the prior exec's callback was always discarded as stale. `setOutput` was never reached and the statusline stayed empty until `refreshInterval` was removed or the command became faster. Guard the interval callback with an `activeChildRef` check so a pending exec is allowed to finish. State-change triggers (model switch, token count, branch, etc.) still go through `scheduleUpdate` → `doUpdate` directly and legitimately preempt stale children; only the periodic tick yields. The existing 5s exec timeout is still the hard ceiling. Also drop the redundant `'refreshInterval' in raw` check — the `typeof raw.refreshInterval === 'number'` guard already excludes missing / undefined values. Tests: - Add regression test `'skips periodic ticks while a previous exec is still running'` — three ticks during one unfinished exec trigger zero new spawns; the next tick after callback completion does spawn. - Update two existing tests to resolve the mount exec before expecting subsequent ticks (the old tests implicitly relied on the starvation behavior being tolerated). * test(cli): assert user-visible lines state in starvation regression Self-review insight: the existing `skips periodic ticks while a previous exec is still running` test only counted `exec` calls — it confirmed the guard prevents redundant spawns, but would have silently passed even if the eventual callback was still being discarded as stale (which is the actual user-visible symptom of the starvation bug). Add `expect(result.current.lines).toEqual(['done'])` after resolving the mount's pending callback. Without the guard, generationRef would have bumped 3 times during the yielded ticks, the callback's captured gen would fail the stale check, `setOutput` would never fire, and `lines` would stay empty — now caught explicitly. * perf(cli): dedupe statusLine output to skip unchanged Footer re-renders Review feedback on #3383 (narrow terminal stacking): when `refreshInterval` fires at 1s and the command output is unchanged, the mount-and-setOutput cycle still allocates a new array and triggers a Footer re-render. Under certain narrow-terminal conditions, Ink's erase-line accounting mis-counts wrapped rows and stale content accumulates on screen. The Footer-layout root cause is in #3311's narrow-mode flex setup and Ink's truncate semantics, which is out of scope for this PR. But we can cut the re-render surface here by preserving the `lines` array reference when the command produces identical output — a strict Pareto improvement for any caller (clock-style statuslines with second-precision still re-render; rate-limit / branch / CI-status style statuslines that change infrequently stop triggering work every tick). Tests: - `preserves the same lines array reference when output is unchanged` asserts referential equality after a re-exec with identical stdout. - `produces a new reference when output changes` guards against over-eager dedup that would miss legitimate updates. * fix(cli): stabilize Footer rendering in narrow terminals Narrow-terminal E2E feedback on #3383: with `refreshInterval` at 1s, empty lines were accumulating above the input prompt each tick. Root cause is in the Footer flex layout — originally from #3311 — where Ink miscounts logical rows vs the physical rows the terminal actually uses. Two adjustments, both idiomatic (used elsewhere in the repo already): 1. Left column — `minWidth={0}`. Without this, Yoga's `min-width: auto` default keeps the Box at its natural content width, so a statusline wider than the terminal doesn't engage `<Text wrap="truncate">`; the text renders at content-width and the terminal wraps it physically. `minWidth={0}` lets the column shrink so the text child can truncate at container width. 2. Right section — `flexWrap="wrap"`. With multiple indicators (sandbox label, debug badge, dream, context-usage) the row can exceed a narrow terminal's width. Without `flexWrap` Ink lays them out in a single logical row, but the terminal physically wraps to two — Ink's erase sequence (`\e[2K\e[1A…` per logical row) then clears one row while two exist, and the extra row ghosts every re-render. With `wrap` Ink tracks the second row explicitly and erases correctly. Together these make the Footer's row count match between Ink's logical view and the terminal's physical view, so frequent re-renders (as `refreshInterval` enables) stop accumulating ghost rows. Needs verification in a real narrow TTY — from this environment I can reason about the flex semantics and confirm both props are supported by Ink's Box, but actually observing ghost-row elimination requires process.stdout.columns on a real terminal. * Revert "fix(cli): stabilize Footer rendering in narrow terminals" This reverts commit9758cda85f. Reason: I could not reproduce BZ-D's reported ghost-row stacking in tmux (40x25, 2-line statusline + real exec + Static history + refreshInterval: 1) over 14+ ticks. Both `minWidth={0}` and `flexWrap="wrap"` are legitimate defensive idioms, but without a failing repro I can't verify they address the reported bug, and I shouldn't ship a speculative layout change as "the fix". Keeping the output-dedup commit (e1d321186) — that one is a strict improvement regardless of the underlying Ink behavior. Will request BZ-D's specific terminal setup and reopen with a verified fix (or confirm the issue is specific to a particular emulator, not flex/Ink).
261 lines
14 KiB
Markdown
261 lines
14 KiB
Markdown
# Status Line
|
||
|
||
> Display custom information in the footer using a shell command.
|
||
|
||
The status line lets you run a shell command whose output is displayed in the footer's left section. The command receives structured JSON context via stdin, so it can show session-aware information like the current model, token usage, git branch, or anything else you can script.
|
||
|
||
```
|
||
Single-line status (default approval mode — 1 row):
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ user@host ~/project (main) ctx:34% 🔒 docker | Debug | 67% │ ← status line
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
Multi-line status (up to 2 lines — 2 rows):
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ user@host ~/project (main) ctx:34% 🔒 docker | Debug | 67% │ ← status line 1
|
||
│ ████████░░░░░░░░░░ 34% context │ ← status line 2
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
Multi-line status + non-default mode (3 rows max):
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ user@host ~/project (main) ctx:34% 🔒 docker | Debug | 67% │ ← status line 1
|
||
│ ████████░░░░░░░░░░ 34% context │ ← status line 2
|
||
│ auto-accept edits (shift + tab to cycle) │ ← mode indicator
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
When configured, the status line replaces the default "? for shortcuts" hint. High-priority messages (Ctrl+C/D exit prompts, Esc, vim INSERT mode) temporarily override the status line. The status line text is truncated to fit within the available width.
|
||
|
||
## Prerequisites
|
||
|
||
- [`jq`](https://jqlang.github.io/jq/) is recommended for parsing the JSON input (install via `brew install jq`, `apt install jq`, etc.)
|
||
- Simple commands that don't need JSON data (e.g. `git branch --show-current`) work without `jq`
|
||
|
||
## Quick setup
|
||
|
||
The easiest way to configure a status line is the `/statusline` command. It launches a setup agent that reads your shell PS1 configuration and generates a matching status line:
|
||
|
||
```
|
||
/statusline
|
||
```
|
||
|
||
You can also give it specific instructions:
|
||
|
||
```
|
||
/statusline show model name and context usage percentage
|
||
```
|
||
|
||
## Manual configuration
|
||
|
||
Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"statusLine": {
|
||
"type": "command",
|
||
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model ctx:${pct}%\""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Description |
|
||
| ----------------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `type` | `"command"` | Yes | Must be `"command"` |
|
||
| `command` | string | Yes | Shell command to execute. Receives JSON via stdin, stdout is displayed (up to 2 lines). |
|
||
| `refreshInterval` | number | No | Re-run the command every N seconds (minimum 1). Useful for data that changes without an Agent state event (clock, quota, uptime). |
|
||
|
||
## JSON input
|
||
|
||
The command receives a JSON object via stdin with the following fields:
|
||
|
||
```json
|
||
{
|
||
"session_id": "abc-123",
|
||
"version": "0.14.1",
|
||
"model": {
|
||
"display_name": "qwen-3-235b"
|
||
},
|
||
"context_window": {
|
||
"context_window_size": 131072,
|
||
"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"
|
||
}
|
||
}
|
||
```
|
||
|
||
| 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.<id>.api` | object | Per-model API stats: `total_requests`, `total_errors`, `total_latency_ms` |
|
||
| `metrics.models.<id>.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)`.
|
||
|
||
## Examples
|
||
|
||
### Model and token usage
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"statusLine": {
|
||
"type": "command",
|
||
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); pct=$(echo \"$input\" | jq -r '.context_window.used_percentage'); echo \"$model ctx:${pct}%\""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Output: `qwen-3-235b ctx:34%`
|
||
|
||
### Git branch + directory
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"statusLine": {
|
||
"type": "command",
|
||
"command": "input=$(cat); branch=$(echo \"$input\" | jq -r '.git.branch // empty'); dir=$(basename \"$(echo \"$input\" | jq -r '.workspace.current_dir')\"); echo \"$dir${branch:+ ($branch)}\""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Output: `my-project (main)`
|
||
|
||
> 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`
|
||
|
||
### Live clock and git branch
|
||
|
||
Use `refreshInterval` when the statusline shows data that changes without an Agent event (e.g. the clock, uptime, or rate-limit counters):
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"statusLine": {
|
||
"type": "command",
|
||
"command": "input=$(cat); branch=$(echo \"$input\" | jq -r '.git.branch // \"no-git\"'); echo \"$(date +%H:%M:%S) ($branch)\"",
|
||
"refreshInterval": 1
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Output (refreshed every second): `14:32:07 (main)`
|
||
|
||
### Script file for complex commands
|
||
|
||
For longer commands, save a script file at `~/.qwen/statusline-command.sh`:
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
input=$(cat)
|
||
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)")
|
||
[ "$pct" != "0" ] 2>/dev/null && parts+=("ctx:${pct}%")
|
||
([ "$added" -gt 0 ] || [ "$removed" -gt 0 ]) 2>/dev/null && parts+=("+${added}/-${removed}")
|
||
|
||
echo "${parts[*]}"
|
||
```
|
||
|
||
Then reference it in settings:
|
||
|
||
```json
|
||
{
|
||
"ui": {
|
||
"statusLine": {
|
||
"type": "command",
|
||
"command": "bash ~/.qwen/statusline-command.sh"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Behavior
|
||
|
||
- **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). Set `refreshInterval` (seconds) to additionally re-run the command on a timer — useful for data that changes without an Agent event (clock, rate limits, build status).
|
||
- **Timeout**: Commands that take longer than 5 seconds are killed. The status line clears on failure.
|
||
- **Output**: Multi-line output is supported (up to 2 lines; extra lines are discarded). Each line is rendered as a separate row with dimmed colors in the footer's left section. Lines that exceed the available width are truncated.
|
||
- **Hot reload**: Changes to `ui.statusLine` in settings take effect immediately — no restart required.
|
||
- **Shell**: Commands run via `/bin/sh` on macOS/Linux. On Windows, `cmd.exe` is used by default — wrap POSIX commands with `bash -c "..."` or point to a bash script (e.g. `bash ~/.qwen/statusline-command.sh`).
|
||
- **Removal**: Delete the `ui.statusLine` key from settings to disable. The "? for shortcuts" hint returns.
|
||
|
||
## 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 '{"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 — or set `refreshInterval` to re-run the command on a timer |
|
||
| Command too slow | Complex script | Optimize the script or move heavy work to a background cache |
|