mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
feat(plan): add "Yes, restore previous mode" option when exiting plan mode (#3008)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(plan): add "Yes, restore previous mode" option when exiting plan mode When exiting plan mode, users previously had no way to restore their original approval mode (e.g. YOLO). Add a new default option that restores the pre-plan approval mode, with a dynamic label showing which mode will be restored. Closes #3002 * test: add fallback test for RestorePrevious when no prePlanMode recorded * fix: handle RestorePrevious in telemetry and ACP mode notification - Add RestorePrevious to telemetry decision mapping as ACCEPT - Fix sendCurrentModeUpdateNotification to read actual mode for RestorePrevious instead of defaulting to 'default' * test: add plan confirmation tests for RestorePrevious in permissionUtils
This commit is contained in:
parent
1356c05e3f
commit
9c0bbfba6c
14 changed files with 142 additions and 0 deletions
|
|
@ -697,6 +697,10 @@ export class Session implements SessionContext {
|
|||
case ToolConfirmationOutcome.ProceedAlways:
|
||||
newModeId = 'auto-edit';
|
||||
break;
|
||||
case ToolConfirmationOutcome.RestorePrevious:
|
||||
// onConfirm has already restored the mode; read the actual current mode
|
||||
newModeId = this.config.getApprovalMode() as ApprovalModeValue;
|
||||
break;
|
||||
case ToolConfirmationOutcome.ProceedOnce:
|
||||
default:
|
||||
newModeId = 'default';
|
||||
|
|
@ -1045,6 +1049,7 @@ export class Session implements SessionContext {
|
|||
case ToolConfirmationOutcome.ProceedAlwaysServer:
|
||||
case ToolConfirmationOutcome.ProceedAlwaysTool:
|
||||
case ToolConfirmationOutcome.ModifyWithEditor:
|
||||
case ToolConfirmationOutcome.RestorePrevious:
|
||||
break;
|
||||
default: {
|
||||
const resultOutcome: never = outcome;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,49 @@ describe('permissionUtils', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('returns plan options with RestorePrevious including prePlanMode', () => {
|
||||
const options = toPermissionOptions({
|
||||
type: 'plan',
|
||||
title: 'Would you like to proceed?',
|
||||
plan: 'Test plan',
|
||||
prePlanMode: 'yolo',
|
||||
onConfirm: async () => undefined,
|
||||
});
|
||||
|
||||
expect(options).toHaveLength(4);
|
||||
expect(options[0]).toMatchObject({
|
||||
optionId: ToolConfirmationOutcome.RestorePrevious,
|
||||
name: 'Yes, restore previous mode (yolo)',
|
||||
kind: 'allow_once',
|
||||
});
|
||||
expect(options[1]).toMatchObject({
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
name: 'Yes, and auto-accept edits',
|
||||
});
|
||||
expect(options[2]).toMatchObject({
|
||||
optionId: ToolConfirmationOutcome.ProceedOnce,
|
||||
name: 'Yes, and manually approve edits',
|
||||
});
|
||||
expect(options[3]).toMatchObject({
|
||||
optionId: ToolConfirmationOutcome.Cancel,
|
||||
name: 'No, keep planning (esc)',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults prePlanMode to "default" when not provided in plan options', () => {
|
||||
const options = toPermissionOptions({
|
||||
type: 'plan',
|
||||
title: 'Would you like to proceed?',
|
||||
plan: 'Test plan',
|
||||
onConfirm: async () => undefined,
|
||||
});
|
||||
|
||||
expect(options[0]).toMatchObject({
|
||||
optionId: ToolConfirmationOutcome.RestorePrevious,
|
||||
name: 'Yes, restore previous mode (default)',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to rootCommand when exec permissionRules are unavailable', () => {
|
||||
const options = toPermissionOptions({
|
||||
type: 'exec',
|
||||
|
|
|
|||
|
|
@ -171,6 +171,11 @@ export function toPermissionOptions(
|
|||
);
|
||||
case 'plan':
|
||||
return [
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.RestorePrevious,
|
||||
name: `Yes, restore previous mode (${confirmation.prePlanMode ?? 'default'})`,
|
||||
kind: 'allow_once',
|
||||
},
|
||||
{
|
||||
optionId: ToolConfirmationOutcome.ProceedAlways,
|
||||
name: 'Yes, and auto-accept edits',
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,8 @@ export default {
|
|||
'Always allow for this user': 'Für diesen Benutzer immer erlauben',
|
||||
'Always allow {{action}} for this user':
|
||||
'{{action}} für diesen Benutzer immer erlauben',
|
||||
'Yes, restore previous mode ({{mode}})':
|
||||
'Ja, vorherigen Modus wiederherstellen ({{mode}})',
|
||||
'Yes, and auto-accept edits': 'Ja, und Änderungen automatisch akzeptieren',
|
||||
'Yes, and manually approve edits': 'Ja, und Änderungen manuell genehmigen',
|
||||
'No, keep planning (esc)': 'Nein, weiter planen (Esc)',
|
||||
|
|
|
|||
|
|
@ -1236,6 +1236,8 @@ export default {
|
|||
'Always allow for this user': 'Always allow for this user',
|
||||
'Always allow {{action}} for this user':
|
||||
'Always allow {{action}} for this user',
|
||||
'Yes, restore previous mode ({{mode}})':
|
||||
'Yes, restore previous mode ({{mode}})',
|
||||
'Yes, and auto-accept edits': 'Yes, and auto-accept edits',
|
||||
'Yes, and manually approve edits': 'Yes, and manually approve edits',
|
||||
'No, keep planning (esc)': 'No, keep planning (esc)',
|
||||
|
|
|
|||
|
|
@ -918,6 +918,8 @@ export default {
|
|||
'このプロジェクトで{{action}}を常に許可',
|
||||
'Always allow for this user': 'このユーザーに常に許可',
|
||||
'Always allow {{action}} for this user': 'このユーザーに{{action}}を常に許可',
|
||||
'Yes, restore previous mode ({{mode}})':
|
||||
'はい、以前のモードに戻す ({{mode}})',
|
||||
'Yes, and auto-accept edits': 'はい、編集を自動承認',
|
||||
'Yes, and manually approve edits': 'はい、編集を手動承認',
|
||||
'No, keep planning (esc)': 'いいえ、計画を続ける (Esc)',
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,8 @@ export default {
|
|||
'Always allow for this user': 'Sempre permitir para este usuário',
|
||||
'Always allow {{action}} for this user':
|
||||
'Sempre permitir {{action}} para este usuário',
|
||||
'Yes, restore previous mode ({{mode}})':
|
||||
'Sim, restaurar modo anterior ({{mode}})',
|
||||
'Yes, and auto-accept edits': 'Sim, e aceitar edições automaticamente',
|
||||
'Yes, and manually approve edits': 'Sim, e aprovar edições manualmente',
|
||||
'No, keep planning (esc)': 'Não, continuar planejando (esc)',
|
||||
|
|
|
|||
|
|
@ -1114,6 +1114,8 @@ export default {
|
|||
'Always allow for this user': 'Всегда разрешать для этого пользователя',
|
||||
'Always allow {{action}} for this user':
|
||||
'Всегда разрешать {{action}} для этого пользователя',
|
||||
'Yes, restore previous mode ({{mode}})':
|
||||
'Да, восстановить предыдущий режим ({{mode}})',
|
||||
'Yes, and auto-accept edits': 'Да, и автоматически принимать правки',
|
||||
'Yes, and manually approve edits': 'Да, и вручную подтверждать правки',
|
||||
'No, keep planning (esc)': 'Нет, продолжить планирование (esc)',
|
||||
|
|
|
|||
|
|
@ -1170,6 +1170,7 @@ export default {
|
|||
'Always allow {{action}} in this project': '在本项目中总是允许{{action}}',
|
||||
'Always allow for this user': '对该用户总是允许',
|
||||
'Always allow {{action}} for this user': '对该用户总是允许{{action}}',
|
||||
'Yes, restore previous mode ({{mode}})': '是,恢复之前的模式 ({{mode}})',
|
||||
'Yes, and auto-accept edits': '是,并自动接受编辑',
|
||||
'Yes, and manually approve edits': '是,并手动批准编辑',
|
||||
'No, keep planning (esc)': '否,继续规划 (esc)',
|
||||
|
|
|
|||
|
|
@ -302,6 +302,13 @@ export const ToolConfirmationMessage: React.FC<
|
|||
const planProps = confirmationDetails;
|
||||
|
||||
question = planProps.title;
|
||||
options.push({
|
||||
key: 'restore-previous',
|
||||
label: t('Yes, restore previous mode ({{mode}})', {
|
||||
mode: planProps.prePlanMode ?? 'default',
|
||||
}),
|
||||
value: ToolConfirmationOutcome.RestorePrevious,
|
||||
});
|
||||
options.push({
|
||||
key: 'proceed-always',
|
||||
label: t('Yes, and auto-accept edits'),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function getDecisionFromOutcome(
|
|||
): ToolCallDecision {
|
||||
switch (outcome) {
|
||||
case ToolConfirmationOutcome.ProceedOnce:
|
||||
case ToolConfirmationOutcome.RestorePrevious:
|
||||
return ToolCallDecision.ACCEPT;
|
||||
case ToolConfirmationOutcome.ProceedAlways:
|
||||
case ToolConfirmationOutcome.ProceedAlwaysServer:
|
||||
|
|
|
|||
|
|
@ -201,6 +201,66 @@ describe('ExitPlanModeTool', () => {
|
|||
expect(approvalMode).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should restore pre-plan mode on RestorePrevious', async () => {
|
||||
(mockConfig.getPrePlanMode as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
ApprovalMode.YOLO,
|
||||
);
|
||||
|
||||
const params: ExitPlanModeParams = { plan: 'Restore previous test' };
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const confirmation = await invocation.getConfirmationDetails(signal);
|
||||
|
||||
if (confirmation) {
|
||||
await confirmation.onConfirm(ToolConfirmationOutcome.RestorePrevious);
|
||||
}
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.YOLO,
|
||||
);
|
||||
expect(approvalMode).toBe(ApprovalMode.YOLO);
|
||||
});
|
||||
|
||||
it('should include prePlanMode in confirmation details', async () => {
|
||||
(mockConfig.getPrePlanMode as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
|
||||
const params: ExitPlanModeParams = { plan: 'Test plan' };
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const confirmation = await invocation.getConfirmationDetails(signal);
|
||||
|
||||
expect(confirmation).toMatchObject({
|
||||
type: 'plan',
|
||||
prePlanMode: ApprovalMode.AUTO_EDIT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to DEFAULT on RestorePrevious when no prePlanMode recorded', async () => {
|
||||
// getPrePlanMode() defaults to DEFAULT when prePlanMode is undefined
|
||||
(mockConfig.getPrePlanMode as ReturnType<typeof vi.fn>).mockReturnValue(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
|
||||
const params: ExitPlanModeParams = { plan: 'Fallback test' };
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const confirmation = await invocation.getConfirmationDetails(signal);
|
||||
|
||||
if (confirmation) {
|
||||
await confirmation.onConfirm(ToolConfirmationOutcome.RestorePrevious);
|
||||
}
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
expect(approvalMode).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should remain in plan mode when confirmation is rejected', async () => {
|
||||
const params: ExitPlanModeParams = {
|
||||
plan: 'Remain in planning',
|
||||
|
|
|
|||
|
|
@ -87,12 +87,18 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
|
|||
override async getConfirmationDetails(
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolPlanConfirmationDetails> {
|
||||
const prePlanMode = this.config.getPrePlanMode();
|
||||
const details: ToolPlanConfirmationDetails = {
|
||||
type: 'plan',
|
||||
title: 'Would you like to proceed?',
|
||||
plan: this.params.plan,
|
||||
prePlanMode,
|
||||
onConfirm: async (outcome: ToolConfirmationOutcome) => {
|
||||
switch (outcome) {
|
||||
case ToolConfirmationOutcome.RestorePrevious:
|
||||
this.wasApproved = true;
|
||||
this.setApprovalModeSafely(prePlanMode);
|
||||
break;
|
||||
case ToolConfirmationOutcome.ProceedAlways:
|
||||
this.wasApproved = true;
|
||||
this.setApprovalModeSafely(ApprovalMode.AUTO_EDIT);
|
||||
|
|
|
|||
|
|
@ -667,6 +667,8 @@ export interface ToolPlanConfirmationDetails {
|
|||
/** @see ToolEditConfirmationDetails.hideAlwaysAllow */
|
||||
hideAlwaysAllow?: boolean;
|
||||
plan: string;
|
||||
/** The approval mode that was active before entering plan mode (for display in the UI). */
|
||||
prePlanMode?: string;
|
||||
onConfirm: (
|
||||
outcome: ToolConfirmationOutcome,
|
||||
payload?: ToolConfirmationPayload,
|
||||
|
|
@ -711,6 +713,8 @@ export enum ToolConfirmationOutcome {
|
|||
/** Persist the permission rule to the user settings (user scope). */
|
||||
ProceedAlwaysUser = 'proceed_always_user',
|
||||
ModifyWithEditor = 'modify_with_editor',
|
||||
/** Restore the approval mode that was active before entering plan mode. */
|
||||
RestorePrevious = 'restore_previous',
|
||||
Cancel = 'cancel',
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue