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
7 changes: 6 additions & 1 deletion hatchling/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import sys
import argparse
from hatchling.core.logging.logging_config import configure_logging
from hatchling.core.logging.logging_manager import logging_manager
from hatchling.config.settings_registry import SettingsRegistry
Expand All @@ -25,6 +26,10 @@ async def main_async():
Raises:
Exception: Any unhandled exceptions that occur during execution.
"""
parser = argparse.ArgumentParser(description="Hatchling CLI Chat Interface")
parser.add_argument("-f", "--input-file", type=str, help="Path to a file containing commands to execute.")
args = parser.parse_args()

settings_registry = None
try:

Expand All @@ -35,7 +40,7 @@ async def main_async():
default_language_code=settings_registry.settings.ui.language_code)

# Create and run CLI chat interface
cli_chat = CLIChat(settings_registry)
cli_chat = CLIChat(settings_registry, input_file_path=args.input_file)

await cli_chat.initialize_and_run()

Expand Down
5 changes: 5 additions & 0 deletions hatchling/config/languages/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ disable_tools_description = "Disable MCP tools"
version_name = "version"
version_description = "Display the current version of Hatchling"

# Load command
[commands.load]
description = "Execute commands from a file"
args_description = "Path to the file containing commands"

# Hatch commands
[commands.hatch]

Expand Down
5 changes: 5 additions & 0 deletions hatchling/config/languages/fr.toml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ disable_tools_description = "Désactiver les outils MCP"
version_name = "version"
version_description = "Afficher la version actuelle de Hatchling"

# Load command
[commands.load]
description = "Exécuter les commandes depuis un fichier"
args_description = "Chemin vers le fichier contenant les commandes"

# Hatch commands
[commands.hatch]
env_create_name = "env:créer"
Expand Down
22 changes: 21 additions & 1 deletion hatchling/ui/base_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
class BaseChatCommands(AbstractCommands):
"""Handles processing of command inputs in the chat interface."""

def __init__(self, chat_session, settings_registry, style, cli_chat):
super().__init__(chat_session, settings_registry, style)
self.cli_chat = cli_chat

def _register_commands(self) -> None:
"""Register all available chat commands with their handlers."""
# New standardized command registration format with i18n support
Expand Down Expand Up @@ -79,6 +83,13 @@ def _register_commands(self) -> None:
'description': translate("commands.base.version_description"),
'is_async': False,
'args': {}
},
':load': {
'handler': self._cmd_load,
'description': translate("commands.load.description"),
'is_async': True,
'args_description': translate("commands.load.args_description"),
'example': ":load my_script.txt"
}
}

Expand Down Expand Up @@ -196,4 +207,13 @@ def _cmd_version(self, _: str) -> bool:
bool: True to continue the chat session.
"""
self.logger.info(f"Hatchling version: {__version__}")
return True
return True

async def _cmd_load(self, filename: str) -> bool:
"""Handles the :load command to execute commands from a file."""
if not filename:
self.logger.error("Error: Please provide a filename for the :load command.")
return True # Continue interactive session

await self.cli_chat._process_commands_from_file(filename)
return True # Continue interactive session
6 changes: 4 additions & 2 deletions hatchling/ui/chat_command_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@

class ChatCommandHandler:
"""Handles processing of command inputs in the chat interface."""
def __init__(self, chat_session, settings_registry: SettingsRegistry, style: Optional[Style] = None):
def __init__(self, chat_session, settings_registry: SettingsRegistry, style: Optional[Style] = None, cli_chat=None):
"""Initialize the command handler.

Args:
chat_session: The chat session this handler is associated with.
settings_registry (SettingsRegistry): The settings registry containing configuration.
style (Optional[Style]): Style for formatting command output.
cli_chat: The CLIChat instance for accessing its methods.
"""


self.settings_registry = settings_registry
self.base_commands = BaseChatCommands(chat_session, settings_registry, style)
self.cli_chat = cli_chat
self.base_commands = BaseChatCommands(chat_session, settings_registry, style, cli_chat)
self.hatch_commands = HatchCommands(chat_session, settings_registry, style)
self.settings_commands = SettingsCommands(chat_session, settings_registry, style)
self.mcp_commands = MCPCommands(chat_session, settings_registry, style)
Expand Down
52 changes: 44 additions & 8 deletions hatchling/ui/cli_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
class CLIChat:
"""Command-line interface for chat functionality."""

def __init__(self, settings_registry: SettingsRegistry):
def __init__(self, settings_registry: SettingsRegistry, input_file_path: str = None):
"""Initialize the CLI chat interface.

Args:
settings (SettingsRegistry): The settings management instance containing configuration.
input_file_path (str, optional): Path to a file containing commands to execute. Defaults to None.
"""
# Store settings first
self.settings_registry = settings_registry
self.input_file_path = input_file_path

# Get a logger - styling is already configured at the application level
self.logger = logging_manager.get_session("CLIChat")
Expand Down Expand Up @@ -101,7 +103,7 @@ def __init__(self, settings_registry: SettingsRegistry):
mcp_manager.publisher.subscribe(self.cli_event_subscriber)

# Initialize command handler
self.cmd_handler = ChatCommandHandler(self.chat_session, self.settings_registry, self.command_style)
self.cmd_handler = ChatCommandHandler(self.chat_session, self.settings_registry, self.command_style, self)

# Setup key bindings
self.key_bindings = self._create_key_bindings()
Expand Down Expand Up @@ -184,8 +186,12 @@ def _get_right_prompt(self) -> FormattedText:
return FormattedText([('class:right-prompt', right_prompt_text)])

async def start_interactive_session(self) -> None:
"""Run an interactive chat session with message history."""

"""Run an interactive chat session with message history or process commands from a file."""

if self.input_file_path:
await self._process_commands_from_file(self.input_file_path)
# After processing the file, continue to interactive mode

#async with aiohttp.ClientSession() as session:
while True:
try:
Expand All @@ -196,7 +202,7 @@ async def start_interactive_session(self) -> None:
]
# Use patch_stdout to prevent output interference
with patch_stdout():
user_message = await self.prompt_session.prompt_async(
user_input = await self.prompt_session.prompt_async(
FormattedText(prompt_message),
completer=self.cmd_handler.command_completer,
lexer=self.cmd_handler.command_lexer,
Expand All @@ -207,14 +213,14 @@ async def start_interactive_session(self) -> None:
)

# Process as command if applicable
is_command, should_continue = await self.cmd_handler.process_command(user_message)
is_command, should_continue = await self.cmd_handler.process_command(user_input)
if is_command:
if not should_continue:
break
continue

# Handle normal message
if not user_message.strip():
if not user_input.strip():
# Skip empty input
continue

Expand All @@ -226,7 +232,7 @@ async def start_interactive_session(self) -> None:

try:
# Send the message (this will trigger streaming events)
await self.chat_session.send_message(user_message)
await self.chat_session.send_message(user_input)

# Wait for all processing to complete (tool chains, etc.)
await self._monitor_right_to_prompt()
Expand All @@ -244,6 +250,36 @@ async def start_interactive_session(self) -> None:
except Exception as e:
self.logger.error(f"Error: {e}")
print_pt(FormattedText([('red', f'\nError: {e}')]))

async def _process_commands_from_file(self, file_path: str) -> None:
"""Processes commands from a given file path."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
user_input = line.strip()
if not user_input:
continue

print_pt(FormattedText([('green', f'You: {user_input}')]))

is_command, should_continue = await self.cmd_handler.process_command(user_input)
if is_command:
if not should_continue:
break
continue

self.cli_event_subscriber.set_processing_user_message(True)
try:
await self.chat_session.send_message(user_input)
await self._monitor_right_to_prompt()
except Exception as send_error:
self.cli_event_subscriber.set_processing_user_message(False)
self.logger.error(f"Error processing message from file: {send_error}")
print_pt('')
except FileNotFoundError:
self.logger.error(f"Error: Input file not found at {file_path}")
except Exception as e:
self.logger.error(f"Error processing input file: {e}")

async def initialize_and_run(self) -> None:
"""Initialize the environment and run the interactive chat session."""
Expand Down