coworking-agents 0.3.1 → 0.5.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,26 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0 — 2026-10-11
4
+ - 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
+ - Codex models and efforts come from Codex's own model list; Claude efforts low to max
6
+ - 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
7
+ - Ranking by AI, task and session with the spend that happened in the chosen period
8
+ - 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
9
+ - Roles (Backend, Frontend, Reviewer), economy mode, filtered test logs, attempt limit, new session with a summary, optional auto-apply within limits
10
+ - 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
11
+ - Releases: notes of the newer version fetched from its package, install-aware update command, "unavailable" when npm can't be reached
12
+ - Search across tasks, live sessions, today and this month; replay includes sessions that ended in the last hour
13
+
14
+ ## 0.4.0 — 2026-10-11
15
+ - 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
16
+ - Answer questions, approve blocked tools, review the diff and test results, conclude, hand off or retry — all from the office
17
+ - Queue with priority, dependencies, max parallel agents and automatic agent choice
18
+ - Cost ranking by task and by AI (day, week, month), budget per task, Save / More capacity / Recommended / manual model and effort
19
+ - Recommendations with the reason, warnings for loops, retries and budget
20
+ - Notification center and a releases board with the office admin
21
+ - Read-only MCP server: `coworking-agents mcp`
22
+ - Linux: send text to agents running in tmux
23
+ - Private mode hides repo names and titles in the report and usage ranking
24
+
25
+ ## 0.3.1 — 2026-10-10
26
+ - 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,30 @@ 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** 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:
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.
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
+ - **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
188
+ - **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.
191
+ - **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
+ - **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*.
193
+ - **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
+
195
+ Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folders, results, diffs and notice texts stay hidden.
196
+
197
+ ## Notices, releases and the office admin
198
+
199
+ 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
+
201
+ ## MCP (read-only)
202
+
203
+ `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.
204
+
137
205
  ## Walk around (Gather-style)
138
206
 
139
207
  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 +277,8 @@ Click the elevator (or press Enter next to it) to ride between floors, up to the
209
277
  |---|---|---|---|
210
278
  | See sessions (Claude Code, Codex, other CLIs) | ✓ | ✓ | ✓ |
211
279
  | 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 |
280
+ | Reply / answer / compact from the office | ✓ | tmux, else copy and go | tmux, else copy and go |
281
+ | Tasks in the background | ✓ | ✓ | ✓ (needs the CLIs on PATH) |
213
282
 
214
283
  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
284
 
@@ -288,6 +357,7 @@ module.exports = {
288
357
  npm test # builds fake ~/.claude and ~/.codex folders and checks the collector and the server
289
358
  npm run demo # the fake office with every state
290
359
  npm run screenshots # regenerates docs/*.png from demo mode (needs agent-browser on PATH)
360
+ npm run video # re-records docs/demo.webp and docs/tour.webp (needs agent-browser and img2webp)
291
361
  node bin/coworking-agents.js --json # one snapshot of the state, as JSON
292
362
  ```
293
363
 
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,10 @@ 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 — 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.
33
+ - Semantic search over history only if plain text search stops being enough; smarter automatic assignment.
@@ -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.1",
3
+ "version": "0.5.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/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');
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', map: 'Mapa das versões', unavailable: 'Não consegui consultar o npm agora; a informação está indisponível.', sayNew: v => `Saiu a ${v}! Olha o que ela traz:`, sayNewNoNotes: v => `Saiu a ${v}! Ainda não consegui buscar as novidades dela.`, sayCur: v => `Você está na ${v}. Nela eu trouxe:`, 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', map: 'Release map', unavailable: 'Couldn\'t reach npm right now; this information is unavailable.', sayNew: v => `${v} is out! Here's what it brings:`, sayNewNoNotes: v => `${v} is out! I couldn't fetch its notes yet.`, sayCur: v => `You're on ${v}. In it I brought:`, 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' },
@@ -426,6 +456,64 @@ const store = {
426
456
  const qs = new URLSearchParams(location.search);
427
457
  let lang = qs.get('lang') || store.get('lang', (navigator.language || 'pt').toLowerCase().startsWith('pt') ? 'pt' : 'en');
428
458
  if (!Object.prototype.hasOwnProperty.call(I18N, lang)) lang = 'pt';
459
+
460
+ // tasks: controls, cost and monitoring strings (merged into the blocks above)
461
+ {
462
+ const pt = {
463
+ proposal: (p, m, e) => `Proposta: ${p} (${m} · ${e}).`, runningCfg: 'Em execução', currentCfg: 'Configuração', pendingCfg: 'Pendente', appliedOk: (m, e) => `Aplicado: ${m} · ${e}`,
464
+ partUnknown: 'parte desconhecida', saveTip: 'Um perfil abaixo: modelo e esforço mais baratos a partir da próxima etapa', moreTip: 'Um perfil acima: mais capacidade a partir da próxima etapa',
465
+ monitored: 'Sessão monitorada (só leitura)', monitoredWhy: 'Foi aberta fora do escritório: dá para ver tudo, mas não trocar modelo ou esforço daqui.', recFor: (p, m, e) => `Recomendado: ${p} (${m} · ${e})`, notApplied: 'não aplicado (sem controle sobre esta sessão)', asTask: 'Criar uma tarefa a partir desta',
466
+ managed: 'Sessão gerenciada pelo escritório', openTask: 'abrir tarefa', src: 'fonte: stream do CLI', bySession: 'Por sessão (mês)', sessionsNote: 'Todas as conversas do mês, lidas do disco.',
467
+ settings: 'Ajustes da fila e da economia', maxAttempts: 'Tentativas por tarefa', autoApply: 'Aplicar recomendações sozinho', autoMax: 'Até o perfil', autoPerTask: 'Mudanças automáticas por tarefa', autoNote: 'Só dentro desses limites, e cada mudança fica registrada na tarefa.',
468
+ recommended: 'Recomendado', role: 'Papel', roles: { backend: 'Backend', frontend: 'Frontend', reviewer: 'Revisor' }, lean: 'Modo econômico (instruções curtas: ler só o necessário, sem logs longos)', leanOn: 'econômico',
469
+ privateNote: 'Modo privado: nomes, pastas, resultados e diffs ficam escondidos.', kinds: { task: 'tarefa', session: 'sessão ao vivo', today: 'hoje', month: 'este mês' }, resumed: 'mesma sessão',
470
+ ev: { created: 'criada', start: 'início', text: 'mensagem', tool: 'ferramenta', result: 'resultado', test: 'testes', state: 'estado', warn: 'alerta', config: 'configuração', applied: 'aplicado', reply: 'resposta', approve: 'aprovado', retry: 'nova tentativa', fresh: 'nova sessão', cancelling: 'cancelando', cancelled: 'cancelada', done: 'concluída', error: 'erro', worktree: 'worktree' },
471
+ subagents: n => `${n} subagente(s)`, srcClaude: 'por modelo, do total do Claude Code (inclui subagentes)', srcCodex: 'por turno, dos tokens do Codex; subagentes do Codex não aparecem no stream',
472
+ fresh: 'Nova sessão com resumo', freshTip: 'Começa uma sessão nova levando só um resumo: decisões, testes e pendências. Gasta menos contexto, mas perde o cache da sessão antiga.',
473
+ since: c => `Desde o commit ${c}:`, newFiles: 'Arquivos novos', cancelling: 'Cancelando… confirma quando o processo terminar.', similar: n => `${n} tarefa(s) parecida(s) no histórico`,
474
+ };
475
+ const en = {
476
+ proposal: (p, m, e) => `Proposal: ${p} (${m} · ${e}).`, runningCfg: 'Running', currentCfg: 'Configuration', pendingCfg: 'Pending', appliedOk: (m, e) => `Applied: ${m} · ${e}`,
477
+ partUnknown: 'part unknown', saveTip: 'One profile down: cheaper model and effort from the next step', moreTip: 'One profile up: more capacity from the next step',
478
+ monitored: 'Monitored session (read-only)', monitoredWhy: 'It was opened outside the office: you can see everything, but not change model or effort from here.', recFor: (p, m, e) => `Recommended: ${p} (${m} · ${e})`, notApplied: 'not applied (no control over this session)', asTask: 'Create a task from this one',
479
+ managed: 'Session managed by the office', openTask: 'open task', src: 'source: CLI stream', bySession: 'By session (month)', sessionsNote: 'Every conversation this month, read from disk.',
480
+ settings: 'Queue and economy settings', maxAttempts: 'Attempts per task', autoApply: 'Apply recommendations automatically', autoMax: 'Up to profile', autoPerTask: 'Automatic changes per task', autoNote: 'Only within these limits, and every change is logged on the task.',
481
+ recommended: 'Recommended', role: 'Role', roles: { backend: 'Backend', frontend: 'Frontend', reviewer: 'Reviewer' }, lean: 'Economy mode (short instructions: read only what is needed, no long logs)', leanOn: 'economy',
482
+ privateNote: 'Private mode: names, folders, results and diffs are hidden.', kinds: { task: 'task', session: 'live session', today: 'today', month: 'this month' }, resumed: 'same session',
483
+ ev: { created: 'created', start: 'start', text: 'message', tool: 'tool', result: 'result', test: 'tests', state: 'state', warn: 'warning', config: 'configuration', applied: 'applied', reply: 'reply', approve: 'approved', retry: 'retry', fresh: 'new session', cancelling: 'cancelling', cancelled: 'cancelled', done: 'done', error: 'error', worktree: 'worktree' },
484
+ subagents: n => `${n} subagent(s)`, srcClaude: 'per model, from Claude Code\'s total (includes subagents)', srcCodex: 'per turn, from Codex tokens; Codex subagents are not in the stream',
485
+ fresh: 'New session with a summary', freshTip: 'Starts a new session carrying only a summary: decisions, tests and open items. Less context, but the old session\'s cache is lost.',
486
+ since: c => `Since commit ${c}:`, newFiles: 'New files', cancelling: 'Cancelling… confirmed when the process exits.', similar: n => `${n} similar task(s) in history`,
487
+ };
488
+ Object.assign(I18N.pt.tasks, pt); Object.assign(I18N.en.tasks, en);
489
+ Object.assign(I18N.pt.tasks.recWhy, { scope: 'escopo amplo (muitos arquivos ou o projeto todo)', attempts: 'já teve tentativas sem sucesso', 'history-up': 'tarefas parecidas falharam nesse perfil', 'history-down': 'tarefas parecidas deram certo num perfil mais barato' });
490
+ Object.assign(I18N.en.tasks.recWhy, { scope: 'wide scope (many files or the whole project)', attempts: 'previous attempts failed', 'history-up': 'similar tasks failed at this profile', 'history-down': 'similar tasks succeeded at a cheaper profile' });
491
+ Object.assign(I18N.pt.tasks.reasons, { nogit: 'pediu worktree, mas a pasta não é um repositório git', worktree: 'não consegui criar a worktree', detached: 'o servidor reiniciou; o agente ainda está rodando' });
492
+ Object.assign(I18N.en.tasks.reasons, { nogit: 'a worktree was asked but the folder is not a git repo', worktree: 'could not create the worktree', detached: 'the server restarted; the agent is still running' });
493
+ Object.assign(I18N.pt.tasks.warn, { high: w => `Esta execução já custou ${fmtUSD(w.usd || 0)}, mais de 3× a mediana das tarefas concluídas (${fmtUSD(w.median || 0)}).`, budgetUnknown: 'Não dá para aplicar o orçamento: o custo deste modelo é desconhecido.', budget: 'Chegou no orçamento e foi pausada. Uma chamada que já estava em andamento pode ter passado um pouco do limite.' });
494
+ Object.assign(I18N.en.tasks.warn, { high: w => `This run already cost ${fmtUSD(w.usd || 0)}, over 3× the median finished task (${fmtUSD(w.median || 0)}).`, budgetUnknown: 'The budget cannot be enforced: this model\'s cost is unknown.', budget: 'Hit the budget and was paused. A call already in progress may have gone slightly over.' });
495
+ Object.assign(I18N.pt.tasks.fail, { attempts: 'Chegou no limite de tentativas (ajuste em Ajustes).', private: 'Escondido no modo privado.', notask: 'Tarefa não encontrada.' });
496
+ Object.assign(I18N.en.tasks.fail, { attempts: 'Hit the attempt limit (change it in Settings).', private: 'Hidden in private mode.', notask: 'Task not found.' });
497
+ Object.assign(I18N.pt.notices, { hidden: 'Aviso escondido (modo privado)', privateNote: 'Modo privado: os textos dos avisos ficam escondidos.' });
498
+ Object.assign(I18N.en.notices, { hidden: 'Notice hidden (private mode)', privateNote: 'Private mode: notice texts are hidden.' });
499
+ }
500
+
501
+
502
+ // office objects that open the panels, and the recommendation card
503
+ {
504
+ I18N.pt.officeObj = { tasks: (q, w, b, d) => `Quadro de tarefas\n${q} na fila · ${w} trabalhando · ${b} esperam você · ${d} concluídas`, costs: (d, w) => `Monitor de custos\nHoje ${d} · 7 dias ${w} (estimativa a preço de API)`, notices: n => `Mural de avisos\n${n ? n + ' não lido(s)' : 'nada novo'}`, history: 'Arquivo de histórico\nTudo o que aconteceu no escritório', releases: (c, n) => `Quadro de versões\nInstalada ${c}${n ? ' · nova: ' + n : ''}`,
505
+ menu: { tasks: 'Tarefas', usage: 'Custos e limites', feed: 'Histórico', notices: 'Avisos', version: 'Atualizações' } };
506
+ I18N.en.officeObj = { tasks: (q, w, b, d) => `Task board\n${q} queued · ${w} working · ${b} waiting on you · ${d} done`, costs: (d, w) => `Costs screen\nToday ${d} · 7 days ${w} (estimate at API prices)`, notices: n => `Notice board\n${n ? n + ' unread' : 'nothing new'}`, history: 'History archive\nEverything that happened in the office', releases: (c, n) => `Releases board\nInstalled ${c}${n ? ' · new: ' + n : ''}`,
507
+ menu: { tasks: 'Tasks', usage: 'Costs and limits', feed: 'History', notices: 'Notices', version: 'Updates' } };
508
+ Object.assign(I18N.pt.tasks, { rcCurrent: 'Atual', rcRecommended: 'Recomendado', rcLoading: 'calculando…', rcPrivate: 'escondido no modo privado', rcMissing: 'Faltam dados', rcAlready: 'Já está na configuração recomendada.', rcExternal: 'Sessão aberta fora do escritório: dá para ver a recomendação, mas não aplicar daqui. Crie uma tarefa para ter controle.', unknownModel: 'modelo desconhecido', unknownEffort: 'esforço desconhecido',
509
+ missing: { desc: 'a descrição é curta demais para avaliar o escopo', history: 'ainda não há tarefas parecidas concluídas para comparar', codexModel: 'o modelo padrão do Codex não foi encontrado; o custo pode ficar desconhecido' } });
510
+ Object.assign(I18N.en.tasks, { rcCurrent: 'Current', rcRecommended: 'Recommended', rcLoading: 'working it out…', rcPrivate: 'hidden in private mode', rcMissing: 'Missing data', rcAlready: 'Already on the recommended setup.', rcExternal: 'Session opened outside the office: you can see the recommendation but not apply it here. Create a task to get control.', unknownModel: 'unknown model', unknownEffort: 'unknown effort',
511
+ missing: { desc: 'the description is too short to judge the scope', history: 'no similar finished tasks to compare yet', codexModel: 'Codex default model not found; cost may stay unknown' } });
512
+ Object.assign(I18N.pt.tasks.recWhy, { 'signal-loop': 'a tarefa repetiu ações sem progresso', 'signal-retry': 'falhou mais de uma vez', 'signal-high': 'esta execução está bem acima do custo normal' });
513
+ Object.assign(I18N.en.tasks.recWhy, { 'signal-loop': 'the task repeated actions without progress', 'signal-retry': 'it failed more than once', 'signal-high': 'this run is far above the usual cost' });
514
+ I18N.pt.ver.admin = 'Admin do escritório'; I18N.en.ver.admin = 'Office admin';
515
+ }
516
+
429
517
  let T = I18N[lang];
430
518
  // sound is on by default; alerts follow the browser permission (asked on open). Only the buttons turn them off.
431
519
  let soundOn = store.get('sound', true);
@@ -89,11 +89,12 @@ function renderOverlay() {
89
89
  for (const wb of windowBoxes) parts.push(`<button class="wall-switch" data-weather="1" title="${esc(weather ? T.weather.now(weather.temp) : T.weather.ask)}" style="left:${wb.x * S}px;top:${wb.y * S}px;width:${wb.w * S}px;height:${wb.h * S}px"></button>`);
90
90
  if (usageBox) parts.push(`<button class="wall-switch usage-hit" data-usage="1" title="${esc(usageGauges().map(m => `${m.label}: ${Math.round(m.pct)}%${m.resetsAt ? ` (${T.usage.resets} ${untilText(m.resetsAt)})` : ''}`).concat([T.usage.open]).join('\n'))}" style="left:${usageBox.x * S}px;top:${usageBox.y * S}px;width:${usageBox.w * S}px;height:${usageBox.h * S}px"></button>`);
91
91
  if (switchBox) parts.push(`<button class="wall-switch" data-switch="1" title="${esc(T.lights)}" aria-pressed="${lightsOn}" style="left:${switchBox.x * S}px;top:${switchBox.y * S}px;width:${switchBox.w * S}px;height:${switchBox.h * S}px"></button>`);
92
- if (boardBox && data) { const c = counts(); parts.push(`<div class="hit wall-switch" data-tab="today" title="${esc(T.today.title)} · "${c.work} ${esc(T.working)} · ${c.need} ${esc(T.needYou)} · ${c.turn} ${esc(T.yourTurn)} · ${c.sleep} ${esc(T.asleep)}" style="left:${boardBox.x * S}px;top:${boardBox.y * S}px;width:${boardBox.w * S}px;height:${boardBox.h * S}px"></div>`); }
92
+ if (boardBox && data) parts.push(`<button class="wall-switch" data-tab="tasks" title="${esc(typeof taskBoardTip === 'function' ? taskBoardTip() : T.tasks.tab(0))}" style="left:${boardBox.x * S}px;top:${boardBox.y * S}px;width:${boardBox.w * S}px;height:${boardBox.h * S}px"></button>`);
93
93
  const box = (b, attr, title) => b ? parts.push(`<button class="wall-switch" ${attr} title="${esc(title)}" style="left:${b.x * S}px;top:${b.y * S}px;width:${b.w * S}px;height:${b.h * S}px"></button>`) : 0;
94
94
  { const m = usageData && usageData.month && usageData.month.top && usageData.month.top[0], live = m && data && data.people.find(p => p.id === m.id);
95
95
  box(aotmBox, 'data-ach="1"', m ? T.ach.aotmTip((live && live.name) || m.title || m.id.slice(0, 8), Math.round(m.activeMs / 36e5), m.tools) : T.ach.noMonth); }
96
96
  box(trophyBox, 'data-trophyroom="1"', T.trophy.shelf);
97
+ if (typeof officeTips === 'function') { const o = officeTips(); box(costBox, 'data-tab="usage"', o.costs); box(noticesBox, 'data-tab="notices"', o.notices); box(historyBox, 'data-tab="feed"', o.history); box(releaseBox, 'data-tab="version"', o.releases); }
97
98
  for (const g of gameHits) box(g, g.attr, g.title);
98
99
  { const working = data ? data.people.filter(p => !p.leaving && !['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)).length : 0;
99
100
  box(moodBox, 'data-mood="1"', T.mood[!working ? 'calm' : officeMood() === 'rush' ? 'rush' : 'normal']); }
@@ -97,6 +97,7 @@ function renderPanel() {
97
97
  ${canCompact(p) ? `<div class="actions"><button class="btn compact" data-compact-panel="${esc(p.id)}">⤓ ${esc(T.alerts.compact)} · ${Math.round(ctxPct * 100)}%</button></div>` : ''}
98
98
  <div class="actions"><button class="btn" id="btn-goto" data-id="${esc(p.id)}">›_ ${esc(p.agent === 'codex' && !p.pid ? P.gotoCodex : P.goto)}</button><span class="note" id="goto-msg"></span></div>
99
99
  ${p.coarse ? `<p class="note">${esc(T.panel.coarse)}</p>` : ''}
100
+ ${typeof taskCardHtml === 'function' ? taskCardHtml(p) : ''}
100
101
  <section class="card now" style="--c:${col}"><small>${esc(P.doing)}</small><p><span class="ico">${KIND_ICON[p.doing ? p.doing.kind : ''] || '•'}</span>${p.doing ? `<code>${esc(p.doing.tool)}</code> ` : ''}${esc(now)}</p>${p.doing ? `<span class="note">${esc(T.for(ago(p.doing.for)))}</span>` : ''}${p.state === 'waiting' ? `<p class="note">${esc(P.waitNote)}</p>` : ''}</section>
101
102
  ${p.lastPrompt ? `<section class="card quote"><small>${esc(P.lastPrompt)}${p.lastPromptAt ? ' · ' + esc(ago(data.now - p.lastPromptAt)) : ''}</small><p>${esc(p.lastPrompt)}</p></section>` : ''}
102
103
  ${p.lastReply ? `<section class="card quote reply"><small>${esc(P.lastReply)}${p.lastReplyAt ? ' · ' + esc(ago(data.now - p.lastReplyAt)) : ''}</small><p>${esc(p.lastReply)}</p></section>` : ''}
@@ -96,6 +96,7 @@ function nearThing() {
96
96
  if (near(ax, ay)) return { label: T.walk.talk(cell.p.name), run: () => (achBump('talks'), cell.p.state === 'needs_you' || cell.p.state === 'waiting' ? openTalk(cell.p.id) : showPerson(cell.p.id)) };
97
97
  }
98
98
  for (const g of gameHits) if (near(g.x + g.w / 2, g.y + g.h, 24)) return { label: g.title, run: () => { const m = /data-tab="(\w+)"/.exec(g.attr); if (m) openReport(m[1]); else if (/trophyroom/.test(g.attr)) setTrophyView(true); else if (/arcade/.test(g.attr)) openArcade(); } };
99
+ if (typeof officeTips === 'function') { const o = officeTips(); for (const [b, tab, label] of [[boardBox, 'tasks', o.tasks], [costBox, 'usage', o.costs], [noticesBox, 'notices', o.notices], [historyBox, 'feed', o.history], [releaseBox, 'version', o.releases]]) if (b && near(b.x + b.w / 2, b.y + b.h + 14, 26)) return { label: label.split('\n')[0], run: () => openReport(tab) }; }
99
100
  if (radioBox && near(radioBox.x + 7, radioBox.y + 20, 26)) return { label: radioOn ? T.radio.on : T.radio.off, run: () => setRadio(!radioOn) };
100
101
  if (catBox && near(cat.x + 6, cat.y + 6, 18)) return { label: T.walk.pet, run: petCat };
101
102
  if (elevatorBox && near(elevatorBox.x + 14, elevatorBox.y + 30, 22)) return { label: T.elevator.title, run: openElevator };
@@ -2,7 +2,7 @@
2
2
  // ---------------- replay: scrub the office back through the last hour ----------------
3
3
  // Every agent's state at a past moment comes from its own timeline (measured events), so the office
4
4
  // re-plays what really happened. Live data keeps arriving; it's just shown as of the chosen minute.
5
- let replayAt = 0, replayPlay = 0, liveData = null;
5
+ let replayAt = 0, replayPlay = 0, liveData = null, replayEnded = [];
6
6
  const replayEl = document.createElement('div');
7
7
  replayEl.className = 'replay'; replayEl.hidden = true;
8
8
  replayEl.innerHTML = '<button class="hb" data-r="play">▶</button><input type="range" min="-3600" max="0" step="15" value="0"><span class="rt"></span><button class="hb" data-r="live"></button>';
@@ -13,7 +13,10 @@ const KIND_STATE = { idle: 'idle', ask: 'needs_you' };
13
13
  function applyReplay(d) {
14
14
  if (!replayAt) return d;
15
15
  const T0 = replayAt;
16
- return { ...d, people: d.people.filter(p => !p.startedAt || p.startedAt <= T0).map(p => {
16
+ // sessions that have ended since then come back as they were (measured timelines from /api/replay)
17
+ const ghosts = replayEnded.filter(g => g.startedAt <= T0 && T0 < g.endedAt && !d.people.some(p => p.id === g.id))
18
+ .map(g => ({ state: 'idle', doing: null, files: [], editing: [], subagents: [], tools: {}, today: {}, recent: [], since: g.startedAt, ...g }));
19
+ return { ...d, people: [...d.people, ...ghosts].filter(p => !p.startedAt || p.startedAt <= T0).map(p => {
17
20
  const seg = (p.timeline || []).find(g => g.from <= T0 && T0 < g.to);
18
21
  const state = seg ? (KIND_STATE[seg.kind] || seg.kind) : 'asleep';
19
22
  return { ...p, state, doing: null, subagents: [], since: seg ? seg.from : p.since };
@@ -31,7 +34,7 @@ function toggleReplay(on) {
31
34
  if (on) achBump('replay');
32
35
  replayEl.hidden = !on;
33
36
  replayEl.querySelector('[data-r="live"]').textContent = T.replay.live;
34
- if (!on) { clearInterval(replayPlay); replayPlay = 0; setReplay(0); } else setReplay(-1800);
37
+ if (!on) { clearInterval(replayPlay); replayPlay = 0; setReplay(0); } else { setReplay(-1800); fetch('api/replay').then(r => r.json()).then(list => { replayEnded = Array.isArray(list) ? list : []; if (replayAt) setReplay(Number(rRange.value)); }).catch(() => {}); }
35
38
  }
36
39
  rRange.addEventListener('input', () => setReplay(Number(rRange.value)));
37
40
  replayEl.addEventListener('click', e => {
@@ -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();