Skip to content

Commit 556954c

Browse files
committed
Add Windows Terminal support to terminal.py FoundationAgents#1897
1 parent ce43e48 commit 556954c

File tree

1 file changed

+62
-10
lines changed

1 file changed

+62
-10
lines changed

metagpt/tools/libs/terminal.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import os
33
import re
4+
import sys
45
from asyncio import Queue
56
from asyncio.subprocess import PIPE, STDOUT
67
from typing import Optional
@@ -22,8 +23,20 @@ class Terminal:
2223
"""
2324

2425
def __init__(self):
25-
self.shell_command = ["bash"] # FIXME: should consider windows support later
26-
self.command_terminator = "\n"
26+
#self.shell_command = ["bash"] # FIXME: should consider windows support later
27+
#self.command_terminator = "\n"
28+
# Add Windows Terminal support to terminal.py #1897
29+
if sys.platform.startswith("win"):
30+
self.shell_command = ["cmd.exe"] # Windows
31+
self.executable = None
32+
self.command_terminator = "\r\n"
33+
self.pwd_command = "cd"
34+
else:
35+
self.shell_command = ["bash"] # Linux / macOS
36+
self.executable = "bash"
37+
self.command_terminator = "\n"
38+
self.pwd_command = "pwd"
39+
2740
self.stdout_queue = Queue(maxsize=1000)
2841
self.observer = TerminalReporter()
2942
self.process: Optional[asyncio.subprocess.Process] = None
@@ -33,6 +46,9 @@ def __init__(self):
3346
# serve cmd have a space behind it,
3447
"serve ": "Use Deployer.deploy_to_public instead.",
3548
}
49+
# yswang add
50+
self.chat_id = ""
51+
self.role = None;
3652

3753
async def _start_process(self):
3854
# Start a persistent shell process
@@ -41,17 +57,19 @@ async def _start_process(self):
4157
stdin=PIPE,
4258
stdout=PIPE,
4359
stderr=STDOUT,
44-
executable="bash",
60+
executable=self.executable,
4561
env=os.environ.copy(),
46-
cwd=DEFAULT_WORKSPACE_ROOT.absolute(),
62+
#cwd=DEFAULT_WORKSPACE_ROOT.absolute(),
63+
cwd=str(DEFAULT_WORKSPACE_ROOT) if sys.platform.startswith("win") else DEFAULT_WORKSPACE_ROOT, # Windows
4764
)
4865
await self._check_state()
4966

5067
async def _check_state(self):
5168
"""
5269
Check the state of the terminal, e.g. the current directory of the terminal process. Useful for agent to understand.
5370
"""
54-
output = await self.run_command("pwd")
71+
#output = await self.run_command("pwd")
72+
output = await self.run_command(self.pwd_command)
5573
logger.info("The terminal is at:", output)
5674

5775
async def run_command(self, cmd: str, daemon=False) -> str:
@@ -74,6 +92,8 @@ async def run_command(self, cmd: str, daemon=False) -> str:
7492
output = ""
7593
# Remove forbidden commands
7694
commands = re.split(r"\s*&&\s*", cmd)
95+
96+
"""
7797
for cmd_name, reason in self.forbidden_commands.items():
7898
# "true" is a pass command in linux terminal.
7999
for index, command in enumerate(commands):
@@ -87,7 +107,23 @@ async def run_command(self, cmd: str, daemon=False) -> str:
87107
self.process.stdin.write(
88108
f'echo "{END_MARKER_VALUE}"{self.command_terminator}'.encode() # write EOF
89109
) # Unique marker to signal command end
110+
"""
111+
skip_cmd = "echo Skipped" if sys.platform.startswith("win") else "true"
112+
for cmd_name, reason in self.forbidden_commands.items():
113+
# "true" is a pass command in linux terminal.
114+
for index, command in enumerate(commands):
115+
if cmd_name in command:
116+
output += f"Failed to execute {command}. {reason}\n"
117+
commands[index] = skip_cmd
118+
cmd = " && ".join(commands)
119+
# Send the command
120+
self.process.stdin.write((cmd + self.command_terminator).encode())
121+
122+
marker_cmd = f"echo {END_MARKER_VALUE}"
123+
self.process.stdin.write((marker_cmd + self.command_terminator).encode()) # Unique marker to signal command end
124+
90125
await self.process.stdin.drain()
126+
91127
if daemon:
92128
asyncio.create_task(self._read_and_process_output(cmd))
93129
else:
@@ -116,7 +152,10 @@ async def execute_in_conda_env(self, cmd: str, env, daemon=False) -> str:
116152
This function wraps `run_command`, prepending the necessary Conda activation commands
117153
to ensure the specified environment is active for the command's execution.
118154
"""
119-
cmd = f"conda run -n {env} {cmd}"
155+
#cmd = f"conda run -n {env} {cmd}"
156+
# windows & linux conda run
157+
cmd = f"conda activate {env} && {cmd}" if sys.platform.startswith("win") else f"conda run -n {env} {cmd}"
158+
120159
return await self.run_command(cmd, daemon=daemon)
121160

122161
async def get_stdout_output(self) -> str:
@@ -147,10 +186,10 @@ async def _read_and_process_output(self, cmd, daemon=False) -> str:
147186
continue
148187
*lines, tmp = output.splitlines(True)
149188
for line in lines:
150-
line = line.decode()
189+
line = line.decode(errors="ignore")
151190
ix = line.rfind(END_MARKER_VALUE)
152191
if ix >= 0:
153-
line = line[0:ix]
192+
line = line[:ix]
154193
if line:
155194
await observer.async_report(line, "output")
156195
# report stdout in real-time
@@ -164,8 +203,21 @@ async def _read_and_process_output(self, cmd, daemon=False) -> str:
164203

165204
async def close(self):
166205
"""Close the persistent shell process."""
167-
self.process.stdin.close()
168-
await self.process.wait()
206+
if self.process:
207+
self.process.stdin.close()
208+
await self.process.wait()
209+
210+
# yswang add
211+
def set_chat_id(self, chat_id: str):
212+
self.chat_id = chat_id
213+
self.observer.set_chat_id(chat_id)
214+
215+
def get_chat_id(self) -> str:
216+
return self.chat_id
217+
218+
def set_role(self, role):
219+
self.observer.set_role(role)
220+
169221

170222

171223
@register_tool(include_functions=["run"])

0 commit comments

Comments
 (0)