From 07fa105bf3c770d1c12b079257841a6ede1f7598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 30 Jun 2026 21:33:42 -0400 Subject: [PATCH] Add virtual office sidebar showing real-time agent status A collapsible right sidebar renders agent presence as a visual office floor plan grouped by department. Each agent shows at their desk with a status ring (active/thinking/busy/idle) and a tooltip revealing their current task on hover. Toggle via the Office button in the left nav. --- .../src/renderer/src/shared/Shell.tsx | 25 +++ .../src/renderer/src/shared/VirtualOffice.tsx | 204 ++++++++++++++++++ AgentCompany-New/src/renderer/src/styles.css | 19 ++ 3 files changed, 248 insertions(+) create mode 100644 AgentCompany-New/src/renderer/src/shared/VirtualOffice.tsx diff --git a/AgentCompany-New/src/renderer/src/shared/Shell.tsx b/AgentCompany-New/src/renderer/src/shared/Shell.tsx index 0d717105..b3c510d2 100644 --- a/AgentCompany-New/src/renderer/src/shared/Shell.tsx +++ b/AgentCompany-New/src/renderer/src/shared/Shell.tsx @@ -5,6 +5,7 @@ import type { IconName } from "./Icon"; import { OrgSwitcher } from "./OrgSwitcher"; import { SearchDialog } from "./SearchDialog"; import { TopbarSlotContext } from "./topbar-context"; +import { VirtualOffice } from "./VirtualOffice"; export type RouteId = | "home" @@ -73,6 +74,7 @@ export function Shell({ children: ReactNode; }) { const [searchOpen, setSearchOpen] = useState(false); + const [officeOpen, setOfficeOpen] = useState(false); const [actionsSlot, setActionsSlot] = useState(null); useEffect(() => { const handler = (event: KeyboardEvent) => { @@ -122,6 +124,15 @@ export function Shell({
+ {officeOpen && ( + + )} a.orgId === orgId); + return orgAgents.map((agent) => { + const p = presence.find((r) => r.principalType === "agent" && r.principalId === agent.id); + const activeTask = tasks.find( + (t) => t.orgId === orgId && t.assigneeAgentId === agent.id && t.status === "in_progress", + ); + const status = mapStatus(p?.status); + return { + agent, + status, + statusText: p?.statusText ?? "", + currentTask: activeTask?.title ?? null, + }; + }); +} + +function mapStatus(raw: string | undefined): PresenceStatus { + if (!raw) return "idle"; + if (raw === "thinking") return "thinking"; + if (raw === "waiting_approval" || raw === "busy") return "busy"; + if (raw === "active" || raw === "online") return "active"; + return "idle"; +} + +const STATUS_RING: Record = { + active: "var(--green)", + thinking: "var(--accent)", + busy: "var(--amber)", + idle: "var(--text-4)", +}; + +const DEPT_FLOOR: Record = { + engineering: { label: "Engineering", color: "var(--accent-soft)" }, + product: { label: "Product", color: "rgba(168, 130, 255, 0.08)" }, + design: { label: "Design", color: "rgba(255, 170, 130, 0.08)" }, + marketing: { label: "Marketing", color: "rgba(130, 200, 255, 0.08)" }, + operations: { label: "Operations", color: "rgba(180, 180, 180, 0.06)" }, +}; + +function floorForDept(dept: string): { label: string; color: string } { + const key = dept.toLowerCase(); + return DEPT_FLOOR[key] ?? { label: dept, color: "rgba(180, 180, 180, 0.06)" }; +} + +export function VirtualOffice(props: VirtualOfficeProps) { + const desks = resolveDesks(props); + const [hoveredId, setHoveredId] = useState(null); + + const departments = new Map(); + for (const desk of desks) { + const dept = desk.agent.department; + if (!departments.has(dept)) departments.set(dept, []); + departments.get(dept)!.push(desk); + } + + if (desks.length === 0) { + return
No agents yet
; + } + + return ( +
+ {[...departments.entries()].map(([dept, deptDesks]) => { + const floor = floorForDept(dept); + return ( +
+
+ {floor.label} +
+
+ {deptDesks.map((desk) => ( + + ))} +
+
+ ); + })} +
+ ); +} + +function DeskCell({ + desk, + hovered, + onHover, +}: { + desk: AgentDesk; + hovered: boolean; + onHover: (id: string | null) => void; +}) { + const ring = STATUS_RING[desk.status]; + const isWorking = desk.status === "active" || desk.status === "thinking"; + return ( +
onHover(desk.agent.id)} + onMouseLeave={() => onHover(null)} + > +
+ +
+
+
+ {desk.agent.name.split(" ")[0]} +
+ {hovered && desk.currentTask && ( +
+ {desk.currentTask} +
+ )} +
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/styles.css b/AgentCompany-New/src/renderer/src/styles.css index e3dc071f..a4a0922d 100644 --- a/AgentCompany-New/src/renderer/src/styles.css +++ b/AgentCompany-New/src/renderer/src/styles.css @@ -369,6 +369,25 @@ a { color: inherit; } white-space: nowrap; } +.office-sidebar { + width: 220px; + flex-shrink: 0; + display: flex; + flex-direction: column; + border-left: 1px solid var(--border-soft); + background: var(--bg); + overflow: hidden; +} +.office-sidebar-header { + height: 48px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--sp-3); + border-bottom: 1px solid var(--border-soft); +} + .main { min-width: 0; flex: 1;