feat: floating action button for starting tools globally

- New StartToolFAB component: fixed floating button (bottom-right) opens
  a modal with workspace selector + ToolStarter
- Added to AppShell: available on every page except mobile terminal
- Dashboard (home): removed old CreateSessionForm and 'Quick create' section,
  replaced with FAB + 'Workspaces quick access' prompt
- Sessions page: removed inline workspace selector + ToolStarter, now
  shows prompt to use the FAB
- Styles: .start-tool-fab with hover scale, shadow, mobile offset above tab bar

Quality gates: tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped)
This commit is contained in:
2026-06-01 22:09:49 +02:00
parent 78e808bc54
commit 280a6ff2fa
5 changed files with 194 additions and 181 deletions
+5 -92
View File
@@ -6,14 +6,11 @@ import {
checkInstanceHealth,
} from "../api/sessions";
import { getUserConfig } from "../api/settings";
import { listAllWorkspaces } from "../api/workspaces";
import { ErrorState, LoadingState } from "../components/data-states";
import { SessionList } from "../components/session-list";
import { SessionCard } from "../components/session-card";
import { ToolStarter } from "../components/tool-starter";
import { useInstanceActions } from "../hooks/use-instance-actions";
import type { InstanceHealth } from "../api/sessions";
import type { Workspace } from "../types/workspace";
type SessionsStatus = "loading" | "ready" | "error";
@@ -22,12 +19,6 @@ export const SessionsPage = () => {
const [sessions, setSessions] = useState<Session[]>([]);
const [lastSessionId, setLastSessionId] = useState<string | null>(null);
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [workspacesLoading, setWorkspacesLoading] = useState(true);
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
null,
);
const [tunnelHealth, setTunnelHealth] = useState<
Record<string, InstanceHealth>
>({});
@@ -51,21 +42,6 @@ export const SessionsPage = () => {
void loadSessions();
}, [loadSessions]);
// Load workspaces for the tool-starter flow
useEffect(() => {
const load = async () => {
try {
const data = await listAllWorkspaces();
setWorkspaces(data);
} catch {
// ignore
} finally {
setWorkspacesLoading(false);
}
};
void load();
}, []);
const {
loadingSessionId,
dirtyDeleteSession,
@@ -123,11 +99,6 @@ export const SessionsPage = () => {
const lastSession = sessions.find((s) => s.id === lastSessionId) ?? null;
const handleToolStarted = async () => {
setSelectedWorkspace(null);
await loadSessions();
};
return (
<section className="stack sessions-page">
<div className="page-header">
@@ -173,71 +144,13 @@ export const SessionsPage = () => {
/>
</div>
{/* Create Session — workspace-first */}
{/* Create Session — use floating button */}
<div className="create-session-section">
<h2>Start New Tool</h2>
{workspacesLoading ? (
<p className="muted">Loading workspaces...</p>
) : workspaces.length === 0 ? (
<p className="muted">
No workspaces yet.{" "}
<a href="/workspaces">Create a workspace first</a>.
</p>
) : (
<div className="stack">
{!selectedWorkspace ? (
<div className="form-group">
<label htmlFor="workspace-select">
Select a workspace to start a tool in
</label>
<select
id="workspace-select"
value=""
onChange={(e) => {
const ws = workspaces.find(
(w) => w.id === e.target.value,
);
if (ws) setSelectedWorkspace(ws);
}}
>
<option value="">
Choose a workspace...
</option>
{workspaces.map((ws) => (
<option key={ws.id} value={ws.id}>
{ws.project_name} / {ws.repo_name} /{" "}
{ws.name}
</option>
))}
</select>
</div>
) : (
<div className="card">
<div className="tool-starter-header">
<h4>
{selectedWorkspace.project_name} /{" "}
{selectedWorkspace.repo_name} /{" "}
{selectedWorkspace.name}
</h4>
<button
className="ghost-button small"
onClick={() =>
setSelectedWorkspace(null)
}
type="button"
>
Change
</button>
</div>
<ToolStarter
workspace={selectedWorkspace}
onStarted={handleToolStarted}
onCancel={() => setSelectedWorkspace(null)}
/>
</div>
)}
</div>
)}
<p className="muted">
Use the floating button (bottom-right) to start a tool in any
workspace.
</p>
</div>
{/* Dirty Delete Confirmation Modal */}