The office
Why the UI is a floor plan, and what the furniture actually encodes.
The office is not a skin over a dashboard. It is the application: the canvas fills the viewport, and every other surface — the info popout, the inspector, the brief dock, the full-page sheets — floats over it. This page is about what the floor plan encodes, because almost all of it is load-bearing.
The canvas is the application
The office is rendered unconditionally. There is exactly one WebGLRenderer, created in a mount effect whose only dependency is the store, and the top-level layout renders OfficeCanvas as a sibling of everything else rather than inside a tab switch. Selecting the Projects page or the Settings page mounts a page sheet over the office; it never unmounts it.
Three consequences follow, and they matter before you touch the code:
- Nothing is rebuilt on navigation. The camera keeps the orbit position you left it in, the avatars keep their animation phase, and the socket keeps its subscription. A page sheet is a view, not a route that reconstructs state.
- The office is always live. A run streaming while you read a transcript is the same office you were watching a moment ago — the same employees, the same statuses, the same seats. There is no second source of truth for "what is happening".
- The renderer owns its own object graph. Avatars, floors, module instances and material sets are created and disposed inside the imperative scene API. React pushes data in through it and never re-creates the scene.
One development caveat. React's StrictMode double-invokes effects, so the mount effect runs twice in development. The scene is built to survive that — the second run disposes and rebuilds — but an effect you add that allocates without a matching teardown will leak once in dev and not in production.
Seats and desks are names, not coordinates
The office model is a single GLB, loaded once, and its named empty nodes are the entire interface between the asset and the org chart. Nothing hard-codes a coordinate: an avatar is placed by looking its seatId up in the loaded scene and taking that node's world position. Three anchor prefixes exist, and the lookup recognises exactly these:
| Prefix | What it is | Used for |
|---|---|---|
Seat_ | Where a person stands or sits | Placing an avatar; Role.seatId holds one of these names |
Desk_ | The desk surface in front of a seat | Computing which way an avatar faces |
Anchor_Room_ | The centre of a room | Role.roomId; the label the UI shows for where someone works |
The seat-to-desk mapping is derived rather than stored: Desk_Dev_01 is found by swapping the Seat_ prefix for Desk_ on the seat name. Facing is then computed from the vector between the seat and its desk, so an avatar turns toward its own desk without anybody writing down an angle. Not every seat has a desk — the eight meeting chairs deliberately do not — so the desk probe falls through to the room anchor, and a failure to find either leaves the avatar facing forward rather than throwing.
Missing anchors do not crash the office. Every failed lookup is recorded and logged once, the employee is parked on a bench row beside the dev floor, and the overlay names them: a hand-edited org chart should give a visibly wrong but working office, not a blank viewport.
The real asset contract
The core office ships 21 seats, 13 desks and 7 room anchors, and those counts are asserted rather than eyeballed. The Blender script that builds the furniture counts the empties carrying each reserved prefix and exits with status 1 if the counts are wrong, printing === ANCHOR CONTRACT VIOLATED === and the specific mismatch. Its closing line reads anchor contract: 21 seats, 13 desks, 7 rooms. The real anchor names are:
- Seats (21):
Seat_CEO,Seat_Dev_01throughSeat_Dev_10,Seat_Office2,Seat_Office3, andSeat_Meeting_01throughSeat_Meeting_08. - Desks (13):
Desk_CEO,Desk_Dev_01throughDesk_Dev_10,Desk_Office2,Desk_Office3. - Rooms (7):
Anchor_Room_CEO,Anchor_Room_DevFloor,Anchor_Room_Lobby,Anchor_Room_Lounge,Anchor_Room_Meeting,Anchor_Room_Office2,Anchor_Room_Office3.
Thirteen desks and twenty-one seats is not an oversight. The eight meeting chairs have no desk of their own, which is exactly what the shipped asset's node counts say — so the arithmetic is a description of the room, not an aspiration about it. The org chart refers to people by these names, which means a rename in Blender is a rename of where somebody sits. That is why the names are a contract with an assertion behind it rather than a convention.
Colour is status
An avatar is built procedurally in the browser — a stylised humanoid of about a dozen boxes and one sphere — from three fields on Role.appearance:
| Field | What it does |
|---|---|
bodyColor | Hex colour of the torso. Falls back to #94a3b8 if absent. |
accentColor | Hex colour of the trim and headset. Falls back to #334155. |
height | A visual height multiplier, clamped to 0.8–1.3. The type's own doc comment claims 0.9–1.15 and is stale. |
The head colour is derived rather than authored: it is the body colour blended 55% toward a skin tone. The scale is applied to the whole group, so height changes the avatar's stature without changing any of the proportions.
Status is overlaid on that identity. The real union has seven values, and the labels and colours come from one table shared by the 3D avatars and the console's status dots — so a dot in the org chart and a body in the office always agree:
| Status | Label in the UI | Colour | Body language |
|---|---|---|---|
offline | offline | #64748b | Flat, body colour greyed 70% |
idle | idle | #38bdf8 | Gentle bob, slight head sway |
thinking | thinking | #a78bfa | Pulse, hand raised toward the chin |
working | working | #a3e635 | Typing arms, leaning in, tablet visible |
talking | in a meeting | #f0abfc | Turned, gesturing, arms spread |
blocked | needs approval | #fbbf24 | Amber pulse, arms down |
error | error | #f87171 | Red, still, leaning back |
Status is carried three ways at once: a floor ring when the employee is selected, an emissive tint (a chest lamp plus a wash over the body and head), and posture. That redundancy makes the office legible from a distance where a colour alone would not be, and it is why the emissive and body colour ease between states rather than snapping.
An awkward truth worth stating. The talking status exists, has a label ("in a meeting"), a colour and a full set of body language, and is never actually emitted by the server. No code path sets it. The status vocabulary anticipates a behaviour the engine does not yet have, and the console shows "in a meeting" for nobody. Similarly, queued and done are run statuses used for badge tones and filters — they are not employee statuses at all, and acting appears nowhere in the repository. If you are looking for a status that never appears, this is why.
A debate is speech, not a paragraph
A debate stage runs a fixed number of rounds, defaults to rounds = max(1, spec.rounds ?? 2), and each participating role speaks exactly once per round. Each of those turns emits a speech event of kind debate, attributed to the employee who produced it and addressed to the other participants, with the text clipped to 1,200 characters. After the rounds, roles[0] takes the facilitator's turn, rules on the disagreement, and emits a speech of kind report clipped to 2,000 characters. That verdict is the stage's summary and what the next stage receives.
The transcript genuinely carries one entry per participant per round, each hanging off an employee id, which makes "who argued for what, and who ruled on it" a question with an answer rather than a reconstruction from a blob of prose. The same shape appears in review-loop, where reviewers critique in parallel and the chair synthesises.
Nobody walks to the meeting room — but idle employees do walk. The old documentation said a debate "genuinely walks the participants to the meeting room". It does not: a debate arrives as individual speeches from named employees rather than a collapsed paragraph, and employee.moved is still emitted only by hire() and setSeat(), so a re-seat is a teleport rather than a route. What is new, and what an earlier revision of this page flatly denied, is a client-side locomotion layer: an employee whose status is idle gets up and walks a real route, computed on a grid sampled from the loaded floor geometry rather than from authored path data, and walks back to the desk the moment work arrives. It is a rendering behaviour rather than an office record — two consoles watching the same floor agree about the work and disagree about the strolls — and it stands down under prefers-reduced-motion.
A floor is a jigsaw, and it grows itself
A floor is not one room. It is the core office plus a ring of room modules bolted onto its walls, and the rule that makes it a jigsaw is a single one: a module attaches through a port — a doorway on a wall, with an outward normal — and the thing it attaches to must have a doorway facing back. Everything else follows from that.
The layout code is built to guarantee three properties, because a building that violates any of them looks broken in a way no amount of art fixes:
- Nothing overlaps. Every candidate placement is collision-tested against the core and against every module already placed. A port that would collide is spent, not squeezed — a room pushed into another room is worse than a room not built.
- Nothing escapes the ring. Growth walks outward from the core, so a floor stays a bounded, plausible building rather than a spiral that wanders off the plate.
- The answer is deterministic. Ports are considered in a fixed order and module kinds are tried least-used-first, which is what keeps the mix varied instead of producing a train of identical pods. The same floor with the same kit always grows the same way, so a 3D view never reshuffles itself between reloads.
Growth is triggered by the roster, not by a button. When an employee is hired and capacity is below what the roster needs, the orchestrator plans a floor of one more module and persists it; the same machinery runs at boot, because a roster can grow while the office is down and a kit can change between versions. The operator has a hand on it through addRoom and removeRoom — a meeting room nobody is hired into still has to be asked for — and a removal never takes a floor below what the roster needs. A layout referencing a module kind the current kit does not carry is filtered at load rather than left as a pointer at nothing.
Generated seats are namespaced by instance, as B1::Seat_POD4_02. The kit has one Seat_POD4_02 per pod-type module, so without the prefix two pods would claim the same seat and the second employee would sit in the first pod's chair. The prefix travels through the desk lookup too, so a seat in a grown module finds its desk in the same module rather than back in the core.
Multiple floors
Each organisation gets its own floor, and the floor number is a field on the workspace — with a 4.2-metre step between storeys, against office walls three metres tall. The camera, the plate colours and the walls are derived from that.
Every floor is built, not only the one you are looking at. The summaries the server sends carry each floor's grown layout — a floor you are not looking at still has to be drawn as a plate of the right shape, and it grows whether or not anyone is watching. Only the active floor shows its walls and module meshes; the others show a coloured plate, and the active floor's own plate is hidden because the real model already has a slab there and two coplanar surfaces z-fight.
Switching floors is a protocol operation rather than a UI trick. The console sends the selectWorkspace command and the server answers with a fresh office.updated carrying the whole context — people, skills, money, runs and floor — so the swap is one event rather than a sequence of panel reloads. There is no selectFloor command; the floor is the organisation. Switching orgs changes the camera with the floor, because the alternative is a camera pointed at a building that is no longer in front of it.
Generated, not curated
The awkward part, and it belongs on this page rather than in a footnote: the office is generated, and the composition is a first guess.
The desks, chairs, meeting table and every anchor are built by blender/scripts/03_office_furniture.py, and room modules by a companion block script. That script exists because the original furniture was authored in interactive Blender sessions that were never written back to a script — which was not a tidiness problem. Running the shell script and re-exporting silently replaced the furnished office with a bare room, taking all twenty-one seat anchors with it. It was recovered once from a build artifact and could easily not have been.
What that buys is reproducibility and a checked contract. What it does not buy is taste. The module placement is a least-used-first rule rather than art direction, the room mix is whatever the kit cycles through, and the result reads as a plausible office rather than a designed one — some furniture satisfied the anchor contract and reads oddly at certain camera angles. Treat the floor plan as infrastructure with a decent look, not as a rendering showcase.
If the office looks wrong
| Symptom | Likely cause | What to check |
|---|---|---|
| An employee is standing on the bench row, and the overlay names them | Their seatId is not an anchor in the loaded model, or they have no seat at all | The seat list in the org panel; the console logs the missing anchor name exactly once |
Nobody has a desk and the room count is core only | The block kit failed to load or the module kinds do not match | The floor panel's provenance line and its problem field, which says why there is no kit |
| The office is a flat plate with no walls on another floor | Expected — only the active floor renders walls | Which floor the selector says you are on |
| A floor is wider than the camera frames | It grew modules; the plate extent is recomputed from the placed instances | Reset the view, which re-frames the whole floor |
| The viewport reports a lost WebGL context | The GPU or driver dropped the context | The error state names it; reloading restores the office and the socket re-sends the whole state |
The other half of this page's territory is where the office is allowed to be — Projects, organisations and floors — and the model each employee is spending is Model routing.
Linked from
Did this page answer your question?