coworking-agents 0.4.0 โ†’ 0.6.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 (53) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +23 -10
  3. package/ROADMAP.md +4 -5
  4. package/bin/coworking-agents.js +10 -3
  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/js/achievements.js +19 -0
  26. package/public/js/alerts.js +32 -1
  27. package/public/js/collection.js +3 -1
  28. package/public/js/fun2.js +15 -2
  29. package/public/js/help.js +11 -2
  30. package/public/js/i18n.js +112 -14
  31. package/public/js/overlay.js +2 -1
  32. package/public/js/panel.js +3 -1
  33. package/public/js/personal.js +29 -1
  34. package/public/js/player.js +1 -0
  35. package/public/js/replay.js +6 -3
  36. package/public/js/report.js +41 -4
  37. package/public/js/scene.js +6 -4
  38. package/public/js/tasks.js +487 -106
  39. package/public/js/topbar.js +12 -1
  40. package/public/phone/index.html +16 -0
  41. package/public/phone/phone.css +19 -0
  42. package/public/phone/phone.js +51 -0
  43. package/public/style.css +102 -5
  44. package/src/collect.js +32 -3
  45. package/src/demo.js +4 -4
  46. package/src/focus.js +113 -6
  47. package/src/phone.js +88 -0
  48. package/src/qr.js +116 -0
  49. package/src/server.js +58 -12
  50. package/src/sources/codex.js +13 -3
  51. package/src/tasks.js +454 -128
  52. package/src/update.js +27 -8
  53. package/src/usage.js +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 โ€” 2026-10-13
4
+ - 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
+ - Backup: export and import templates, queue and economy settings, cost goal, decor, outfits and progress as one JSON file (Personalise tab)
6
+ - Achievements: First try (tasks done without a retry), Approved (reviews that said OK), Penny-wise (weeks under the cost goal)
7
+ - Weekly card: best day, the AI used most and the week's cost; a notice on Sundays when it is ready
8
+ - 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
9
+ - Agents that say they are working but write nothing for 10 minutes get a notice
10
+ - File clashes land in the notices, with a pause button when one side is an office task
11
+ - Month's cost goal in Usage: spent so far, end-of-month projection, and a notice when the pace or the total goes over
12
+ - Shop: beanie, bow, wizard hat, round glasses, mustache, green and blue shirts; bought outfits say *Wear* and open the wardrobe instead of *Move*
13
+ - 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
14
+ - 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
15
+ - Task templates: save a request you repeat with its folder and settings; fill the form or create and run with one click
16
+ - The new-task form stays open and keeps what you typed across the periodic refresh
17
+ - 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
18
+ - 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
19
+ - 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
20
+ - Codex subagent cost counted from the subagents' own logs, once
21
+ - Compact: shrink a task's or an outside conversation's context in the background (Claude `/compact`, Codex compaction), keeping the same session
22
+ - 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
23
+ - Codex tasks run through `codex app-server`, the mode where Codex can ask before leaving its sandbox; cost per model call, counted once
24
+ - "Open the conversation" jumps to the exact conversation in VS Code, the Claude app or the Codex app
25
+ - 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
26
+ - 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)
27
+ - Sessions opened outside show their real effort (Codex from its log, Claude from its settings)
28
+ - Task detail shows current, pending and recommended config once, in the recommendation card
29
+ - Update check every hour and when the releases tab opens
30
+
31
+ ## 0.5.0 โ€” 2026-10-11
32
+ - 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
33
+ - Codex models and efforts come from Codex's own model list; Claude efforts low to max
34
+ - Cost per real model id, unknown cost shown as unknown (never as zero), no double counting on resumed runs; budget says when it can't be enforced
35
+ - Ranking by AI, task and session with the spend that happened in the chosen period
36
+ - Clean retries, full diff since the task started (commits, staged, new files), worktree errors reported, no conclude while running, cancel waits for the process, running agents survive a server restart
37
+ - Roles (Backend, Frontend, Reviewer), economy mode, filtered test logs, attempt limit, new session with a summary, optional auto-apply within limits
38
+ - Notices keep arriving with the tab in the background and recover what was pending; Apply applies exactly what the notice proposed; private mode hides tasks and notices
39
+ - Releases: notes of the newer version fetched from its package, install-aware update command, "unavailable" when npm can't be reached
40
+ - Search across tasks, live sessions, today and this month; replay includes sessions that ended in the last hour
41
+
3
42
  ## 0.4.0 โ€” 2026-10-11
4
43
  - Tasks: create a task, pick folder, agent and profile; it runs in the background (Claude Code or Codex), in its own worktree if you want
5
44
  - Answer questions, approve blocked tools, review the diff and test results, conclude, hand off or retry โ€” all from the office
package/README.md CHANGED
@@ -180,20 +180,33 @@ 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** (โ˜ฐ โ†’ report tabs) and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*) and a profile. 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
+
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
+ - **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
+ - **Templates:** save a request you repeat (folder, agent, profile, role, test command) and fire it again with one click.
190
+ - **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
191
+ - **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.
192
+ - **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.
193
+ - **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.
194
+ - **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.
195
+ - **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*.
196
+ - **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%.
197
+ - **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.
198
+
199
+ Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folders, results, diffs and notice texts stay hidden.
184
200
 
185
- - **Answer** its questions and **approve** tools it was not allowed to use; it resumes the same session.
186
- - **Review** the result, the files changed, the diff and your test command's output, then **conclude**, **retry** or **hand off** to the other agent (with a summary of what was done).
187
- - **Queue:** priority, "after task X" dependencies and how many agents run at once.
188
- - **Cost:** per task (tokens in/out/cache, estimated USD, attempts), a ranking by task and by AI for today / 7 / 30 days, and an optional budget that pauses the task when reached.
189
- - **Profiles and controls:** Economy, Balanced, Deep. *Save*, *More capacity*, *Apply recommendation* or pick model and effort by hand; changes apply from the next step and are logged. The recommendation is plain rules plus what similar past tasks taught (no AI call to choose).
190
- - **Warnings:** the same action repeated without progress, two failed attempts, budget reached.
201
+ ## Notices, releases and the office admin
191
202
 
192
- Only tasks created here can be controlled. Sessions found by monitoring stay read-only. Costs are estimates at API prices, not your bill or plan percentage. Tasks live in `~/.config/coworking-agents/tasks/`.
203
+ 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.
193
204
 
194
- ## Notices, releases and the office admin
205
+ ## Phone panel (opt-in)
206
+
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.
195
208
 
196
- 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. **Releases** shows the installed version, the newest on npm, when it checked and what changed, with the update command to copy. The office admin waves a sign while a new version is unseen, keeps it until you run it, then celebrates.
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.
197
210
 
198
211
  ## MCP (read-only)
199
212
 
package/ROADMAP.md CHANGED
@@ -25,8 +25,7 @@ Estimated cost ยท wait reminder ยท git state on the door ยท daily report as Mark
25
25
 
26
26
  - **Seasonal decor** by date (subtle, off with a toggle).
27
27
 
28
- ## Tasks (0.4.0) โ€” next steps
29
- - Automatic assignment beyond "least busy agent"; per-profile instructions (Backend, Frontend, Reviewer).
30
- - Mid-run permission prompts in the page (today: denied tools are listed after the step and you approve to resume).
31
- - Compare similar tasks by tests, cost and retries to tune recommendations further; optional auto-apply.
32
- - Semantic search over history only if plain text search stops being enough.
28
+ ## Tasks โ€” next steps
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.
31
+ - Semantic search over history only if plain text search stops being enough; smarter automatic assignment.
@@ -12,11 +12,12 @@ 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
+ --phone also serve a small phone panel on your Wi-Fi (own token, QR code in the terminal; trusted networks only)
20
21
  --demo [N] show a fake office with every state and N agents (default 10)
21
22
  --json print one snapshot and exit
22
23
  mcp run a read-only MCP server on stdio (claude mcp add coworking -- npx coworking-agents mcp)
@@ -58,16 +59,18 @@ async function reuse() {
58
59
  const privacy = flag('--private'), demo = flag('--demo');
59
60
  if (!demo && !privacy && !flag('--port')) {
60
61
  const s = await reuse();
62
+ 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
63
  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
64
  }
63
65
  // keep the same token across restarts so an open tab keeps working (file is 0600, only yours)
64
66
  let token;
65
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 {} }
66
68
  let hq;
67
- try { hq = await start({ port, privacy, demo, token }); }
69
+ const phone = flag('--phone');
70
+ try { hq = await start({ port, privacy, demo, token, phone }); }
68
71
  catch (e) {
69
72
  if (e.code !== 'EADDRINUSE') throw e;
70
- hq = await start({ port: 0, privacy, demo, token }); // port taken by something else: use a free one
73
+ hq = await start({ port: 0, privacy, demo, token, phone }); // port taken by something else: use a free one
71
74
  }
72
75
  if (!demo && !privacy) {
73
76
  try {
@@ -79,6 +82,10 @@ async function reuse() {
79
82
  } catch {}
80
83
  }
81
84
  console.log(`coworking-agents โ†’ ${hq.url}${privacy ? ' (private mode)' : ''}\nCtrl+C to close.`);
85
+ if (phone) {
86
+ 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`);
87
+ else console.log(`\nPhone panel not started: ${hq.phoneReason === 'nolan' ? 'no local network address found' : hq.phoneReason}`);
88
+ }
82
89
  if (!flag('--no-open')) open(hq.url);
83
90
  if (!demo && !flag('--no-update-check')) {
84
91
  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.4.0",
3
+ "version": "0.6.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"
@@ -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) achAdd('underGoal', keys[keys.length - 1]); // one per finished 7-day window
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;
@@ -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();
@@ -36,11 +37,29 @@ function checkAlerts() {
36
37
  const key = `lim:${id}:${win}:${step}:${w.resetsAt}`;
37
38
  if (w.usedPercent >= step && !alerted.has(key)) {
38
39
  alerted.add(key);
40
+ 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
41
  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
42
  }
41
43
  }
42
44
  }
43
45
  }
46
+ // pace: at the rate of the last ~hour, will the window run out before it resets? said once per window
47
+ for (const [id, l] of Object.entries(L)) {
48
+ for (const [win, w] of [['5h', l && l.primary], ['week', l && l.secondary]]) {
49
+ if (!w || !w.resetsAt) continue;
50
+ const k = id + ':' + win, list = (paceSamples.get(k) || []).filter(x => x.resetsAt === w.resetsAt && data.now - x.at < 36e5);
51
+ if (!list.length || list[list.length - 1].pct !== w.usedPercent) list.push({ at: data.now, pct: w.usedPercent, resetsAt: w.resetsAt });
52
+ paceSamples.set(k, list); try { store.set('pace', JSON.stringify([...paceSamples])); } catch {}
53
+ const first = list[0], hrs = (data.now - first.at) / 36e5, rate = hrs >= .25 ? (w.usedPercent - first.pct) / hrs : 0; // % per hour
54
+ if (rate <= 0 || w.usedPercent < 40 || w.usedPercent >= 100) continue;
55
+ const left = (100 - w.usedPercent) / rate * 36e5, key = `pace:${k}:${w.resetsAt}`;
56
+ if (data.now + left < w.resetsAt - 6e5 && !alerted.has(key) && !paceSaid.includes(key)) {
57
+ 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}`;
58
+ 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)), {});
59
+ notifyOS(A.pace(label, untilText(data.now + left)), A.paceWhy(Math.round(w.usedPercent), Math.round(rate), untilText(w.resetsAt)));
60
+ }
61
+ }
62
+ }
44
63
  for (const p of data.people) {
45
64
  const pct = ctxPct(p), key = `ctx:${p.id}:${p.compactedAt || 0}`;
46
65
  if (pct >= .85 && !alerted.has(key)) {
@@ -48,6 +67,16 @@ function checkAlerts() {
48
67
  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
68
  }
50
69
  }
70
+ // stalled: says it is working but wrote nothing to its log for a while (stuck, looping on a slow command, waiting on the network)
71
+ for (const p of data.people) {
72
+ if (p.leaving || !p.lastActivity || ['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)) continue;
73
+ const quiet = data.now - p.lastActivity, key = `stall:${p.id}:${p.lastActivity}`;
74
+ if (quiet >= STALL_MS && !alerted.has(key)) {
75
+ alerted.add(key); const mins = Math.round(quiet / 60000), what = p.doing ? `${p.doing.tool}${p.doing.what ? ' ' + p.doing.what : ''}` : '';
76
+ if (typeof pushNotice === 'function') pushNotice('stall:' + p.id, 'warn', A.stall(p.name, mins), A.stallWhy(what), { agent: p.id });
77
+ if (!firstAlertPass) notifyOS(A.stall(p.name, mins), A.stallWhy(what));
78
+ }
79
+ }
51
80
  // waiting on you for a while (a question or a command to approve): one reminder per wait
52
81
  const seen = new Set();
53
82
  for (const p of data.people) {
@@ -74,6 +103,8 @@ function checkAlerts() {
74
103
  }
75
104
  firstAlertPass = false;
76
105
  }
106
+ let paceSaid = []; try { paceSaid = JSON.parse(store.get('paceSaid', '[]')) || []; } catch { paceSaid = []; }
107
+ let paceSamples = new Map(); try { paceSamples = new Map(JSON.parse(store.get('pace', '[]'))); } catch { paceSamples = new Map(); }
77
108
  let firstAlertPass = true; // what's already true on load is noted, not announced
78
109
 
79
110
  async function compactAgent(id, btn) {
@@ -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/fun2.js CHANGED
@@ -42,8 +42,8 @@ function missionsHtml() {
42
42
  }
43
43
 
44
44
  // ---- your outfit: hats, shades and shirts from the shop, worn by your character ----
45
- const OUTFIT = { cap: 'hat', crown: 'hat', headphones: 'hat', party: 'hat', shades: 'face', shirt_gold: 'shirt', shirt_pink: 'shirt', shirt_black: 'shirt' };
46
- const SHIRTS = { shirt_gold: '#d9a441', shirt_pink: '#e8579a', shirt_black: '#2a2630' };
45
+ const OUTFIT = { cap: 'hat', crown: 'hat', headphones: 'hat', party: 'hat', beanie: 'hat', bow: 'hat', wizard: 'hat', shades: 'face', glasses: 'face', mustache: 'face', shirt_gold: 'shirt', shirt_pink: 'shirt', shirt_black: 'shirt', shirt_green: 'shirt', shirt_blue: 'shirt' };
46
+ const SHIRTS = { shirt_gold: '#d9a441', shirt_pink: '#e8579a', shirt_black: '#2a2630', shirt_green: '#3fa66b', shirt_blue: '#3b5dc9' };
47
47
  function wearing(slot) { const w = (prog.style || {})['wear_' + slot]; return w && owns(w) ? w : ''; }
48
48
  function youLook() {
49
49
  const lk = Art.look('you:' + (avatars.__you || 0)), sh = wearing('shirt');
@@ -55,6 +55,11 @@ function drawOutfit(x, y) { // over the standing sprite's head (top at y - 2)
55
55
  if (hat === 'crown') { r(x + 3, y - 4, 10, 4, '#ffd84d'); r(x + 3, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 7, 2, 3, '#ffd84d'); r(x + 11, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 3, 2, 1, '#e43b44'); }
56
56
  if (hat === 'headphones') { r(x + 1, y - 3, 14, 2, PAL.ink); r(x, y + 3, 3, 5, '#3b5dc9'); r(x + 13, y + 3, 3, 5, '#3b5dc9'); }
57
57
  if (hat === 'party') Art.drawHat('party', x, y - 2);
58
+ if (hat === 'beanie') { r(x + 2, y - 3, 12, 4, '#3b5dc9'); r(x + 2, y, 12, 1, '#26408f'); r(x + 4, y - 2, 1, 2, '#5b7ce0'); r(x + 8, y - 2, 1, 2, '#5b7ce0'); r(x + 7, y - 5, 2, 2, '#ffffff'); }
59
+ if (hat === 'bow') { r(x + 10, y - 2, 2, 3, '#e8579a'); r(x + 14, y - 2, 2, 3, '#e8579a'); r(x + 12, y - 1, 2, 2, '#b8336f'); }
60
+ if (hat === 'wizard') { r(x + 1, y - 1, 14, 2, '#5a3a9c'); r(x + 4, y - 4, 8, 3, '#6b45b8'); r(x + 6, y - 7, 5, 3, '#6b45b8'); r(x + 8, y - 9, 3, 2, '#6b45b8'); r(x + 7, y - 3, 1, 1, '#ffd84d'); r(x + 9, y - 6, 1, 1, '#ffd84d'); }
61
+ if (wearing('face') === 'glasses') { r(x + 3, y + 4, 4, 1, PAL.ink); r(x + 3, y + 6, 4, 1, PAL.ink); r(x + 3, y + 5, 1, 1, PAL.ink); r(x + 6, y + 5, 1, 1, PAL.ink); r(x + 9, y + 4, 4, 1, PAL.ink); r(x + 9, y + 6, 4, 1, PAL.ink); r(x + 9, y + 5, 1, 1, PAL.ink); r(x + 12, y + 5, 1, 1, PAL.ink); r(x + 7, y + 5, 2, 1, PAL.ink); }
62
+ if (wearing('face') === 'mustache') { r(x + 5, y + 8, 6, 1, '#5a3a22'); r(x + 4, y + 9, 2, 1, '#5a3a22'); r(x + 10, y + 9, 2, 1, '#5a3a22'); }
58
63
  if (wearing('face') === 'shades') { r(x + 3, y + 5, 10, 2, PAL.ink); r(x + 4, y + 5, 3, 1, '#3a4566'); r(x + 9, y + 5, 3, 1, '#3a4566'); }
59
64
  }
60
65
  function outfitHtml() {
@@ -219,6 +224,14 @@ function shareCard() {
219
224
  g.fillStyle = '#c0cbdc'; g.font = '14px system-ui, sans-serif'; g.fillText(T.share.week, 24, 76);
220
225
  const stat = (y, big, label) => { g.fillStyle = '#ffffff'; g.font = 'bold 30px system-ui, sans-serif'; g.fillText(big, 24, y); g.fillStyle = '#9a9187'; g.font = '13px system-ui, sans-serif'; g.fillText(label, 24, y + 18); };
221
226
  stat(126, hours.toFixed(1) + 'h', T.share.hours); stat(186, fmtK(tools), T.share.actions); stat(246, `${tiers} ยท ${legends}โ˜…`, T.share.trophies);
227
+ // the week in three lines: best day, the AI used most, what it cost (estimated)
228
+ const best = days.slice().sort((a, b) => (d(b).activeMs || 0) - (d(a).activeMs || 0))[0], usd = days.reduce((n, k) => n + (d(k).usd || 0), 0);
229
+ const byAi = {}; for (const k of days) for (const [ai, v] of Object.entries(d(k).byAgent || {})) byAi[ai] = (byAi[ai] || 0) + (v.activeMs || 0);
230
+ const topAi = Object.entries(byAi).sort((a, b) => b[1] - a[1])[0];
231
+ const line = (y, label, value) => { g.fillStyle = '#9a9187'; g.font = '13px system-ui, sans-serif'; g.fillText(label, 300, y); g.fillStyle = '#ffffff'; g.font = 'bold 14px system-ui, sans-serif'; g.fillText(value, 420, y); };
232
+ if (best && d(best).activeMs) line(232, T.share.bestDay, `${new Date(best + 'T12:00:00').toLocaleDateString(lang === 'pt' ? 'pt-BR' : 'en', { weekday: 'long' })} ยท ${(d(best).activeMs / 36e5).toFixed(1)}h`);
233
+ if (topAi) line(254, T.share.topAi, (AGENT[topAi[0]] || {}).label || topAi[0]);
234
+ if (usd) line(276, T.share.cost, fmtUSD(usd));
222
235
  g.fillStyle = '#d97757'; g.fillRect(0, 290, 600, 25); g.fillStyle = '#ffffff'; g.font = 'bold 13px system-ui, sans-serif'; g.fillText('npx coworking-agents', 24, 307);
223
236
  c.toBlob(b => { if (!b) return; const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `coworking-week-${weekKey()}.png`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); toast(`<b>${esc(T.share.saved)}</b>${esc(a.download)}`, 'ok'); achBump('shares'); }, 'image/png');
224
237
  }
package/public/js/help.js CHANGED
@@ -72,7 +72,8 @@ function sceneBox() {
72
72
  const x0 = Math.max(Math.round(W * .3), Math.ceil((c.right - b0.left) / DOCK_S) + 18);
73
73
  // the office must end before the tools (and their speech clouds before the "+N" tag)
74
74
  const tools = document.querySelector('.bar .tools'), tl = tools ? (tools.getBoundingClientRect().left - b0.left) / DOCK_S : W;
75
- const fit = Math.max(1, Math.min(4, Math.floor((tl - x0 - 30) / SLOT)));
75
+ const reserve = typeof drawLucas === 'function' && adminState !== 'idle' ? 60 : 0; // room for the admin and his sign
76
+ const fit = Math.max(1, Math.min(4, Math.floor((tl - x0 - 30 - reserve) / SLOT)));
76
77
  if (fit !== PER_OFFICE) { PER_OFFICE = fit; if (data) renderDock(); }
77
78
  return { W, H, x0, desk: H - 7 };
78
79
  }
@@ -120,13 +121,21 @@ function drawDock(t) {
120
121
  ctx.save(); ctx.globalCompositeOperation = 'lighter';
121
122
  for (const lx of lamps) { const g = ctx.createRadialGradient(lx + 5, desk - 6, 0, lx + 5, desk - 6, 30); g.addColorStop(0, 'rgba(255,190,90,.35)'); g.addColorStop(1, 'rgba(255,190,90,0)'); ctx.fillStyle = g; ctx.fillRect(lx - 30, desk - 40, 70, 40); }
122
123
  ctx.restore();
124
+ // the office admin stands behind the counter in the monitor's place, only while there's version news (tasks.js)
125
+ const adminOn = typeof drawLucas === 'function' && adminState !== 'idle';
126
+ if (adminOn) {
127
+ const tl0 = document.querySelector('.bar .tools').getBoundingClientRect().left - bar.getBoundingClientRect().left;
128
+ const ax = Math.max(x0 + PER_OFFICE * SLOT + 44, Math.floor(tl0 / DOCK_S) - 22), ay = desk - 18;
129
+ drawLucas(dockCtx, ax, ay, 1, t);
130
+ Object.assign(adminEl.style, { left: (ax - 30) * DOCK_S + 'px', top: Math.max(0, ay - 2) * DOCK_S + 'px', width: 46 * DOCK_S + 'px', height: (desk - ay + 2) * DOCK_S + 'px' });
131
+ }
123
132
  // the counter
124
133
  r(0, desk, W, 2, '#dc9a6c'); r(0, desk + 2, W, H - desk - 2, '#8f4f3a'); r(0, H - 1, W, 1, PAL.ink);
125
134
  for (const lx of lamps) Art.drawLamp(lx, desk - 12, true);
126
135
  // retro monitor sign on the right
127
136
  const tl = document.querySelector('.bar .tools').getBoundingClientRect().left - bar.getBoundingClientRect().left;
128
137
  const mx = Math.floor(tl / DOCK_S) - 40;
129
- if (mx > x0 + PER_OFFICE * SLOT + 20) {
138
+ if (!adminOn && mx > x0 + PER_OFFICE * SLOT + 20) {
130
139
  r(mx - 2, desk - 26, 34, 24, PAL.ink); r(mx, desk - 24, 30, 20, '#0c1410');
131
140
  for (let i = 0; i < 4; i++) r(mx + 3, desk - 21 + i * 4, [18, 16, 20, 14][i], 2, '#3ddc84');
132
141
  if (f % 2) r(mx + 3 + 15, desk - 9, 3, 2, '#3ddc84');