Skip to content
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
dcaf377
feat(cli): add /advisor command for second-opinion conversation review
yiliang114 Jul 23, 2026
e30af3f
Merge branch 'main' into feat/advisor-command
wenshao Jul 26, 2026
ef6837f
Merge branch 'main' into feat/advisor-command
wenshao Jul 29, 2026
6bfa709
Merge branch 'main' into feat/advisor-command
wenshao Jul 30, 2026
48770c7
Merge branch 'main' into feat/advisor-command
wenshao Jul 31, 2026
2c8d998
Merge branch 'main' into feat/advisor-command
wenshao Jul 31, 2026
5bd917a
Merge branch 'main' into feat/advisor-command
yiliang114 Jul 31, 2026
2b1946d
fix(cli): translate advisor command description
yiliang114 Jul 31, 2026
6f7eb48
Merge branch 'main' into feat/advisor-command
qwen-code-dev-bot Jul 31, 2026
c9b0967
fix(cli): address review feedback for /advisor command (#7567)
Aug 1, 2026
793d2cf
fix(cli): refine /advisor abort handling, cache sharing, and docs (#7…
Aug 1, 2026
c1d3d1a
Merge branch 'main' into feat/advisor-command
qwen-code-dev-bot Aug 1, 2026
64277bc
Merge branch 'main' into feat/advisor-command
qwen-code-dev-bot Aug 1, 2026
866c6cd
fix(cli): address /advisor review feedback — docs wording, i18n, test…
Aug 1, 2026
466183d
fix(cli): address /advisor review feedback — rendering, tools, guard …
Aug 1, 2026
9cff267
test(cli): cover advisor inline code fences
yiliang114 Aug 3, 2026
5d05741
test(cli): cover advisor model setting schema
yiliang114 Aug 3, 2026
372242f
fix(cli): address /advisor review feedback — docs accuracy, shared fe…
Aug 3, 2026
30334aa
test(cli): register advisor i18n keys as must-translate (#7567)
Aug 3, 2026
940f01b
fix(cli): address /advisor review feedback — fence-aware normalizer, …
Aug 3, 2026
e384dac
fix(cli): repair advisor test type cast that broke tsc build (#7567)
Aug 4, 2026
34b06b0
fix(cli): address /advisor review feedback — parser-mirroring fence t…
Aug 4, 2026
07a5435
Merge branch 'main' into feat/advisor-command
yiliang114 Aug 4, 2026
8fa8eaf
fix(cli): address round-4 /advisor review — fence tracker parser pari…
Aug 4, 2026
f8a80df
fix(cli): address round-5 /advisor review — normalizer corruption gua…
Aug 5, 2026
d5481c8
Merge branch 'main' into feat/advisor-command
yiliang114 Aug 7, 2026
4020893
fix(cli): address advisor review gaps
yiliang114 Aug 7, 2026
43f8710
fix(cli): harden advisor reviews
yiliang114 Aug 8, 2026
2ae879a
Merge branch 'main' into feat/advisor-command
yiliang114 Aug 8, 2026
90fe74b
fix(cli): preserve ACP slash command cancellation
yiliang114 Aug 8, 2026
9c8d1a2
fix(acp): preserve slash command lifecycle
yiliang114 Aug 8, 2026
bb0eab4
Merge remote-tracking branch 'origin/main' into automation/pr-7567-apply
yiliang114 Aug 9, 2026
2d78079
test(acp): verify advisor output without recording
yiliang114 Aug 9, 2026
4143976
fix(cli): scope advisor ACP behavior
yiliang114 Aug 9, 2026
199808d
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 Aug 9, 2026
291d79b
fix(cli): require structured advisor output
yiliang114 Aug 9, 2026
ed8283f
docs(acp): correct slash result support
yiliang114 Aug 9, 2026
c79fef8
fix(cli): honor advisor context and JSON mode
yiliang114 Aug 10, 2026
439c15e
fix(cli): preserve sibling ACP slash command cancellation
yiliang114 Aug 10, 2026
5548ccb
Merge commit '4cf943219db115074e7f4c6bff33984b3d8cef0e' into HEAD
yiliang114 Aug 10, 2026
d534165
merge: sync main into advisor command
yiliang114 Aug 14, 2026
07ede5c
fix(cli): use shared recording skip set
yiliang114 Aug 14, 2026
485726d
fix(cli): close advisor ACP telemetry gaps
yiliang114 Aug 14, 2026
78754a0
fix(cli): keep advisor JSON paths compatible
yiliang114 Aug 14, 2026
baaf4e7
Merge remote-tracking branch 'origin/main' into automation-pr-7567
yiliang114 Aug 15, 2026
5c8110c
fix(cli): close advisor telemetry compile gaps
yiliang114 Aug 15, 2026
f403e65
fix: resolve advisor transcript gate + response_format endpoint compat
yiliang114 Aug 15, 2026
4d2762f
fix: key advisor recording skips on command identity + pin fallbacks
yiliang114 Aug 15, 2026
c8c9ccb
fix(cli): record /clear user-turn before the session switch
yiliang114 Aug 16, 2026
dc4d6d0
chore: merge main into advisor command
yiliang114 Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ The `extra_body` field allows you to add custom parameters to the request body s
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `fastModel` | string | Model used for generating [prompt suggestions](../features/followup-suggestions) and speculative execution. Leave empty to use the main model. A smaller/faster model (e.g., `qwen3-coder-flash`) reduces latency and cost. Can also be set via `/model --fast`. | `""` |

#### advisorModel

| Setting | Type | Description | Default |
| -------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `advisorModel` | string | Model used by [`/advisor`](../features/commands.md#17-second-opinion-advisor) for second-opinion reviews of the conversation. Leave empty to use the main model. A model at least as capable as the main model is recommended. Setting this sends the recent conversation transcript to that model, even when it uses another provider. | `""` |

#### visionModel

| Setting | Type | Description | Default |
Expand Down
73 changes: 68 additions & 5 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,70 @@ The `/btw` command allows you to ask quick side questions without interrupting o
>
> Use `/btw` when you need a quick answer without derailing your main task. It's especially useful for clarifying concepts, checking facts, or getting quick explanations while staying focused on your primary workflow.

### 1.7 Session Recap (`/recap`)
### 1.7 Second Opinion (`/advisor`)

The `/advisor` command runs an independent, read-only review of the conversation so far and returns a structured second opinion — without performing the task or interrupting the main conversation.

| Command | Description |
| ------------------ | -------------------------------------- |
| `/advisor` | Review the conversation above |
| `/advisor <focus>` | Focus the review on a specific concern |

**How It Works:**

- The review is sent as a separate, single-turn API call with recent conversation context (up to the last 40 messages)
- The reviewer model **cannot execute tools** — tools are stripped at the request level (the same mechanism as `/btw`), so the review never writes code or runs commands; every claim must be grounded in the visible transcript
- The main conversation is **not** interrupted; the review is shown only to you
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
- The review is rendered as a boxed markdown block with four fixed sections — **Verdict**, **Risks**, **Missing evidence**, and **Recommendation** — under an `/advisor · <model>` header that names the resolved reviewer model
- Unlike `/btw`, which is fire-and-forget and leaves the session usable, `/advisor` blocks input until the review returns; over a full context window with a strong reviewer this can take tens of seconds
- By default the main model is used; set [`advisorModel`](../configuration/settings.md#advisormodel) to route the review to a different (typically stronger) model — the recent transcript is sent to that model even when it uses another provider

**Example:**

```
> /advisor is my fix for the null check actually correct?

Consulting advisor...

╭──────────────────────────────────────────────────────╮
│ /advisor · qwen3-max │
│ │
│ Verdict │
│ The approach is sound, but the edge case at line 42 │
│ is unverified. │
│ │
│ Risks │
│ - The fix assumes the config is always loaded; a │
│ startup race could leave it null. │
│ │
│ Missing evidence │
│ - No test exercises the null-config path in the │
│ visible transcript. │
│ │
│ Recommendation │
│ Add a focused unit test for the null-config branch │
│ before merging. │
╰──────────────────────────────────────────────────────╯
```

The review renders in a bordered box whose header names the resolved reviewer model. An unknown `advisorModel` is not validated up front — if the provider rejects it, `/advisor` reports the failure, so check the model name; only unresolvable alias selectors (e.g. `fast` with no fast model configured) fall back to the main model. Advisor requests do not use configured model fallbacks.

**Supported Execution Modes:**

| Mode | Behavior |
| -------------------- | --------------------------------------------------- |
| Interactive | Renders the four-section review in the conversation |
| ACP (Agent Protocol) | Returns the review as a message result |

> [!tip]
>
> Use `/advisor` for a second opinion before committing to a direction — it is especially useful for catching flawed assumptions, unverified claims, or risky next steps. Configure `advisorModel` to get the review from a different model than the one driving the main conversation.

> [!note]
>
> `advisorModel` is set in settings only; unlike `fastModel` and `visionModel`, it has no `/model` flag counterpart yet.

### 1.8 Session Recap (`/recap`)

The `/recap` command generates a short "where you left off" summary of the
current session, so you can resume an old conversation without scrolling
Expand Down Expand Up @@ -268,7 +331,7 @@ this setting.
> `general.showSessionRecap` to `true` to enable the auto-trigger; the
> manual `/recap` command always works regardless of this setting.

### 1.8 Diff Viewer (`/diff`)
### 1.9 Diff Viewer (`/diff`)

The `/diff` command opens an interactive diff viewer showing uncommitted changes and per-turn diffs. Use ←/→ to switch between the current git diff and individual conversation turns, ↑/↓ to browse files, and Enter to view inline diffs.

Expand Down Expand Up @@ -373,7 +436,7 @@ Use **Load more** at the bottom to fetch the next page of commits (50 per page).
>
> `/log` requires a git repository workspace. If the workspace is not a git repository or has no commits, the dialog shows a placeholder message.

### 1.9 Information, Settings, and Help
### 1.10 Information, Settings, and Help

Commands for obtaining information and performing system settings.

Expand Down Expand Up @@ -412,7 +475,7 @@ Commands for obtaining information and performing system settings.
>
> `/config` reads and writes individual settings by dot-path key (e.g. `general.vimMode`), complementing the interactive `/settings` editor. Running `/config` with no argument (or `--help`) lists every settable key with its type and current value. `/config <key>` prints the current value — except for boolean keys, where it toggles the value. `/config <key>=<value>` sets the value. Changes are written to user settings (`~/.qwen/settings.json`). Only `boolean`, `string`, `number`, and `enum` settings can be changed this way — `array` and `object` settings must be edited in `settings.json` directly. Sensitive values (API keys, tokens, base URLs) are masked in output, and setting `tools.approvalMode` to `yolo` is blocked.

### 1.10 Common Shortcuts
### 1.11 Common Shortcuts

| Shortcut | Function | Note |
| ------------------ | ----------------------- | ------------------------------------------------------------------------- |
Expand All @@ -422,7 +485,7 @@ Commands for obtaining information and performing system settings.
| `Ctrl/cmd+Z` | Undo input | Text editing |
| `Ctrl/cmd+Shift+Z` | Redo input | Text editing |

### 1.11 Authentication Commands
### 1.12 Authentication Commands

Use `/auth` inside a Qwen Code session to configure authentication. Use `/doctor` to inspect the current authentication and environment status.

Expand Down
218 changes: 218 additions & 0 deletions packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14909,6 +14909,224 @@ describe('Session', () => {
_meta: { source: 'slash_command' },
},
});
expect(
mockChatRecordingService.recordSlashCommand,
).toHaveBeenCalledWith(
expect.objectContaining({ rawCommand: '/compress' }),
);
});

it('does not record /advisor in the ACP transcript', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockResolvedValueOnce({
type: 'message',
messageType: 'info',
content: 'Review complete.',
resolvedCommand: {
name: 'advisor',
kind: CommandKind.BUILT_IN,
},
});
mockChatRecordingService.recordUserMessage.mockClear();
mockChatRecordingService.recordSlashCommand.mockClear();

await session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/advisor' }],
});

expect(
mockChatRecordingService.recordUserMessage,
).not.toHaveBeenCalled();
expect(
mockChatRecordingService.recordSlashCommand,
).not.toHaveBeenCalled();
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'test-session-id',
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'Review complete.' },
_meta: { source: 'slash_command' },
},
});
expect(finishedSpy).toHaveBeenCalledTimes(1);
});

it('keeps replay records for other ACP slash-command messages', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
let finish!: () => void;
const delayed = new Promise<void>((resolve) => {
finish = resolve;
});
let markStarted!: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockImplementationOnce(async () => {
markStarted();
await delayed;
return {
type: 'message',
messageType: 'info',
content: 'Side answer.',
};
});
mockChatRecordingService.recordUserMessage.mockClear();
mockChatRecordingService.recordSlashCommand.mockClear();

const prompt = session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/btw question' }],
});
await started;
await session.cancelPendingPrompt();
finish();

await expect(prompt).resolves.toEqual({ stopReason: 'end_turn' });

expect(finishedSpy).toHaveBeenCalledTimes(1);
expect(mockChatRecordingService.recordUserMessage).toHaveBeenCalledWith(
'/btw question',
);
expect(
mockChatRecordingService.recordSlashCommand,
).toHaveBeenCalledWith(
expect.objectContaining({ rawCommand: '/btw question' }),
);
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'test-session-id',
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'Side answer.' },
_meta: { source: 'slash_command' },
},
});
});

it('returns cancelled when /advisor finishes after cancellation', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
let markStarted!: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockImplementationOnce(async (_input, abortController) => {
markStarted();
await new Promise<void>((resolve) => {
abortController.signal.addEventListener('abort', () => resolve(), {
once: true,
});
});
return {
type: 'message',
messageType: 'error',
content: 'Advisor review failed: aborted',
resolvedCommand: {
name: 'advisor',
kind: CommandKind.BUILT_IN,
},
};
});

const prompt = session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/advisor' }],
});
await started;
await session.cancelPendingPrompt();

await expect(prompt).resolves.toEqual({ stopReason: 'cancelled' });
expect(finishedSpy).toHaveBeenCalledTimes(1);
});

it('records completion when /advisor returns an error message', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockResolvedValueOnce({
type: 'message',
messageType: 'error',
content: 'Advisor review failed: provider rejected schema',
resolvedCommand: {
name: 'advisor',
kind: CommandKind.BUILT_IN,
},
});

await expect(
session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/advisor' }],
}),
).rejects.toThrow('Advisor review failed: provider rejected schema');

expect(finishedSpy).toHaveBeenCalledTimes(1);
});

it('records a custom command shadowing the advisor name', async () => {
// R18-6: the recording gate must classify by the RESOLVED command,
// not the raw token — a user-defined `advisor` command keeps its
// user-turn record while the built-in advisor's is skipped.
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockResolvedValueOnce({
type: 'submit_prompt',
content: [{ text: 'Shadowed advisor prompt' }],
resolvedCommand: {
name: 'advisor',
kind: CommandKind.FILE,
},
});
mockChatRecordingService.recordUserMessage.mockClear();

await session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/advisor check my work' }],
});

expect(mockChatRecordingService.recordUserMessage).toHaveBeenCalled();
});

it('preserves an expanded slash prompt cancelled before model send', async () => {
const finishedSpy = vi
.spyOn(core, 'logConversationFinishedEvent')
.mockImplementation(() => {});
vi.mocked(
nonInteractiveCliCommands.handleSlashCommand,
).mockImplementationOnce(async (_input, abortController) => {
abortController.abort();
return {
type: 'submit_prompt',
content: [{ text: 'Expanded prompt' }],
};
});

await expect(
session.prompt({
sessionId: 'test-session-id',
prompt: [{ type: 'text', text: '/custom' }],
}),
).resolves.toEqual({ stopReason: 'cancelled' });

expect(mockChat.addHistory).toHaveBeenCalledWith({
role: 'user',
parts: [{ text: 'Expanded prompt' }],
});
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
expect(finishedSpy).toHaveBeenCalledTimes(1);
});

it('marks streamed slash-command messages with their source', async () => {
Expand Down
Loading
Loading