feat(shell): enable PTY by default and various enhancements

### Shell & Interactive Terminal Improvements
- PTY shell is now enabled by default instead of disabled
- Improved shell output rendering, process termination, and added fallback warning
- Background commands now properly capture subprocess PIDs on non-Windows

### Coding Plan Improvements
- Simplified auth message, added /model tip, improved system info display
- Reordered model list to prioritize glm-5, kimi-k2.5, MiniMax-M2.5
- Model selection is now preserved when updating if the model still exists

### Other Changes
- Added shared symlink utility; debug logs now have latest alias
- Unknown settings warnings go to debug log instead of user-facing warnings
- Fixed subagent confirmation state detection
- Removed debug UI from AgentCreationWizard

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-05 11:28:13 +08:00
parent 991ae9febc
commit b48e3caa75
31 changed files with 729 additions and 314 deletions

View file

@ -389,13 +389,24 @@ export const useAuthCommand = (
{
type: MessageType.INFO,
text: t(
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).',
'Authenticated successfully with {{region}}. API key and model configs saved to settings.json.',
{ region: t('Alibaba Cloud Coding Plan') },
),
},
Date.now(),
);
// Hint about /model command
addItem(
{
type: MessageType.INFO,
text: t(
'Tip: Use /model to switch between available Coding Plan models.',
),
},
Date.now(),
);
// Log success
const authEvent = new AuthEvent(
AuthType.USE_OPENAI,

View file

@ -11,7 +11,7 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -32,7 +32,7 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -53,7 +53,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode true* │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -74,7 +74,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false* │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false* │
│ ▼ │
@ -95,7 +95,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode (Modified in System) false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -116,7 +116,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode (Modified in Workspace) false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -137,7 +137,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -158,7 +158,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false* │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -179,7 +179,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode false │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE false │
│ ▼ │
@ -200,7 +200,7 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
│ Language: Model auto │
│ Theme Qwen Dark │
│ Vim Mode true* │
│ Interactive Shell (PTY) false │
│ Interactive Shell (PTY) true │
│ Preferred Editor │
│ Auto-connect to IDE true* │
│ ▼ │

View file

@ -120,45 +120,6 @@ export function AgentCreationWizard({
);
}, [state.currentStep, state.generationMethod]);
const renderDebugContent = useCallback(() => {
if (process.env['NODE_ENV'] !== 'development') {
return null;
}
return (
<Box borderStyle="single" borderColor={theme.status.warning} padding={1}>
<Box flexDirection="column">
<Text color={theme.status.warning} bold>
Debug Info:
</Text>
<Text color={theme.text.secondary}>Step: {state.currentStep}</Text>
<Text color={theme.text.secondary}>
Can Proceed: {state.canProceed ? 'Yes' : 'No'}
</Text>
<Text color={theme.text.secondary}>
Generating: {state.isGenerating ? 'Yes' : 'No'}
</Text>
<Text color={theme.text.secondary}>Location: {state.location}</Text>
<Text color={theme.text.secondary}>
Method: {state.generationMethod}
</Text>
{state.validationErrors.length > 0 && (
<Text color={theme.status.error}>
Errors: {state.validationErrors.join(', ')}
</Text>
)}
</Box>
</Box>
);
}, [
state.currentStep,
state.canProceed,
state.isGenerating,
state.location,
state.generationMethod,
state.validationErrors,
]);
const renderStepFooter = useCallback(() => {
const getNavigationInstructions = () => {
// Special case: During generation in description input step, only show cancel option
@ -331,7 +292,6 @@ export function AgentCreationWizard({
>
{renderStepHeader()}
{renderStepContent()}
{renderDebugContent()}
{renderStepFooter()}
</Box>
</Box>

View file

@ -481,6 +481,111 @@ describe('useCodingPlanUpdates', () => {
).toBe(true);
});
it('should show "model preserved" message when current model exists in new template', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'qwen3.5-plus',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
],
};
// Simulate the user's current model being one that exists in the new template
mockConfig.getModel.mockReturnValue('qwen3.5-plus');
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should show plain success message without "switched"
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('updated successfully'),
}),
expect.any(Number),
);
expect(mockAddItem).not.toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('switched'),
}),
expect.any(Number),
);
// Reset mock
mockConfig.getModel.mockReturnValue('qwen-max');
});
it('should show "model switched" message when current model is not in new template', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,
version: 'old-version-hash',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'removed-model',
baseUrl: chinaConfig.baseUrl,
envKey: CODING_PLAN_ENV_KEY,
},
],
};
// The user's current model no longer exists in the new template
mockConfig.getModel.mockReturnValue('removed-model');
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
await waitFor(() => {
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should show "model switched" message
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('switched'),
}),
expect.any(Number),
);
// Reset mock
mockConfig.getModel.mockReturnValue('qwen-max');
});
it('should handle update errors gracefully', async () => {
mockSettings.merged.codingPlan = {
region: CodingPlanRegion.CHINA,

View file

@ -42,6 +42,7 @@ export function useCodingPlanUpdates(
/**
* Execute the Coding Plan configuration update.
* Removes old Coding Plan configs and replaces them with new ones from the template.
* Preserves the user's current model selection if it still exists in the new template.
* Uses the region from settings.codingPlan.region (defaults to CHINA).
*/
const executeUpdate = useCallback(
@ -82,6 +83,12 @@ export function useCodingPlanUpdates(
...(nonCodingPlanConfigs as Array<Record<string, unknown>>),
] as Array<Record<string, unknown>>;
// Record the user's current model before the update
const previousModel = config.getModel();
const previousModelStillAvailable = newConfigs.some(
(cfg) => cfg.id === previousModel,
);
// Hot-reload model providers configuration first (in-memory only)
const updatedModelProviders = {
...(settings.merged.modelProviders as
@ -112,12 +119,34 @@ export function useCodingPlanUpdates(
const activeModel = config.getModel();
if (previousModelStillAvailable && activeModel === previousModel) {
addItem(
{
type: 'info',
text: t('{{region}} configuration updated successfully.', {
region: t('Alibaba Cloud Coding Plan'),
}),
},
Date.now(),
);
} else {
addItem(
{
type: 'info',
text: t(
'{{region}} configuration updated successfully. Model switched to "{{model}}".',
{ region: t('Alibaba Cloud Coding Plan'), model: activeModel },
),
},
Date.now(),
);
}
addItem(
{
type: 'info',
text: t(
'{{region}} configuration updated successfully. Model switched to "{{model}}".',
{ region: t('Alibaba Cloud Coding Plan'), model: activeModel },
'Tip: Use /model to switch between available Coding Plan models.',
),
},
Date.now(),

View file

@ -59,6 +59,7 @@ import {
type TrackedToolCall,
type TrackedCompletedToolCall,
type TrackedCancelledToolCall,
type TrackedExecutingToolCall,
type TrackedWaitingToolCall,
} from './useReactToolScheduler.js';
import { promises as fs } from 'node:fs';
@ -358,6 +359,23 @@ export const useGeminiStream = (
if (toolCalls.some((tc) => tc.status === 'awaiting_approval')) {
return StreamingState.WaitingForConfirmation;
}
// Check if any executing subagent task has a pending confirmation
if (
toolCalls.some((tc) => {
if (tc.status !== 'executing') return false;
const liveOutput = (tc as TrackedExecutingToolCall).liveOutput;
return (
typeof liveOutput === 'object' &&
liveOutput !== null &&
'type' in liveOutput &&
liveOutput.type === 'task_execution' &&
'pendingConfirmation' in liveOutput &&
liveOutput.pendingConfirmation != null
);
})
) {
return StreamingState.WaitingForConfirmation;
}
if (
isResponding ||
toolCalls.some(