mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(cli): statusline not re-rendering when switching from preset to command type (#4706)
* fix(cli): statusline not re-rendering when switching from preset to command type When `/statusline [prompt]` triggers the statusline-setup agent to change the config type from preset to command, the in-memory LoadedSettings is never updated (the agent edits settings.json on disk via Edit/Write tools), so useStatusLine continues rendering the stale preset config. - Add LoadedSettings.reloadScopeFromDisk() to re-read a settings file from disk with env-var resolution and rawJson sync - Add notifyStatusLineReloaded callback that clears the stale preset override and bumps statusLineSettingsVersion to trigger re-render - Wire an onComplete callback in statuslineCommand that reloads user settings and notifies the statusline hook after the agent turn completes - Clear submitPromptOnCompleteRef on cancel/error to prevent stale callbacks from leaking to subsequent turns - Update statusline docs with preset mode reference, worktree JSON field, and preset-specific troubleshooting entries * fix(cli): reload settings on stream idle instead of onComplete onComplete fires when processGeminiStreamEvents returns, which happens as soon as tool calls are scheduled — before the statusline-setup agent has finished writing to settings.json. The fix adds a reload effect in useStatusLine that triggers when streamingState transitions to Idle (all tools done, no pending continuations), which is the true end of a model turn. The onComplete callback is kept as belt-and-suspenders for non-agentic slash commands (those that don't invoke tools), but the primary sync point is now the streamingState → Idle transition. * docs: add /statusline to commands reference with link to status-line page * refactor(cli): remove onComplete mechanism, keep only idle-reload path The onComplete callback fires when processGeminiStreamEvents returns, which is before the statusline-setup agent finishes writing settings.json. Remove the entire onComplete + notifyStatusLineReloaded plumbing and rely solely on the streamingState → Idle reload effect in useStatusLine. Also optimize the reload effect to compare the serialized statusLine config before and after reloading from disk, only bumping the re-render key when the config actually changed — avoids unnecessary doUpdate() calls on turns that didn't touch statusline settings. * chore(cli): shorten statusline reload comment --------- Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
This commit is contained in:
parent
6fd713f7b6
commit
a623a41ef3
10 changed files with 425 additions and 51 deletions
|
|
@ -31,16 +31,18 @@ These commands help you save, restore, and summarize work progress.
|
|||
|
||||
Commands for adjusting interface appearance and work environment.
|
||||
|
||||
| Command | Description | Usage Examples |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
||||
| `/clear` | Clear terminal screen content | `/clear` (shortcut: `Ctrl+L`) |
|
||||
| `/context` | Show context window usage breakdown | `/context` |
|
||||
| → `detail` | Show per-item context usage breakdown | `/context detail` |
|
||||
| `/diff` | Open an interactive diff viewer showing uncommitted changes and per-turn diffs. Use ←/→ to switch between current git diff and individual conversation turns, ↑/↓ to browse files | `/diff` |
|
||||
| `/theme` | Change Qwen Code visual theme | `/theme` |
|
||||
| `/vim` | Turn input area Vim editing mode on/off | `/vim` |
|
||||
| `/directory` | Manage multi-directory support workspace | `/dir add ./src,./tests` |
|
||||
| `/editor` | Open dialog to select supported editor | `/editor` |
|
||||
| Command | Description | Usage Examples |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `/clear` | Clear terminal screen content | `/clear` (shortcut: `Ctrl+L`) |
|
||||
| `/context` | Show context window usage breakdown | `/context` |
|
||||
| → `detail` | Show per-item context usage breakdown | `/context detail` |
|
||||
| `/diff` | Open an interactive diff viewer showing uncommitted changes and per-turn diffs. Use ←/→ to switch between current git diff and individual conversation turns, ↑/↓ to browse files | `/diff` |
|
||||
| `/theme` | Change Qwen Code visual theme | `/theme` |
|
||||
| `/vim` | Turn input area Vim editing mode on/off | `/vim` |
|
||||
| `/directory` | Manage multi-directory support workspace | `/dir add ./src,./tests` |
|
||||
| `/editor` | Open dialog to select supported editor | `/editor` |
|
||||
| `/statusline` | Open interactive [status line](./status-line.md) preset dialog | `/statusline` |
|
||||
| `/statusline <text>` | Generate a command-mode [status line](./status-line.md) via agent | `/statusline show model and git branch` |
|
||||
|
||||
### 1.3 Language Settings
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Status Line
|
||||
|
||||
> Display custom information in the footer using a shell command.
|
||||
> Display custom information in the footer.
|
||||
|
||||
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.
|
||||
The status line shows session-aware information — model name, token usage, git branch, and more — in the footer's left section. There are two configuration modes:
|
||||
|
||||
- **Preset mode** — pick from built-in data items via an interactive dialog or JSON config. No scripting required.
|
||||
- **Command mode** — run a shell command that receives structured JSON context via stdin. Full flexibility for custom formatting.
|
||||
|
||||
```
|
||||
Single-line status (default approval mode — 1 row):
|
||||
|
|
@ -26,26 +29,130 @@ Multi-line status + non-default mode (3 rows max):
|
|||
|
||||
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:
|
||||
The easiest way to configure a status line is the `/statusline` command. It opens an interactive dialog where you can select preset items, toggle theme colors, and see a live preview:
|
||||
|
||||
```
|
||||
/statusline
|
||||
```
|
||||
|
||||
You can also give it specific instructions:
|
||||
This opens the preset mode configurator. Use arrow keys to navigate, space to toggle items, and enter to confirm. Your selection is saved to settings automatically.
|
||||
|
||||
You can also give `/statusline` specific instructions to have it generate a command-mode configuration:
|
||||
|
||||
```
|
||||
/statusline show model name and context usage percentage
|
||||
```
|
||||
|
||||
## Manual configuration
|
||||
---
|
||||
|
||||
## Preset mode
|
||||
|
||||
Preset mode provides a set of built-in data items that you can pick and combine — no shell commands, no `jq`, no scripting. Items are rendered as `item1 | item2 | item3` in a single line.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": {
|
||||
"statusLine": {
|
||||
"type": "preset",
|
||||
"items": [
|
||||
"model-with-reasoning",
|
||||
"git-branch",
|
||||
"context-remaining",
|
||||
"current-dir",
|
||||
"context-used"
|
||||
],
|
||||
"useThemeColors": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ---------------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | `"preset"` | Yes | Must be `"preset"` |
|
||||
| `items` | string[] | Yes | Ordered list of preset item IDs to display (see table below). Items are joined with `\|` as the separator. |
|
||||
| `useThemeColors` | boolean | No | Apply the active `/theme` color to the status line text. Defaults to `true`. |
|
||||
| `hideContextIndicator` | boolean | No | Hide the built-in context usage indicator in the footer right section. Defaults to `false`. |
|
||||
|
||||
### Available preset items
|
||||
|
||||
| Item ID | Default | Description |
|
||||
| ---------------------- | ------- | ------------------------------------------------------------------ |
|
||||
| `model-with-reasoning` | Yes | Current model name with reasoning level (e.g. `qwen-3-235b high`) |
|
||||
| `model` | | Current model name without reasoning level |
|
||||
| `git-branch` | Yes | Current Git branch name (hidden when not in a git repo) |
|
||||
| `context-remaining` | Yes | Percentage of context window remaining (e.g. `Context 65.7% left`) |
|
||||
| `total-input-tokens` | | Total input tokens used in session (e.g. `30.0k in`) |
|
||||
| `total-output-tokens` | | Total output tokens used in session (e.g. `5.0k out`) |
|
||||
| `current-dir` | Yes | Current working directory |
|
||||
| `project-name` | | Project name (basename of working directory) |
|
||||
| `pull-request-number` | | Open PR number for the current branch (requires `gh` CLI) |
|
||||
| `branch-changes` | | Session file change stats (e.g. `+120 -30`) |
|
||||
| `context-used` | Yes | Percentage of context window used (e.g. `Context 34.3% used`) |
|
||||
| `run-state` | | Compact session state (`Ready`, `Working`, or `Confirm`) |
|
||||
| `qwen-version` | | Qwen Code version (e.g. `v0.14.1`) |
|
||||
| `context-window-size` | | Total context window size (e.g. `131.1k window`) |
|
||||
| `used-tokens` | | Current prompt token count (e.g. `45.0k used`) |
|
||||
| `session-id` | | Current session identifier |
|
||||
|
||||
Items marked **Default** are pre-selected when you first open the `/statusline` dialog.
|
||||
|
||||
### Example output
|
||||
|
||||
With the default items, the status line looks like:
|
||||
|
||||
```
|
||||
qwen-3-235b high | main | Context 65.7% left | /home/user/project | Context 34.3% used
|
||||
```
|
||||
|
||||
### Customizing via the dialog
|
||||
|
||||
Running `/statusline` opens an interactive multi-select dialog:
|
||||
|
||||
```
|
||||
┌ Configure Status Line ────────────────────────────────────────┐
|
||||
│ Select which items to display in the status line. │
|
||||
│ │
|
||||
│ Type to search │
|
||||
│ > │
|
||||
│ │
|
||||
│ [x] Use theme colors Apply colors from the active /theme│
|
||||
│ ─────────────────────── │
|
||||
│ [x] model-with-reasoning Current model name with reasoning │
|
||||
│ [ ] model-only Current model name without reason │
|
||||
│ [x] git-branch Current Git branch when available │
|
||||
│ [x] context-remaining Percentage of context remaining │
|
||||
│ ... │
|
||||
│ │
|
||||
│ Preview │
|
||||
│ qwen-3-235b high | main | Context 65.7% left │
|
||||
│ │
|
||||
│ Use up/down to navigate, space to select, enter to confirm │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Type to filter items by name or description
|
||||
- A live preview updates as you toggle items
|
||||
- Press enter to save the configuration
|
||||
|
||||
---
|
||||
|
||||
## Command mode
|
||||
|
||||
Command mode runs a shell command whose stdout is displayed in the status line. The command receives structured JSON context via stdin for session-aware output.
|
||||
|
||||
### 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`
|
||||
|
||||
### Configuration
|
||||
|
||||
Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:
|
||||
|
||||
|
|
@ -68,7 +175,7 @@ Add a `statusLine` object under the `ui` key in `~/.qwen/settings.json`:
|
|||
| `respectUserColors` | boolean | No | Preserve ANSI color codes in command output instead of applying dimmed footer styling. Defaults to `false`. |
|
||||
| `hideContextIndicator` | boolean | No | Hide the built-in context usage indicator in the footer right section. Defaults to `false`. |
|
||||
|
||||
## JSON input
|
||||
### JSON input
|
||||
|
||||
The command receives a JSON object via stdin with the following fields:
|
||||
|
||||
|
|
@ -93,6 +200,13 @@ The command receives a JSON object via stdin with the following fields:
|
|||
"git": {
|
||||
"branch": "main"
|
||||
},
|
||||
"worktree": {
|
||||
"name": "fix-auth",
|
||||
"path": "/home/user/project/.qwen/worktrees/fix-auth",
|
||||
"branch": "fix-auth",
|
||||
"original_cwd": "/home/user/project",
|
||||
"original_branch": "main"
|
||||
},
|
||||
"metrics": {
|
||||
"models": {
|
||||
"qwen-3-235b": {
|
||||
|
|
@ -135,6 +249,12 @@ The command receives a JSON object via stdin with the following fields:
|
|||
| `workspace.current_dir` | string | Current working directory |
|
||||
| `git` | object \| absent | Present only inside a git repository. |
|
||||
| `git.branch` | string | Current branch name |
|
||||
| `worktree` | object \| absent | Present only when inside an active worktree (created by `enter_worktree`). |
|
||||
| `worktree.name` | string | Worktree slug name |
|
||||
| `worktree.path` | string | Absolute path to the worktree directory |
|
||||
| `worktree.branch` | string | Branch checked out in the worktree |
|
||||
| `worktree.original_cwd` | string | Working directory before entering the worktree |
|
||||
| `worktree.original_branch` | string | Branch that was active before entering the worktree |
|
||||
| `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` |
|
||||
|
|
@ -142,9 +262,9 @@ The command receives a JSON object via stdin with the following fields:
|
|||
|
||||
> **Important:** stdin can only be read once. Always store it in a variable first: `input=$(cat)`.
|
||||
|
||||
## Examples
|
||||
### Examples
|
||||
|
||||
### Model and token usage
|
||||
#### Model and token usage
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -159,7 +279,7 @@ The command receives a JSON object via stdin with the following fields:
|
|||
|
||||
Output: `qwen-3-235b ctx:34%`
|
||||
|
||||
### Git branch + directory
|
||||
#### Git branch + directory
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -176,7 +296,7 @@ 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
|
||||
#### File change stats
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
@ -191,7 +311,7 @@ Output: `my-project (main)`
|
|||
|
||||
Output: `+120/-30 lines`
|
||||
|
||||
### Live clock and git branch
|
||||
#### 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):
|
||||
|
||||
|
|
@ -209,7 +329,7 @@ Use `refreshInterval` when the statusline shows data that changes without an Age
|
|||
|
||||
Output (refreshed every second): `14:32:07 (main)`
|
||||
|
||||
### Script file for complex commands
|
||||
#### Script file for complex commands
|
||||
|
||||
For longer commands, save a script file at `~/.qwen/statusline-command.sh`:
|
||||
|
||||
|
|
@ -246,18 +366,32 @@ Then reference it in settings:
|
|||
|
||||
## 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.
|
||||
**Both modes:**
|
||||
|
||||
- **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).
|
||||
- **Output**: Up to 2 lines. Each line is rendered as a separate row 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.
|
||||
|
||||
**Command mode only:**
|
||||
|
||||
- **Timeout**: Commands that take longer than 5 seconds are killed. The status line clears on failure.
|
||||
- **Refresh**: 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).
|
||||
- **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`).
|
||||
|
||||
**Preset mode only:**
|
||||
|
||||
- **No external dependencies**: Preset items are computed internally — no shell commands, no `jq`, no timeouts.
|
||||
- **Theme integration**: When `useThemeColors` is `true` (default), the status line text uses the active `/theme` color. When `false`, dimmed footer styling is applied.
|
||||
- **PR lookup**: The `pull-request-number` item runs `gh pr view` in the background (2s timeout). It only triggers when the branch changes, not on every update.
|
||||
|
||||
## 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 |
|
||||
| Problem | Cause | Fix |
|
||||
| --------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Status line not showing | Config at wrong path | Must be under `ui.statusLine`, not root-level `statusLine` |
|
||||
| Empty output (command mode) | 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 (command mode) | 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 |
|
||||
| Preset items missing | Conditional items have no data | `git-branch` is hidden outside git repos; `context-used` is hidden when usage is 0; `branch-changes` is hidden when no files changed. This is expected — items appear once their data is available |
|
||||
| PR number not showing | `gh` CLI not installed | Install [GitHub CLI](https://cli.github.com/) and authenticate with `gh auth login`. The lookup runs with a 2s timeout |
|
||||
|
|
|
|||
|
|
@ -3032,6 +3032,167 @@ describe('Settings Loading and Merging', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('reloadScopeFromDisk', () => {
|
||||
it('reloads a scope from disk and resolves home env vars', () => {
|
||||
const homeQwenEnvPath = path.join(
|
||||
path.dirname(USER_SETTINGS_PATH),
|
||||
'.env',
|
||||
);
|
||||
const initialUserSettingsContent = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
statusLine: {
|
||||
type: 'preset',
|
||||
items: ['model'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const reloadedUserSettingsContent = {
|
||||
ui: {
|
||||
theme: '${RELOADED_THEME}',
|
||||
statusLine: {
|
||||
type: 'command',
|
||||
command: 'echo reloaded',
|
||||
},
|
||||
},
|
||||
};
|
||||
let currentUserSettingsContent = JSON.stringify(
|
||||
initialUserSettingsContent,
|
||||
);
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH || p === homeQwenEnvPath,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
return currentUserSettingsContent;
|
||||
}
|
||||
if (p === homeQwenEnvPath) {
|
||||
return 'RELOADED_THEME=light';
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
delete process.env['RELOADED_THEME'];
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
currentUserSettingsContent = JSON.stringify(reloadedUserSettingsContent);
|
||||
|
||||
settings.reloadScopeFromDisk(SettingScope.User);
|
||||
|
||||
expect(settings.user.settings.ui?.theme).toBe('light');
|
||||
expect(settings.user.originalSettings.ui?.theme).toBe(
|
||||
'${RELOADED_THEME}',
|
||||
);
|
||||
expect(settings.user.rawJson).toBe(currentUserSettingsContent);
|
||||
expect(settings.merged.ui?.statusLine).toEqual({
|
||||
type: 'command',
|
||||
command: 'echo reloaded',
|
||||
});
|
||||
|
||||
delete process.env['RELOADED_THEME'];
|
||||
});
|
||||
|
||||
it('clears a scope when its settings file is removed', () => {
|
||||
const userSettingsContent = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
};
|
||||
let userSettingsExists = true;
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH && userSettingsExists,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
return JSON.stringify(userSettingsContent);
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
userSettingsExists = false;
|
||||
|
||||
settings.reloadScopeFromDisk(SettingScope.User);
|
||||
|
||||
expect(settings.user.settings).toEqual({});
|
||||
expect(settings.user.originalSettings).toEqual({});
|
||||
expect(settings.user.rawJson).toBeUndefined();
|
||||
expect(settings.merged.ui).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores top-level array settings during reload', () => {
|
||||
const initialUserSettingsContent = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
};
|
||||
let currentUserSettingsContent = JSON.stringify(
|
||||
initialUserSettingsContent,
|
||||
);
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
return currentUserSettingsContent;
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
currentUserSettingsContent = '[]';
|
||||
|
||||
settings.reloadScopeFromDisk(SettingScope.User);
|
||||
|
||||
expect(settings.user.settings).toEqual({
|
||||
...initialUserSettingsContent,
|
||||
[SETTINGS_VERSION_KEY]: SETTINGS_VERSION,
|
||||
});
|
||||
expect(settings.merged.ui?.theme).toBe('dark');
|
||||
});
|
||||
|
||||
it('keeps existing settings and logs when reload JSON parsing fails', () => {
|
||||
const initialUserSettingsContent = {
|
||||
ui: {
|
||||
theme: 'dark',
|
||||
},
|
||||
};
|
||||
let currentUserSettingsContent = JSON.stringify(
|
||||
initialUserSettingsContent,
|
||||
);
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
|
||||
);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === USER_SETTINGS_PATH) {
|
||||
return currentUserSettingsContent;
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
currentUserSettingsContent = '{bad json';
|
||||
|
||||
settings.reloadScopeFromDisk(SettingScope.User);
|
||||
|
||||
expect(settings.merged.ui?.theme).toBe('dark');
|
||||
expect(mockDebugLogger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('reloadScopeFromDisk(User):'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue persistence', () => {
|
||||
it('preserves models added to settings.json after startup when updating model.name', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
|
|
|
|||
|
|
@ -476,6 +476,36 @@ export class LoadedSettings {
|
|||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
|
||||
reloadScopeFromDisk(scope: SettingScope): void {
|
||||
const file = this.forScope(scope);
|
||||
try {
|
||||
if (!fs.existsSync(file.path)) {
|
||||
file.settings = {};
|
||||
file.originalSettings = {};
|
||||
file.rawJson = undefined;
|
||||
this._merged = this.computeMergedSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(file.path, 'utf-8');
|
||||
const parsed = JSON.parse(stripJsonComments(content));
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const resolved = resolveEnvVarsInObject(
|
||||
parsed as Settings,
|
||||
getHomeEnvFallbackVars(),
|
||||
);
|
||||
file.settings = resolved;
|
||||
file.originalSettings = structuredClone(parsed) as Settings;
|
||||
file.rawJson = content;
|
||||
}
|
||||
} catch (err) {
|
||||
debugLogger.warn(
|
||||
`reloadScopeFromDisk(${scope}): ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
this._merged = this.computeMergedSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-level hooks from user settings (not merged with workspace).
|
||||
* These hooks should always be loaded regardless of folder trust.
|
||||
|
|
|
|||
|
|
@ -1610,9 +1610,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
: 2 // "! " = 2 chars
|
||||
: commandSearchActive
|
||||
? 6 // "(r:) " (inner) + " " (outer) = 6 cols
|
||||
: showYoloStyling
|
||||
? 2 // "* " = 2 chars
|
||||
: 2; // "> " = 2 chars
|
||||
: 2; // "> " or "* " = 2 chars
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -149,9 +149,7 @@ export function getHookShortDescription(eventName: string): string {
|
|||
[HookEventName.PreToolUse]: t('Before tool execution'),
|
||||
[HookEventName.PostToolUse]: t('After tool execution'),
|
||||
[HookEventName.PostToolUseFailure]: t('After tool execution fails'),
|
||||
[HookEventName.PostToolBatch]: t(
|
||||
'After all tool calls in a batch resolve',
|
||||
),
|
||||
[HookEventName.PostToolBatch]: t('After all tool calls in a batch resolve'),
|
||||
[HookEventName.Notification]: t('When notifications are sent'),
|
||||
[HookEventName.UserPromptSubmit]: t('When the user submits a prompt'),
|
||||
[HookEventName.SessionStart]: t('When a new session is started'),
|
||||
|
|
|
|||
|
|
@ -1857,6 +1857,7 @@ export const useGeminiStream = (
|
|||
);
|
||||
|
||||
if (processingStatus === StreamProcessingStatus.UserCancelled) {
|
||||
submitPromptOnCompleteRef.current = null;
|
||||
isSubmittingQueryRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -1882,7 +1883,9 @@ export const useGeminiStream = (
|
|||
const onComplete = submitPromptOnCompleteRef.current;
|
||||
if (onComplete) {
|
||||
submitPromptOnCompleteRef.current = null;
|
||||
void onComplete();
|
||||
void onComplete().catch((err) => {
|
||||
debugLogger.error('onComplete callback failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// After the turn completes, wire up notifications for any background
|
||||
|
|
@ -1922,6 +1925,7 @@ export const useGeminiStream = (
|
|||
});
|
||||
}
|
||||
} finally {
|
||||
submitPromptOnCompleteRef.current = null;
|
||||
setIsResponding(false);
|
||||
isSubmittingQueryRef.current = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ vi.mock('child_process');
|
|||
|
||||
const mockSettings = {
|
||||
merged: {} as Record<string, unknown>,
|
||||
reloadScopeFromDisk: vi.fn(),
|
||||
};
|
||||
vi.mock('../contexts/SettingsContext.js', () => ({
|
||||
useSettings: () => mockSettings,
|
||||
|
|
@ -136,6 +137,7 @@ describe('useStatusLine', () => {
|
|||
stdinWrittenData = '';
|
||||
stdinErrorHandler = undefined;
|
||||
mockKill = vi.fn();
|
||||
mockSettings.reloadScopeFromDisk.mockImplementation(() => undefined);
|
||||
|
||||
// Set up exec mock implementation
|
||||
vi.mocked(child_process.exec).mockImplementation(((
|
||||
|
|
@ -366,6 +368,38 @@ describe('useStatusLine', () => {
|
|||
expect(result.current.lines).toEqual(['test-model | #4118']);
|
||||
});
|
||||
|
||||
it('reloads status line settings from disk when streaming becomes idle', async () => {
|
||||
setStatusLineConfig({
|
||||
type: 'preset',
|
||||
items: ['model'],
|
||||
});
|
||||
const { result, rerender } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(result.current.lines).toEqual(['test-model']);
|
||||
|
||||
mockSettings.reloadScopeFromDisk.mockImplementationOnce(() => {
|
||||
setStatusLineConfig({
|
||||
type: 'preset',
|
||||
items: ['model-with-reasoning'],
|
||||
});
|
||||
});
|
||||
|
||||
mockUIState.streamingState = StreamingState.Responding;
|
||||
rerender();
|
||||
mockConfig.getContentGeneratorConfig.mockReturnValue({
|
||||
contextWindowSize: 131072,
|
||||
reasoning: { effort: 'high' },
|
||||
});
|
||||
mockUIState.streamingState = StreamingState.Idle;
|
||||
rerender();
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(mockSettings.reloadScopeFromDisk).toHaveBeenCalledOnce();
|
||||
expect(result.current.lines).toEqual(['test-model high']);
|
||||
});
|
||||
|
||||
it('uses command settings when a stale preset override no longer matches the settings type', () => {
|
||||
setStatusLineConfig({
|
||||
type: 'command',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { exec, type ChildProcess } from 'child_process';
|
||||
import { createDebugLogger } from '@qwen-code/qwen-code-core';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
|
|
@ -614,6 +615,23 @@ export function useStatusLine(): {
|
|||
updatePullRequestNumber,
|
||||
]);
|
||||
|
||||
// File edits made during a turn bypass in-memory settings; reload the user
|
||||
// scope on idle, then re-render only if ui.statusLine changed.
|
||||
const [settingsReloadKey, setSettingsReloadKey] = useState(0);
|
||||
const prevStreamingForReloadRef = useRef(streamingState);
|
||||
useEffect(() => {
|
||||
const prev = prevStreamingForReloadRef.current;
|
||||
prevStreamingForReloadRef.current = streamingState;
|
||||
if (prev !== streamingState && streamingState === 'idle') {
|
||||
const before = JSON.stringify(settings.merged.ui?.statusLine);
|
||||
settings.reloadScopeFromDisk(SettingScope.User);
|
||||
const after = JSON.stringify(settings.merged.ui?.statusLine);
|
||||
if (before !== after) {
|
||||
setSettingsReloadKey((k) => k + 1);
|
||||
}
|
||||
}
|
||||
}, [streamingState, settings]);
|
||||
|
||||
// Re-execute immediately when the command itself changes (hot reload).
|
||||
// Skip the first run — the mount effect below already handles it.
|
||||
useEffect(() => {
|
||||
|
|
@ -634,6 +652,7 @@ export function useStatusLine(): {
|
|||
statusLinePresetUseThemeColors,
|
||||
statusLinePresetItemsKey,
|
||||
statusLineSettingsVersion,
|
||||
settingsReloadKey,
|
||||
]);
|
||||
|
||||
// Re-render preset output once the async GitHub PR lookup returns.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
diff --git a/node_modules/ink/package.json b/node_modules/ink/package.json
|
||||
--- a/node_modules/ink/package.json
|
||||
+++ b/node_modules/ink/package.json
|
||||
@@ -11,8 +11,16 @@
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -14,2 +14,10 @@
|
||||
- "types": "./build/index.d.ts",
|
||||
- "default": "./build/index.js"
|
||||
+ ".": {
|
||||
|
|
@ -17,6 +14,3 @@ diff --git a/node_modules/ink/package.json b/node_modules/ink/package.json
|
|||
+ "./components/CursorContext": {
|
||||
+ "default": "./build/components/CursorContext.js"
|
||||
+ }
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue