mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat: Implement Plan Mode for Safe Code Planning (#658)
This commit is contained in:
@@ -21,15 +21,20 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
|
||||
let subText = '';
|
||||
|
||||
switch (approvalMode) {
|
||||
case ApprovalMode.PLAN:
|
||||
textColor = Colors.AccentBlue;
|
||||
textContent = 'plan mode';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
textColor = Colors.AccentGreen;
|
||||
textContent = 'accepting edits';
|
||||
subText = ' (shift + tab to toggle)';
|
||||
textContent = 'auto-accept edits';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
textColor = Colors.AccentRed;
|
||||
textContent = 'YOLO mode';
|
||||
subText = ' (ctrl + y to toggle)';
|
||||
subText = ' (shift + tab to cycle)';
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
default:
|
||||
|
||||
@@ -133,12 +133,6 @@ export const Help: React.FC<Help> = ({ commands }) => (
|
||||
</Text>{' '}
|
||||
- Open input in external editor
|
||||
</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
Ctrl+Y
|
||||
</Text>{' '}
|
||||
- Toggle YOLO mode
|
||||
</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
Enter
|
||||
@@ -155,7 +149,7 @@ export const Help: React.FC<Help> = ({ commands }) => (
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
Shift+Tab
|
||||
</Text>{' '}
|
||||
- Toggle auto-accepting edits
|
||||
- Cycle approval modes
|
||||
</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
|
||||
41
packages/cli/src/ui/components/PlanSummaryDisplay.tsx
Normal file
41
packages/cli/src/ui/components/PlanSummaryDisplay.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
|
||||
import { Colors } from '../colors.js';
|
||||
import type { PlanResultDisplay } from '@qwen-code/qwen-code-core';
|
||||
|
||||
interface PlanSummaryDisplayProps {
|
||||
data: PlanResultDisplay;
|
||||
availableHeight?: number;
|
||||
childWidth: number;
|
||||
}
|
||||
|
||||
export const PlanSummaryDisplay: React.FC<PlanSummaryDisplayProps> = ({
|
||||
data,
|
||||
availableHeight,
|
||||
childWidth,
|
||||
}) => {
|
||||
const { message, plan } = data;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text color={Colors.AccentGreen} wrap="wrap">
|
||||
{message}
|
||||
</Text>
|
||||
</Box>
|
||||
<MarkdownDisplay
|
||||
text={plan}
|
||||
isPending={false}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { EOL } from 'node:os';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import type {
|
||||
ToolCallConfirmationDetails,
|
||||
@@ -66,6 +67,30 @@ describe('ToolConfirmationMessage', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render plan confirmation with markdown plan content', () => {
|
||||
const confirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'plan',
|
||||
title: 'Would you like to proceed?',
|
||||
plan: '# Implementation Plan\n- Step one\n- Step two'.replace(/\n/g, EOL),
|
||||
onConfirm: vi.fn(),
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationMessage
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Yes, and auto-accept edits');
|
||||
expect(lastFrame()).toContain('Yes, and manually approve edits');
|
||||
expect(lastFrame()).toContain('No, keep planning');
|
||||
expect(lastFrame()).toContain('Implementation Plan');
|
||||
expect(lastFrame()).toContain('Step one');
|
||||
});
|
||||
|
||||
describe('with folder trust', () => {
|
||||
const editConfirmationDetails: ToolCallConfirmationDetails = {
|
||||
type: 'edit',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import type {
|
||||
ToolCallConfirmationDetails,
|
||||
ToolExecuteConfirmationDetails,
|
||||
@@ -235,6 +236,33 @@ export const ToolConfirmationMessage: React.FC<
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'plan') {
|
||||
const planProps = confirmationDetails;
|
||||
|
||||
question = planProps.title;
|
||||
options.push({
|
||||
label: 'Yes, and auto-accept edits',
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
});
|
||||
options.push({
|
||||
label: 'Yes, and manually approve edits',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
});
|
||||
options.push({
|
||||
label: 'No, keep planning (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
});
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||
<MarkdownDisplay
|
||||
text={planProps.plan}
|
||||
isPending={false}
|
||||
availableTerminalHeight={availableBodyContentHeight()}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const infoProps = confirmationDetails;
|
||||
const displayUrls =
|
||||
|
||||
@@ -18,9 +18,11 @@ import { TOOL_STATUS } from '../../constants.js';
|
||||
import type {
|
||||
TodoResultDisplay,
|
||||
TaskResultDisplay,
|
||||
PlanResultDisplay,
|
||||
Config,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { AgentExecutionDisplay } from '../subagents/index.js';
|
||||
import { PlanSummaryDisplay } from '../PlanSummaryDisplay.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||
@@ -35,6 +37,7 @@ export type TextEmphasis = 'high' | 'medium' | 'low';
|
||||
type DisplayRendererResult =
|
||||
| { type: 'none' }
|
||||
| { type: 'todo'; data: TodoResultDisplay }
|
||||
| { type: 'plan'; data: PlanResultDisplay }
|
||||
| { type: 'string'; data: string }
|
||||
| { type: 'diff'; data: { fileDiff: string; fileName: string } }
|
||||
| { type: 'task'; data: TaskResultDisplay };
|
||||
@@ -63,6 +66,18 @@ const useResultDisplayRenderer = (
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
typeof resultDisplay === 'object' &&
|
||||
resultDisplay !== null &&
|
||||
'type' in resultDisplay &&
|
||||
resultDisplay.type === 'plan_summary'
|
||||
) {
|
||||
return {
|
||||
type: 'plan',
|
||||
data: resultDisplay as PlanResultDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for SubagentExecutionResultDisplay (for non-task tools)
|
||||
if (
|
||||
typeof resultDisplay === 'object' &&
|
||||
@@ -102,6 +117,18 @@ const TodoResultRenderer: React.FC<{ data: TodoResultDisplay }> = ({
|
||||
data,
|
||||
}) => <TodoDisplay todos={data.todos} />;
|
||||
|
||||
const PlanResultRenderer: React.FC<{
|
||||
data: PlanResultDisplay;
|
||||
availableHeight?: number;
|
||||
childWidth: number;
|
||||
}> = ({ data, availableHeight, childWidth }) => (
|
||||
<PlanSummaryDisplay
|
||||
data={data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component to render subagent execution results
|
||||
*/
|
||||
@@ -229,6 +256,13 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
{displayRenderer.type === 'todo' && (
|
||||
<TodoResultRenderer data={displayRenderer.data} />
|
||||
)}
|
||||
{displayRenderer.type === 'plan' && (
|
||||
<PlanResultRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'task' && (
|
||||
<SubagentExecutionRenderer
|
||||
data={displayRenderer.data}
|
||||
|
||||
@@ -53,7 +53,7 @@ export function AgentsManagerDialog({
|
||||
const manager = config.getSubagentManager();
|
||||
|
||||
// Load agents from all levels separately to show all agents including conflicts
|
||||
const allAgents = await manager.listSubagents();
|
||||
const allAgents = await manager.listSubagents({ force: true });
|
||||
|
||||
setAvailableAgents(allAgents);
|
||||
}, [config]);
|
||||
|
||||
Reference in New Issue
Block a user