(null);
const [isRemoving, setIsRemoving] = useState(false);
+ const [isSpawning, setIsSpawning] = useState(false);
// Live workers only: merged/terminated sessions leave the sidebar and stay
// reachable through the board's Done / Terminated bar (SessionsBoard).
const sessions = workerSessions(workspace.sessions).filter(sessionIsActive);
+ // The project's live orchestrator (if any) backs the hover Orchestrator
+ // button: navigate to it when present, otherwise spawn one first.
+ const orchestrator = workspace.sessions.find((s) => isOrchestratorSession(s) && sessionIsActive(s));
+
+ // Mirrors ShellTopbar's launcher: attach to the running orchestrator, or
+ // spawn one via the daemon and follow it once the workspace refetches.
+ const openOrchestrator = async () => {
+ if (orchestrator) {
+ selection.goSession(workspace.id, orchestrator.id);
+ return;
+ }
+ setIsSpawning(true);
+ try {
+ const sessionId = await spawnOrchestrator(workspace.id);
+ await queryClient.invalidateQueries({ queryKey: workspaceQueryKey });
+ selection.goSession(workspace.id, sessionId);
+ } catch (err) {
+ console.error("Failed to spawn orchestrator:", err);
+ } finally {
+ setIsSpawning(false);
+ }
+ };
const onProjectClick = () => {
if (!expanded) {
@@ -418,9 +452,10 @@ function ProjectItem({
"h-auto gap-[9px] rounded-[5px] px-1.5 py-[7px] text-[13px] font-medium text-muted-foreground transition-[padding]",
"hover:bg-interactive-hover hover:text-muted-foreground active:bg-interactive-hover active:text-muted-foreground",
"data-[active=true]:bg-interactive-active data-[active=true]:font-semibold data-[active=true]:text-foreground",
- // Make room for the kebab action when the row is hovered, focused, or
- // its menu is open (the absolutely-positioned action replaces the count).
- "group-hover/menu-item:pr-[34px] group-focus-within/menu-item:pr-[34px] group-has-data-[state=open]/menu-item:pr-[34px]",
+ // Make room for the hover actions (dashboard, orchestrator, kebab)
+ // when the row is hovered, focused, or its menu is open (the
+ // absolutely-positioned cluster replaces the count).
+ "group-hover/menu-item:pr-[78px] group-focus-within/menu-item:pr-[78px] group-has-data-[state=open]/menu-item:pr-[78px]",
// Icon rail: the old 36px letter tile.
"group-data-[collapsible=icon]:size-9! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:rounded-lg group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:font-semibold",
)}
@@ -439,30 +474,69 @@ function ProjectItem({
{sessions.length}
- {/* Per-project actions: a kebab that reveals on row hover (replacing the
- session count) — surfaces the daemon/CLI removal capability in the UI. */}
-
-
-
-
-
-
-
- selection.goSettings(workspace.id)}>
-
- Project settings
-
-
- void removeProject()}
- >
-
- Remove project
-
-
-
+ {/* Per-project hover actions: dashboard board, orchestrator, and a kebab
+ menu. The cluster reveals on row hover/focus (or while the kebab is
+ open), replacing the session count, and stays hidden in the icon rail. */}
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+ {isSpawning ? "Spawning…" : orchestrator ? "Orchestrator" : "Spawn orchestrator"}
+
+
+
+
+
+
+
+ selection.goSettings(workspace.id)}>
+
+ Project settings
+
+
+ void removeProject()}
+ >
+
+ Remove project
+
+
+
+
{removeError && (
{removeError}