Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
90a67a7
fix(cli): improve slash command history feedback
DragonnZhang Aug 2, 2026
e7183bb
fix(cli): centralize auxiliary model guard
DragonnZhang Aug 2, 2026
facb38f
test(cli): cover model picker left key
DragonnZhang Aug 2, 2026
296961e
Merge branch 'main' into dragon/slash-command-feedback
wenshao Aug 2, 2026
d7a14bf
fix(cli): persist slash command invocation hiding across resume
DragonnZhang Aug 7, 2026
ce6fbb9
Merge remote-tracking branch 'upstream/main' into dragon/slash-comman…
DragonnZhang Aug 7, 2026
4b2b65f
Merge branch 'main' into dragon/slash-command-feedback
qwen-code-dev-bot Aug 7, 2026
8d19d46
fix: keep model picker feedback consistent
DragonnZhang Aug 7, 2026
81618a6
docs(cli): align design doc with /stats root-level hiding
qwen-code-dev-bot Aug 7, 2026
15227e1
fix(cli): preserve model picker history safely
DragonnZhang Aug 7, 2026
d1140e6
fix: preserve hidden command feedback on resume
DragonnZhang Aug 7, 2026
90f0145
fix(desktop): match slash results to invocations
DragonnZhang Aug 7, 2026
c1dfe04
fix: keep slash command feedback paired with invocations
Aug 7, 2026
b44d4d3
fix(desktop): bound slash result lookup to user turn
DragonnZhang Aug 7, 2026
deed974
test(cli): cover hidden command result replay on resume
DragonnZhang Aug 7, 2026
24b12bf
Merge main into dragon/slash-command-feedback
DragonnZhang Aug 11, 2026
c2ab3fc
Merge remote-tracking branch 'origin/main' into dragon/slash-command-…
qwen-code-dev-bot Aug 11, 2026
cf3cc8c
Merge branch 'main' into dragon/slash-command-feedback
qwen-code-dev-bot Aug 11, 2026
64fc673
fix(cli): guard dialog result recording
DragonnZhang Aug 11, 2026
751deb0
fix(cli,desktop): tighten slash-command feedback recording
qwen-code-dev-bot Aug 11, 2026
a3df7ee
Merge remote-tracking branch 'origin/main' into dragon/slash-command-…
qwen-code-dev-bot Aug 11, 2026
0502eed
chore(cli): merge main and reconcile /effort feedback recording
qwen-code-dev-bot Aug 12, 2026
cd49a42
fix(cli): guard partially persisted model switches
DragonnZhang Aug 12, 2026
92d1dc3
Merge branch 'main' into dragon/slash-command-feedback
qwen-code-dev-bot Aug 12, 2026
b6b0cc9
test(cli): cover isPickerOnlyModelInvocation regex boundaries directly
qwen-code-dev-bot Aug 12, 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
40 changes: 40 additions & 0 deletions docs/design/slash-command-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Slash command history feedback

## Problem

Interactive slash commands are added to the TUI history before their action is
known. Commands that only open a dialog can therefore leave a bare invocation
behind after the dialog closes. The model picker has the same problem when it
is dismissed without a selection.

## Design

- Do not add the built-in `/auth`, `/settings`, `/status`, `/help`, `/theme`,
`/editor`, `/diff`, or `/stats` invocations to visible TUI history. Bare
`/effort`, `/model`, and `/statusline` pickers are hidden too. Their existing
UI remains unchanged, as do chat recording and slash-command telemetry. User
and project commands that override those names keep their invocation
history.
- Root matches apply to the bare command only; subcommands keep their
invocation because they perform work (for example `/status paths` prints
session paths).
- Resolve the command before adding its invocation so aliases use the canonical
command name for this decision.
- Preserve invocations for commands that directly perform work, change session
state, write data, or enter a management/security workflow. Argument-sensitive
commands only hide their bare picker form; for example, `/effort` is hidden
while `/effort high` remains visible, and `/model` is hidden while
`/model <id>` remains visible.
- Commands that fail before opening their dialog keep the invocation paired
with the failure message: `/theme` under `NO_COLOR` is not hidden because it
prints feedback instead of opening the picker, and a hidden picker-shaped
`/model` invocation is revealed when its arguments are rejected.
- Record the hiding decision in the chat record (`hiddenInvocation`) so
`/resume`, `/branch`, and session previews reconstruct the same history the
live session displayed instead of bringing the bare invocation row back.
- When the primary model picker is dismissed without a selection, add an info
message identifying the unchanged model. Successful selections keep their
existing feedback. The other pickers leave no trace when dismissed; the
model picker states the outcome explicitly because the active model is
session-critical and otherwise invisible in history, so a silent close would
leave it ambiguous whether the model changed.
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -2835,4 +2835,5 @@ export default {
"Els canvis del gestor d'habilitats automàtiques estan desactivats en mode segur.",
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
"Els canvis del gestor d'habilitats automàtiques només estan disponibles en espais de treball de confiança. Marca aquesta carpeta com a fiable amb `/trust` i torna-ho a provar.",
'Kept model as {{model}}': 'Model mantingut com a {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -2316,4 +2316,5 @@ export default {
'Änderungen durch den Auto-Skill-Kurator sind im Sicherheitsmodus deaktiviert.',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Änderungen durch den Auto-Skill-Kurator sind nur in vertrauenswürdigen Arbeitsbereichen verfügbar. Stufen Sie diesen Ordner mit `/trust` als vertrauenswürdig ein und versuchen Sie es erneut.',
'Kept model as {{model}}': 'Modell als {{model}} beibehalten',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2827,4 +2827,5 @@ export default {
'Auto-skill curator changes are disabled in safe mode.',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.',
'Kept model as {{model}}': 'Kept model as {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -2321,4 +2321,5 @@ export default {
'Les modifications du gestionnaire de compétences automatiques sont désactivées en mode sécurisé.',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Les modifications du gestionnaire de compétences automatiques ne sont disponibles que dans les espaces de travail approuvés. Marquez ce dossier comme approuvé avec `/trust`, puis réessayez.',
'Kept model as {{model}}': 'Modèle conservé : {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -2082,4 +2082,5 @@ export default {
'セーフモードでは自動スキル管理による変更は無効です。',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'自動スキル管理による変更は信頼済みのワークスペースでのみ利用できます。`/trust` でこのフォルダーを信頼してから、もう一度お試しください。',
'Kept model as {{model}}': 'モデルは {{model}} のままです',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2300,4 +2300,5 @@ export default {
'As alterações do gerenciador de habilidades automáticas estão desativadas no modo seguro.',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'As alterações do gerenciador de habilidades automáticas estão disponíveis apenas em espaços de trabalho confiáveis. Marque esta pasta como confiável usando `/trust` e tente novamente.',
'Kept model as {{model}}': 'Modelo mantido como {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -2291,4 +2291,5 @@ export default {
'Изменения куратора автоматических навыков отключены в безопасном режиме.',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'Изменения куратора автоматических навыков доступны только в доверенных рабочих пространствах. Сделайте эту папку доверенной с помощью `/trust` и повторите попытку.',
'Kept model as {{model}}': 'Оставлена модель {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -2406,4 +2406,5 @@ export default {
'安全模式下禁止變更自動技能管理器。',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'只有受信任的工作區可以變更自動技能管理器。請透過 `/trust` 信任此資料夾後再試一次。',
'Kept model as {{model}}': '模型保持為 {{model}}',
};
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -2608,4 +2608,5 @@ export default {
'安全模式下禁止更改自动技能管理器。',
'Auto-skill curator changes are only available in trusted workspaces. Trust this folder via `/trust` and try again.':
'仅受信任的工作区可以更改自动技能管理器。请通过 `/trust` 信任此文件夹后重试。',
'Kept model as {{model}}': '模型保持为 {{model}}',
};
8 changes: 7 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1428,6 +1428,7 @@ export const AppContainer = (props: AppContainerProps) => {
setThemeError,
historyManager.addItem,
initializationResult.themeError,
config,
);

const {
Expand Down Expand Up @@ -1487,7 +1488,12 @@ export const AppContainer = (props: AppContainerProps) => {
openEditorDialog,
handleEditorSelect,
exitEditorDialog,
} = useEditorSettings(settings, setEditorError, historyManager.addItem);
} = useEditorSettings(
settings,
setEditorError,
historyManager.addItem,
config,
);
Comment on lines +1491 to +1496

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] The new config wiring from AppContainer into useEditorSettings (and identically into useThemeCommand at ~1426-1431) has no test: AppContainer.test.tsx mocks both hooks wholesale (vi.mock('./hooks/useThemeCommand.js'), vi.mock('./hooks/useEditorSettings.js')), and the parameter is optional (config?: Config), so nothing type-checks the wiring either. — Failure scenario: a future change that drops the config argument from either call site compiles and leaves every test green (the hook-level tests inject their own config), while /editor selection feedback and /theme-under-NO_COLOR feedback silently stop calling recordSlashCommand — resumed/branched sessions and desktop reconstruction then lose those outcome lines with no error anywhere.

// AppContainer.test.tsx — assert the hooks receive the config instance:
expect(mockUseEditorSettings).toHaveBeenCalledWith(
  mockLoadedSettings,
  mockSetEditorError,
  mockAddItem,
  config,
);
// (and the same for mockUseThemeCommand's trailing arg)
中文说明

[Suggestion] AppContainer 向 useEditorSettings 传入 config 的新接线(以及 ~1426-1431 处 useThemeCommand 的相同接线)没有任何测试:AppContainer.test.tsx 对这两个 hook 做了整体 mock(vi.mock('./hooks/useThemeCommand.js')vi.mock('./hooks/useEditorSettings.js')),且该参数是可选的(config?: Config),因此类型检查也无法兜住这个接线。— 失败场景:未来某个改动把任一调用点的 config 参数删掉后,编译通过、所有测试仍为绿色(hook 层测试自带 config 注入),而 /editor 选择反馈与 NO_COLOR 下的 /theme 反馈会静默停止调用 recordSlashCommand——resume/branch 会话与桌面端重建将丢失这些结果行,且任何地方都不会报错。

修复见英文部分代码块:在 AppContainer.test.tsx 中断言两个 hook mock 的末尾参数是 config 实例。

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


const { isSettingsDialogOpen, openSettingsDialog, closeSettingsDialog } =
useSettingsCommand();
Expand Down
74 changes: 72 additions & 2 deletions packages/cli/src/ui/auth/useAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const createSettings = () => ({
})),
});

const createConfig = () => {
const createConfig = (recordSlashCommand = vi.fn()) => {
const modelsConfig = {
syncAfterAuthRefresh: vi.fn(),
};
Expand All @@ -68,6 +68,7 @@ const createConfig = () => {
reloadModelProvidersConfig: vi.fn(),
refreshAuth: vi.fn(async () => undefined),
getModelsConfig: vi.fn(() => modelsConfig),
getChatRecordingService: vi.fn(() => ({ recordSlashCommand })),
};
};

Expand Down Expand Up @@ -99,7 +100,8 @@ describe('useAuthCommand', () => {

it('configures DeepSeek via the unified provider submit', async () => {
const settings = createSettings();
const config = createConfig();
const recordSlashCommand = vi.fn();
const config = createConfig(recordSlashCommand);
const addItem = vi.fn();

const { result } = renderHook(() =>
Expand All @@ -112,6 +114,10 @@ describe('useAuthCommand', () => {
modelIds: ['deepseek-v4-flash', 'deepseek-v4-pro'],
};

act(() => {
result.current.openAuthDialog();
});

await act(async () => {
await result.current.handleProviderSubmit(deepseekProvider, inputs);
});
Expand Down Expand Up @@ -139,6 +145,70 @@ describe('useAuthCommand', () => {
}),
expect.any(Number),
);
expect(recordSlashCommand).toHaveBeenCalledWith({
phase: 'result',
rawCommand: '/auth',
outputHistoryItems: [
expect.objectContaining({
text: expect.stringContaining('Successfully configured DeepSeek'),
}),
],
});
});

it('keeps live feedback but skips the /auth record when the dialog auto-opened', async () => {
const settings = createSettings();
const recordSlashCommand = vi.fn();
const config = createConfig(recordSlashCommand);
const addItem = vi.fn();

const { result } = renderHook(() =>
useAuthCommand(settings as never, config as never, addItem),
);

await act(async () => {
await result.current.handleProviderSubmit(deepseekProvider, {
baseUrl: resolveBaseUrl(deepseekProvider),
apiKey: 'sk-deepseek',
modelIds: ['deepseek-v4-flash'],
});
});

expect(addItem).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('Successfully configured DeepSeek'),
}),
expect.any(Number),
);
expect(recordSlashCommand).not.toHaveBeenCalled();
});

it('clears the /auth recording latch when a command-opened dialog closes', async () => {
const settings = createSettings();
const recordSlashCommand = vi.fn();
const config = createConfig(recordSlashCommand);
const addItem = vi.fn();

const { result } = renderHook(() =>
useAuthCommand(settings as never, config as never, addItem),
);

act(() => {
result.current.openAuthDialog();
result.current.closeAuthDialog();
result.current.onAuthError('later unauthorized');
});

await act(async () => {
await result.current.handleProviderSubmit(deepseekProvider, {
baseUrl: resolveBaseUrl(deepseekProvider),
apiKey: 'sk-deepseek',
modelIds: ['deepseek-v4-flash'],
});
});

expect(addItem).toHaveBeenCalledTimes(1);
expect(recordSlashCommand).not.toHaveBeenCalled();
});

it('configures OpenRouter via the unified provider submit', async () => {
Expand Down
34 changes: 23 additions & 11 deletions packages/cli/src/ui/auth/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type ProviderConfig,
type ProviderSetupInputs,
} from '@qwen-code/qwen-code-core';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { LoadedSettings } from '../../config/settings.js';
import { createLoadedSettingsAdapter } from '../../config/loadedSettingsAdapter.js';
import { useQwenAuth } from '../hooks/useQwenAuth.js';
Expand Down Expand Up @@ -106,6 +106,10 @@ export const useAuthCommand = (
isAuthenticating,
);

// The dialog also auto-opens at startup when unauthenticated; only a
// command-opened dialog has an /auth invocation record to pair with.
const openedViaCommandRef = useRef(false);

// -- Shared helpers -------------------------------------------------------

const onAuthError = useCallback(
Expand Down Expand Up @@ -181,16 +185,22 @@ export const useAuthCommand = (

completeAuthentication();

addItem(
{
type: MessageType.INFO,
text: t(
'Successfully configured {{provider}}. Use /model to switch models.',
{ provider: providerConfig.label },
),
},
Date.now(),
);
const feedbackItem: HistoryItemWithoutId & Record<string, unknown> = {
type: MessageType.INFO,
text: t(
'Successfully configured {{provider}}. Use /model to switch models.',
{ provider: providerConfig.label },
),
};
addItem(feedbackItem, Date.now());
if (openedViaCommandRef.current) {
openedViaCommandRef.current = false;
Comment thread
DragonnZhang marked this conversation as resolved.
config.getChatRecordingService?.()?.recordSlashCommand({
phase: 'result',
rawCommand: '/auth',
outputHistoryItems: [feedbackItem],
});
}

logAuth(config, new AuthEvent(protocol, 'manual', 'success'));
} catch (error) {
Expand All @@ -205,10 +215,12 @@ export const useAuthCommand = (
// -- Dialog open / close / cancel ----------------------------------------

const openAuthDialog = useCallback(() => {
openedViaCommandRef.current = true;
setIsAuthDialogOpen(true);
}, []);

const closeAuthDialog = useCallback(() => {
openedViaCommandRef.current = false;
setIsAuthDialogOpen(false);
setAuthError(null);
}, []);
Expand Down
34 changes: 33 additions & 1 deletion packages/cli/src/ui/commands/modelCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { modelCommand } from './modelCommand.js';
import { modelCommand, isPickerOnlyModelInvocation } from './modelCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { SettingScope } from '../../config/settings.js';
Expand Down Expand Up @@ -2643,3 +2643,35 @@ describe('modelCommand', () => {
});
});
});

describe('isPickerOnlyModelInvocation', () => {
it.each([
'',
' ',
'--fast',
'--voice',
'--vision',
'--compaction',
'--image',
'--project',
'--global',
'--fast --project',
'--vision --global',
' --fast --voice ',
])('treats %j as picker-only', (args) => {
expect(isPickerOnlyModelInvocation(args)).toBe(true);
});

it.each([
'--fast qwen3-coder-flash',
'--vision qwen-vl-max',
'--project qwen-max',
'--fast --global qwen-max',
'--invalid-flag',
'--fastx',
'qwen-max',
'qwen-max write a one-off prompt',
])('treats %j as not picker-only', (args) => {
expect(isPickerOnlyModelInvocation(args)).toBe(false);
});
});
18 changes: 18 additions & 0 deletions packages/cli/src/ui/commands/modelCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ const COMPACTION_MODEL_CONFIGURATION_HINT =
const IMAGE_MODEL_CONFIGURATION_HINT =
'Configure a model with imageOnly: true, baseUrl, and envKey in settings.modelProviders. Run /model --image <model-id> to select it.';

const MODEL_PICKER_FLAGS = [
'fast',
'voice',
'vision',
'compaction',
'image',
'project',
'global',
] as const;
const MODEL_PICKER_FLAG_PATTERN = `(?:${MODEL_PICKER_FLAGS.join('|')})`;
const MODEL_PICKER_ONLY_PATTERN = new RegExp(
`^(?:\\s*--${MODEL_PICKER_FLAG_PATTERN})*\\s*$`,
);

export function isPickerOnlyModelInvocation(args: string): boolean {
Comment thread
DragonnZhang marked this conversation as resolved.
return MODEL_PICKER_ONLY_PATTERN.test(args);
}
Comment on lines +64 to +66

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]**R5-2: The picker-only classification is never tested for the flag-plus-model-id shape (/model --fast qwen3-max); every tested input is either flags-only (hidden) or flag-free (/model qwen3-max, kept), and there is no unit test of isPickerOnlyModelInvocation anywhere. — Failure scenario: the trailing \s*$ anchor is the only thing keeping /model --fast qwen3-max (a real auxiliary-model switch that performs work) visible — a measured plausible relaxation mutation keeps every existing test green while classifying flag+id invocations as picker-only, silently hiding work-performing invocations and contradicting the design doc's 'commands that directly perform work keep their invocation'

// add a keep-case to the direct-action it.each, e.g.
['/model --fast qwen3-max', 'model'],
// or unit-test:
// isPickerOnlyModelInvocation('--fast qwen3-max') === false
中文说明

picker-only 分类从未测试过 flag+模型 id 形态(/model --fast qwen3-max);所有被测输入要么是纯 flag(隐藏)、要么不含 flag(/model qwen3-max,保留),且任何地方都没有 isPickerOnlyModelInvocation 的单元测试。— 具体代价:结尾的 \s*$ 锚点是让 /model --fast qwen3-max(执行实际工作的辅助模型切换)保持可见的唯一防线——已实测一个合理的放宽变异可让全部现有测试保持绿色、同时把 flag+id 调用归为 picker-only,从而静默隐藏执行工作的调用,违反设计文档"直接执行工作的命令保留 invocation"。建议修复:在 direct-action it.each 中补一个保留用例(如 ['/model --fast qwen3-max', 'model']),或直接单测 isPickerOnlyModelInvocation('--fast qwen3-max') === false

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


/**
* Parse --project / --global scope flags from the argument string.
* Returns the resolved scope override and the remaining args with flags stripped.
Expand Down
Loading
Loading