Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Settings are organized into categories. Most settings should be placed within th
| Setting | Type | Description | Default |
| ------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.outputStyle` | string | Name of the output style that shapes how responses are written: `Concise`, `Proactive`, `Explanatory`, or `Learning` (case-insensitive). Leave unset, or set `default`, for the default style. `--output-style` overrides it for one run. | `undefined` |
Comment thread
qqqys marked this conversation as resolved.
Outdated
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.enableAutoUpdate` | boolean | Enable automatic update checks and installations on startup. | `true` |
| `general.showSessionRecap` | boolean | Auto-show a one-line "where you left off" recap when returning to the terminal after being away. Off by default. Use `/recap` to trigger manually regardless of this setting. | `false` |
Expand Down
13 changes: 13 additions & 0 deletions docs/users/features/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ qwen -p "Summarize this repository" \
--append-system-prompt "Return exactly three bullets."
```

### Choose an Output Style

Use `--output-style` to pick one of the built-in output styles for this run. A style is a named block of instructions layered onto the built-in prompt that changes how the answer is written — `Concise` leads with the result and drops preamble and narration, `Proactive` starts working instead of proposing, `Explanatory` adds short notes about the codebase along the way. It overrides the `general.outputStyle` setting; `default` selects no style.
Comment thread
qqqys marked this conversation as resolved.

```bash
qwen -p "Why does the build fail on Windows?" --output-style Concise
```

> [!note]
>
> - `Learning` asks you to write part of the code and waits for a reply, so it is skipped in headless runs.
> - An unknown style name prints a warning and the run continues with the default style.
Comment thread
qqqys marked this conversation as resolved.

> [!note]
>
> - `--system-prompt` applies only to the current run's main session.
Expand Down
75 changes: 75 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,12 @@ describe('parseArguments', () => {
expect(argv.systemPrompt).toBeUndefined();
});

it('should parse --output-style', async () => {
process.argv = ['node', 'script.js', '--output-style', 'Concise'];
const argv = await parseArguments();
expect(argv.outputStyle).toBe('Concise');
});

it('should allow -r flag as alias for --resume', async () => {
process.argv = [
'node',
Expand Down Expand Up @@ -1453,6 +1459,75 @@ describe('loadCliConfig', () => {
});
});

describe('output style', () => {
it('leaves the style unset by default', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const config = await loadCliConfig({}, argv);
expect(config.getOutputStyle()).toBeUndefined();
});

it('selects a built-in style from general.outputStyle, case-insensitively', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const config = await loadCliConfig(
{ general: { outputStyle: 'concise' } },
argv,
);
expect(config.getOutputStyle()?.name).toBe('Concise');
});

it('lets --output-style override the setting', async () => {
process.argv = ['node', 'script.js', '--output-style', 'Explanatory'];
const argv = await parseArguments();
const config = await loadCliConfig(
{ general: { outputStyle: 'Concise' } },
argv,
);
expect(config.getOutputStyle()?.name).toBe('Explanatory');
});

it('treats "default" as no style, even when the setting names one', async () => {
Comment thread
qqqys marked this conversation as resolved.
process.argv = ['node', 'script.js', '--output-style', 'default'];
const argv = await parseArguments();
const config = await loadCliConfig(
{ general: { outputStyle: 'Concise' } },
argv,
);
expect(config.getOutputStyle()).toBeUndefined();
});

it('warns about an unknown style and falls back to the default', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
const config = await loadCliConfig(
{ general: { outputStyle: 'Verbose' } },
argv,
);
expect(config.getOutputStyle()).toBeUndefined();
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Unknown output style "Verbose" (from general.outputStyle)',
),
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('Concise, Proactive, Explanatory, Learning'),
);
});

it('names the flag when the unknown style came from --output-style', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
process.argv = ['node', 'script.js', '--output-style', 'Verbose'];
const argv = await parseArguments();
const config = await loadCliConfig({}, argv);
expect(config.getOutputStyle()).toBeUndefined();
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('(from --output-style)'),
);
});
});

it('should propagate runtime sleep prevention setting', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments();
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import {
type WebSearchSettings,
MAX_SUBAGENT_DEPTH_LIMIT,
addDaemonRequestAttribute,
BUILT_IN_OUTPUT_STYLES,
getBuiltInOutputStyle,
type OutputStyleDefinition,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import { hooksCommand } from '../commands/hooks.js';
Expand Down Expand Up @@ -145,6 +148,7 @@ export interface CliArgs {
promptInteractive: string | undefined;
systemPrompt: string | undefined;
appendSystemPrompt: string | undefined;
outputStyle: string | undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R8-2: [probe] CliArgs.outputStyle type lies about the repeated-flag runtime shape

The field is declared string | undefined, but a repeated --output-style flag makes yargs deliver a string[] at runtime — a fact this diff's own test asserts (expect(Array.isArray(argv.outputStyle)).toBe(true) at config.test.ts:1627), hidden by the return result as unknown as CliArgs cast at config.ts:934. Today the single read site defends itself — loadCliConfig passes the value into resolveOutputStyle(argvStyle: unknown, …) — but the next reader who touches argv.outputStyle trusting the declaration (argv.outputStyle.trim(), a length check) compiles clean and throws TypeError: argv.outputStyle.trim is not a function at runtime, only on the repeated-flag path — exactly the intermittent, input-shape-dependent failure that is miserable to chase, because the common single-flag case never reproduces it. The adjacent fallbackModel: string[] | undefined in the same interface shows repeatable flags get their honest yargs shape.

Witness:

PROBE argv.outputStyle = ["Concise","Proactive"] isArray: true
(runtime shape is string[] while config.ts:160 declares string | undefined)
Suggested change
outputStyle: string | undefined;
outputStyle: string | string[] | undefined;

resolveOutputStyle already accepts unknown, so no other change is needed. This is a type-only fix — the existing repeated-flag test in packages/cli/src/config/config.test.ts already pins the runtime array behaviour the widened type describes.

中文说明

该字段声明为 string | undefined,但重复的 --output-style 参数会让 yargs 在运行时交付一个 string[]——本 diff 自己的测试就断言了这一点(config.test.ts:1627 的 expect(Array.isArray(argv.outputStyle)).toBe(true)),只是被 config.ts:934 的 return result as unknown as CliArgs 强转掩盖了。目前唯一的读取点自我防御——loadCliConfig 把值传入 resolveOutputStyle(argvStyle: unknown, …)——但下一个信任声明类型而直接操作 argv.outputStyle 的读者(argv.outputStyle.trim()、长度检查)能通过编译,却在运行时抛出 TypeError: argv.outputStyle.trim is not a function,而且只发生在重复参数路径上——这正是那种难以追查的、依赖输入形态的偶发故障,因为常见的单参数场景永远无法复现。同一接口里相邻的 fallbackModel: string[] | undefined 表明可重复参数本应使用诚实的 yargs 形状。

证据(探针):argv.outputStyle = ["Concise","Proactive"] isArray: true(运行时形状是 string[],而 config.ts:160 声明为 string | undefined)。

建议修复:把字段声明为真实运行时形状 outputStyle: string | string[] | undefined;resolveOutputStyle 已接受 unknown,无需其他改动)。这是纯类型修复——现有的重复参数测试已经钉住了该类型所描述的运行时数组行为。

— qwen3.8-max via Qwen Code /review (v0.22.3)

yolo: boolean | undefined;
bare: boolean | undefined;
safeMode?: boolean | undefined;
Expand Down Expand Up @@ -687,6 +691,11 @@ export async function parseArguments(): Promise<CliArgs> {
description:
'Append instructions to the main session system prompt for this run. Can be combined with --system-prompt.',
})
.option('output-style', {
type: 'string',
description:
'Output style for this run, for example "Concise" or "Explanatory". Overrides the general.outputStyle setting; "default" selects no style.',
})
.option('sandbox', {
alias: 's',
type: 'boolean',
Expand Down Expand Up @@ -1497,6 +1506,35 @@ export class SessionIdConflictError extends Error {
}
}

/**
* Resolves the output style for this session. `--output-style` wins over
* `general.outputStyle`; an unset, empty, or `default` value means no style.
* An unknown name is reported and the session falls back to the default
* style rather than refusing to start, so a typo in settings.json never
* locks the user out.
*/
export function resolveOutputStyle(
argvStyle: string | undefined,
settingsStyle: string | undefined,
): OutputStyleDefinition | undefined {
const name = (argvStyle ?? settingsStyle)?.trim();
Comment thread
qqqys marked this conversation as resolved.
Outdated
if (!name || name.toLowerCase() === 'default') {
return undefined;
}
const style = getBuiltInOutputStyle(name);
if (style) {
return style;
}
const known = BUILT_IN_OUTPUT_STYLES.map((s) => s.name).join(', ');
const source =
argvStyle !== undefined ? '--output-style' : 'general.outputStyle';
const warning = `Unknown output style "${name}" (from ${source}); using the default style. Available styles: ${known}.`;
debugLogger.warn(warning);
// eslint-disable-next-line no-console
console.error(`WARNING: ${warning}`);
return undefined;
}

export async function loadCliConfig(
settings: Settings,
argv: CliArgs,
Expand Down Expand Up @@ -2160,6 +2198,10 @@ export async function loadCliConfig(
question,
systemPrompt: argv.systemPrompt,
appendSystemPrompt: argv.appendSystemPrompt,
outputStyle: resolveOutputStyle(
argv.outputStyle,
settings.general?.outputStyle,
),
// Legacy fields – kept for backward compatibility with getCoreTools() etc.
coreTools:
bareMode || safeMode
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,16 @@ const SETTINGS_SCHEMA = {
description: 'The preferred editor to open files in.',
showInDialog: true,
},
outputStyle: {
Comment thread
qqqys marked this conversation as resolved.
type: 'string',
label: 'Output Style',
category: 'General',
requiresRestart: false,
Comment thread
qqqys marked this conversation as resolved.
Outdated
default: undefined as string | undefined,
description:
'Name of the output style that shapes how responses are written, for example "Concise" or "Explanatory". Leave unset for the default style.',
showInDialog: false,
},
vimMode: {
type: 'boolean',
label: 'Vim Mode',
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,7 @@ describe('gemini.tsx main function kitty protocol', () => {
promptInteractive: undefined,
systemPrompt: undefined,
appendSystemPrompt: undefined,
outputStyle: undefined,
query: undefined,
yolo: undefined,
bare: undefined,
Expand Down Expand Up @@ -1878,6 +1879,7 @@ describe('gemini.tsx main function kitty protocol', () => {
promptInteractive: undefined,
systemPrompt: undefined,
appendSystemPrompt: undefined,
outputStyle: undefined,
query: undefined,
yolo: undefined,
bare: undefined,
Expand Down Expand Up @@ -2004,6 +2006,7 @@ describe('gemini.tsx main function kitty protocol', () => {
promptInteractive: undefined,
systemPrompt: undefined,
appendSystemPrompt: undefined,
outputStyle: undefined,
query: undefined,
yolo: undefined,
bare: undefined,
Expand Down Expand Up @@ -2126,6 +2129,7 @@ describe('gemini.tsx main function kitty protocol', () => {
promptInteractive: undefined,
systemPrompt: undefined,
appendSystemPrompt: undefined,
outputStyle: undefined,
query: undefined,
yolo: undefined,
bare: undefined,
Expand Down
Loading