coworking-agents 0.5.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 (66) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +47 -4
  3. package/ROADMAP.md +2 -4
  4. package/bin/coworking-agents.js +48 -6
  5. package/docs/achievements.png +0 -0
  6. package/docs/building.png +0 -0
  7. package/docs/camera.png +0 -0
  8. package/docs/christmas.png +0 -0
  9. package/docs/customise.png +0 -0
  10. package/docs/game-room.png +0 -0
  11. package/docs/halloween.png +0 -0
  12. package/docs/house-wall.png +0 -0
  13. package/docs/mobile.png +0 -0
  14. package/docs/office-day.png +0 -0
  15. package/docs/office-night.png +0 -0
  16. package/docs/panel.png +0 -0
  17. package/docs/paper.png +0 -0
  18. package/docs/servers.png +0 -0
  19. package/docs/talk.png +0 -0
  20. package/docs/top-bar.png +0 -0
  21. package/docs/trophy-room.png +0 -0
  22. package/docs/usage.png +0 -0
  23. package/docs/work-site.png +0 -0
  24. package/package.json +1 -1
  25. package/public/apple-touch-icon.png +0 -0
  26. package/public/icon-192.png +0 -0
  27. package/public/icon-512.png +0 -0
  28. package/public/index.html +3 -1
  29. package/public/js/achievements.js +33 -2
  30. package/public/js/alerts.js +33 -1
  31. package/public/js/ambient.js +1 -1
  32. package/public/js/collection.js +3 -1
  33. package/public/js/fun.js +7 -2
  34. package/public/js/fun2.js +15 -2
  35. package/public/js/i18n.js +109 -24
  36. package/public/js/overlay.js +1 -1
  37. package/public/js/panel.js +2 -1
  38. package/public/js/personal.js +112 -1
  39. package/public/js/report.js +59 -5
  40. package/public/js/scene.js +1 -0
  41. package/public/js/tasks.js +480 -52
  42. package/public/js/topbar.js +22 -3
  43. package/public/manifest.webmanifest +15 -0
  44. package/public/phone/index.html +26 -0
  45. package/public/phone/phone.css +115 -0
  46. package/public/phone/phone.js +167 -0
  47. package/public/phone/phone.webmanifest +13 -0
  48. package/public/phone/visit.html +17 -0
  49. package/public/phone/visit.js +12 -0
  50. package/public/style.css +93 -0
  51. package/public/sw.js +4 -0
  52. package/src/appinstall.js +105 -0
  53. package/src/autostart.js +67 -0
  54. package/src/collect.js +32 -3
  55. package/src/demo.js +8 -3
  56. package/src/focus.js +117 -7
  57. package/src/menubar.js +118 -0
  58. package/src/menubar.jxa +82 -0
  59. package/src/phone.js +180 -0
  60. package/src/qr.js +116 -0
  61. package/src/said.js +59 -0
  62. package/src/server.js +150 -11
  63. package/src/sources/codex.js +13 -3
  64. package/src/tasks.js +373 -33
  65. package/src/tray.ps1 +100 -0
  66. package/src/update.js +6 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
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
+
44
+ ## 0.6.0 — 2026-10-13
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
46
+ - Backup: export and import templates, queue and economy settings, cost goal, decor, outfits and progress as one JSON file (Personalise tab)
47
+ - Achievements: First try (tasks done without a retry), Approved (reviews that said OK), Penny-wise (weeks under the cost goal)
48
+ - Weekly card: best day, the AI used most and the week's cost; a notice on Sundays when it is ready
49
+ - Ready to ship?: concluding a task first shows tests, review, files, what is not committed and the branch; commit with an editable message and open a PR with `gh`, only when you press it
50
+ - Agents that say they are working but write nothing for 10 minutes get a notice
51
+ - File clashes land in the notices, with a pause button when one side is an office task
52
+ - Month's cost goal in Usage: spent so far, end-of-month projection, and a notice when the pace or the total goes over
53
+ - Shop: beanie, bow, wizard hat, round glasses, mustache, green and blue shirts; bought outfits say *Wear* and open the wardrobe instead of *Move*
54
+ - Phone panel (opt-in `--phone`): a small page on your Wi-Fi to see who needs you and answer permissions, replies and single-choice questions; its own restricted server and token, QR code in the terminal and in the office
55
+ - Automatic review (opt-in): when a task finishes with changes, a read-only reviewer with the same AI reads the diff and its verdict shows on the original task
56
+ - Task templates: save a request you repeat with its folder and settings; fill the form or create and run with one click
57
+ - The new-task form stays open and keeps what you typed across the periodic refresh
58
+ - Daily report to paste: cost of the day's sessions, what each agent did, what is still open (tasks and agents waiting on you), as Markdown or Slack markup
59
+ - Limits: at the pace of the last hour, the office warns when a 5-hour or weekly window will run out before it resets; limit warnings also land in the 🔔 notices with a tip to make it last
60
+ - Switch in the terminal: change the model and effort of a running Claude or Codex terminal session from the office (Claude `/model` + `/effort`; Codex picker, this session only); recommendation notices do it on Apply
61
+ - Codex subagent cost counted from the subagents' own logs, once
62
+ - Compact: shrink a task's or an outside conversation's context in the background (Claude `/compact`, Codex compaction), keeping the same session
63
+ - Permissions mid-run: a task pauses when the agent wants a tool it may not use (Claude) or to leave its sandbox (Codex) and the office asks Allow / Always allow / Deny, on the card and as a notice
64
+ - Codex tasks run through `codex app-server`, the mode where Codex can ask before leaving its sandbox; cost per model call, counted once
65
+ - "Open the conversation" jumps to the exact conversation in VS Code, the Claude app or the Codex app
66
+ - Sessions opened outside are watched while they run: when there is a real reason (stuck on one step, nearly full context, the kind of work) the office sends a notice with the recommended model and effort; Apply puts it in that agent's request box
67
+ - Ask any Claude or Codex session from the office, even one opened in VS Code or the app: the request continues the same conversation with the model and effort you choose (the recommended ones come pre-picked)
68
+ - Sessions opened outside show their real effort (Codex from its log, Claude from its settings)
69
+ - Task detail shows current, pending and recommended config once, in the recommendation card
70
+ - Update check every hour and when the releases tab opens
71
+
3
72
  ## 0.5.0 — 2026-10-11
4
73
  - Agent panel: tasks started by the office show running vs pending model and effort, cost and Save / More capacity / Apply recommendation; monitored sessions show the recommendation as not applied
5
74
  - Codex models and efforts come from Codex's own model list; Claude efforts low to max
package/README.md CHANGED
@@ -180,16 +180,25 @@ Agents away from their desks (kitchen, meeting room, nap corner) carry a name ta
180
180
 
181
181
  ## Tasks: hand work to agents from the office
182
182
 
183
- Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*), a profile and optionally a role (Backend, Frontend, Reviewer). The agent runs in the background (`claude -p` / `codex exec`), in its own git worktree if you tick it. You never need the terminal:
183
+ Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*), a profile and optionally a role (Backend, Frontend, Reviewer). The agent runs in the background (`claude -p` / `codex app-server`), in its own git worktree if you tick it. You never need the terminal:
184
184
 
185
- - **Answer** its questions and **approve** tools it was not allowed to use; it resumes the same session. **Retry**, start a **new session with a summary**, or **hand off** to the other agent with the decisions, tests and open items.
185
+ - **Permissions mid-run:** when the agent wants a tool it may not use (Claude) or to step outside its sandbox (Codex), the run pauses and the office asks: *Allow*, *Always allow* (for that task) or *Deny*, on the card, in the task and as a 🔔 notice. **Answer** its questions; it resumes the same session. **Retry**, start a **new session with a summary**, or **hand off** to the other agent with the decisions, tests and open items.
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
+ - **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
+ - **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.
194
+ - **Templates:** save a request you repeat (folder, agent, profile, role, test command) and fire it again with one click.
187
195
  - **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
188
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.
189
- - **Click the agent:** a session started by the office shows its task, running vs pending config, cost and the same controls. A session opened elsewhere is **monitored**: you see the recommendation, marked as not applied, and can turn it into a task.
190
- - **Cost:** per real model, tokens in/out/cache, attempts and subagents (Claude's totals include them; Codex's stream doesn't show them). Unknown prices show as unknown, never as zero. Ranking by AI, task and session for today / 7 / 30 days. A budget pauses the task; when the cost is unknown it says the budget can't be enforced, and a call already running may go slightly over.
197
+ - **Click the agent:** a session started by the office shows its task, running vs pending config, cost and the same controls. A session opened elsewhere (terminal, VS Code extension, the Claude or Codex app) shows its real model and effort, the recommendation, and an **Ask this agent** box: the request continues that same conversation in the background (`claude -p --resume` / Codex `thread/resume`) with the model and effort you pick, and from then on it has the task controls. It waits while the agent is busy so two replies never mix; **Open the conversation** jumps straight to it in VS Code, the Claude app or the Codex app (reopen it there if it was already open). A session running in a terminal (Terminal, iTerm or tmux) also gets **Switch in the terminal**: the office types `/model` and `/effort` into Claude, or drives Codex's own `/model` picker choosing *this session only*, so Codex's saved default is untouched (Codex in macOS Terminal needs the Accessibility permission). While it runs the office keeps watching and, when there's a concrete reason (stuck on one step for 10+ min, context almost full, the work looks deeper or lighter than its setup), sends a notice with the recommendation: *Apply* picks it for the next request, *Not now* leaves it.
198
+ - **Cost:** per real model, tokens in/out/cache, attempts and subagents (Claude's totals include them; Codex subagents are read from their own logs, which point at the parent conversation). Unknown prices show as unknown, never as zero. Ranking by AI, task and session for today / 7 / 30 days. A budget pauses the task; when the cost is unknown it says the budget can't be enforced, and a call already running may go slightly over.
191
199
  - **Recommendations:** local rules on scope, wording and attempts plus what similar finished tasks show (success with tests, cost, retries). No AI call to choose. Optional **auto-apply** up to a profile and a number of changes per task, all logged.
192
200
  - **Warnings:** the same action repeated without progress, two failed attempts, a run costing over 3× the median finished task, budget reached or unenforceable. Notices carry the exact proposal: *Apply*, *See why*, *Not now*.
201
+ - **Compact:** shrinks the same session's context (Claude `/compact`, Codex compaction) without running anything else, for office tasks and for conversations opened in VS Code or the app once their context passes 50%.
193
202
  - **Economy mode:** short instructions on demand (read only what's needed, no long logs, no re-exploring), filtered logs, attempt and parallel limits, resume to keep the cache, or a fresh session with a summary when the context is too big.
194
203
 
195
204
  Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folders, results, diffs and notice texts stay hidden.
@@ -198,6 +207,40 @@ Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folder
198
207
 
199
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.
200
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
+
231
+ ## Phone panel (opt-in)
232
+
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.
241
+
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.
243
+
201
244
  ## MCP (read-only)
202
245
 
203
246
  `claude mcp add coworking -- npx coworking-agents mcp` gives other agents `list_agents`, `get_agent_status`, `get_conflicts`, `get_daily_report` and `list_tasks`. Nothing through MCP changes anything.
package/ROADMAP.md CHANGED
@@ -26,8 +26,6 @@ Estimated cost · wait reminder · git state on the door · daily report as Mark
26
26
  - **Seasonal decor** by date (subtle, off with a toggle).
27
27
 
28
28
  ## Tasks — next steps
29
- - Mid-step permission prompts in the page (today denied tools are listed after the step; approving resumes with them allowed).
30
- - Compaction of a headless Claude session (`/compact` through `-p`) once it's confirmed to work; today the option is a fresh session with a summary.
31
- - Codex subagent usage (not in `codex exec --json`).
32
- - CI: left out on purpose for now (GitHub Actions removed); `npm test` runs before every commit.
29
+ - Switching model and effort in VS Code and the desktop apps (only terminals take typing; there it applies from the next request sent from the office).
30
+ - CI: left out for now; `npm test` runs before every commit.
33
31
  - Semantic search over history only if plain text search stops being enough; smarter automatic assignment.
@@ -12,11 +12,16 @@ const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1]
12
12
  if (flag('-h') || flag('--help')) {
13
13
  console.log(`coworking-agents — a pixel office for your AI coding agents (Claude Code, Codex, …)
14
14
 
15
- npx coworking-agents [--port 4777] [--no-open] [--private]
15
+ npx coworking-agents [--port 4777] [--no-open] [--private] [--phone]
16
16
 
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)
24
+ --phone also serve a small phone panel on your Wi-Fi (own token, QR code in the terminal; trusted networks only)
20
25
  --demo [N] show a fake office with every state and N agents (default 10)
21
26
  --json print one snapshot and exit
22
27
  mcp run a read-only MCP server on stdio (claude mcp add coworking -- npx coworking-agents mcp)
@@ -25,6 +30,17 @@ if (flag('-h') || flag('--help')) {
25
30
  }
26
31
 
27
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
+ }
28
44
 
29
45
  if (flag('--json')) {
30
46
  const { snapshot } = require('../src/collect');
@@ -33,11 +49,28 @@ if (flag('--json')) {
33
49
  return;
34
50
  }
35
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
+ }
36
57
  function open(url) {
58
+ if (wantsApp() && openApp(url)) return;
37
59
  const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
38
60
  const a = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
39
61
  execFile(cmd, a, () => {});
40
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
+ }
41
74
 
42
75
  // The token link lives in a file only you can read (0600): a second run reopens the same
43
76
  // office, and a foreign program on port 4777 can't impersonate it (it doesn't have the token).
@@ -58,27 +91,36 @@ async function reuse() {
58
91
  const privacy = flag('--private'), demo = flag('--demo');
59
92
  if (!demo && !privacy && !flag('--port')) {
60
93
  const s = await reuse();
94
+ if (s && flag('--phone')) { console.log(`coworking-agents is already running without the phone panel (${s.base}). Close it (Ctrl+C in its terminal) and start again with --phone.`); return; }
61
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; }
62
96
  }
63
97
  // keep the same token across restarts so an open tab keeps working (file is 0600, only yours)
64
- let token;
65
- 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 {} };
66
102
  let hq;
67
- try { hq = await start({ port, privacy, demo, token }); }
103
+ const phone = flag('--phone');
104
+ const menubar = flag('--menubar');
105
+ try { hq = await start({ port, privacy, demo, token, phone, menubar, phoneToken, onPhoneToken }); }
68
106
  catch (e) {
69
107
  if (e.code !== 'EADDRINUSE') throw e;
70
- hq = await start({ port: 0, privacy, demo, token }); // 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
71
109
  }
72
110
  if (!demo && !privacy) {
73
111
  try {
74
112
  fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
75
- 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 });
76
114
  // the file stays on exit: it keeps the token for the next run (reuse() pings before trusting it)
77
115
  const bye = () => process.exit(0);
78
116
  process.on('SIGINT', bye); process.on('SIGTERM', bye);
79
117
  } catch {}
80
118
  }
81
119
  console.log(`coworking-agents → ${hq.url}${privacy ? ' (private mode)' : ''}\nCtrl+C to close.`);
120
+ if (phone) {
121
+ if (hq.phone) console.log(`\nPhone (same Wi-Fi, trusted networks only — the LAN link is not encrypted) → ${hq.phone.url}\n${require('../src/qr').toTerminal(require('../src/qr').encode(hq.phone.url))}\n`);
122
+ else console.log(`\nPhone panel not started: ${hq.phoneReason === 'nolan' ? 'no local network address found' : hq.phoneReason}`);
123
+ }
82
124
  if (!flag('--no-open')) open(hq.url);
83
125
  if (!demo && !flag('--no-update-check')) {
84
126
  const up = await require('../src/update').startChecks();
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.5.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>
@@ -60,6 +60,7 @@ function achFacts() {
60
60
  models: models.size,
61
61
  skills: Math.max(0, ...cred.map(c => (c.skills || []).length)), mcps: Math.max(0, ...cred.map(c => (c.mcps || []).length)),
62
62
  parallel: achCount('parallel'), aiKinds: achCount('aiKinds'), pets: achCount('pets'), zeroQueue: achCount('zeroQueue'),
63
+ cleanTasks: achCount('cleanTasks'), reviewOk: achCount('reviewOk'), underGoal: achCount('underGoal'),
63
64
  clashes: achCount('clashes'), team: achCount('team'), bigCtx: achCount('bigCtx'), files: achCount('files'), sessionTools: achCount('sessionTools'),
64
65
  fast: achCount('fast'), patience: achCount('patience'), compacts: achCount('compacts'), focusDone: achCount('focusDone'),
65
66
  replay: achCount('replay'), avatars: achCount('avatars'), lights: achCount('lights'), weatherDays: achCount('weatherDays'),
@@ -110,6 +111,9 @@ const ACH = [
110
111
  { id: 'writer', g: 'live', icon: '📄', fact: 'files', steps: [5, 15, 30, 60, 100] },
111
112
  { id: 'workaholic', g: 'live', icon: '⚙', fact: 'sessionTools', steps: [200, 500, 1000, 2000, 4000] },
112
113
  { id: 'bump', g: 'live', icon: '💥', fact: 'clashes', steps: [1, 5, 20, 50, 100] },
114
+ { id: 'cleanrun', g: 'live', icon: '🎯', fact: 'cleanTasks', steps: [10, 25, 50, 100, 250] },
115
+ { id: 'approved', g: 'live', icon: '🔎', fact: 'reviewOk', steps: [1, 5, 20, 50, 100] },
116
+ { id: 'thrifty', g: 'live', icon: '🐷', fact: 'underGoal', steps: [1, 4, 12, 26, 52] },
113
117
  { id: 'fast', g: 'live', icon: '🏃', fact: 'fast', steps: [1, 10, 50, 200, 500] },
114
118
  { id: 'patience', g: 'live', icon: '🗿', fact: 'patience', steps: [1, 5, 15, 40, 100] },
115
119
  { id: 'inbox', g: 'live', icon: '✓', fact: 'zeroQueue', steps: [1, 10, 50, 150, 500] },
@@ -202,7 +206,22 @@ function checkAchievements() {
202
206
  }
203
207
  for (const p of live) if (p.repo) { achAdd('repos', p.repo.name); if (p.branch && p.branch !== 'HEAD') achAdd('branches', p.repo.name + ':' + p.branch); }
204
208
  { const clean = trees.size >= 3 && [...trees.values()].every(g => g.dirty === 0); if (clean && !achClean) achBump('cleanDesk'); achClean = clean; }
209
+ // Sunday: the week's card is ready (once a week)
210
+ if (new Date().getDay() === 0 && usageData && usageData.days && typeof pushNotice === 'function' && store.get('weekCardSaid', '') !== weekKey()) {
211
+ store.set('weekCardSaid', weekKey()); pushNotice('week:' + weekKey(), 'ok', T.share.ready, T.share.readyWhy, { tab: 'paper' });
212
+ }
205
213
  for (const c of data.fileClashes || []) achAdd('clashes', c.file + '|' + c.who.slice().sort().join(','));
214
+ // office tasks: done on the first attempt, a review that said OK, and weeks spent under the month's cost goal
215
+ for (const t of (typeof taskData !== 'undefined' && taskData.tasks) || []) {
216
+ if (t.state === 'done' && t.attempts === 1) achAdd('cleanTasks', t.id);
217
+ if (t.review && /^\s*OK\b/i.test(t.review.text || '')) achAdd('reviewOk', t.id);
218
+ }
219
+ { const goal = Number(((typeof taskData !== 'undefined' && taskData.settings) || {}).monthGoal) || 0, days = (usageData && usageData.days) || {};
220
+ if (goal) {
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
+ 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) { 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
+ } }
206
225
  const need = live.filter(p => p.state === 'needs_you' || p.state === 'waiting').length;
207
226
  if (achQueueWas >= 2 && need === 0) achBump('zeroQueue');
208
227
  achQueueWas = need;
@@ -286,8 +305,20 @@ const shadeHex = (hex, f) => Art.shade(hex, f);
286
305
  if (at === seq.length) { at = 0; achBump('konami'); confettiAt = Date.now(); checkAchievements(); }
287
306
  });
288
307
  }
289
- // confetti over the office (konami, or any legend-tier unlock)
290
- 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
+ }
291
322
  function drawConfetti(t) {
292
323
  const age = Date.now() - confettiAt;
293
324
  if (age > 4000) return;
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
  // ---------------- alerts: rate limits filling up, contexts close to compaction, and the compact action ----------------
3
3
  const CTX_WARN = .75; // from here a "compact" button shows over the agent
4
- const LIMIT_STEPS = [80, 95]; // notify once per window when usage crosses these
4
+ const LIMIT_STEPS = [80, 95];
5
+ const STALL_MS = 10 * 60000; // working, but nothing new in its log for this long // notify once per window when usage crosses these
5
6
  const WAIT_NUDGE = 5 * 60000; // an agent waiting on you this long gets one reminder
6
7
  const waitStart = new Map(); // id → { state, at }: when it started waiting
7
8
  const alerted = new Set();
@@ -22,6 +23,7 @@ function toast(html, kind) {
22
23
  setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, 7000);
23
24
  }
24
25
  function notifyOS(title, body) {
26
+ if (typeof quietNow === 'function' && quietNow()) return; // do not disturb
25
27
  playTune('limit');
26
28
  if (notifyOn && !focusing() && 'Notification' in window && Notification.permission === 'granted' && document.hidden) { try { new Notification(title, { body }); } catch {} }
27
29
  }
@@ -36,11 +38,29 @@ function checkAlerts() {
36
38
  const key = `lim:${id}:${win}:${step}:${w.resetsAt}`;
37
39
  if (w.usedPercent >= step && !alerted.has(key)) {
38
40
  alerted.add(key);
41
+ if (typeof pushNotice === 'function') { const label = `${(AGENT[id] || {}).label || id} · ${win === '5h' ? T.usage.fiveHour : T.usage.week}`; pushNotice(`lim:${id}:${win}`, step >= 95 ? 'warn' : 'ok', `${A.limit(Math.round(w.usedPercent))} · ${label}`, `${T.usage.resets} ${untilText(w.resetsAt)}. ${A.limitTip}`, {}); }
39
42
  if (!firstAlertPass) { const label = `${(AGENT[id] || {}).label || id} · ${win === '5h' ? T.usage.fiveHour : T.usage.week}`; toast(`<b>${esc(A.limit(Math.round(w.usedPercent)))}</b>${esc(label)} · ${esc(T.usage.resets)} ${esc(untilText(w.resetsAt))}`, step >= 95 ? 'bad' : 'warn'); notifyOS(A.limit(Math.round(w.usedPercent)), `${label} · ${T.usage.resets} ${untilText(w.resetsAt)}`); }
40
43
  }
41
44
  }
42
45
  }
43
46
  }
47
+ // pace: at the rate of the last ~hour, will the window run out before it resets? said once per window
48
+ for (const [id, l] of Object.entries(L)) {
49
+ for (const [win, w] of [['5h', l && l.primary], ['week', l && l.secondary]]) {
50
+ if (!w || !w.resetsAt) continue;
51
+ const k = id + ':' + win, list = (paceSamples.get(k) || []).filter(x => x.resetsAt === w.resetsAt && data.now - x.at < 36e5);
52
+ if (!list.length || list[list.length - 1].pct !== w.usedPercent) list.push({ at: data.now, pct: w.usedPercent, resetsAt: w.resetsAt });
53
+ paceSamples.set(k, list); try { store.set('pace', JSON.stringify([...paceSamples])); } catch {}
54
+ const first = list[0], hrs = (data.now - first.at) / 36e5, rate = hrs >= .25 ? (w.usedPercent - first.pct) / hrs : 0; // % per hour
55
+ if (rate <= 0 || w.usedPercent < 40 || w.usedPercent >= 100) continue;
56
+ const left = (100 - w.usedPercent) / rate * 36e5, key = `pace:${k}:${w.resetsAt}`;
57
+ if (data.now + left < w.resetsAt - 6e5 && !alerted.has(key) && !paceSaid.includes(key)) {
58
+ alerted.add(key); paceSaid = [...paceSaid.slice(-20), key]; try { store.set('paceSaid', JSON.stringify(paceSaid)); } catch {} const label = `${(AGENT[id] || {}).label || id} · ${win === '5h' ? T.usage.fiveHour : T.usage.week}`;
59
+ if (typeof pushNotice === 'function') pushNotice('pace:' + k, 'warn', A.pace(label, untilText(data.now + left)), A.paceWhy(Math.round(w.usedPercent), Math.round(rate), untilText(w.resetsAt)), {});
60
+ notifyOS(A.pace(label, untilText(data.now + left)), A.paceWhy(Math.round(w.usedPercent), Math.round(rate), untilText(w.resetsAt)));
61
+ }
62
+ }
63
+ }
44
64
  for (const p of data.people) {
45
65
  const pct = ctxPct(p), key = `ctx:${p.id}:${p.compactedAt || 0}`;
46
66
  if (pct >= .85 && !alerted.has(key)) {
@@ -48,6 +68,16 @@ function checkAlerts() {
48
68
  if (!firstAlertPass) { toast(`<b>${esc(A.context(Math.round(pct * 100)))}</b>${esc(p.name)} · ${esc(p.title || '')}`, 'warn'); notifyOS(A.context(Math.round(pct * 100)), p.name); }
49
69
  }
50
70
  }
71
+ // stalled: says it is working but wrote nothing to its log for a while (stuck, looping on a slow command, waiting on the network)
72
+ for (const p of data.people) {
73
+ if (p.leaving || !p.lastActivity || ['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)) continue;
74
+ const quiet = data.now - p.lastActivity, key = `stall:${p.id}:${p.lastActivity}`;
75
+ if (quiet >= STALL_MS && !alerted.has(key)) {
76
+ alerted.add(key); const mins = Math.round(quiet / 60000), what = p.doing ? `${p.doing.tool}${p.doing.what ? ' ' + p.doing.what : ''}` : '';
77
+ if (typeof pushNotice === 'function') pushNotice('stall:' + p.id, 'warn', A.stall(p.name, mins), A.stallWhy(what), { agent: p.id });
78
+ if (!firstAlertPass) notifyOS(A.stall(p.name, mins), A.stallWhy(what));
79
+ }
80
+ }
51
81
  // waiting on you for a while (a question or a command to approve): one reminder per wait
52
82
  const seen = new Set();
53
83
  for (const p of data.people) {
@@ -74,6 +104,8 @@ function checkAlerts() {
74
104
  }
75
105
  firstAlertPass = false;
76
106
  }
107
+ let paceSaid = []; try { paceSaid = JSON.parse(store.get('paceSaid', '[]')) || []; } catch { paceSaid = []; }
108
+ let paceSamples = new Map(); try { paceSamples = new Map(JSON.parse(store.get('pace', '[]'))); } catch { paceSamples = new Map(); }
77
109
  let firstAlertPass = true; // what's already true on load is noted, not announced
78
110
 
79
111
  async function compactAgent(id, btn) {
@@ -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];
@@ -100,6 +100,8 @@ const SHOP = [
100
100
  { id: 'bigtank', price: 900, icon: '🐠' },
101
101
  { id: 'cap', price: 150, icon: '🧢' }, { id: 'headphones', price: 300, icon: '🎧' }, { id: 'shades', price: 250, icon: '🕶' }, { id: 'party', price: 200, icon: '🥳' },
102
102
  { id: 'shirt_pink', price: 300, icon: '👕' }, { id: 'shirt_black', price: 300, icon: '👕' }, { id: 'shirt_gold', price: 400, icon: '👕' }, { id: 'crown', price: 1500, icon: '👑' },
103
+ { id: 'beanie', price: 200, icon: '🧶' }, { id: 'bow', price: 150, icon: '🎀' }, { id: 'wizard', price: 700, icon: '🧙' }, { id: 'glasses', price: 200, icon: '👓' }, { id: 'mustache', price: 150, icon: '🥸' },
104
+ { id: 'shirt_green', price: 300, icon: '👕' }, { id: 'shirt_blue', price: 300, icon: '👕' },
103
105
  ];
104
106
  const owns = id => prog.owned.includes(id);
105
107
  function buy(id) {
@@ -113,7 +115,7 @@ function renderShop() {
113
115
  const S2 = T.shop, body = reportEl.querySelector('.report-body'), c = coins();
114
116
  body.innerHTML = tabsHtml() + `<div class="wallet"><span class="coin"></span><b>${c.toLocaleString(lang === 'pt' ? 'pt-BR' : 'en')}</b> ${esc(T.ach.coins)}<span class="note">${esc(S2.sub)}</span></div>
115
117
  <div class="shop">${SHOP.map(it => { const has = owns(it.id), can = c >= it.price; return `<div class="item ${has ? 'owned' : ''}"><span class="ach-ico">${it.icon}</span><div><b>${esc(S2.items[it.id].name)}</b><p>${esc(S2.items[it.id].desc)}</p>
116
- ${has ? `<small class="ok">${esc(S2.owned)}</small> <button class="btn small" data-move-item>${esc(S2.move)}</button>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>
118
+ ${has && typeof OUTFIT !== 'undefined' && OUTFIT[it.id] ? `<small class="ok">${esc(S2.inWardrobe)}</small> <button class="btn small" data-tab="office">${esc(S2.wear)}</button>` : has ? `<small class="ok">${esc(S2.owned)}</small> <button class="btn small" data-move-item>${esc(S2.move)}</button>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>
117
119
  ${prog.owned.length ? `<p><button class="btn small" data-move-item>✥ ${esc(S2.decoTitle)}</button></p>` : ''}`;
118
120
  }
119
121
  reportEl.addEventListener('click', e => { if (e.target.closest('[data-share-card]')) return shareCard(); if (e.target.closest('[data-visit-photo]')) return visitPhoto(); const b = e.target.closest('[data-buy]'); if (b) buy(b.dataset.buy); if (e.target.closest('[data-move-item]')) setDecorating(true); });
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 ----