coworking-agents 0.4.0 โ 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +23 -10
- package/ROADMAP.md +4 -5
- package/bin/coworking-agents.js +10 -3
- package/docs/achievements.png +0 -0
- package/docs/building.png +0 -0
- package/docs/camera.png +0 -0
- package/docs/christmas.png +0 -0
- package/docs/customise.png +0 -0
- package/docs/game-room.png +0 -0
- package/docs/halloween.png +0 -0
- package/docs/house-wall.png +0 -0
- package/docs/mobile.png +0 -0
- package/docs/office-day.png +0 -0
- package/docs/office-night.png +0 -0
- package/docs/panel.png +0 -0
- package/docs/paper.png +0 -0
- package/docs/servers.png +0 -0
- package/docs/talk.png +0 -0
- package/docs/top-bar.png +0 -0
- package/docs/trophy-room.png +0 -0
- package/docs/usage.png +0 -0
- package/docs/work-site.png +0 -0
- package/package.json +1 -1
- package/public/js/achievements.js +19 -0
- package/public/js/alerts.js +32 -1
- package/public/js/collection.js +3 -1
- package/public/js/fun2.js +15 -2
- package/public/js/help.js +11 -2
- package/public/js/i18n.js +112 -14
- package/public/js/overlay.js +2 -1
- package/public/js/panel.js +3 -1
- package/public/js/personal.js +29 -1
- package/public/js/player.js +1 -0
- package/public/js/replay.js +6 -3
- package/public/js/report.js +41 -4
- package/public/js/scene.js +6 -4
- package/public/js/tasks.js +487 -106
- package/public/js/topbar.js +12 -1
- package/public/phone/index.html +16 -0
- package/public/phone/phone.css +19 -0
- package/public/phone/phone.js +51 -0
- package/public/style.css +102 -5
- package/src/collect.js +32 -3
- package/src/demo.js +4 -4
- package/src/focus.js +113 -6
- package/src/phone.js +88 -0
- package/src/qr.js +116 -0
- package/src/server.js +58 -12
- package/src/sources/codex.js +13 -3
- package/src/tasks.js +454 -128
- package/src/update.js +27 -8
- package/src/usage.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 โ 2026-10-13
|
|
4
|
+
- Tasks redesigned: list grouped by waiting on you / running / done with coloured state pills; detail with a clear header, alerts on top, result and review on the left, cost and model on the right, one primary action, quiet secondary ones and a red cancel, activity folded
|
|
5
|
+
- Backup: export and import templates, queue and economy settings, cost goal, decor, outfits and progress as one JSON file (Personalise tab)
|
|
6
|
+
- Achievements: First try (tasks done without a retry), Approved (reviews that said OK), Penny-wise (weeks under the cost goal)
|
|
7
|
+
- Weekly card: best day, the AI used most and the week's cost; a notice on Sundays when it is ready
|
|
8
|
+
- Ready to ship?: concluding a task first shows tests, review, files, what is not committed and the branch; commit with an editable message and open a PR with `gh`, only when you press it
|
|
9
|
+
- Agents that say they are working but write nothing for 10 minutes get a notice
|
|
10
|
+
- File clashes land in the notices, with a pause button when one side is an office task
|
|
11
|
+
- Month's cost goal in Usage: spent so far, end-of-month projection, and a notice when the pace or the total goes over
|
|
12
|
+
- Shop: beanie, bow, wizard hat, round glasses, mustache, green and blue shirts; bought outfits say *Wear* and open the wardrobe instead of *Move*
|
|
13
|
+
- Phone panel (opt-in `--phone`): a small page on your Wi-Fi to see who needs you and answer permissions, replies and single-choice questions; its own restricted server and token, QR code in the terminal and in the office
|
|
14
|
+
- Automatic review (opt-in): when a task finishes with changes, a read-only reviewer with the same AI reads the diff and its verdict shows on the original task
|
|
15
|
+
- Task templates: save a request you repeat with its folder and settings; fill the form or create and run with one click
|
|
16
|
+
- The new-task form stays open and keeps what you typed across the periodic refresh
|
|
17
|
+
- Daily report to paste: cost of the day's sessions, what each agent did, what is still open (tasks and agents waiting on you), as Markdown or Slack markup
|
|
18
|
+
- Limits: at the pace of the last hour, the office warns when a 5-hour or weekly window will run out before it resets; limit warnings also land in the ๐ notices with a tip to make it last
|
|
19
|
+
- Switch in the terminal: change the model and effort of a running Claude or Codex terminal session from the office (Claude `/model` + `/effort`; Codex picker, this session only); recommendation notices do it on Apply
|
|
20
|
+
- Codex subagent cost counted from the subagents' own logs, once
|
|
21
|
+
- Compact: shrink a task's or an outside conversation's context in the background (Claude `/compact`, Codex compaction), keeping the same session
|
|
22
|
+
- Permissions mid-run: a task pauses when the agent wants a tool it may not use (Claude) or to leave its sandbox (Codex) and the office asks Allow / Always allow / Deny, on the card and as a notice
|
|
23
|
+
- Codex tasks run through `codex app-server`, the mode where Codex can ask before leaving its sandbox; cost per model call, counted once
|
|
24
|
+
- "Open the conversation" jumps to the exact conversation in VS Code, the Claude app or the Codex app
|
|
25
|
+
- Sessions opened outside are watched while they run: when there is a real reason (stuck on one step, nearly full context, the kind of work) the office sends a notice with the recommended model and effort; Apply puts it in that agent's request box
|
|
26
|
+
- Ask any Claude or Codex session from the office, even one opened in VS Code or the app: the request continues the same conversation with the model and effort you choose (the recommended ones come pre-picked)
|
|
27
|
+
- Sessions opened outside show their real effort (Codex from its log, Claude from its settings)
|
|
28
|
+
- Task detail shows current, pending and recommended config once, in the recommendation card
|
|
29
|
+
- Update check every hour and when the releases tab opens
|
|
30
|
+
|
|
31
|
+
## 0.5.0 โ 2026-10-11
|
|
32
|
+
- Agent panel: tasks started by the office show running vs pending model and effort, cost and Save / More capacity / Apply recommendation; monitored sessions show the recommendation as not applied
|
|
33
|
+
- Codex models and efforts come from Codex's own model list; Claude efforts low to max
|
|
34
|
+
- Cost per real model id, unknown cost shown as unknown (never as zero), no double counting on resumed runs; budget says when it can't be enforced
|
|
35
|
+
- Ranking by AI, task and session with the spend that happened in the chosen period
|
|
36
|
+
- Clean retries, full diff since the task started (commits, staged, new files), worktree errors reported, no conclude while running, cancel waits for the process, running agents survive a server restart
|
|
37
|
+
- Roles (Backend, Frontend, Reviewer), economy mode, filtered test logs, attempt limit, new session with a summary, optional auto-apply within limits
|
|
38
|
+
- Notices keep arriving with the tab in the background and recover what was pending; Apply applies exactly what the notice proposed; private mode hides tasks and notices
|
|
39
|
+
- Releases: notes of the newer version fetched from its package, install-aware update command, "unavailable" when npm can't be reached
|
|
40
|
+
- Search across tasks, live sessions, today and this month; replay includes sessions that ended in the last hour
|
|
41
|
+
|
|
3
42
|
## 0.4.0 โ 2026-10-11
|
|
4
43
|
- Tasks: create a task, pick folder, agent and profile; it runs in the background (Claude Code or Codex), in its own worktree if you want
|
|
5
44
|
- Answer questions, approve blocked tools, review the diff and test results, conclude, hand off or retry โ all from the office
|
package/README.md
CHANGED
|
@@ -180,20 +180,33 @@ Agents away from their desks (kitchen, meeting room, nap corner) carry a name ta
|
|
|
180
180
|
|
|
181
181
|
## Tasks: hand work to agents from the office
|
|
182
182
|
|
|
183
|
-
Open **Tasks**
|
|
183
|
+
Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*), a profile and optionally a role (Backend, Frontend, Reviewer). The agent runs in the background (`claude -p` / `codex app-server`), in its own git worktree if you tick it. You never need the terminal:
|
|
184
|
+
|
|
185
|
+
- **Permissions mid-run:** when the agent wants a tool it may not use (Claude) or to step outside its sandbox (Codex), the run pauses and the office asks: *Allow*, *Always allow* (for that task) or *Deny*, on the card, in the task and as a ๐ notice. **Answer** its questions; it resumes the same session. **Retry**, start a **new session with a summary**, or **hand off** to the other agent with the decisions, tests and open items.
|
|
186
|
+
- **Review** everything since the task started: commits, staged and unstaged changes and new files, plus your test command's output (filtered to the failing lines and the tail).
|
|
187
|
+
- **Automatic review** (opt-in in the settings): a task that finishes with changes gets a read-only reviewer with the same AI (Claude in plan mode, Codex in a read-only sandbox; edits are refused automatically); the verdict, problems with file:line and missing tests show on the original task.
|
|
188
|
+
- **Ready to ship?** Concluding first lists tests, review verdict, files changed, what is not committed and the branch. *Commit* (editable message) and *Open PR* (pushes the branch and runs `gh pr create`, needs `gh auth login`) run only when you press them.
|
|
189
|
+
- **Templates:** save a request you repeat (folder, agent, profile, role, test command) and fire it again with one click.
|
|
190
|
+
- **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
|
|
191
|
+
- **Model and effort:** Claude `haiku`/`sonnet`/`opus` with efforts low โ max; Codex models and efforts from Codex's own model list. *Save*, *More capacity*, *Apply recommendation* or pick by hand. A change made while a step runs is shown as **pending** and marked **applied** when the next step starts.
|
|
192
|
+
- **Click the agent:** a session started by the office shows its task, running vs pending config, cost and the same controls. A session opened elsewhere (terminal, VS Code extension, the Claude or Codex app) shows its real model and effort, the recommendation, and an **Ask this agent** box: the request continues that same conversation in the background (`claude -p --resume` / Codex `thread/resume`) with the model and effort you pick, and from then on it has the task controls. It waits while the agent is busy so two replies never mix; **Open the conversation** jumps straight to it in VS Code, the Claude app or the Codex app (reopen it there if it was already open). A session running in a terminal (Terminal, iTerm or tmux) also gets **Switch in the terminal**: the office types `/model` and `/effort` into Claude, or drives Codex's own `/model` picker choosing *this session only*, so Codex's saved default is untouched (Codex in macOS Terminal needs the Accessibility permission). While it runs the office keeps watching and, when there's a concrete reason (stuck on one step for 10+ min, context almost full, the work looks deeper or lighter than its setup), sends a notice with the recommendation: *Apply* picks it for the next request, *Not now* leaves it.
|
|
193
|
+
- **Cost:** per real model, tokens in/out/cache, attempts and subagents (Claude's totals include them; Codex subagents are read from their own logs, which point at the parent conversation). Unknown prices show as unknown, never as zero. Ranking by AI, task and session for today / 7 / 30 days. A budget pauses the task; when the cost is unknown it says the budget can't be enforced, and a call already running may go slightly over.
|
|
194
|
+
- **Recommendations:** local rules on scope, wording and attempts plus what similar finished tasks show (success with tests, cost, retries). No AI call to choose. Optional **auto-apply** up to a profile and a number of changes per task, all logged.
|
|
195
|
+
- **Warnings:** the same action repeated without progress, two failed attempts, a run costing over 3ร the median finished task, budget reached or unenforceable. Notices carry the exact proposal: *Apply*, *See why*, *Not now*.
|
|
196
|
+
- **Compact:** shrinks the same session's context (Claude `/compact`, Codex compaction) without running anything else, for office tasks and for conversations opened in VS Code or the app once their context passes 50%.
|
|
197
|
+
- **Economy mode:** short instructions on demand (read only what's needed, no long logs, no re-exploring), filtered logs, attempt and parallel limits, resume to keep the cache, or a fresh session with a summary when the context is too big.
|
|
198
|
+
|
|
199
|
+
Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folders, results, diffs and notice texts stay hidden.
|
|
184
200
|
|
|
185
|
-
|
|
186
|
-
- **Review** the result, the files changed, the diff and your test command's output, then **conclude**, **retry** or **hand off** to the other agent (with a summary of what was done).
|
|
187
|
-
- **Queue:** priority, "after task X" dependencies and how many agents run at once.
|
|
188
|
-
- **Cost:** per task (tokens in/out/cache, estimated USD, attempts), a ranking by task and by AI for today / 7 / 30 days, and an optional budget that pauses the task when reached.
|
|
189
|
-
- **Profiles and controls:** Economy, Balanced, Deep. *Save*, *More capacity*, *Apply recommendation* or pick model and effort by hand; changes apply from the next step and are logged. The recommendation is plain rules plus what similar past tasks taught (no AI call to choose).
|
|
190
|
-
- **Warnings:** the same action repeated without progress, two failed attempts, budget reached.
|
|
201
|
+
## Notices, releases and the office admin
|
|
191
202
|
|
|
192
|
-
|
|
203
|
+
The ๐ bell collects agent requests, task updates, recommendations (*Apply*, *See why*, *Not now*), cost alerts and new versions, grouped so repeats don't pile up. It keeps working with the tab in the background and recovers what was already pending when you open the office. **Releases** shows the installed version, the newest on npm (or *unavailable*), when it checked, a clickable map of every release and the notes of the newer one, with the update command for how you installed it. The office admin appears behind the top-bar counter only when there's version news: he waves the sign until you've seen it, keeps holding it until you update, then celebrates.
|
|
193
204
|
|
|
194
|
-
##
|
|
205
|
+
## Phone panel (opt-in)
|
|
206
|
+
|
|
207
|
+
`npx coworking-agents --phone` also serves a small page for your phone on the same Wi-Fi: who needs you, tasks with their permission questions (*Allow* / *Deny*), reply, conclude or cancel a task, and pick an option when an agent asks a single-choice question. Scan the QR code printed in the terminal or shown in โฐ โ ๐ฑ Phone.
|
|
195
208
|
|
|
196
|
-
|
|
209
|
+
It is a separate server that only answers on your LAN address, with its own token (new each run), an exact Host and Origin check, and none of the office's other routes (files, diffs, launching, windows). The office itself stays on `127.0.0.1`. The local network link is plain HTTP, so use it only on networks you trust.
|
|
197
210
|
|
|
198
211
|
## MCP (read-only)
|
|
199
212
|
|
package/ROADMAP.md
CHANGED
|
@@ -25,8 +25,7 @@ Estimated cost ยท wait reminder ยท git state on the door ยท daily report as Mark
|
|
|
25
25
|
|
|
26
26
|
- **Seasonal decor** by date (subtle, off with a toggle).
|
|
27
27
|
|
|
28
|
-
## Tasks
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- Semantic search over history only if plain text search stops being enough.
|
|
28
|
+
## Tasks โ next steps
|
|
29
|
+
- Switching model and effort in VS Code and the desktop apps (only terminals take typing; there it applies from the next request sent from the office).
|
|
30
|
+
- CI: left out for now; `npm test` runs before every commit.
|
|
31
|
+
- Semantic search over history only if plain text search stops being enough; smarter automatic assignment.
|
package/bin/coworking-agents.js
CHANGED
|
@@ -12,11 +12,12 @@ const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1]
|
|
|
12
12
|
if (flag('-h') || flag('--help')) {
|
|
13
13
|
console.log(`coworking-agents โ a pixel office for your AI coding agents (Claude Code, Codex, โฆ)
|
|
14
14
|
|
|
15
|
-
npx coworking-agents [--port 4777] [--no-open] [--private]
|
|
15
|
+
npx coworking-agents [--port 4777] [--no-open] [--private] [--phone]
|
|
16
16
|
|
|
17
17
|
--port port to listen on (default 4777, falls back to a free one)
|
|
18
18
|
--no-open don't open the browser
|
|
19
19
|
--private hide session titles, paths and file names (screen sharing)
|
|
20
|
+
--phone also serve a small phone panel on your Wi-Fi (own token, QR code in the terminal; trusted networks only)
|
|
20
21
|
--demo [N] show a fake office with every state and N agents (default 10)
|
|
21
22
|
--json print one snapshot and exit
|
|
22
23
|
mcp run a read-only MCP server on stdio (claude mcp add coworking -- npx coworking-agents mcp)
|
|
@@ -58,16 +59,18 @@ async function reuse() {
|
|
|
58
59
|
const privacy = flag('--private'), demo = flag('--demo');
|
|
59
60
|
if (!demo && !privacy && !flag('--port')) {
|
|
60
61
|
const s = await reuse();
|
|
62
|
+
if (s && flag('--phone')) { console.log(`coworking-agents is already running without the phone panel (${s.base}). Close it (Ctrl+C in its terminal) and start again with --phone.`); return; }
|
|
61
63
|
if (s) { console.log(`coworking-agents already running โ ${s.base}/?t=${s.token}`); if (!flag('--no-open')) open(`${s.base}/?t=${s.token}`); return; }
|
|
62
64
|
}
|
|
63
65
|
// keep the same token across restarts so an open tab keeps working (file is 0600, only yours)
|
|
64
66
|
let token;
|
|
65
67
|
if (!demo && !privacy) { try { const prev = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); if (/^[0-9a-f]{48}$/.test(prev.token)) token = prev.token; } catch {} }
|
|
66
68
|
let hq;
|
|
67
|
-
|
|
69
|
+
const phone = flag('--phone');
|
|
70
|
+
try { hq = await start({ port, privacy, demo, token, phone }); }
|
|
68
71
|
catch (e) {
|
|
69
72
|
if (e.code !== 'EADDRINUSE') throw e;
|
|
70
|
-
hq = await start({ port: 0, privacy, demo, token }); // port taken by something else: use a free one
|
|
73
|
+
hq = await start({ port: 0, privacy, demo, token, phone }); // port taken by something else: use a free one
|
|
71
74
|
}
|
|
72
75
|
if (!demo && !privacy) {
|
|
73
76
|
try {
|
|
@@ -79,6 +82,10 @@ async function reuse() {
|
|
|
79
82
|
} catch {}
|
|
80
83
|
}
|
|
81
84
|
console.log(`coworking-agents โ ${hq.url}${privacy ? ' (private mode)' : ''}\nCtrl+C to close.`);
|
|
85
|
+
if (phone) {
|
|
86
|
+
if (hq.phone) console.log(`\nPhone (same Wi-Fi, trusted networks only โ the LAN link is not encrypted) โ ${hq.phone.url}\n${require('../src/qr').toTerminal(require('../src/qr').encode(hq.phone.url))}\n`);
|
|
87
|
+
else console.log(`\nPhone panel not started: ${hq.phoneReason === 'nolan' ? 'no local network address found' : hq.phoneReason}`);
|
|
88
|
+
}
|
|
82
89
|
if (!flag('--no-open')) open(hq.url);
|
|
83
90
|
if (!demo && !flag('--no-update-check')) {
|
|
84
91
|
const up = await require('../src/update').startChecks();
|
package/docs/achievements.png
CHANGED
|
Binary file
|
package/docs/building.png
CHANGED
|
Binary file
|
package/docs/camera.png
CHANGED
|
Binary file
|
package/docs/christmas.png
CHANGED
|
Binary file
|
package/docs/customise.png
CHANGED
|
Binary file
|
package/docs/game-room.png
CHANGED
|
Binary file
|
package/docs/halloween.png
CHANGED
|
Binary file
|
package/docs/house-wall.png
CHANGED
|
Binary file
|
package/docs/mobile.png
CHANGED
|
Binary file
|
package/docs/office-day.png
CHANGED
|
Binary file
|
package/docs/office-night.png
CHANGED
|
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
|
package/docs/trophy-room.png
CHANGED
|
Binary file
|
package/docs/usage.png
CHANGED
|
Binary file
|
package/docs/work-site.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coworking-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A pixel-art coworking office for your AI coding agents (Claude Code, Codex, โฆ): see who's working, who needs you, and who's editing the same checkout.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"coworking-agents": "bin/coworking-agents.js"
|
|
@@ -60,6 +60,7 @@ function achFacts() {
|
|
|
60
60
|
models: models.size,
|
|
61
61
|
skills: Math.max(0, ...cred.map(c => (c.skills || []).length)), mcps: Math.max(0, ...cred.map(c => (c.mcps || []).length)),
|
|
62
62
|
parallel: achCount('parallel'), aiKinds: achCount('aiKinds'), pets: achCount('pets'), zeroQueue: achCount('zeroQueue'),
|
|
63
|
+
cleanTasks: achCount('cleanTasks'), reviewOk: achCount('reviewOk'), underGoal: achCount('underGoal'),
|
|
63
64
|
clashes: achCount('clashes'), team: achCount('team'), bigCtx: achCount('bigCtx'), files: achCount('files'), sessionTools: achCount('sessionTools'),
|
|
64
65
|
fast: achCount('fast'), patience: achCount('patience'), compacts: achCount('compacts'), focusDone: achCount('focusDone'),
|
|
65
66
|
replay: achCount('replay'), avatars: achCount('avatars'), lights: achCount('lights'), weatherDays: achCount('weatherDays'),
|
|
@@ -110,6 +111,9 @@ const ACH = [
|
|
|
110
111
|
{ id: 'writer', g: 'live', icon: '๐', fact: 'files', steps: [5, 15, 30, 60, 100] },
|
|
111
112
|
{ id: 'workaholic', g: 'live', icon: 'โ', fact: 'sessionTools', steps: [200, 500, 1000, 2000, 4000] },
|
|
112
113
|
{ id: 'bump', g: 'live', icon: '๐ฅ', fact: 'clashes', steps: [1, 5, 20, 50, 100] },
|
|
114
|
+
{ id: 'cleanrun', g: 'live', icon: '๐ฏ', fact: 'cleanTasks', steps: [10, 25, 50, 100, 250] },
|
|
115
|
+
{ id: 'approved', g: 'live', icon: '๐', fact: 'reviewOk', steps: [1, 5, 20, 50, 100] },
|
|
116
|
+
{ id: 'thrifty', g: 'live', icon: '๐ท', fact: 'underGoal', steps: [1, 4, 12, 26, 52] },
|
|
113
117
|
{ id: 'fast', g: 'live', icon: '๐', fact: 'fast', steps: [1, 10, 50, 200, 500] },
|
|
114
118
|
{ id: 'patience', g: 'live', icon: '๐ฟ', fact: 'patience', steps: [1, 5, 15, 40, 100] },
|
|
115
119
|
{ id: 'inbox', g: 'live', icon: 'โ', fact: 'zeroQueue', steps: [1, 10, 50, 150, 500] },
|
|
@@ -202,7 +206,22 @@ function checkAchievements() {
|
|
|
202
206
|
}
|
|
203
207
|
for (const p of live) if (p.repo) { achAdd('repos', p.repo.name); if (p.branch && p.branch !== 'HEAD') achAdd('branches', p.repo.name + ':' + p.branch); }
|
|
204
208
|
{ const clean = trees.size >= 3 && [...trees.values()].every(g => g.dirty === 0); if (clean && !achClean) achBump('cleanDesk'); achClean = clean; }
|
|
209
|
+
// Sunday: the week's card is ready (once a week)
|
|
210
|
+
if (new Date().getDay() === 0 && usageData && usageData.days && typeof pushNotice === 'function' && store.get('weekCardSaid', '') !== weekKey()) {
|
|
211
|
+
store.set('weekCardSaid', weekKey()); pushNotice('week:' + weekKey(), 'ok', T.share.ready, T.share.readyWhy, { tab: 'paper' });
|
|
212
|
+
}
|
|
205
213
|
for (const c of data.fileClashes || []) achAdd('clashes', c.file + '|' + c.who.slice().sort().join(','));
|
|
214
|
+
// office tasks: done on the first attempt, a review that said OK, and weeks spent under the month's cost goal
|
|
215
|
+
for (const t of (typeof taskData !== 'undefined' && taskData.tasks) || []) {
|
|
216
|
+
if (t.state === 'done' && t.attempts === 1) achAdd('cleanTasks', t.id);
|
|
217
|
+
if (t.review && /^\s*OK\b/i.test(t.review.text || '')) achAdd('reviewOk', t.id);
|
|
218
|
+
}
|
|
219
|
+
{ const goal = Number(((typeof taskData !== 'undefined' && taskData.settings) || {}).monthGoal) || 0, days = (usageData && usageData.days) || {};
|
|
220
|
+
if (goal) {
|
|
221
|
+
const keys = Array.from({ length: 7 }, (_, i) => { const d = new Date(Date.now() - (i + 1) * 864e5); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); });
|
|
222
|
+
const worked = keys.filter(k => days[k] && days[k].tools >= 10).length, spent = keys.reduce((n, k) => n + ((days[k] && days[k].usd) || 0), 0);
|
|
223
|
+
if (worked >= 3 && spent <= goal * 7 / 30) achAdd('underGoal', keys[keys.length - 1]); // one per finished 7-day window
|
|
224
|
+
} }
|
|
206
225
|
const need = live.filter(p => p.state === 'needs_you' || p.state === 'waiting').length;
|
|
207
226
|
if (achQueueWas >= 2 && need === 0) achBump('zeroQueue');
|
|
208
227
|
achQueueWas = need;
|
package/public/js/alerts.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// ---------------- alerts: rate limits filling up, contexts close to compaction, and the compact action ----------------
|
|
3
3
|
const CTX_WARN = .75; // from here a "compact" button shows over the agent
|
|
4
|
-
const LIMIT_STEPS = [80, 95];
|
|
4
|
+
const LIMIT_STEPS = [80, 95];
|
|
5
|
+
const STALL_MS = 10 * 60000; // working, but nothing new in its log for this long // notify once per window when usage crosses these
|
|
5
6
|
const WAIT_NUDGE = 5 * 60000; // an agent waiting on you this long gets one reminder
|
|
6
7
|
const waitStart = new Map(); // id โ { state, at }: when it started waiting
|
|
7
8
|
const alerted = new Set();
|
|
@@ -36,11 +37,29 @@ function checkAlerts() {
|
|
|
36
37
|
const key = `lim:${id}:${win}:${step}:${w.resetsAt}`;
|
|
37
38
|
if (w.usedPercent >= step && !alerted.has(key)) {
|
|
38
39
|
alerted.add(key);
|
|
40
|
+
if (typeof pushNotice === 'function') { const label = `${(AGENT[id] || {}).label || id} ยท ${win === '5h' ? T.usage.fiveHour : T.usage.week}`; pushNotice(`lim:${id}:${win}`, step >= 95 ? 'warn' : 'ok', `${A.limit(Math.round(w.usedPercent))} ยท ${label}`, `${T.usage.resets} ${untilText(w.resetsAt)}. ${A.limitTip}`, {}); }
|
|
39
41
|
if (!firstAlertPass) { const label = `${(AGENT[id] || {}).label || id} ยท ${win === '5h' ? T.usage.fiveHour : T.usage.week}`; toast(`<b>${esc(A.limit(Math.round(w.usedPercent)))}</b>${esc(label)} ยท ${esc(T.usage.resets)} ${esc(untilText(w.resetsAt))}`, step >= 95 ? 'bad' : 'warn'); notifyOS(A.limit(Math.round(w.usedPercent)), `${label} ยท ${T.usage.resets} ${untilText(w.resetsAt)}`); }
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
// pace: at the rate of the last ~hour, will the window run out before it resets? said once per window
|
|
47
|
+
for (const [id, l] of Object.entries(L)) {
|
|
48
|
+
for (const [win, w] of [['5h', l && l.primary], ['week', l && l.secondary]]) {
|
|
49
|
+
if (!w || !w.resetsAt) continue;
|
|
50
|
+
const k = id + ':' + win, list = (paceSamples.get(k) || []).filter(x => x.resetsAt === w.resetsAt && data.now - x.at < 36e5);
|
|
51
|
+
if (!list.length || list[list.length - 1].pct !== w.usedPercent) list.push({ at: data.now, pct: w.usedPercent, resetsAt: w.resetsAt });
|
|
52
|
+
paceSamples.set(k, list); try { store.set('pace', JSON.stringify([...paceSamples])); } catch {}
|
|
53
|
+
const first = list[0], hrs = (data.now - first.at) / 36e5, rate = hrs >= .25 ? (w.usedPercent - first.pct) / hrs : 0; // % per hour
|
|
54
|
+
if (rate <= 0 || w.usedPercent < 40 || w.usedPercent >= 100) continue;
|
|
55
|
+
const left = (100 - w.usedPercent) / rate * 36e5, key = `pace:${k}:${w.resetsAt}`;
|
|
56
|
+
if (data.now + left < w.resetsAt - 6e5 && !alerted.has(key) && !paceSaid.includes(key)) {
|
|
57
|
+
alerted.add(key); paceSaid = [...paceSaid.slice(-20), key]; try { store.set('paceSaid', JSON.stringify(paceSaid)); } catch {} const label = `${(AGENT[id] || {}).label || id} ยท ${win === '5h' ? T.usage.fiveHour : T.usage.week}`;
|
|
58
|
+
if (typeof pushNotice === 'function') pushNotice('pace:' + k, 'warn', A.pace(label, untilText(data.now + left)), A.paceWhy(Math.round(w.usedPercent), Math.round(rate), untilText(w.resetsAt)), {});
|
|
59
|
+
notifyOS(A.pace(label, untilText(data.now + left)), A.paceWhy(Math.round(w.usedPercent), Math.round(rate), untilText(w.resetsAt)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
44
63
|
for (const p of data.people) {
|
|
45
64
|
const pct = ctxPct(p), key = `ctx:${p.id}:${p.compactedAt || 0}`;
|
|
46
65
|
if (pct >= .85 && !alerted.has(key)) {
|
|
@@ -48,6 +67,16 @@ function checkAlerts() {
|
|
|
48
67
|
if (!firstAlertPass) { toast(`<b>${esc(A.context(Math.round(pct * 100)))}</b>${esc(p.name)} ยท ${esc(p.title || '')}`, 'warn'); notifyOS(A.context(Math.round(pct * 100)), p.name); }
|
|
49
68
|
}
|
|
50
69
|
}
|
|
70
|
+
// stalled: says it is working but wrote nothing to its log for a while (stuck, looping on a slow command, waiting on the network)
|
|
71
|
+
for (const p of data.people) {
|
|
72
|
+
if (p.leaving || !p.lastActivity || ['idle', 'asleep', 'needs_you', 'waiting'].includes(p.state)) continue;
|
|
73
|
+
const quiet = data.now - p.lastActivity, key = `stall:${p.id}:${p.lastActivity}`;
|
|
74
|
+
if (quiet >= STALL_MS && !alerted.has(key)) {
|
|
75
|
+
alerted.add(key); const mins = Math.round(quiet / 60000), what = p.doing ? `${p.doing.tool}${p.doing.what ? ' ' + p.doing.what : ''}` : '';
|
|
76
|
+
if (typeof pushNotice === 'function') pushNotice('stall:' + p.id, 'warn', A.stall(p.name, mins), A.stallWhy(what), { agent: p.id });
|
|
77
|
+
if (!firstAlertPass) notifyOS(A.stall(p.name, mins), A.stallWhy(what));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
51
80
|
// waiting on you for a while (a question or a command to approve): one reminder per wait
|
|
52
81
|
const seen = new Set();
|
|
53
82
|
for (const p of data.people) {
|
|
@@ -74,6 +103,8 @@ function checkAlerts() {
|
|
|
74
103
|
}
|
|
75
104
|
firstAlertPass = false;
|
|
76
105
|
}
|
|
106
|
+
let paceSaid = []; try { paceSaid = JSON.parse(store.get('paceSaid', '[]')) || []; } catch { paceSaid = []; }
|
|
107
|
+
let paceSamples = new Map(); try { paceSamples = new Map(JSON.parse(store.get('pace', '[]'))); } catch { paceSamples = new Map(); }
|
|
77
108
|
let firstAlertPass = true; // what's already true on load is noted, not announced
|
|
78
109
|
|
|
79
110
|
async function compactAgent(id, btn) {
|
package/public/js/collection.js
CHANGED
|
@@ -100,6 +100,8 @@ const SHOP = [
|
|
|
100
100
|
{ id: 'bigtank', price: 900, icon: '๐ ' },
|
|
101
101
|
{ id: 'cap', price: 150, icon: '๐งข' }, { id: 'headphones', price: 300, icon: '๐ง' }, { id: 'shades', price: 250, icon: '๐ถ' }, { id: 'party', price: 200, icon: '๐ฅณ' },
|
|
102
102
|
{ id: 'shirt_pink', price: 300, icon: '๐' }, { id: 'shirt_black', price: 300, icon: '๐' }, { id: 'shirt_gold', price: 400, icon: '๐' }, { id: 'crown', price: 1500, icon: '๐' },
|
|
103
|
+
{ id: 'beanie', price: 200, icon: '๐งถ' }, { id: 'bow', price: 150, icon: '๐' }, { id: 'wizard', price: 700, icon: '๐ง' }, { id: 'glasses', price: 200, icon: '๐' }, { id: 'mustache', price: 150, icon: '๐ฅธ' },
|
|
104
|
+
{ id: 'shirt_green', price: 300, icon: '๐' }, { id: 'shirt_blue', price: 300, icon: '๐' },
|
|
103
105
|
];
|
|
104
106
|
const owns = id => prog.owned.includes(id);
|
|
105
107
|
function buy(id) {
|
|
@@ -113,7 +115,7 @@ function renderShop() {
|
|
|
113
115
|
const S2 = T.shop, body = reportEl.querySelector('.report-body'), c = coins();
|
|
114
116
|
body.innerHTML = tabsHtml() + `<div class="wallet"><span class="coin"></span><b>${c.toLocaleString(lang === 'pt' ? 'pt-BR' : 'en')}</b> ${esc(T.ach.coins)}<span class="note">${esc(S2.sub)}</span></div>
|
|
115
117
|
<div class="shop">${SHOP.map(it => { const has = owns(it.id), can = c >= it.price; return `<div class="item ${has ? 'owned' : ''}"><span class="ach-ico">${it.icon}</span><div><b>${esc(S2.items[it.id].name)}</b><p>${esc(S2.items[it.id].desc)}</p>
|
|
116
|
-
${has ? `<small class="ok">${esc(S2.owned)}</small> <button class="btn small" data-move-item>${esc(S2.move)}</button>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>
|
|
118
|
+
${has && typeof OUTFIT !== 'undefined' && OUTFIT[it.id] ? `<small class="ok">${esc(S2.inWardrobe)}</small> <button class="btn small" data-tab="office">${esc(S2.wear)}</button>` : has ? `<small class="ok">${esc(S2.owned)}</small> <button class="btn small" data-move-item>${esc(S2.move)}</button>` : `<button class="btn small" data-buy="${it.id}" ${can ? '' : 'disabled'}><span class="coin"></span> ${it.price}</button>`}</div></div>`; }).join('')}</div>
|
|
117
119
|
${prog.owned.length ? `<p><button class="btn small" data-move-item>โฅ ${esc(S2.decoTitle)}</button></p>` : ''}`;
|
|
118
120
|
}
|
|
119
121
|
reportEl.addEventListener('click', e => { if (e.target.closest('[data-share-card]')) return shareCard(); if (e.target.closest('[data-visit-photo]')) return visitPhoto(); const b = e.target.closest('[data-buy]'); if (b) buy(b.dataset.buy); if (e.target.closest('[data-move-item]')) setDecorating(true); });
|
package/public/js/fun2.js
CHANGED
|
@@ -42,8 +42,8 @@ function missionsHtml() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// ---- your outfit: hats, shades and shirts from the shop, worn by your character ----
|
|
45
|
-
const OUTFIT = { cap: 'hat', crown: 'hat', headphones: 'hat', party: 'hat', shades: 'face', shirt_gold: 'shirt', shirt_pink: 'shirt', shirt_black: 'shirt' };
|
|
46
|
-
const SHIRTS = { shirt_gold: '#d9a441', shirt_pink: '#e8579a', shirt_black: '#2a2630' };
|
|
45
|
+
const OUTFIT = { cap: 'hat', crown: 'hat', headphones: 'hat', party: 'hat', beanie: 'hat', bow: 'hat', wizard: 'hat', shades: 'face', glasses: 'face', mustache: 'face', shirt_gold: 'shirt', shirt_pink: 'shirt', shirt_black: 'shirt', shirt_green: 'shirt', shirt_blue: 'shirt' };
|
|
46
|
+
const SHIRTS = { shirt_gold: '#d9a441', shirt_pink: '#e8579a', shirt_black: '#2a2630', shirt_green: '#3fa66b', shirt_blue: '#3b5dc9' };
|
|
47
47
|
function wearing(slot) { const w = (prog.style || {})['wear_' + slot]; return w && owns(w) ? w : ''; }
|
|
48
48
|
function youLook() {
|
|
49
49
|
const lk = Art.look('you:' + (avatars.__you || 0)), sh = wearing('shirt');
|
|
@@ -55,6 +55,11 @@ function drawOutfit(x, y) { // over the standing sprite's head (top at y - 2)
|
|
|
55
55
|
if (hat === 'crown') { r(x + 3, y - 4, 10, 4, '#ffd84d'); r(x + 3, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 7, 2, 3, '#ffd84d'); r(x + 11, y - 6, 2, 2, '#ffd84d'); r(x + 7, y - 3, 2, 1, '#e43b44'); }
|
|
56
56
|
if (hat === 'headphones') { r(x + 1, y - 3, 14, 2, PAL.ink); r(x, y + 3, 3, 5, '#3b5dc9'); r(x + 13, y + 3, 3, 5, '#3b5dc9'); }
|
|
57
57
|
if (hat === 'party') Art.drawHat('party', x, y - 2);
|
|
58
|
+
if (hat === 'beanie') { r(x + 2, y - 3, 12, 4, '#3b5dc9'); r(x + 2, y, 12, 1, '#26408f'); r(x + 4, y - 2, 1, 2, '#5b7ce0'); r(x + 8, y - 2, 1, 2, '#5b7ce0'); r(x + 7, y - 5, 2, 2, '#ffffff'); }
|
|
59
|
+
if (hat === 'bow') { r(x + 10, y - 2, 2, 3, '#e8579a'); r(x + 14, y - 2, 2, 3, '#e8579a'); r(x + 12, y - 1, 2, 2, '#b8336f'); }
|
|
60
|
+
if (hat === 'wizard') { r(x + 1, y - 1, 14, 2, '#5a3a9c'); r(x + 4, y - 4, 8, 3, '#6b45b8'); r(x + 6, y - 7, 5, 3, '#6b45b8'); r(x + 8, y - 9, 3, 2, '#6b45b8'); r(x + 7, y - 3, 1, 1, '#ffd84d'); r(x + 9, y - 6, 1, 1, '#ffd84d'); }
|
|
61
|
+
if (wearing('face') === 'glasses') { r(x + 3, y + 4, 4, 1, PAL.ink); r(x + 3, y + 6, 4, 1, PAL.ink); r(x + 3, y + 5, 1, 1, PAL.ink); r(x + 6, y + 5, 1, 1, PAL.ink); r(x + 9, y + 4, 4, 1, PAL.ink); r(x + 9, y + 6, 4, 1, PAL.ink); r(x + 9, y + 5, 1, 1, PAL.ink); r(x + 12, y + 5, 1, 1, PAL.ink); r(x + 7, y + 5, 2, 1, PAL.ink); }
|
|
62
|
+
if (wearing('face') === 'mustache') { r(x + 5, y + 8, 6, 1, '#5a3a22'); r(x + 4, y + 9, 2, 1, '#5a3a22'); r(x + 10, y + 9, 2, 1, '#5a3a22'); }
|
|
58
63
|
if (wearing('face') === 'shades') { r(x + 3, y + 5, 10, 2, PAL.ink); r(x + 4, y + 5, 3, 1, '#3a4566'); r(x + 9, y + 5, 3, 1, '#3a4566'); }
|
|
59
64
|
}
|
|
60
65
|
function outfitHtml() {
|
|
@@ -219,6 +224,14 @@ function shareCard() {
|
|
|
219
224
|
g.fillStyle = '#c0cbdc'; g.font = '14px system-ui, sans-serif'; g.fillText(T.share.week, 24, 76);
|
|
220
225
|
const stat = (y, big, label) => { g.fillStyle = '#ffffff'; g.font = 'bold 30px system-ui, sans-serif'; g.fillText(big, 24, y); g.fillStyle = '#9a9187'; g.font = '13px system-ui, sans-serif'; g.fillText(label, 24, y + 18); };
|
|
221
226
|
stat(126, hours.toFixed(1) + 'h', T.share.hours); stat(186, fmtK(tools), T.share.actions); stat(246, `${tiers} ยท ${legends}โ
`, T.share.trophies);
|
|
227
|
+
// the week in three lines: best day, the AI used most, what it cost (estimated)
|
|
228
|
+
const best = days.slice().sort((a, b) => (d(b).activeMs || 0) - (d(a).activeMs || 0))[0], usd = days.reduce((n, k) => n + (d(k).usd || 0), 0);
|
|
229
|
+
const byAi = {}; for (const k of days) for (const [ai, v] of Object.entries(d(k).byAgent || {})) byAi[ai] = (byAi[ai] || 0) + (v.activeMs || 0);
|
|
230
|
+
const topAi = Object.entries(byAi).sort((a, b) => b[1] - a[1])[0];
|
|
231
|
+
const line = (y, label, value) => { g.fillStyle = '#9a9187'; g.font = '13px system-ui, sans-serif'; g.fillText(label, 300, y); g.fillStyle = '#ffffff'; g.font = 'bold 14px system-ui, sans-serif'; g.fillText(value, 420, y); };
|
|
232
|
+
if (best && d(best).activeMs) line(232, T.share.bestDay, `${new Date(best + 'T12:00:00').toLocaleDateString(lang === 'pt' ? 'pt-BR' : 'en', { weekday: 'long' })} ยท ${(d(best).activeMs / 36e5).toFixed(1)}h`);
|
|
233
|
+
if (topAi) line(254, T.share.topAi, (AGENT[topAi[0]] || {}).label || topAi[0]);
|
|
234
|
+
if (usd) line(276, T.share.cost, fmtUSD(usd));
|
|
222
235
|
g.fillStyle = '#d97757'; g.fillRect(0, 290, 600, 25); g.fillStyle = '#ffffff'; g.font = 'bold 13px system-ui, sans-serif'; g.fillText('npx coworking-agents', 24, 307);
|
|
223
236
|
c.toBlob(b => { if (!b) return; const a = document.createElement('a'); a.href = URL.createObjectURL(b); a.download = `coworking-week-${weekKey()}.png`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); toast(`<b>${esc(T.share.saved)}</b>${esc(a.download)}`, 'ok'); achBump('shares'); }, 'image/png');
|
|
224
237
|
}
|
package/public/js/help.js
CHANGED
|
@@ -72,7 +72,8 @@ function sceneBox() {
|
|
|
72
72
|
const x0 = Math.max(Math.round(W * .3), Math.ceil((c.right - b0.left) / DOCK_S) + 18);
|
|
73
73
|
// the office must end before the tools (and their speech clouds before the "+N" tag)
|
|
74
74
|
const tools = document.querySelector('.bar .tools'), tl = tools ? (tools.getBoundingClientRect().left - b0.left) / DOCK_S : W;
|
|
75
|
-
const
|
|
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');
|