From 4344e79e680c2a8ab47ebb16b858172d10708e40 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Mon, 9 Feb 2026 20:45:29 +0800 Subject: [PATCH] Add benchmark results for fast3 and fast4, implement KeepAliveLspBridge, and add tests for staged strategies - Added new benchmark result files: compare_2026-02-09_score_fast3.json and compare_2026-02-09_score_fast4.json. - Implemented KeepAliveLspBridge to maintain a persistent LSP connection across multiple queries, improving performance. - Created unit tests for staged clustering strategies in test_staged_stage3_fast_strategies.py, ensuring correct behavior of score and dir_rr strategies. --- .codex/skills/parallel-dev-cycle/SKILL.md | 34 +- .../phases/01-session-init.md | 2 +- .../phases/02-agent-execution.md | 2 +- .../phases/03-result-aggregation.md | 2 +- .../phases/04-completion-summary.md | 2 +- .../roles/code-developer.md | 6 +- .../roles/exploration-planner.md | 2 +- .../roles/requirements-analyst.md | 2 +- .../roles/validation-archivist.md | 2 +- ccw/frontend/ENSOAI_INTEGRATION_PLAN.md | 313 ++++++++++++ ...SSUE_BOARD_NATIVE_CLI_ORCHESTRATOR_PLAN.md | 232 +++++++++ ccw/frontend/package.json | 4 +- .../components/issue/hub/IssueBoardPanel.tsx | 444 +++++++++++++++++ .../src/components/issue/hub/IssueDrawer.tsx | 38 +- .../components/issue/hub/IssueHubHeader.tsx | 9 +- .../src/components/issue/hub/IssueHubTabs.tsx | 3 +- .../components/issue/hub/IssueTerminalTab.tsx | 402 +++++++++++++++ .../src/components/issue/hub/QueuePanel.tsx | 112 ++++- .../components/issue/queue/QueueActions.tsx | 17 +- .../src/components/issue/queue/QueueBoard.tsx | 171 +++++++ .../src/components/issue/queue/QueueCard.tsx | 6 +- .../issue/queue/QueueExecuteInSession.tsx | 290 +++++++++++ .../components/issue/queue/SolutionDrawer.tsx | 18 +- ccw/frontend/src/hooks/index.ts | 1 + ccw/frontend/src/hooks/useIssues.ts | 55 ++- ccw/frontend/src/hooks/useWebSocket.ts | 32 ++ ccw/frontend/src/lib/api.ts | 149 +++++- ccw/frontend/src/lib/queryKeys.ts | 1 + ccw/frontend/src/locales/en/issues.json | 40 ++ ccw/frontend/src/locales/en/orchestrator.json | 11 + ccw/frontend/src/locales/zh/issues.json | 40 ++ ccw/frontend/src/locales/zh/orchestrator.json | 12 +- ccw/frontend/src/main.tsx | 1 + ccw/frontend/src/pages/IssueHubPage.tsx | 3 + .../src/pages/orchestrator/PropertyPanel.tsx | 78 +++ ccw/frontend/src/stores/cliSessionStore.ts | 132 +++++ ccw/frontend/src/types/flow.ts | 22 + ccw/src/core/routes/cli-sessions-routes.ts | 153 ++++++ ccw/src/core/routes/issue-routes.ts | 85 +++- ccw/src/core/routes/orchestrator-routes.ts | 22 + ccw/src/core/server.ts | 6 + .../services/cli-session-command-builder.ts | 110 +++++ ccw/src/core/services/cli-session-manager.ts | 380 ++++++++++++++ ccw/src/core/services/cli-session-policy.ts | 55 +++ ccw/src/core/services/flow-executor.ts | 39 ++ ccw/src/core/services/rate-limiter.ts | 49 ++ ccw/tests/cli-session-command-builder.test.js | 62 +++ ...compare_staged_realtime_vs_dense_rerank.py | 8 + .../compare_2026-02-09_dir_rr_fast4.json | 356 ++++++++++++++ .../compare_2026-02-09_keepalive3.json | 171 +++++++ .../compare_2026-02-09_keepalive3b.json | 171 +++++++ .../compare_2026-02-09_score_fast3.json | 208 ++++++++ .../compare_2026-02-09_score_fast4.json | 356 ++++++++++++++ .../compare_2026-02-09_score_fast5.json | 462 ++++++++++++++++++ codex-lens/src/codexlens/cli/commands.py | 75 +++ .../src/codexlens/cli/embedding_manager.py | 290 ++++++++++- codex-lens/src/codexlens/config.py | 2 +- .../src/codexlens/lsp/keepalive_bridge.py | 135 +++++ .../src/codexlens/lsp/standalone_manager.py | 12 +- .../src/codexlens/search/binary_searcher.py | 32 ++ .../src/codexlens/search/chain_search.py | 254 ++++++++-- .../test_staged_stage3_fast_strategies.py | 56 +++ package-lock.json | 37 ++ package.json | 1 + 64 files changed, 6154 insertions(+), 123 deletions(-) create mode 100644 ccw/frontend/ENSOAI_INTEGRATION_PLAN.md create mode 100644 ccw/frontend/ISSUE_BOARD_NATIVE_CLI_ORCHESTRATOR_PLAN.md create mode 100644 ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx create mode 100644 ccw/frontend/src/components/issue/hub/IssueTerminalTab.tsx create mode 100644 ccw/frontend/src/components/issue/queue/QueueBoard.tsx create mode 100644 ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx create mode 100644 ccw/frontend/src/stores/cliSessionStore.ts create mode 100644 ccw/src/core/routes/cli-sessions-routes.ts create mode 100644 ccw/src/core/services/cli-session-command-builder.ts create mode 100644 ccw/src/core/services/cli-session-manager.ts create mode 100644 ccw/src/core/services/cli-session-policy.ts create mode 100644 ccw/src/core/services/rate-limiter.ts create mode 100644 ccw/tests/cli-session-command-builder.test.js create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_dir_rr_fast4.json create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_keepalive3.json create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_keepalive3b.json create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_score_fast3.json create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_score_fast4.json create mode 100644 codex-lens/benchmarks/results/compare_2026-02-09_score_fast5.json create mode 100644 codex-lens/src/codexlens/lsp/keepalive_bridge.py create mode 100644 codex-lens/tests/test_staged_stage3_fast_strategies.py diff --git a/.codex/skills/parallel-dev-cycle/SKILL.md b/.codex/skills/parallel-dev-cycle/SKILL.md index 833e6cd2..49f1758e 100644 --- a/.codex/skills/parallel-dev-cycle/SKILL.md +++ b/.codex/skills/parallel-dev-cycle/SKILL.md @@ -1,6 +1,6 @@ --- name: parallel-dev-cycle -description: Multi-agent parallel development cycle with requirement analysis, exploration planning, code development, and validation. Supports continuous iteration with markdown progress documentation. Triggers on "parallel-dev-cycle". +description: Multi-agent parallel development cycle with requirement analysis, exploration planning, code development, and validation. Orchestration runs inline in main flow (no separate orchestrator agent). Supports continuous iteration with markdown progress documentation. Triggers on "parallel-dev-cycle". allowed-tools: spawn_agent, wait, send_input, close_agent, AskUserQuestion, Read, Write, Edit, Bash, Glob, Grep --- @@ -12,6 +12,8 @@ Multi-agent parallel development cycle using Codex subagent pattern with four sp 3. **Code Development** (CD) - Code development with debug strategy support 4. **Validation & Archival Summary** (VAS) - Validation and archival summary +Orchestration logic (phase management, state updates, feedback coordination) runs **inline in the main flow** — no separate orchestrator agent is spawned. Only 4 worker agents are allocated. + Each agent **maintains one main document** (e.g., requirements.md, plan.json, implementation.md) that is completely rewritten per iteration, plus auxiliary logs (changes.log, debug-log.ndjson) that are append-only. ## Architecture Overview @@ -22,10 +24,10 @@ Each agent **maintains one main document** (e.g., requirements.md, plan.json, im └────────────────────────────┬────────────────────────────────┘ │ v - ┌──────────────────────┐ - │ Orchestrator Agent │ (Coordinator) - │ (spawned once) │ - └──────────────────────┘ + ┌──────────────────────────────┐ + │ Main Flow (Inline Orchestration) │ + │ Phase 1 → 2 → 3 → 4 │ + └──────────────────────────────┘ │ ┌────────────────────┼────────────────────┐ │ │ │ @@ -44,10 +46,10 @@ Each agent **maintains one main document** (e.g., requirements.md, plan.json, im └────────┘ │ v - ┌──────────────────────┐ - │ Summary Report │ - │ & Markdown Docs │ - └──────────────────────┘ + ┌──────────────────────────────┐ + │ Summary Report │ + │ & Markdown Docs │ + └──────────────────────────────┘ ``` ## Key Design Principles @@ -56,7 +58,7 @@ Each agent **maintains one main document** (e.g., requirements.md, plan.json, im 2. **Version-Based Overwrite**: Main documents completely rewritten per version; logs append-only 3. **Automatic Archival**: Old main document versions automatically archived to `history/` directory 4. **Complete Audit Trail**: Changes.log (NDJSON) preserves all change history -5. **Parallel Coordination**: Four agents launched simultaneously; coordination via shared state and orchestrator +5. **Parallel Coordination**: Four agents launched simultaneously; coordination via shared state and inline main flow 6. **File References**: Use short file paths instead of content passing 7. **Self-Enhancement**: RA agent proactively extends requirements based on context 8. **Shared Discovery Board**: All agents share exploration findings via `discoveries.ndjson` — read on start, write as you discover, eliminating redundant codebase exploration @@ -315,7 +317,7 @@ All agents share a real-time discovery board at `coordination/discoveries.ndjson 3. Deduplicate — check existing entries; skip if same `type` + dedup key value already exists 4. Append-only — never modify or delete existing lines -### Agent → Orchestrator Communication +### Agent → Main Flow Communication ``` PHASE_RESULT: - phase: ra | ep | cd | vas @@ -325,7 +327,7 @@ PHASE_RESULT: - issues: [] ``` -### Orchestrator → Agent Communication +### Main Flow → Agent Communication Feedback via `send_input` (file refs + issue summary, never full content): ``` @@ -337,7 +339,7 @@ Feedback via `send_input` (file refs + issue summary, never full content): 1. [Specific fix] ``` -**Rules**: Only orchestrator writes state file. Agents read state, write to own `.progress/{agent}/` directory only. +**Rules**: Only main flow writes state file. Agents read state, write to own `.progress/{agent}/` directory only. ## Core Rules @@ -346,7 +348,7 @@ Feedback via `send_input` (file refs + issue summary, never full content): 3. **Parse Every Output**: Extract PHASE_RESULT data from each agent for next phase 4. **Auto-Continue**: After each phase, execute next pending phase automatically 5. **Track Progress**: Update TodoWrite dynamically with attachment/collapse pattern -6. **Single Writer**: Only orchestrator writes to master state file; agents report via PHASE_RESULT +6. **Single Writer**: Only main flow writes to master state file; agents report via PHASE_RESULT 7. **File References**: Pass file paths between agents, not content 8. **DO NOT STOP**: Continuous execution until all phases complete or max iterations reached @@ -357,11 +359,11 @@ Feedback via `send_input` (file refs + issue summary, never full content): | Agent timeout | send_input requesting convergence, then retry | | State corrupted | Rebuild from progress markdown files and changes.log | | Agent failed | Re-spawn agent with previous context | -| Conflicting results | Orchestrator sends reconciliation request | +| Conflicting results | Main flow sends reconciliation request | | Missing files | RA/EP agents identify and request clarification | | Max iterations reached | Generate summary with remaining issues documented | -## Coordinator Checklist +## Coordinator Checklist (Main Flow) ### Before Each Phase diff --git a/.codex/skills/parallel-dev-cycle/phases/01-session-init.md b/.codex/skills/parallel-dev-cycle/phases/01-session-init.md index b278eaec..7b33bad2 100644 --- a/.codex/skills/parallel-dev-cycle/phases/01-session-init.md +++ b/.codex/skills/parallel-dev-cycle/phases/01-session-init.md @@ -258,4 +258,4 @@ function checkControlSignals(cycleId) { ## Next Phase -Return to orchestrator, then auto-continue to [Phase 2: Agent Execution](02-agent-execution.md). +Return to main flow, then auto-continue to [Phase 2: Agent Execution](02-agent-execution.md). diff --git a/.codex/skills/parallel-dev-cycle/phases/02-agent-execution.md b/.codex/skills/parallel-dev-cycle/phases/02-agent-execution.md index 3e0e4e70..83acbca5 100644 --- a/.codex/skills/parallel-dev-cycle/phases/02-agent-execution.md +++ b/.codex/skills/parallel-dev-cycle/phases/02-agent-execution.md @@ -446,4 +446,4 @@ Execution timeout reached. Please: ## Next Phase -Return to orchestrator, then auto-continue to [Phase 3: Result Aggregation & Iteration](03-result-aggregation.md). +Return to main flow, then auto-continue to [Phase 3: Result Aggregation & Iteration](03-result-aggregation.md). diff --git a/.codex/skills/parallel-dev-cycle/phases/03-result-aggregation.md b/.codex/skills/parallel-dev-cycle/phases/03-result-aggregation.md index 81dfa182..22bec56a 100644 --- a/.codex/skills/parallel-dev-cycle/phases/03-result-aggregation.md +++ b/.codex/skills/parallel-dev-cycle/phases/03-result-aggregation.md @@ -227,4 +227,4 @@ Phase 3: Result Aggregation ## Next Phase If iteration continues: Return to Phase 2. -If iteration completes: Return to orchestrator, then auto-continue to [Phase 4: Completion & Summary](04-completion-summary.md). +If iteration completes: Return to main flow, then auto-continue to [Phase 4: Completion & Summary](04-completion-summary.md). diff --git a/.codex/skills/parallel-dev-cycle/phases/04-completion-summary.md b/.codex/skills/parallel-dev-cycle/phases/04-completion-summary.md index 5e18c54e..9b421f4a 100644 --- a/.codex/skills/parallel-dev-cycle/phases/04-completion-summary.md +++ b/.codex/skills/parallel-dev-cycle/phases/04-completion-summary.md @@ -83,7 +83,7 @@ Object.values(agents).forEach(id => { ### Step 4.4: Return Result ```javascript -console.log('\n=== Parallel Dev Cycle Orchestrator Finished ===') +console.log('\n=== Parallel Dev Cycle Finished ===') return { status: 'completed', diff --git a/.codex/skills/parallel-dev-cycle/roles/code-developer.md b/.codex/skills/parallel-dev-cycle/roles/code-developer.md index dd383b48..cc7b3d10 100644 --- a/.codex/skills/parallel-dev-cycle/roles/code-developer.md +++ b/.codex/skills/parallel-dev-cycle/roles/code-developer.md @@ -181,7 +181,7 @@ When tests fail during implementation, the CD agent MUST initiate the hypothesis |---------|-----------|--------| | **Test Failure** | Automated tests fail during implementation | Start debug workflow | | **Integration Conflict** | Blockers logged in `issues.md` | Start debug workflow | -| **VAS Feedback** | Orchestrator provides validation failure feedback | Start debug workflow | +| **VAS Feedback** | Main flow provides validation failure feedback | Start debug workflow | ### Debug Workflow Phases @@ -356,7 +356,7 @@ PHASE_RESULT: - Used to guide development - **RA (Requirements Analyst)**: "Requirement FR-X means..." - Used for clarification -- **Orchestrator**: "Fix these issues in next iteration" +- **Main Flow**: "Fix these issues in next iteration" - Used for priority setting ### Sends To: @@ -364,7 +364,7 @@ PHASE_RESULT: - Used for test generation - **RA (Requirements Analyst)**: "FR-X is unclear, need clarification" - Used for requirement updates -- **Orchestrator**: "Found blocker X, need help" +- **Main Flow**: "Found blocker X, need help" - Used for decision making ## Code Quality Standards diff --git a/.codex/skills/parallel-dev-cycle/roles/exploration-planner.md b/.codex/skills/parallel-dev-cycle/roles/exploration-planner.md index 5d99c882..54c74795 100644 --- a/.codex/skills/parallel-dev-cycle/roles/exploration-planner.md +++ b/.codex/skills/parallel-dev-cycle/roles/exploration-planner.md @@ -333,7 +333,7 @@ PHASE_RESULT: ### Receives From: - **RA (Requirements Analyst)**: "Definitive requirements, version X.Y.Z" - Used to structure plan -- **Orchestrator**: "Continue planning with iteration X" +- **Main Flow**: "Continue planning with iteration X" - Used to update plan for extensions ### Sends To: diff --git a/.codex/skills/parallel-dev-cycle/roles/requirements-analyst.md b/.codex/skills/parallel-dev-cycle/roles/requirements-analyst.md index f7cdb098..e77b72d5 100644 --- a/.codex/skills/parallel-dev-cycle/roles/requirements-analyst.md +++ b/.codex/skills/parallel-dev-cycle/roles/requirements-analyst.md @@ -427,7 +427,7 @@ This section documents auto-generated requirements by the RA agent. ### Integration Notes -- Self-enhancement is **internal to RA agent** - no orchestrator changes needed +- Self-enhancement is **internal to RA agent** - no main flow changes needed - Read-only access to codebase and cycle state required - Enhanced requirements are **transparently marked** for user review - User can accept, modify, or reject enhanced requirements in next iteration diff --git a/.codex/skills/parallel-dev-cycle/roles/validation-archivist.md b/.codex/skills/parallel-dev-cycle/roles/validation-archivist.md index c2c73b38..5de1ea8c 100644 --- a/.codex/skills/parallel-dev-cycle/roles/validation-archivist.md +++ b/.codex/skills/parallel-dev-cycle/roles/validation-archivist.md @@ -420,7 +420,7 @@ PHASE_RESULT: ### Sends To: - **CD (Developer)**: "These tests are failing, needs fixes" - Used for prioritizing work -- **Orchestrator**: "Quality report and recommendations" +- **Main Flow**: "Quality report and recommendations" - Used for final sign-off ## Quality Standards diff --git a/ccw/frontend/ENSOAI_INTEGRATION_PLAN.md b/ccw/frontend/ENSOAI_INTEGRATION_PLAN.md new file mode 100644 index 00000000..d5786e47 --- /dev/null +++ b/ccw/frontend/ENSOAI_INTEGRATION_PLAN.md @@ -0,0 +1,313 @@ +# EnsoAI → CCW Frontend 集成计划(架构识别 / 多 CLI / 远程连接 / 功能盘点) + +日期:2026-02-09 +目标:把 `G:\github_lib\EnsoAI` 的“多路智能并行工作流”核心体验,按 CCW 现有能力边界,规划性集成到 `D:\Claude_dms3\ccw\frontend`(Web Dashboard)。 + +> 注:由于本环境 `read_file` 工具仅允许读取 `D:\Claude_dms3`,EnsoAI 代码引用路径来自本地检索结果与 PowerShell 阅读,不影响结论准确性。 + +--- + +## 1) EnsoAI 架构速览(你要找的“架构骨架”) + +EnsoAI 是一个 Electron 桌面应用,典型三段式: + +- **Main Process(主进程)**:`G:\github_lib\EnsoAI\src\main` + - IPC handlers、系统服务集成、Git/PTY/远程共享/代理、Claude 生态集成(Provider、MCP、IDE Bridge) +- **Preload(桥接层)**:`G:\github_lib\EnsoAI\src\preload` + - `window.electronAPI.*` 暴露给渲染进程(settings、hapi、cloudflared、mcp、git、terminal 等) +- **Renderer(UI)**:`G:\github_lib\EnsoAI\src\renderer` + - React UI:Worktree/Git、Monaco 编辑器、xterm 终端、多 Agent Chat、设置面板等 +- **Shared(共享)**:`G:\github_lib\EnsoAI\src\shared` + - types/i18n/ipc channel 等 + +核心思路:**把“多 Agent 并行”落到“多 worktree + 多终端会话 + 可恢复的 AI session”上**,并提供一揽子系统集成(Claude IDE Bridge / MCP / 远程共享 / proxy)。 + +--- + +## 2) EnsoAI 如何调用多 CLI(重点:两条路径) + +### 2.1 非交互任务调用(更像“工具/能力调用”) + +入口:`G:\github_lib\EnsoAI\src\main\services\ai\providers.ts` + +- 按 provider 选择 CLI: + - Claude:`claude -p --output-format ... --model ... [--session-id] [--no-session-persistence]` + - Codex:`codex exec -m ...`(prompt 走 stdin) + - Gemini:`gemini -o ... -m ... --yolo`(prompt 走 stdin) + - Cursor:`agent -p --output-format ... --model ...`(不支持部分 claude flags) +- 统一策略:**通过用户 shell 包装 spawn**,prompt 统一写 stdin;实现了 kill 逻辑与输出解析(尤其 Claude JSON 输出的版本兼容)。 + +适用场景:commit message 生成、code review、分支命名等“非交互式一次性任务”。 + +### 2.2 交互终端调用(更像“人机对话/操作台”) + +入口:`G:\github_lib\EnsoAI\src\renderer\components\chat\AgentTerminal.tsx` + +关键点: + +- **会话恢复**:`--resume ` / `--session-id `(并对 Cursor/Claude initialized 的差异做了兼容) +- **IDE 集成**:Claude 追加 `--ide` +- **环境包装(environment wrapper)**: + - `native`:直接执行 ` ...` + - `hapi`:`hapi ...` 或 `npx -y @twsxtd/hapi ...`,并注入 `CLI_API_TOKEN` + - `happy`:`happy ...`(同理) +- **跨平台兼容**: + - WSL:用 `wsl.exe -e sh -lc ...` 进入登录 shell + - PowerShell:用 `& { }` 避免 `--session-id` 被 PowerShell 当成自身参数 +- **tmux 支持(非 Windows + Claude)**:把会话包进 tmux,保证“断开 UI 也能续接”一类体验 + +适用场景:多 Agent 终端式并行对话、跨会话恢复、长任务驻留。 + +### 2.3 CLI 探测/安装(多 CLI 的“可用性治理”) + +- 探测:`G:\github_lib\EnsoAI\src\main\services\cli\CliDetector.ts` + - 内置:`claude/codex/droid/gemini/auggie/cursor-agent/opencode` + - 通过 PTY 执行 ` --version`,Windows 放宽超时 +- 安装(EnsoAI 自身 CLI,用于打开目录等):`G:\github_lib\EnsoAI\src\main\services\cli\CliInstaller.ts` + - 跨平台脚本生成(macOS/Windows/Linux),以及可能的管理员权限处理 + +--- + +## 3) EnsoAI 的“远程连接 / 系统集成”到底做了什么 + +### 3.1 Claude IDE Bridge(Claude Code CLI 的 IDE 集成) + +入口:`G:\github_lib\EnsoAI\src\main\services\claude\ClaudeIdeBridge.ts` + +机制要点: + +- 本机启动 WebSocket server(绑定 127.0.0.1,随机端口) +- 写 lock file 到: + - `~/.claude/ide/.lock`(或 `CLAUDE_CONFIG_DIR/ide/.lock`) + - payload 含:`pid/workspaceFolders/ideName/transport/ws/runningInWindows/authToken` +- Claude Code 客户端连接时需 header: + - `x-claude-code-ide-authorization: ` + - 可选 `x-claude-code-workspace` 辅助路由 +- 可向 Claude Code 广播通知: + - `selection_changed` + - `at_mentioned` +- 为“运行 Claude CLI 子进程”注入 env: + - `ENABLE_IDE_INTEGRATION=true` + - `CLAUDE_CODE_SSE_PORT=`(配合 `claude --ide`) + +这属于“系统集成层”,不是普通 API 配置;它解决的是:**Claude Code CLI 与 IDE/宿主之间的状态互通**。 + +### 3.2 MCP Servers 管理(与 Claude 生态配置打通) + +入口:`G:\github_lib\EnsoAI\src\main\services\claude\McpManager.ts` + +- 读写 `~/.claude.json` 的 `mcpServers` +- 支持: + - stdio MCP:`command/args/env`(UI 可编辑) + - HTTP/SSE MCP:`type/url/headers`(一般保留原配置,避免破坏) + +### 3.3 远程共享(Hapi)与公网暴露(Cloudflared) + +- Hapi:`G:\github_lib\EnsoAI\src\main\services\hapi\HapiServerManager.ts` + `src/main/ipc/hapi.ts` + - `hapi server` 或 fallback `npx -y @twsxtd/hapi server` + - env:`WEBAPP_PORT/CLI_API_TOKEN/TELEGRAM_BOT_TOKEN/WEBAPP_URL/ALLOWED_CHAT_IDS` + - UI 文案明确:**Web + Telegram 远程共享 agent sessions** +- Cloudflared:`G:\github_lib\EnsoAI\src\main\services\hapi\CloudflaredManager.ts` + - 安装到 userData/bin(避开 asar 只读) + - 支持 quick tunnel / token auth tunnel,可选 `--protocol http2` + +### 3.4 Proxy(代理) + +入口:`G:\github_lib\EnsoAI\src\main\services\proxy\ProxyConfig.ts` + +- Electron session 级代理 + 子进程环境变量注入(HTTP(S)_PROXY / NO_PROXY) +- 内置代理联通性测试(HEAD 请求多个探测 URL) + +--- + +## 4) EnsoAI 功能点全量清单(按模块) + +> 这里是“罗列所有功能点”的汇总视图;后续集成计划会用它做映射。 + +### 4.1 多 Agent / 多 CLI(核心卖点) + +- 多 Agent 终端会话并行(Claude/Codex/Gemini + 额外探测 Droid/Auggie/Cursor/OpenCode) +- 会话持久化与恢复(session-id / resume) +- CLI 可用性探测(installed/version/timeout) +- 环境包装:native +(hapi/happy)+ tmux + WSL/PowerShell 兼容 + +### 4.2 Git / Worktree 工作流(核心载体) + +- Worktree 创建/切换/删除;分支为“一等公民” +- Source Control 面板: + - 文件变更列表、stage/unstage、commit、history、branch 切换 + - diff viewer / diff review modal +- 三栏 Merge Editor(冲突解决) +- 远端同步:fetch/pull/push 等 + +### 4.3 编辑器与文件系统 + +- Monaco 编辑器、多标签、拖拽排序 +- 文件树 CRUD(新建/重命名/删除) +- 预览:Markdown / PDF / Image +- 未保存变更保护、冲突提示 + +### 4.4 AI 辅助开发 + +- AI commit message 生成 +- AI code review(围绕 diff 的审查/优化) +- 分支命名辅助(推测) + +### 4.5 Claude 生态集成 + +- Claude Provider 管理(监听/应用 `~/.claude/settings.json`) +- MCP servers 管理(`~/.claude.json`,stdio + HTTP/SSE) +- Prompts / Plugins 管理 +- Claude IDE Bridge(WS + lock file + selection/@ 通知) + +### 4.6 远程与网络 + +- Hapi remote sharing(Web + Telegram) +- Cloudflared tunnel 暴露本地服务 +- 代理设置与测试 + +### 4.7 通用体验 + +- 多窗口、多工作空间 +- 主题同步(终端主题) +- 快捷键/命令面板 +- 设置 JSON 持久化 +- Web Inspector、更新通知 + +--- + +## 5) CCW Frontend 基线(与 EnsoAI 的可复用对齐点) + +CCW 前端已具备的关键“载体/底座”: + +- **CLI Viewer(多 pane)**:`ccw/frontend/src/pages/CliViewerPage.tsx` + - 已支持 split/grid 布局、聚焦 pane、执行恢复与 WebSocket 输出接入 +- **CLI Endpoints / Installations**: + - `ccw/frontend/src/pages/EndpointsPage.tsx`(litellm/custom/wrapper) + - `ccw/frontend/src/pages/InstallationsPage.tsx`(install/upgrade/uninstall) +- **MCP Manager(含 Cross-CLI)**:`ccw/frontend/src/pages/McpManagerPage.tsx` +- **后端 CLI executor 体系**:`ccw/src/tools/cli-executor-core.ts` + `ccw/src/core/routes/cli-routes.ts` + - 已有 active executions 恢复、history/sqlite、tool availability、wrapper/tools 路由等能力 + +CCW 与 EnsoAI 的主要差异(决定“集成方式”): + +- EnsoAI 是“桌面 IDE/工作台”,CCW 是“Web Dashboard + Orchestrator” +- EnsoAI 的 Worktree/Git/Editor/PTY 属于重资产 IDE 能力;CCW 目前不承担这些(除非明确要扩域) +- EnsoAI 的 IDE Bridge / Hapi/Cloudflared 属于系统集成层;CCW 当前需要新增后端能力才能对齐 + +--- + +## 6) 集成策略(把 EnsoAI 优势带到 CCW,但不把 CCW 变成 IDE) + +优先级建议: + +1) **先移植“多 Agent 并行调度 + 可恢复输出体验”**(与 CCW 现有 CLI viewer/cli executor 高度匹配) +2) 再做 **“多 CLI 治理与配置同步”体验收敛**(Installations/Endpoints/MCP 的信息架构对齐) +3) 最后才考虑 **系统集成层**(Claude IDE Bridge、远程共享、tunnel/proxy 等) + +--- + +## 7) Roadmap(集成到 `ccw/frontend` 的分期落地) + +### Phase 0 — 对齐与设计(1~2 天) + +- 产出:统一术语与信息架构 + - Agent / Tool / Endpoint / Wrapper 的定义 + - “并行会话”= 多 execution 并发 + CLI viewer 多 pane 展示 +- 产出:UI 草图(Agent Matrix + 单 Agent Details + 快捷动作) + +### Phase 1(MVP)— Agent Matrix(最接近 EnsoAI 卖点,且最可落地) + +目标:在 CCW 中“一屏管理多 Agent 并行执行”,并可一键把多个 execution 打开到 CLI Viewer 的多个 pane。 + +前端交付(建议): + +- 新页面:`/agents`(或并入 `/cli-viewer` 的“Matrix 模式”) + - 卡片:每个 Agent(claude/codex/gemini/...)展示 installed/version/status + - 快捷动作: + - Run prompt(选择 mode:analysis/plan/review…) + - Open in CLI Viewer(自动分配 pane) + - Resume / View history(打开对应 conversation/execution) + - 并行启动:一次选多个 Agent → 并发触发多次执行 → 同步创建 viewer tabs +- 复用:`cliStreamStore` + `viewerStore` + +后端依赖(最小化): + +- 若现有 `/api/cli/*` 已满足执行与 active 恢复:仅需补一个 “agents list/status” 聚合接口(可由现有 installations/status 拼装) + +验收标准: + +- 同一 prompt 可对多个 agent 并行执行,输出稳定落到 CLI Viewer 多 pane +- 刷新页面后仍可恢复 running executions(复用 `/api/cli/active`) +- 每个 execution 可追溯到 history(已有 sqlite/history 体系) + +### Phase 2 — Wrapper/Endpoint 体验收敛(对齐 EnsoAI 的 environment wrapper 思路) + +目标:把 EnsoAI 的 `native/hapi/happy/tmux/wsl` 思路映射成 CCW 的 endpoint/wrapper 配置,形成一致的“运行环境选择”体验。 + +前端交付(建议): + +- 在 Agent Matrix 卡片中加入: + - “运行环境”下拉(native / wrapper endpoints) + - “会话策略”选项(new / resume / no-persistence 等,取决于后端 executor 支持) +- 在 EndpointsPage 中标注 “可用于哪些 agent/tool” 的适配关系(避免用户创建了 wrapper 但不知道生效范围) + +后端依赖: + +- wrapper endpoints 的能力需要明确:是否等价于 EnsoAI 的 `hapi/happy` 前缀包装?(如果是,需在 cli executor 的 command builder 支持这一类 wrapper) + +### Phase 3 — Claude IDE Bridge(可选,大改动,系统集成层) + +目标:在 CCW(Node 服务)侧实现类似 EnsoAI 的 IDE Bridge,并在前端提供状态面板。 + +前端交付(建议): + +- Settings 新增 “IDE Integration(Claude Code)”: + - Bridge enable/disable + - 当前端口、lock file 路径、连接状态、最近一次 selection/@ 通知 + - “复制 token / 复制诊断信息”按钮 + +后端依赖: + +- 需要在 CCW server 里增加 WS server + lock file 写入逻辑(参考 EnsoAI 的 `ClaudeIdeBridge.ts`) +- 需要明确 Claude Code CLI 的兼容要求(协议版本、headers、env key) + +### Phase 4 — 远程共享与公网暴露(可选,偏产品功能) + +目标:把 EnsoAI 的 Hapi + Cloudflared 能力变成 CCW 的“远程访问/协作”能力。 + +前端交付(建议): + +- Settings 新增 “Remote Sharing”: + - 启停服务、端口、token、允许列表 + - Cloudflared 安装/启停、URL 展示、一键复制 + +后端依赖: + +- 需要引入 hapi/cloudflared 管理模块(或选择更贴近 CCW 的方案,例如仅 cloudflared 暴露 CCW server) +- 需要明确安全边界(认证、token 轮换、日志脱敏、默认不开放) + +--- + +## 8) EnsoAI 功能 → CCW 现状 → 集成建议(映射表) + +| EnsoAI 功能 | CCW 现状 | 建议集成方式(优先级) | +|---|---|---| +| 多 Agent 并行会话 | 有 CLI viewer + executor,但缺“一屏矩阵” | Phase 1:新增 Agent Matrix + 自动 pane 分配 | +| CLI 探测/安装 | 有 InstallationsPage | Phase 1:把 Installations 信息在 Matrix 中复用展示 | +| environment wrapper(hapi/happy/tmux/wsl) | 有 wrapper endpoints 概念 | Phase 2:把 wrapper endpoints 显式接到 Agent 执行选择 | +| Claude MCP servers 管理 | 有 MCP Manager(含 Cross-CLI) | 现有功能可继续增强(模板/同步策略) | +| Claude IDE Bridge | 暂无 | Phase 3(可选)系统集成层模块 + Settings 面板 | +| Hapi remote sharing + Cloudflared | 暂无 | Phase 4(可选) | +| Worktree/Git/Editor/Merge | 暂无(且是 IDE 域) | 默认不集成;若要做需单独立项 | + +--- + +## 9) 下一步(如果你要我继续落地) + +你可以选择一个切入点,我可以直接在 `ccw/frontend` 开工实现: + +1) **Phase 1(推荐)**:新增 `/agents` 页面(Agent Matrix),复用现有 `/api/cli/*` 与 CLI viewer +2) **Phase 2**:把 wrapper endpoints 打通到“Agent 运行环境选择” +3) **Phase 3/4**:需要先确认产品边界与安全策略,再做后端设计 + diff --git a/ccw/frontend/ISSUE_BOARD_NATIVE_CLI_ORCHESTRATOR_PLAN.md b/ccw/frontend/ISSUE_BOARD_NATIVE_CLI_ORCHESTRATOR_PLAN.md new file mode 100644 index 00000000..75a2d9f3 --- /dev/null +++ b/ccw/frontend/ISSUE_BOARD_NATIVE_CLI_ORCHESTRATOR_PLAN.md @@ -0,0 +1,232 @@ +# CCW Issue 看板 × 原生 CLI 窗口 × 队列管理 × 编排器(tmux-like)集成规划 + +日期:2026-02-09 +输入参考:`.workflow/.analysis/ANL-codemoss-issue-panel-2026-02-09/report.md`(CodeMoss 看板/会话机制) + +## 0) 已确认的关键决策(来自本次讨论) + +- **终端形态**:Web 内嵌终端(`xterm.js`)+ 后端 **PTY**(需要 TTY 行为) +- **默认会话壳**:先启动 `bash`(在 Windows 下优先走 WSL bash,其次 Git-Bash;都不可用时再降级) +- **连续性**:用 `resumeKey` 作为“逻辑会话键”(可映射到 CLI 的 `--resume/--continue/--session-id` 等) +- **Queue 增强优先级**:先做 **执行面**(Queue item → 投递到某个 CLI 会话并执行),管理面后置 +- **resumeKey 映射**:两种策略都要支持(`nativeResume` 与 `promptConcat`,见 4.6) + +## 1) 要做的“产品形态”(一句话) + +把 CCW 的 `Issues + Queue + Orchestrator + CLI Viewer` 变成一个统一的 **Issue Workbench**: + +- 中间:Issue 看板(Kanban)+ Queue 管理(可视化/可执行/可重排) +- 右侧:可切换的 **CLI 会话窗口**(Claude/Codex/Gemini/…),Web 内嵌终端承载,能向不同会话发送消息/命令 +- 编排器:能把节点输出/指令 **路由** 到某个 CLI 会话(类似 tmux 的 send-keys 到某个 pane / session) + +--- + +## 2) 当前 CCW 已具备的关键底座(可直接复用) + +### 2.1 IssueHub 的“右侧抽屉”模式 + +- IssueHub tabs:`ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx`(目前:issues/queue/discovery) +- IssueDrawer(右侧抽屉 + tabs):`ccw/frontend/src/components/issue/hub/IssueDrawer.tsx` +- SolutionDrawer(右侧抽屉):`ccw/frontend/src/components/issue/queue/SolutionDrawer.tsx` + +这让“右侧弹窗/抽屉内嵌 CLI 窗口”的 UI 成本很低:新增一个 tab 或替换为更通用的 drawer 即可。 + +### 2.2 Kanban DnD 组件 + +- `ccw/frontend/src/components/shared/KanbanBoard.tsx`(@hello-pangea/dnd) + +可以直接用于 Issue Board(以及 Queue Board)。 + +### 2.3 “原生 CLI 输出”的 L0 形态已经存在(流式 + WS) + +- 后端:`ccw/src/core/routes/cli-routes.ts` + - `/api/cli/execute` 支持 streaming,并 WS 广播 `CLI_EXECUTION_STARTED` + `CLI_OUTPUT`(以及 completed/error) +- 前端:`ccw/frontend/src/pages/CliViewerPage.tsx` + `ccw/frontend/src/stores/cliStreamStore.ts` + - 多 pane 输出、执行恢复(`/api/cli/active`) + +这套能力仍然有价值(例如:一次性非 TTY 执行、回放、聚合输出)。但本次已确认 MVP 需要 **PTY + xterm** 的真终端体验,因此会新增一套 “CLI Session(PTY)” 的生命周期与 WS 双向通道。 + +### 2.4 编排器右侧滑出 ExecutionMonitor 可复用交互 + +- `ccw/frontend/src/pages/orchestrator/ExecutionMonitor.tsx` + +可作为 Issue 看板右侧“CLI/执行监控”面板的交互模板(布局、滚动、自动跟随、固定宽度/可变宽度)。 + +--- + +## 3) 参考:CodeMoss 的关键设计点(值得吸收) + +来自 `.workflow/.analysis/ANL-codemoss-issue-panel-2026-02-09/report.md`: + +1) **Kanban task 数据里直接挂执行关联字段**(threadId/engineType/modelId/autoStart/branchName 等) +2) **拖到 inprogress 可触发自动动作**(启动会话/执行) +3) “原生 CLI”更像 **长驻进程/协议**(codex app-server JSON-lines),UI 只是事件的消费者 + +CCW 的“差异现实”: + +- CCW 是 Web Dashboard,不是 Tauri/Electron;但我们仍然选择 **node-pty + xterm.js**(后端持有 PTY 会话,前端 attach 显示),以满足“原生 CLI 窗口 / tmux-like 路由”的需求。 + +--- + +## 4) 目标能力拆解(功能点清单) + +### 4.1 Issue Board(Kanban) + +- 新增 tab:`board`(`/issues?tab=board`) +- 列:`open` / `in_progress` / `resolved` / `completed`(`closed` 可独立列或筛选隐藏) +- 支持: + - 列内排序(sortOrder) + - 跨列拖拽(更新 issue.status) + - 选中卡片 → 右侧 Issue Workbench Drawer +- 自动动作(对齐 CodeMoss): + - 从非 `in_progress` 拖到 `in_progress`:触发 `onDragToInProgress(issue)`(可配置) + +### 4.2 右侧 “原生 CLI 窗口”(PTY + xterm.js) + +右侧 drawer 增加 `Terminal`/`CLI` tab(或将 IssueDrawer 升级为 “IssueWorkbenchDrawer”): + +- 会话列表(per issue / per queue / global) + - 多 CLI:Claude/Codex/Gemini/… + - 每个会话绑定: + - `sessionKey`:后端 PTY 会话 ID(用于 attach/close/send/resize) + - `resumeKey`:逻辑连续性(用于生成 CLI 的 resume/continue 参数,或用于生成“同一会话下的命令模板”) + - `tool/model/workingDir/envWrapper`(与 `ENSOAI_INTEGRATION_PLAN.md` 的能力对齐) +- 终端能力(MVP) + - attach:打开/切换某个会话的 xterm 视图 + - send:向会话发送文本(send-keys / paste)并可选择自动回车 + - resize:前端尺寸变化时同步 PTY size + - close:关闭会话(释放进程与资源) +- 操作 + - `Send`:把“指令/消息”直接投递到当前 PTY 会话(tmux send-keys 语义) + - `Open in CLI Viewer`:可选(如果保留 `/api/cli/execute` 回放/聚合,则用于打开对应 execution;否则用于打开独立 CLI Viewer 的“会话视图”) + +### 4.3 队列管理增强(Queue Workbench) + +队列本身已有 API 结构(多队列 index + active_queue_ids、merge/split/activate 等,见 `ccw/src/core/routes/issue-routes.ts`),但前端仍偏“展示 + 少量操作”。 + +建议增强项: + +- 多队列:列表/切换 active queues(UI 对接 `/api/queue/history` + `/api/queue/activate`) +- 可视化重排: + - 列表重排(按 execution_group 分组,组内 drag reorder) + - 将 item 拖到不同 execution_group(更新 execution_group + execution_order) + - 依赖图/阻塞原因(depends_on / blocked) +- 与执行联动: + - item `Execute in Session`(把 queue item 直接投递到指定 CLI 会话并执行;本次确认优先做这个) + - item `Send to Orchestrator`(创建/触发一个 flow 执行) + +### 4.4 编排器(Orchestrator)与 CLI 会话的“tmux-like 路由” + +目标:编排器不只“跑一次性 CLI”,还能“向某个会话发送消息/指令”。 + +建议抽象:`CliMux`(CLI Multiplexer) + +- **MVP(本次确认)CliMuxPersistent**:以 PTY 会话为核心,提供 `send(sessionKey, text)` / `resize` / `close` +- **补充(可选)CliMuxStateless**:保留 `/api/cli/execute` 作为非 TTY 执行与审计回放通道 + +编排器侧最小改动思路: + +- 继续保持 node 数据模型统一(prompt-template),但新增可选字段: + - `targetSessionKey?: string`(或 `issueId` + `sessionName` 组合) + - `delivery?: 'newExecution' | 'sendToSession'`(默认 newExecution) +- FlowExecutor 在执行节点时: + - `delivery=newExecution`:沿用现有 executeCliTool + - `delivery=sendToSession`:调用 CliMux.send(targetSessionKey, instruction) + +这样就形成“像 tmux 一样把消息打到指定 pane/session”的能力。 + +### 4.5 后端 CLI Session(PTY)协议草案(用于前后端对齐) + +目标:让前端的 xterm 以 “attach 到 sessionKey” 的方式工作;Queue/Orchestrator 以 “send 到 sessionKey” 的方式工作。 + +- REST(建议) + - `POST /api/cli-sessions`:创建会话(入参:`tool/model/workingDir/envWrapper/resumeKey/resumeStrategy/shell=bash`;出参:`sessionKey`) + - `GET /api/cli-sessions`:列出会话(用于切换/恢复) + - `POST /api/cli-sessions/:sessionKey/send`:发送文本(支持 `appendNewline`) + - `POST /api/cli-sessions/:sessionKey/execute`:按 tool + resumeStrategy 生成并发送“可执行命令”(Queue/Orchestrator 走这个更稳) + - `POST /api/cli-sessions/:sessionKey/resize`:同步 rows/cols + - `POST /api/cli-sessions/:sessionKey/close`:关闭并回收 +- WS 事件(建议) + - `CLI_SESSION_CREATED` / `CLI_SESSION_CLOSED` + - `CLI_SESSION_OUTPUT`(`sessionKey`, `data`, `stream=stdout|stderr`) + - `CLI_SESSION_ERROR`(`sessionKey`, `message`) +- 兼容策略 + - 现有 `/api/cli/execute` + `CLI_OUTPUT` 保留:用于非 TTY 一次性执行、可审计回放、与现有 `CliViewerPage` 兼容 + - 新增“会话视图”可先复用 `cliStreamStore` 的 pane 结构(key 从 `transactionId` 扩展到 `sessionKey`) + +### 4.6 `resumeKey` 映射策略(两种都实现) + +`resumeKey` 的定位:一个跨 UI/Queue/Orchestrator 的“逻辑会话键”。同一个 `resumeKey` 下可能会对应: + +- **策略 A:`nativeResume`(优先)** + - 由 CLI 自己保存与恢复上下文(例如:`--resume/--continue/--session-id`) + - 优点:上下文更可靠,输出格式更稳定;适合 Claude/Codex 这类有明确 session 概念的 CLI + - 风险:不同 CLI 旗标/行为差异较大,需要 per-tool adapter(并要跟版本兼容) + +- **策略 B:`promptConcat`(必须支持)** + - CCW 自己维护 `resumeKey -> transcript/context`,每次执行把“历史 + 本次指令”拼成 prompt + - 优点:对 CLI 依赖更少;即使 CLI 不支持 resume 也能连续;适合 Gemini/其它工具 + - 风险:token 成本、上下文截断策略、以及敏感信息/审计(需要明确 retention) + +落地建议:后端新增 `ToolAdapter`(按 `tool` 输出“命令模板 + resume 参数 + prompt 注入方式”),`/api/cli-sessions/:sessionKey/execute` 统一走 adapter,Queue/Orchestrator 不直接拼 shell 命令。 + +--- + +## 5) 数据模型建议(对齐 CodeMoss,但保持 CCW 简洁) + +### 5.1 Issue 扩展字段(建议在后端 issue schema 中落盘) + +在 `Issue` 增加: + +- `sortOrder?: number`(用于 board 列内排序) +- `sessions?: Array<{ sessionKey: string; resumeKey: string; tool: string; model?: string; workingDir?: string; envWrapper?: string; createdAt: string }>` + +注意: + +- 后端 `ccw/src/commands/issue.ts` 中已经出现 `status: 'queued'` 等状态迹象;前端 `Issue['status']` 需要对齐,否则 board/filters 会漏状态。 + +### 5.2 QueueItem 与 Session 的绑定(可选) + +- `QueueItem.sessionKey?: string`(允许把某条 queue item 固定投递到某会话) + +--- + +## 6) 分期交付(从快到慢) + +### Phase 1(2~6 天):PTY 会话底座 + Drawer 内嵌终端 + Queue “执行面” + +- [BE] 新增 `CLI Session (PTY)`:create/list/attach/close/send/resize(WS 双向:output + input + resize) +- [FE] IssueDrawer 增加 `Terminal` tab:xterm attach 会话、切换会话、发送文本、resize、close +- [FE] QueuePanel 增加 `Execute in Session`:选/新建 sessionKey,将 queue item 渲染成可执行指令投递到会话 +- [Auto] 支持“拖到 in_progress 自动执行”:触发 `Execute in Session`(可配置开关) + +### Phase 2(2~5 天):Issue Board(Kanban)+ Queue 管理面(多队列/重排/分组) + +- [UI] IssueHubTabs 增加 `board`,IssueBoard 复用 `KanbanBoard`(状态列/排序/拖拽) +- [UI] QueuePanel 增加多队列切换、execution_group 分组与 drag reorder、blocked/depends_on 可视化 +- [API] 若缺:跨 group 更新/排序落盘所需 endpoints(与现有 `/api/queue/reorder` 区分) + +### Phase 3(3~8 天):编排器与 CliMux 集成(tmux-like 路由) + +- [Core] 引入 `CliMuxPersistent`(PTY 会话:send/resize/close) +- [Orchestrator] prompt-template 增加 `targetSessionKey/delivery` +- [UI] ExecutionMonitor 增加“路由到会话”的快捷操作(选择 session) + +### Phase 4(长期):安全、隔离、可观测、远程连接 + +- 资源隔离与安全策略(命令/路径白名单、审计日志、速率限制) +- 会话回收(空闲超时、最大并发、OOM 保护) +- 远程连接(可选):会话分享、只读 attach、与外部 agent/daemon 的桥接(对齐 EnsoAI 的 Hapi/Cloudflared 思路) + +--- + +## 7) 尚未决策(下一轮需要明确) + +1) `bash` 的落地方式(跨平台): + - Linux/macOS:`bash -l`(或用户配置的 default shell) + - Windows:优先 `wsl.exe -e bash -li`,其次 Git-Bash(`bash.exe`),都不可用时是否允许降级到 `pwsh` +2) per-tool 的“nativeResume”参数与触发方式(需要按版本验证): + - Claude:`--resume/--continue/--session-id` 的选择,以及在 PTY 下如何稳定触发一次“发送”(命令式 `-p` vs 交互式 send-keys) + - Codex/Gemini:各自的 session/resume 旗标与输出格式(是否能稳定 machine-readable) +3) 安全边界: + - 是否需要 per-workspace 的路径白名单、命令白名单、以及审计日志(特别是 Queue/Orchestrator 自动投递) diff --git a/ccw/frontend/package.json b/ccw/frontend/package.json index 2d6aa781..177f8629 100644 --- a/ccw/frontend/package.json +++ b/ccw/frontend/package.json @@ -55,7 +55,9 @@ "tailwind-merge": "^2.5.0", "web-vitals": "^5.1.0", "zod": "^4.1.13", - "zustand": "^5.0.0" + "zustand": "^5.0.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" }, "devDependencies": { "@playwright/test": "^1.57.0", diff --git a/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx b/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx new file mode 100644 index 00000000..fbdc51ea --- /dev/null +++ b/ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx @@ -0,0 +1,444 @@ +// ======================================== +// Issue Board Panel +// ======================================== +// Kanban board view for issues (status-driven) with local ordering. + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import type { DropResult } from '@hello-pangea/dnd'; +import { AlertCircle, LayoutGrid } from 'lucide-react'; +import { Card } from '@/components/ui/Card'; +import { Switch } from '@/components/ui/Switch'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'; +import { KanbanBoard, type KanbanColumn, type KanbanItem } from '@/components/shared/KanbanBoard'; +import { IssueCard } from '@/components/shared/IssueCard'; +import { IssueDrawer } from '@/components/issue/hub/IssueDrawer'; +import { cn } from '@/lib/utils'; +import { useIssues, useIssueMutations } from '@/hooks'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; +import { createCliSession, executeInCliSession } from '@/lib/api'; +import type { Issue } from '@/lib/api'; + +type IssueBoardStatus = Issue['status']; +type ToolName = 'claude' | 'codex' | 'gemini'; +type ResumeStrategy = 'nativeResume' | 'promptConcat'; + +const BOARD_COLUMNS: Array<{ id: IssueBoardStatus; titleKey: string }> = [ + { id: 'open', titleKey: 'issues.status.open' }, + { id: 'in_progress', titleKey: 'issues.status.inProgress' }, + { id: 'resolved', titleKey: 'issues.status.resolved' }, + { id: 'completed', titleKey: 'issues.status.completed' }, + { id: 'closed', titleKey: 'issues.status.closed' }, +]; + +type BoardOrder = Partial>; + +function storageKey(projectPath: string | null | undefined): string { + const base = projectPath ? encodeURIComponent(projectPath) : 'global'; + return `ccw.issueBoard.order:${base}`; +} + +interface AutoStartConfig { + enabled: boolean; + tool: ToolName; + mode: 'analysis' | 'write'; + resumeStrategy: ResumeStrategy; +} + +function autoStartStorageKey(projectPath: string | null | undefined): string { + const base = projectPath ? encodeURIComponent(projectPath) : 'global'; + return `ccw.issueBoard.autoStart:${base}`; +} + +function safeParseAutoStart(value: string | null): AutoStartConfig { + const defaults: AutoStartConfig = { + enabled: false, + tool: 'claude', + mode: 'write', + resumeStrategy: 'nativeResume', + }; + if (!value) return defaults; + try { + const parsed = JSON.parse(value) as Partial; + return { + enabled: Boolean(parsed.enabled), + tool: parsed.tool === 'codex' || parsed.tool === 'gemini' ? parsed.tool : 'claude', + mode: parsed.mode === 'analysis' ? 'analysis' : 'write', + resumeStrategy: parsed.resumeStrategy === 'promptConcat' ? 'promptConcat' : 'nativeResume', + }; + } catch { + return defaults; + } +} + +function safeParseOrder(value: string | null): BoardOrder { + if (!value) return {}; + try { + const parsed = JSON.parse(value) as unknown; + if (!parsed || typeof parsed !== 'object') return {}; + return parsed as BoardOrder; + } catch { + return {}; + } +} + +function buildColumns( + issues: Issue[], + order: BoardOrder, + formatTitle: (statusId: IssueBoardStatus) => string +): KanbanColumn[] { + const byId = new Map(issues.map((i) => [i.id, i])); + const columns: KanbanColumn[] = []; + + for (const col of BOARD_COLUMNS) { + const desired = (order[col.id] ?? []).map((id) => byId.get(id)).filter(Boolean) as Issue[]; + const desiredIds = new Set(desired.map((i) => i.id)); + + const remaining = issues + .filter((i) => i.status === col.id && !desiredIds.has(i.id)) + .sort((a, b) => { + const at = a.updatedAt || a.createdAt; + const bt = b.updatedAt || b.createdAt; + return bt.localeCompare(at); + }); + + const items = [...desired, ...remaining].map((issue) => ({ + ...issue, + id: issue.id, + title: issue.title, + status: issue.status, + })); + + columns.push({ + id: col.id, + title: formatTitle(col.id), + items, + icon: , + }); + } + + return columns; +} + +function syncOrderWithIssues(prev: BoardOrder, issues: Issue[]): BoardOrder { + const statusById = new Map(issues.map((i) => [i.id, i.status])); + const next: BoardOrder = {}; + + for (const { id: status } of BOARD_COLUMNS) { + const existing = prev[status] ?? []; + const filtered = existing.filter((id) => statusById.get(id) === status); + const present = new Set(filtered); + + const missing = issues + .filter((i) => i.status === status && !present.has(i.id)) + .map((i) => i.id); + + next[status] = [...filtered, ...missing]; + } + + return next; +} + +function reorderIds(list: string[], from: number, to: number): string[] { + const next = [...list]; + const [moved] = next.splice(from, 1); + if (moved === undefined) return list; + next.splice(to, 0, moved); + return next; +} + +function buildIssueAutoPrompt(issue: Issue): string { + const lines: string[] = []; + lines.push(`Issue: ${issue.id}`); + lines.push(`Status: ${issue.status}`); + lines.push(`Priority: ${issue.priority}`); + lines.push(''); + lines.push(`Title: ${issue.title}`); + if (issue.context) { + lines.push(''); + lines.push('Context:'); + lines.push(String(issue.context)); + } + + if (Array.isArray(issue.solutions) && issue.solutions.length > 0) { + lines.push(''); + lines.push('Solutions:'); + for (const s of issue.solutions) { + lines.push(`- [${s.status}] ${s.description}`); + if (s.approach) lines.push(` Approach: ${s.approach}`); + } + } + + lines.push(''); + lines.push('Instruction:'); + lines.push( + 'Start working on this issue in this repository. Prefer small, testable changes; run relevant tests; report blockers if any.' + ); + return lines.join('\n'); +} + +export function IssueBoardPanel() { + const { formatMessage } = useIntl(); + const projectPath = useWorkflowStore(selectProjectPath); + + const { issues, isLoading, error } = useIssues(); + const { updateIssue } = useIssueMutations(); + + const [order, setOrder] = useState({}); + const [selectedIssue, setSelectedIssue] = useState(null); + const [drawerInitialTab, setDrawerInitialTab] = useState<'overview' | 'terminal'>('overview'); + const [optimisticError, setOptimisticError] = useState(null); + const [autoStart, setAutoStart] = useState(() => safeParseAutoStart(null)); + + // Load order when project changes + useEffect(() => { + const key = storageKey(projectPath); + const loaded = safeParseOrder(localStorage.getItem(key)); + setOrder(loaded); + }, [projectPath]); + + // Load auto-start config when project changes + useEffect(() => { + const key = autoStartStorageKey(projectPath); + setAutoStart(safeParseAutoStart(localStorage.getItem(key))); + }, [projectPath]); + + // Keep order consistent with current issues (status moves, deletions, new issues) + useEffect(() => { + setOrder((prev) => syncOrderWithIssues(prev, issues)); + }, [issues]); + + // Persist order + useEffect(() => { + const key = storageKey(projectPath); + try { + localStorage.setItem(key, JSON.stringify(order)); + } catch { + // ignore quota errors + } + }, [order, projectPath]); + + // Persist auto-start config + useEffect(() => { + const key = autoStartStorageKey(projectPath); + try { + localStorage.setItem(key, JSON.stringify(autoStart)); + } catch { + // ignore quota errors + } + }, [autoStart, projectPath]); + + const columns = useMemo( + () => + buildColumns(issues, order, (statusId) => { + const col = BOARD_COLUMNS.find((c) => c.id === statusId); + if (!col) return statusId; + return formatMessage({ id: col.titleKey }); + }), + [issues, order, formatMessage] + ); + + const idsByStatus = useMemo(() => { + const map: Record = {}; + for (const col of columns) { + map[col.id] = col.items.map((i) => i.id); + } + return map; + }, [columns]); + + const handleItemClick = useCallback((issue: Issue) => { + setDrawerInitialTab('overview'); + setSelectedIssue(issue); + }, []); + + const handleCloseDrawer = useCallback(() => { + setSelectedIssue(null); + setOptimisticError(null); + }, []); + + const handleDragEnd = useCallback( + async (result: DropResult, sourceColumn: string, destColumn: string) => { + const issueId = result.draggableId; + const issue = issues.find((i) => i.id === issueId); + if (!issue) return; + + setOptimisticError(null); + + const sourceStatus = sourceColumn as IssueBoardStatus; + const destStatus = destColumn as IssueBoardStatus; + const sourceIds = idsByStatus[sourceStatus] ?? []; + const destIds = idsByStatus[destStatus] ?? []; + + // Update local order first (optimistic) + setOrder((prev) => { + const next = { ...prev }; + if (sourceStatus === destStatus) { + next[sourceStatus] = reorderIds(sourceIds, result.source.index, result.destination!.index); + return next; + } + + const nextSource = [...sourceIds]; + nextSource.splice(result.source.index, 1); + + const nextDest = [...destIds]; + nextDest.splice(result.destination!.index, 0, issueId); + + next[sourceStatus] = nextSource; + next[destStatus] = nextDest; + return next; + }); + + // Status update + if (sourceStatus !== destStatus) { + try { + await updateIssue(issueId, { status: destStatus }); + + // Auto action: drag to in_progress opens the drawer on terminal tab. + if (destStatus === 'in_progress' && sourceStatus !== 'in_progress') { + setDrawerInitialTab('terminal'); + setSelectedIssue({ ...issue, status: destStatus }); + + if (autoStart.enabled) { + if (!projectPath) { + setOptimisticError('Auto-start failed: no project path selected'); + return; + } + + try { + const created = await createCliSession({ + workingDir: projectPath, + preferredShell: 'bash', + tool: autoStart.tool, + resumeKey: issueId, + }); + await executeInCliSession(created.session.sessionKey, { + tool: autoStart.tool, + prompt: buildIssueAutoPrompt({ ...issue, status: destStatus }), + mode: autoStart.mode, + resumeKey: issueId, + resumeStrategy: autoStart.resumeStrategy, + }); + } catch (e) { + setOptimisticError(`Auto-start failed: ${e instanceof Error ? e.message : String(e)}`); + } + } + } + } catch (e) { + setOptimisticError(e instanceof Error ? e.message : String(e)); + } + } + }, + [issues, idsByStatus, updateIssue] + ); + + if (error) { + return ( + + +

+ {formatMessage({ id: 'issues.queue.error.title' })} +

+

{error.message}

+
+ ); + } + + return ( + <> +
+
+
+ setAutoStart((prev) => ({ ...prev, enabled: checked }))} + /> +
+ {formatMessage({ id: 'issues.board.autoStart.label' })} +
+
+ +
+ + + + + +
+
+
+ + {optimisticError && ( +
+ {optimisticError} +
+ )} + + + columns={columns} + onDragEnd={handleDragEnd} + onItemClick={(item) => handleItemClick(item as unknown as Issue)} + isLoading={isLoading} + emptyColumnMessage={formatMessage({ id: 'issues.emptyState.message' })} + className={cn('gap-4', 'grid')} + renderItem={(item, provided) => ( + handleItemClick(i)} + innerRef={provided.innerRef} + draggableProps={provided.draggableProps} + dragHandleProps={provided.dragHandleProps} + className="w-full" + /> + )} + /> + + + + ); +} + +export default IssueBoardPanel; diff --git a/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx b/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx index a8f8d383..f8155195 100644 --- a/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx +++ b/ccw/frontend/src/components/issue/hub/IssueDrawer.tsx @@ -3,23 +3,25 @@ // ======================================== // Right-side issue detail drawer with Overview/Solutions/History tabs -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; -import { X, FileText, CheckCircle, Circle, Loader2, Tag, History, Hash } from 'lucide-react'; +import { X, FileText, CheckCircle, Circle, Loader2, Tag, History, Hash, Terminal } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; 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'; // ========== Types ========== export interface IssueDrawerProps { issue: Issue | null; isOpen: boolean; onClose: () => void; + initialTab?: TabValue; } -type TabValue = 'overview' | 'solutions' | 'history' | 'json'; +type TabValue = 'overview' | 'solutions' | 'history' | 'terminal' | 'json'; // ========== Status Configuration ========== const statusConfig: Record }> = { @@ -39,20 +41,25 @@ const priorityConfig: Record('overview'); + const [activeTab, setActiveTab] = useState(initialTab); - // Reset to overview when issue changes - useState(() => { + // Reset to initial tab when opening/switching issues + useEffect(() => { + if (!isOpen || !issue) return; + setActiveTab(initialTab); + }, [initialTab, isOpen, issue?.id]); + + // ESC key to close + useEffect(() => { + if (!isOpen) return; const handleEsc = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen) { - onClose(); - } + if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); - }); + }, [isOpen, onClose]); if (!issue || !isOpen) { return null; @@ -126,6 +133,10 @@ export function IssueDrawer({ issue, isOpen, onClose }: IssueDrawerProps) { {formatMessage({ id: 'issues.detail.tabs.history' })} + + + {formatMessage({ id: 'issues.detail.tabs.terminal' })} + JSON @@ -213,6 +224,11 @@ export function IssueDrawer({ issue, isOpen, onClose }: IssueDrawerProps) { )} + {/* Terminal Tab */} + + + + {/* History Tab */}
diff --git a/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx b/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx index 75f0fcb9..0721021b 100644 --- a/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx +++ b/ccw/frontend/src/components/issue/hub/IssueHubHeader.tsx @@ -4,9 +4,9 @@ // Dynamic header component for IssueHub import { useIntl } from 'react-intl'; -import { AlertCircle, Radar, ListTodo } from 'lucide-react'; +import { AlertCircle, Radar, ListTodo, LayoutGrid } from 'lucide-react'; -type IssueTab = 'issues' | 'queue' | 'discovery'; +type IssueTab = 'issues' | 'board' | 'queue' | 'discovery'; interface IssueHubHeaderProps { currentTab: IssueTab; @@ -22,6 +22,11 @@ export function IssueHubHeader({ currentTab }: IssueHubHeaderProps) { title: formatMessage({ id: 'issues.title' }), description: formatMessage({ id: 'issues.description' }), }, + board: { + icon: , + title: formatMessage({ id: 'issues.board.pageTitle' }), + description: formatMessage({ id: 'issues.board.description' }), + }, queue: { icon: , title: formatMessage({ id: 'issues.queue.pageTitle' }), diff --git a/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx b/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx index 083811c8..c5824fc6 100644 --- a/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx +++ b/ccw/frontend/src/components/issue/hub/IssueHubTabs.tsx @@ -7,7 +7,7 @@ import { useIntl } from 'react-intl'; import { Button } from '@/components/ui/Button'; import { cn } from '@/lib/utils'; -export type IssueTab = 'issues' | 'queue' | 'discovery'; +export type IssueTab = 'issues' | 'board' | 'queue' | 'discovery'; interface IssueHubTabsProps { currentTab: IssueTab; @@ -19,6 +19,7 @@ export function IssueHubTabs({ currentTab, onTabChange }: IssueHubTabsProps) { const tabs: Array<{ value: IssueTab; label: string }> = [ { value: 'issues', label: formatMessage({ id: 'issues.hub.tabs.issues' }) }, + { value: 'board', label: formatMessage({ id: 'issues.hub.tabs.board' }) }, { value: 'queue', label: formatMessage({ id: 'issues.hub.tabs.queue' }) }, { value: 'discovery', label: formatMessage({ id: 'issues.hub.tabs.discovery' }) }, ]; diff --git a/ccw/frontend/src/components/issue/hub/IssueTerminalTab.tsx b/ccw/frontend/src/components/issue/hub/IssueTerminalTab.tsx new file mode 100644 index 00000000..2c73149d --- /dev/null +++ b/ccw/frontend/src/components/issue/hub/IssueTerminalTab.tsx @@ -0,0 +1,402 @@ +// ======================================== +// IssueTerminalTab +// ======================================== +// Embedded xterm.js terminal for PTY-backed CLI sessions. + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Plus, RefreshCw, XCircle } from 'lucide-react'; +import { Terminal as XTerm } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { Button } from '@/components/ui/Button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'; +import { Input } from '@/components/ui/Input'; +import { cn } from '@/lib/utils'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; +import { + closeCliSession, + createCliSession, + executeInCliSession, + fetchCliSessionBuffer, + fetchCliSessions, + resizeCliSession, + sendCliSessionText, + type CliSession, +} from '@/lib/api'; +import { useCliSessionStore } from '@/stores/cliSessionStore'; + +type ToolName = 'claude' | 'codex' | 'gemini'; +type ResumeStrategy = 'nativeResume' | 'promptConcat'; + +export function IssueTerminalTab({ issueId }: { issueId: string }) { + const { formatMessage } = useIntl(); + const projectPath = useWorkflowStore(selectProjectPath); + + const sessionsByKey = useCliSessionStore((s) => s.sessions); + const outputChunks = useCliSessionStore((s) => s.outputChunks); + const setSessions = useCliSessionStore((s) => s.setSessions); + const upsertSession = useCliSessionStore((s) => s.upsertSession); + const setBuffer = useCliSessionStore((s) => s.setBuffer); + const clearOutput = useCliSessionStore((s) => s.clearOutput); + + const sessions = useMemo(() => Object.values(sessionsByKey).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), [sessionsByKey]); + + const [selectedSessionKey, setSelectedSessionKey] = useState(''); + const [isLoadingSessions, setIsLoadingSessions] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [isClosing, setIsClosing] = useState(false); + const [error, setError] = useState(null); + + const [tool, setTool] = useState('claude'); + const [mode, setMode] = useState<'analysis' | 'write'>('analysis'); + const [resumeKey, setResumeKey] = useState(issueId); + const [resumeStrategy, setResumeStrategy] = useState('nativeResume'); + const [prompt, setPrompt] = useState(''); + const [isExecuting, setIsExecuting] = useState(false); + + const terminalHostRef = useRef(null); + const xtermRef = useRef(null); + const fitAddonRef = useRef(null); + const lastChunkIndexRef = useRef(0); + + const pendingInputRef = useRef(''); + const flushTimerRef = useRef(null); + + const flushInput = async () => { + const sessionKey = selectedSessionKey; + if (!sessionKey) return; + const pending = pendingInputRef.current; + pendingInputRef.current = ''; + if (!pending) return; + try { + await sendCliSessionText(sessionKey, { text: pending, appendNewline: false }); + } catch (e) { + // Ignore transient failures (WS output still shows process state) + } + }; + + const scheduleFlush = () => { + if (flushTimerRef.current !== null) return; + flushTimerRef.current = window.setTimeout(async () => { + flushTimerRef.current = null; + await flushInput(); + }, 30); + }; + + useEffect(() => { + setIsLoadingSessions(true); + setError(null); + fetchCliSessions() + .then((r) => { + setSessions(r.sessions as unknown as CliSession[]); + }) + .catch((e) => setError(e instanceof Error ? e.message : String(e))) + .finally(() => setIsLoadingSessions(false)); + }, [setSessions]); + + // Auto-select a session if none selected yet + useEffect(() => { + if (selectedSessionKey) return; + if (sessions.length === 0) return; + setSelectedSessionKey(sessions[sessions.length - 1]?.sessionKey ?? ''); + }, [sessions, selectedSessionKey]); + + // Init xterm + useEffect(() => { + if (!terminalHostRef.current) return; + if (xtermRef.current) return; + + const term = new XTerm({ + convertEol: true, + cursorBlink: true, + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: 12, + scrollback: 5000, + }); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + term.open(terminalHostRef.current); + fitAddon.fit(); + + // Forward keystrokes to backend (batched) + term.onData((data) => { + if (!selectedSessionKey) return; + pendingInputRef.current += data; + scheduleFlush(); + }); + + xtermRef.current = term; + fitAddonRef.current = fitAddon; + + return () => { + try { + term.dispose(); + } finally { + xtermRef.current = null; + fitAddonRef.current = null; + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Attach to selected session: clear terminal and load buffer + useEffect(() => { + const term = xtermRef.current; + const fitAddon = fitAddonRef.current; + if (!term || !fitAddon) return; + + lastChunkIndexRef.current = 0; + term.reset(); + term.clear(); + + if (!selectedSessionKey) return; + clearOutput(selectedSessionKey); + + fetchCliSessionBuffer(selectedSessionKey) + .then(({ buffer }) => { + setBuffer(selectedSessionKey, buffer || ''); + }) + .catch(() => { + // ignore + }) + .finally(() => { + fitAddon.fit(); + }); + }, [selectedSessionKey, setBuffer, clearOutput]); + + // Stream new output chunks into xterm + useEffect(() => { + const term = xtermRef.current; + if (!term) return; + if (!selectedSessionKey) return; + + const chunks = outputChunks[selectedSessionKey] ?? []; + const start = lastChunkIndexRef.current; + if (start >= chunks.length) return; + + for (let i = start; i < chunks.length; i++) { + term.write(chunks[i].data); + } + lastChunkIndexRef.current = chunks.length; + }, [outputChunks, selectedSessionKey]); + + // Resize observer -> fit + resize backend + useEffect(() => { + const host = terminalHostRef.current; + const term = xtermRef.current; + const fitAddon = fitAddonRef.current; + if (!host || !term || !fitAddon) return; + + const resize = () => { + fitAddon.fit(); + if (selectedSessionKey) { + void (async () => { + try { + await resizeCliSession(selectedSessionKey, { cols: term.cols, rows: term.rows }); + } catch { + // ignore + } + })(); + } + }; + + const ro = new ResizeObserver(resize); + ro.observe(host); + return () => ro.disconnect(); + }, [selectedSessionKey]); + + const handleCreateSession = async () => { + setIsCreating(true); + setError(null); + try { + const created = await createCliSession({ + workingDir: projectPath || undefined, + preferredShell: 'bash', + cols: xtermRef.current?.cols, + rows: xtermRef.current?.rows, + tool, + model: undefined, + resumeKey, + }); + upsertSession(created.session as unknown as CliSession); + setSelectedSessionKey(created.session.sessionKey); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setIsCreating(false); + } + }; + + const handleCloseSession = async () => { + if (!selectedSessionKey) return; + setIsClosing(true); + setError(null); + try { + await closeCliSession(selectedSessionKey); + setSelectedSessionKey(''); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setIsClosing(false); + } + }; + + const handleExecute = async () => { + if (!selectedSessionKey) return; + if (!prompt.trim()) return; + setIsExecuting(true); + setError(null); + try { + await executeInCliSession(selectedSessionKey, { + tool, + prompt: prompt.trim(), + mode, + resumeKey: resumeKey.trim() || undefined, + resumeStrategy, + category: 'user', + }); + setPrompt(''); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setIsExecuting(false); + } + }; + + const handleRefreshSessions = async () => { + setIsLoadingSessions(true); + setError(null); + try { + const r = await fetchCliSessions(); + setSessions(r.sessions as unknown as CliSession[]); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setIsLoadingSessions(false); + } + }; + + return ( +
+
+
+ +
+ + + + + + +
+ +
+
+
{formatMessage({ id: 'issues.terminal.exec.tool' })}
+ +
+ +
+
{formatMessage({ id: 'issues.terminal.exec.mode' })}
+ +
+
+ +
+
+
{formatMessage({ id: 'issues.terminal.exec.resumeKey' })}
+ setResumeKey(e.target.value)} placeholder={issueId} /> +
+ +
+
+ {formatMessage({ id: 'issues.terminal.exec.resumeStrategy' })} +
+ +
+
+ +
+
{formatMessage({ id: 'issues.terminal.exec.prompt.label' })}
+