feat: 增加对 Windows CLI 工具的支持,允许使用 cmd 作为首选 shell,并改进错误处理

This commit is contained in:
catlog22
2026-02-20 11:14:22 +08:00
parent 113d0bd234
commit d6bf941113
10 changed files with 174 additions and 28 deletions

View File

@@ -29,7 +29,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/RadioGroup';
export type CliTool = 'claude' | 'gemini' | 'qwen' | 'codex' | 'opencode';
export type LaunchMode = 'default' | 'yolo';
export type ShellKind = 'bash' | 'pwsh';
export type ShellKind = 'bash' | 'pwsh' | 'cmd';
export interface CliSessionConfig {
tool: CliTool;
@@ -69,7 +69,10 @@ export function CliConfigModal({
const [tool, setTool] = React.useState<CliTool>('gemini');
const [model, setModel] = React.useState<string | undefined>(MODEL_OPTIONS.gemini[0]);
const [launchMode, setLaunchMode] = React.useState<LaunchMode>('yolo');
const [preferredShell, setPreferredShell] = React.useState<ShellKind>('bash');
// Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files)
const [preferredShell, setPreferredShell] = React.useState<ShellKind>(
typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash'
);
const [workingDir, setWorkingDir] = React.useState<string>(defaultWorkingDir ?? '');
const [isSubmitting, setIsSubmitting] = React.useState(false);
@@ -216,8 +219,9 @@ export function CliConfigModal({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bash">bash</SelectItem>
<SelectItem value="pwsh">pwsh</SelectItem>
<SelectItem value="cmd">cmd ( Windows)</SelectItem>
<SelectItem value="bash">bash (Git Bash/WSL)</SelectItem>
<SelectItem value="pwsh">pwsh (PowerShell)</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -46,6 +46,7 @@ import {
import { useIssues, useIssueQueue } from '@/hooks/useIssues';
import { useTerminalGridStore, selectTerminalGridFocusedPaneId } from '@/stores/terminalGridStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { toast } from '@/stores/notificationStore';
import { CliConfigModal, type CliSessionConfig } from './CliConfigModal';
// ========== Types ==========
@@ -79,6 +80,7 @@ const LAYOUT_PRESETS = [
];
type LaunchMode = 'default' | 'yolo';
type ShellKind = 'bash' | 'pwsh' | 'cmd';
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
type CliTool = (typeof CLI_TOOLS)[number];
@@ -124,6 +126,9 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
const [isCreating, setIsCreating] = useState(false);
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
const [selectedShell, setSelectedShell] = useState<ShellKind>(
typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash'
);
const [isConfigOpen, setIsConfigOpen] = useState(false);
// Helper to get or create a focused pane
@@ -140,18 +145,29 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
setIsCreating(true);
try {
const targetPaneId = getOrCreateFocusedPane();
if (!targetPaneId) return;
if (!targetPaneId) {
toast.error('无法创建会话', '未能获取或创建窗格');
return;
}
await createSessionAndAssign(targetPaneId, {
workingDir: projectPath,
preferredShell: 'bash',
preferredShell: selectedShell,
tool: selectedTool,
launchMode,
}, projectPath);
} catch (error: unknown) {
// Handle both Error instances and ApiError-like objects
const message = error instanceof Error
? error.message
: (error as { message?: string })?.message
? (error as { message: string }).message
: String(error);
toast.error(`CLI 会话创建失败 (${selectedTool})`, message);
} finally {
setIsCreating(false);
}
}, [projectPath, createSessionAndAssign, selectedTool, launchMode, getOrCreateFocusedPane]);
}, [projectPath, createSessionAndAssign, selectedTool, selectedShell, launchMode, getOrCreateFocusedPane]);
const handleConfigure = useCallback(() => {
setIsConfigOpen(true);
@@ -164,7 +180,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
const targetPaneId = getOrCreateFocusedPane();
if (!targetPaneId) throw new Error('Failed to create pane');
const created = await createSessionAndAssign(
await createSessionAndAssign(
targetPaneId,
{
workingDir: config.workingDir || projectPath,
@@ -175,8 +191,15 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
},
projectPath
);
if (!created?.session?.sessionKey) throw new Error('createSessionAndAssign failed');
} catch (error: unknown) {
// Handle both Error instances and ApiError-like objects
const message = error instanceof Error
? error.message
: (error as { message?: string })?.message
? (error as { message: string }).message
: String(error);
toast.error(`CLI 会话创建失败 (${config.tool})`, message);
throw error;
} finally {
setIsCreating(false);
}
@@ -249,6 +272,31 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="gap-2">
<span>{formatMessage({ id: 'terminalDashboard.toolbar.shell' })}</span>
<span className="text-xs text-muted-foreground">
{selectedShell === 'cmd' ? 'cmd' : selectedShell === 'pwsh' ? 'pwsh' : 'bash'}
</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={selectedShell}
onValueChange={(v) => setSelectedShell(v as ShellKind)}
>
<DropdownMenuRadioItem value="cmd">
cmd {formatMessage({ id: 'terminalDashboard.toolbar.shellCmdDesc' })}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bash">
bash (Git Bash/WSL)
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="pwsh">
pwsh (PowerShell)
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleQuickCreate}

View File

@@ -152,7 +152,9 @@ async function fetchApi<T>(
if (contentType && contentType.includes('application/json')) {
try {
const body = await response.json();
// Check both 'message' and 'error' fields for error message
if (body.message) error.message = body.message;
else if (body.error) error.message = body.error;
if (body.code) error.code = body.code;
} catch (parseError) {
// Silently ignore JSON parse errors for non-JSON responses
@@ -6344,7 +6346,8 @@ export interface CreateCliSessionInput {
workingDir?: string;
cols?: number;
rows?: number;
preferredShell?: 'bash' | 'pwsh';
/** Shell to use for spawning CLI tools on Windows. */
preferredShell?: 'bash' | 'pwsh' | 'cmd';
tool?: string;
model?: string;
resumeKey?: string;

View File

@@ -83,6 +83,8 @@
"mode": "Mode",
"modeDefault": "Default",
"modeYolo": "Yolo",
"shell": "Shell",
"shellCmdDesc": "(Recommended for Windows)",
"quickCreate": "Quick Create",
"configure": "Configure...",
"fullscreen": "Fullscreen",

View File

@@ -21,7 +21,14 @@
"toolbar": {
"refresh": "刷新",
"clearAll": "清空所有",
"settings": "设置"
"settings": "设置",
"back": "返回",
"addExecution": "添加",
"running": "运行中",
"executions": "执行",
"executionsList": "最近执行",
"fullscreen": "全屏",
"exitFullscreen": "退出全屏"
},
"emptyState": {
"title": "暂无 CLI 执行",

View File

@@ -83,6 +83,8 @@
"mode": "模式",
"modeDefault": "默认",
"modeYolo": "Yolo",
"shell": "Shell",
"shellCmdDesc": "(推荐 Windows)",
"quickCreate": "快速创建",
"configure": "配置...",
"fullscreen": "全屏",

View File

@@ -341,9 +341,16 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
);
return { paneId: newPaneId, session };
} catch (error) {
console.error('Failed to create CLI session:', error);
return null;
} catch (error: unknown) {
// Handle both Error instances and ApiError objects
const errorMsg = error instanceof Error
? error.message
: (error as { message?: string })?.message
? (error as { message: string }).message
: String(error);
console.error('Failed to create CLI session:', errorMsg, { config, projectPath, rawError: error });
// Re-throw with meaningful message so UI can display it
throw new Error(errorMsg);
}
},