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 @@ -36,7 +36,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentSystemLabs/agent-office/main/i
- **Workers at desks.** Walk up to an empty desk, press **E**, and pick Claude Code, Codex, OpenCode, Grok, Muse, DeepSeek Harness, Pi or Cursor, each with its model and reasoning effort. The agent's live terminal shows on its laptop, and anyone can open it and type.
- **Talk instead of typing.** Hold **Ctrl+Space** (or the **🎤**) in a worker's terminal or a prompt box and say what you want: it's typed in for you to send. Your browser does the listening, so there's nothing to install.
- **You can't miss who needs you.** A worker that stops to ask you something lights a red beacon over its desk, puts a banner on your screen saying who and what for, and sounds an alarm. One that has finished jumps up and down and dings. Press **N** to go straight to whoever is waiting.
- **From your phone, too.** `/lite` is the office in 2D: every worker and what it's waiting on, its terminal with the keys a phone keyboard lacks, and the boards. The 3D office offers it on a phone or a slow computer.
- **From your phone, too.** `/lite` is the office in 2D: every worker and what it's waiting on, its terminal with the keys a phone keyboard lacks, and the boards. **☀️** beside **🏢 3D** switches that page to dark (**🌙**), and the browser keeps the choice. The 3D office offers it on a phone or a slow computer.
- **GitHub on the walls.** Issues and pull requests hang on cork boards. Hand an issue to a worker, queue tasks, give a worker its own git worktree and open its PR with one key (if one gets deleted behind the office's back, the worker waits at its desk until you rebuild it). One task can span several projects: the worker gets a worktree of each, and a PR in each that links the others.
- **Agents that manage agents.** Every worker can list, hire, message and send home the others, through an `agent-office` MCP server (Claude Code, Codex, OpenCode) or the `office-workers` command. Ask one to "send everyone whose PR merged home" and it does, deleting their worktrees and branches unless they hold unpushed work. A worker that opens its pull request itself (`gh pr create`) shows it at its desk, and one the office missed can be told which is its own (`office-workers pr`).
- **Together.** Voice, chat, screen sharing on the lounge TV and a shared whiteboard.
Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Everything in the office, room by room. Back to the [README](../README.md).
- **Workers act out what they're doing.** A working worker acts out its latest tool call, so you can tell what it's doing from across the room without reading its card. Reading files, it holds up a stack of papers and flips through them. Editing, it hunches over the keys and types flat out. Running tests or a build, it leans back with its hands behind its head. Searching or fetching the web, a globe spins beside its laptop. When its tests or build fail twice in a row, it puts its head in its hands. Waiting on you, it jumps for a couple of seconds, then crosses its arms and taps its foot. When it finishes, it does a little spin and a puff of confetti goes up. Claude Code hooks tell a shell command that runs tests from one that only reads, and they report failures. Codex and OpenCode report only the tool's name, so their shell commands are acted out as plain typing.
- **Next worker that needs you.** Press **N** to go straight to the first worker waiting on someone (it needs input, or it's done and nobody has looked), standing behind it and looking at its laptop. Press **N** again for the next one, and after the last you start over: the ones that need input come first, then the ones that are done, and whoever has waited longest is first among each. A pin at the edge of the screen points the way to each waiting worker you can't see, red for *needs input* and green for *done*, and the top bar counts them (`🙋 2 need you · ✅ 1 done`, in red and pulsing while anyone needs you; click it to do the same as **N**). With nobody waiting on your floor, **N** tells you which floor has someone.
- **Notifications.** You don't have to be in the office tab. The tab title counts the workers waiting on someone, like `(2) my-project · Agent Office`. Allow desktop notifications (the office asks when you first hire a worker, or turn them on under **⚙️**) and a worker that needs you or finishes while you're in another tab or app pops one up (*🙋 Byte needs you*, with what it's asking; that one stays up until it's dealt with). Click it to go straight to that worker: the office comes to the front with you at its desk and its terminal open. For the whole team, paste a Slack or Discord incoming webhook under **⚙️** → *Team notifications* (or start with `--webhook <url>`), and the office posts to that channel when a worker has needed input or been done for a few seconds with nobody at its terminal.
- **The 2D view, for your phone.** Open `/lite` (or **📱 2D view** in the **☰** menu) for the office without the 3D. It lists every worker on the floor, the ones waiting on someone first, each with what it's asking (*🙋 Wants permission: Bash: npm test*) or doing, its branch and PR, and who typed to it last. Tap one for its terminal, with a row of the keys a phone keyboard hasn't got (**1** **2** **3** to answer a question or a permission prompt, the arrows, Enter, Tab, Esc and Ctrl+C) and a box to send it a prompt. **✍️** on a card sends one without opening the terminal. **📌 Issues**, **🔀 PRs**, **📋 Queue** and **✨ New task** open the same windows as in the office, and the floor at the top switches projects, with a button straight to any floor where someone's waiting. The tab title counts who's waiting, and once you allow notifications (**🔔**) a worker that needs you pops one up and buzzes the phone. The 3D office offers the 2D view as it loads on a phone, and on a computer where it draws fewer than 20 frames a second for ten seconds. **Stay in 3D** stops it asking in that browser. A browser that can't draw the 3D office at all (no WebGL) goes straight to the 2D view, and signing in from `/lite` brings you back there. Teammates see you under *In the office* as *📱 on the 2D view*, not standing anywhere in it.
- **The 2D view, for your phone.** Open `/lite` (or **📱 2D view** in the **☰** menu) for the office without the 3D. **☀️** beside **🏢 3D** switches that page to dark, and the button shows **🌙** while it's dark. The browser keeps the choice. It lists every worker on the floor, the ones waiting on someone first, each with what it's asking (*🙋 Wants permission: Bash: npm test*) or doing, its branch and PR, and who typed to it last. Tap one for its terminal, with a row of the keys a phone keyboard hasn't got (**1** **2** **3** to answer a question or a permission prompt, the arrows, Enter, Tab, Esc and Ctrl+C) and a box to send it a prompt. **✍️** on a card sends one without opening the terminal. **📌 Issues**, **🔀 PRs**, **📋 Queue** and **✨ New task** open the same windows as in the office, and the floor at the top switches projects, with a button straight to any floor where someone's waiting. The tab title counts who's waiting, and once you allow notifications (**🔔**) a worker that needs you pops one up and buzzes the phone. The 3D office offers the 2D view as it loads on a phone, and on a computer where it draws fewer than 20 frames a second for ten seconds. **Stay in 3D** stops it asking in that browser. A browser that can't draw the 3D office at all (no WebGL) goes straight to the 2D view, and signing in from `/lite` brings you back there. Teammates see you under *In the office* as *📱 on the 2D view*, not standing anywhere in it.
- **Task cards.** A card over each worker's head names what it's on ("Fix Login Redirect") and says in one line what it's doing right now. For Claude workers, Claude Haiku writes it from prompts and recent tools through the `claude` CLI. OpenCode, Codex, Grok, Muse, DeepSeek Harness and Cursor use local task summaries and never invoke Claude to name their tasks.
- **Survives restarts.** Workers are saved to disk with their agent choice and session ID, and their terminals run in a small process of their own. When the server restarts (a dev-server reload, a `kill` of its process), the agents keep working through it, and the new server picks every terminal back up, scrollback and all. Open terminals reconnect by themselves. Ctrl+C stops the workers; the next start wakes them back up in their sessions, and **R** resumes the Claude Code, OpenCode, Codex, Grok, Muse, DeepSeek Harness, Pi or Cursor session of one that exited. DeepSeek Harness workers are the exception to keeping working through a restart: their ACP session is a child of the office process, so it stops with the office and comes back *offline*, resumable with **R**. When a restart does take a worker's terminal with it (the machine rebooted, the terminal process died or was replaced by a newer one), a worker that was in the middle of something is resumed with *continue*, so it carries on by itself. The chat and each worker's last 3,000 lines of terminal are kept too, so after a restart (or a crash) you can still scroll back through what the workers printed and what everyone said.
- **Search.** Press **/** (or **🔎 Search** in the **☰** menu) to search the chat and every worker's terminal at once, including what they showed before the office restarted. Click a terminal line to open that terminal scrolled to it, with the line lit up.
Expand Down
47 changes: 47 additions & 0 deletions src/client/lite.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ body.lite { min-height: 100dvh; background: var(--paper); -webkit-tap-highlight-

.lite-bar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 8px; padding: 10px 12px; padding-top: max(10px, env(safe-area-inset-top)); background: var(--paper-2); border-bottom: 3px solid var(--ink); }
.lite-bar .btn { height: 44px; flex: none; text-decoration: none; }
/* ☀️ while the page is light, 🌙 while it's dark (lite.ts), beside 🏢 3D. */
.lite-theme { min-width: 44px; padding: 0 10px; justify-content: center; font-size: 18px; }
.lite-floor { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; margin: 0; }
.lite-floor-label { font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.lite-floor select {
Expand Down Expand Up @@ -76,3 +78,48 @@ body.lite { min-height: 100dvh; background: var(--paper); -webkit-tap-highlight-
.modal.board .column { min-width: 78vw; }
.modal.board header .board-status { display: none; }
}

/* Dark mode (☀️/🌙 beside 🏢 3D). The same ink-and-paper office, at night: cream lines on a dark page. */
body.lite.dark {
--ink: #f4efe6;
--paper: #14131a;
--paper-2: #211e28;
--card: #2c2836;
--muted: #b7aea2;
color-scheme: dark;
background: var(--paper);
}
body.lite.dark :is(
.btn:not(.primary):not(.danger):not(.on),
.lite-worker:not(.exited):not(.offline),
.modal:not(.term),
.modal textarea,
.modal input[type=text],
.modal input[type=password],
.modal select
) {
background-color: var(--card);
}
body.lite.dark :is(.btn, .lite-worker, .modal:not(.term)) { box-shadow: 0 3px 0 #07060a; }
body.lite.dark .btn:not(.primary):not(.danger):not(.on):hover { background: #3a3446; }
body.lite.dark .modal:not(.term) { box-shadow: 0 6px 0 #07060a; }
body.lite.dark .lite-nav .n { background: #f4efe6; color: #1a1820; }
body.lite.dark .lite-nav .btn.primary .n { background: #1a1820; color: #f4efe6; }
body.lite.dark .lite-go { background: #4a2030; }
body.lite.dark .lite-go:hover { background: #5c2840; }
body.lite.dark .lite-worker.exited,
body.lite.dark .lite-worker.offline { background: #1a1820; }
body.lite.dark .lite-floor select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23f4efe6' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
body.lite.dark .column { background: rgba(244, 239, 230, .06); }
body.lite.dark .modal.term { background: #1e1f2e; box-shadow: none; }
body.lite.dark .pill.starting,
body.lite.dark .pill.idle,
body.lite.dark .pill.working,
body.lite.dark .pill.exited,
body.lite.dark .pill.offline,
body.lite.dark .pill.lost { color: #1a1820; }
@media (max-width: 640px) {
body.lite.dark :is(.modal.term, .modal.board, .modal.gh-window, .modal.desk-changes, .modal.meeting-window, .modal[aria-label="Task queue"]) { box-shadow: none; }
}
14 changes: 14 additions & 0 deletions src/client/lite.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,21 @@
<span class="lite-floor-label">Floor</span>
<select id="floor" aria-label="Floor"></select>
</label>
<button id="btn-theme" class="btn lite-theme" type="button" aria-pressed="false" aria-label="Light mode" title="Light mode. Switch the 2D view to dark.">☀️</button>
<a id="to-3d" class="btn" href="/?3d=1" title="The 3D office, where you walk around">🏢 3D</a>
<script>
try {
if (localStorage.getItem('ao-lite-dark') === '1') {
document.body.classList.add('dark');
const theme = document.getElementById('btn-theme');
theme.textContent = '🌙';
theme.setAttribute('aria-pressed', 'true');
theme.setAttribute('aria-label', 'Dark mode');
theme.title = 'Dark mode. Switch the 2D view to light.';
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#211e28');
}
} catch (e) { /* storage blocked: stay on the light page */ }
</script>
</header>
<main class="lite-main">
<p id="floor-meta" class="lite-meta"></p>
Expand Down
26 changes: 26 additions & 0 deletions src/client/lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ import { repoChoices } from './shared/hiring';
// The tab title counts the workers waiting on someone, on every floor, as the 3D office's does.
import { renderTitle } from './shared/title';

/** Dark mode for this page, kept in the browser. ☀️ is light, 🌙 is dark (the button beside 🏢 3D). */
const LITE_DARK_KEY = 'ao-lite-dark';

function applyLiteTheme(dark: boolean) {
document.body.classList.toggle('dark', dark);
const btn = document.getElementById('btn-theme');
if (btn) {
btn.textContent = dark ? '🌙' : '☀️';
btn.title = dark ? 'Dark mode. Switch the 2D view to light.' : 'Light mode. Switch the 2D view to dark.';
btn.setAttribute('aria-pressed', dark ? 'true' : 'false');
btn.setAttribute('aria-label', dark ? 'Dark mode' : 'Light mode');
}
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', dark ? '#211e28' : '#fff1de');
}

applyLiteTheme(document.body.classList.contains('dark'));
document.getElementById('btn-theme')?.addEventListener('click', () => {
const dark = !document.body.classList.contains('dark');
try {
localStorage.setItem(LITE_DARK_KEY, dark ? '1' : '0');
} catch {
// storage blocked: the page still switches until it's reloaded
}
applyLiteTheme(dark);
});

// Sent here because this browser can't draw the 3D office (see noWebGL in core/scene.ts).
if (new URLSearchParams(location.search).get('why') === 'webgl') {
history.replaceState(null, '', location.pathname);
Expand Down