feat: add session name input to tool starter

ToolStarter (used by FAB and workspace detail):
- Add Session Name text input, defaulting to workspace.name
- Pass user-provided name to createInstance display_name parameter
- If left empty or only whitespace, falls back to auto-generated name

Quality gates: tsc --noEmit pass, npm run build pass, 82/82 tests pass
This commit is contained in:
Developer
2026-06-10 20:26:34 +00:00
parent 1d10283fc9
commit 3c222d4f0f
6 changed files with 419 additions and 390 deletions
+6 -5
View File
@@ -41,11 +41,12 @@ const SessionItem = ({ session }: { session: Session }) => {
// - Everything else falls back to the project page
const hasTerminal = session.tool_type_interfaces.includes("terminal");
const hasWeb = session.tool_type_interfaces.includes("web");
const href = session.url && hasWeb
? session.url
: hasTerminal
? `/instances/${session.id}/terminal`
: `/projects/${session.project_id}`;
const href =
session.url && hasWeb
? session.url
: hasTerminal
? `/instances/${session.id}/terminal`
: `/projects/${session.project_id}`;
const tooltipParts = [session.display_name, session.project_name];
if (session.workspace_name) tooltipParts.push(session.workspace_name);
@@ -171,8 +171,7 @@ export function SessionCard({
{" "}
/ <span>{session.repository_name}</span>
</>
)}
{" "}
)}{" "}
· {session.tool_type_name}
</p>
{session.clone_mode && (
@@ -3,128 +3,137 @@ import { SessionCard } from "./session-card";
import type { InstanceHealth } from "../../../api/sessions";
export interface SessionListProps {
sessions: Session[];
onOpen?: (session: Session) => void;
onStart?: (session: Session) => void;
onStop?: (session: Session) => void;
onDelete?: (session: Session) => void;
onRecreateTunnel?: (session: Session) => void;
onRename?: (session: Session, newName: string) => void;
actionBusyId?: string | null;
tunnelHealth?: Record<string, InstanceHealth>;
showGrouping?: boolean;
activeTitle?: string;
recentTitle?: string;
maxRecent?: number;
emptyMessage?: string;
sessions: Session[];
onOpen?: (session: Session) => void;
onStart?: (session: Session) => void;
onStop?: (session: Session) => void;
onDelete?: (session: Session) => void;
onRecreateTunnel?: (session: Session) => void;
onRename?: (session: Session, newName: string) => void;
actionBusyId?: string | null;
tunnelHealth?: Record<string, InstanceHealth>;
showGrouping?: boolean;
activeTitle?: string;
recentTitle?: string;
maxRecent?: number;
emptyMessage?: string;
}
const activeStatuses = ["running", "building", "starting", "probing", "pending", "unhealthy"];
const activeStatuses = [
"running",
"building",
"starting",
"probing",
"pending",
"unhealthy",
];
const recentStatuses = ["stopped", "error"];
export function SessionList({
sessions,
onOpen,
onStart,
onStop,
onDelete,
onRecreateTunnel,
onRename,
actionBusyId = null,
tunnelHealth = {},
showGrouping = true,
activeTitle = "Active Sessions",
recentTitle = "Recent Sessions",
maxRecent = 5,
emptyMessage = "No sessions",
sessions,
onOpen,
onStart,
onStop,
onDelete,
onRecreateTunnel,
onRename,
actionBusyId = null,
tunnelHealth = {},
showGrouping = true,
activeTitle = "Active Sessions",
recentTitle = "Recent Sessions",
maxRecent = 5,
emptyMessage = "No sessions",
}: SessionListProps) {
const activeSessions = sessions.filter((s) => activeStatuses.includes(s.status));
const recentSessions = sessions
.filter((s) => recentStatuses.includes(s.status))
.slice(0, maxRecent);
const activeSessions = sessions.filter((s) =>
activeStatuses.includes(s.status),
);
const recentSessions = sessions
.filter((s) => recentStatuses.includes(s.status))
.slice(0, maxRecent);
if (!showGrouping) {
return (
<div className="sessions-grid">
{sessions.length === 0 ? (
<p className="muted">{emptyMessage}</p>
) : (
sessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))
)}
</div>
);
}
if (!showGrouping) {
return (
<div className="sessions-grid">
{sessions.length === 0 ? (
<p className="muted">{emptyMessage}</p>
) : (
sessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))
)}
</div>
);
}
return (
<div className="session-list">
{/* Active Sessions */}
<div className="session-group">
<div className="session-group-header">
<h3>{activeTitle}</h3>
{activeSessions.length > 0 && (
<span className="badge">{activeSessions.length}</span>
)}
</div>
{activeSessions.length === 0 ? (
<p className="muted">No active sessions</p>
) : (
<div className="sessions-grid">
{activeSessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))}
</div>
)}
</div>
return (
<div className="session-list">
{/* Active Sessions */}
<div className="session-group">
<div className="session-group-header">
<h3>{activeTitle}</h3>
{activeSessions.length > 0 && (
<span className="badge">{activeSessions.length}</span>
)}
</div>
{activeSessions.length === 0 ? (
<p className="muted">No active sessions</p>
) : (
<div className="sessions-grid">
{activeSessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))}
</div>
)}
</div>
{/* Recent Sessions */}
{recentSessions.length > 0 && (
<div className="session-group">
<div className="session-group-header">
<h3>{recentTitle}</h3>
<span className="badge">{recentSessions.length}</span>
</div>
<div className="sessions-grid">
{recentSessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))}
</div>
</div>
)}
</div>
);
{/* Recent Sessions */}
{recentSessions.length > 0 && (
<div className="session-group">
<div className="session-group-header">
<h3>{recentTitle}</h3>
<span className="badge">{recentSessions.length}</span>
</div>
<div className="sessions-grid">
{recentSessions.map((session) => (
<SessionCard
key={session.id}
session={session}
onOpen={onOpen}
onStart={onStart}
onStop={onStop}
onDelete={onDelete}
onRecreateTunnel={onRecreateTunnel}
onRename={onRename}
isBusy={actionBusyId === session.id}
tunnelHealth={tunnelHealth[session.id] || null}
/>
))}
</div>
</div>
)}
</div>
);
}
@@ -33,6 +33,7 @@ export function ToolStarter({
const [sshKeysLoading, setSshKeysLoading] = useState(true);
const [selectedSshKeyIds, setSelectedSshKeyIds] = useState<string[]>([]);
const [displayName, setDisplayName] = useState(workspace.name);
const [starting, setStarting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -119,7 +120,7 @@ export function ToolStarter({
workspace.project_id,
workspace.repo_id,
selectedToolTypeId,
workspace.name,
displayName.trim() || undefined,
undefined,
undefined,
undefined,
@@ -140,7 +141,7 @@ export function ToolStarter({
} finally {
setStarting(false);
}
}, [selectedToolTypeId, selectedProfileId, workspace, onStarted]);
}, [selectedToolTypeId, selectedProfileId, displayName, workspace, onStarted]);
return (
<div className="tool-starter">
@@ -187,6 +188,19 @@ export function ToolStarter({
{toolTypesError && <span className="error-text">{toolTypesError}</span>}
</div>
{/* Session Name */}
<div className="form-group">
<label htmlFor="session-name">Session Name</label>
<input
id="session-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My dev environment"
disabled={starting}
/>
</div>
{/* Config Profile */}
{selectedToolTypeId && (
<div className="form-group">