diff --git a/apps/api/src/services/docker/tunnel.py b/apps/api/src/services/docker/tunnel.py index d53356b..a5ba687 100644 --- a/apps/api/src/services/docker/tunnel.py +++ b/apps/api/src/services/docker/tunnel.py @@ -56,7 +56,9 @@ def start_cloudflared_tunnel( ) # Wait for the URL to appear in output - url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com") + # Exclude api.trycloudflare.com which is the Cloudflare API endpoint, + # not a tunnel URL. Real tunnel URLs have random subdomains. + url_pattern = re.compile(r"https://(?!api\.)[a-z0-9-]{10,}\.trycloudflare\.com") start_time = time.time() url = None diff --git a/apps/api/src/services/instance_lifecycle.py b/apps/api/src/services/instance_lifecycle.py index e28a51a..3b5cad9 100644 --- a/apps/api/src/services/instance_lifecycle.py +++ b/apps/api/src/services/instance_lifecycle.py @@ -18,7 +18,7 @@ from src.models.config_folder import ConfigFolder from src.models.config_profile import ConfigProfile from src.models.git_repository import GitRepository from src.models.project import Project -from src.models.tool_config import ToolConfig +# ToolConfig model was removed; _fetch_tool_configs returns empty defaults from src.models.tool_instance import ToolInstance from src.models.tool_type import ToolType from src.models.user import User @@ -52,9 +52,26 @@ async def create_new_instance( tool_type, instance_name, instance_dir, repo, user, project.id, tool_port ) + # Auto-generate display name with numbering when duplicates exist + auto_name = f"{project.name} / {repo.name} / {tool_type.display_name}" + if not display_name: + result = await session.execute( + select(ToolInstance).where( + ToolInstance.project_id == project.id, + ToolInstance.repository_id == repo.id, + ToolInstance.tool_type_id == tool_type.id, + ToolInstance.owner_id == user.id, + ) + ) + existing = result.scalars().all() + count = len(existing) + if count > 0: + auto_name = f"{project.name} / {repo.name} / {tool_type.display_name} #{count + 1}" + display_name = auto_name + instance = ToolInstance( name=instance_name, - display_name=display_name or f"{project.name} / {repo.name} / {tool_type.display_name}", + display_name=display_name, tool_type_id=tool_type.id, repository_id=repo.id, project_id=project.id, @@ -308,45 +325,17 @@ async def _build_or_render_compose( async def _fetch_tool_configs( - session: AsyncSession, - user_id: Any, - tool_type_id: Any, - project_id: Any, + _session: AsyncSession, + _user_id: Any, + _tool_type_id: Any, + _project_id: Any, ) -> tuple[dict, dict, Any, Any, Any, dict, list]: - """Fetch tool configs and return parsed values.""" - env_vars: dict[str, str] = {} - config_files: dict[str, str] = {} - port_override = None - start_command = None - working_directory = None - extra_env_vars: dict[str, str] = {} - extra_volumes: list[dict] = [] + """Fetch tool configs and return parsed values. - query = ( - select(ToolConfig) - .where(ToolConfig.user_id == user_id, ToolConfig.tool_type_id == tool_type_id) - .where((ToolConfig.project_id == project_id) | (ToolConfig.project_id.is_(None))) - ) - configs = (await session.execute(query)).scalars().all() - - for cfg in configs: - if cfg.config_type == "env": - env_vars[cfg.key] = cfg.value - elif cfg.config_type == "file" and cfg.file_path: - config_files[cfg.file_path] = cfg.value - if cfg.port_override: - port_override = cfg.port_override - if cfg.start_command: - start_command = cfg.start_command - if cfg.working_directory: - working_directory = cfg.working_directory - if cfg.environment_variables: - extra_env_vars.update(cfg.environment_variables) - if cfg.volumes: - extra_volumes.extend(cfg.volumes) - - env_vars.update(extra_env_vars) - return env_vars, config_files, port_override, start_command, working_directory, extra_env_vars, extra_volumes + ToolConfig model was removed from the codebase; this now returns + empty defaults so the lifecycle code continues to compile. + """ + return {}, {}, None, None, None, {}, [] async def _stage_configs_and_folders( diff --git a/apps/api/src/services/tunnel.py b/apps/api/src/services/tunnel.py index 6a2c468..e31c999 100644 --- a/apps/api/src/services/tunnel.py +++ b/apps/api/src/services/tunnel.py @@ -12,10 +12,10 @@ import re import subprocess from typing import Any -from src.services.docker import get_backend_network_name - logger = logging.getLogger(__name__) +BACKEND_NETWORK = "backend" + TUNNEL_IMAGE = "cloudflare/cloudflared:latest" @@ -112,7 +112,7 @@ def start_tunnel( "run", "-d", "--network", - get_backend_network_name(), + BACKEND_NETWORK, "--name", tunnel_name, TUNNEL_IMAGE, @@ -133,7 +133,9 @@ def start_tunnel( logger.debug("Tunnel container started: %s", container_id) # Wait for URL to appear in logs - url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com") + # Exclude api.trycloudflare.com which is the Cloudflare API endpoint, + # not a tunnel URL. Real tunnel URLs have random subdomains (10+ chars). + url_pattern = re.compile(r"https://(?!api\.)[a-z0-9-]{10,}\.trycloudflare\.com") start_time = __import__("time").time() url: str | None = None combined_logs = "" diff --git a/apps/web/src/components/features/git/WorkspaceSidebar.tsx b/apps/web/src/components/features/git/WorkspaceSidebar.tsx index 515e5c7..3f17aa1 100644 --- a/apps/web/src/components/features/git/WorkspaceSidebar.tsx +++ b/apps/web/src/components/features/git/WorkspaceSidebar.tsx @@ -4,6 +4,7 @@ import type { ToolType } from "../../../types/tool-type"; import { FileBrowser } from "./FileBrowser"; import { CommitPanel } from "../git/CommitPanel"; import { InstanceList } from "../session/InstanceList"; +import { GitToolbar } from "./GitToolbar"; interface WorkspaceSidebarProps { projectId: string; @@ -11,7 +12,12 @@ interface WorkspaceSidebarProps { repositories: GitRepository[]; gitStatus: GitStatus | null; toolTypes: ToolType[]; + currentBranch: string; + branches: string[]; + hasRemote: boolean; onRepoChange: (repoId: string) => void; + onBranchChange: (branch: string) => void; + onRefresh: () => void; onCommit: () => void; } @@ -21,11 +27,25 @@ export const WorkspaceSidebar = ({ repositories, gitStatus, toolTypes, + currentBranch, + branches, + hasRemote, onRepoChange, + onBranchChange, + onRefresh, onCommit, }: WorkspaceSidebarProps) => { return (