diff --git a/.ccw/workflows/cli-templates/schemas/fix-plan-json-schema.json b/.ccw/workflows/cli-templates/schemas/fix-plan-json-schema.json index ce72c2c1..72d05335 100644 --- a/.ccw/workflows/cli-templates/schemas/fix-plan-json-schema.json +++ b/.ccw/workflows/cli-templates/schemas/fix-plan-json-schema.json @@ -294,5 +294,22 @@ } } } + }, + + "deprecated": true, + "deprecated_message": "Migrated to plan-overview-fix-schema.json (extends base) + task-schema.json.", + "migration_guide": { + "plan_level": "→ plan-overview-fix-schema.json (继承 base,含 fix_context)", + "task_level": "→ .task/FIX-*.json (task-schema.json)", + "field_mapping": { + "root_cause": "→ fix_context.root_cause", + "strategy": "→ fix_context.strategy", + "severity": "→ fix_context.severity", + "risk_level": "→ fix_context.risk_level", + "test_strategy": "→ plan-overview-fix-schema.test_strategy", + "rollback_plan": "→ plan-overview-fix-schema.rollback_plan", + "tasks[].verification[]": "→ convergence.criteria[]", + "tasks[].risk": "→ risks[] (结构化)" + } } } diff --git a/.ccw/workflows/cli-templates/schemas/plan-json-schema.json b/.ccw/workflows/cli-templates/schemas/plan-json-schema.json index 8e8c14c3..7d60c067 100644 --- a/.ccw/workflows/cli-templates/schemas/plan-json-schema.json +++ b/.ccw/workflows/cli-templates/schemas/plan-json-schema.json @@ -440,5 +440,22 @@ } } } + }, + + "deprecated": true, + "deprecated_message": "Migrated to plan-overview-base-schema.json + task-schema.json (PLANNING 区块).", + "migration_guide": { + "plan_level": "→ plan-overview-base-schema.json", + "task_level": "→ .task/TASK-*.json (task-schema.json)", + "field_mapping": { + "tasks[].modification_points": "→ files[].change (合并入 files)", + "tasks[].reference": "→ reference (直接迁移)", + "tasks[].rationale": "→ rationale (直接迁移)", + "tasks[].verification": "→ test.manual_checks + test.success_metrics (合并入 test)", + "tasks[].risks": "→ risks (直接迁移)", + "tasks[].code_skeleton": "→ code_skeleton (直接迁移)", + "tasks[].acceptance": "→ convergence.criteria (已删除别名,直接用 convergence)", + "tasks[].cli_execution_id": "→ cli_execution.id (已存在)" + } } } diff --git a/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json b/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json new file mode 100644 index 00000000..f4337857 --- /dev/null +++ b/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "plan-overview-base-schema.json", + "title": "Plan Overview Base Schema", + "description": "计划概览基础 schema — 所有计划类型共享字段。Feature 类型直接使用此 schema,其他类型 (fix/tdd/review) 通过 allOf 继承并扩展。", + "type": "object", + "required": ["summary", "approach", "task_ids", "task_count", "_metadata"], + + "properties": { + "summary": { + "type": "string", + "description": "2-3 句计划概述" + }, + "approach": { + "type": "string", + "description": "高层实施策略和方法论" + }, + "task_ids": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "引用 .task/ 下的任务 ID 列表 (如 ['TASK-001', 'TASK-002'])" + }, + "task_count": { + "type": "integer", + "minimum": 1, + "description": "任务数量 (应等于 task_ids.length)" + }, + + "complexity": { + "type": "string", + "enum": ["Low", "Medium", "High"], + "description": "计划复杂度" + }, + "estimated_time": { + "type": "string", + "description": "总估算时间 (如 '30 minutes', '2 hours')" + }, + "recommended_execution": { + "type": "string", + "enum": ["Agent", "Codex"], + "description": "推荐执行方式" + }, + + "data_flow": { + "type": "object", + "properties": { + "diagram": { + "type": "string", + "description": "ASCII/文本形式的数据流图 (如 'A → B → C')" + }, + "stages": { + "type": "array", + "items": { + "type": "object", + "required": ["stage", "input", "output", "component"], + "properties": { + "stage": { + "type": "string", + "description": "阶段名称" + }, + "input": { + "type": "string", + "description": "输入数据格式/类型" + }, + "output": { + "type": "string", + "description": "输出数据格式/类型" + }, + "component": { + "type": "string", + "description": "处理该阶段的组件/模块" + }, + "transformations": { + "type": "array", + "items": { "type": "string" }, + "description": "该阶段的数据转换" + } + } + }, + "description": "详细数据流阶段" + }, + "dependencies": { + "type": "array", + "items": { "type": "string" }, + "description": "外部依赖或数据源" + } + }, + "description": "全局数据流设计" + }, + + "design_decisions": { + "type": "array", + "items": { + "type": "object", + "required": ["decision", "rationale"], + "properties": { + "decision": { + "type": "string", + "description": "设计决策" + }, + "rationale": { + "type": "string", + "description": "决策原因" + }, + "tradeoff": { + "type": "string", + "description": "权衡取舍" + }, + "alternatives": { + "type": "array", + "items": { "type": "string" }, + "description": "考虑过的替代方案" + } + } + }, + "description": "影响整个计划的全局设计决策" + }, + + + "_metadata": { + "type": "object", + "required": ["timestamp", "source", "plan_type"], + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 规划时间戳" + }, + "source": { + "type": "string", + "enum": ["cli-lite-planning-agent", "direct-planning"], + "description": "规划来源" + }, + "planning_mode": { + "type": "string", + "enum": ["direct", "agent-based"], + "description": "规划执行模式" + }, + "plan_type": { + "type": "string", + "enum": ["feature", "fix", "tdd", "review", "collaborative", "requirement"], + "description": "计划类型 — 消费端据此选择对应的扩展 schema 校验" + }, + "schema_version": { + "type": "string", + "default": "2.0", + "description": "Schema 版本" + }, + "exploration_angles": { + "type": "array", + "items": { "type": "string" }, + "description": "用于上下文的探索角度" + }, + "duration_seconds": { + "type": "integer", + "description": "规划耗时 (秒)" + } + } + } + }, + + "additionalProperties": true, + + "_extension_guide": { + "description": "扩展指南 — 通过 allOf + $ref 继承此 base schema", + "available_extensions": { + "fix": "plan-overview-fix-schema.json — fix_context, test_strategy, rollback_plan", + "tdd": "(未来) plan-overview-tdd-schema.json — tdd_cycles, coverage_targets, phase_requirements", + "review": "(未来) plan-overview-review-schema.json — review_dimensions, finding_summary" + }, + "feature_note": "feature 类型直接使用此 base schema,无需额外扩展" + }, + + "_field_migration_from_plan_json": { + "summary": "直接迁移", + "approach": "直接迁移", + "tasks": "→ task_ids[] (引用 .task/ 下的独立文件)", + "complexity": "直接迁移", + "estimated_time": "直接迁移", + "recommended_execution": "直接迁移", + "data_flow": "直接迁移", + "design_decisions": "直接迁移", + "flow_control": "→ 删除 (由引擎从 task depends_on + parallel_group 自动推断)", + "focus_paths": "→ 删除 (由引擎从 task focus_paths / files[].path 聚合)", + "_metadata": "扩展 (新增 plan_type, schema_version)" + } +} diff --git a/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json b/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json new file mode 100644 index 00000000..7129776a --- /dev/null +++ b/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "plan-overview-fix-schema.json", + "title": "Fix Plan Overview Schema", + "description": "Fix 计划概览 — 继承 base + fix 专属字段。_metadata.plan_type 必须为 'fix'。", + "allOf": [ + { "$ref": "plan-overview-base-schema.json" } + ], + "required": ["fix_context"], + "properties": { + "fix_context": { + "type": "object", + "required": ["root_cause", "strategy", "severity", "risk_level"], + "properties": { + "root_cause": { + "type": "string", + "description": "根因描述 (从 fix-plan-json-schema.root_cause 迁移)" + }, + "strategy": { + "type": "string", + "enum": ["immediate_patch", "comprehensive_fix", "refactor"], + "description": "修复策略 (从 fix-plan-json-schema.strategy 迁移)" + }, + "severity": { + "type": "string", + "enum": ["Low", "Medium", "High", "Critical"], + "description": "Bug 严重等级 (从 fix-plan-json-schema.severity 迁移)" + }, + "risk_level": { + "type": "string", + "enum": ["low", "medium", "high"], + "description": "修复风险等级 (从 fix-plan-json-schema.risk_level 迁移)" + } + }, + "additionalProperties": false, + "description": "Fix 专属上下文 — 整合原 fix-plan-json-schema 的顶层 fix 字段" + }, + + "test_strategy": { + "type": "object", + "properties": { + "scope": { + "type": "string", + "enum": ["unit", "integration", "e2e", "smoke", "full"], + "description": "修复后的测试范围" + }, + "specific_tests": { + "type": "array", + "items": { "type": "string" }, + "description": "需运行的特定测试文件或模式" + }, + "manual_verification": { + "type": "array", + "items": { "type": "string" }, + "description": "手动验证步骤 (无自动化测试时)" + } + }, + "additionalProperties": false, + "description": "修复验证测试策略 (从 fix-plan-json-schema.test_strategy 迁移)" + }, + + "rollback_plan": { + "type": "object", + "properties": { + "strategy": { + "type": "string", + "enum": ["git_revert", "feature_flag", "manual"], + "description": "回滚策略" + }, + "steps": { + "type": "array", + "items": { "type": "string" }, + "description": "回滚步骤" + } + }, + "additionalProperties": false, + "description": "回滚计划 (从 fix-plan-json-schema.rollback_plan 迁移)" + }, + + "_metadata": { + "type": "object", + "properties": { + "diagnosis_angles": { + "type": "array", + "items": { "type": "string" }, + "description": "诊断角度 (fix 特有,从 fix-plan-json-schema._metadata.diagnosis_angles 迁移)" + } + }, + "description": "Fix 扩展的 metadata 字段 (与 base _metadata 合并)" + } + }, + + "_field_migration_from_fix_plan_json": { + "root_cause": "→ fix_context.root_cause", + "strategy": "→ fix_context.strategy", + "severity": "→ fix_context.severity", + "risk_level": "→ fix_context.risk_level", + "test_strategy": "直接迁移", + "rollback_plan": "直接迁移", + "tasks": "→ task_ids[] (引用 .task/ 下的独立文件)", + "_metadata.diagnosis_angles": "直接迁移" + } +} diff --git a/.ccw/workflows/cli-templates/schemas/task-schema.json b/.ccw/workflows/cli-templates/schemas/task-schema.json index a5935567..52421b72 100644 --- a/.ccw/workflows/cli-templates/schemas/task-schema.json +++ b/.ccw/workflows/cli-templates/schemas/task-schema.json @@ -123,6 +123,10 @@ "items": { "type": "string" }, "description": "修改描述列表" }, + "change": { + "type": "string", + "description": "单条变更描述 (精确修改说明,合并自 modification_points.change)" + }, "conflict_risk": { "type": "string", "enum": ["low", "medium", "high"], @@ -163,6 +167,16 @@ "minimum": 0, "maximum": 100, "description": "覆盖率目标 (%)" + }, + "manual_checks": { + "type": "array", + "items": { "type": "string" }, + "description": "手动验证步骤 (合并自 verification_detail)" + }, + "success_metrics": { + "type": "array", + "items": { "type": "string" }, + "description": "量化成功指标 (如 '响应时间 <200ms', '覆盖率 >80%',合并自 verification_detail)" } }, "additionalProperties": false, @@ -174,6 +188,132 @@ "description": "回归检查点" }, + "_comment_PLANNING": "PLANNING 区块 (可选) — 规划详情 (reference + rationale + risks + code_skeleton)", + "reference": { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "参考模式名称" + }, + "files": { + "type": "array", + "items": { "type": "string" }, + "description": "参考文件路径" + }, + "examples": { + "type": "string", + "description": "参考指南或示例" + } + }, + "additionalProperties": false, + "description": "参考实现资料" + }, + "rationale": { + "type": "object", + "properties": { + "chosen_approach": { + "type": "string", + "description": "选定方案及原因" + }, + "alternatives_considered": { + "type": "array", + "items": { "type": "string" }, + "description": "被考虑但未选择的替代方案" + }, + "decision_factors": { + "type": "array", + "items": { "type": "string" }, + "description": "影响决策的关键因素 (性能、可维护性、成本等)" + }, + "tradeoffs": { + "type": "string", + "description": "选定方案的已知权衡" + } + }, + "additionalProperties": false, + "description": "设计决策原因 (Medium/High complexity 时使用)" + }, + "risks": { + "type": "array", + "items": { + "type": "object", + "required": ["description", "probability", "impact", "mitigation"], + "properties": { + "description": { + "type": "string", + "description": "风险描述" + }, + "probability": { + "type": "string", + "enum": ["Low", "Medium", "High"], + "description": "发生概率" + }, + "impact": { + "type": "string", + "enum": ["Low", "Medium", "High"], + "description": "影响程度" + }, + "mitigation": { + "type": "string", + "description": "缓解策略" + }, + "fallback": { + "type": "string", + "description": "缓解失败时的替代方案" + } + }, + "additionalProperties": false + }, + "description": "结构化风险评估" + }, + "code_skeleton": { + "type": "object", + "properties": { + "interfaces": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "definition": { "type": "string" }, + "purpose": { "type": "string" } + } + }, + "description": "关键接口/类型定义" + }, + "key_functions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "purpose": { "type": "string" }, + "returns": { "type": "string" } + } + }, + "description": "关键函数签名" + }, + "classes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "methods": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "description": "关键类结构" + } + }, + "additionalProperties": false, + "description": "代码骨架 (High complexity 时使用)" + }, + "_comment_EXECUTION": "EXECUTION 区块 (可选) — 执行策略", "meta": { "type": "object", @@ -268,11 +408,6 @@ "type": "array", "description": "支撑证据" }, - "risk_items": { - "type": "array", - "items": { "type": "string" }, - "description": "风险项" - }, "inputs": { "type": "array", "items": { "type": "string" }, @@ -356,7 +491,8 @@ "_field_usage_by_producer": { "workflow-plan": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + FILES + EXECUTION(meta+cli_execution) + CONTEXT(context_package_path)", "lite-plan": "IDENTITY + CLASSIFICATION + DEPENDENCIES + CONVERGENCE + FILES", - "req-plan": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + CONTEXT(inputs/outputs/risk_items)", + "lite-plan (v2)": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + FILES(+change) + IMPLEMENTATION(+manual_checks +success_metrics) + PLANNING(reference + rationale + risks + code_skeleton)", + "req-plan": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + PLANNING(risks) + CONTEXT(inputs/outputs)", "collaborative-plan": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + FILES + CONTEXT(source)", "issue-resolve": "IDENTITY + CLASSIFICATION + SCOPE + DEPENDENCIES + CONVERGENCE + FILES(with target) + IMPLEMENTATION + CONTEXT(commit/source)", "review-cycle": "IDENTITY + CLASSIFICATION + FILES + CONVERGENCE + IMPLEMENTATION + CONTEXT(source/evidence)", diff --git a/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx b/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx index f932abcf..841ff439 100644 --- a/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx +++ b/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx @@ -18,6 +18,7 @@ import { useIssues, useIssueMutations } from '@/hooks'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import { createCliSession, executeInCliSession } from '@/lib/api'; import type { Issue } from '@/lib/api'; +import { useTerminalPanelStore } from '@/stores/terminalPanelStore'; type IssueBoardStatus = Issue['status']; type ToolName = 'claude' | 'codex' | 'gemini'; @@ -318,6 +319,8 @@ export function IssueBoardPanel() { resumeKey: issueId, resumeStrategy: autoStart.resumeStrategy, }, projectPath); + // Auto-open terminal panel to show execution output + useTerminalPanelStore.getState().openTerminal(created.session.sessionKey); } catch (e) { setOptimisticError(`Auto-start failed: ${e instanceof Error ? e.message : String(e)}`); } diff --git a/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx b/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx index f8155195..734fe249 100644 --- a/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx +++ b/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'; import { cn } from '@/lib/utils'; import type { Issue } from '@/lib/api'; -import { IssueTerminalTab } from './IssueTerminalTab'; +import { useOpenTerminalPanel } from '@/stores/terminalPanelStore'; // ========== Types ========== export interface IssueDrawerProps { @@ -43,6 +43,7 @@ const priorityConfig: Record(initialTab); // Reset to initial tab when opening/switching issues @@ -224,9 +225,21 @@ export function IssueDrawer({ issue, isOpen, onClose, initialTab = 'overview' }: )} - {/* Terminal Tab */} + {/* Terminal Tab - Link to Terminal Panel */} - +
+ +

{formatMessage({ id: 'home.terminalPanel.openInPanel' })}

+ +
{/* History Tab */} diff --git a/ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx b/ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx index 5cae355f..89855f60 100644 --- a/ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx +++ b/ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx @@ -20,6 +20,7 @@ import { type QueueItem, } from '@/lib/api'; import { useCliSessionStore } from '@/stores/cliSessionStore'; +import { useTerminalPanelStore } from '@/stores/terminalPanelStore'; type ToolName = 'claude' | 'codex' | 'gemini'; type ResumeStrategy = 'nativeResume' | 'promptConcat'; @@ -170,6 +171,8 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl resumeStrategy, }, projectPath); setLastExecution({ executionId: result.executionId, command: result.command }); + // Auto-open terminal panel to show execution output + useTerminalPanelStore.getState().openTerminal(sessionKey); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { diff --git a/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx b/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx index f413dc61..9f9d3507 100644 --- a/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx +++ b/ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'; import { QueueExecuteInSession } from '@/components/issue/queue/QueueExecuteInSession'; import { QueueSendToOrchestrator } from '@/components/issue/queue/QueueSendToOrchestrator'; -import { IssueTerminalTab } from '@/components/issue/hub/IssueTerminalTab'; +import { useOpenTerminalPanel } from '@/stores/terminalPanelStore'; import { useIssueQueue } from '@/hooks'; import { cn } from '@/lib/utils'; import type { QueueItem } from '@/lib/api'; @@ -39,6 +39,7 @@ const statusConfig: Record('overview'); const { data: queue } = useIssueQueue(); const itemId = item?.item_id; @@ -257,9 +258,21 @@ export function SolutionDrawer({ item, isOpen, onClose }: SolutionDrawerProps) { )} - {/* Terminal Tab */} + {/* Terminal Tab - Link to Terminal Panel */} - +
+ +

{formatMessage({ id: 'home.terminalPanel.openInPanel' })}

+ +
{/* JSON Tab */} diff --git a/ccw/frontend/src/components/layout/AppShell.tsx b/ccw/frontend/src/components/layout/AppShell.tsx index 4d2f9b08..d47830ef 100644 --- a/ccw/frontend/src/components/layout/AppShell.tsx +++ b/ccw/frontend/src/components/layout/AppShell.tsx @@ -11,6 +11,7 @@ import { Sidebar } from './Sidebar'; import { MainContent } from './MainContent'; import { CliStreamMonitor } from '@/components/shared/CliStreamMonitor'; import { NotificationPanel } from '@/components/notification'; +import { TerminalPanel } from '@/components/terminal-panel'; import { AskQuestionDialog, A2UIPopupCard } from '@/components/a2ui'; import { BackgroundImage } from '@/components/shared/BackgroundImage'; import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores'; @@ -200,6 +201,9 @@ export function AppShell({ onClose={handleNotificationPanelClose} /> + {/* Terminal Panel - Global Drawer */} + + {/* Ask Question Dialog - For ask_question MCP tool (legacy) */} {currentQuestion && ( void; } // ========== Component ========== export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { - const projectPath = useWorkflowStore(selectProjectPath); - - const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId); + const { formatMessage } = useIntl(); const panelView = useTerminalPanelStore((s) => s.panelView); - const setPanelView = useTerminalPanelStore((s) => s.setPanelView); + const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId); - const sessionsByKey = useCliSessionStore((s) => s.sessions); + const sessions = useCliSessionStore((s) => s.sessions); const outputChunks = useCliSessionStore((s) => s.outputChunks); const setBuffer = useCliSessionStore((s) => s.setBuffer); const clearOutput = useCliSessionStore((s) => s.clearOutput); - const activeSession = activeTerminalId ? sessionsByKey[activeTerminalId] : null; + const projectPath = useWorkflowStore(selectProjectPath); - const [prompt, setPrompt] = useState(''); - const [isExecuting, setIsExecuting] = useState(false); - const [error, setError] = useState(null); + const activeSession: CliSessionMeta | undefined = activeTerminalId + ? sessions[activeTerminalId] + : undefined; + + // ========== xterm State ========== - // xterm refs const terminalHostRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); const lastChunkIndexRef = useRef(0); - // Input batching refs (same pattern as IssueTerminalTab) + // PTY input batching const pendingInputRef = useRef(''); const flushTimerRef = useRef(null); - const activeSessionKeyRef = useRef(null); - // Keep ref in sync with activeTerminalId for closures - useEffect(() => { - activeSessionKeyRef.current = activeTerminalId; - }, [activeTerminalId]); + // Command execution + const [prompt, setPrompt] = useState(''); + const [isExecuting, setIsExecuting] = useState(false); - const flushInput = async () => { - const sessionKey = activeSessionKeyRef.current; + const flushInput = useCallback(async () => { + const sessionKey = activeTerminalId; if (!sessionKey) return; const pending = pendingInputRef.current; pendingInputRef.current = ''; @@ -77,18 +71,19 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { } catch { // Ignore transient failures } - }; + }, [activeTerminalId, projectPath]); - const scheduleFlush = () => { + const scheduleFlush = useCallback(() => { if (flushTimerRef.current !== null) return; flushTimerRef.current = window.setTimeout(async () => { flushTimerRef.current = null; await flushInput(); }, 30); - }; + }, [flushInput]); - // ========== xterm Initialization ========== + // ========== xterm Lifecycle ========== + // Init xterm instance useEffect(() => { if (!terminalHostRef.current) return; if (xtermRef.current) return; @@ -107,7 +102,7 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { // Forward keystrokes to backend (batched) term.onData((data) => { - if (!activeSessionKeyRef.current) return; + if (!activeTerminalId) return; pendingInputRef.current += data; scheduleFlush(); }); @@ -126,8 +121,7 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // ========== Attach to Active Session ========== - + // Attach to selected session: clear terminal and load buffer useEffect(() => { const term = xtermRef.current; const fitAddon = fitAddonRef.current; @@ -152,8 +146,7 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { }); }, [activeTerminalId, projectPath, setBuffer, clearOutput]); - // ========== Stream Output Chunks ========== - + // Stream new output chunks into xterm useEffect(() => { const term = xtermRef.current; if (!term) return; @@ -169,8 +162,7 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { lastChunkIndexRef.current = chunks.length; }, [outputChunks, activeTerminalId]); - // ========== Resize Observer ========== - + // Resize observer -> fit + resize backend useEffect(() => { const host = terminalHostRef.current; const term = xtermRef.current; @@ -179,11 +171,10 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { const resize = () => { fitAddon.fit(); - const sessionKey = activeSessionKeyRef.current; - if (sessionKey) { + if (activeTerminalId) { void (async () => { try { - await resizeCliSession(sessionKey, { cols: term.cols, rows: term.rows }, projectPath || undefined); + await resizeCliSession(activeTerminalId, { cols: term.cols, rows: term.rows }, projectPath || undefined); } catch { // ignore } @@ -194,32 +185,30 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) { const ro = new ResizeObserver(resize); ro.observe(host); return () => ro.disconnect(); - }, [projectPath]); + }, [activeTerminalId, projectPath]); - // ========== Execute Command ========== + // ========== Command Execution ========== const handleExecute = async () => { - if (!activeTerminalId) return; - if (!prompt.trim()) return; + if (!activeTerminalId || !prompt.trim()) return; setIsExecuting(true); - setError(null); + const sessionTool = (activeSession?.tool || 'claude') as 'claude' | 'codex' | 'gemini'; try { await executeInCliSession(activeTerminalId, { - tool: activeSession?.tool || 'claude', + tool: sessionTool, prompt: prompt.trim(), mode: 'analysis', category: 'user', }, projectPath || undefined); setPrompt(''); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); + } catch { + // Error shown in terminal output } finally { setIsExecuting(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { - // Ctrl+Enter or Cmd+Enter to execute if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); void handleExecute(); @@ -232,92 +221,82 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
{/* Header */}
-
-

- {activeSession - ? `${activeSession.tool || 'cli'} - ${activeSession.sessionKey}` - : 'Terminal Panel'} -

- {activeSession?.tool && ( - +
+ + + {panelView === 'queue' + ? formatMessage({ id: 'home.terminalPanel.executionQueue' }) + : activeSession + ? `${activeSession.tool || 'cli'} - ${activeSession.sessionKey}` + : formatMessage({ id: 'home.terminalPanel.title' })} + + {activeSession?.workingDir && panelView === 'terminal' && ( + {activeSession.workingDir} )}
-
- {/* Tabs */} - setPanelView(v as PanelView)} - className="flex-1 flex flex-col min-h-0" - > -
- - Terminal - Queue - -
- - {/* Terminal View */} - - {activeTerminalId ? ( -
- {/* xterm container */} -
-
-
- - {/* Command input area */} -
- {error && ( -
{error}
- )} -
-