feat: improve session naming, project display, rename support, tab titles

Backend:
- sessions.py: include workspace_name in session response
- instance_service.py: auto-generate display names as
  'Project / Workspace / Tool #N' instead of 'Workspace / Tool #N'
- instance_service.py: add rename_tool_instance() service function
- tool_instances.py: add PATCH /instances/{id} endpoint for renaming
  display_name

Frontend:
- api/sessions.ts: add workspace_name to Session type, add renameInstance()
- use-instance-actions.ts: add handleRename, set document.title when opening
- session-card.tsx: click-to-edit display_name inline; always show project
  context line (Project / Workspace or Repo / Tool)
- session-list.tsx: pass through onRename prop
- SessionsPage.tsx: wire handleRename to SessionCard and SessionList
- app-shell.tsx: sidebar tooltip includes workspace or repo name
- use-terminal-page.ts: set document.title based on active terminal session

Quality gates: py_compile all backend files pass, tsc --noEmit pass,
npm run build pass, 82/82 tests pass
This commit is contained in:
Developer
2026-06-10 17:04:03 +00:00
parent 886be83af5
commit 1d10283fc9
13 changed files with 243 additions and 11 deletions
+8
View File
@@ -53,6 +53,13 @@ async def get_user_sessions(
repo = await session.get(GitRepository, instance.repository_id)
project = await session.get(Project, instance.project_id)
workspace_name = None
if instance.workspace_id:
from src.models import Workspace as WorkspaceModel
workspace = await session.get(WorkspaceModel, instance.workspace_id)
if workspace:
workspace_name = workspace.name
sessions.append(
{
"id": str(instance.id),
@@ -64,6 +71,7 @@ async def get_user_sessions(
"repository_id": str(instance.repository_id),
"project_name": project.name if project else "unknown",
"project_id": str(instance.project_id),
"workspace_name": workspace_name,
"status": instance.status,
"url": instance.url,
"clone_mode": instance.clone_mode,
+40
View File
@@ -29,6 +29,7 @@ from src.services.tool.instance_service import (
create_tool_instance,
delete_tool_instance,
recreate_instance_tunnel,
rename_tool_instance,
restart_tool_instance,
start_tool_instance,
stop_tool_instance,
@@ -153,6 +154,45 @@ async def get_instance(
}
@router.patch(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}",
summary="Rename instance",
description="Update the display name of a tool instance.",
)
async def rename_instance(
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
data: dict,
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
_user = await _get_user(session, user_id)
_project = await _get_owned_project(project_id, user_id, session)
display_name = data.get("display_name", "").strip()
if not display_name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="display_name is required",
)
try:
instance = await rename_tool_instance(
session, user_id, project_id, repo_id, instance_id, display_name
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
)
return {
"id": str(instance.id),
"name": instance.name,
"display_name": instance.display_name,
}
@router.post(
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
summary="Start instance",
+27 -2
View File
@@ -13,7 +13,8 @@ from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.models import ConfigProfile, GitRepository, SSHKey, ToolInstance, ToolType
from src.models import ConfigProfile, GitRepository, Project, SSHKey, ToolInstance, ToolType
from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest
from src.services.git.clone import check_dirty_state, clone_repository
from src.services.config.config_profile_resolver import (
ConfigProfileCycleError,
@@ -935,8 +936,10 @@ async def create_tool_instance(
if data.display_name:
instance_display = data.display_name
else:
project = await session.get(Project, project_id)
project_name = project.name if project else "Unknown"
scope_name = workspace.name if workspace else repo.name
auto_name = f"{scope_name} / {tool_type.display_name}"
auto_name = f"{project_name} / {scope_name} / {tool_type.display_name}"
if workspace:
count_query = (
@@ -2161,3 +2164,25 @@ async def stop_tool_instance(
message="Instance stopped",
)
return {"status": instance.status}
async def rename_tool_instance(
session: AsyncSession,
user_id: uuid.UUID,
project_id: uuid.UUID,
repo_id: uuid.UUID,
instance_id: uuid.UUID,
display_name: str,
) -> ToolInstance:
"""Rename a tool instance (update display_name only)."""
instance = await session.get(ToolInstance, instance_id)
if instance is None or instance.repository_id != repo_id:
raise ValueError("instance not found")
if instance.owner_id != user_id:
raise ValueError("not authorized")
instance.display_name = display_name.strip()
await session.commit()
await session.refresh(instance)
return instance