coworking-agents 0.3.0 → 0.4.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 — 2026-10-11
4
+ - 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
+ - Answer questions, approve blocked tools, review the diff and test results, conclude, hand off or retry — all from the office
6
+ - Queue with priority, dependencies, max parallel agents and automatic agent choice
7
+ - Cost ranking by task and by AI (day, week, month), budget per task, Save / More capacity / Recommended / manual model and effort
8
+ - Recommendations with the reason, warnings for loops, retries and budget
9
+ - Notification center and a releases board with the office admin
10
+ - Read-only MCP server: `coworking-agents mcp`
11
+ - Linux: send text to agents running in tmux
12
+ - Private mode hides repo names and titles in the report and usage ranking
13
+
14
+ ## 0.3.1 — 2026-10-10
15
+ - Crisp Bug Catcher, redesigned rooftop and server room
package/README.md CHANGED
@@ -1,23 +1,67 @@
1
1
  <p align="center">
2
- <img src="docs/logo.png" width="128" alt="coworking-agents logo">
2
+ <img src="docs/logo.png" width="112" alt="coworking-agents logo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">coworking-agents</h1>
6
6
 
7
- <p align="center"><b>A live pixel-art office for your AI coding agents.</b><br>Claude Code · Codex · walk around · trophies · zero dependencies · local only</p>
7
+ <p align="center"><b>A live pixel-art office for your AI coding agents.</b><br>Every Claude Code and Codex session becomes a person at a desk.</p>
8
8
 
9
- <p align="center"><a href="https://www.npmjs.com/package/coworking-agents"><img src="https://img.shields.io/npm/v/coworking-agents" alt="npm"></a></p>
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/coworking-agents"><img src="https://img.shields.io/npm/v/coworking-agents?color=e8804a" alt="npm version"></a>
11
+ <a href="https://www.npmjs.com/package/coworking-agents"><img src="https://img.shields.io/npm/dm/coworking-agents?color=4f7fd9" alt="npm downloads"></a>
12
+ <img src="https://img.shields.io/badge/node-%E2%89%A518-5bbf6a" alt="Node 18+">
13
+ <img src="https://img.shields.io/badge/dependencies-0-5bbf6a" alt="zero dependencies">
14
+ <img src="https://img.shields.io/badge/runs-100%25%20local-c75fa8" alt="runs 100% local">
15
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-8a8494" alt="MIT license"></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="https://portfolio-delta-five-78.vercel.app/demos/coworking/index.html?lang=en"><b>▶ Try the live demo</b></a> ·
20
+ <a href="#quick-start">Quick start</a> ·
21
+ <a href="#see-it-in-action">See it in action</a> ·
22
+ <a href="#how-it-works">How it works</a> ·
23
+ <a href="#as-a-claude-code-plugin">Claude Code plugin</a>
24
+ </p>
25
+
26
+ <p align="center"><img src="docs/demo.webp" alt="The office at night: agents typing at their desks, toasts asking for you, the camera flying in to one agent and its side panel"></p>
27
+
28
+ At a glance you see who is working, who needs you (and what they are asking), whose turn it is, and which sessions are editing the same checkout, even across different AIs. Reply to an agent or jump to its terminal tab straight from the office.
29
+
30
+ ## Try it in 10 seconds
31
+
32
+ ```bash
33
+ npx coworking-agents --demo # a fake office with every state, no sessions needed
34
+ npx coworking-agents # your real sessions
35
+ ```
36
+
37
+ Or **[open the live demo](https://portfolio-delta-five-78.vercel.app/demos/coworking/index.html?lang=en)** in your browser: nothing to install. It runs the same `--demo` office (with no server and no AI calls), so you can click the agents, open the panels and walk around.
10
38
 
11
- **A live pixel-art office for your AI coding agents.** Every open session (Claude Code, Codex, …) becomes a person at a desk. At a glance you see who is working, who needs you (and what they are asking), whose turn it is, and which sessions are editing the same checkout, even across different AIs.
39
+ ## See it in action
12
40
 
13
- ![The office by day](docs/office-day.png)
41
+ <p align="center"><img src="docs/tour.webp" alt="Tour: the office, the camera flying in to an agent with its side panel, talking to an agent that finished, the usage report, the server room and the rooftop"></p>
14
42
 
15
- ![The office at night: monitors and desk lamps light the room](docs/office-night.png)
43
+ <p align="center"><sub>The office → click an agent (camera + side panel) → talk to one that is waiting → usage report → server room → rooftop.</sub></p>
44
+
45
+ ## How it works
46
+
47
+ ```mermaid
48
+ flowchart LR
49
+ CC["Claude Code<br/>~/.claude sessions + transcripts"] --> S
50
+ CX["Codex<br/>~/.codex sessions"] --> S
51
+ OT["Gemini CLI, Aider, OpenCode…<br/>running processes"] --> S
52
+ S["Local Node server<br/>zero dependencies · 127.0.0.1"] -- "state of every session, every second (SSE)" --> O["The office in your browser<br/>pixel-art canvas"]
53
+ O -- "reply · answer · compact · go to terminal" --> T["Your Terminal / iTerm tab"]
54
+ ```
55
+
56
+ 1. **Read, never write.** The server reads the files each AI already keeps on disk (the live-session registry and the transcripts). No hooks, nothing installed into the AIs, no model is ever called.
57
+ 2. **One snapshot per second.** It works out what every session is doing (editing, in the terminal, thinking, delegating, waiting for you) and streams it to the page over Server-Sent Events.
58
+ 3. **Drawn as an office.** One room per repository, one desk per session, the kitchen when it's your turn, the meeting room when it delegates, a red **!** when it needs you.
16
59
 
17
60
  ## Tour
18
61
 
19
62
  | | |
20
63
  |---|---|
64
+ | ![The office by day](docs/office-day.png) | **The office.** One room per repository, a desk per session, a shared wing with the kitchen, the glass meeting room and the nap corner. |
21
65
  | ![Top bar](docs/top-bar.png) | **Top bar.** One HUD with the counters and the usage meters; the counter below is where agents waiting on you sit (4 per office, **+** slides to the next). |
22
66
  | ![Talk](docs/talk.png) | **Talk.** Click a waiting agent: it says what it wants at its desk. Reply right there (typed into its Terminal/iTerm tab for Claude Code and Codex CLI) or jump to it. |
23
67
  | ![Camera](docs/camera.png) | **Camera.** Click an agent and the camera flies in to its desk with a spotlight; the side panel shows everything about the session. |
@@ -134,6 +178,27 @@ Agents away from their desks (kitchen, meeting room, nap corner) carry a name ta
134
178
  - **Focus mode:** sounds and system notifications pause for 25 or 50 minutes.
135
179
  - **Replay:** scrub or play back the last hour.
136
180
 
181
+ ## Tasks: hand work to agents from the office
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:
184
+
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.
191
+
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/`.
193
+
194
+ ## Notices, releases and the office admin
195
+
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.
197
+
198
+ ## MCP (read-only)
199
+
200
+ `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.
201
+
137
202
  ## Walk around (Gather-style)
138
203
 
139
204
  You're in the office too, with a **YOU** tag. **WASD** or the arrow keys walk (hold **Shift** to run), a click on the floor walks you there through the doors, and walls stop you. Near an agent, a prop or a door, **Enter** talks, opens or enters. Turn it off in the ☰ menu.
@@ -209,7 +274,8 @@ Click the elevator (or press Enter next to it) to ride between floors, up to the
209
274
  |---|---|---|---|
210
275
  | See sessions (Claude Code, Codex, other CLIs) | ✓ | ✓ | ✓ |
211
276
  | Go to terminal | exact tab (Terminal, iTerm2) | window (needs `xdotool` or `wmctrl`) | window |
212
- | Reply / answer / compact from the office | ✓ | copy and go | copy and go |
277
+ | Reply / answer / compact from the office | ✓ | tmux, else copy and go | tmux, else copy and go |
278
+ | Tasks in the background | ✓ | ✓ | ✓ (needs the CLIs on PATH) |
213
279
 
214
280
  Linux and Windows support is written against their process tools but has not been tested on real machines yet; reports welcome. See the [roadmap](https://github.com/ChaconLucas/coworking-agents/blob/main/ROADMAP.md) for what's waiting on testing and what's next.
215
281
 
@@ -288,6 +354,7 @@ module.exports = {
288
354
  npm test # builds fake ~/.claude and ~/.codex folders and checks the collector and the server
289
355
  npm run demo # the fake office with every state
290
356
  npm run screenshots # regenerates docs/*.png from demo mode (needs agent-browser on PATH)
357
+ npm run video # re-records docs/demo.webp and docs/tour.webp (needs agent-browser and img2webp)
291
358
  node bin/coworking-agents.js --json # one snapshot of the state, as JSON
292
359
  ```
293
360
 
package/ROADMAP.md CHANGED
@@ -9,7 +9,7 @@ These are written or designed, but can't be verified without hardware or data we
9
9
  - **Windows, real-machine test.** The process table (PowerShell/CIM), snapshot and server have never run on a real Windows box. Needed: someone running `npx coworking-agents` and `npx coworking-agents --json` there.
10
10
  - **Windows CPU per process.** Other AI CLIs always show "idle" on Windows because CIM doesn't give CPU %. Needs a cheap measurement (e.g. two `Get-Process` samples) tested on a real machine.
11
11
  - **Linux, real-machine test.** `ps`, `/proc/<pid>/cwd` and the "go to terminal" path (xdotool / wmctrl) were written blind.
12
- - **Typing into a terminal on Linux and Windows.** Reply, answer and `/compact` are macOS only (AppleScript into Terminal/iTerm). Linux would need per-terminal support (tmux `send-keys` is the most portable start); Windows would need Windows Terminal/conhost support.
12
+ - **Typing into a terminal on Linux and Windows.** Reply, answer and `/compact` are macOS only (AppleScript into Terminal/iTerm). Agents inside tmux now get text through `tmux send-keys` on any OS (written, not yet tested on a real Linux box); other Linux terminals and Windows Terminal/conhost still need support.
13
13
  - **Full readers for other AIs.** Gemini CLI, OpenCode, Aider, Cursor Agent, Amp, Goose, Qwen Code, Crush, Droid and Kimi are detected by process only (working/idle). Showing what they are doing (tool, file, question, tokens) needs their on-disk session format, read from real transcripts. Share a sample (redacted) to get one added.
14
14
 
15
15
  ## Next
@@ -24,3 +24,9 @@ Estimated cost · wait reminder · git state on the door · daily report as Mark
24
24
  ## Ideas
25
25
 
26
26
  - **Seasonal decor** by date (subtle, off with a toggle).
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.
@@ -19,10 +19,13 @@ if (flag('-h') || flag('--help')) {
19
19
  --private hide session titles, paths and file names (screen sharing)
20
20
  --demo [N] show a fake office with every state and N agents (default 10)
21
21
  --json print one snapshot and exit
22
+ mcp run a read-only MCP server on stdio (claude mcp add coworking -- npx coworking-agents mcp)
22
23
  --no-update-check don't ask npm whether a newer version is out`);
23
24
  process.exit(0);
24
25
  }
25
26
 
27
+ if (args[0] === 'mcp') { require('../src/mcp').serve(); return; }
28
+
26
29
  if (flag('--json')) {
27
30
  const { snapshot } = require('../src/collect');
28
31
  // write and let Node exit on its own: process.exit() cuts a big snapshot short when stdout is a pipe
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.3.0",
3
+ "version": "0.4.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"
@@ -11,6 +11,7 @@
11
11
  "public",
12
12
  ".claude-plugin",
13
13
  "README.md",
14
+ "CHANGELOG.md",
14
15
  "LICENSE",
15
16
  "commands",
16
17
  "skills",
@@ -25,7 +26,8 @@
25
26
  "start": "node bin/coworking-agents.js",
26
27
  "demo": "node bin/coworking-agents.js --demo",
27
28
  "test": "node test/run.js",
28
- "screenshots": "bash scripts/screenshots.sh"
29
+ "screenshots": "bash scripts/screenshots.sh",
30
+ "video": "node scripts/record-video.js"
29
31
  },
30
32
  "keywords": [
31
33
  "ai",
package/public/index.html CHANGED
@@ -79,6 +79,7 @@
79
79
  <script src="js/work.js"></script>
80
80
  <script src="js/views.js"></script>
81
81
  <script src="js/fun2.js"></script>
82
+ <script src="js/tasks.js"></script>
82
83
  <script src="js/main.js"></script>
83
84
  </body>
84
85
  </html>
@@ -61,6 +61,7 @@ function checkAlerts() {
61
61
  if (firstAlertPass) continue; // already waiting when the page opened: the counters show it
62
62
  const what = p.state === 'needs_you' ? A.waitAsk(mins) : A.waitApprove(mins);
63
63
  toast(`<b>${esc(what)}</b>${esc(p.name)} · ${esc(p.title || '')}`, 'warn');
64
+ if (typeof pushNotice === 'function') pushNotice('agent:' + p.id, 'warn', p.name + ': ' + what, p.title || '', { agent: p.id });
64
65
  notifyOS(what, p.name);
65
66
  }
66
67
  }
package/public/js/fun2.js CHANGED
@@ -91,21 +91,30 @@ function openArcade() {
91
91
  function drawArcadeTitle() {
92
92
  const c = arcadeEl.querySelector('.arcade-cv'); if (!c) return;
93
93
  const g = c.getContext('2d'); arcadeBg(g, performance.now());
94
- g.fillStyle = '#ffd84d'; g.font = 'bold 18px monospace'; g.textAlign = 'center'; g.fillText('BUG CATCHER', AW / 2, 62);
95
- g.fillStyle = '#c0cbdc'; g.font = '10px monospace'; g.fillText(T.arcade.press, AW / 2, 86); g.textAlign = 'left';
94
+ gText(g, AW / 2, 46, 'BUG CATCHER', '#ffd84d', 3, true);
95
+ gText(g, AW / 2, 78, lang === 'pt' ? 'ESPACO OU ENTER' : 'SPACE OR ENTER', '#c0cbdc', 1, true);
96
96
  drawBug(g, AW / 2 - 30, 100, false, 0); drawBug(g, AW / 2 - 4, 104, true, 1); drawBug(g, AW / 2 + 22, 100, false, 0);
97
97
  }
98
+ // crisp text: the office's pixel font, scaled by whole pixels
99
+ function gText(g, x, y, str, c, k = 1, center = false) {
100
+ const txt = String(str).toUpperCase(); if (center) x -= Math.round(txt.length * 4 * k / 2);
101
+ g.fillStyle = c; let cx = Math.round(x);
102
+ for (const ch of txt) { const gl = PIXFONT[ch]; if (gl) for (let i = 0; i < 15; i++) if (gl[i] === '1') g.fillRect(cx + (i % 3) * k, Math.round(y) + ((i / 3) | 0) * k, k, k); cx += 4 * k; }
103
+ }
98
104
  function arcadeBg(g, now) {
99
105
  g.fillStyle = '#141826'; g.fillRect(0, 0, AW, AH);
100
- g.fillStyle = '#1c2236'; for (let y = 0; y < AH; y += 8) { const sh = (y * 13) % 70; g.fillRect(6 + sh % 20, (y + now / 60) % AH, 30 + sh, 2); } // code scrolling behind
106
+ g.fillStyle = '#1c2236'; for (let y = 0; y < AH; y += 8) { const sh = (y * 13) % 70; g.fillRect(6 + sh % 20, Math.round((y + now / 60) % AH), 30 + sh, 2); } // code scrolling behind
101
107
  g.fillStyle = '#2a2f45'; g.fillRect(0, AH - 8, AW, 8);
102
108
  }
103
109
  function drawBug(g, x, y, gold, f) {
104
- const body = gold ? '#ffd84d' : '#63c74d', dark = gold ? '#b07d2a' : '#2f7a3b';
105
- g.fillStyle = '#1b1622'; for (const dx of [-2, 9]) for (const dy of [2, 5]) g.fillRect(x + dx + (f && dx < 0 ? -1 : 0), y + dy + (f ? 1 : 0), 3, 1);
106
- g.fillStyle = dark; g.fillRect(x, y, 10, 8); g.fillStyle = body; g.fillRect(x + 1, y + 1, 8, 6); g.fillStyle = dark; g.fillRect(x + 4, y + 1, 2, 6);
107
- g.fillStyle = '#1b1622'; g.fillRect(x + 2, y - 2, 6, 3); g.fillRect(x + 2, y - 4, 1, 2); g.fillRect(x + 7, y - 4, 1, 2);
108
- if (gold) { g.fillStyle = '#ffffff'; g.fillRect(x + 2, y + 2, 1, 1); }
110
+ x = Math.round(x); y = Math.round(y);
111
+ const body = gold ? '#ffd84d' : '#63c74d', dark = gold ? '#b07d2a' : '#2f7a3b', leg = '#c0cbdc';
112
+ g.fillStyle = leg; for (const [dx, dy] of [[-3, 3], [-3, 6], [11, 3], [11, 6]]) g.fillRect(x + dx + (f ? (dx < 0 ? -1 : 1) : 0), y + dy + (f ? 1 : 0), 4, 1); // legs wiggle
113
+ g.fillStyle = '#0b0a0e'; g.fillRect(x - 1, y - 1, 13, 11);
114
+ g.fillStyle = dark; g.fillRect(x, y, 11, 9); g.fillStyle = body; g.fillRect(x + 1, y + 1, 4, 7); g.fillRect(x + 6, y + 1, 4, 7); // shell, split down the middle
115
+ g.fillStyle = '#ffffff'; g.fillRect(x + 2, y + 2, 1, 2); g.fillRect(x + 7, y + 2, 1, 2);
116
+ g.fillStyle = '#3a4058'; g.fillRect(x + 2, y - 4, 7, 4); g.fillStyle = '#ffffff'; g.fillRect(x + 3, y - 3, 1, 1); g.fillRect(x + 7, y - 3, 1, 1); // head and eyes
117
+ g.fillStyle = leg; g.fillRect(x + 2, y - 7, 1, 3); g.fillRect(x + 8, y - 7, 1, 3); // antennae
109
118
  }
110
119
  function startArcade() {
111
120
  const cv2 = arcadeEl.querySelector('.arcade-cv'); if (!cv2) return;
@@ -129,7 +138,7 @@ function tickArcade(now) {
129
138
  } else if (b.y > AH - 8) { b.miss = true; G.lives--; G.combo = 0; }
130
139
  }
131
140
  G.bugs = G.bugs.filter(b => !b.hit && !b.miss);
132
- for (const s of G.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 80 * dt; s.life -= dt; g.fillStyle = s.c; if (s.txt) { g.font = 'bold 9px monospace'; g.fillText(s.txt, s.x, s.y); } else g.fillRect(s.x, s.y, 2, 2); }
141
+ for (const s of G.sparks) { s.x += s.vx * dt; s.y += s.vy * dt; s.vy += 80 * dt; s.life -= dt; if (s.txt) gText(g, s.x, s.y - 6, s.txt, s.c); else { g.fillStyle = s.c; g.fillRect(Math.round(s.x), Math.round(s.y), 2, 2); } }
133
142
  G.sparks = G.sparks.filter(s => s.life > 0);
134
143
  // the laptop paddle
135
144
  const px = Math.round(G.x), py = AH - 18;
@@ -137,14 +146,14 @@ function tickArcade(now) {
137
146
  g.fillStyle = '#8b9bb4'; g.fillRect(px, py + 2, 28, 4); g.fillStyle = '#d97757'; g.fillRect(px + 11, py - 4, 6, 2);
138
147
  // HUD: hearts, score, combo
139
148
  for (let i = 0; i < 3; i++) { g.fillStyle = i < G.lives ? '#e43b44' : '#3a3448'; g.fillRect(6 + i * 10, 6, 3, 2); g.fillRect(10 + i * 10, 6, 3, 2); g.fillRect(6 + i * 10, 8, 7, 2); g.fillRect(7 + i * 10, 10, 5, 1); g.fillRect(8 + i * 10, 11, 3, 1); }
140
- g.fillStyle = '#ffffff'; g.font = 'bold 10px monospace'; g.textAlign = 'right'; g.fillText(String(G.score), AW - 6, 14); g.textAlign = 'left';
141
- if (G.combo >= 5) { g.fillStyle = '#ff6ec7'; g.font = 'bold 9px monospace'; g.fillText('COMBO x2', AW / 2 - 22, 14); }
149
+ gText(g, AW - 6 - String(G.score).length * 8, 5, G.score, '#ffffff', 2);
150
+ if (G.combo >= 5) gText(g, AW / 2, 6, 'COMBO X2', '#ff6ec7', 1, true);
142
151
  const sc = arcadeEl.querySelector('#arcade-score'); if (sc) sc.textContent = `${T.arcade.score}: ${G.score}`;
143
152
  if (G.lives <= 0) {
144
153
  const best = Number(store.get('arcadeBest', 0)) || 0;
145
154
  if (G.score > best) { store.set('arcadeBest', G.score); toast(`<b>${esc(T.arcade.record)}</b>${G.score}`, 'ok'); const be = arcadeEl.querySelector('#arcade-best'); if (be) be.textContent = G.score; }
146
155
  achRecord.arcadeBest = Math.max(achRecord.arcadeBest || 0, G.score); achBump('arcadeGames');
147
- g.fillStyle = 'rgba(0,0,0,.65)'; g.fillRect(0, 56, AW, 46); g.fillStyle = '#ffd84d'; g.font = 'bold 14px monospace'; g.textAlign = 'center'; g.fillText(T.arcade.over, AW / 2, 76); g.fillStyle = '#ffffff'; g.font = '10px monospace'; g.fillText(`${T.arcade.score}: ${G.score}`, AW / 2, 92); g.textAlign = 'left';
156
+ g.fillStyle = 'rgba(0,0,0,.7)'; g.fillRect(0, 56, AW, 46); gText(g, AW / 2, 64, 'GAME OVER', '#ffd84d', 2, true); gText(g, AW / 2, 84, `${G.score}`, '#ffffff', 2, true);
148
157
  game2 = null; return;
149
158
  }
150
159
  requestAnimationFrame(tickArcade);
package/public/js/i18n.js CHANGED
@@ -122,6 +122,21 @@ const I18N = {
122
122
  scoreboard: { title: 'Placar da semana entre as IAs', hours: 'horas', actions: 'ações', tokens: 'tokens' },
123
123
  share: { card: 'Cartão da semana', visit: 'Foto para visita', week: 'Minha semana no escritório', hours: 'horas de agentes trabalhando', actions: 'ações', trophies: 'níveis de conquista · lendas', saved: 'Cartão salvo', visitSaved: 'Foto para visita salva', visitNote: 'Sem nomes, branches nem o nome do escritório.' },
124
124
  elevator: { title: 'Elevador', sub: 'Escolha o andar: terraço, escritório, sala de troféus ou servidores.', terrace: '🌇 Terraço', trophy: '🏆 Sala de troféus', servers: '🖧 Sala de servidores', office: '🏢 Escritório', floor: n => `🏢 Andar ${n}` },
125
+ tasks: { tab: n => n ? `Tarefas (${n})` : 'Tarefas', sub: 'Crie uma tarefa e um agente trabalha nela em segundo plano. Você responde, aprova e revisa por aqui, sem abrir o terminal. Só as tarefas criadas aqui podem ser controladas; as sessões abertas por fora seguem só monitoradas.',
126
+ newT: 'Nova tarefa', title: 'Título (opcional)', desc: 'O que fazer', folder: 'Pasta', agent: 'Agente', auto: 'Automático', profile: 'Perfil', prio: 'Prioridade', prios: ['', 'Baixa', 'Normal', 'Alta'],
127
+ budget: 'Orçamento (US$, 0 = sem limite)', test: 'Comando de teste (opcional)', wt: 'Worktree separada (isola as alterações)', perm: 'Permissões', perms: { acceptEdits: 'Pode editar arquivos', plan: 'Só planejar', default: 'Pedir para tudo' },
128
+ dep: 'Depois de', none: 'nenhuma', create: 'Criar tarefa', rec: 'Recomendar', recWhy: { default: 'tarefa comum de desenvolvimento', deep: 'parece investigação, arquitetura ou mudança ampla', eco: 'parece texto, documentação ou ajuste localizado', history: 'tarefas parecidas falharam ou precisaram de novas tentativas nesse perfil' },
129
+ profiles: { eco: 'Econômico', bal: 'Equilibrado', deep: 'Profundo' }, states: { pending: 'Na fila', running: 'Trabalhando', blocked: 'Espera você', done: 'Concluída', failed: 'Falhou', cancelled: 'Cancelada' },
130
+ reasons: { question: 'fez uma pergunta', review: 'pronta para revisar', approval: 'precisa de permissão', budget: 'chegou no orçamento', restart: 'o servidor reiniciou; responda para retomar', error: 'deu erro' },
131
+ reply: 'Responder', send: 'Enviar', approve: 'Aprovar e continuar', cancel: 'Cancelar', cancelSure: 'Cancelar a tarefa? O que o agente já alterou continua alterado.', conclude: 'Concluir', retry: 'Tentar de novo', handoff: 'Passar para outro agente', del: 'Apagar', diff: 'Ver diff',
132
+ save: 'Economizar', more: 'Aumentar capacidade', applyRec: 'Aplicar recomendação', pick: 'Escolher', model: 'Modelo', effort: 'Esforço', cliDefault: 'padrão do CLI', next: 'vale a partir da próxima etapa', now: 'aplicado', configLog: 'Mudanças de configuração',
133
+ cost: 'Custo estimado', costUnknown: 'custo desconhecido (modelo padrão do CLI)', tokens: 'tokens', attempts: n => `${n} tentativa(s)`, files: 'Arquivos alterados', tests: c => c === 0 ? '✓ testes passaram' : `✗ testes falharam (código ${c})`, denied: 'Ferramentas bloqueadas', events: 'Atividade', back: '← Tarefas',
134
+ parallel: 'Agentes ao mesmo tempo', ranking: 'Ranking de custo', byTask: 'Por tarefa', byAi: 'Por IA', period: { day: 'Hoje', week: '7 dias', month: '30 dias' }, perDone: 'por tarefa concluída', costNote: 'Estimativa a preço de API. Não é o valor cobrado nem a porcentagem do seu plano.',
135
+ empty: 'Nenhuma tarefa ainda.', search: 'Buscar no histórico', fail: { nodir: 'Pasta inválida (precisa estar dentro da sua pasta pessoal).', nodesc: 'Escreva o que fazer.', unsupported: 'Agente não suportado.', state: 'Não dá agora.', demo: 'Modo demo: nada roda.' },
136
+ warn: { loop: n => `Repetiu a mesma ação (${n}) várias vezes sem progresso.`, retry: 'Já teve duas tentativas sem sucesso. Considere aumentar o esforço.', budget: 'Chegou no orçamento e foi pausada.' } },
137
+ notices: { tab: n => n ? `Avisos (${n})` : 'Avisos', sub: 'Pedidos dos agentes, tarefas, recomendações, orçamento e novidades do app, num lugar só.', empty: 'Nenhum aviso.', apply: 'Aplicar', why: 'Ver motivo', notNow: 'Agora não', open: 'Abrir', readAll: 'Marcar tudo como lido', times: n => `×${n}`,
138
+ task: (t, r) => `Tarefa “${t}”: ${r}`, update: v => `Versão ${v} disponível` },
139
+ ver: { tab: 'Versões', sub: 'Versão instalada, a mais nova no npm e o que mudou.', installed: 'Instalada', latest: 'No npm', checked: 'Verificado', never: 'ainda não', upToDate: 'Você está na versão mais nova.', news: 'Ver novidades', copy: 'Copiar comando', copied: 'Copiado!', later: 'Lembrar depois', npx: 'Se você usa npx:', global: 'Se instalou global:', plugin: 'No plugin do Claude Code: /plugin update coworking-agents', note: 'Copiar o comando não atualiza nada sozinho: rode, reinicie e a versão nova aparece aqui.', admin: 'Admin do escritório', celebrate: v => `Versão ${v} no ar! 🎉` },
125
140
  queue: { tab: n => n ? `Fila (${n})` : 'Fila', sub: 'Quem está esperando você, de quem espera há mais tempo para quem espera menos.', empty: 'Ninguém esperando você. 🎉', answer: 'Responder', reply: 'Próxima instrução' },
126
141
  quick: ['Pode seguir.', 'Roda os testes.', 'Faz o commit e o push.', 'Explica o que você fez.', 'Continua de onde parou.', 'Revisa o código antes de seguir.'],
127
142
  welcome: { title: a => `Bem-vindo de volta! Você ficou fora ${a}`, body: (d, a, l, need) => `Enquanto isso: ${d} terminaram, ${a} chegaram, ${l} saíram. ${need} esperando você agora.`, queue: 'Ver a fila' },
@@ -150,7 +165,7 @@ const I18N = {
150
165
  clockIn: 'Relógio de ponto: quem chegou hoje', clockNone: 'Ninguém bateu o ponto hoje ainda.',
151
166
  thoughts: ['será que esqueceram de mim?', 'hora do café?', 'terminei… alguém?', 'zzz… brincadeira, tô aqui', 'posso fazer mais uma coisa', 'que silêncio…', 'alguém viu meu grampeador?', 'revisando meus commits mentalmente', 'tô de olho no relógio', 'me chama que eu vou!'],
152
167
  radio: { label: '♪ Rádio chiptune', on: 'Rádio ligado: a estação segue a IA que mais trabalha, o ritmo segue o escritório (clique para desligar)', off: 'Rádio desligado (clique para ligar)' },
153
- menu: 'Menu', langName: 'Idioma: português (trocar para inglês)', menuItems: { launch: 'Nova sessão', deco: 'Decorar (mover itens)', walk: 'Andar pelo escritório', office: 'Personalizar escritório', photo: 'Foto do escritório', radio: 'Rádio', notify: 'Avisos do sistema', sound: 'Sons', focus: 'Modo foco', replay: 'Replay da última hora', search: 'Buscar agente', lang: 'English' },
168
+ menu: 'Menu', langName: 'Idioma: português (trocar para inglês)', menuItems: { launch: 'Nova sessão', deco: 'Decorar (mover itens)', walk: 'Andar pelo escritório', office: 'Personalizar escritório', photo: 'Foto do escritório', radio: 'Rádio', notify: 'Avisos do sistema', sound: 'Sons', focus: 'Modo foco', replay: 'Replay da última hora', search: 'Buscar agente', tasks: 'Tarefas', notices: 'Avisos', lang: 'English' },
154
169
  focus: { off: 'Modo foco: silencia sons e notificações por 25 min', on: m => `Modo foco: ${m} min restantes (clique para estender ou desligar)`, ended: 'Modo foco terminou' },
155
170
  feed: { title: 'Histórico', sub: 'O que aconteceu nas últimas 24 horas (guardado só neste navegador)', empty: 'Nada ainda. Chegadas, saídas, pedidos e entregas aparecem aqui.', clear: 'Limpar', thisHour: 'Nesta hora', kinds: { arrive: 'chegou', leave: 'saiu', done: 'terminou', needs_you: 'precisa de você', waiting: 'espera aprovação' } },
156
171
  git: { dirty: n => `${n} arquivo${n > 1 ? 's' : ''} sem commit`, clean: 'tudo commitado', ahead: n => `${n} commit${n > 1 ? 's' : ''} para enviar`, behind: n => `${n} commit${n > 1 ? 's' : ''} para puxar`, label: 'Git' },
@@ -329,6 +344,21 @@ const I18N = {
329
344
  scoreboard: { title: "This week's AI scoreboard", hours: 'hours', actions: 'actions', tokens: 'tokens' },
330
345
  share: { card: 'Weekly card', visit: 'Visitor photo', week: 'My week at the office', hours: 'hours of agents at work', actions: 'actions', trophies: 'achievement tiers · legends', saved: 'Card saved', visitSaved: 'Visitor photo saved', visitNote: 'No names, branches or office name.' },
331
346
  elevator: { title: 'Elevator', sub: 'Pick a floor: rooftop, office, trophy room or servers.', terrace: '🌇 Rooftop', trophy: '🏆 Trophy room', servers: '🖧 Server room', office: '🏢 Office', floor: n => `🏢 Floor ${n}` },
347
+ tasks: { tab: n => n ? `Tasks (${n})` : 'Tasks', sub: 'Create a task and an agent works on it in the background. You answer, approve and review here, no terminal needed. Only tasks created here can be controlled; sessions opened elsewhere stay monitor-only.',
348
+ newT: 'New task', title: 'Title (optional)', desc: 'What to do', folder: 'Folder', agent: 'Agent', auto: 'Automatic', profile: 'Profile', prio: 'Priority', prios: ['', 'Low', 'Normal', 'High'],
349
+ budget: 'Budget (USD, 0 = no limit)', test: 'Test command (optional)', wt: 'Separate worktree (isolates the changes)', perm: 'Permissions', perms: { acceptEdits: 'May edit files', plan: 'Plan only', default: 'Ask for everything' },
350
+ dep: 'After', none: 'none', create: 'Create task', rec: 'Recommend', recWhy: { default: 'regular development task', deep: 'looks like investigation, architecture or a wide change', eco: 'looks like text, docs or a small fix', history: 'similar tasks failed or needed retries at this profile' },
351
+ profiles: { eco: 'Economy', bal: 'Balanced', deep: 'Deep' }, states: { pending: 'Queued', running: 'Working', blocked: 'Waiting on you', done: 'Done', failed: 'Failed', cancelled: 'Cancelled' },
352
+ reasons: { question: 'asked a question', review: 'ready for review', approval: 'needs permission', budget: 'hit the budget', restart: 'the server restarted; reply to resume', error: 'errored' },
353
+ reply: 'Reply', send: 'Send', approve: 'Approve and continue', cancel: 'Cancel', cancelSure: 'Cancel the task? What the agent already changed stays changed.', conclude: 'Conclude', retry: 'Try again', handoff: 'Hand off to another agent', del: 'Delete', diff: 'See diff',
354
+ save: 'Save', more: 'More capacity', applyRec: 'Apply recommendation', pick: 'Choose', model: 'Model', effort: 'Effort', cliDefault: 'CLI default', next: 'applies from the next step', now: 'applied', configLog: 'Configuration changes',
355
+ cost: 'Estimated cost', costUnknown: 'cost unknown (CLI default model)', tokens: 'tokens', attempts: n => `${n} attempt(s)`, files: 'Files changed', tests: c => c === 0 ? '✓ tests passed' : `✗ tests failed (code ${c})`, denied: 'Blocked tools', events: 'Activity', back: '← Tasks',
356
+ parallel: 'Agents at once', ranking: 'Cost ranking', byTask: 'By task', byAi: 'By AI', period: { day: 'Today', week: '7 days', month: '30 days' }, perDone: 'per finished task', costNote: 'Estimate at API prices. Not what you are billed nor your plan percentage.',
357
+ empty: 'No tasks yet.', search: 'Search history', fail: { nodir: 'Invalid folder (must be inside your home folder).', nodesc: 'Write what to do.', unsupported: 'Agent not supported.', state: 'Not possible now.', demo: 'Demo mode: nothing runs.' },
358
+ warn: { loop: n => `Repeated the same action (${n}) several times without progress.`, retry: 'Two attempts failed already. Consider more effort.', budget: 'Hit the budget and was paused.' } },
359
+ notices: { tab: n => n ? `Notices (${n})` : 'Notices', sub: 'Agent requests, tasks, recommendations, budget and app news in one place.', empty: 'No notices.', apply: 'Apply', why: 'See why', notNow: 'Not now', open: 'Open', readAll: 'Mark all as read', times: n => `×${n}`,
360
+ task: (t, r) => `Task “${t}”: ${r}`, update: v => `Version ${v} available` },
361
+ ver: { tab: 'Releases', sub: 'Installed version, newest on npm and what changed.', installed: 'Installed', latest: 'On npm', checked: 'Checked', never: 'not yet', upToDate: 'You are on the newest version.', news: 'See what\'s new', copy: 'Copy command', copied: 'Copied!', later: 'Remind me later', npx: 'If you use npx:', global: 'If installed globally:', plugin: 'In the Claude Code plugin: /plugin update coworking-agents', note: 'Copying the command updates nothing by itself: run it, restart, and the new version shows here.', admin: 'Office admin', celebrate: v => `Version ${v} is live! 🎉` },
332
362
  queue: { tab: n => n ? `Queue (${n})` : 'Queue', sub: 'Who is waiting on you, longest wait first.', empty: 'Nobody is waiting on you. 🎉', answer: 'Answer', reply: 'Next instruction' },
333
363
  quick: ['Go ahead.', 'Run the tests.', 'Commit and push.', 'Explain what you did.', 'Pick up where you left off.', 'Review the code before going on.'],
334
364
  welcome: { title: a => `Welcome back! You were away ${a}`, body: (d, a, l, need) => `Meanwhile: ${d} finished, ${a} arrived, ${l} left. ${need} waiting on you now.`, queue: 'Open the queue' },
@@ -357,7 +387,7 @@ const I18N = {
357
387
  clockIn: 'Punch clock: who arrived today', clockNone: 'Nobody clocked in today yet.',
358
388
  thoughts: ['did they forget about me?', 'coffee time?', 'done… anyone?', 'zzz… kidding, still here', 'I can do one more thing', 'so quiet…', 'has anyone seen my stapler?', 'reviewing my commits in my head', 'watching the clock', 'call me, I am ready!'],
359
389
  radio: { label: '♪ Chiptune radio', on: 'Radio on: the station follows the AI working the most, the beat follows the office (click to turn off)', off: 'Radio off (click to turn on)' },
360
- menu: 'Menu', langName: 'Language: English (switch to Portuguese)', menuItems: { launch: 'New session', deco: 'Decorate (move items)', walk: 'Walk around the office', office: 'Customise office', photo: 'Office photo', radio: 'Radio', notify: 'System alerts', sound: 'Sounds', focus: 'Focus mode', replay: 'Replay last hour', search: 'Search agents', lang: 'Português' },
390
+ menu: 'Menu', langName: 'Language: English (switch to Portuguese)', menuItems: { launch: 'New session', deco: 'Decorate (move items)', walk: 'Walk around the office', office: 'Customise office', photo: 'Office photo', radio: 'Radio', notify: 'System alerts', sound: 'Sounds', focus: 'Focus mode', replay: 'Replay last hour', search: 'Search agents', tasks: 'Tasks', notices: 'Notices', lang: 'Português' },
361
391
  focus: { off: 'Focus mode: mutes sounds and notifications for 25 min', on: m => `Focus mode: ${m} min left (click to extend or turn off)`, ended: 'Focus mode ended' },
362
392
  feed: { title: 'History', sub: 'What happened in the last 24 hours (kept in this browser only)', empty: 'Nothing yet. Arrivals, departures, requests and finished work show up here.', clear: 'Clear', thisHour: 'This hour', kinds: { arrive: 'arrived', leave: 'left', done: 'finished', needs_you: 'needs you', waiting: 'waits for approval' } },
363
393
  git: { dirty: n => `${n} uncommitted file${n > 1 ? 's' : ''}`, clean: 'all committed', ahead: n => `${n} commit${n > 1 ? 's' : ''} to push`, behind: n => `${n} commit${n > 1 ? 's' : ''} to pull`, label: 'Git' },
@@ -41,7 +41,7 @@ let reportTab = 'usage', todayData = null;
41
41
  function openReport(tab) { reportTab = tab || 'usage'; achBump('tab_' + reportTab); reportEl.hidden = false; renderReport(); loadUsage(true); if (reportTab === 'today') loadToday(); }
42
42
  async function loadToday() { try { todayData = await fetch('api/report').then(r => r.json()); } catch {} if (!reportEl.hidden) renderReport(); }
43
43
  reportEl.addEventListener('click', e => { const t = e.target.closest('[data-tab]'); if (t) { reportTab = t.dataset.tab; achBump('tab_' + reportTab); if (reportTab === 'today') loadToday(); renderReport(); } });
44
- function tabsHtml() { return `<div class="rtabs">${[['queue', T.queue.tab(typeof queueList === 'function' ? queueList().filter(p => p.state !== 'idle').length : 0)], ['usage', T.usage.title], ['today', T.today.title], ['paper', T.paper.tab], ['ach', T.ach.title], ['shop', T.shop.title], ['office', T.office.tab], ['feed', T.feed.title]].map(([k, l]) => `<button data-tab="${k}" class="${reportTab === k ? 'on' : ''}">${esc(l)}</button>`).join('')}</div>`; }
44
+ function tabsHtml() { return `<div class="rtabs">${[['tasks', T.tasks.tab(typeof tasksWaiting === 'function' ? tasksWaiting() : 0)], ['notices', T.notices.tab(typeof unreadNotices === 'function' ? unreadNotices() : 0)], ['queue', T.queue.tab(typeof queueList === 'function' ? queueList().filter(p => p.state !== 'idle').length : 0)], ['usage', T.usage.title], ['today', T.today.title], ['paper', T.paper.tab], ['ach', T.ach.title], ['shop', T.shop.title], ['office', T.office.tab], ['feed', T.feed.title], ['version', T.ver.tab]].map(([k, l]) => `<button data-tab="${k}" class="${reportTab === k ? 'on' : ''}">${esc(l)}</button>`).join('')}</div>`; }
45
45
  // activity feed: newest first, grouped by hour
46
46
  function renderFeed() {
47
47
  const F = T.feed, body = reportEl.querySelector('.report-body');
@@ -97,6 +97,9 @@ reportEl.addEventListener('click', async e => {
97
97
  function closeReport() { reportEl.hidden = true; }
98
98
 
99
99
  function renderReport() {
100
+ if (reportTab === 'tasks') return renderTasks();
101
+ if (reportTab === 'notices') return renderNotices();
102
+ if (reportTab === 'version') return renderVersion();
100
103
  if (reportTab === 'today') return renderToday();
101
104
  if (reportTab === 'feed') return renderFeed();
102
105
  if (reportTab === 'ach') return renderAchievements();