diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index f6b8b31f28..2335a1054d 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -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 ` | Generate a command-mode [status line](./status-line.md) via agent | `/statusline show model and git branch` | ### 1.3 Language Settings diff --git a/docs/users/features/status-line.md b/docs/users/features/status-line.md index c7e5763f0f..c0b3eb6be5 100644 --- a/docs/users/features/status-line.md +++ b/docs/users/features/status-line.md @@ -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..api` | object | Per-model API stats: `total_requests`, `total_errors`, `total_latency_ms` | | `metrics.models..tokens` | object | Per-model token usage: `prompt`, `completion`, `total`, `cached`, `thoughts` | | `metrics.files` | object | File change stats: `total_lines_added`, `total_lines_removed` | @@ -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 | diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 1adcb34472..0356898252 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -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); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 5d6c3e24d5..456b142526 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -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. diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 94616d2f9a..3f29474ed7 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -1590,9 +1590,7 @@ export const InputPrompt: React.FC = ({ : 2 // "! " = 2 chars : commandSearchActive ? 6 // "(r:) " (inner) + " " (outer) = 6 cols - : showYoloStyling - ? 2 // "* " = 2 chars - : 2; // "> " = 2 chars + : 2; // "> " or "* " = 2 chars return ( <> diff --git a/packages/cli/src/ui/components/hooks/constants.ts b/packages/cli/src/ui/components/hooks/constants.ts index af25998419..5c3c86715b 100644 --- a/packages/cli/src/ui/components/hooks/constants.ts +++ b/packages/cli/src/ui/components/hooks/constants.ts @@ -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'), diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 36ae32153b..5f86c1f27c 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -1853,6 +1853,7 @@ export const useGeminiStream = ( ); if (processingStatus === StreamProcessingStatus.UserCancelled) { + submitPromptOnCompleteRef.current = null; isSubmittingQueryRef.current = false; return; } @@ -1878,7 +1879,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 @@ -1918,6 +1921,7 @@ export const useGeminiStream = ( }); } } finally { + submitPromptOnCompleteRef.current = null; setIsResponding(false); isSubmittingQueryRef.current = false; } diff --git a/packages/cli/src/ui/hooks/useStatusLine.test.ts b/packages/cli/src/ui/hooks/useStatusLine.test.ts index c6398888e3..c2b851b8a4 100644 --- a/packages/cli/src/ui/hooks/useStatusLine.test.ts +++ b/packages/cli/src/ui/hooks/useStatusLine.test.ts @@ -23,6 +23,7 @@ vi.mock('child_process'); const mockSettings = { merged: {} as Record, + 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', diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts index 2dfca88aed..504923be10 100644 --- a/packages/cli/src/ui/hooks/useStatusLine.ts +++ b/packages/cli/src/ui/hooks/useStatusLine.ts @@ -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. diff --git a/patches/ink+7.0.3.patch b/patches/ink+7.0.3.patch index 9a0b3405e4..9b28cb09cb 100644 --- a/patches/ink+7.0.3.patch +++ b/patches/ink+7.0.3.patch @@ -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"