Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
237 changes: 157 additions & 80 deletions libs/deepagents-cli/deepagents_cli/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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()

Expand Down
9 changes: 7 additions & 2 deletions libs/deepagents-cli/deepagents_cli/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import re
import sys
from pathlib import Path

from prompt_toolkit import PromptSession
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion libs/deepagents-cli/deepagents_cli/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
10 changes: 10 additions & 0 deletions libs/deepagents/middleware/resumable_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import sys
from collections.abc import Awaitable, Callable
from typing import Any, cast

Expand Down Expand Up @@ -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,
Expand Down