diff --git a/gitea/coder.py b/gitea/coder.py index 1f3d4b1..c7d3edb 100644 --- a/gitea/coder.py +++ b/gitea/coder.py @@ -1,21 +1,24 @@ """ -title: Gitea Coder - Workflow Role with Scope Enforcement +title: Gitea Coder - Workflow Role with Automatic Branch Management author: Jeff Smith + minimax version: 1.0.0 license: MIT -description: High-level workflow role for LLM-based code generation with scope gating and quality gates +description: High-level workflow role for LLM-based code generation with automatic branch management and quality gates requirements: pydantic, httpx changelog: 1.0.0: - Initial implementation of gitea_coder role + - Automatic branch management (system derives from chat_id) + - LLM no longer manages branch names - Branch creation with scope gating (prevents main pushes) - Enforces branch naming conventions (feature/, fix/, refactor/, etc.) - Generates detailed commit messages with ticket references - Creates PRs from branches - Reads ticket requirements from issues - Unified file operations workflow - - Added diff-based updates with apply_diff() - - Added size delta gating in commit_changes() for quality control + - Diff-based updates with apply_diff() + - Size delta gating in commit_changes() for quality control + - Complete CRUD operations: create_file, replace_file, delete_file, rename_file """ from typing import Optional, Callable, Any, Dict, List, Tuple @@ -31,16 +34,24 @@ class Tools: """ Gitea Coder Role - High-level workflow automation for code generation tasks. - This role implements the coder workflow: - reads ticket → understands issue → creates/modifies branch → commits with detailed messages + CRITICAL ARCHITECTURAL CHANGE (v1.0.0): + - LLM no longer manages branch names + - System automatically derives branch from chat_id session + - All operations use __chat_id__ parameter for branch detection + - create_feature_branch() generates branch name, then caches for session + - LLM focuses on code/content, not infrastructure + + Workflow: + reads ticket → creates/modifies branch (system-managed) → commits with detailed messages Key Features: + - Automatic branch management (chat_id → working_branch mapping) - Branch scope gating (prevents main/master pushes) - Enforces branch naming conventions - Auto-generates conventional commit messages - Quality gates for file changes (size delta validation) - Diff-based updates to prevent accidental file replacements - - Session caching for chat_id → default_branch mapping + - Complete CRUD operations (create, replace, delete, rename) """ class Valves(BaseModel): @@ -123,8 +134,8 @@ class Tools: # Handle user valves configuration self.user_valves = self.UserValves() - # Session cache: chat_id → default_branch (with TTL) - self._session_cache: Dict[str, Tuple[str, float]] = {} + # Session cache: chat_id → {working_branch, default_branch} (with TTL) + self._session_cache: Dict[str, Tuple[Dict[str, Any], float]] = {} self._cache_ttl_seconds = 3600 # 1 hour # Initialize underlying dev operations (for actual API calls) @@ -227,11 +238,14 @@ class Tools: def _get_cached_data(self, chat_id: str, key: str) -> Optional[Any]: """Get cached data for chat session with TTL""" - cache_key = f"{chat_id}:{key}" + if not chat_id: + return None + + cache_key = f"{chat_id}" if cache_key in self._session_cache: data, timestamp = self._session_cache[cache_key] if time.time() - timestamp < self._cache_ttl_seconds: - return data + return data.get(key) else: # Expired, remove from cache del self._session_cache[cache_key] @@ -239,8 +253,68 @@ class Tools: def _set_cached_data(self, chat_id: str, key: str, data: Any) -> None: """Set cached data for chat session with TTL""" - cache_key = f"{chat_id}:{key}" - self._session_cache[cache_key] = (data, time.time()) + if not chat_id: + return + + cache_key = f"{chat_id}" + if cache_key not in self._session_cache: + self._session_cache[cache_key] = ({}, time.time()) + self._session_cache[cache_key][0][key] = data + self._session_cache[cache_key] = (self._session_cache[cache_key][0], time.time()) + + def _get_working_branch( + self, __chat_id__: str = None, __user__: dict = None + ) -> str: + """ + Get the working branch for the current session. + + CRITICAL: This is the primary method for branch detection. + - LLM should NOT pass branch names to functions + - System automatically derives branch from chat_id + - Falls back to default branch if no session cached + + Args: + __chat_id__: Session ID for cache lookup + __user__: User context + + Returns: + Working branch name (system-managed) + """ + # Try to get from session cache first + if __chat_id__: + working_branch = self._get_cached_data(__chat_id__, "working_branch") + if working_branch: + return working_branch + + # Fall back to default branch + return self._get_branch(None, __user__) + + def _generate_branch_name( + self, issue_number: int, title: str, scope: str = "feature" + ) -> str: + """ + System-managed branch name generation. + + LLM should NOT call this directly - called by create_feature_branch() + + Args: + issue_number: The issue/ticket number + title: Issue title (for description) + scope: Branch scope prefix + + Returns: + Branch name in format: scope/issue-id-short-description + """ + # Clean up title for branch name + slug = re.sub(r"[^a-z0-9\s-]", "", title.lower()) + slug = re.sub(r"[\s-]+", "-", slug) + slug = slug.strip("-") + + # Truncate and add issue number + if len(slug) > 30: + slug = slug[:30].strip("-") + + return f"{scope}/{issue_number}-{slug}" def _validate_branch_name(self, branch_name: str) -> tuple[bool, str]: """ @@ -254,7 +328,7 @@ class Tools: return False, ( f"Branch '{branch_name}' is protected. " f"Direct commits to protected branches are not allowed. " - f"Create a feature branch instead." + f"Create a feature branch first using create_feature_branch()." ) # Check if it starts with an allowed scope @@ -333,22 +407,38 @@ class Tools: async def workflow_summary( self, __user__: dict = None, + __chat_id__: str = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Get a summary of available coder workflows and commands. + CRITICAL: LLM does NOT manage branch names. System handles it automatically. + Returns: Markdown-formatted workflow guide """ - output = """# 🚀 Gitea Coder Workflow Guide + # Get current working branch + working_branch = self._get_working_branch(__chat_id__, __user__) + + output = f"""# 🚀 Gitea Coder Workflow Guide (v1.0.0) + +## ⚠️ CRITICAL ARCHITECTURAL CHANGE + +**LLM no longer manages branch names!** +- System automatically derives branch from chat_id session +- LLM should NOT pass branch names to functions +- Use __chat_id__ parameter for automatic branch detection ## Quick Start 1. **Read the ticket:** `read_ticket(issue_number)` -2. **Create feature branch:** `create_feature_branch(issue_number)` -3. **Make changes:** `apply_diff()` or `commit_changes()` -4. **Create PR:** `create_pull_request()` +2. **Create feature branch:** `create_feature_branch(issue_number, title)` + - System generates and caches branch name +3. **Make changes:** `apply_diff()` or `commit_changes()` + - Branch automatically detected from chat_id +4. **Create PR:** `create_pull_request(title)` + - Uses cached working branch ## Available Commands @@ -356,15 +446,19 @@ class Tools: - `read_ticket(issue_number)` - Get full issue details ### 🌿 Branch Management -- `create_feature_branch(issue_number, title)` - Create scoped branch -- `get_branch_status()` - See current working branch -- `list_my_branches()` - List your branches +- `create_feature_branch(issue_number, title)` - System creates & caches branch +- `get_branch_status()` - See current working branch (from cache) +- `list_my_branches()` - List all branches in repository -### 📝 File Operations +### 📝 File Operations (NO branch parameter needed!) - `apply_diff(path, diff, message)` - Apply unified diff patch - `commit_changes(path, content, message)` - Commit with size delta gate - `replace_file(path, content, message)` - Replace entire file - `create_file(path, content, message)` - Create new file +- `delete_file(path, message)` - Delete a file ⭐ NEW +- `rename_file(old_path, new_path, message)` - Rename a file ⭐ NEW +- `get_file(path)` - Read file content +- `list_files(path)` - List directory contents ### 🔍 Quality Gates - Size delta checks (default: 50% max change) @@ -374,7 +468,31 @@ class Tools: ### 📦 Pull Requests - `create_pull_request(title, description)` - Create PR from current branch -## Branch Naming Convention +## How Branch Management Works + +``` +User creates chat with chat_id = "abc123" + +1. create_feature_branch(42, "Add login") + ↓ + System generates: "feature/42-add-login" + System creates branch + System caches: chat_id "abc123" → "feature/42-add-login" + +2. apply_diff(path="auth.py", diff="...") + ↓ + System checks cache: "abc123" → "feature/42-add-login" + System automatically uses that branch + +3. commit_changes(path="config.py", content="...") + ↓ + System checks cache: "abc123" → "feature/42-add-login" + System automatically uses that branch +``` + +**LLM never needs to know or manage branch names!** + +## Branch Naming Convention (System-Generated) ``` /- @@ -405,10 +523,12 @@ Allowed scopes: `feature/`, `fix/`, `refactor/`, `docs/`, `test/`, `chore/`, `wi # Read the ticket ticket = read_ticket(42) -# Create branch (auto-extracts from ticket) +# Create branch (system generates & caches name) create_feature_branch(42, ticket["title"]) +# System creates: feature/42-add-login +# System caches: chat_id → feature/42-add-login -# Make changes using diff +# Make changes (NO branch parameter - auto-detected from chat_id) apply_diff( path="src/auth.py", diff=\"\"\"--- a/src/auth.py @@ -419,19 +539,26 @@ apply_diff( \"\"\", message="feat(auth): add login method to Auth class" ) +# System uses cached branch: feature/42-add-login -# Create PR +# Create PR (auto-detects current branch) create_pull_request( title="feat(auth): add login method", body="Implements login functionality as specified in #42" ) +# System uses cached branch: feature/42-add-login ``` +## Current Session Status + +**Working Branch:** `{working_branch}` +**Session Cached:** {f"Yes - chat_id: {__chat_id__}" if __chat_id__ and self._get_cached_data(__chat_id__, "working_branch") else "No - create a feature branch first"} + ## Tips -- Use `suggest_branch_name(issue_number)` to get branch name suggestions -- Use `list_my_branches()` to track your active work -- Always reference issues in commits: `Refs: #42` +- Use `get_branch_status()` to verify current working branch +- Always use `__chat_id__` parameter in function calls +- LLM should focus on code/content, not infrastructure - Use diff-based updates for incremental changes - Large changes should be split into multiple commits """ @@ -593,14 +720,16 @@ create_pull_request( output += "\n" # Suggested branch name - suggested = self._suggest_branch_name(issue_number, title) + suggested = self._generate_branch_name(issue_number, title) output += "## 🌿 Suggested Branch Name\n\n" output += f"```\n{suggested}\n```\n\n" # Next steps output += "## 🚀 Next Steps\n\n" output += f"1. Create branch: `create_feature_branch({issue_number}, \"{title[:50]}...\")`\n" + output += " - System will generate & cache branch name\n" output += "2. Make changes using `apply_diff()` or `commit_changes()`\n" + output += " - Branch automatically detected from chat_id\n" output += "3. Create PR: `create_pull_request()`\n" return output.strip() @@ -613,94 +742,6 @@ create_pull_request( except Exception as e: return f"Error: Unexpected failure during issue fetch: {type(e).__name__}: {e}" - def _suggest_branch_name( - self, issue_number: int, title: str, scope: str = "feature" - ) -> str: - """ - Suggest a branch name based on issue number and title. - - Args: - issue_number: The issue number - title: The issue title - scope: Branch scope prefix (default: feature) - - Returns: - Suggested branch name in format: scope/issue-id-short-description - """ - # Clean up title for branch name - # Remove special characters, lowercase, replace spaces with hyphens - slug = re.sub(r"[^a-z0-9\s-]", "", title.lower()) - slug = re.sub(r"[\s-]+", "-", slug) - slug = slug.strip("-") - - # Truncate and add issue number - if len(slug) > 30: - slug = slug[:30].strip("-") - - return f"{scope}/{issue_number}-{slug}" - - async def suggest_branch_name( - self, - issue_number: int, - repo: Optional[str] = None, - scope: str = "feature", - __user__: dict = None, - __event_emitter__: Callable[[dict], Any] = None, - ) -> str: - """ - Get branch name suggestions based on issue number. - - Args: - issue_number: The issue number - repo: Repository in 'owner/repo' format - scope: Branch scope prefix - __user__: User context - - Returns: - Suggested branch name - """ - token = self._get_token(__user__) - if not token: - return "Error: GITEA_TOKEN not configured." - - try: - owner, repo_name = self._resolve_repo(repo, __user__) - except ValueError as e: - return f"Error: {e}" - - # Fetch issue title - try: - async with httpx.AsyncClient( - timeout=30.0, verify=self.valves.VERIFY_SSL - ) as client: - response = await client.get( - self._api_url(f"/repos/{owner}/{repo_name}/issues/{issue_number}"), - headers=self._headers(__user__), - ) - response.raise_for_status() - issue = response.json() - title = issue.get("title", "") - except Exception: - title = "" - - suggested = self._suggest_branch_name(issue_number, title, scope) - - # Check if branch exists - try: - async with httpx.AsyncClient( - timeout=30.0, verify=self.valves.VERIFY_SSL - ) as client: - branch_response = await client.get( - self._api_url(f"/repos/{owner}/{repo_name}/branches/{suggested}"), - headers=self._headers(__user__), - ) - if branch_response.status_code == 200: - suggested += " (already exists)" - except Exception: - pass - - return suggested - async def create_feature_branch( self, issue_number: int, @@ -714,23 +755,39 @@ create_pull_request( """ Create a new feature branch for a ticket. - This is the main entry point for the coder workflow. It: - 1. Validates the branch name against allowed scopes - 2. Prevents commits to protected branches - 3. Creates the branch from the default/base branch - 4. Caches the branch name for the session + CRITICAL: This is where branch names are generated and cached. + After this call, ALL operations use the cached branch automatically. + + This method: + 1. Generates branch name from issue_number + title (system-managed) + 2. Validates against allowed scopes + 3. Prevents commits to protected branches + 4. Creates the branch from the default/base branch + 5. Caches the branch name for the session (chat_id → working_branch) Args: issue_number: The ticket/issue number title: Optional title (will fetch from issue if not provided) repo: Repository in 'owner/repo' format scope: Branch scope (feature, fix, refactor, etc.) - __chat_id__: Session ID for caching + __chat_id__: Session ID for caching (REQUIRED for branch operations) __user__: User context Returns: - Branch creation confirmation + Branch creation confirmation with cached branch info """ + # Validate that chat_id is provided + if not __chat_id__: + return """❌ **Chat ID Required** + +To create a feature branch, you must provide the __chat_id__ parameter. +This is required for the system to: +1. Cache the branch name for the session +2. Automatically detect the branch for all future operations + +Example: create_feature_branch(42, title, __chat_id__="session123") +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -758,8 +815,8 @@ create_pull_request( except Exception as e: return f"Error: Could not fetch issue #{issue_number}: {e}" - # Generate branch name - branch_name = self._suggest_branch_name(issue_number, title, scope) + # Generate branch name (SYSTEM-MANAGED) + branch_name = self._generate_branch_name(issue_number, title, scope) # Validate branch name is_valid, error_msg = self._validate_branch_name(branch_name) @@ -779,12 +836,11 @@ create_pull_request( # Get base branch (with caching) base_branch = self._get_branch(None, __user__) - if __chat_id__: - cached_base = self._get_cached_data(__chat_id__, "default_branch") - if cached_base: - base_branch = cached_base - else: - self._set_cached_data(__chat_id__, "default_branch", base_branch) + cached_base = self._get_cached_data(__chat_id__, "default_branch") + if cached_base: + base_branch = cached_base + else: + self._set_cached_data(__chat_id__, "default_branch", base_branch) try: async with httpx.AsyncClient( @@ -801,7 +857,9 @@ create_pull_request( # Handle branch already exists if response.status_code == 409: - return f"⚠️ **Branch Already Exists**\n\nBranch `{branch_name}` already exists in `{owner}/{repo_name}`.\n\nUse it or create a new one." + # Still cache it even if it exists + self._set_cached_data(__chat_id__, "working_branch", branch_name) + return f"⚠️ **Branch Already Exists (Cached)**\n\nBranch `{branch_name}` already exists in `{owner}/{repo_name}`.\n\n**Branch has been cached for this session.**\nAll future operations will automatically use this branch.\n\nNext steps:\n1. Make changes using `apply_diff()` or `commit_changes()`\n2. Commit with descriptive messages\n3. Create PR when ready: `create_pull_request()`\n" response.raise_for_status() @@ -813,22 +871,29 @@ create_pull_request( } ) - # Cache the branch for session - if __chat_id__: - self._set_cached_data(__chat_id__, "working_branch", branch_name) + # Cache the branch for session (CRITICAL!) + self._set_cached_data(__chat_id__, "working_branch", branch_name) - return f"""✅ **Feature Branch Created Successfully** + return f"""✅ **Feature Branch Created & Cached Successfully** **Branch:** `{branch_name}` **Base Branch:** `{base_branch}` **Repository:** `{owner}/{repo_name}` **Issue:** #{issue_number} +**Session Cached:** ✅ +- chat_id: `{__chat_id__}` +- working_branch: `{branch_name}` + +**What This Means:** +✅ All future operations will automatically use this branch +✅ LLM does NOT need to manage branch names +✅ Just make changes - system handles the rest + **Next Steps:** -1. Make changes to files -2. Use `apply_diff()` for incremental changes or `commit_changes()` for full replacements -3. Commit with descriptive messages -4. Create PR when ready: `create_pull_request()` +1. Make changes to files using `apply_diff()` or `commit_changes()` +2. Branch automatically detected from chat_id: `{__chat_id__}` +3. Create PR when ready: `create_pull_request()` **Branch Naming Convention:** - Format: `/-` @@ -842,7 +907,9 @@ create_pull_request( except httpx.HTTPStatusError as e: error_msg = self._format_error(e, "branch creation") if e.response.status_code == 409: - return f"Error: Branch `{branch_name}` already exists." + # Cache it even if exists + self._set_cached_data(__chat_id__, "working_branch", branch_name) + return f"⚠️ **Branch Already Exists (Cached)**\n\nBranch `{branch_name}` already exists.\n\n**Cached for session.** All operations will use this branch." return f"Error: Failed to create branch. {error_msg}" except Exception as e: return f"Error: Unexpected failure during branch creation: {type(e).__name__}: {e}" @@ -857,9 +924,11 @@ create_pull_request( """ Get the current working branch status for the session. + CRITICAL: Shows the system-cached working branch. + Args: repo: Repository in 'owner/repo' format - __chat_id__: Session ID for cache lookup + __chat_id__: Session ID for cache lookup (REQUIRED) __user__: User context Returns: @@ -874,7 +943,7 @@ create_pull_request( except ValueError as e: return f"Error: {e}" - # Check for cached working branch + # Get cached branches working_branch = None default_branch = None if __chat_id__: @@ -887,12 +956,42 @@ create_pull_request( output = f"# 🌿 Branch Status: {owner}/{repo_name}\n\n" output += f"**Default Branch:** `{default_branch}`\n" - if working_branch: - output += f"**Working Branch:** `{working_branch}`\n\n" - output += "**Session cached - use this branch for commits.**\n" + if __chat_id__: + output += f"**Session ID:** `{__chat_id__}`\n\n" else: - output += "\n**No working branch set for this session.**\n" - output += "Create one: `create_feature_branch(issue_number, title)`\n" + output += "\n" + + if working_branch: + output += f"## ✅ Working Branch Cached\n\n" + output += f"**Branch:** `{working_branch}`\n\n" + output += "**All file operations will automatically use this branch.**\n" + output += f"LLM should NOT pass branch names to functions.\n\n" + + # Try to verify branch exists + try: + async with httpx.AsyncClient( + timeout=30.0, verify=self.valves.VERIFY_SSL + ) as client: + branch_response = await client.get( + self._api_url(f"/repos/{owner}/{repo_name}/branches/{working_branch}"), + headers=self._headers(__user__), + ) + if branch_response.status_code == 200: + output += "**Status:** ✅ Branch exists and is ready for commits\n" + else: + output += "**Status:** ⚠️ Branch may have been deleted\n" + except Exception: + output += "**Status:** Unable to verify (check repository)\n" + + else: + output += "## ❌ No Working Branch\n\n" + output += "**No branch cached for this session.**\n\n" + output += "To create a branch:\n" + output += f"```python\ncreate_feature_branch(\n issue_number=42,\n title=\"Add login feature\",\n __chat_id__=\"{__chat_id__ or 'your_chat_id'}\"\n)\n```\n\n" + output += "**This will:**\n" + output += "1. Generate a branch name (system-managed)\n" + output += "2. Create the branch\n" + output += "3. Cache it for all future operations\n" return output @@ -1006,7 +1105,6 @@ create_pull_request( diff_content: str, message: Optional[str] = None, repo: Optional[str] = None, - branch: Optional[str] = None, auto_message: bool = True, __user__: dict = None, __chat_id__: str = None, @@ -1015,6 +1113,8 @@ create_pull_request( """ Apply a unified diff patch to a file. + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + This is the PREFERRED method for making changes as it: 1. Is precise about what changes 2. Prevents accidental file replacements @@ -1025,14 +1125,30 @@ create_pull_request( diff_content: Unified diff in standard format message: Commit message (auto-generated if not provided) repo: Repository in 'owner/repo' format - branch: Branch name (defaults to working branch or default) auto_message: Generate commit message if not provided __user__: User context - __chat_id__: Session ID for branch caching + __chat_id__: Session ID for branch detection (REQUIRED) + __event_emitter__: Event emitter callback Returns: Commit details and diff summary """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first: +```python +create_feature_branch( + issue_number=42, + title="Feature title", + __chat_id__="your_chat_id" +) +``` +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -1042,12 +1158,6 @@ create_pull_request( except ValueError as e: return f"Error: {e}" - effective_branch = branch - if not effective_branch and __chat_id__: - effective_branch = self._get_cached_data(__chat_id__, "working_branch") - if not effective_branch: - effective_branch = self._get_branch(None, __user__) - if __event_emitter__: await __event_emitter__( { @@ -1134,7 +1244,7 @@ create_pull_request( output = f"✅ **Diff Applied Successfully**\n\n" output += f"**File:** `{path}`\n" - output += f"**Branch:** `{effective_branch}`\n" + output += f"**Branch:** `{effective_branch}` (auto-detected from chat_id)\n" output += f"**Commit:** `{commit_sha}`\n" output += f"**Changes:** +{added_lines} lines, -{removed_lines} lines\n\n" output += f"**Message:** {message}\n" @@ -1363,7 +1473,6 @@ create_pull_request( content: str, message: Optional[str] = None, repo: Optional[str] = None, - branch: Optional[str] = None, max_delta_percent: Optional[float] = None, __user__: dict = None, __chat_id__: str = None, @@ -1372,26 +1481,43 @@ create_pull_request( """ Commit file changes with automatic content detection and size delta gating. + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + This method: 1. Detects whether to create or replace a file 2. Validates file size changes against threshold (quality gate) 3. Auto-generates commit message if not provided - 4. Commits to the appropriate branch + 4. Commits to the system-cached working branch Args: path: File path to create or update content: New file content message: Commit message (auto-generated if not provided) repo: Repository in 'owner/repo' format - branch: Branch name (defaults to working branch or default) max_delta_percent: Override for size delta threshold (quality gate) __user__: User context - __chat_id__: Session ID for branch caching + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress Returns: Commit details or error with guidance """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first: +```python +create_feature_branch( + issue_number=42, + title="Feature title", + __chat_id__="your_chat_id" +) +``` +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -1401,12 +1527,6 @@ create_pull_request( except ValueError as e: return f"Error: {e}" - effective_branch = branch - if not effective_branch and __chat_id__: - effective_branch = self._get_cached_data(__chat_id__, "working_branch") - if not effective_branch: - effective_branch = self._get_branch(None, __user__) - # Use provided threshold or default from valves delta_threshold = max_delta_percent or self.valves.MAX_SIZE_DELTA_PERCENT @@ -1490,20 +1610,21 @@ This change exceeds the size delta threshold, which may indicate: +new line to add -line to remove \"\"\", - message="feat(scope): description of changes" + message="feat(scope): description of changes", + __chat_id__="your_chat_id" ) ``` 2. **Fetch and review current content**: ```python - current = get_file("{path}") + current = get_file("{path}", __chat_id__="your_chat_id") # Compare with what you want to change ``` 3. **Commit with override** (not recommended): Increase the threshold if this is intentional: ```python - commit_changes(..., max_delta_percent=100) + commit_changes(..., max_delta_percent=100, __chat_id__="your_chat_id") ``` **Why this gate exists:** @@ -1573,7 +1694,7 @@ Large file replacements by LLMs often indicate the model didn't properly underst output = f"""✅ **{action} File Successfully** **File:** `{path}` -**Branch:** `{effective_branch}` +**Branch:** `{effective_branch}` (auto-detected from chat_id) **Commit:** `{commit_sha}` **Message:** {message} @@ -1602,17 +1723,35 @@ Large file replacements by LLMs often indicate the model didn't properly underst """ Create a pull request from the current branch. + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + Args: title: PR title body: PR description (auto-populates from issue if linked) repo: Repository in 'owner/repo' format __user__: User context - __chat_id__: Session ID for branch caching + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress Returns: PR creation confirmation with details """ + # Get working branch from session cache (SYSTEM-MANAGED) + head_branch = self._get_working_branch(__chat_id__, __user__) + + if not head_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first: +```python +create_feature_branch( + issue_number=42, + title="Feature title", + __chat_id__="your_chat_id" +) +``` +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured." @@ -1622,15 +1761,6 @@ Large file replacements by LLMs often indicate the model didn't properly underst except ValueError as e: return f"Error: {e}" - # Get current branch - head_branch = None - if __chat_id__: - head_branch = self._get_cached_data(__chat_id__, "working_branch") - - if not head_branch: - # Try to guess from recent commits or use default - head_branch = self._get_branch(None, __user__) - base_branch = self._get_branch(None, __user__) # Validate that head branch is not a protected branch @@ -1692,7 +1822,7 @@ Large file replacements by LLMs often indicate the model didn't properly underst return f"""✅ **Pull Request Created Successfully** **PR #{pr_number}:** {title} -**Branch:** `{head_branch}` → `{base_branch}` +**Branch:** `{head_branch}` → `{base_branch}` (auto-detected from chat_id) **URL:** {pr_url} **Description:** @@ -1723,13 +1853,15 @@ get_pull_request({pr_number}) content: str, message: str, repo: Optional[str] = None, - branch: Optional[str] = None, __user__: dict = None, + __chat_id__: str = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Update an existing file in the repository (creates commit). + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + WARNING: This replaces the entire file content. For incremental changes, use `apply_diff()` instead to prevent accidental data loss. @@ -1738,13 +1870,22 @@ get_pull_request({pr_number}) content: New file content as string message: Commit message repo: Repository in 'owner/repo' format - branch: Branch name (defaults to repository default) __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress Returns: Commit details and success confirmation """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first. +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -1754,8 +1895,6 @@ get_pull_request({pr_number}) except ValueError as e: return f"Error: {e}" - effective_branch = self._get_branch(branch, __user__) - if __event_emitter__: await __event_emitter__( { @@ -1814,7 +1953,7 @@ get_pull_request({pr_number}) output = f"**File Updated Successfully**\n\n" output += f"**File:** `{owner}/{repo_name}/{path}`\n" - output += f"**Branch:** `{effective_branch}`\n" + output += f"**Branch:** `{effective_branch}` (auto-detected from chat_id)\n" output += f"**Commit:** `{commit_sha}`\n" output += f"**Message:** {message}\n\n" output += "_Use `apply_diff()` for incremental changes to prevent data loss._\n" @@ -1835,13 +1974,15 @@ get_pull_request({pr_number}) content: str, message: str, repo: Optional[str] = None, - branch: Optional[str] = None, __user__: dict = None, + __chat_id__: str = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ Create a new file in the repository. + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + For adding content to existing files, use `apply_diff()` instead. Args: @@ -1849,13 +1990,22 @@ get_pull_request({pr_number}) content: Initial file content as string message: Commit message repo: Repository in 'owner/repo' format - branch: Branch name (defaults to repository default) __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress Returns: Commit details and success confirmation """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first. +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -1865,8 +2015,6 @@ get_pull_request({pr_number}) except ValueError as e: return f"Error: {e}" - effective_branch = self._get_branch(branch, __user__) - if __event_emitter__: await __event_emitter__( { @@ -1905,7 +2053,7 @@ get_pull_request({pr_number}) output = f"**File Created Successfully**\n\n" output += f"**File:** `{owner}/{repo_name}/{path}`\n" - output += f"**Branch:** `{effective_branch}`\n" + output += f"**Branch:** `{effective_branch}` (auto-detected from chat_id)\n" output += f"**Commit:** `{commit_sha}`\n" output += f"**Message:** {message}\n" @@ -1919,27 +2067,42 @@ get_pull_request({pr_number}) except Exception as e: return f"Error: Unexpected failure during file creation: {type(e).__name__}: {e}" - async def get_file( + async def delete_file( self, path: str, + message: str, repo: Optional[str] = None, - branch: Optional[str] = None, __user__: dict = None, + __chat_id__: str = None, __event_emitter__: Callable[[dict], Any] = None, + __event_call__: Callable[[dict], Any] = None, ) -> str: """ - Get the contents of a file from the repository. + Delete a file from the repository. + + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. Args: - path: Full path to the file (e.g., 'src/main.py') + path: File path to delete + message: Commit message for the deletion repo: Repository in 'owner/repo' format - branch: Branch name (defaults to repository default) __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress + __event_call__: Confirmation callback (for user confirmation) Returns: - File content with metadata (SHA, size, branch) + Confirmation with commit details """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first. +""" + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -1949,7 +2112,277 @@ get_pull_request({pr_number}) except ValueError as e: return f"Error: {e}" - effective_branch = self._get_branch(branch, __user__) + # Confirmation dialog + if __event_call__: + result = await __event_call__( + { + "type": "confirmation", + "data": { + "title": "Confirm File Deletion", + "message": f"Delete `{path}` from `{owner}/{repo_name}` ({effective_branch})?", + }, + } + ) + if result is None or result is False: + return "⚠️ File deletion cancelled by user." + if isinstance(result, dict) and not result.get("confirmed"): + return "⚠️ File deletion cancelled by user." + + if __event_emitter__: + await __event_emitter__( + { + "type": "status", + "data": {"description": f"Deleting {path}...", "done": False}, + } + ) + + try: + async with httpx.AsyncClient( + timeout=30.0, verify=self.valves.VERIFY_SSL + ) as client: + # Get file SHA + get_response = await client.get( + self._api_url(f"/repos/{owner}/{repo_name}/contents/{path}"), + headers=self._headers(__user__), + params={"ref": effective_branch}, + ) + + if get_response.status_code == 404: + return f"Error: File not found: `{path}`" + + get_response.raise_for_status() + file_info = get_response.json() + sha = file_info.get("sha") + + if not sha: + return "Error: Could not retrieve file SHA for deletion." + + # Delete file + response = await client.delete( + self._api_url(f"/repos/{owner}/{repo_name}/contents/{path}"), + headers=self._headers(__user__), + json={ + "message": message, + "branch": effective_branch, + "sha": sha, + }, + ) + response.raise_for_status() + result = response.json() + + commit_sha = result.get("commit", {}).get("sha", "unknown")[:8] + + if __event_emitter__: + await __event_emitter__( + { + "type": "status", + "data": {"description": "Done", "done": True, "hidden": True}, + } + ) + + output = f"**File Deleted Successfully**\n\n" + output += f"**File:** `{owner}/{repo_name}/{path}`\n" + output += f"**Branch:** `{effective_branch}` (auto-detected from chat_id)\n" + output += f"**Commit:** `{commit_sha}`\n" + output += f"**Message:** {message}\n" + + return output + + except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"file deletion for '{path}'") + if e.response.status_code == 404: + return f"Error: File not found: `{path}`" + return f"Error: Failed to delete file. {error_msg}" + except Exception as e: + return f"Error: Unexpected failure during file deletion: {type(e).__name__}: {e}" + + async def rename_file( + self, + old_path: str, + new_path: str, + message: Optional[str] = None, + repo: Optional[str] = None, + __user__: dict = None, + __chat_id__: str = None, + __event_emitter__: Callable[[dict], Any] = None, + ) -> str: + """ + Rename a file in the repository. + + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + + This is done by: + 1. Getting the content of the old file + 2. Creating a new file with the new name + 3. Deleting the old file + + Args: + old_path: Current file path + new_path: New file path + message: Commit message (auto-generated if not provided) + repo: Repository in 'owner/repo' format + __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) + __event_emitter__: Event emitter for progress + + Returns: + Confirmation with commit details + """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + return """❌ **No Working Branch** + +No branch cached for this session. Create a feature branch first. +""" + + token = self._get_token(__user__) + if not token: + return "Error: GITEA_TOKEN not configured in UserValves settings." + + try: + owner, repo_name = self._resolve_repo(repo, __user__) + except ValueError as e: + return f"Error: {e}" + + if __event_emitter__: + await __event_emitter__( + { + "type": "status", + "data": { + "description": f"Renaming {old_path} to {new_path}...", + "done": False, + }, + } + ) + + try: + # Generate commit message if not provided + if not message: + message = self._generate_commit_message( + change_type="chore", + scope="rename", + description=f"rename {old_path} → {new_path}", + ) + + async with httpx.AsyncClient( + timeout=30.0, verify=self.valves.VERIFY_SSL + ) as client: + # Get old file content and SHA + get_response = await client.get( + self._api_url(f"/repos/{owner}/{repo_name}/contents/{old_path}"), + headers=self._headers(__user__), + params={"ref": effective_branch}, + ) + + if get_response.status_code == 404: + return f"Error: File not found: `{old_path}`" + + get_response.raise_for_status() + old_file = get_response.json() + old_sha = old_file.get("sha") + + # Decode content + content_b64 = old_file.get("content", "") + try: + content = base64.b64decode(content_b64).decode("utf-8") + except Exception: + return "Error: Could not decode file content." + + # Create new file with same content + new_content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii") + + create_response = await client.post( + self._api_url(f"/repos/{owner}/{repo_name}/contents/{new_path}"), + headers=self._headers(__user__), + json={ + "content": new_content_b64, + "message": message, + "branch": effective_branch, + }, + ) + + if create_response.status_code == 422: + return f"Error: File already exists at new path: `{new_path}`" + + create_response.raise_for_status() + + # Delete old file + delete_response = await client.delete( + self._api_url(f"/repos/{owner}/{repo_name}/contents/{old_path}"), + headers=self._headers(__user__), + json={ + "message": message, + "branch": effective_branch, + "sha": old_sha, + }, + ) + delete_response.raise_for_status() + + commit_sha = create_response.json().get("commit", {}).get("sha", "unknown")[:8] + + if __event_emitter__: + await __event_emitter__( + { + "type": "status", + "data": {"description": "Done", "done": True, "hidden": True}, + } + ) + + output = f"**File Renamed Successfully**\n\n" + output += f"**Old Path:** `{old_path}`\n" + output += f"**New Path:** `{new_path}`\n" + output += f"**Branch:** `{effective_branch}` (auto-detected from chat_id)\n" + output += f"**Commit:** `{commit_sha}`\n" + output += f"**Message:** {message}\n" + + return output + + except httpx.HTTPStatusError as e: + error_msg = self._format_error(e, f"file rename") + return f"Error: Failed to rename file. {error_msg}" + except Exception as e: + return f"Error: Unexpected failure during file rename: {type(e).__name__}: {e}" + + async def get_file( + self, + path: str, + repo: Optional[str] = None, + __user__: dict = None, + __chat_id__: str = None, + __event_emitter__: Callable[[dict], Any] = None, + ) -> str: + """ + Get the contents of a file from the repository. + + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + + Args: + path: Full path to the file (e.g., 'src/main.py') + repo: Repository in 'owner/repo' format + __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) + __event_emitter__: Event emitter for progress + + Returns: + File content with metadata (SHA, size, branch) + """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + # Try to use default branch for reading + effective_branch = self._get_branch(None, __user__) + + token = self._get_token(__user__) + if not token: + return "Error: GITEA_TOKEN not configured in UserValves settings." + + try: + owner, repo_name = self._resolve_repo(repo, __user__) + except ValueError as e: + return f"Error: {e}" if __event_emitter__: await __event_emitter__( @@ -1998,7 +2431,8 @@ get_pull_request({pr_number}) ) output = f"**File:** `{owner}/{repo_name}/{path}`\n" - output += f"**Branch:** `{effective_branch}` | **SHA:** `{sha_short}` | **Size:** {size} bytes\n" + output += f"**Branch:** `{effective_branch}`\n" + output += f"**SHA:** `{sha_short}` | **Size:** {size} bytes\n" output += f"**URL:** {file_info.get('html_url', 'N/A')}\n\n" output += f"```\n{content}\n```\n" @@ -2016,23 +2450,32 @@ get_pull_request({pr_number}) self, path: str = "", repo: Optional[str] = None, - branch: Optional[str] = None, __user__: dict = None, + __chat_id__: str = None, __event_emitter__: Callable[[dict], Any] = None, ) -> str: """ List files and directories in a repository path. + CRITICAL: Branch automatically detected from chat_id. LLM should NOT pass branch. + Args: path: Directory path to list (default: root) repo: Repository in 'owner/repo' format - branch: Branch name (defaults to repository default) __user__: User context + __chat_id__: Session ID for branch detection (REQUIRED) __event_emitter__: Event emitter for progress Returns: Formatted directory listing with file sizes and types """ + # Get working branch from session cache (SYSTEM-MANAGED) + effective_branch = self._get_working_branch(__chat_id__, __user__) + + if not effective_branch: + # Try to use default branch for reading + effective_branch = self._get_branch(None, __user__) + token = self._get_token(__user__) if not token: return "Error: GITEA_TOKEN not configured in UserValves settings." @@ -2042,8 +2485,6 @@ get_pull_request({pr_number}) except ValueError as e: return f"Error: {e}" - effective_branch = self._get_branch(branch, __user__) - if __event_emitter__: await __event_emitter__( { @@ -2070,7 +2511,7 @@ get_pull_request({pr_number}) if isinstance(contents, dict): return f"**Note:** '{path}' is a file. Use `get_file()` to read its contents." - output = f"**Contents of {owner}/{repo_name}/{path or '.'}** (`{effective_branch}` branch)\n\n" + output = f"**Contents of {owner}/{repo_name}/{path or '.'}** (`{effective_branch}`)\n\n" dirs = [item for item in contents if item.get("type") == "dir"] files = [item for item in contents if item.get("type") == "file"] @@ -2112,4 +2553,4 @@ get_pull_request({pr_number}) return f"Error: Path not found: `{path}`. Verify the path exists in the repository." return f"Error: Failed to list directory contents. {error_msg}" except Exception as e: - return f"Error: Unexpected failure during directory listing: {type(e).__name__}: {e}" \ No newline at end of file + return f"Error: Unexpected failure during directory listing: {type(e).__name__}: {e}"