Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/renderer/components/SessionsBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type AttentionZone, type WorkspaceSession, attentionZone, workerSession
import { useSessionScmSummary, type SessionPRSummary } from "../hooks/useSessionScmSummary";
import { useWorkspaceQuery, workspaceQueryKey } from "../hooks/useWorkspaceQuery";
import { DashboardSubhead } from "./DashboardSubhead";
import { NotificationCenter } from "./NotificationCenter";
import { OrchestratorIcon } from "./icons";
import { NewTaskDialog } from "./NewTaskDialog";
import { spawnOrchestrator } from "../lib/spawn-orchestrator";
Expand Down Expand Up @@ -126,6 +127,7 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {

const actions = projectId ? (
<>
<NotificationCenter />
<button
aria-label="New task"
className="dashboard-app-header__accent-btn"
Expand All @@ -146,7 +148,9 @@ export function SessionsBoard({ projectId }: SessionsBoardProps) {
{isSpawning ? "Spawning..." : orchestrator ? "Orchestrator" : "Spawn Orchestrator"}
</button>
</>
) : undefined;
) : (
<NotificationCenter />
);

return (
<div className="flex h-full min-h-0 flex-col bg-background text-foreground">
Expand Down
132 changes: 62 additions & 70 deletions frontend/src/renderer/components/ShellTopbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate, useParams } from "@tanstack/react-router";
import { GitBranch, LayoutDashboard, PanelRightClose, PanelRightOpen, Plus, Square, Trash2 } from "lucide-react";
import { useState } from "react";
import { NotificationCenter } from "./NotificationCenter";
import {
findProjectOrchestrator,
isOrchestratorSession,
Expand Down Expand Up @@ -70,11 +69,13 @@ export function ShellTopbar() {
// removed, or data still loading) shows an empty crumb — never the raw
// route slug. "agent-orchestrator" is the root-board crumb only.
const projectId = session?.workspaceId ?? params.projectId;
const isProjectBoardRoute = !isSessionRoute && Boolean(projectId);
const project = projectId ? all.find((workspace) => workspace.id === projectId) : undefined;
const projectLabel = project?.name ?? session?.workspaceName ?? (projectId ? "" : "agent-orchestrator");
const projectLabel = session?.workspaceName ?? "";
const orchestrator = projectId ? findProjectOrchestrator(all, projectId) : undefined;

if (!isSessionRoute) {
return null;
}

const openBoard = () =>
projectId ? void navigate({ to: "/projects/$projectId", params: { projectId } }) : void navigate({ to: "/" });

Expand Down Expand Up @@ -132,7 +133,7 @@ export function ShellTopbar() {
return (
<header className={cn("dashboard-app-header", isMac && "is-under-titlebar-nav")} style={dragStyle}>
<div className="session-topbar__lead">
{isSessionRoute && isOrchestrator ? (
{isOrchestrator ? (
<div className="topbar-project-pills-group">
<div className="topbar-project-line">
<span className="dashboard-app-header__project">{projectLabel}</span>
Expand All @@ -145,87 +146,78 @@ export function ShellTopbar() {
</span>
</div>
</div>
) : isSessionRoute ? (
) : (
<div className="session-topbar__identity">
<div className="session-topbar__branch">
<GitBranch className="h-3 w-3 shrink-0" aria-hidden="true" />
<span className="truncate">{session?.branch || `session/${session?.id ?? ""}`}</span>
</div>
{session ? <SessionStatusPill session={session} /> : null}
</div>
) : isProjectBoardRoute ? null : (
<div className="topbar-project-line">
<span className="dashboard-app-header__project">{projectLabel}</span>
</div>
)}
</div>

<div className="dashboard-app-header__spacer" />

<div className="dashboard-app-header__actions">
<NotificationCenter style={noDragStyle} />
{isSessionRoute ? (
{isOrchestrator ? (
<>
{isOrchestrator ? (
<>
<button
aria-label="New task"
className="dashboard-app-header__primary-btn"
onClick={openNewTask}
style={noDragStyle}
type="button"
>
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
New task
</button>
<button
aria-label="Open Kanban"
className="dashboard-app-header__accent-btn"
onClick={openBoard}
style={noDragStyle}
type="button"
>
<LayoutDashboard className="h-3.5 w-3.5" aria-hidden="true" />
Kanban
</button>
</>
) : null}
{/* Kill control sits beside the orchestrator link for active workers —
moved here from the inspector's Summary "Danger zone". */}
{!isOrchestrator && session && sessionIsActive(session) ? <TopbarKillButton session={session} /> : null}
{!isOrchestrator && (
<button
aria-label="Open orchestrator"
className="dashboard-app-header__primary-btn dashboard-app-header__primary-btn--compact"
disabled={isSpawning}
onClick={() => void openOrchestrator()}
style={noDragStyle}
type="button"
>
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
{isSpawning ? "Spawning…" : "Orchestrator"}
</button>
)}
{/* Inspector collapse (worker sessions only — orchestrators have no rail). */}
{!isOrchestrator && (
<button
aria-label={isInspectorOpen ? "Close inspector panel" : "Open inspector panel"}
aria-pressed={isInspectorOpen}
className="dashboard-app-header__icon-btn"
onClick={toggleInspector}
style={noDragStyle}
title={`${isInspectorOpen ? "Close" : "Open"} inspector · ⌘⇧B`}
type="button"
>
{isInspectorOpen ? (
<PanelRightClose className="h-[15px] w-[15px]" aria-hidden="true" />
) : (
<PanelRightOpen className="h-[15px] w-[15px]" aria-hidden="true" />
)}
</button>
)}
<button
aria-label="New task"
className="dashboard-app-header__primary-btn"
onClick={openNewTask}
style={noDragStyle}
type="button"
>
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
New task
</button>
<button
aria-label="Open Kanban"
className="dashboard-app-header__accent-btn"
onClick={openBoard}
style={noDragStyle}
type="button"
>
<LayoutDashboard className="h-3.5 w-3.5" aria-hidden="true" />
Kanban
</button>
</>
) : null}
{/* Kill control sits beside the orchestrator link for active workers —
moved here from the inspector's Summary "Danger zone". */}
{!isOrchestrator && session && sessionIsActive(session) ? <TopbarKillButton session={session} /> : null}
{!isOrchestrator && (
<button
aria-label="Open orchestrator"
className="dashboard-app-header__primary-btn dashboard-app-header__primary-btn--compact"
disabled={isSpawning}
onClick={() => void openOrchestrator()}
style={noDragStyle}
type="button"
>
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
{isSpawning ? "Spawning…" : "Orchestrator"}
</button>
)}
{/* Inspector collapse (worker sessions only — orchestrators have no rail). */}
{!isOrchestrator && (
<button
aria-label={isInspectorOpen ? "Close inspector panel" : "Open inspector panel"}
aria-pressed={isInspectorOpen}
className="dashboard-app-header__icon-btn"
onClick={toggleInspector}
style={noDragStyle}
title={`${isInspectorOpen ? "Close" : "Open"} inspector · ⌘⇧B`}
type="button"
>
{isInspectorOpen ? (
<PanelRightClose className="h-[15px] w-[15px]" aria-hidden="true" />
) : (
<PanelRightOpen className="h-[15px] w-[15px]" aria-hidden="true" />
)}
</button>
)}
</div>
<NewTaskDialog
open={isNewTaskOpen}
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/renderer/routes/_shell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { createFileRoute, Outlet, useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useQueryClient } from "@tanstack/react-query";
import { type CSSProperties, useCallback, useEffect } from "react";
import { ShellTopbar } from "../components/ShellTopbar";
Expand Down Expand Up @@ -40,11 +40,15 @@ function errorMessage(error: unknown) {
// instead of Zustand. The daemon-status effect runs here exactly once.
function ShellLayout() {
const navigate = useNavigate();
const matchRoute = useMatchRoute();
const queryClient = useQueryClient();
const workspaceQuery = useWorkspaceQuery();
const workspaces = workspaceQuery.data ?? [];
const daemonStatus = useDaemonStatus(queryClient);
const { theme, setTheme, isSidebarOpen, toggleSidebar } = useUiStore();
const isSessionRoute =
Boolean(matchRoute({ to: "/projects/$projectId/sessions/$sessionId", fuzzy: true })) ||
Boolean(matchRoute({ to: "/sessions/$sessionId", fuzzy: true }));

const updateWorkspaces = useCallback(
(updater: (workspaces: WorkspaceSummary[]) => WorkspaceSummary[]) => {
Expand Down Expand Up @@ -185,7 +189,7 @@ function ShellLayout() {
>
<Sidebar
daemonStatus={daemonStatus}
underTopbar
underTopbar={isSessionRoute}
onCreateProject={createProject}
onRemoveProject={removeProject}
workspaceError={workspaceQuery.isError ? errorMessage(workspaceQuery.error) : undefined}
Expand Down