@caglarutkuguler/cubicle 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,13 +65,14 @@ URL parameters:
65
65
  - `?company=PREFIX` opens a specific Paperclip company (for example `?company=MEG`). With several companies, a picker also appears in the header.
66
66
  - `?lang=en` or `?lang=tr` sets the language.
67
67
  - `?demo` shows fake agents.
68
+ - `?kiosk` fills the screen with the office for a TV or a second monitor: cards and footer hidden, names and bubbles scale with the screen, cursor hidden. Combined with the auto-updater, a wall display picks up new versions by itself.
68
69
 
69
70
  ## What the office shows
70
71
 
71
72
  | Status | In the office |
72
73
  | --- | --- |
73
74
  | `running` | Sits at its desk, monitor on, typing; bubble shows the task |
74
- | `waiting` | Sits at its desk with a hand up, amber screen — it needs your input or approval |
75
+ | `waiting` | Sits at its desk with a hand up, amber screen — it needs your input or approval. With Paperclip this comes from issues whose review is waiting on the board (for example pending ask-user questions); a busy agent keeps typing but its bubble flashes the issue id, and the header counts how many need you |
75
76
  | `idle` | Wanders the lounge; says "✓ done" right after finishing a run |
76
77
  | `error` | Slumped at its desk, red screen, error text on its card |
77
78
  | `paused` | "zZ" |
@@ -96,6 +97,21 @@ Use a board API key with the narrowest read-only scope Paperclip lets you create
96
97
 
97
98
  **Feed.** Any process can write `{ "company": "…", "agents": [{ "id", "name", "role", "status", "task", "error" }] }` to a file or serve it over HTTP. Full spec with status aliases: [`docs/FEED.md`](docs/FEED.md). A sample is in [`examples/feed.json`](examples/feed.json).
98
99
 
100
+ ## How it compares
101
+
102
+ Cubicle is one of several pixel offices for AI agents, all inspired by [Pixel Agents](https://github.com/pixel-agents-hq/pixel-agents). They optimise for different things:
103
+
104
+ | | Cubicle | [Pixel Agents](https://github.com/pixel-agents-hq/pixel-agents) | [Agent Pixels](https://github.com/gcampton/Agent-Pixels) | [agents-in-the-office](https://github.com/gukosowa/agents-in-the-office) |
105
+ | --- | --- | --- | --- | --- |
106
+ | Works with | Paperclip, Claude Code, any JSON feed | Claude Code | Paperclip | Claude Code, Gemini CLI |
107
+ | Runs as | Standalone page (`npx`, systemd, kiosk) | VS Code extension or `npx` browser app | Paperclip plugin | Standalone app |
108
+ | Setup | One command, nothing to build | Marketplace install; build from source to hack on it | Build against the Paperclip plugin SDK | See its README |
109
+ | Access to your agents | Read-only by design (GET allowlist) | Watches local Claude Code sessions | Inside Paperclip | Watches local sessions |
110
+ | Shows "needs you" | Yes: permission prompts, board questions | Yes: waiting for approval | Not listed | Yes: approval alerts |
111
+ | Office editor, art | Fixed room, programmer art | Layout editor, furniture and character assets, pets | 80+ characters, multi-room views | Tile map editor, sound packs |
112
+
113
+ **Pick Cubicle** if you want something you can trust to run next to production agents (it can only read), that starts in seconds with no build, or that shows Paperclip and Claude Code in one place. **Pick another one** if you want to design your office, richer art, or tighter editor integration.
114
+
99
115
  ## Run it as a service (Linux / WSL)
100
116
 
101
117
  systemd user units are in [`examples/systemd/`](examples/systemd/): `cubicle.service` (Paperclip, port 3200), `cubicle-claude.service` (Claude Code, port 3201), and an optional updater.
@@ -131,7 +147,7 @@ journalctl --user -u cubicle-update.service # what it did
131
147
 
132
148
  ## Contributing
133
149
 
134
- Issues and pull requests are welcome. Ideas: adapters for other agent runtimes (write a feed, send a PR with an example), a kiosk mode for wall displays, meeting-room animations when agents hand work to each other, per-agent sprite customisation, replay of a recorded day, and multi-source offices (Paperclip and Claude Code on one floor).
150
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md) for the few ground rules (zero dependencies, read-only). Look for issues labelled [good first issue](https://github.com/caglarutkuguler/cubicle/labels/good%20first%20issue). Ideas: adapters for other agent runtimes (write a feed, send a PR with an example), a kiosk mode for wall displays, meeting-room animations when agents hand work to each other, per-agent sprite customisation, replay of a recorded day, and multi-source offices (Paperclip and Claude Code on one floor).
135
151
 
136
152
  ## Releasing
137
153
 
package/bin/cubicle.js CHANGED
@@ -32,6 +32,11 @@ if (process.argv[2] === 'install-hooks') {
32
32
  return;
33
33
  }
34
34
 
35
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
36
+ console.log(require('../package.json').version);
37
+ process.exit(0);
38
+ }
39
+
35
40
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
36
41
  console.log(`Cubicle — a live pixel-art office for your AI agents
37
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caglarutkuguler/cubicle",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A live pixel-art office for your AI agents — Paperclip, Claude Code, or any JSON feed. Zero dependencies, read-only.",
5
5
  "bin": {
6
6
  "cubicle": "bin/cubicle.js"
package/public/index.html CHANGED
@@ -17,13 +17,21 @@
17
17
  select { background:var(--panel); color:var(--text); border:1px solid var(--line); border-radius:6px; padding:3px 6px; font:inherit; }
18
18
  #stage { position:relative; width:100%; aspect-ratio:352/208; border:2px solid var(--line); border-radius:8px; overflow:hidden; background:#2a2d3d; }
19
19
  canvas { position:absolute; inset:0; width:100%; height:100%; image-rendering:pixelated; }
20
- #labels { position:absolute; inset:0; pointer-events:none; }
20
+ #labels { position:absolute; inset:0; pointer-events:none; font-size:11px; }
21
21
  .lbl { position:absolute; transform:translate(-50%,-100%); display:flex; flex-direction:column; align-items:center; white-space:nowrap; }
22
- .bub { background:#fff; color:#1d1f2b; border-radius:9px; padding:2px 7px; font-size:11px; font-weight:600; margin-bottom:3px; box-shadow:0 1px 0 rgba(0,0,0,.3); max-width:210px; overflow:hidden; text-overflow:ellipsis; }
22
+ .bub { background:#fff; color:#1d1f2b; border-radius:.82em; padding:.18em .64em; font-size:1em; font-weight:600; margin-bottom:.27em; box-shadow:0 1px 0 rgba(0,0,0,.3); max-width:210px; overflow:hidden; text-overflow:ellipsis; }
23
23
  .bub.err { background:#ffdede; color:#8a1111; }
24
24
  .bub.ok { background:#dcffe2; color:#145c24; }
25
25
  .bub.wait { background:#fff1c9; color:#6b4d00; }
26
- .name { background:rgba(15,16,26,.78); color:#fff; font-size:11px; font-weight:600; padding:1px 6px; border-radius:5px; }
26
+ .name { background:rgba(15,16,26,.78); color:#fff; font-size:1em; font-weight:600; padding:.09em .55em; border-radius:.45em; }
27
+ /* ?kiosk: the office fills the screen, for a TV or a second monitor */
28
+ body.kiosk { overflow:hidden; cursor:none; }
29
+ body.kiosk .wrap { max-width:none; padding:0; height:100vh; display:flex; align-items:center; justify-content:center; }
30
+ body.kiosk header { position:fixed; top:0; left:0; right:0; z-index:2; margin:0; padding:1vh 1.4vw; background:linear-gradient(rgba(10,11,18,.8), rgba(10,11,18,0)); }
31
+ body.kiosk h1 { font-size:clamp(16px, 1.5vw, 34px); }
32
+ body.kiosk h1 small, body.kiosk #status { font-size:clamp(12px, 1.05vw, 24px); }
33
+ body.kiosk #stage { width:min(100vw, calc(100vh * 352 / 208)); border:0; border-radius:0; }
34
+ body.kiosk #cards, body.kiosk footer, body.kiosk #banner, body.kiosk #company { display:none !important; }
27
35
  #banner { display:none; margin-bottom:10px; padding:8px 12px; border-radius:8px; background:#2b2f47; border:1px solid var(--line); color:var(--muted); }
28
36
  #cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:8px; margin-top:12px; }
29
37
  .card { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:9px 11px; }
@@ -75,6 +83,7 @@
75
83
  noCompany: 'No company yet.', summary: (n, r) => `${n} agents · ${r} working`,
76
84
  running: 'working', idle: 'idle', error: 'error', paused: 'paused', waiting: 'needs you', noTask: 'no open task assigned',
77
85
  done: '✓ done', errBubble: '⚠ error', waitBubble: '✋ needs you',
86
+ asks: 'waiting for your answer on', needYou: (n) => `${n} need${n === 1 ? 's' : ''} you`,
78
87
  chatter: ['☕', 'coffee break', '…', 'hmm', "what's next?"],
79
88
  hint: 'Working agents sit at their desks and type; idle agents hang out in the lounge. Refreshes every 4 s.',
80
89
  demo: 'Demo mode — fake agents. Remove <code>?demo</code> from the URL to watch your own Paperclip company.',
@@ -87,6 +96,7 @@
87
96
  noCompany: 'Henüz şirket yok.', summary: (n, r) => `${n} ajan · ${r} çalışıyor`,
88
97
  running: 'çalışıyor', idle: 'boşta', error: 'hata', paused: 'duraklatıldı', waiting: 'sizi bekliyor', noTask: 'atanmış açık görev yok',
89
98
  done: '✓ bitirdi', errBubble: '⚠ hata', waitBubble: '✋ sizi bekliyor',
99
+ asks: 'cevabınızı bekliyor:', needYou: (n) => `${n} sizi bekliyor`,
90
100
  chatter: ['☕', 'kahve molası', '…', 'hmm', 'sıradaki iş?'],
91
101
  hint: 'Çalışan ajan masasına oturup yazar; boştaki ajan dinlenme alanında dolaşır. 4 saniyede bir yenilenir.',
92
102
  demo: "Demo modu: sahte ajanlar. Kendi Paperclip şirketinizi izlemek için adresten <code>?demo</code> kısmını kaldırın.",
@@ -94,12 +104,16 @@
94
104
  },
95
105
  };
96
106
  const params = new URLSearchParams(location.search);
97
- const lang = (params.get('lang') || navigator.language || 'en').slice(0, 2).toLowerCase() === 'tr' ? 'tr' : 'en';
107
+ // Any language with a block in STR is picked from ?lang=xx or the browser language; English otherwise.
108
+ const wantLang = (params.get('lang') || navigator.language || 'en').slice(0, 2).toLowerCase();
109
+ const lang = STR[wantLang] ? wantLang : 'en';
98
110
  const L = STR[lang];
99
111
  document.documentElement.lang = lang;
100
112
  // Becomes true in init() as well when the page is served without the Cubicle
101
113
  // proxy behind it (e.g. the hosted demo on GitHub Pages).
102
114
  let DEMO = params.has('demo');
115
+ const KIOSK = params.has('kiosk');
116
+ if (KIOSK) document.body.classList.add('kiosk');
103
117
 
104
118
  // ---------- setup ----------
105
119
  const T = 16, CW = 352, CH = 208;
@@ -244,7 +258,8 @@
244
258
  else b.style.display = 'none';
245
259
  }
246
260
  const running = agents.filter((a) => a.status === 'running').length;
247
- statusEl.textContent = `${L.summary(agents.length, running)} · ${new Date().toLocaleTimeString(lang)}`;
261
+ const needYou = [...sprites.values()].filter((x) => x.status === 'waiting' || x.ask).length;
262
+ statusEl.textContent = `${L.summary(agents.length, running)}${needYou ? ' · ' + L.needYou(needYou) : ''} · ${new Date().toLocaleTimeString(lang)}`;
248
263
  } catch (e) {
249
264
  const code = Number(e && e.message);
250
265
  statusEl.textContent = src === feed ? L.feedUnreachable
@@ -270,6 +285,18 @@
270
285
  return null;
271
286
  }
272
287
 
288
+ // Paperclip: an open issue whose review is waiting on the board (a pending question or
289
+ // interaction for a human) means the agent needs you, even though Paperclip reports it idle.
290
+ function askingBoard(agentId, issues) {
291
+ for (const i of issues) {
292
+ if (i.assigneeAgentId !== agentId || ['done', 'cancelled'].includes(i.status)) continue;
293
+ const paths = (i.reviewAttention && i.reviewAttention.paths) || [];
294
+ const p = paths.find((x) => /board|user|human/i.test(String(x && x.responder || '')));
295
+ if (p) return { issue: i, reason: String(p.label || '') };
296
+ }
297
+ return null;
298
+ }
299
+
273
300
  function sync(agents, issues) {
274
301
  const sorted = [...agents].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
275
302
  const seen = new Set();
@@ -300,6 +327,9 @@
300
327
  s.status = a.status;
301
328
  s.error = a.errorReason || '';
302
329
  s.task = a.task !== undefined ? a.task : currentTask(a.id, issues);
330
+ s.waitReason = '';
331
+ s.ask = a.task === undefined && a.status !== 'error' ? askingBoard(a.id, issues) : null;
332
+ if (s.ask && a.status === 'idle') { s.status = 'waiting'; s.task = s.ask.issue; s.waitReason = s.ask.reason; }
303
333
  if (s.prevStatus === 'running' && s.status === 'idle') setBubble(s, L.done, 'ok', 6);
304
334
  s.prevStatus = s.status;
305
335
  s.el.querySelector('.name').textContent = s.name;
@@ -324,7 +354,9 @@
324
354
  cardsEl.innerHTML = list.map((s) => {
325
355
  const t = s.task;
326
356
  const taskHtml = t ? `${taskLink(t)} ${esc(t.title)}` : `<span style="color:var(--muted)">${L.noTask}</span>`;
327
- const err = s.status === 'error' && s.error ? `<div class="st" style="color:#ffb3b8">${esc(s.error)}</div>` : '';
357
+ const err = s.status === 'error' && s.error ? `<div class="st" style="color:#ffb3b8">${esc(s.error)}</div>`
358
+ : s.status === 'waiting' && s.waitReason ? `<div class="st" style="color:#ffe08a">${esc(s.waitReason)}</div>`
359
+ : s.ask ? `<div class="st" style="color:#ffe08a">✋ ${L.asks} ${taskLink(s.ask.issue)} ${esc(s.ask.issue.title)}</div>` : '';
328
360
  return `<div class="card"><div class="top"><span class="sw" style="background:${s.shirt}"></span><b>${esc(s.name)}</b></div>
329
361
  <div class="role">${esc(s.role)}</div>
330
362
  <div class="st"><span class="pill p-${esc(s.status)}">${esc(L[s.status] || s.status)}</span>${taskHtml}</div>${err}</div>`;
@@ -472,14 +504,20 @@
472
504
 
473
505
  function placeLabels(now) {
474
506
  const k = cv.clientWidth / CW;
507
+ // Keep bubbles narrower than the gap between neighbouring desks so they never overlap.
508
+ const bubMax = Math.max(80, Math.floor(3.3 * T * k - 6)) + 'px';
509
+ // In kiosk mode names and bubbles grow with the screen (11px at the default ~3x scale).
510
+ if (KIOSK) labelsEl.style.fontSize = Math.max(11, Math.round(11 * k / 3)) + 'px';
475
511
  for (const s of sprites.values()) {
476
512
  s.el.style.left = (s.x * k) + 'px';
477
513
  s.el.style.top = ((s.y - 20) * k) + 'px';
478
514
  const b = s.el.querySelector('.bub');
515
+ b.style.maxWidth = bubMax;
479
516
  let text = null, cls = '';
480
517
  if (s.bubble && now < s.bubbleUntil) { text = s.bubble; cls = s.bubbleClass; }
481
518
  else if (s.status === 'error' && s.seated) { text = L.errBubble; cls = 'err'; }
482
519
  else if (s.status === 'waiting' && s.seated) { text = L.waitBubble; cls = 'wait'; }
520
+ else if (s.typing && s.ask && Math.floor(now / 3) % 2) { text = '✋ ' + (s.ask.issue.identifier || L.waiting); cls = 'wait'; }
483
521
  else if (s.typing && s.task) text = '⌨ ' + (s.task.identifier || s.task.title);
484
522
  else if (s.status === 'paused') text = 'zZ';
485
523
  if (text) { b.textContent = text; b.className = 'bub ' + cls; b.style.display = ''; }