341 lines
16 KiB
Python
341 lines
16 KiB
Python
"""
|
|
Interactive shell for the Edison application.
|
|
"""
|
|
import logging
|
|
import os
|
|
import sys
|
|
from termcolor import colored
|
|
from prompt_toolkit import PromptSession
|
|
from prompt_toolkit.history import FileHistory
|
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
from prompt_toolkit.lexers import PygmentsLexer
|
|
from pygments.lexers.shell import BashLexer
|
|
from edison.core import api_client, command_executor
|
|
from edison.utils import os_utils, markdown_utils
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def handle_streaming_output_interactive(token):
|
|
"""
|
|
Handle a token from the streaming API response in interactive mode.
|
|
|
|
Args:
|
|
token (str): A token from the streaming response.
|
|
"""
|
|
# Print the token without a newline and flush immediately
|
|
print(token, end='', flush=True)
|
|
|
|
def get_command_explanation(client, command, structured=False):
|
|
"""
|
|
Get an explanation for a command.
|
|
|
|
Args:
|
|
client: The OpenAI client.
|
|
command (str): The command to explain.
|
|
structured (bool): Whether to use structured explanation format.
|
|
|
|
Returns:
|
|
str: The explanation of the command.
|
|
"""
|
|
try:
|
|
system_prompt = """Explain this shell command in simple terms, breaking down each part:"""
|
|
|
|
if structured:
|
|
system_prompt = """
|
|
Explain this shell command in a structured format with these sections:
|
|
|
|
## Overview
|
|
A brief overview of what the command does
|
|
|
|
## Command Breakdown
|
|
Break down each part of the command with explanations
|
|
|
|
## Options/Flags
|
|
Explain any options or flags used
|
|
|
|
## Examples
|
|
Provide 1-2 simple examples of variations
|
|
|
|
## Cautions
|
|
Note any potential issues or cautions
|
|
"""
|
|
|
|
response = client.chat.completions.create(
|
|
model="gpt-4o-mini",
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": command}
|
|
]
|
|
)
|
|
return response.choices[0].message.content
|
|
except Exception as e:
|
|
logger.error(f"Error getting command explanation: {e}")
|
|
return "Could not generate explanation."
|
|
|
|
def interactive_mode(client, config):
|
|
"""
|
|
Start an interactive shell.
|
|
|
|
Args:
|
|
client: The OpenAI client.
|
|
config (dict): The configuration dictionary.
|
|
"""
|
|
# Create history file path
|
|
history_path = os.path.expanduser("~/.edison_history")
|
|
history_dir = os.path.dirname(history_path)
|
|
os.makedirs(history_dir, exist_ok=True)
|
|
|
|
# Create prompt session
|
|
history = FileHistory(history_path)
|
|
session = PromptSession(
|
|
history=history,
|
|
auto_suggest=AutoSuggestFromHistory(),
|
|
lexer=PygmentsLexer(BashLexer)
|
|
)
|
|
|
|
# Print welcome message
|
|
print(colored("Edison Interactive Shell", "blue", attrs=["bold"]))
|
|
print("Type your queries in natural language.")
|
|
print("Type '!help' for help, '!exit' or '!quit' to exit, or press Ctrl+D to exit.")
|
|
|
|
while True:
|
|
try:
|
|
# Get user input
|
|
query = session.prompt("edison> ")
|
|
|
|
# Handle special commands
|
|
if query.strip().lower() in ["!exit", "!quit", "exit", "quit"]:
|
|
break
|
|
elif query.strip() == "!help":
|
|
show_help()
|
|
continue
|
|
elif not query.strip():
|
|
continue
|
|
|
|
# Generate command
|
|
try:
|
|
# Use streaming if enabled in config
|
|
if config.get("streaming", True):
|
|
# Check if we should show the "Generating command: " prefix
|
|
if config.get("show_generating_prefix", True):
|
|
print(colored("Generating command: ", "yellow"), end='', flush=True)
|
|
try:
|
|
command = api_client.generate_command_streaming(
|
|
client, config, query, handle_streaming_output_interactive
|
|
)
|
|
print() # Add a newline after streaming is complete
|
|
except KeyboardInterrupt:
|
|
print("\nCommand generation cancelled.")
|
|
continue
|
|
else:
|
|
print(colored("Generating command...", "yellow"))
|
|
command = api_client.generate_command(client, config, query)
|
|
|
|
# Print command with appropriate styling based on config
|
|
ui_config = config.get("ui", {})
|
|
if ui_config.get("rich_formatting", True):
|
|
command_style = ui_config.get("command_style", "panel")
|
|
theme = ui_config.get("theme", "monokai")
|
|
shell = config.get("shell", "bash")
|
|
|
|
if command_style == "panel":
|
|
# Use rich panel style
|
|
from edison.utils.markdown_utils import print_command_rich
|
|
print_command_rich(command, shell=shell, theme=theme)
|
|
else:
|
|
# Use simple highlighting
|
|
print("Command: " + colored(command, 'blue'))
|
|
else:
|
|
# Use traditional styling
|
|
print("Command: " + colored(command, 'blue'))
|
|
|
|
# Ask user what to do with the same prompt as non-interactive mode
|
|
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [c]opy to clipboard [x]plain ==> "
|
|
if os_utils.missing_posix_display():
|
|
prompt_text = "Execute command? [Y]es [n]o [m]odify (prompt/command) [x]plain ==> "
|
|
|
|
action = session.prompt(prompt_text)
|
|
|
|
if action.lower() in ["y", "yes", "e", "execute", ""]:
|
|
# Execute command
|
|
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
|
try:
|
|
command_executor.execute_command(shell, command)
|
|
except Exception as e:
|
|
logger.error(f"Error executing command: {e}")
|
|
print(colored(f"Error executing command: {e}", 'red'))
|
|
elif action.lower() in ["m", "modify"]:
|
|
# Ask whether to modify prompt or command
|
|
mod_choice = session.prompt("Modify [p]rompt or [c]ommand? [c] ==> ")
|
|
|
|
if mod_choice.lower() in ["p", "prompt"]:
|
|
# Modify the prompt (natural language description)
|
|
new_prompt = session.prompt("Modified prompt: ")
|
|
|
|
if not new_prompt.strip():
|
|
print(colored("Empty prompt. Command execution cancelled.", "yellow"))
|
|
continue
|
|
|
|
# Generate new command from modified prompt
|
|
print(colored("Generating new command from modified prompt...", "yellow"))
|
|
|
|
# Use streaming if enabled
|
|
if config.get("streaming", True):
|
|
# Check if we should show the "Generating command: " prefix
|
|
if config.get("show_generating_prefix", True):
|
|
print(colored("Generating command: ", "yellow"), end='', flush=True)
|
|
try:
|
|
new_command = api_client.generate_command_streaming(
|
|
client, config, new_prompt, handle_streaming_output_interactive
|
|
)
|
|
print() # Add a newline after streaming is complete
|
|
except KeyboardInterrupt:
|
|
print("\nCommand generation cancelled.")
|
|
continue
|
|
else:
|
|
new_command = api_client.generate_command(client, config, new_prompt)
|
|
|
|
# Print the new command with styling
|
|
ui_config = config.get("ui", {})
|
|
if ui_config.get("rich_formatting", True):
|
|
command_style = ui_config.get("command_style", "panel")
|
|
theme = ui_config.get("theme", "monokai")
|
|
shell = config.get("shell", "bash")
|
|
|
|
if command_style == "panel":
|
|
# Use rich panel style
|
|
from edison.utils.markdown_utils import print_command_rich
|
|
print_command_rich(new_command, shell=shell, theme=theme)
|
|
else:
|
|
# Use simple highlighting
|
|
print("Command: " + colored(new_command, 'blue'))
|
|
else:
|
|
# Use traditional styling
|
|
print("Command: " + colored(new_command, 'blue'))
|
|
|
|
# Ask for confirmation
|
|
confirm = session.prompt("Execute this command? [Y/n] ")
|
|
if confirm.lower() in ["n", "no"]:
|
|
print(colored("Command execution cancelled.", "yellow"))
|
|
continue
|
|
|
|
# Execute the new command
|
|
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
|
try:
|
|
command_executor.execute_command(shell, new_command)
|
|
except Exception as e:
|
|
logger.error(f"Error executing command: {e}")
|
|
print(colored(f"Error executing command: {e}", 'red'))
|
|
else:
|
|
# Modify the command directly
|
|
modified_command = session.prompt("Modified command: ", default=command)
|
|
|
|
if not modified_command.strip():
|
|
print(colored("Empty command. Command execution cancelled.", "yellow"))
|
|
continue
|
|
|
|
# Print the modified command with styling
|
|
ui_config = config.get("ui", {})
|
|
if ui_config.get("rich_formatting", True):
|
|
command_style = ui_config.get("command_style", "panel")
|
|
theme = ui_config.get("theme", "monokai")
|
|
shell = config.get("shell", "bash")
|
|
|
|
if command_style == "panel":
|
|
# Use rich panel style
|
|
from edison.utils.markdown_utils import print_command_rich
|
|
print_command_rich(modified_command, shell=shell, theme=theme)
|
|
else:
|
|
# Use simple highlighting
|
|
print("Command: " + colored(modified_command, 'blue'))
|
|
else:
|
|
# Use traditional styling
|
|
print("Command: " + colored(modified_command, 'blue'))
|
|
|
|
# Ask for confirmation
|
|
confirm = session.prompt("Execute modified command? [Y/n] ")
|
|
if confirm.lower() in ["n", "no"]:
|
|
print(colored("Command execution cancelled.", "yellow"))
|
|
continue
|
|
|
|
# Execute the modified command
|
|
shell = config.get("shell", os.environ.get("SHELL", "bash"))
|
|
try:
|
|
command_executor.execute_command(shell, modified_command)
|
|
except Exception as e:
|
|
logger.error(f"Error executing command: {e}")
|
|
print(colored(f"Error executing command: {e}", 'red'))
|
|
elif action.lower() in ["c", "copy"]:
|
|
# Copy command to clipboard
|
|
if not os_utils.missing_posix_display():
|
|
try:
|
|
import pyperclip
|
|
pyperclip.copy(command)
|
|
print(colored("Command copied to clipboard.", "green"))
|
|
except Exception as e:
|
|
print(colored(f"Error copying to clipboard: {e}", "red"))
|
|
elif action.lower() in ["x", "explain"]:
|
|
# Explain command
|
|
print(colored("Generating explanation...", "yellow"))
|
|
|
|
# Get UI configuration
|
|
ui_config = config.get("ui", {})
|
|
structured = ui_config.get("structured_explanations", True)
|
|
use_rich = ui_config.get("rich_formatting", True)
|
|
|
|
# Generate explanation
|
|
explanation = get_command_explanation(client, command, structured=structured)
|
|
|
|
if use_rich:
|
|
print() # Add a blank line before the explanation
|
|
# Format the explanation with rich formatting
|
|
formatted_explanation = markdown_utils.format_command_explanation(explanation, use_rich=True)
|
|
print(formatted_explanation)
|
|
else:
|
|
print(colored("\nExplanation:", "green", attrs=["bold"]))
|
|
# Format the explanation with terminal-friendly markdown
|
|
formatted_explanation = markdown_utils.format_command_explanation(explanation)
|
|
print(formatted_explanation)
|
|
elif action.lower() in ["n", "no", "s", "skip"]:
|
|
# Skip
|
|
print(colored("Command skipped.", "yellow"))
|
|
else:
|
|
# Skip if input not recognized
|
|
print(colored("Command skipped - input not recognized.", "yellow"))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error: {e}")
|
|
print(colored(f"Error: {e}", "red"))
|
|
|
|
except KeyboardInterrupt:
|
|
continue
|
|
except EOFError:
|
|
break
|
|
|
|
print("Goodbye!")
|
|
|
|
def show_help():
|
|
"""
|
|
Show help information.
|
|
"""
|
|
help_text = """
|
|
Edison Interactive Shell Help
|
|
|
|
Commands:
|
|
!help - Show this help message
|
|
!exit, !quit - Exit the interactive shell
|
|
exit, quit - Exit the interactive shell
|
|
Ctrl+D - Exit the interactive shell
|
|
|
|
Actions:
|
|
y, yes, <enter> - Execute the generated command
|
|
n, no - Skip the command
|
|
m, modify - Modify the prompt or command before executing
|
|
When selected, you'll be asked whether to modify:
|
|
- [p]rompt: Change the natural language description
|
|
- [c]ommand: Directly edit the generated command
|
|
c, copy - Copy the command to clipboard
|
|
x, explain - Get an explanation of the command
|
|
"""
|
|
print(help_text)
|