Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6784f0c
feat(ui): add customizable status line
wenshao Apr 5, 2026
8d85492
feat(ui): rewrite customizable status line
wenshao Apr 6, 2026
959690b
fix: regenerate settings.schema.json via generate:settings-schema
wenshao Apr 6, 2026
be13adb
fix: add SettingsContext to Footer tests
wenshao Apr 6, 2026
c219f7c
fix: address review feedback from Copilot
wenshao Apr 6, 2026
24251db
fix: track vimEnabled changes in status line triggers
wenshao Apr 6, 2026
1a985bb
fix: exec cwd, output trimming, and status line alignment
wenshao Apr 6, 2026
4c4e638
fix: kill child process when statusLine config is removed
wenshao Apr 6, 2026
9bba05b
fix: add ASK_USER_QUESTION to statusline-setup agent, clear debounce …
wenshao Apr 6, 2026
b1af941
docs: add status line user documentation
wenshao Apr 6, 2026
12e1ef4
docs: add prerequisites, hot-reload note, fix troubleshooting test JSON
wenshao Apr 6, 2026
813d863
docs: guard division by zero in script example
wenshao Apr 6, 2026
5b9c94b
docs: fix jsonc trailing commas that break settings parser
wenshao Apr 6, 2026
3aa246a
docs: quote $input in inline command examples
wenshao Apr 6, 2026
e4e3c21
fix: handle PS1 newlines in statusline-setup agent prompt
wenshao Apr 6, 2026
f67c9c5
fix: clarify footer comment and add Windows shell note to docs
wenshao Apr 6, 2026
f807118
docs: use sh -c in troubleshooting test command
wenshao Apr 6, 2026
0e9c361
fix: use explicit Agent tool wording in /statusline prompt
wenshao Apr 6, 2026
51964fa
Merge remote-tracking branch 'origin/main' into feature/status-line-c…
wenshao Apr 7, 2026
7902806
docs: add ui.statusLine entry to settings reference
wenshao Apr 7, 2026
24a28d5
refactor(status-line): redesign JSON input schema and add context fields
BZ-D Apr 8, 2026
c369538
fix(test): add missing metrics and model fields to Footer test mock
wenshao Apr 8, 2026
520ed4e
fix: address audit findings across status-line and verbose-mode features
wenshao Apr 8, 2026
0be4d32
Merge remote-tracking branch 'origin/main' into feature/status-line-c…
wenshao Apr 8, 2026
55b1ab1
fix(status-line): derive remaining_percentage from used and reject em…
wenshao Apr 8, 2026
fc7ac2a
fix(statusline-setup): clarify agent prompt for script execution and …
wenshao Apr 8, 2026
841eb3c
fix: address reviewer feedback — stdin error logging, JSON schema, i18n
wenshao Apr 8, 2026
7804946
refactor(footer): inline status line in footer left section
wenshao Apr 8, 2026
a1c33cd
refactor(status-line): remove padding config
wenshao Apr 8, 2026
eaaa553
fix(footer): prevent status line from pushing right items off screen
wenshao Apr 8, 2026
f9b88c8
fix(footer): use wrap instead of truncate for status line text
wenshao Apr 8, 2026
cf879f0
refactor(footer): match upstream layout — status line + hints coexist
wenshao Apr 8, 2026
50bf5cc
fix(footer): truncate hints/mode row to prevent extra lines
wenshao Apr 8, 2026
63a14ae
fix(footer): remove Box wrapper from indicators for proper truncation
wenshao Apr 8, 2026
2a28132
fix(footer): suppress hint when status line active, hide on exit prompts
wenshao Apr 8, 2026
bcd0b5e
docs: update status line documentation to reflect inline footer layout
wenshao Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/features/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export default {
language: 'i18n',
channels: 'Channels',
hooks: 'Hooks',
'status-line': 'Status Line',
'scheduled-tasks': 'Scheduled Tasks',
};
174 changes: 174 additions & 0 deletions docs/users/features/status-line.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Status Line

> Display custom information beneath the footer using a shell command.

The status line lets you run a shell command whose output is displayed as a persistent line below the footer bar. 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.

```
┌─────────────────────────────────────────────────────────────────┐
│ ? for shortcuts 🔒 docker | Debug | ◼◼◼◻ 67% │
├─────────────────────────────────────────────────────────────────┤
│ user@host ~/project (main) qwen-3-235b ctx:34% │ ← status line
└─────────────────────────────────────────────────────────────────┘
```

## 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.id'); tokens=$(echo \"$input\" | jq -r '.context_window.last_prompt_token_count'); echo \"$model ctx:$tokens\"",
"padding": 0
}
}
}
```

| Field | Type | Required | Description |
| --------- | ----------- | -------- | ------------------------------------------------------------------------------------- |
| `type` | `"command"` | Yes | Must be `"command"` |
| `command` | string | Yes | Shell command to execute. Receives JSON via stdin, first line of stdout is displayed. |
| `padding` | number | No | Horizontal padding (default: `0`) |

## JSON input

The command receives a JSON object via stdin with the following fields:

```json
{
"session_id": "abc-123",
"cwd": "/home/user/project",
"model": {
"id": "qwen-3-235b"
},
"context_window": {
"context_window_size": 131072,
"last_prompt_token_count": 45000
},
"vim": {
"mode": "INSERT"
}
}
```

| 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"`). |

> **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.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}%\""
}
}
}
```

Output: `qwen-3-235b ctx:34%`

### Git branch + directory

```json
{
"ui": {
"statusLine": {
"type": "command",
"command": "branch=$(git branch --show-current 2>/dev/null); dir=$(basename \"$PWD\"); echo \"$dir${branch:+ ($branch)}\""
}
}
}
```

Output: `my-project (main)`

> Note: `git` and `pwd` run in the workspace directory automatically.

### 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.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)

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

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), or vim mode is toggled. 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.
- **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 status line disappears and 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 '{"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 |
6 changes: 2 additions & 4 deletions packages/cli/src/commands/channel/pidfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('writeServiceInfo + readServiceInfo', () => {
writeServiceInfo(['telegram']);

// Now simulate dead process

process.kill = vi.fn(() => {
throw new Error('ESRCH');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -122,7 +122,6 @@ describe('signalService', () => {
});

it('returns false when process is not found', () => {

process.kill = vi.fn(() => {
throw new Error('ESRCH');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -140,7 +139,6 @@ describe('signalService', () => {

describe('waitForExit', () => {
it('returns true immediately if process is already dead', async () => {

process.kill = vi.fn(() => {
throw new Error('ESRCH');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -152,7 +150,7 @@ describe('waitForExit', () => {

it('returns true when process dies within timeout', async () => {
let alive = true;

process.kill = vi.fn(() => {
if (!alive) throw new Error('ESRCH');
return true;
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,17 @@ const SETTINGS_SCHEMA = {
description: 'The color theme for the UI.',
showInDialog: true,
},
statusLine: {
type: 'object',
label: 'Status Line',
category: 'UI',
requiresRestart: false,
default: undefined as
| { type: 'command'; command: string; padding?: number }
| undefined,
description: 'Custom status line display configuration.',
showInDialog: false,
},
customThemes: {
type: 'object',
label: 'Custom Themes',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { vimCommand } from '../ui/commands/vimCommand.js';
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
import { insightCommand } from '../ui/commands/insightCommand.js';
import { statuslineCommand } from '../ui/commands/statuslineCommand.js';

const builtinDebugLogger = createDebugLogger('BUILTIN_COMMAND_LOADER');

Expand Down Expand Up @@ -118,6 +119,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
setupGithubCommand,
terminalSetupCommand,
insightCommand,
statuslineCommand,
];

return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/ui/commands/statuslineCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import type { SlashCommand, SubmitPromptActionReturn } from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';

export const statuslineCommand: SlashCommand = {
name: 'statusline',
get description() {
return t("Set up Qwen Code's status line UI");
},
kind: CommandKind.BUILT_IN,
action: (_context, args): SubmitPromptActionReturn => {
const prompt =
args.trim() || 'Configure my statusLine from my shell PS1 configuration';
return {
type: 'submit_prompt',
content: [
{
text: `Create an Agent with subagent_type "statusline-setup" and the following prompt:\n\n${prompt}`,
},
],
};
},
};
18 changes: 11 additions & 7 deletions packages/cli/src/ui/components/Footer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as useTerminalSize from '../hooks/useTerminalSize.js';
import { type UIState, UIStateContext } from '../contexts/UIStateContext.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
import { VimModeProvider } from '../contexts/VimModeContext.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
import type { LoadedSettings } from '../../config/settings.js';

vi.mock('../hooks/useTerminalSize.js');
Expand Down Expand Up @@ -52,14 +53,17 @@ const createMockSettings = (): LoadedSettings =>

const renderWithWidth = (width: number, uiState: UIState) => {
useTerminalSizeMock.mockReturnValue({ columns: width, rows: 24 });
const mockSettings = createMockSettings();
return render(
<ConfigContext.Provider value={createMockConfig() as never}>
<VimModeProvider settings={createMockSettings()}>
<UIStateContext.Provider value={uiState}>
<Footer />
</UIStateContext.Provider>
</VimModeProvider>
</ConfigContext.Provider>,
<SettingsContext.Provider value={mockSettings}>
<ConfigContext.Provider value={createMockConfig() as never}>
<VimModeProvider settings={mockSettings}>
<UIStateContext.Provider value={uiState}>
<Footer />
</UIStateContext.Provider>
</VimModeProvider>
</ConfigContext.Provider>
</SettingsContext.Provider>,
);
};

Expand Down
Loading
Loading