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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ deploy/coolify.sh ssh 'node /opt/agent-office/bin/agent-office.js accounts invit
| P | Give a task to a new worker, or to the one at this desk |
| C | See a worker's changes: diff, commit, open a PR |
| N | Go to the next worker that's waiting on you |
| Shift + 1–9 | Go straight to that floor (numbered as on the elevator's panel) |
| X | Send a worker home |
| L | Hang a sign over a desk ("Operations", "Code cleanup") |
| T / Enter | Chat |
Expand Down
1 change: 1 addition & 0 deletions docs/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Back to the [README](../README.md).
| H | These controls; in a car, honk the horn |
| T / Enter | Chat |
| G / 1–6 | Emote: hold G for the wheel (point and let go) or press 1–6 to wave, give a thumbs up, clap, dance, point or facepalm; everyone on your floor sees it |
| Shift + 1–9 | Go straight to that floor (numbered from the bottom up, as on the elevator's panel), standing in the same spot there; from outside, the elevator takes you in |
| / | Search the chat and every terminal on your floor |
| Ctrl + K (⌘K on a Mac) | Command palette: find a worker, issue, PR, service, board, teammate or action; Enter opens it, Shift+Enter walks you there first |
| V | Join voice; in voice, hold to talk (you're muted when you let go) |
Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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/<owner>/<repo>` 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 <dir>`) too: it stays off after a restart, and the office keeps its own data in that folder as before.
- **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/<owner>/<repo>` 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. **Shift** and a floor's number (**Shift + 2** for floor 2, as the elevator's panel numbers them) does the same from anywhere, without the list. 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 <dir>`) 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.
- **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.
Expand Down
3 changes: 2 additions & 1 deletion src/client/features/emotes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export function installEmotes(ctx: Ctx, deps: EmotesDeps) {
return true;
}
const n = /^(?:Digit|Numpad)([1-6])$/.exec(e.code);
if (!n) return false;
// With Shift, the digits go to another floor (see features/floorkeys).
if (!n || e.shiftKey) return false;
emoteWheel.close();
emote(EMOTES[Number(n[1]) - 1].id);
return true;
Expand Down
33 changes: 33 additions & 0 deletions src/client/features/floorkeys/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Shift+1 to Shift+9: straight to that floor, numbered from the bottom up as the elevator's panel
* numbers them, the way the floor list takes you there. Shift, because the digits on their own are the
* emotes, and Ctrl or Alt with a digit is the browser's (switching tabs, on Windows and Linux). By the
* key's place on the keyboard, so it's the same keys on any layout.
*/
import type { Ctx } from '../../core/context';
import { store } from '../../state';
import { clip, toast } from '../../ui/dom';

export interface FloorKeysDeps {
/** Straight to another floor (see switchFloor in core/travel.ts). */
switchFloor(floorId: string): void;
}

const DIGITS = ['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9'];

/** Registers Shift+1 to Shift+9. */
export function installFloorKeys(ctx: Ctx, deps: FloorKeysDeps) {
ctx.keys.bind({
code: DIGITS,
repeat: false,
run: (e) => {
// The digit on its own (7 to 9; 1 to 6 are the emotes') isn't one of these.
if (!e.shiftKey) return false;
const n = DIGITS.indexOf(e.code) + 1;
const floor = store.floors[n - 1];
if (!floor) return void toast(`There's no floor ${n}: the building has ${store.floors.length}`, 'warn');
if (floor.cloning) return void toast(`${clip(floor.name, 32)} is still being cloned`, 'warn');
deps.switchFloor(floor.id);
},
});
}
2 changes: 2 additions & 0 deletions src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { installCoffee } from './features/coffee';
import { installDictation } from './features/dictation';
import { installDog } from './features/dog';
import { installEmotes } from './features/emotes';
import { installFloorKeys } from './features/floorkeys';
import { installGolf } from './features/golf';
import { installGong } from './features/gong';
import { installGallery, installHanging } from './features/hanging';
Expand Down Expand Up @@ -178,6 +179,7 @@ parts.pointer = installPointer(ctx, core, parts);
installChat(ctx);
parts.talk = installVoice(ctx, { tv: parts.tv });
installDictation(ctx);
installFloorKeys(ctx, { switchFloor: (floorId) => parts.travel.switchFloor(floorId) });
parts.hud = installHud(ctx, core, parts);

// ---- Main loop ---------------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/client/ui/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const HELP_ROWS: readonly (readonly [string, string])[] = [
['O', 'Open a pull request for a worker on its own branch, or see the one it has'],
['T', 'Chat'],
['G / 1–6', 'Emote: hold G, point at one and let go (or tap G and click one), or press 1–6: wave, thumbs up, clap, dance, point, facepalm. Everyone on your floor sees it'],
['Shift+1–9', 'Go straight to that floor, numbered from the bottom up as on the elevator’s panel, standing in the same spot there'],
['/', 'Search the chat and every terminal on your floor, back to before the office last restarted'],
[IS_MAC ? '⌘K' : 'Ctrl+K', 'Command palette: type a few letters to find a worker, issue, PR, service, board, teammate or action. Enter opens it, Shift+Enter walks you over to it first'],
['V', 'Join voice. In voice, hold V to talk (push to talk): you’re muted once you let go. Leave voice from the ☰ menu'],
Expand Down
23 changes: 23 additions & 0 deletions tests/floorkeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import type { Ctx } from '../src/client/core/context.js';
import { Keys } from '../src/client/core/registry.js';
import { installFloorKeys } from '../src/client/features/floorkeys/index.js';
import type { FloorInfo } from '../src/shared/protocol.js';
import { store } from '../src/client/state/index.js';

const floor = (id: string) => ({ id, name: id, waiting: 0, people: 0, palette: 0 }) as FloorInfo;
const press = (code: string, shiftKey: boolean) => ({ code, key: '', shiftKey, repeat: false, preventDefault() {} }) as unknown as KeyboardEvent;

test('Shift and a digit go straight to that floor, numbered from the bottom up; the digit alone does not', () => {
const keys = new Keys<KeyboardEvent>();
const went: string[] = [];
installFloorKeys({ keys } as unknown as Ctx, { switchFloor: (id) => went.push(id) });
store.floors = [floor('api'), floor('web'), floor('admin')];
assert.equal(keys.handle(press('Digit2', true)), 'bound');
assert.equal(keys.handle(press('Digit3', true)), 'bound');
assert.deepEqual(went, ['web', 'admin']);
assert.equal(keys.handle(press('Digit1', false)), 'claimed');
assert.equal(keys.handle(press('Digit0', true)), null);
assert.deepEqual(went, ['web', 'admin']);
});