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:
@@ -53,6 +53,13 @@ async def get_user_sessions(
|
|||||||
repo = await session.get(GitRepository, instance.repository_id)
|
repo = await session.get(GitRepository, instance.repository_id)
|
||||||
project = await session.get(Project, instance.project_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(
|
sessions.append(
|
||||||
{
|
{
|
||||||
"id": str(instance.id),
|
"id": str(instance.id),
|
||||||
@@ -64,6 +71,7 @@ async def get_user_sessions(
|
|||||||
"repository_id": str(instance.repository_id),
|
"repository_id": str(instance.repository_id),
|
||||||
"project_name": project.name if project else "unknown",
|
"project_name": project.name if project else "unknown",
|
||||||
"project_id": str(instance.project_id),
|
"project_id": str(instance.project_id),
|
||||||
|
"workspace_name": workspace_name,
|
||||||
"status": instance.status,
|
"status": instance.status,
|
||||||
"url": instance.url,
|
"url": instance.url,
|
||||||
"clone_mode": instance.clone_mode,
|
"clone_mode": instance.clone_mode,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from src.services.tool.instance_service import (
|
|||||||
create_tool_instance,
|
create_tool_instance,
|
||||||
delete_tool_instance,
|
delete_tool_instance,
|
||||||
recreate_instance_tunnel,
|
recreate_instance_tunnel,
|
||||||
|
rename_tool_instance,
|
||||||
restart_tool_instance,
|
restart_tool_instance,
|
||||||
start_tool_instance,
|
start_tool_instance,
|
||||||
stop_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(
|
@router.post(
|
||||||
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
|
"/{project_id}/repositories/{repo_id}/instances/{instance_id}/start",
|
||||||
summary="Start instance",
|
summary="Start instance",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ from fastapi import HTTPException, status
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.git.clone import check_dirty_state, clone_repository
|
||||||
from src.services.config.config_profile_resolver import (
|
from src.services.config.config_profile_resolver import (
|
||||||
ConfigProfileCycleError,
|
ConfigProfileCycleError,
|
||||||
@@ -935,8 +936,10 @@ async def create_tool_instance(
|
|||||||
if data.display_name:
|
if data.display_name:
|
||||||
instance_display = data.display_name
|
instance_display = data.display_name
|
||||||
else:
|
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
|
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:
|
if workspace:
|
||||||
count_query = (
|
count_query = (
|
||||||
@@ -2161,3 +2164,25 @@ async def stop_tool_instance(
|
|||||||
message="Instance stopped",
|
message="Instance stopped",
|
||||||
)
|
)
|
||||||
return {"status": instance.status}
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface Session {
|
|||||||
repository_id: string;
|
repository_id: string;
|
||||||
project_name: string;
|
project_name: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
|
workspace_name?: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
container_status?: string;
|
container_status?: string;
|
||||||
@@ -188,6 +189,19 @@ export async function checkInstanceHealth(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function renameInstance(
|
||||||
|
projectId: string,
|
||||||
|
repoId: string,
|
||||||
|
instanceId: string,
|
||||||
|
displayName: string,
|
||||||
|
): Promise<{ id: string; name: string; display_name: string }> {
|
||||||
|
const response = await apiClient.patch(
|
||||||
|
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`,
|
||||||
|
{ display_name: displayName },
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function recreateInstanceTunnel(
|
export async function recreateInstanceTunnel(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
repoId: string,
|
repoId: string,
|
||||||
|
|||||||
@@ -47,13 +47,18 @@ const SessionItem = ({ session }: { session: Session }) => {
|
|||||||
? `/instances/${session.id}/terminal`
|
? `/instances/${session.id}/terminal`
|
||||||
: `/projects/${session.project_id}`;
|
: `/projects/${session.project_id}`;
|
||||||
|
|
||||||
|
const tooltipParts = [session.display_name, session.project_name];
|
||||||
|
if (session.workspace_name) tooltipParts.push(session.workspace_name);
|
||||||
|
else if (session.repository_name) tooltipParts.push(session.repository_name);
|
||||||
|
tooltipParts.push(`(${session.status})`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
target={`session-${session.id}`}
|
target={`session-${session.id}`}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="nav-item session-item"
|
className="nav-item session-item"
|
||||||
title={`${session.display_name} (${session.status})`}
|
title={tooltipParts.join(" ")}
|
||||||
>
|
>
|
||||||
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
<span className={`session-status ${isRunning ? "running" : ""}`} />
|
||||||
<Icon name={session.tool_icon as IconName} size="sm" />
|
<Icon name={session.tool_icon as IconName} size="sm" />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface SessionCardProps {
|
|||||||
onStop?: (session: Session) => void;
|
onStop?: (session: Session) => void;
|
||||||
onDelete?: (session: Session) => void;
|
onDelete?: (session: Session) => void;
|
||||||
onRecreateTunnel?: (session: Session) => void;
|
onRecreateTunnel?: (session: Session) => void;
|
||||||
|
onRename?: (session: Session, newName: string) => void;
|
||||||
isBusy?: boolean;
|
isBusy?: boolean;
|
||||||
tunnelHealth?: {
|
tunnelHealth?: {
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
@@ -43,12 +44,15 @@ export function SessionCard({
|
|||||||
onStop,
|
onStop,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRecreateTunnel,
|
onRecreateTunnel,
|
||||||
|
onRename,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
tunnelHealth = null,
|
tunnelHealth = null,
|
||||||
}: SessionCardProps) {
|
}: SessionCardProps) {
|
||||||
const [showStopConfirm, setShowStopConfirm] = useState(false);
|
const [showStopConfirm, setShowStopConfirm] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [showActionSheet, setShowActionSheet] = useState(false);
|
const [showActionSheet, setShowActionSheet] = useState(false);
|
||||||
|
const [isEditingName, setIsEditingName] = useState(false);
|
||||||
|
const [editName, setEditName] = useState(session.display_name);
|
||||||
const isMobile = useMobileViewport();
|
const isMobile = useMobileViewport();
|
||||||
|
|
||||||
const status = statusConfig[session.status] || {
|
const status = statusConfig[session.status] || {
|
||||||
@@ -103,7 +107,43 @@ export function SessionCard({
|
|||||||
<div className="session-card-content">
|
<div className="session-card-content">
|
||||||
<div className="session-card-header">
|
<div className="session-card-header">
|
||||||
<div className="session-card-title">
|
<div className="session-card-title">
|
||||||
<h4>{session.display_name}</h4>
|
{isEditingName ? (
|
||||||
|
<div className="session-card-rename">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
onRename?.(session, editName);
|
||||||
|
setIsEditingName(false);
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setEditName(session.display_name);
|
||||||
|
setIsEditingName(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (editName.trim() && editName !== session.display_name) {
|
||||||
|
onRename?.(session, editName);
|
||||||
|
}
|
||||||
|
setIsEditingName(false);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h4
|
||||||
|
onClick={() => {
|
||||||
|
setEditName(session.display_name);
|
||||||
|
setIsEditingName(true);
|
||||||
|
}}
|
||||||
|
title="Click to rename"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{session.display_name}
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
<div className="session-card-status-badges">
|
<div className="session-card-status-badges">
|
||||||
<span className={`status-badge ${status.color}`}>
|
<span className={`status-badge ${status.color}`}>
|
||||||
{status.label}
|
{status.label}
|
||||||
@@ -118,10 +158,22 @@ export function SessionCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="muted session-card-meta">
|
<p className="muted session-card-meta session-card-context">
|
||||||
{session.tool_type_name}
|
<strong>{session.project_name}</strong>
|
||||||
{session.project_name && ` · ${session.project_name}`}
|
{session.workspace_name && (
|
||||||
{session.repository_name && ` · ${session.repository_name}`}
|
<>
|
||||||
|
{" "}
|
||||||
|
/ <span>{session.workspace_name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!session.workspace_name && session.repository_name && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
/ <span>{session.repository_name}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{" "}
|
||||||
|
· {session.tool_type_name}
|
||||||
</p>
|
</p>
|
||||||
{session.clone_mode && (
|
{session.clone_mode && (
|
||||||
<p className="muted session-card-meta">
|
<p className="muted session-card-meta">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface SessionListProps {
|
|||||||
onStop?: (session: Session) => void;
|
onStop?: (session: Session) => void;
|
||||||
onDelete?: (session: Session) => void;
|
onDelete?: (session: Session) => void;
|
||||||
onRecreateTunnel?: (session: Session) => void;
|
onRecreateTunnel?: (session: Session) => void;
|
||||||
|
onRename?: (session: Session, newName: string) => void;
|
||||||
actionBusyId?: string | null;
|
actionBusyId?: string | null;
|
||||||
tunnelHealth?: Record<string, InstanceHealth>;
|
tunnelHealth?: Record<string, InstanceHealth>;
|
||||||
showGrouping?: boolean;
|
showGrouping?: boolean;
|
||||||
@@ -28,6 +29,7 @@ export function SessionList({
|
|||||||
onStop,
|
onStop,
|
||||||
onDelete,
|
onDelete,
|
||||||
onRecreateTunnel,
|
onRecreateTunnel,
|
||||||
|
onRename,
|
||||||
actionBusyId = null,
|
actionBusyId = null,
|
||||||
tunnelHealth = {},
|
tunnelHealth = {},
|
||||||
showGrouping = true,
|
showGrouping = true,
|
||||||
@@ -56,6 +58,7 @@ export function SessionList({
|
|||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onRecreateTunnel={onRecreateTunnel}
|
onRecreateTunnel={onRecreateTunnel}
|
||||||
|
onRename={onRename}
|
||||||
isBusy={actionBusyId === session.id}
|
isBusy={actionBusyId === session.id}
|
||||||
tunnelHealth={tunnelHealth[session.id] || null}
|
tunnelHealth={tunnelHealth[session.id] || null}
|
||||||
/>
|
/>
|
||||||
@@ -88,6 +91,7 @@ export function SessionList({
|
|||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onRecreateTunnel={onRecreateTunnel}
|
onRecreateTunnel={onRecreateTunnel}
|
||||||
|
onRename={onRename}
|
||||||
isBusy={actionBusyId === session.id}
|
isBusy={actionBusyId === session.id}
|
||||||
tunnelHealth={tunnelHealth[session.id] || null}
|
tunnelHealth={tunnelHealth[session.id] || null}
|
||||||
/>
|
/>
|
||||||
@@ -113,6 +117,7 @@ export function SessionList({
|
|||||||
onStop={onStop}
|
onStop={onStop}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onRecreateTunnel={onRecreateTunnel}
|
onRecreateTunnel={onRecreateTunnel}
|
||||||
|
onRename={onRename}
|
||||||
isBusy={actionBusyId === session.id}
|
isBusy={actionBusyId === session.id}
|
||||||
tunnelHealth={tunnelHealth[session.id] || null}
|
tunnelHealth={tunnelHealth[session.id] || null}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
deleteInstance,
|
deleteInstance,
|
||||||
startInstance,
|
startInstance,
|
||||||
recreateInstanceTunnel,
|
recreateInstanceTunnel,
|
||||||
|
renameInstance,
|
||||||
} from "../api/sessions";
|
} from "../api/sessions";
|
||||||
import type { Session } from "../api/sessions";
|
import type { Session } from "../api/sessions";
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ interface UseInstanceActionsReturn {
|
|||||||
handleDelete: (session: Session) => Promise<void>;
|
handleDelete: (session: Session) => Promise<void>;
|
||||||
handleForceDelete: (session: Session) => Promise<void>;
|
handleForceDelete: (session: Session) => Promise<void>;
|
||||||
handleRecreateTunnel: (session: Session) => Promise<void>;
|
handleRecreateTunnel: (session: Session) => Promise<void>;
|
||||||
|
handleRename: (session: Session, newName: string) => Promise<void>;
|
||||||
clearDirtyDelete: () => void;
|
clearDirtyDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ export function useInstanceActions(
|
|||||||
url = `/projects/${session.project_id}`;
|
url = `/projects/${session.project_id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.title = `${session.display_name} — Headquarter`;
|
||||||
const w = window.open(url, `session-${session.id}`);
|
const w = window.open(url, `session-${session.id}`);
|
||||||
tabRefs.current.set(key, w);
|
tabRefs.current.set(key, w);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -177,6 +180,27 @@ export function useInstanceActions(
|
|||||||
[loadingSessionId, onRefresh],
|
[loadingSessionId, onRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRename = useCallback(
|
||||||
|
async (session: Session, newName: string) => {
|
||||||
|
if (!newName.trim()) return;
|
||||||
|
setLoadingSessionId(session.id);
|
||||||
|
try {
|
||||||
|
await renameInstance(
|
||||||
|
session.project_id,
|
||||||
|
session.repository_id,
|
||||||
|
session.id,
|
||||||
|
newName.trim(),
|
||||||
|
);
|
||||||
|
await onRefresh();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoadingSessionId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onRefresh],
|
||||||
|
);
|
||||||
|
|
||||||
const clearDirtyDelete = useCallback(() => {
|
const clearDirtyDelete = useCallback(() => {
|
||||||
setDirtyDeleteSession(null);
|
setDirtyDeleteSession(null);
|
||||||
setDirtyDeleteFiles([]);
|
setDirtyDeleteFiles([]);
|
||||||
@@ -192,6 +216,7 @@ export function useInstanceActions(
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
handleForceDelete,
|
handleForceDelete,
|
||||||
handleRecreateTunnel,
|
handleRecreateTunnel,
|
||||||
|
handleRename,
|
||||||
clearDirtyDelete,
|
clearDirtyDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ export const useTerminalPage = () => {
|
|||||||
}
|
}
|
||||||
}, [loading, sessions.length, error, instanceId, createSession]);
|
}, [loading, sessions.length, error, instanceId, createSession]);
|
||||||
|
|
||||||
|
// Update document title based on active terminal session
|
||||||
|
useEffect(() => {
|
||||||
|
if (!instanceId) {
|
||||||
|
document.title = "Terminal — Headquarter";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const active = sessions.find((s) => s.id === activeSessionId);
|
||||||
|
const name = active?.name ?? `Instance ${instanceId.slice(0, 8)}`;
|
||||||
|
document.title = `${name} — Terminal — Headquarter`;
|
||||||
|
return () => {
|
||||||
|
document.title = "Headquarter";
|
||||||
|
};
|
||||||
|
}, [instanceId, activeSessionId, sessions]);
|
||||||
|
|
||||||
// Sync refs with sessions
|
// Sync refs with sessions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
|||||||
@@ -72,5 +72,15 @@ export function useWorkspaceFiles(
|
|||||||
refresh();
|
refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
return { entries, content, currentPath, loading, error, refresh, loadFile, saveFile, navigateTo };
|
return {
|
||||||
|
entries,
|
||||||
|
content,
|
||||||
|
currentPath,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refresh,
|
||||||
|
loadFile,
|
||||||
|
saveFile,
|
||||||
|
navigateTo,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const SessionsPage = () => {
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
handleForceDelete,
|
handleForceDelete,
|
||||||
handleRecreateTunnel,
|
handleRecreateTunnel,
|
||||||
|
handleRename,
|
||||||
clearDirtyDelete,
|
clearDirtyDelete,
|
||||||
} = useInstanceActions({ onRefresh: loadSessions });
|
} = useInstanceActions({ onRefresh: loadSessions });
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ export const SessionsPage = () => {
|
|||||||
session={lastSession}
|
session={lastSession}
|
||||||
onOpen={handleOpen}
|
onOpen={handleOpen}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onRename={handleRename}
|
||||||
isBusy={loadingSessionId === lastSession.id}
|
isBusy={loadingSessionId === lastSession.id}
|
||||||
tunnelHealth={tunnelHealth[lastSession.id] || null}
|
tunnelHealth={tunnelHealth[lastSession.id] || null}
|
||||||
/>
|
/>
|
||||||
@@ -139,6 +141,7 @@ export const SessionsPage = () => {
|
|||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onRecreateTunnel={handleRecreateTunnel}
|
onRecreateTunnel={handleRecreateTunnel}
|
||||||
|
onRename={handleRename}
|
||||||
actionBusyId={loadingSessionId}
|
actionBusyId={loadingSessionId}
|
||||||
tunnelHealth={tunnelHealth}
|
tunnelHealth={tunnelHealth}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -150,8 +150,16 @@ function MobileTabBar({
|
|||||||
/* ─── Files Tab ─── */
|
/* ─── Files Tab ─── */
|
||||||
|
|
||||||
function FilesTab({ workspaceId }: { workspaceId: string }) {
|
function FilesTab({ workspaceId }: { workspaceId: string }) {
|
||||||
const { entries, content, currentPath, loadFile, saveFile, loading, error, navigateTo } =
|
const {
|
||||||
useWorkspaceFiles(workspaceId);
|
entries,
|
||||||
|
content,
|
||||||
|
currentPath,
|
||||||
|
loadFile,
|
||||||
|
saveFile,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
navigateTo,
|
||||||
|
} = useWorkspaceFiles(workspaceId);
|
||||||
const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId);
|
const { status, commit, push, pull, fetch } = useWorkspaceGit(workspaceId);
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||||
const [editContent, setEditContent] = useState<string | null>(null);
|
const [editContent, setEditContent] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
/* Session card rename */
|
||||||
|
.session-card-rename input {
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card-context {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card-context strong {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile Sessions Page */
|
/* Mobile Sessions Page */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.sessions-page {
|
.sessions-page {
|
||||||
|
|||||||
Reference in New Issue
Block a user