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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/client/features/bookshelf/ui.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/client/ui/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
47 changes: 47 additions & 0 deletions src/client/ui/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('mermaid').default> | undefined;
let mermaidInitialized = false;
let mermaidId = 0;

const purify = DOMPurify(window);
purify.addHook('afterSanitizeAttributes', (node) => {
Expand Down Expand Up @@ -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<void> {
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+.*$/, '');
Expand Down