diff --git a/ccw/bin/ccw-mcp.js b/ccw/bin/ccw-mcp.js index 9920425f..0be73d3d 100644 --- a/ccw/bin/ccw-mcp.js +++ b/ccw/bin/ccw-mcp.js @@ -4,4 +4,19 @@ * Entry point for running CCW tools as an MCP server */ -import '../dist/mcp-server/index.js'; +// IMPORTANT: +// MCP stdio servers must not write arbitrary text to stdout. +// stdout is reserved for JSON-RPC protocol messages. +// Redirect common console output to stderr to avoid breaking handshake. +const toStderr = (...args) => console.error(...args); +console.log = toStderr; +console.info = toStderr; +console.debug = toStderr; +console.dir = toStderr; + +try { + await import('../dist/mcp-server/index.js'); +} catch (err) { + console.error('[ccw-mcp] Failed to start MCP server:', err); + process.exit(1); +} diff --git a/ccw/frontend/src/components/terminal-dashboard/CliConfigModal.tsx b/ccw/frontend/src/components/terminal-dashboard/CliConfigModal.tsx index 34de149a..e858d0fd 100644 --- a/ccw/frontend/src/components/terminal-dashboard/CliConfigModal.tsx +++ b/ccw/frontend/src/components/terminal-dashboard/CliConfigModal.tsx @@ -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('gemini'); const [model, setModel] = React.useState(MODEL_OPTIONS.gemini[0]); const [launchMode, setLaunchMode] = React.useState('yolo'); - const [preferredShell, setPreferredShell] = React.useState('bash'); + // Default to 'cmd' on Windows for better compatibility with npm CLI tools (.cmd files) + const [preferredShell, setPreferredShell] = React.useState( + typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('win') ? 'cmd' : 'bash' + ); const [workingDir, setWorkingDir] = React.useState(defaultWorkingDir ?? ''); const [isSubmitting, setIsSubmitting] = React.useState(false); @@ -216,8 +219,9 @@ export function CliConfigModal({ - bash - pwsh + cmd (推荐 Windows) + bash (Git Bash/WSL) + pwsh (PowerShell) diff --git a/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx b/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx index 824c8714..98fc0c35 100644 --- a/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx +++ b/ccw/frontend/src/components/terminal-dashboard/DashboardToolbar.tsx @@ -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('gemini'); const [launchMode, setLaunchMode] = useState('yolo'); + const [selectedShell, setSelectedShell] = useState( + 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 + + + {formatMessage({ id: 'terminalDashboard.toolbar.shell' })} + + {selectedShell === 'cmd' ? 'cmd' : selectedShell === 'pwsh' ? 'pwsh' : 'bash'} + + + + setSelectedShell(v as ShellKind)} + > + + cmd {formatMessage({ id: 'terminalDashboard.toolbar.shellCmdDesc' })} + + + bash (Git Bash/WSL) + + + pwsh (PowerShell) + + + + + ( 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; diff --git a/ccw/frontend/src/locales/en/terminal-dashboard.json b/ccw/frontend/src/locales/en/terminal-dashboard.json index 39708b0a..ce829e5f 100644 --- a/ccw/frontend/src/locales/en/terminal-dashboard.json +++ b/ccw/frontend/src/locales/en/terminal-dashboard.json @@ -83,6 +83,8 @@ "mode": "Mode", "modeDefault": "Default", "modeYolo": "Yolo", + "shell": "Shell", + "shellCmdDesc": "(Recommended for Windows)", "quickCreate": "Quick Create", "configure": "Configure...", "fullscreen": "Fullscreen", diff --git a/ccw/frontend/src/locales/zh/cli-viewer.json b/ccw/frontend/src/locales/zh/cli-viewer.json index b7a77e30..6cbc1561 100644 --- a/ccw/frontend/src/locales/zh/cli-viewer.json +++ b/ccw/frontend/src/locales/zh/cli-viewer.json @@ -21,7 +21,14 @@ "toolbar": { "refresh": "刷新", "clearAll": "清空所有", - "settings": "设置" + "settings": "设置", + "back": "返回", + "addExecution": "添加", + "running": "运行中", + "executions": "执行", + "executionsList": "最近执行", + "fullscreen": "全屏", + "exitFullscreen": "退出全屏" }, "emptyState": { "title": "暂无 CLI 执行", diff --git a/ccw/frontend/src/locales/zh/terminal-dashboard.json b/ccw/frontend/src/locales/zh/terminal-dashboard.json index cbeb31bd..e191d5e7 100644 --- a/ccw/frontend/src/locales/zh/terminal-dashboard.json +++ b/ccw/frontend/src/locales/zh/terminal-dashboard.json @@ -83,6 +83,8 @@ "mode": "模式", "modeDefault": "默认", "modeYolo": "Yolo", + "shell": "Shell", + "shellCmdDesc": "(推荐 Windows)", "quickCreate": "快速创建", "configure": "配置...", "fullscreen": "全屏", diff --git a/ccw/frontend/src/stores/terminalGridStore.ts b/ccw/frontend/src/stores/terminalGridStore.ts index 1e7ed387..be359113 100644 --- a/ccw/frontend/src/stores/terminalGridStore.ts +++ b/ccw/frontend/src/stores/terminalGridStore.ts @@ -341,9 +341,16 @@ export const useTerminalGridStore = create()( ); 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); } }, diff --git a/ccw/src/core/services/cli-session-command-builder.ts b/ccw/src/core/services/cli-session-command-builder.ts index 6255cf42..c2d39949 100644 --- a/ccw/src/core/services/cli-session-command-builder.ts +++ b/ccw/src/core/services/cli-session-command-builder.ts @@ -1,6 +1,6 @@ import path from 'path'; -export type CliSessionShellKind = 'wsl-bash' | 'git-bash' | 'pwsh'; +export type CliSessionShellKind = 'wsl-bash' | 'git-bash' | 'pwsh' | 'cmd'; export type CliSessionResumeStrategy = 'nativeResume' | 'promptConcat'; diff --git a/ccw/src/core/services/cli-session-manager.ts b/ccw/src/core/services/cli-session-manager.ts index b30c243d..038497a4 100644 --- a/ccw/src/core/services/cli-session-manager.ts +++ b/ccw/src/core/services/cli-session-manager.ts @@ -34,7 +34,8 @@ export interface CreateCliSessionOptions { 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; @@ -224,10 +225,59 @@ export class CliSessionManager { // Native CLI interactive session: spawn the CLI process directly const launchMode = options.launchMode ?? 'default'; const config = getLaunchConfig(options.tool, launchMode); - shellKind = 'git-bash'; // PTY shell kind label (not actually a shell) - file = config.command; - args = config.args; cliTool = options.tool; + + // Build the full command string with arguments + const fullCommand = config.args.length > 0 + ? `${config.command} ${config.args.join(' ')}` + : config.command; + + // On Windows, CLI tools installed via npm are typically .cmd files. + // node-pty cannot spawn .cmd files directly, so we need a shell wrapper. + // On Unix systems, direct spawn usually works. + if (os.platform() === 'win32') { + // Use user's preferred shell (default to cmd for reliability) + const shell = options.preferredShell ?? 'cmd'; + + if (shell === 'cmd') { + shellKind = 'cmd'; + file = 'cmd.exe'; + args = ['/c', fullCommand]; + } else if (shell === 'pwsh') { + shellKind = 'pwsh'; + // Check for PowerShell Core (pwsh) or fall back to Windows PowerShell + const pwshPath = spawnSync('where', ['pwsh'], { encoding: 'utf8', windowsHide: true }); + if (pwshPath.status === 0) { + file = 'pwsh'; + } else { + file = 'powershell'; + } + args = ['-NoLogo', '-Command', fullCommand]; + } else { + // bash - try git-bash or WSL + const gitBash = findGitBashExe(); + if (gitBash) { + shellKind = 'git-bash'; + file = gitBash; + args = ['-l', '-i', '-c', fullCommand]; + } else if (isWslAvailable()) { + shellKind = 'wsl-bash'; + file = 'wsl.exe'; + args = ['-e', 'bash', '-l', '-i', '-c', fullCommand]; + } else { + // Fall back to cmd if no bash available + shellKind = 'cmd'; + file = 'cmd.exe'; + args = ['/c', fullCommand]; + } + } + } else { + // Unix: direct spawn works for most CLI tools + shellKind = 'git-bash'; + file = config.command; + args = config.args; + } + } else { // Legacy shell session: spawn bash/pwsh const preferredShell = options.preferredShell ?? 'bash'; @@ -237,13 +287,21 @@ export class CliSessionManager { args = picked.args; } - const pty = nodePty.spawn(file, args, { - name: 'xterm-256color', - cols: options.cols ?? 120, - rows: options.rows ?? 30, - cwd: workingDir, - env: process.env as Record - }); + let pty: nodePty.IPty; + try { + pty = nodePty.spawn(file, args, { + name: 'xterm-256color', + cols: options.cols ?? 120, + rows: options.rows ?? 30, + cwd: workingDir, + env: process.env as Record + }); + } catch (spawnError: unknown) { + const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError); + const toolInfo = options.tool ? `tool '${options.tool}' (` : ''; + const shellInfo = options.tool ? `)` : `shell '${file}'`; + throw new Error(`Failed to spawn ${toolInfo}${shellInfo}: ${errorMsg}. Ensure the CLI tool is installed and available in PATH.`); + } const session: CliSessionInternal = { sessionKey,