# Development Guide This guide explains how to add new commands, features, and modules to the Discord ComfyUI bot. ## Table of Contents - [Adding a New Command](#adding-a-new-command) - [Adding a New Feature Module](#adding-a-new-feature-module) - [Adding Configuration Options](#adding-configuration-options) - [Working with Workflows](#working-with-workflows) - [Best Practices](#best-practices) - [Common Patterns](#common-patterns) - [Testing Your Changes](#testing-your-changes) --- ## Adding a New Command Commands are organized in the `commands/` directory by functionality. Here's how to add a new command: ### Step 1: Choose the Right Module Determine which existing command module your command belongs to: - **generation.py** - Image/video generation commands - **workflow.py** - Workflow template management - **upload.py** - File upload commands - **history.py** - History viewing and retrieval - **workflow_changes.py** - Runtime workflow parameter management If none fit, create a new module (see [Adding a New Feature Module](#adding-a-new-feature-module)). ### Step 2: Add Your Command Function Edit the appropriate module in `commands/` and add your command to the `setup_*_commands()` function: ```python # commands/generation.py def setup_generation_commands(bot, config): # ... existing commands ... @bot.command(name="my-new-command", aliases=["mnc", "my-cmd"]) @require_comfy_client # Use this decorator if you need bot.comfy async def my_new_command(ctx: commands.Context, *, args: str = "") -> None: """ Brief description of what your command does. Usage: ttr!my-new-command [arguments] Longer description with examples and details. """ # Parse arguments if needed if not args: await ctx.reply("Please provide arguments!", mention_author=False) return try: # Your command logic here result = await bot.comfy.some_method(args) # Send response await ctx.reply(f"Success! Result: {result}", mention_author=False) except Exception as exc: logger.exception("Failed to execute my-new-command") await ctx.reply( f"An error occurred: {type(exc).__name__}: {exc}", mention_author=False, ) ``` ### Step 3: Import Required Dependencies At the top of your command module, import what you need: ```python import logging from discord.ext import commands from discord_utils import require_comfy_client, parse_labeled_args from config import ARG_PROMPT_KEY, ARG_TYPE_KEY from job_queue import Job logger = logging.getLogger(__name__) ``` ### Step 4: Test Your Command Run the bot and test your command: ```bash python bot.py ``` Then in Discord: ``` ttr!my-new-command test arguments ``` --- ## Adding a New Feature Module If your commands don't fit existing modules, create a new one: ### Step 1: Create the Module File Create `commands/your_feature.py`: ```python """ commands/your_feature.py ======================== Description of what this module handles. """ from __future__ import annotations import logging from discord.ext import commands from discord_utils import require_comfy_client logger = logging.getLogger(__name__) def setup_your_feature_commands(bot, config): """ Register your feature commands with the bot. Parameters ---------- bot : commands.Bot The Discord bot instance. config : BotConfig The bot configuration object. """ @bot.command(name="feature-command") @require_comfy_client async def feature_command(ctx: commands.Context, *, args: str = "") -> None: """Command description.""" await ctx.reply("Feature command executed!", mention_author=False) @bot.command(name="another-command", aliases=["ac"]) async def another_command(ctx: commands.Context) -> None: """Another command description.""" await ctx.reply("Another command!", mention_author=False) ``` ### Step 2: Register in commands/__init__.py Edit `commands/__init__.py` to import and register your module: ```python from .generation import setup_generation_commands from .workflow import setup_workflow_commands from .upload import setup_upload_commands from .history import setup_history_commands from .workflow_changes import setup_workflow_changes_commands from .your_feature import setup_your_feature_commands # ADD THIS def register_all_commands(bot, config): """Register all bot commands.""" setup_generation_commands(bot, config) setup_workflow_commands(bot) setup_upload_commands(bot) setup_history_commands(bot) setup_workflow_changes_commands(bot) setup_your_feature_commands(bot, config) # ADD THIS ``` ### Step 3: Update Documentation Add your module to `CLAUDE.md`: ```markdown ### File Structure ``` commands/ ├── __init__.py ├── generation.py ├── workflow.py ├── upload.py ├── history.py ├── workflow_changes.py └── your_feature.py # Your new module ``` ``` --- ## Adding Configuration Options Configuration is centralized in `config.py`. Here's how to add new options: ### Step 1: Add Constants (if needed) Edit `config.py` and add constants in the appropriate section: ```python # ======================================== # Your Feature Constants # ======================================== MY_FEATURE_DEFAULT_VALUE = 42 """Default value for my feature.""" MY_FEATURE_MAX_LIMIT = 100 """Maximum limit for my feature.""" ``` ### Step 2: Add to BotConfig (if environment variable) If your config comes from environment variables, add it to `BotConfig`: ```python @dataclass class BotConfig: """Configuration container for the Discord ComfyUI bot.""" discord_bot_token: str comfy_server: str comfy_output_path: str comfy_history_limit: int workflow_file: Optional[str] = None my_feature_enabled: bool = False # ADD THIS my_feature_value: int = MY_FEATURE_DEFAULT_VALUE # ADD THIS ``` ### Step 3: Load in from_env() Add loading logic in `BotConfig.from_env()`: ```python @classmethod def from_env(cls) -> BotConfig: """Create a BotConfig instance by loading from environment.""" # ... existing code ... # Load your feature config my_feature_enabled = os.getenv("MY_FEATURE_ENABLED", "false").lower() == "true" try: my_feature_value = int(os.getenv("MY_FEATURE_VALUE", str(MY_FEATURE_DEFAULT_VALUE))) except ValueError: my_feature_value = MY_FEATURE_DEFAULT_VALUE return cls( # ... existing parameters ... my_feature_enabled=my_feature_enabled, my_feature_value=my_feature_value, ) ``` ### Step 4: Use in Your Commands Access config in your commands: ```python def setup_your_feature_commands(bot, config): @bot.command(name="feature") async def feature_command(ctx: commands.Context): if not config.my_feature_enabled: await ctx.reply("Feature is disabled!", mention_author=False) return value = config.my_feature_value await ctx.reply(f"Feature value: {value}", mention_author=False) ``` ### Step 5: Document the Environment Variable Update `CLAUDE.md` and add to `.env.example` (if you create one): ```bash # Feature Configuration MY_FEATURE_ENABLED=true MY_FEATURE_VALUE=42 ``` --- ## Working with Workflows The bot has separate concerns for workflows: - **WorkflowManager** (`workflow_manager.py`) - Template storage and node manipulation - **WorkflowStateManager** (`workflow_state.py`) - Runtime state (prompt, negative_prompt, input_image) - **ComfyClient** (`comfy_client.py`) - Uses both managers to generate images ### Adding New Workflow Node Types If you need to manipulate new types of nodes in workflows: #### Step 1: Add Method to WorkflowManager Edit `workflow_manager.py`: ```python def find_and_replace_my_node( self, workflow: Dict[str, Any], my_value: str ) -> Dict[str, Any]: """ Find and replace my custom node type. This searches for nodes of a specific class_type and updates their inputs. Parameters ---------- workflow : Dict[str, Any] The workflow definition to modify. my_value : str The value to inject. Returns ------- Dict[str, Any] The modified workflow. """ for node_id, node in workflow.items(): if node.get("class_type") == "MyCustomNodeType" and node.get("inputs"): # Check metadata for specific node identification meta = node.get("_meta", {}) if "My Custom Node" in meta.get("title", ""): workflow[node_id]["inputs"]["my_input"] = my_value logger.debug("Replaced my_value in node %s", node_id) return workflow ``` #### Step 2: Add to apply_state_changes() Update `apply_state_changes()` to include your new manipulation: ```python def apply_state_changes( self, workflow: Dict[str, Any], prompt: Optional[str] = None, negative_prompt: Optional[str] = None, input_image: Optional[str] = None, my_custom_value: Optional[str] = None, # ADD THIS randomize_seed: bool = True, ) -> Dict[str, Any]: """Apply multiple state changes to a workflow in one pass.""" if randomize_seed: workflow = self.find_and_replace_seed(workflow) if prompt is not None: workflow = self.find_and_replace_prompt(workflow, prompt) if negative_prompt is not None: workflow = self.find_and_replace_negative_prompt(workflow, negative_prompt) if input_image is not None: workflow = self.find_and_replace_input_image(workflow, input_image) # ADD THIS if my_custom_value is not None: workflow = self.find_and_replace_my_node(workflow, my_custom_value) return workflow ``` #### Step 3: Add State to WorkflowStateManager Edit `workflow_state.py` to track the new state: ```python def __init__(self, state_file: Optional[str] = None): """Initialize the workflow state manager.""" self._state: Dict[str, Any] = { "prompt": None, "negative_prompt": None, "input_image": None, "my_custom_value": None, # ADD THIS } # ... rest of init ... def set_my_custom_value(self, value: str) -> None: """Set the custom value.""" self._state["my_custom_value"] = value if self._state_file: try: self.save_to_file() except Exception: pass def get_my_custom_value(self) -> Optional[str]: """Get the custom value.""" return self._state.get("my_custom_value") ``` #### Step 4: Use in ComfyClient The ComfyClient will automatically use your new state if you update `generate_image_with_workflow()`: ```python async def generate_image_with_workflow(self) -> tuple[List[bytes], List[dict[str, Any]], str]: # ... existing code ... # Get current state changes changes = self.state_manager.get_changes() prompt = changes.get("prompt") negative_prompt = changes.get("negative_prompt") input_image = changes.get("input_image") my_custom_value = changes.get("my_custom_value") # ADD THIS # Apply changes using WorkflowManager workflow = self.workflow_manager.apply_state_changes( workflow, prompt=prompt, negative_prompt=negative_prompt, input_image=input_image, my_custom_value=my_custom_value, # ADD THIS randomize_seed=True, ) # ... rest of method ... ``` --- ## Best Practices ### Command Design 1. **Use descriptive names**: `ttr!generate` is better than `ttr!gen` (but provide aliases) 2. **Validate inputs early**: Check arguments before starting long operations 3. **Provide clear feedback**: Tell users what's happening and when it's done 4. **Handle errors gracefully**: Catch exceptions and show user-friendly messages 5. **Use decorators**: `@require_comfy_client` eliminates boilerplate ### Code Organization 1. **One responsibility per module**: Don't mix unrelated commands 2. **Keep functions small**: If a function is > 50 lines, consider splitting it 3. **Use type hints**: Help future developers understand your code 4. **Document with docstrings**: Explain what, why, and how ### Discord Best Practices 1. **Use `mention_author=False`**: Prevents spam from @mentions 2. **Use `delete_after=X`**: For temporary status messages 3. **Use `ephemeral=True`**: For interaction responses (buttons/modals) 4. **Limit file attachments**: Discord has a 4-file limit (use `MAX_IMAGES_PER_RESPONSE`) ### Performance 1. **Use job queue for long operations**: Queue generation requests 2. **Use typing indicator**: `async with ctx.typing():` shows bot is working 3. **Batch operations**: Don't send 10 separate messages when 1 will do 4. **Close resources**: Always close aiohttp sessions, file handles --- ## Common Patterns ### Pattern 1: Labeled Argument Parsing For commands with `key:value` syntax: ```python from discord_utils import parse_labeled_args from config import ARG_PROMPT_KEY, ARG_TYPE_KEY @bot.command(name="my-command") async def my_command(ctx: commands.Context, *, args: str = ""): # Parse labeled arguments parsed = parse_labeled_args(args, [ARG_PROMPT_KEY, ARG_TYPE_KEY]) prompt = parsed.get("prompt") # None if not provided image_type = parsed.get("type") or "input" # Default to "input" if not prompt: await ctx.reply("Please provide a prompt!", mention_author=False) return ``` ### Pattern 2: Queued Job Execution For long-running operations: ```python from job_queue import Job @bot.command(name="long-operation") @require_comfy_client async def long_operation(ctx: commands.Context, *, args: str = ""): try: # Define the job function async def _run_job(): async with ctx.typing(): result = await bot.comfy.some_long_operation(args) await ctx.reply(f"Done! Result: {result}", mention_author=False) # Submit to queue position = await bot.jobq.submit( Job( label=f"long-operation:{ctx.author.id}", run=_run_job, ) ) await ctx.reply( f"Queued ✅ (position: {position})", mention_author=False, delete_after=2.0, ) except Exception as exc: logger.exception("Failed to queue long operation") await ctx.reply( f"An error occurred: {type(exc).__name__}: {exc}", mention_author=False, ) ``` ### Pattern 3: File Attachments For uploading files to ComfyUI: ```python @bot.command(name="upload-and-process") @require_comfy_client async def upload_and_process(ctx: commands.Context): if not ctx.message.attachments: await ctx.reply("Please attach a file!", mention_author=False) return for attachment in ctx.message.attachments: try: # Download attachment data = await attachment.read() # Upload to ComfyUI result = await bot.comfy.upload_image( data, attachment.filename, image_type="input", ) # Process the uploaded file filename = result.get("name") await ctx.reply(f"Uploaded: {filename}", mention_author=False) except Exception as exc: logger.exception("Failed to process attachment") await ctx.reply( f"Failed: {attachment.filename}: {exc}", mention_author=False, ) ``` ### Pattern 4: Interactive UI (Buttons) For adding buttons to messages: ```python from discord.ui import View, Button import discord class MyView(View): def __init__(self, data: str): super().__init__(timeout=None) self.data = data @discord.ui.button(label="Click Me", style=discord.ButtonStyle.primary) async def button_callback( self, interaction: discord.Interaction, button: discord.ui.Button ): # Handle button click await interaction.response.send_message( f"You clicked! Data: {self.data}", ephemeral=True, ) @bot.command(name="interactive") async def interactive(ctx: commands.Context): view = MyView(data="example") await ctx.reply("Click the button:", view=view, mention_author=False) ``` ### Pattern 5: Using Configuration Access bot configuration in commands: ```python def setup_my_commands(bot, config): @bot.command(name="check-config") async def check_config(ctx: commands.Context): # Access config values server = config.comfy_server output_path = config.comfy_output_path await ctx.reply( f"Server: {server}\nOutput: {output_path}", mention_author=False, ) ``` --- ## Testing Your Changes ### Manual Testing Checklist 1. **Start the bot**: `python bot.py` - Verify no import errors - Check configuration loads correctly - Confirm all commands register 2. **Test basic functionality**: ``` ttr!test ttr!help ttr!your-new-command ``` 3. **Test error handling**: - Run command with missing arguments - Run command with invalid arguments - Test when ComfyUI is unavailable (if applicable) 4. **Test edge cases**: - Very long inputs - Special characters - Concurrent command execution - Commands while queue is full ### Syntax Validation Check for syntax errors without running: ```bash python -m py_compile bot.py python -m py_compile commands/your_feature.py ``` ### Check Imports Verify all imports work: ```bash python -c "from commands.your_feature import setup_your_feature_commands; print('OK')" ``` ### Code Style Follow these conventions: - **Indentation**: 4 spaces (no tabs) - **Line length**: Max 100 characters (documentation can be longer) - **Docstrings**: Use Google style or NumPy style (match existing code) - **Imports**: Group stdlib, third-party, local (separated by blank lines) - **Type hints**: Use modern syntax (`dict[str, Any]` not `Dict[str, Any]`) --- ## Example: Complete Feature Addition Here's a complete example adding a "status" command: ### 1. Create commands/status.py ```python """ commands/status.py ================== Bot status and diagnostics commands. """ from __future__ import annotations import logging import asyncio from discord.ext import commands from discord_utils import require_comfy_client logger = logging.getLogger(__name__) def setup_status_commands(bot, config): """Register status commands with the bot.""" @bot.command(name="status", aliases=["s", "stat"]) async def status_command(ctx: commands.Context) -> None: """ Show bot status and queue information. Usage: ttr!status Displays: - Bot connection status - ComfyUI connection status - Current queue size - Configuration info """ # Check bot status latency_ms = round(bot.latency * 1000) # Check queue if hasattr(bot, "jobq"): queue_size = await bot.jobq.get_queue_size() else: queue_size = 0 # Check ComfyUI comfy_status = "✅ Connected" if hasattr(bot, "comfy") else "❌ Not configured" # Build status message status_msg = [ "**Bot Status**", f"• Latency: {latency_ms}ms", f"• Queue size: {queue_size}", f"• ComfyUI: {comfy_status}", f"• Server: {config.comfy_server}", ] await ctx.reply("\n".join(status_msg), mention_author=False) @bot.command(name="ping") async def ping_command(ctx: commands.Context) -> None: """ Check bot responsiveness. Usage: ttr!ping """ latency_ms = round(bot.latency * 1000) await ctx.reply(f"🏓 Pong! Latency: {latency_ms}ms", mention_author=False) ``` ### 2. Register in commands/__init__.py ```python from .status import setup_status_commands def register_all_commands(bot, config): # ... existing registrations ... setup_status_commands(bot, config) ``` ### 3. Update CLAUDE.md ```markdown - **commands/status.py** - Bot status and diagnostics commands ``` ### 4. Test ```bash python bot.py ``` In Discord: ``` ttr!status ttr!ping ttr!s (alias test) ``` --- ## Getting Help If you're stuck: 1. **Check CLAUDE.md**: Architecture and patterns documented there 2. **Read existing commands**: See how similar features are implemented 3. **Check logs**: Run bot and check console output for errors 4. **Test incrementally**: Add small pieces and test frequently Remember: The refactored architecture makes adding features straightforward. Follow the patterns, and your code will fit right in! 🚀