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:
Edenman 2026-06-03 14:17:16 +08:00 committed by GitHub
parent 6fd713f7b6
commit a623a41ef3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 425 additions and 51 deletions

View file

@ -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

View file

@ -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 |

View file

@ -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);

View file

@ -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.

View file

@ -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 (
<>

View file

@ -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'),

View file

@ -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;
}

View file

@ -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',

View file

@ -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.

View file

@ -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"