coworking-agents 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +35 -2
  3. package/bin/coworking-agents.js +40 -5
  4. package/docs/achievements.png +0 -0
  5. package/docs/building.png +0 -0
  6. package/docs/camera.png +0 -0
  7. package/docs/christmas.png +0 -0
  8. package/docs/customise.png +0 -0
  9. package/docs/game-room.png +0 -0
  10. package/docs/halloween.png +0 -0
  11. package/docs/house-wall.png +0 -0
  12. package/docs/mobile.png +0 -0
  13. package/docs/office-day.png +0 -0
  14. package/docs/office-night.png +0 -0
  15. package/docs/panel.png +0 -0
  16. package/docs/paper.png +0 -0
  17. package/docs/servers.png +0 -0
  18. package/docs/talk.png +0 -0
  19. package/docs/top-bar.png +0 -0
  20. package/docs/trophy-room.png +0 -0
  21. package/docs/usage.png +0 -0
  22. package/docs/work-site.png +0 -0
  23. package/package.json +1 -1
  24. package/public/apple-touch-icon.png +0 -0
  25. package/public/icon-192.png +0 -0
  26. package/public/icon-512.png +0 -0
  27. package/public/index.html +3 -1
  28. package/public/js/achievements.js +15 -3
  29. package/public/js/alerts.js +1 -0
  30. package/public/js/ambient.js +1 -1
  31. package/public/js/fun.js +7 -2
  32. package/public/js/i18n.js +59 -14
  33. package/public/js/overlay.js +1 -1
  34. package/public/js/personal.js +84 -1
  35. package/public/js/report.js +18 -1
  36. package/public/js/scene.js +1 -0
  37. package/public/js/tasks.js +276 -17
  38. package/public/js/topbar.js +10 -2
  39. package/public/manifest.webmanifest +15 -0
  40. package/public/phone/index.html +15 -5
  41. package/public/phone/phone.css +112 -16
  42. package/public/phone/phone.js +151 -35
  43. package/public/phone/phone.webmanifest +13 -0
  44. package/public/phone/visit.html +17 -0
  45. package/public/phone/visit.js +12 -0
  46. package/public/style.css +34 -0
  47. package/public/sw.js +4 -0
  48. package/src/appinstall.js +105 -0
  49. package/src/autostart.js +67 -0
  50. package/src/collect.js +18 -1
  51. package/src/demo.js +6 -1
  52. package/src/focus.js +4 -1
  53. package/src/menubar.js +118 -0
  54. package/src/menubar.jxa +82 -0
  55. package/src/phone.js +105 -13
  56. package/src/said.js +59 -0
  57. package/src/server.js +115 -10
  58. package/src/tasks.js +191 -13
  59. package/src/tray.ps1 +100 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-10-13
4
+ - Phone app, live: the office pushes every change as it happens (the 3-second poll only runs while the stream is down)
5
+ - Phone app: tap an agent to see it in full: what it is asking or doing now, your last request, its last answer, what it did, the files it touched, helpers, model, context, cost (texts hidden in private mode)
6
+ - Phone app: write to an agent that finished its turn, typed into its terminal (same rule as the office)
7
+ - Phone app: see a task's changes (summary and coloured diff) before concluding
8
+ - Menu bar (and Windows tray tip): the fullest token limit as a small bar next to the counts, so token use is visible at a glance
9
+ - macOS notifications now come from a small hidden Coworking Agents app (the menu bar script compiled with osacompile, office icon, re-signed ad hoc), so they show its name and icon instead of Script Editor's
10
+ - Phone tab bar sits lower, closer to the bottom edge
11
+ - Phone app shell: the page no longer scrolls (only the middle does), so the header stays below the notch and the tab bar stays on the bottom on iPhone, even after the keyboard
12
+ - Phone app in the office's own look: pixel font, pixel buttons and borders, each agent's face; the page names its files per run so a home-screen icon never keeps an old version
13
+ - Phone app redone: tabs for Now (who needs you, every agent and what it is doing), Tasks (reply, allow, conclude, retry, cancel, and a new task in a folder already in use), Office (the live picture without names) and Usage (today, month, goal, limits); respects the notch and the home bar
14
+ - New app icon: an agent at its desk with the laptop and the speech bubble
15
+ - Create the app on this computer (free, no store, no signing): `coworking-agents install-app` or Personalise — a Coworking Agents app in Applications (macOS), the Start Menu and Desktop (Windows) or the applications menu (Linux); opening it starts the office if needed in an app window. `uninstall-app` removes it
16
+ - The phone panel goes on the home screen like an app (iPhone and Android); its code is kept across restarts, and *Change the phone code* cuts every phone off
17
+ - First run asks once: *Install as an app* (and optionally start with the computer) or *Keep the browser*; the answer decides how it opens from the terminal next time and can be changed in Personalise
18
+ - Use it as an app, if you want: *Install as an app* (its own window and Dock/taskbar icon; nothing cached), `--app` for an app window of Chrome/Edge/Brave/Chromium, and *Start with the computer* (`coworking-agents autostart on|off`): hidden at login with the tray icon, through each system's own login entry, no shell. The browser keeps working as before
19
+ - Tray icon on Windows (PowerShell + Windows Forms: red dot, menu, balloons) and Linux (zenity icon and tooltip, or notify-send notices); on macOS a red count, today's spend, limits, who is working and tasks asking permission; system notifications when nobody has the office open, outside quiet hours
20
+ - macOS menu bar icon (Personalise or `--menubar`): how many are working and who needs you; clicking one opens the office on that agent; it goes away when the office closes. No dependency: the system's own JavaScript for Automation reads a private status file
21
+ - Office events: confetti when a task's tests pass, a PR is opened or a week ends under the cost goal; rain inside the office when tests or a task fail (calmer with reduced motion)
22
+ - Office mood: *stuck* when many agents wait on you or sit silent — the room turns grey-blue and the radio slows; a warm tone when everything flows
23
+ - Files touched most (Repositories panel): the files agent sessions edited most over the last 7 days, by repo and AI (off in private mode)
24
+ - Security: typing into Terminal re-checks, at that moment, that the agent still owns the tab (never into a bare shell); the heavy routes (search in logs, gh, cleanup) run one at a time
25
+ - TV mode (☰ → 📺 or `?tv=1`): full screen with only the office and big counters; Esc leaves
26
+ - Waiting on you today (daily report and the pasted summary): measured time the agents waited on your answers and office tasks stood blocked on you, with the longest waits
27
+ - Budgets per day and per repository: once reached, new office tasks wait and running ones stop
28
+ - Standing instructions per repository (Repositories panel): sent with every task the office creates there
29
+ - GitHub through gh: turn an open issue into a task (title, text and link), and review a pull request by number with a read-only reviewer that gets the diff as data
30
+ - Cleanup (Repositories panel): remove worktrees of finished tasks; uncommitted work is never removed and a branch is deleted only when merged or pushed
31
+ - Permission rules: always deny anything containing a text (wins over everything) and always allow an exact command, with arguments but nothing chained; one click denies the usual dangerous ones
32
+ - Secrets before commit: Ready to ship? scans the lines the task added and its new files for keys, tokens, private keys and credential files; the commit is blocked until you remove them or confirm (the value is never shown)
33
+ - Visits (with `--phone`): a one-hour, LAN-only, read-only link that shows a picture of the office drawn without names plus how many are working; its own token, PNG-only frames, ended on demand
34
+ - Sounds and do not disturb (Personalise tab): turn each sound on or off; a daily quiet window (may cross midnight) mutes sounds and system notifications while notices still land in the bell
35
+ - Compare models: a table on your finished tasks per AI, model and effort with average cost, attempts, tests passing and how many ended well
36
+ - Search what was said: the tasks search box also looks in what you asked and what Claude and Codex answered over the last 7 days (never tool output; off in private mode)
37
+ - Message several agents (☰ → 📣): one request to the agents you pick; typed into terminal sessions that are waiting, continued in the background for VS Code and app conversations, busy ones left out
38
+ - Repositories panel (☰ → 📦): who is in each repo now, what is not committed, to push or behind, worktrees, open tasks, PRs and today's activity
39
+ - Batch: repeat a new task in other folders (one per line), one task each, every folder checked by the server
40
+ - Schedules: run a saved template every day, on weekdays or on one weekday at a set time (while the office is open; catches up the same day)
41
+ - Chains: a task set to *chain* starts as soon as the previous one ended well (no need to conclude it) and gets its result; a failed step stops the chain and says so
42
+ - Security: a pentest sweep in the tests reads every route from server.js (cookie, custom header, foreign and look-alike origins, missing/foreign Host, path traversal, login redirect, security headers); XSS checks on every HTML builder; PR links only as https; importing a backup lists the templates' test commands before you accept
43
+
3
44
  ## 0.6.0 — 2026-10-13
4
45
  - Tasks redesigned: list grouped by waiting on you / running / done with coloured state pills; detail with a clear header, alerts on top, result and review on the left, cost and model on the right, one primary action, quiet secondary ones and a red cancel, activity folded
5
46
  - Backup: export and import templates, queue and economy settings, cost goal, decor, outfits and progress as one JSON file (Personalise tab)
package/README.md CHANGED
@@ -186,6 +186,11 @@ Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Code
186
186
  - **Review** everything since the task started: commits, staged and unstaged changes and new files, plus your test command's output (filtered to the failing lines and the tail).
187
187
  - **Automatic review** (opt-in in the settings): a task that finishes with changes gets a read-only reviewer with the same AI (Claude in plan mode, Codex in a read-only sandbox; edits are refused automatically); the verdict, problems with file:line and missing tests show on the original task.
188
188
  - **Ready to ship?** Concluding first lists tests, review verdict, files changed, what is not committed and the branch. *Commit* (editable message) and *Open PR* (pushes the branch and runs `gh pr create`, needs `gh auth login`) run only when you press them.
189
+ - **Schedules, chains and batches:** run a saved template every day, on weekdays or on one weekday (while the office is open); *chain* a task so it starts when the previous one ended well and receives its result (a failure stops the chain); repeat a new task in several folders at once.
190
+ - **Compare models** on your own finished tasks (cost, attempts, tests, ended well) and **search what was said**: the search box also looks in what you asked and what the agents answered over the last 7 days (never tool output, off in private mode).
191
+ - **Safety nets:** *Ready to ship?* blocks a commit that carries keys, tokens, private keys or credential files until you remove them or confirm; standing **permission rules** answer on their own (deny anywhere always wins; allow only an exact command, nothing chained); budgets **per day and per repository** hold new tasks and stop running ones.
192
+ - **GitHub (through your gh):** turn an open issue into a task, or review a pull request by number with a read-only reviewer that gets the diff as data.
193
+ - **Per repository:** standing instructions sent with every task there, and a **cleanup** of finished tasks' worktrees that never removes uncommitted work or unmerged branches.
189
194
  - **Templates:** save a request you repeat (folder, agent, profile, role, test command) and fire it again with one click.
190
195
  - **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
191
196
  - **Model and effort:** Claude `haiku`/`sonnet`/`opus` with efforts low → max; Codex models and efforts from Codex's own model list. *Save*, *More capacity*, *Apply recommendation* or pick by hand. A change made while a step runs is shown as **pending** and marked **applied** when the next step starts.
@@ -202,11 +207,39 @@ Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folder
202
207
 
203
208
  The 🔔 bell collects agent requests, task updates, recommendations (*Apply*, *See why*, *Not now*), cost alerts and new versions, grouped so repeats don't pile up. It keeps working with the tab in the background and recovers what was already pending when you open the office. **Releases** shows the installed version, the newest on npm (or *unavailable*), when it checked, a clickable map of every release and the notes of the newer one, with the update command for how you installed it. The office admin appears behind the top-bar counter only when there's version news: he waves the sign until you've seen it, keeps holding it until you update, then celebrates.
204
209
 
210
+ ## As an app (optional)
211
+
212
+ The office works in any browser. If you prefer an app:
213
+
214
+ - **Create the app** (Personalise, or `npx coworking-agents install-app`): a *Coworking Agents* app made on your computer — free, no store, no signing — in Applications (macOS), the Start Menu and Desktop (Windows) or the applications menu (Linux). Opening it starts the office if needed and shows it in an app window. `uninstall-app` removes it.
215
+ - **On the phone:** with `--phone`, add the phone panel to the home screen (Safari → Share → Add to Home Screen; Chrome → Add to Home screen). It opens full screen on the same Wi-Fi; the code is kept, and *Change the phone code* cuts every phone off.
216
+ - **Install as an app** (Personalise, or Chrome/Edge's install icon): its own window, no tabs, an icon in the Dock or taskbar. Nothing is cached; it is the same live office.
217
+ - **`npx coworking-agents --app`** opens it in an app window of Chrome, Edge, Brave or Chromium without installing anything.
218
+ - **Start with the computer:** `npx coworking-agents autostart on` (or the switch in Personalise) starts it hidden at login with the tray icon; `autostart off` undoes it. It uses each system's own login entry (macOS LaunchAgent, Windows Startup folder, Linux XDG autostart) with the program and its arguments, never a shell line.
219
+
220
+ ## More panels
221
+
222
+ - **📦 Repositories:** who is in each repo now, what is not committed, to push or behind, worktrees, open tasks, PRs and today's activity.
223
+ - **📣 Message several agents:** one request to the agents you pick, typed into terminal sessions that are waiting and continued in the background for VS Code and app conversations; busy ones are left out.
224
+ - **Office events and mood:** confetti for tests passing, a PR opened or a week under the cost goal; rain when tests or a task fail; the room turns grey-blue when many agents are stuck on you and warm when everything flows.
225
+ - **Files touched most** (Repositories): what agent sessions edited most this week.
226
+ - **🖥 Tray icon** (Personalise or `--menubar`): how many are working, who needs you (red), a small bar with the fullest token limit (`●2 ▰▰▱▱▱ 41%`), today's spend, the limits, who is working on what and tasks asking permission; clicking opens the office on that agent or task, and it notifies you when nobody has the office open (outside quiet hours). macOS uses JavaScript for Automation, Windows uses PowerShell and Windows Forms, Linux uses `zenity` (icon and tooltip) or `notify-send` (notices only) — all part of the system or one package away. It reads a private status file the office writes and goes away when the office closes.
227
+ - **📺 TV mode** (or `?tv=1`): full screen, only the office and big counters; Esc leaves.
228
+ - **Waiting on you today** in the daily report: measured time the agents waited on your answers and tasks stood blocked on you.
229
+ - **Sounds and do not disturb** (Personalise): each sound on or off, and a daily quiet window that mutes sounds and system notifications.
230
+
205
231
  ## Phone panel (opt-in)
206
232
 
207
- `npx coworking-agents --phone` also serves a small page for your phone on the same Wi-Fi: who needs you, tasks with their permission questions (*Allow* / *Deny*), reply, conclude or cancel a task, and pick an option when an agent asks a single-choice question. Scan the QR code printed in the terminal or shown in ☰ → 📱 Phone.
233
+ `npx coworking-agents --phone` also serves a small app for your phone on the same Wi-Fi, in the office's pixel look, updated live as things change. Scan the QR code printed in the terminal or shown in ☰ → 📱 Phone, then add it to the home screen.
234
+
235
+ - **Now:** who needs you and every agent with what it is doing.
236
+ - **Tap an agent** to see it in full: what it asks or does right now, your last request, its last answer, what it did, the files it touched, its helpers, model, context and cost. When it finished its turn you can **write to it**; the text is typed into its terminal, like the office does (not into the Claude app or VS Code).
237
+ - **Tasks:** answer permission questions (*Allow* / *Deny*), reply, conclude, retry or cancel, **see the changes** (summary and coloured diff) and start a new task in a folder already in use.
238
+ - **Office:** a picture of the office drawn without names. **Usage:** today's and the month's spend and the limits.
239
+
240
+ In private mode the phone shows no texts.
208
241
 
209
- It is a separate server that only answers on your LAN address, with its own token (new each run), an exact Host and Origin check, and none of the office's other routes (files, diffs, launching, windows). The office itself stays on `127.0.0.1`. The local network link is plain HTTP, so use it only on networks you trust.
242
+ It is a separate server that only answers on your LAN address, with its own token (kept so the home-screen icon keeps working; *Change the phone code* replaces it), an exact Host and Origin check, and only the phone's own routes: none of the office's files, launching or windows (a task's diff is the one thing it reads, and not in private mode). The office itself stays on `127.0.0.1`. The local network link is plain HTTP, so use it only on networks you trust. With the phone panel on, **👀 Visit** creates a one-hour, read-only link (its own token) that shows only a picture of the office drawn without names and how many are working; end it any time.
210
243
 
211
244
  ## MCP (read-only)
212
245
 
@@ -17,6 +17,10 @@ if (flag('-h') || flag('--help')) {
17
17
  --port port to listen on (default 4777, falls back to a free one)
18
18
  --no-open don't open the browser
19
19
  --private hide session titles, paths and file names (screen sharing)
20
+ --app open in an app window (Chrome, Edge, Brave or Chromium) instead of a browser tab
21
+ install-app create the Coworking Agents app on this computer (uninstall-app removes it)
22
+ autostart on|off start hidden with the computer, with the tray icon
23
+ --menubar show a menu bar icon with who needs you (macOS; also a switch in Personalise)
20
24
  --phone also serve a small phone panel on your Wi-Fi (own token, QR code in the terminal; trusted networks only)
21
25
  --demo [N] show a fake office with every state and N agents (default 10)
22
26
  --json print one snapshot and exit
@@ -26,6 +30,17 @@ if (flag('-h') || flag('--help')) {
26
30
  }
27
31
 
28
32
  if (args[0] === 'mcp') { require('../src/mcp').serve(); return; }
33
+ // the app: `coworking-agents install-app` / `uninstall-app` (made on this computer, free, no store)
34
+ if (args[0] === 'install-app') { const r = require('../src/appinstall').install(require('../src/update').installMode()); console.log(`Created: ${r.where}\nOpen "Coworking Agents" like any app. Remove it with: coworking-agents uninstall-app`); return; }
35
+ if (args[0] === 'uninstall-app') { require('../src/appinstall').uninstall(); console.log('Removed the Coworking Agents app.'); return; }
36
+ // start with the computer: `coworking-agents autostart on | off | status`
37
+ if (args[0] === 'autostart') {
38
+ const a = require('../src/autostart');
39
+ if (args[1] === 'on') { const r = a.enable(require('../src/update').installMode()); console.log(`On: the office starts hidden at login, with its tray icon.\n${r.file}\nUndo with: coworking-agents autostart off`); }
40
+ else if (args[1] === 'off') { a.disable(); console.log('Off: the office no longer starts with the computer.'); }
41
+ else console.log(a.enabled() ? 'Autostart is on.' : 'Autostart is off. Turn it on with: coworking-agents autostart on');
42
+ return;
43
+ }
29
44
 
30
45
  if (flag('--json')) {
31
46
  const { snapshot } = require('../src/collect');
@@ -34,11 +49,28 @@ if (flag('--json')) {
34
49
  return;
35
50
  }
36
51
 
52
+ // the choice made in the office the first time ("use as an app" or "keep the browser") decides how it opens next time
53
+ function wantsApp() {
54
+ if (flag('--app')) return true;
55
+ try { return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config', 'coworking-agents', 'tasks', 'tasks.json'), 'utf8')).settings.appChoice === 'app'; } catch { return false; }
56
+ }
37
57
  function open(url) {
58
+ if (wantsApp() && openApp(url)) return;
38
59
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
39
60
  const a = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
40
61
  execFile(cmd, a, () => {});
41
62
  }
63
+ // --app: an app window of Chrome, Edge, Brave or Chromium (no tabs, no address bar); the default browser otherwise
64
+ function openApp(url) {
65
+ const fs = require('fs'), path = require('path'), { spawnSync, spawn } = require('child_process');
66
+ const pf = process.env.ProgramFiles || 'C:\\Program Files', pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', local = process.env.LOCALAPPDATA || '';
67
+ const candidates = process.platform === 'darwin' ? ['Google Chrome', 'Microsoft Edge', 'Brave Browser', 'Chromium'].map(n => `/Applications/${n}.app/Contents/MacOS/${n}`)
68
+ : process.platform === 'win32' ? [path.join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'), path.join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe')]
69
+ : ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'microsoft-edge', 'brave-browser'].map(n => { try { const r = spawnSync('which', [n], { encoding: 'utf8' }); return r.status === 0 ? r.stdout.trim() : ''; } catch { return ''; } });
70
+ const exe = candidates.find(c => c && fs.existsSync(c)); if (!exe) return false;
71
+ spawn(exe, ['--app=' + url], { detached: true, stdio: 'ignore' }).unref();
72
+ return true;
73
+ }
42
74
 
43
75
  // The token link lives in a file only you can read (0600): a second run reopens the same
44
76
  // office, and a foreign program on port 4777 can't impersonate it (it doesn't have the token).
@@ -63,19 +95,22 @@ async function reuse() {
63
95
  if (s) { console.log(`coworking-agents already running → ${s.base}/?t=${s.token}`); if (!flag('--no-open')) open(`${s.base}/?t=${s.token}`); return; }
64
96
  }
65
97
  // keep the same token across restarts so an open tab keeps working (file is 0600, only yours)
66
- let token;
67
- if (!demo && !privacy) { try { const prev = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); if (/^[0-9a-f]{48}$/.test(prev.token)) token = prev.token; } catch {} }
98
+ let token, phoneToken;
99
+ if (!demo && !privacy) { try { const prev = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); if (/^[0-9a-f]{48}$/.test(prev.token)) token = prev.token; if (/^[0-9a-f]{32}$/.test(prev.phoneToken || '')) phoneToken = prev.phoneToken; } catch {} }
100
+ // the phone code is kept with the office token (0600 file) so a home-screen icon keeps working; "new code" replaces it
101
+ const onPhoneToken = t => { phoneToken = t; try { const cur = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); fs.writeFileSync(STATE_FILE, JSON.stringify({ ...cur, phoneToken: t }), { mode: 0o600 }); } catch {} };
68
102
  let hq;
69
103
  const phone = flag('--phone');
70
- try { hq = await start({ port, privacy, demo, token, phone }); }
104
+ const menubar = flag('--menubar');
105
+ try { hq = await start({ port, privacy, demo, token, phone, menubar, phoneToken, onPhoneToken }); }
71
106
  catch (e) {
72
107
  if (e.code !== 'EADDRINUSE') throw e;
73
- hq = await start({ port: 0, privacy, demo, token, phone }); // port taken by something else: use a free one
108
+ hq = await start({ port: 0, privacy, demo, token, phone, menubar, phoneToken, onPhoneToken }); // port taken by something else: use a free one
74
109
  }
75
110
  if (!demo && !privacy) {
76
111
  try {
77
112
  fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
78
- fs.writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, base: hq.base, token: hq.token }), { mode: 0o600 });
113
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ pid: process.pid, base: hq.base, token: hq.token, phoneToken: hq.phone ? hq.phone.url.split('t=')[1] : phoneToken }), { mode: 0o600 });
79
114
  // the file stays on exit: it keeps the token for the next run (reuse() pings before trusting it)
80
115
  const bye = () => process.exit(0);
81
116
  process.on('SIGINT', bye); process.on('SIGTERM', bye);
Binary file
package/docs/building.png CHANGED
Binary file
package/docs/camera.png CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/docs/mobile.png CHANGED
Binary file
Binary file
Binary file
package/docs/panel.png CHANGED
Binary file
package/docs/paper.png CHANGED
Binary file
package/docs/servers.png CHANGED
Binary file
package/docs/talk.png CHANGED
Binary file
package/docs/top-bar.png CHANGED
Binary file
Binary file
package/docs/usage.png CHANGED
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coworking-agents",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A pixel-art coworking office for your AI coding agents (Claude Code, Codex, …): see who's working, who needs you, and who's editing the same checkout.",
5
5
  "bin": {
6
6
  "coworking-agents": "bin/coworking-agents.js"
Binary file
Binary file
Binary file
package/public/index.html CHANGED
@@ -7,7 +7,9 @@
7
7
  <link rel="icon" href="favicon.ico?v=4" sizes="any">
8
8
  <link rel="icon" href="favicon.png?v=4" type="image/png" sizes="64x64">
9
9
  <link rel="icon" href="favicon.svg?v=4" type="image/svg+xml">
10
- <link rel="apple-touch-icon" href="apple-touch-icon.png?v=3">
10
+ <link rel="apple-touch-icon" href="apple-touch-icon.png?v=4">
11
+ <link rel="manifest" href="manifest.webmanifest">
12
+ <meta name="theme-color" content="#17151a">
11
13
  <link rel="stylesheet" href="style.css">
12
14
  </head>
13
15
  <body>
@@ -220,7 +220,7 @@ function checkAchievements() {
220
220
  if (goal) {
221
221
  const keys = Array.from({ length: 7 }, (_, i) => { const d = new Date(Date.now() - (i + 1) * 864e5); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); });
222
222
  const worked = keys.filter(k => days[k] && days[k].tools >= 10).length, spent = keys.reduce((n, k) => n + ((days[k] && days[k].usd) || 0), 0);
223
- if (worked >= 3 && spent <= goal * 7 / 30) achAdd('underGoal', keys[keys.length - 1]); // one per finished 7-day window
223
+ if (worked >= 3 && spent <= goal * 7 / 30) { const n0 = achCount('underGoal'); achAdd('underGoal', keys[keys.length - 1]); if (achCount('underGoal') > n0) confettiAt = Date.now(); } // one per finished 7-day window, celebrated
224
224
  } }
225
225
  const need = live.filter(p => p.state === 'needs_you' || p.state === 'waiting').length;
226
226
  if (achQueueWas >= 2 && need === 0) achBump('zeroQueue');
@@ -305,8 +305,20 @@ const shadeHex = (hex, f) => Art.shade(hex, f);
305
305
  if (at === seq.length) { at = 0; achBump('konami'); confettiAt = Date.now(); checkAchievements(); }
306
306
  });
307
307
  }
308
- // confetti over the office (konami, or any legend-tier unlock)
309
- let confettiAt = 0;
308
+ // confetti over the office (konami, any legend-tier unlock, tests passing, a PR opened, a week under the cost goal)
309
+ let confettiAt = 0, officeRainAt = 0;
310
+ const reducedMotion = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } };
311
+ // rain inside the office when tests fail or a task fails: a grey cloud and drops for a few seconds
312
+ function drawOfficeRain(t) {
313
+ const age = Date.now() - officeRainAt; if (age > 4500) return;
314
+ const fade = age < 400 ? age / 400 : age > 3800 ? (4500 - age) / 700 : 1;
315
+ ctx.fillStyle = `rgba(30, 40, 60, ${(.32 * fade).toFixed(3)})`; ctx.fillRect(0, 0, W, H);
316
+ if (reducedMotion()) return;
317
+ for (let i = 0; i < 120; i++) {
318
+ const h = Art.hash('rn' + i), x = (h % W) + (age / 25) % 8, y = ((h >>> 8) % H + age / 1000 * (260 + (h >>> 16) % 160)) % H;
319
+ r(x, y, 1, 6, `rgba(170, 210, 255, ${(.8 * fade).toFixed(3)})`);
320
+ }
321
+ }
310
322
  function drawConfetti(t) {
311
323
  const age = Date.now() - confettiAt;
312
324
  if (age > 4000) return;
@@ -23,6 +23,7 @@ function toast(html, kind) {
23
23
  setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, 7000);
24
24
  }
25
25
  function notifyOS(title, body) {
26
+ if (typeof quietNow === 'function' && quietNow()) return; // do not disturb
26
27
  playTune('limit');
27
28
  if (notifyOn && !focusing() && 'Notification' in window && Notification.permission === 'granted' && document.hidden) { try { new Notification(title, { body }); } catch {} }
28
29
  }
@@ -101,7 +101,7 @@ function radioNote(freq, at, len, wave, vol) {
101
101
  }
102
102
  function radioTick() {
103
103
  if (!radioOn || !soundOn) return;
104
- const mood = officeMood(), bpm = mood === 'rush' ? 132 : mood === 'calm' ? 78 : 104, step = 60 / bpm / 2;
104
+ const mood = officeMood(), bpm = mood === 'rush' ? 132 : mood === 'calm' ? 78 : mood === 'stuck' ? 66 : 104, step = 60 / bpm / 2;
105
105
  const st = STATIONS[radioStation()];
106
106
  while (radioNext < audio.currentTime + .25) {
107
107
  const bar = Math.floor(radioStep / 16), beat = radioStep % 16, chord = [0, 3, 4, 2][bar % 4];
package/public/js/fun.js CHANGED
@@ -113,9 +113,12 @@ function drawHolidayWindow(x, y, w, h, t, sky) {
113
113
  // ---- office mood: how busy the office is changes the room ----
114
114
  // rush = most of the office working (the ON AIR sign lights, the coffee machine never stops),
115
115
  // calm = nobody working (the lights dim a little and the robot vacuum gets to work)
116
+ // stuck = many agents waiting on you or silent while "working" (the room goes grey-blue and the radio slows down)
116
117
  function officeMood() {
117
118
  if (!data) return 'normal';
118
119
  const live = data.people.filter(p => !p.leaving);
120
+ const blocked = live.filter(p => ['needs_you', 'waiting'].includes(p.state) || (p.lastActivity && !['idle', 'asleep'].includes(p.state) && data.now - p.lastActivity > 10 * 60000)).length;
121
+ if (blocked >= 2 && blocked / Math.max(1, live.length) >= .4) return 'stuck';
119
122
  const work = live.filter(p => !['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)).length;
120
123
  if (work >= 3 && work / Math.max(1, live.length) >= .6) return 'rush';
121
124
  if (live.length && work === 0) return 'calm';
@@ -133,8 +136,10 @@ let moodBox = null;
133
136
  // lit things on the wall are redrawn over the night shade (like the LED panel)
134
137
  let wallLit = [];
135
138
  function moodShade() { // drawn over everything but the lit things, before the lights
136
- if (officeMood() !== 'calm') return;
137
- ctx.fillStyle = 'rgba(20, 18, 40, .12)'; ctx.fillRect(0, 0, W, H);
139
+ const m = officeMood();
140
+ if (m === 'calm') { ctx.fillStyle = 'rgba(20, 18, 40, .12)'; ctx.fillRect(0, 0, W, H); }
141
+ if (m === 'stuck') { ctx.fillStyle = 'rgba(40, 58, 92, .28)'; ctx.fillRect(0, 0, W, H); } // grey-blue: things are stuck on you
142
+ if (m === 'rush') { ctx.fillStyle = 'rgba(255, 176, 84, .05)'; ctx.fillRect(0, 0, W, H); } // warm: it flows
138
143
  }
139
144
 
140
145
  // ---- the cat: visits whoever has been waiting on you the longest, and likes being petted ----