From d466e47f573755edd00e2fa938ab0ff9be8a5fdd Mon Sep 17 00:00:00 2001 From: LawrenceYoong Date: Sat, 7 Nov 2026 13:06:44 +0800 Subject: [PATCH 1/2] Render Mermaid diagrams in the docs bookshelf --- README.md | 2 +- docs/features.md | 2 +- package-lock.json | 1 + package.json | 1 + src/client/features/bookshelf/ui.ts | 3 +- src/client/ui/markdown.css | 3 ++ src/client/ui/markdown.ts | 47 +++++++++++++++++++++++++++++ 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fac9441a5..2b7a4d997 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i - **Other maps.** Turn the whole building into a castle: sit on a throne of iron blades while your workers line up before you when they're done, send new ones off through the Hand of the King, and watch their beards grow long and grey as they toil. Send one home and the Kingsguard runs up from the dungeon, marches it down the stairs and throws it in a cell, where it starves, dies and rots down to a skeleton. Or into a space station in orbit, the Earth turning outside its windows: you run it from the captain's chair on the bridge, and a worker sent home is marched to the airlock and blown out into space, to drift off past the observation windows with everyone who went before it. Or make a map of your own, with its own way of seeing workers off in JSON ([docs/maps.md](docs/maps.md)). -There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). +There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). The bookshelf also renders Mermaid diagrams in project Markdown docs. ## Requirements diff --git a/docs/features.md b/docs/features.md index f320d9b99..72f66155a 100644 --- a/docs/features.md +++ b/docs/features.md @@ -58,7 +58,7 @@ Everything in the office, room by room. Back to the [README](../README.md). - **Services board.** When a worker starts a web server (`npm run dev`, a preview build, `python -m http.server`), it appears within a few seconds on the **🌐 Services** board, which hangs on the wall by the lounge and is also in the **☰** menu. Each entry shows the worker, its branch and the page's title. Click a row to copy one command that opens that server on your own computer. Or run `agent-office tunnel` on your computer once and leave it running: every server a worker starts opens there on the same port, by itself, and closes when the worker stops it ([Workers' servers on your own computer](tunnel.md)). Hire a worker with its own worktree, ask it to run the dev server, and your designer can review the branch in their own browser. - **Pictures on the walls.** Press **F** (or **🖼️ Hang a picture** in the **☰** menu) and paste a link to any image online: a team photo, a diagram, a meme. Pick one of six frames, then aim at a wall and click to hang it. Scroll to size it first. Where it can't go, over a window or a board, the preview turns red. Everyone sees it right away, and it stays up across restarts. Look at a picture and press **E** for a closer look, or to move, edit or take it down. - **A whiteboard to draw on together.** A whiteboard on wheels stands out on the floor between the desks and the lounge. Press **E** at it (or **📝 Whiteboard** in the **☰** menu) and [Excalidraw](https://excalidraw.com) opens, with shapes, arrows, freehand, text and pasted pictures. Everyone on the floor who opens it draws on the same board at once: you see their strokes as they draw, their cursors with their names, and what they have selected. The top of the window shows who's drawing, and so does the hint at the board. Whatever is drawn shows on the whiteboard in the office for everyone walking past, and stays there across restarts. Each floor has its own. Press Esc once to finish what you're doing and again to close it. -- **A bookshelf of the project's docs.** A bookshelf stands against the south wall, between the balcony doors and the window. Press **E** at it to read every Markdown file in the project: the README, the docs folder, each package's notes. Type in the filter box to narrow the list down. The letters only need to be in order, so `gs` finds `getting-started.md`, and each extra word narrows it again. Use **↑ ↓** and **Enter** to open one, and **Page Up/Down** to read on without leaving the box. Docs look the way GitHub shows them, with tables, code, callouts, task lists and the project's own pictures. A link to another doc opens it right there, **☰ Contents** jumps to any heading, and the shelf reopens on the doc you last read. While you read, your character holds an open book, turning a page now and then. Everyone else sees it, along with what you're reading under your name tag. A page turns with a soft swish when you open a doc or scroll a screenful. Turn the swish off with the 🔈 at the top of the bookshelf, or under **⚙️ → Sound & voice**. +- **A bookshelf of the project's docs.** A bookshelf stands against the south wall, between the balcony doors and the window. Press **E** at it to read every Markdown file in the project: the README, the docs folder, each package's notes. Type in the filter box to narrow the list down. The letters only need to be in order, so `gs` finds `getting-started.md`, and each extra word narrows it again. Use **↑ ↓** and **Enter** to open one, and **Page Up/Down** to read on without leaving the box. Docs look the way GitHub shows them, with tables, code, callouts, task lists, the project's own pictures, and rendered Mermaid diagrams. Mermaid stays optional and is loaded only for a document that contains a Mermaid code block; invalid diagrams keep their source visible. A link to another doc opens it right there, **☰ Contents** jumps to any heading, and the shelf reopens on the doc you last read. While you read, your character holds an open book, turning a page now and then. Everyone else sees it, along with what you're reading under your name tag. A page turns with a soft swish when you open a doc or scroll a screenful. Turn the swish off with the 🔈 at the top of the bookshelf, or under **⚙️ → Sound & voice**. - **An office dog.** Every floor has a dog. It naps under the desks of workers who are busy, trots after people for a while, sniffs around and hangs out on the lounge rug. When a worker needs input, it drops everything, runs to that desk and barks until someone opens that worker's terminal, so you can see from across the room who's waiting. It stays there until the worker is answered. Walk up and press **E** to pet it, and everyone sees it wag. Name it under **⚙️** → *Office dog*, and pick its breed (a pup, a corgi, a dachshund, a pug, a shiba or a pomeranian) and its coat there too: they're for everyone on that floor. Until someone picks them, each floor's dog is a breed and coat of its own. - **Sent home with a box.** A worker you send home (**X** at its desk) doesn't just vanish. Its light goes out, its face falls, and its things go into a cardboard box: a plant, a framed photo, its mug and a rubber duck. Its laptop snaps shut, and it hops off its chair and trudges out with the box in its arms, out the exit door, down the steps and off along the sidewalk. Floors above the bottom one have no exit door, so there it walks out onto the balcony instead, climbs up on the railing and jumps: its parachute pops open and it circles down onto the lot out front, box and all, then walks off along the sidewalk. The desk is free straight away, so you can hire someone new while the last one is still on the way out. In the castle it goes to the dungeon instead and on the space station out of the airlock, and a map of your own can script its own way of seeing workers off ([Sending workers home](maps.md#sending-workers-home)). - **Voice.** Browser-to-browser WebRTC voice. Volume depends on how close you stand, but people are never fully silent. **V** joins, and once you're in, **V** is push to talk: hold it to talk, and you're muted when you let go. **M** mutes and unmutes. Pick **Push to talk** under **⚙️** to join muted, or keep an open mic. Leave voice from the **☰** menu. diff --git a/package-lock.json b/package-lock.json index e3f643997..7c689d8db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "concurrently": "^10.0.5", "dompurify": "^3.4.16", "marked": "^18.0.14", + "mermaid": "^11.17.2", "playwright-core": "^1.63.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/package.json b/package.json index 66e7bd2e9..fb563d82b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "concurrently": "^10.0.5", "dompurify": "^3.4.16", "marked": "^18.0.14", + "mermaid": "^11.17.2", "playwright-core": "^1.63.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/client/features/bookshelf/ui.ts b/src/client/features/bookshelf/ui.ts index 091bebec1..10897ce6b 100644 --- a/src/client/features/bookshelf/ui.ts +++ b/src/client/features/bookshelf/ui.ts @@ -1,7 +1,7 @@ import './ui.css'; import { isDocPath, resolveDocLink, type DocFile, type DocList, type DocText } from '../../../shared/docs'; import { clip, h, openModal, setDoing, timeAgo, toast } from '../../ui/dom'; -import { markdownFile } from '../../ui/markdown'; +import { markdownFile, renderMermaidDiagrams } from '../../ui/markdown'; // The bookshelf: every Markdown file in the floor's project, to read without leaving the office. // The filter box over the list picks docs out as you type (the letters in order, not necessarily @@ -322,6 +322,7 @@ export function openBookshelf(deps: ShelfDeps) { const body = doc.text.trim() ? markdownFile(doc.text) : h('div.md', {}, h('p.none', {}, 'This file is empty.')); wire(body, path); page.replaceChildren(body); + void renderMermaidDiagrams(body); const dir = path.slice(0, path.length - nameOf(path).length); crumbs.replaceChildren(dir ? h('span.dir', {}, dir) : '', nameOf(path)); crumbs.title = path; diff --git a/src/client/ui/markdown.css b/src/client/ui/markdown.css index e9bfc34e6..6d1f1d5dd 100644 --- a/src/client/ui/markdown.css +++ b/src/client/ui/markdown.css @@ -15,6 +15,9 @@ .md code { font: 12.5px ui-monospace, 'SF Mono', Menlo, Consolas, monospace; background: #f3ebe0; border-radius: 6px; padding: 1px 5px; } .md pre { background: #1e1f2e; color: #e9ecef; border: 2px solid var(--ink); border-radius: 10px; padding: 12px 14px; overflow: auto; } .md pre code { background: none; padding: 0; color: inherit; font-size: 12.5px; line-height: 1.5; } +.md .mermaid-diagram { margin: 0 0 12px; padding: 12px; overflow: auto; border: 1.5px solid #ddd0bf; border-radius: 10px; background: #fffaf3; } +.md .mermaid-diagram svg { display: block; width: max-content; min-width: 100%; height: auto; margin: 0 auto; } +.md .mermaid-error-note { margin: 0 0 6px; color: var(--muted); font-size: 12px; font-style: italic; } .md blockquote { margin-left: 0; margin-right: 0; padding: 2px 14px; border-left: 4px solid #d9cbb8; color: var(--muted); } .md ul, .md ol { padding-left: 26px; } .md li + li { margin-top: 3px; } diff --git a/src/client/ui/markdown.ts b/src/client/ui/markdown.ts index c8737e225..f8847ab56 100644 --- a/src/client/ui/markdown.ts +++ b/src/client/ui/markdown.ts @@ -9,6 +9,9 @@ import { h } from './dom'; // In issue and PR comments GitHub turns a single newline into a line break, unlike in .md files. const md = new Marked({ gfm: true, breaks: true }); const mdFile = new Marked({ gfm: true }); +let mermaid: Promise | undefined; +let mermaidInitialized = false; +let mermaidId = 0; const purify = DOMPurify(window); purify.addHook('afterSanitizeAttributes', (node) => { @@ -117,6 +120,50 @@ export function markdownFile(src: string): HTMLElement { return el; } +/** Render fenced Mermaid blocks in project docs; ordinary Markdown stays synchronous and lightweight. */ +export async function renderMermaidDiagrams(root: HTMLElement): Promise { + const blocks = [...root.querySelectorAll('pre > code.language-mermaid')]; + if (!blocks.length) return; + + try { + const load = (mermaid ??= import('mermaid').then((module) => module.default)); + const renderer = await load; + if (!mermaidInitialized) { + renderer.initialize({ + startOnLoad: false, + securityLevel: 'strict', + theme: 'default', + htmlLabels: false, + maxTextSize: 100_000, + maxEdges: 500, + suppressErrorRendering: true, + }); + mermaidInitialized = true; + } + + for (const code of blocks) { + const pre = code.parentElement; + if (!pre) continue; + try { + const { svg } = await renderer.render(`agent-office-mermaid-${++mermaidId}`, code.textContent ?? ''); + const figure = h('figure.mermaid-diagram', { role: 'img', 'aria-label': 'Mermaid diagram' }); + figure.append(purify.sanitize(svg, { + RETURN_DOM_FRAGMENT: true, + USE_PROFILES: { svg: true, svgFilters: true }, + FORBID_TAGS: ['script', 'foreignObject'], + FORBID_ATTR: ['onload', 'onclick'], + })); + pre.replaceWith(figure); + } catch { + pre.classList.add('mermaid-error'); + pre.before(h('p.mermaid-error-note', {}, 'This diagram could not be rendered. Its source is shown below.')); + } + } + } catch { + // If Mermaid cannot load, the sanitized fenced source remains readable in the document. + } +} + /** https://github.com/owner/repo from an issue or PR URL. */ export function repoUrlOf(itemUrl: string): string { return itemUrl.replace(/\/(pull|issues)\/\d+.*$/, ''); From 585561c233f1b215bb0d852c8e8fb9a818a3b249 Mon Sep 17 00:00:00 2001 From: LawrenceYoong Date: Sat, 7 Nov 2026 15:26:42 +0800 Subject: [PATCH 2/2] Add project docs shortcut and system overview --- README.md | 4 +-- docs/features.md | 2 +- docs/system-overview.md | 51 ++++++++++++++++++++++++++++++++ src/client/features/hud/index.ts | 4 +-- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 docs/system-overview.md diff --git a/README.md b/README.md index 2b7a4d997..e728b6a02 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ and jump into any of them together. Every GitHub repo is a floor of the building [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat-square)](#run-locally) [![Built with TypeScript](https://img.shields.io/badge/built%20with-TypeScript-3178c6?style=flat-square)](https://www.typescriptlang.org) -[**Run locally**](#run-locally) · [**Deploy to AWS**](#deploy-to-aws-ec2) · [**Azure**](#deploy-to-azure) · [**Railway**](#deploy-to-railway) · [**Fly.io**](#deploy-to-flyio) · [**Dokploy**](#deploy-to-dokploy) · [**Coolify**](#deploy-to-coolify) · [**Any server**](#deploy-to-any-ubuntu-or-debian-server) · [**Add users**](#add-users) · [**Controls**](#controls) · [**Features**](docs/features.md) · [**How it works**](docs/how-it-works.md) +[**Run locally**](#run-locally) · [**Deploy to AWS**](#deploy-to-aws-ec2) · [**Azure**](#deploy-to-azure) · [**Railway**](#deploy-to-railway) · [**Fly.io**](#deploy-to-flyio) · [**Dokploy**](#deploy-to-dokploy) · [**Coolify**](#deploy-to-coolify) · [**Any server**](#deploy-to-any-ubuntu-or-debian-server) · [**Add users**](#add-users) · [**Controls**](#controls) · [**Features**](docs/features.md) · [**System overview**](docs/system-overview.md) · [**How it works**](docs/how-it-works.md) ```sh curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/install.sh | bash @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i - **Other maps.** Turn the whole building into a castle: sit on a throne of iron blades while your workers line up before you when they're done, send new ones off through the Hand of the King, and watch their beards grow long and grey as they toil. Send one home and the Kingsguard runs up from the dungeon, marches it down the stairs and throws it in a cell, where it starves, dies and rots down to a skeleton. Or into a space station in orbit, the Earth turning outside its windows: you run it from the captain's chair on the bridge, and a worker sent home is marched to the airlock and blown out into space, to drift off past the observation windows with everyone who went before it. Or make a map of your own, with its own way of seeing workers off in JSON ([docs/maps.md](docs/maps.md)). -There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). The bookshelf also renders Mermaid diagrams in project Markdown docs. +There's a lot more (a rooftop bar, an office dog, an arcade, supercars in the garage to drive round a scenic loop past a farm, pines, mountains and a beach): see [docs/features.md](docs/features.md). Open **☰ → 📚 Docs** from anywhere to read the current project's docs without walking to the bookshelf. The bookshelf renders Mermaid diagrams in Markdown; the [system overview](docs/system-overview.md) is the quick flow map for this project. ## Requirements diff --git a/docs/features.md b/docs/features.md index 72f66155a..8506b8402 100644 --- a/docs/features.md +++ b/docs/features.md @@ -4,7 +4,7 @@ Everything in the office, room by room. Back to the [README](../README.md). - **A floor per project.** The first time the office starts in a terminal it walks you through picking your first project (see [Run locally](../README.md#run-locally)). Otherwise you start inside the elevator, and it asks for one: pick one of the repositories your `gh` login can see (or type `owner/name`) and the office clones it into the workspace folder, `~/agent-office//` unless you pick another (**📁 Change folder** right there, for admins). While it clones, its floor on the panel says how far along it is (*Downloading 64% · 231 MiB · 1.5 MiB/s*), and you can close the panel and carry on: everyone hears when the floor opens. Whoever added it, or an admin, can stop it with the **⏹️** beside it. A clone that hears nothing from GitHub for 3 minutes is stopped as stalled, one that would have to ask something (an ssh host key, a password) fails straight away and says what to run on the office's machine, and restarting the office doesn't stop one. To add another project, or go to one, walk into the elevator on the north wall and press **E**. Or click the project name in the top-left corner for the list of floors: pick one and you're there in a blink, standing on the same spot in the office. From outside it (the balcony, the street, the garage) it takes you in, and you step out of that floor's elevator. Each floor has its own desks, workers, issues and PR boards, task queue, services and pictures, and its own wall and floor colors, so you always know where you are. You only see and hear the people on your floor. The elevator panel shows how many workers are busy or waiting on someone on each floor, and you get a heads-up when a worker on another floor starts waiting. An admin can take a project off the building with the **🗑** beside its floor in the elevator: its workers stop, and everyone on it rides the elevator to another floor. Nothing is deleted. The checkout stays on disk with its workers, queue and pictures, and adding the same repository again moves back into it (as long as the workspace folder hasn't moved since). That goes for the project the office was started in (`agent-office `) too: it stays off after a restart, and the office keeps its own data in that folder as before. - **Walk around.** Use WASD, Space to jump, and drag the mouse to orbit the camera. Everyone in the office sees everyone else move in real time. -- **A quiet screen.** The office stays in view: the top bar holds just the floor you're on, **🤖 Workers** and the **☰** menu (or **Tab**). The menu opens every window (issues, PRs, the queue, services, the whiteboard, search, voice, screen sharing, settings) and lists what can show on screen: the workers, who's in the office, spend, Claude limits, the chat and the floor details. Turn each one on or off there, or hide a panel with its **✕**. Pin anything you use a lot to the top bar. The choices are kept in your browser. Chat lines fade after a few seconds; hover the chat or press **T** to read back. What needs you shows up on the top bar by itself: the workers waiting on someone (click to go to them, like **N**), the mute button while you're in voice, **Sharing** while you share your screen, and **⬆️ Update** when there's a new version. +- **A quiet screen.** The office stays in view: the top bar holds just the floor you're on, **🤖 Workers** and the **☰** menu (or **Tab**). The menu opens every window (issues, PRs, the queue, services, the whiteboard, search, voice, screen sharing, settings) and lists what can show on screen: the workers, who's in the office, spend, Claude limits, the chat and the floor details. **☰ → 📚 Docs** opens the current project's Markdown docs from anywhere, including the office map, without walking to the bookshelf. The bookshelf renders Mermaid diagrams; see the [system overview](system-overview.md) for this project's system flow map. Turn each panel on or off there, or hide it with its **✕**. Pin anything you use a lot to the top bar. The choices are kept in your browser. Chat lines fade after a few seconds; hover the chat or press **T** to read back. What needs you shows up on the top bar by itself: the workers waiting on someone (click to go to them, like **N**), the mute button while you're in voice, **Sharing** while you share your screen, and **⬆️ Update** when there's a new version. - **Coffee.** Press **E** at the coffee machine in the kitchen for a mug and a minute of quicker walking and higher jumps. A little meter under the project name shows how much buzz is left. Drink a third cup before the last one wears off and you get the jitters for a few seconds. - **Boss office.** Stairs along the back wall climb to a glass-walled office on the loft in the corner. From up there you can look down over every desk and watch your workers go. - **Ladder and fire poles.** The floors are stacked like a real building, with a ceiling over each. A ladder up the west wall goes through trapdoors in the ceiling and the floor: press **E** to grab it, **W** / **S** to climb, and **E** (or **Space**) to get off. Climb past the ceiling and you come up through the floor of the floor above. To go down in a hurry, walk into the hole round the fire pole (or press **E** at it). You slide down it spinning and come out of the ceiling below. The pole runs the whole way down the building, so a slide takes you one floor down and swings you off beside its hole there, ready to go again. On the bottom floor you land on a mat and a firehouse bell rings, and pressing **E** at the pole there gives you a twirl. diff --git a/docs/system-overview.md b/docs/system-overview.md new file mode 100644 index 000000000..526141022 --- /dev/null +++ b/docs/system-overview.md @@ -0,0 +1,51 @@ +# System overview + +Back to the [README](../README.md). This is the quick map of how a person, a project floor and its workers fit together. For implementation detail, see [How it works](how-it-works.md) and [Code layout](code-layout.md). + +```mermaid +flowchart LR + person[Teammate] -->|browser: HTTPS + WebSocket| client[Client
HUD, 3D world, windows] + client <-->|messages and state| office[Agent Office server
Node.js] + + office --> floors[Floor manager] + floors -->|one floor per project| project[Project checkout
boards, queue, docs, settings] + project -->|GitHub CLI| github[GitHub
issues, pull requests, branches] + + office --> manager[Worker manager] + manager -->|PTY or ACP session| agent[Agent CLI
Claude, Codex, OpenCode, etc.] + agent -->|reads and changes files| project + agent -->|hooks / worker tools| hooks[Loopback hook server
status, tasks, worker actions] + hooks --> manager + + office --> storage[Office data
accounts, floors, chat, settings] + manager --> workerdata[Project data
sessions, queue, worktrees] + workerdata --> project + + client -->|live presence, chat, voice signaling| peers[Other teammates] + office -->|WebRTC signaling| peers +``` + +## Typical task flow + +```mermaid +sequenceDiagram + actor Teammate + participant Browser as Browser client + participant Office as Agent Office server + participant Floor as Project floor + participant Worker as Agent worker + participant GitHub + + Teammate->>Browser: Choose a project and assign work + Browser->>Office: Send task over WebSocket + Office->>Floor: Create worker and optional worktree + Floor->>Worker: Start agent CLI in project directory + Worker->>Floor: Read and edit project files + Worker-->>Office: Report status through hooks or ACP + Office-->>Browser: Stream status and terminal updates + Worker->>GitHub: Push branch and open pull request + GitHub-->>Floor: Pull request data is refreshed + Floor-->>Browser: Show the PR on the board and worker desk +``` + +The project agent should keep this page aligned with the actual system when architecture changes. Mermaid source stays ordinary Markdown, so it can also be reviewed and edited in GitHub. diff --git a/src/client/features/hud/index.ts b/src/client/features/hud/index.ts index a54730173..f8159996b 100644 --- a/src/client/features/hud/index.ts +++ b/src/client/features/hud/index.ts @@ -66,8 +66,8 @@ export function installHud(ctx: Ctx, core: CoreState, parts: HudParts) { run: () => parts.meeting.showMeeting(), }, { id: 'search', icon: '🔎', label: 'Search', section: 'Open', key: '/', title: () => 'Search the chat and every terminal', run: waiting.showSearch }, - // The office has its bookshelf for them; a map of its own may not. - { id: 'docs', icon: '📚', label: 'Docs', section: 'Open', shown: () => !inOffice(), title: () => 'Read the project’s docs', run: parts.bookshelf.showBookshelf }, + // Keep project docs one click away from the office map too; the bookshelf reads the current floor. + { id: 'docs', icon: '📚', label: 'Docs', section: 'Open', title: () => 'Read the current project’s docs', run: parts.bookshelf.showBookshelf }, { id: 'elevator', icon: '🛗', label: () => (inOffice() ? 'Elevator' : 'Floors'), section: 'Open', count: () => store.floors.reduce((n, f) => n + (f.id === store.floor ? 0 : f.waiting), 0), title: () => (inOffice() ? 'Ride to another project' : 'Go to another project, or add one'), run: travel.showElevator }, { id: 'roof', icon: '🍸', label: 'Rooftop bar', section: 'Open', shown: () => !core.upTop && inOffice() && builtFloors().length > 0, title: () => 'Ride the elevator up to the roof: a DJ, drinks and the city', run: () => travel.ride(ROOF) }, // In voice, V is push to talk, so leaving is only from here.