coworking-agents 0.5.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 +28 -0
- package/README.md +14 -4
- package/ROADMAP.md +2 -4
- 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/i18n.js +60 -20
- package/public/js/panel.js +2 -1
- package/public/js/personal.js +29 -1
- package/public/js/report.js +41 -4
- package/public/js/tasks.js +214 -45
- 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 +59 -0
- package/src/collect.js +14 -2
- package/src/demo.js +2 -2
- package/src/focus.js +113 -6
- package/src/phone.js +88 -0
- package/src/qr.js +116 -0
- package/src/server.js +40 -6
- package/src/sources/codex.js +13 -3
- package/src/tasks.js +191 -29
- package/src/update.js +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
3
31
|
## 0.5.0 โ 2026-10-11
|
|
4
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
|
|
5
33
|
- Codex models and efforts come from Codex's own model list; Claude efforts low to max
|
package/README.md
CHANGED
|
@@ -180,16 +180,20 @@ Agents away from their desks (kitchen, meeting room, nap corner) carry a name ta
|
|
|
180
180
|
|
|
181
181
|
## Tasks: hand work to agents from the office
|
|
182
182
|
|
|
183
|
-
Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*), a profile and optionally a role (Backend, Frontend, Reviewer). The agent runs in the background (`claude -p` / `codex
|
|
183
|
+
Open **Tasks** and describe the work, pick a folder, an agent (Claude Code, Codex or *Automatic*), a profile and optionally a role (Backend, Frontend, Reviewer). The agent runs in the background (`claude -p` / `codex app-server`), in its own git worktree if you tick it. You never need the terminal:
|
|
184
184
|
|
|
185
|
-
- **
|
|
185
|
+
- **Permissions mid-run:** when the agent wants a tool it may not use (Claude) or to step outside its sandbox (Codex), the run pauses and the office asks: *Allow*, *Always allow* (for that task) or *Deny*, on the card, in the task and as a ๐ notice. **Answer** its questions; it resumes the same session. **Retry**, start a **new session with a summary**, or **hand off** to the other agent with the decisions, tests and open items.
|
|
186
186
|
- **Review** everything since the task started: commits, staged and unstaged changes and new files, plus your test command's output (filtered to the failing lines and the tail).
|
|
187
|
+
- **Automatic review** (opt-in in the settings): a task that finishes with changes gets a read-only reviewer with the same AI (Claude in plan mode, Codex in a read-only sandbox; edits are refused automatically); the verdict, problems with file:line and missing tests show on the original task.
|
|
188
|
+
- **Ready to ship?** Concluding first lists tests, review verdict, files changed, what is not committed and the branch. *Commit* (editable message) and *Open PR* (pushes the branch and runs `gh pr create`, needs `gh auth login`) run only when you press them.
|
|
189
|
+
- **Templates:** save a request you repeat (folder, agent, profile, role, test command) and fire it again with one click.
|
|
187
190
|
- **Queue:** priority, "after task X" dependencies, how many agents run at once and how many attempts a task gets.
|
|
188
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.
|
|
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
|
|
190
|
-
- **Cost:** per real model, tokens in/out/cache, attempts and subagents (Claude's totals include them; Codex
|
|
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.
|
|
191
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.
|
|
192
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%.
|
|
193
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.
|
|
194
198
|
|
|
195
199
|
Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folders, results, diffs and notice texts stay hidden.
|
|
@@ -198,6 +202,12 @@ Tasks live in `~/.config/coworking-agents/tasks/`. In private mode names, folder
|
|
|
198
202
|
|
|
199
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.
|
|
200
204
|
|
|
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.
|
|
208
|
+
|
|
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.
|
|
210
|
+
|
|
201
211
|
## MCP (read-only)
|
|
202
212
|
|
|
203
213
|
`claude mcp add coworking -- npx coworking-agents mcp` gives other agents `list_agents`, `get_agent_status`, `get_conflicts`, `get_daily_report` and `list_tasks`. Nothing through MCP changes anything.
|
package/ROADMAP.md
CHANGED
|
@@ -26,8 +26,6 @@ Estimated cost ยท wait reminder ยท git state on the door ยท daily report as Mark
|
|
|
26
26
|
- **Seasonal decor** by date (subtle, off with a toggle).
|
|
27
27
|
|
|
28
28
|
## Tasks โ next steps
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- Codex subagent usage (not in `codex exec --json`).
|
|
32
|
-
- CI: left out on purpose for now (GitHub Actions removed); `npm test` runs before every commit.
|
|
29
|
+
- Switching model and effort in VS Code and the desktop apps (only terminals take typing; there it applies from the next request sent from the office).
|
|
30
|
+
- CI: left out for now; `npm test` runs before every commit.
|
|
33
31
|
- Semantic search over history only if plain text search stops being enough; smarter automatic assignment.
|
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
|
}
|