diff --git a/README.md b/README.md index f32b6c25..f42ff9ba 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,21 @@ uv add deepagents poetry add deepagents ``` +### Windows Installation + +DeepAgents is fully compatible with Windows 11. The CLI uses platform-specific terminal handling: + +- **Windows**: Uses `msvcrt` module for keyboard input (built-in) +- **Unix/Linux/macOS**: Uses `termios` and `tty` modules + +No additional dependencies are required for Windows support. + +**Note**: If you encounter issues with the `langchain` package versions, ensure you have the latest versions: + +```bash +pip install --upgrade langchain langchain-anthropic langchain-core +``` + ## Usage (To run the example below, you will need to `pip install tavily-python`). diff --git a/libs/deepagents-cli/deepagents_cli/execution.py b/libs/deepagents-cli/deepagents_cli/execution.py index a3c78ced..f6ad5885 100644 --- a/libs/deepagents-cli/deepagents_cli/execution.py +++ b/libs/deepagents-cli/deepagents_cli/execution.py @@ -2,9 +2,14 @@ import json import sys -import termios import threading -import tty + +# Platform-specific imports for terminal control +if sys.platform == 'win32': + import msvcrt +else: + import termios + import tty from langchain_core.messages import HumanMessage, ToolMessage from langgraph.types import Command @@ -53,6 +58,154 @@ def _extract_tool_args(action_request: dict) -> dict | None: return None +if sys.platform == 'win32': + def _get_approval_interactive() -> int: + """Windows approval using msvcrt for arrow keys.""" + options = ["approve", "reject"] + selected = 0 + + # Initial render flag + first_render = True + + while True: + if not first_render: + # Move cursor back to start of menu (up 2 lines, then to start of line) + sys.stdout.write("\033[2A\r") + + first_render = False + + # Display options vertically with ANSI color codes + for i, option in enumerate(options): + sys.stdout.write("\r\033[K") # Clear line from cursor to end + + if i == selected: + if option == "approve": + # Green bold with filled checkbox + sys.stdout.write("\033[1;32m☑ Approve\033[0m\n") + else: + # Red bold with filled checkbox + sys.stdout.write("\033[1;31m☑ Reject\033[0m\n") + elif option == "approve": + # Dim with empty checkbox + sys.stdout.write("\033[2m☐ Approve\033[0m\n") + else: + # Dim with empty checkbox + sys.stdout.write("\033[2m☐ Reject\033[0m\n") + + sys.stdout.flush() + + # Read key using msvcrt + if msvcrt.kbhit(): + key = msvcrt.getch() + + if key == b'\xe0': # Arrow key prefix on Windows + key = msvcrt.getch() + if key == b'H': # Up arrow + selected = (selected - 1) % len(options) + elif key == b'P': # Down arrow + selected = (selected + 1) % len(options) + elif key == b'\r': # Enter + sys.stdout.write("\033[1B\n") # Move down past the menu + break + elif key in (b'a', b'A'): + selected = 0 + sys.stdout.write("\033[1B\n") # Move down past the menu + break + elif key in (b'r', b'R'): + selected = 1 + sys.stdout.write("\033[1B\n") # Move down past the menu + break + elif key == b'\x03': # Ctrl+C + sys.stdout.write("\033[1B\n") # Move down past the menu + raise KeyboardInterrupt + + return selected + +else: + def _get_approval_interactive() -> int: + """Unix approval using termios.""" + options = ["approve", "reject"] + selected = 0 # Start with approve selected + + try: + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + + # Initial render flag + first_render = True + + while True: + if not first_render: + # Move cursor back to start of menu (up 2 lines, then to start of line) + sys.stdout.write("\033[2A\r") + + first_render = False + + # Display options vertically with ANSI color codes + for i, option in enumerate(options): + sys.stdout.write("\r\033[K") # Clear line from cursor to end + + if i == selected: + if option == "approve": + # Green bold with filled checkbox + sys.stdout.write("\033[1;32m☑ Approve\033[0m\n") + else: + # Red bold with filled checkbox + sys.stdout.write("\033[1;31m☑ Reject\033[0m\n") + elif option == "approve": + # Dim with empty checkbox + sys.stdout.write("\033[2m☐ Approve\033[0m\n") + else: + # Dim with empty checkbox + sys.stdout.write("\033[2m☐ Reject\033[0m\n") + + sys.stdout.flush() + + # Read key + char = sys.stdin.read(1) + + if char == "\x1b": # ESC sequence (arrow keys) + next1 = sys.stdin.read(1) + next2 = sys.stdin.read(1) + if next1 == "[": + if next2 == "B": # Down arrow + selected = (selected + 1) % len(options) + elif next2 == "A": # Up arrow + selected = (selected - 1) % len(options) + elif char == "\r" or char == "\n": # Enter + sys.stdout.write("\033[1B\n") # Move down past the menu + break + elif char == "\x03": # Ctrl+C + sys.stdout.write("\033[1B\n") # Move down past the menu + raise KeyboardInterrupt + elif char.lower() == "a": + selected = 0 + sys.stdout.write("\033[1B\n") # Move down past the menu + break + elif char.lower() == "r": + selected = 1 + sys.stdout.write("\033[1B\n") # Move down past the menu + break + + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + except (termios.error, AttributeError): + # Fallback for non-Unix systems + console.print(" ☐ (A)pprove (default)") + console.print(" ☐ (R)eject") + choice = input("\nChoice (A/R, default=Approve): ").strip().lower() + if choice == "r" or choice == "reject": + selected = 1 + else: + selected = 0 + + return selected + + def prompt_for_tool_approval(action_request: dict, assistant_id: str | None) -> dict: """Prompt user to approve/reject a tool action with arrow key navigation.""" description = action_request.get("description", "No description available") @@ -88,84 +241,8 @@ def prompt_for_tool_approval(action_request: dict, assistant_id: str | None) -> render_diff_block(preview.diff, preview.diff_title or preview.title) console.print() - options = ["approve", "reject"] - selected = 0 # Start with approve selected - - try: - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - - try: - tty.setraw(fd) - - # Initial render flag - first_render = True - - while True: - if not first_render: - # Move cursor back to start of menu (up 2 lines, then to start of line) - sys.stdout.write("\033[2A\r") - - first_render = False - - # Display options vertically with ANSI color codes - for i, option in enumerate(options): - sys.stdout.write("\r\033[K") # Clear line from cursor to end - - if i == selected: - if option == "approve": - # Green bold with filled checkbox - sys.stdout.write("\033[1;32m☑ Approve\033[0m\n") - else: - # Red bold with filled checkbox - sys.stdout.write("\033[1;31m☑ Reject\033[0m\n") - elif option == "approve": - # Dim with empty checkbox - sys.stdout.write("\033[2m☐ Approve\033[0m\n") - else: - # Dim with empty checkbox - sys.stdout.write("\033[2m☐ Reject\033[0m\n") - - sys.stdout.flush() - - # Read key - char = sys.stdin.read(1) - - if char == "\x1b": # ESC sequence (arrow keys) - next1 = sys.stdin.read(1) - next2 = sys.stdin.read(1) - if next1 == "[": - if next2 == "B": # Down arrow - selected = (selected + 1) % len(options) - elif next2 == "A": # Up arrow - selected = (selected - 1) % len(options) - elif char == "\r" or char == "\n": # Enter - sys.stdout.write("\033[1B\n") # Move down past the menu - break - elif char == "\x03": # Ctrl+C - sys.stdout.write("\033[1B\n") # Move down past the menu - raise KeyboardInterrupt - elif char.lower() == "a": - selected = 0 - sys.stdout.write("\033[1B\n") # Move down past the menu - break - elif char.lower() == "r": - selected = 1 - sys.stdout.write("\033[1B\n") # Move down past the menu - break - - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - - except (termios.error, AttributeError): - # Fallback for non-Unix systems - console.print(" ☐ (A)pprove (default)") - console.print(" ☐ (R)eject") - choice = input("\nChoice (A/R, default=Approve): ").strip().lower() - if choice == "r" or choice == "reject": - selected = 1 - else: - selected = 0 + # Use platform-specific interactive approval + selected = _get_approval_interactive() console.print() diff --git a/libs/deepagents-cli/deepagents_cli/input.py b/libs/deepagents-cli/deepagents_cli/input.py index 1e7cef78..f0621ba1 100644 --- a/libs/deepagents-cli/deepagents_cli/input.py +++ b/libs/deepagents-cli/deepagents_cli/input.py @@ -2,6 +2,7 @@ import os import re +import sys from pathlib import Path from prompt_toolkit import PromptSession @@ -167,7 +168,11 @@ def create_prompt_session(assistant_id: str, session_state: SessionState) -> Pro """Create a configured PromptSession with all features.""" # Set default editor if not already set if "EDITOR" not in os.environ: - os.environ["EDITOR"] = "nano" + # Use platform-appropriate default editor + if sys.platform == 'win32': + os.environ["EDITOR"] = "notepad" + else: + os.environ["EDITOR"] = "nano" # Create key bindings kb = KeyBindings() @@ -218,7 +223,7 @@ def _(event): # Ctrl+E to open in external editor @kb.add("c-e") def _(event): - """Open the current input in an external editor (nano by default).""" + """Open the current input in an external editor (notepad on Windows, nano on Unix).""" event.current_buffer.open_in_editor() from prompt_toolkit.styles import Style diff --git a/libs/deepagents-cli/deepagents_cli/ui.py b/libs/deepagents-cli/deepagents_cli/ui.py index f97f8354..94d38c79 100644 --- a/libs/deepagents-cli/deepagents_cli/ui.py +++ b/libs/deepagents-cli/deepagents_cli/ui.py @@ -375,8 +375,12 @@ def show_interactive_help(): " Alt+Enter Insert newline (Option+Enter on Mac, or ESC then Enter)", style=COLORS["dim"], ) + + # Platform-appropriate editor name + import sys + default_editor = "notepad" if sys.platform == 'win32' else "nano" console.print( - " Ctrl+E Open in external editor (nano by default)", style=COLORS["dim"] + f" Ctrl+E Open in external editor ({default_editor} by default)", style=COLORS["dim"] ) console.print(" Ctrl+T Toggle auto-approve mode", style=COLORS["dim"]) console.print(" Arrow keys Navigate input", style=COLORS["dim"]) diff --git a/libs/deepagents/middleware/resumable_shell.py b/libs/deepagents/middleware/resumable_shell.py index a6a421c3..9700618f 100644 --- a/libs/deepagents/middleware/resumable_shell.py +++ b/libs/deepagents/middleware/resumable_shell.py @@ -2,6 +2,7 @@ from __future__ import annotations +import sys from collections.abc import Awaitable, Callable from typing import Any, cast @@ -30,8 +31,17 @@ class ResumableShellToolMiddleware(ShellToolMiddleware): touches the shell tool again and only performs shutdown when a session is actually active. This keeps behaviour identical for uninterrupted runs while allowing HITL pauses to succeed. + + On Windows, it uses PowerShell instead of bash for better compatibility. """ + def __init__(self, *args, **kwargs): + """Initialize with platform-appropriate shell.""" + # On Windows, use PowerShell; on Unix, use bash (default) + if sys.platform == 'win32' and 'shell_command' not in kwargs: + kwargs['shell_command'] = 'powershell.exe' + super().__init__(*args, **kwargs) + def wrap_tool_call( self, request: ToolCallRequest,