diff --git a/pyproject.toml b/pyproject.toml index e8752ed..399f851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,4 +8,8 @@ dependencies = [ "fastmcp>=2.0.0", "psutil>=7.0.0", "pyside6>=6.8.2.1", + "fastapi>=0.115.0", + "uvicorn>=0.30.0", + "jinja2>=3.1.0", + "websockets>=13.0.0", ] diff --git a/server.py b/server.py index f0070f0..bccc012 100644 --- a/server.py +++ b/server.py @@ -15,7 +15,59 @@ from pydantic import Field # The log_level is necessary for Cline to work: https://github.com/jlowin/fastmcp/issues/81 mcp = FastMCP("Interactive Feedback MCP", log_level="ERROR") +def is_ssh_session() -> bool: + """Check if we're running in an SSH session or remote environment""" + # Check for SSH environment variables + ssh_indicators = [ + 'SSH_CONNECTION', + 'SSH_CLIENT', + 'SSH_TTY' + ] + + for indicator in ssh_indicators: + if os.getenv(indicator): + return True + + # Check if DISPLAY is not set (common in SSH without X11 forwarding) + if sys.platform.startswith('linux') and not os.getenv('DISPLAY'): + return True + + # Check for other remote indicators + if os.getenv('TERM_PROGRAM') == 'vscode' and os.getenv('VSCODE_INJECTION') == '1': + # VSCode remote development + return True + + return False + +def can_use_gui() -> bool: + """Check if GUI can be used in current environment""" + if is_ssh_session(): + return False + + try: + # Try to import Qt and check if display is available + if sys.platform == 'win32': + return True # Windows should generally support GUI + elif sys.platform == 'darwin': + return True # macOS should generally support GUI + else: + # Linux - check for DISPLAY + return bool(os.getenv('DISPLAY')) + except ImportError: + return False + def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]: + """Launch appropriate UI based on environment""" + + if can_use_gui(): + # Use Qt GUI (original implementation) + return launch_qt_feedback_ui(project_directory, summary) + else: + # Use Web UI + return launch_web_feedback_ui(project_directory, summary) + +def launch_qt_feedback_ui(project_directory: str, summary: str) -> dict[str, str]: + """Original Qt GUI implementation""" # Create a temporary file for the feedback result with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp: output_file = tmp.name @@ -58,6 +110,57 @@ def launch_feedback_ui(project_directory: str, summary: str) -> dict[str, str]: os.unlink(output_file) raise e +def launch_web_feedback_ui(project_directory: str, summary: str) -> dict[str, str]: + """Launch Web UI implementation""" + try: + from web_ui import launch_web_feedback_ui as launch_web + return launch_web(project_directory, summary) + except ImportError as e: + # Fallback to command line if web UI fails + print(f"Web UI not available: {e}") + return launch_cli_feedback_ui(project_directory, summary) + +def launch_cli_feedback_ui(project_directory: str, summary: str) -> dict[str, str]: + """Simple command line fallback""" + print(f"\n{'='*60}") + print("Interactive Feedback MCP") + print(f"{'='*60}") + print(f"專案目錄: {project_directory}") + print(f"任務描述: {summary}") + print(f"{'='*60}") + + # Ask for command to run + command = input("要執行的命令 (留空跳過): ").strip() + command_logs = "" + + if command: + print(f"執行命令: {command}") + try: + result = subprocess.run( + command, + shell=True, + cwd=project_directory, + capture_output=True, + text=True, + encoding="utf-8", + errors="ignore" + ) + command_logs = f"$ {command}\n{result.stdout}{result.stderr}" + print(command_logs) + except Exception as e: + command_logs = f"$ {command}\nError: {str(e)}\n" + print(command_logs) + + # Ask for feedback + print(f"\n{'='*60}") + print("請提供您的回饋意見:") + feedback = input().strip() + + return { + "command_logs": command_logs, + "interactive_feedback": feedback + } + def first_line(text: str) -> str: return text.split("\n")[0].strip() diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..c4da73a --- /dev/null +++ b/static/style.css @@ -0,0 +1,447 @@ +/* Interactive Feedback MCP - Modern Dark Theme */ +:root { + --primary-color: #007acc; + --primary-hover: #005999; + --background-color: #1e1e1e; + --surface-color: #2d2d30; + --surface-hover: #383838; + --text-primary: #cccccc; + --text-secondary: #9e9e9e; + --text-accent: #007acc; + --border-color: #464647; + --success-color: #4caf50; + --warning-color: #ff9800; + --error-color: #f44336; + --console-bg: #1a1a1a; + --input-bg: #2d2d30; + --button-bg: #0e639c; + --button-hover-bg: #1177bb; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: var(--background-color); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +h1 { + text-align: center; + color: var(--text-accent); + margin-bottom: 30px; + font-size: 2.5em; + font-weight: 300; +} + +h2, h3 { + color: var(--text-primary); + margin-bottom: 15px; +} + +h3 { + font-size: 1.3em; + font-weight: 500; +} + +.section { + background-color: var(--surface-color); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid var(--border-color); + transition: all 0.3s ease; +} + +.section:hover { + background-color: var(--surface-hover); +} + +.session-info { + background: linear-gradient(135deg, var(--surface-color), var(--surface-hover)); + border-left: 4px solid var(--primary-color); +} + +.session-info p { + margin-bottom: 8px; + font-size: 1.1em; +} + +.session-info strong { + color: var(--text-accent); +} + +.toggle-btn { + width: 100%; + background-color: var(--button-bg); + color: white; + border: none; + padding: 12px 20px; + border-radius: 6px; + font-size: 1.1em; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 10px; +} + +.toggle-btn:hover { + background-color: var(--button-hover-bg); + transform: translateY(-1px); +} + +.command-section { + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.input-group { + display: flex; + gap: 10px; + margin-bottom: 15px; + align-items: center; +} + +.input-group input { + flex: 1; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 12px 15px; + color: var(--text-primary); + font-size: 14px; + transition: all 0.3s ease; +} + +.input-group input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1); +} + +.input-group button { + background-color: var(--button-bg); + color: white; + border: none; + padding: 12px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + min-width: 80px; +} + +.input-group button:hover { + background-color: var(--button-hover-bg); + transform: translateY(-1px); +} + +#stop-btn { + background-color: var(--error-color); +} + +#stop-btn:hover { + background-color: #d32f2f; +} + +.checkbox-group { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 20px; + padding: 10px 0; +} + +.checkbox-group label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 14px; +} + +.checkbox-group input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary-color); +} + +#save-config { + background-color: var(--success-color); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: all 0.3s ease; +} + +#save-config:hover { + background-color: #45a049; +} + +.console-section { + margin-top: 20px; +} + +.console-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.console-header h4 { + margin: 0; + color: var(--text-secondary); +} + +#clear-logs { + background-color: var(--warning-color); + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.3s ease; +} + +#clear-logs:hover { + background-color: #f57c00; +} + +.console { + background-color: var(--console-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 15px; + height: 300px; + overflow-y: auto; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 13px; + line-height: 1.4; + white-space: pre-wrap; + word-wrap: break-word; +} + +.console-line { + margin-bottom: 2px; + color: var(--text-primary); +} + +.console::-webkit-scrollbar { + width: 8px; +} + +.console::-webkit-scrollbar-track { + background: var(--surface-color); + border-radius: 4px; +} + +.console::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +.console::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +.feedback-section { + background: linear-gradient(135deg, var(--surface-color), var(--surface-hover)); + border-left: 4px solid var(--success-color); + flex-grow: 1; + display: flex; + flex-direction: column; +} + +.feedback-description { + background-color: var(--input-bg); + padding: 15px; + border-radius: 6px; + margin-bottom: 15px; + border-left: 3px solid var(--primary-color); + font-style: italic; + color: var(--text-secondary); +} + +#feedback-input { + flex-grow: 1; + min-height: 150px; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 15px; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; + resize: vertical; + transition: all 0.3s ease; + margin-bottom: 15px; +} + +#feedback-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.1); +} + +#feedback-input::placeholder { + color: var(--text-secondary); +} + +#submit-feedback { + background: linear-gradient(135deg, var(--success-color), #66bb6a); + color: white; + border: none; + padding: 15px 30px; + border-radius: 6px; + cursor: pointer; + font-size: 16px; + font-weight: 600; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +#submit-feedback:hover { + background: linear-gradient(135deg, #45a049, #4caf50); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); +} + +.footer { + text-align: center; + margin-top: 30px; + padding: 20px; + color: var(--text-secondary); + font-size: 13px; + border-top: 1px solid var(--border-color); +} + +.footer a { + color: var(--text-accent); + text-decoration: none; + transition: color 0.3s ease; +} + +.footer a:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +.loading { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.spinner { + width: 50px; + height: 50px; + border: 4px solid var(--border-color); + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading p { + color: var(--text-primary); + font-size: 18px; + font-weight: 500; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + h1 { + font-size: 2em; + margin-bottom: 20px; + } + + .input-group { + flex-direction: column; + align-items: stretch; + } + + .input-group button { + margin-top: 10px; + } + + .checkbox-group { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .console { + height: 200px; + } + + #feedback-input { + min-height: 120px; + } +} + +/* Smooth transitions for all interactive elements */ +* { + transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease; +} + +/* Focus styles for accessibility */ +button:focus, +input:focus, +textarea:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* Custom selection colors */ +::selection { + background-color: var(--primary-color); + color: white; +} \ No newline at end of file diff --git a/templates/feedback.html b/templates/feedback.html new file mode 100644 index 0000000..38502ac --- /dev/null +++ b/templates/feedback.html @@ -0,0 +1,245 @@ + + + + + + Interactive Feedback MCP + + + + +
+

Interactive Feedback MCP

+ +
+

專案資訊

+

工作目錄: {{ project_directory }}

+

任務描述: {{ summary }}

+
+ +
+ +
+ + + +
+

回饋意見

+ + + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ddce071 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,41 @@ + + + + + + Interactive Feedback MCP Server + + + +
+

Interactive Feedback MCP

+ +
+

服務器狀態

+

🟢 MCP 服務器正在運行

+

等待來自 AI 助手的互動請求...

+
+ +
+

關於此服務

+

這是一個 Model Context Protocol (MCP) 服務器,用於在 AI 輔助開發工具中提供人在回路的互動回饋功能。

+

當 AI 助手需要用戶回饋時,會自動在瀏覽器中開啟互動頁面。

+
+ +
+

功能特色

+ +
+ + +
+ + \ No newline at end of file diff --git a/test_web_ui.py b/test_web_ui.py new file mode 100644 index 0000000..19d2164 --- /dev/null +++ b/test_web_ui.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +""" +Test script for Interactive Feedback MCP Web UI +""" +import sys +import threading +import time +import socket +from pathlib import Path + +def find_free_port(): + """Find a free port to use for testing""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('', 0)) + s.listen(1) + port = s.getsockname()[1] + return port + +def test_web_ui(keep_running=False): + """Test the Web UI functionality""" + + print("🧪 測試 Interactive Feedback MCP Web UI") + print("=" * 50) + + # Test import + try: + from web_ui import WebUIManager, launch_web_feedback_ui + print("✅ Web UI 模組匯入成功") + except ImportError as e: + print(f"❌ Web UI 模組匯入失敗: {e}") + return False, None + + # Find free port + try: + free_port = find_free_port() + print(f"🔍 找到可用端口: {free_port}") + except Exception as e: + print(f"❌ 尋找可用端口失敗: {e}") + return False, None + + # Test manager creation + try: + manager = WebUIManager(port=free_port) + print("✅ WebUIManager 創建成功") + except Exception as e: + print(f"❌ WebUIManager 創建失敗: {e}") + return False, None + + # Test server start (with timeout) + server_started = False + try: + print("🚀 啟動 Web 服務器...") + + def start_server(): + try: + manager.start_server() + return True + except Exception as e: + print(f"服務器啟動錯誤: {e}") + return False + + # Start server in thread + server_thread = threading.Thread(target=start_server) + server_thread.daemon = True + server_thread.start() + + # Wait a moment and test if server is responsive + time.sleep(3) + + # Test if port is listening + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(1) + result = s.connect_ex((manager.host, manager.port)) + if result == 0: + server_started = True + print("✅ Web 服務器啟動成功") + print(f"🌐 服務器運行在: http://{manager.host}:{manager.port}") + else: + print(f"❌ 無法連接到服務器端口 {manager.port}") + + except Exception as e: + print(f"❌ Web 服務器啟動失敗: {e}") + return False, None + + if not server_started: + print("❌ 服務器未能正常啟動") + return False, None + + # Test session creation + session_info = None + try: + project_dir = str(Path.cwd()) + summary = "測試 Web UI 功能" + session_id = manager.create_session(project_dir, summary) + session_info = { + 'manager': manager, + 'session_id': session_id, + 'url': f"http://{manager.host}:{manager.port}/session/{session_id}" + } + print(f"✅ 測試會話創建成功 (ID: {session_id[:8]}...)") + print(f"🔗 測試 URL: {session_info['url']}") + except Exception as e: + print(f"❌ 會話創建失敗: {e}") + return False, None + + print("\n" + "=" * 50) + print("🎉 所有測試通過!Web UI 準備就緒") + print("📝 注意事項:") + print(" - Web UI 會在 SSH remote 環境下自動啟用") + print(" - 本地環境會繼續使用 Qt GUI") + print(" - 支援即時命令執行和 WebSocket 通訊") + print(" - 提供現代化的深色主題界面") + + return True, session_info + +def test_environment_detection(): + """Test environment detection logic""" + print("🔍 測試環境檢測功能") + print("-" * 30) + + try: + from server import is_ssh_session, can_use_gui + + ssh_detected = is_ssh_session() + gui_available = can_use_gui() + + print(f"SSH 環境檢測: {'是' if ssh_detected else '否'}") + print(f"GUI 可用性: {'是' if gui_available else '否'}") + + if ssh_detected: + print("✅ 將使用 Web UI (適合 SSH remote 開發)") + else: + print("✅ 將使用 Qt GUI (本地環境)") + + return True + + except Exception as e: + print(f"❌ 環境檢測失敗: {e}") + return False + +def test_mcp_integration(): + """Test MCP server integration""" + print("\n🔧 測試 MCP 整合功能") + print("-" * 30) + + try: + from server import interactive_feedback + print("✅ MCP 工具函數可用") + + # Test would require actual MCP call, so just verify import + print("✅ 準備接受來自 AI 助手的調用") + return True + + except Exception as e: + print(f"❌ MCP 整合測試失敗: {e}") + return False + +def interactive_demo(session_info): + """Run interactive demo with the Web UI""" + print(f"\n🌐 Web UI 持久化運行模式") + print("=" * 50) + print(f"服務器地址: http://{session_info['manager'].host}:{session_info['manager'].port}") + print(f"測試會話: {session_info['url']}") + print("\n📖 操作指南:") + print(" 1. 在瀏覽器中開啟上面的測試 URL") + print(" 2. 嘗試以下功能:") + print(" - 點擊 '顯示命令區塊' 按鈕") + print(" - 輸入命令如 'echo Hello World' 並執行") + print(" - 在回饋區域輸入文字") + print(" - 使用 Ctrl+Enter 提交回饋") + print(" 3. 測試 WebSocket 即時通訊功能") + print("\n⌨️ 控制選項:") + print(" - 按 Enter 繼續運行") + print(" - 輸入 'q' 或 'quit' 停止服務器") + + while True: + try: + user_input = input("\n>>> ").strip().lower() + if user_input in ['q', 'quit', 'exit']: + print("🛑 停止服務器...") + break + elif user_input == '': + print(f"🔄 服務器持續運行在: {session_info['url']}") + print(" 瀏覽器應該仍可正常訪問") + else: + print("❓ 未知命令。按 Enter 繼續運行,或輸入 'q' 退出") + except KeyboardInterrupt: + print("\n🛑 收到中斷信號,停止服務器...") + break + + print("✅ Web UI 測試完成") + +if __name__ == "__main__": + print("Interactive Feedback MCP - Web UI 測試") + print("=" * 60) + + # Check if user wants persistent mode + persistent_mode = len(sys.argv) > 1 and sys.argv[1] in ['--persistent', '-p', '--demo'] + + if not persistent_mode: + print("💡 提示: 使用 'python test_web_ui.py --persistent' 啟動持久化測試模式") + print() + + # Test environment detection + env_test = test_environment_detection() + + # Test MCP integration + mcp_test = test_mcp_integration() + + # Test Web UI + web_test, session_info = test_web_ui() + + print("\n" + "=" * 60) + if env_test and mcp_test and web_test: + print("🎊 所有測試完成!準備使用 Interactive Feedback MCP") + print("\n📖 使用方法:") + print(" 1. 在 Cursor/Cline 中配置此 MCP 服務器") + print(" 2. AI 助手會自動調用 interactive_feedback 工具") + print(" 3. 根據環境自動選擇 GUI 或 Web UI") + print(" 4. 提供回饋後繼續工作流程") + + print("\n✨ Web UI 新功能:") + print(" - 支援 SSH remote 開發環境") + print(" - 現代化深色主題界面") + print(" - WebSocket 即時通訊") + print(" - 自動瀏覽器啟動") + print(" - 命令執行和即時輸出") + + if persistent_mode and session_info: + interactive_demo(session_info) + else: + print("\n✅ 測試完成 - 系統已準備就緒!") + if session_info: + print(f"💡 您可以現在就在瀏覽器中測試: {session_info['url']}") + print(" (服務器會繼續運行一小段時間)") + time.sleep(10) # Keep running for a short time for immediate testing + else: + print("❌ 部分測試失敗,請檢查錯誤信息") + sys.exit(1) \ No newline at end of file diff --git a/web_ui.py b/web_ui.py new file mode 100644 index 0000000..98025c3 --- /dev/null +++ b/web_ui.py @@ -0,0 +1,355 @@ +# Interactive Feedback MCP Web UI +# Developed by Fábio Ferreira (https://x.com/fabiomlferreira) +# Web UI version for SSH remote development +import os +import sys +import json +import uuid +import asyncio +import webbrowser +import threading +import subprocess +import psutil +import time +from typing import Dict, Optional, List +from pathlib import Path + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +import uvicorn + +class WebFeedbackSession: + def __init__(self, session_id: str, project_directory: str, summary: str): + self.session_id = session_id + self.project_directory = project_directory + self.summary = summary + self.websocket: Optional[WebSocket] = None + self.feedback_result: Optional[str] = None + self.command_logs: List[str] = [] + self.process: Optional[subprocess.Popen] = None + self.completed = False + self.config = { + "run_command": "", + "execute_automatically": False + } + +class WebUIManager: + def __init__(self, host: str = "127.0.0.1", port: int = 8765): + self.host = host + self.port = port + self.app = FastAPI(title="Interactive Feedback MCP") + self.sessions: Dict[str, WebFeedbackSession] = {} + self.server_process = None + self.setup_routes() + + # Setup static files and templates + script_dir = Path(__file__).parent + static_dir = script_dir / "static" + templates_dir = script_dir / "templates" + static_dir.mkdir(exist_ok=True) + templates_dir.mkdir(exist_ok=True) + + self.app.mount("/static", StaticFiles(directory=static_dir), name="static") + self.templates = Jinja2Templates(directory=templates_dir) + + def setup_routes(self): + @self.app.get("/", response_class=HTMLResponse) + async def index(request: Request): + return self.templates.TemplateResponse("index.html", {"request": request}) + + @self.app.get("/session/{session_id}", response_class=HTMLResponse) + async def session_page(request: Request, session_id: str): + session = self.sessions.get(session_id) + if not session: + return HTMLResponse("Session not found", status_code=404) + + return self.templates.TemplateResponse("feedback.html", { + "request": request, + "session_id": session_id, + "project_directory": session.project_directory, + "summary": session.summary + }) + + @self.app.websocket("/ws/{session_id}") + async def websocket_endpoint(websocket: WebSocket, session_id: str): + await websocket.accept() + + session = self.sessions.get(session_id) + if not session: + await websocket.close(code=4000, reason="Session not found") + return + + session.websocket = websocket + + # Send initial data + await websocket.send_json({ + "type": "init", + "project_directory": session.project_directory, + "summary": session.summary, + "config": session.config, + "logs": session.command_logs + }) + + try: + while True: + data = await websocket.receive_json() + await self.handle_websocket_message(session, data) + + except WebSocketDisconnect: + session.websocket = None + + @self.app.post("/api/complete/{session_id}") + async def complete_session(session_id: str, feedback_data: dict): + session = self.sessions.get(session_id) + if not session: + return {"error": "Session not found"} + + session.feedback_result = feedback_data.get("feedback", "") + session.completed = True + + return {"success": True} + + async def handle_websocket_message(self, session: WebFeedbackSession, data: dict): + message_type = data.get("type") + + if message_type == "run_command": + command = data.get("command", "") + await self.run_command(session, command) + + elif message_type == "stop_command": + await self.stop_command(session) + + elif message_type == "submit_feedback": + feedback = data.get("feedback", "") + session.feedback_result = feedback + session.completed = True + + await session.websocket.send_json({ + "type": "feedback_submitted", + "message": "Feedback submitted successfully" + }) + + elif message_type == "update_config": + session.config.update(data.get("config", {})) + + elif message_type == "clear_logs": + session.command_logs.clear() + await session.websocket.send_json({ + "type": "logs_cleared" + }) + + async def run_command(self, session: WebFeedbackSession, command: str): + if session.process: + await self.stop_command(session) + + if not command.strip(): + await session.websocket.send_json({ + "type": "log", + "data": "Please enter a command to run\n" + }) + return + + session.command_logs.append(f"$ {command}\n") + await session.websocket.send_json({ + "type": "log", + "data": f"$ {command}\n" + }) + + try: + session.process = subprocess.Popen( + command, + shell=True, + cwd=session.project_directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + encoding="utf-8", + errors="ignore" + ) + + # Start threads to read output + threading.Thread( + target=self.read_process_output, + args=(session, session.process.stdout), + daemon=True + ).start() + + threading.Thread( + target=self.read_process_output, + args=(session, session.process.stderr), + daemon=True + ).start() + + # Monitor process completion + threading.Thread( + target=self.monitor_process, + args=(session,), + daemon=True + ).start() + + except Exception as e: + error_msg = f"Error running command: {str(e)}\n" + session.command_logs.append(error_msg) + await session.websocket.send_json({ + "type": "log", + "data": error_msg + }) + + def read_process_output(self, session: WebFeedbackSession, pipe): + try: + for line in iter(pipe.readline, ""): + if not line: + break + session.command_logs.append(line) + if session.websocket: + # Use threading to send async message + threading.Thread( + target=self._send_websocket_message, + args=(session.websocket, { + "type": "log", + "data": line + }), + daemon=True + ).start() + except Exception: + pass + + def monitor_process(self, session: WebFeedbackSession): + if session.process: + exit_code = session.process.wait() + completion_msg = f"\nProcess exited with code {exit_code}\n" + session.command_logs.append(completion_msg) + + if session.websocket: + threading.Thread( + target=self._send_websocket_message, + args=(session.websocket, { + "type": "log", + "data": completion_msg + }), + daemon=True + ).start() + + threading.Thread( + target=self._send_websocket_message, + args=(session.websocket, { + "type": "process_completed", + "exit_code": exit_code + }), + daemon=True + ).start() + + session.process = None + + def _send_websocket_message(self, websocket: WebSocket, message: dict): + """Helper to send websocket message from thread""" + try: + import asyncio + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(websocket.send_json(message)) + loop.close() + except Exception: + pass + + async def stop_command(self, session: WebFeedbackSession): + if session.process: + try: + # Kill process tree + parent = psutil.Process(session.process.pid) + for child in parent.children(recursive=True): + try: + child.kill() + except psutil.Error: + pass + parent.kill() + session.process = None + + await session.websocket.send_json({ + "type": "log", + "data": "\nProcess stopped\n" + }) + + except Exception as e: + await session.websocket.send_json({ + "type": "log", + "data": f"\nError stopping process: {str(e)}\n" + }) + + def create_session(self, project_directory: str, summary: str) -> str: + session_id = str(uuid.uuid4()) + session = WebFeedbackSession(session_id, project_directory, summary) + self.sessions[session_id] = session + return session_id + + def start_server(self): + """Start the web server in a separate thread""" + if self.server_process is not None: + return # Server already running + + def run_server(): + uvicorn.run( + self.app, + host=self.host, + port=self.port, + log_level="error", + access_log=False + ) + + self.server_process = threading.Thread(target=run_server, daemon=True) + self.server_process.start() + + # Wait a moment for server to start + time.sleep(1) + + def open_browser(self, session_id: str): + """Open browser to the session page""" + url = f"http://{self.host}:{self.port}/session/{session_id}" + try: + webbrowser.open(url) + except Exception: + print(f"Please open your browser and navigate to: {url}") + + def wait_for_feedback(self, session_id: str, timeout: int = 300) -> dict: + """Wait for user feedback with timeout""" + session = self.sessions.get(session_id) + if not session: + return {"command_logs": "", "interactive_feedback": "Session not found"} + + # Wait for feedback with timeout + start_time = time.time() + while not session.completed: + if time.time() - start_time > timeout: + return {"command_logs": "", "interactive_feedback": "Timeout waiting for feedback"} + time.sleep(0.1) + + result = { + "command_logs": "".join(session.command_logs), + "interactive_feedback": session.feedback_result or "" + } + + # Clean up session + del self.sessions[session_id] + + return result + +# Global instance +web_ui_manager = WebUIManager() + +def launch_web_feedback_ui(project_directory: str, summary: str) -> dict: + """Launch web UI and wait for feedback""" + + # Start server if not running + web_ui_manager.start_server() + + # Create new session + session_id = web_ui_manager.create_session(project_directory, summary) + + # Open browser + web_ui_manager.open_browser(session_id) + + # Wait for feedback + return web_ui_manager.wait_for_feedback(session_id) \ No newline at end of file