shadok-ai 0.2.31 → 0.2.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "main": "dist/session.js",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.ts .claude/skills/shadok-ai-agents/test/*.test.mjs",
package/public/index.html CHANGED
@@ -2495,11 +2495,22 @@
2495
2495
  pip + `</svg>`;
2496
2496
  return "data:image/svg+xml," + encodeURIComponent(svg);
2497
2497
  }
2498
+ /** Favicon « en train de bosser » : la marque + un arc qui tourne (angle animé). */
2499
+ function faviconWorkingSVG(angle) {
2500
+ const svg =
2501
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">` +
2502
+ `<rect width="32" height="32" rx="7" fill="#14161d"/>` +
2503
+ `<text x="15" y="23" font-family="monospace" font-size="20" fill="#f0a848" text-anchor="middle">›</text>` +
2504
+ `<g transform="rotate(${angle} 24 8)"><circle cx="24" cy="8" r="6" fill="none" stroke="#f0a848" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="26 40"/></g>` +
2505
+ `</svg>`;
2506
+ return "data:image/svg+xml," + encodeURIComponent(svg);
2507
+ }
2498
2508
  /** What each channel is asking for, as the pure module wants it. */
2499
2509
  function attentionInput() {
2500
2510
  return tabs.map((t) => ({
2501
2511
  mood: t.btnEl.classList.contains("needs-answer") ? "needs-answer"
2502
- : t.btnEl.classList.contains("unread") ? "unread" : null,
2512
+ : t.btnEl.classList.contains("unread") ? "unread"
2513
+ : t.btnEl.classList.contains("working") ? "working" : null,
2503
2514
  muted: !!t.muted,
2504
2515
  }));
2505
2516
  }
@@ -2508,10 +2519,14 @@
2508
2519
  // costs no timer at all.
2509
2520
  let blinkTimer = null;
2510
2521
  let blinkPhase = 0;
2522
+ let workTimer = null; // anime le favicon « working » (spinner)
2523
+ let workAngle = 0;
2524
+ function stopWork() { if (workTimer) { clearInterval(workTimer); workTimer = null; } }
2511
2525
  function paint() {
2512
2526
  // The ESM bridge lands after the document is parsed (gotcha #10): until it
2513
2527
  // does, keep the plain badge rather than throwing on the first paint.
2514
2528
  if (!window.notifyState) {
2529
+ stopWork();
2515
2530
  favicon.href = faviconSVG(null);
2516
2531
  document.title = titleBase;
2517
2532
  return;
@@ -2524,8 +2539,21 @@
2524
2539
  away: document.hidden || !document.hasFocus(),
2525
2540
  phase: blinkPhase,
2526
2541
  });
2527
- favicon.href = faviconSVG(s.color);
2528
2542
  document.title = s.badge + titleBase;
2543
+ // « working » = favicon animé (spinner). Le module ne renvoie ce mode que si
2544
+ // RIEN de plus prioritaire (bloqué / non-lu) n'attend — la priorité est donc
2545
+ // gérée en amont : un agent qui attend gagne toujours sur un agent qui bosse.
2546
+ if (s.mode === "working") {
2547
+ if (blinkTimer) { clearInterval(blinkTimer); blinkTimer = null; blinkPhase = 0; }
2548
+ if (!workTimer) {
2549
+ const tick = () => { workAngle = (workAngle + 30) % 360; favicon.href = faviconWorkingSVG(workAngle); };
2550
+ tick();
2551
+ workTimer = setInterval(tick, 120);
2552
+ }
2553
+ return;
2554
+ }
2555
+ stopWork();
2556
+ favicon.href = faviconSVG(s.color);
2529
2557
  if (s.blink && !blinkTimer) {
2530
2558
  blinkTimer = setInterval(() => { blinkPhase ^= 1; paint(); }, window.BLINK_MS || 900);
2531
2559
  } else if (!s.blink && blinkTimer) {
package/public/notify.js CHANGED
@@ -46,19 +46,27 @@ export function notifyState(channels, view) {
46
46
 
47
47
  let blocked = false;
48
48
  let unread = false;
49
+ let working = false;
49
50
  for (const c of channels || []) {
50
51
  if (!c || c.muted) continue; // un canal muté ne remonte aucun signal global
51
52
  if (c.mood === "needs-answer") blocked = true;
52
53
  else if (c.mood === "unread") unread = true;
54
+ else if (c.mood === "working") working = true;
53
55
  }
54
56
 
55
- if (!blocked && !unread) return { color: null, badge: "", blink: false };
56
-
57
- // Seul un agent bloqué justifie de clignoter : une réponse non lue signale
58
- // qu'il s'est passé quelque chose, pas qu'on attend après toi.
59
- const blink = blocked && away;
60
- if (!blink) return { color: blocked ? RED : AMBER, badge: "● ", blink: false };
61
- return phase
62
- ? { color: RED_DIM, badge: "◉ ", blink: true }
63
- : { color: RED, badge: "● ", blink: true };
57
+ // Priorité : « il y a quelque chose à faire » (bloqué) prime sur « quelque
58
+ // chose est arrivé » (non-lu), qui prime sur « ça bosse ». `mode` dit à
59
+ // l'appelant quel favicon dessiner (le mode "working" est ANIMÉ côté DOM).
60
+ if (blocked) {
61
+ // Seul un agent bloqué justifie de clignoter, et seulement si tu es ailleurs.
62
+ if (!away) return { color: RED, badge: "● ", blink: false, mode: "blocked" };
63
+ return phase
64
+ ? { color: RED_DIM, badge: "◉ ", blink: true, mode: "blocked" }
65
+ : { color: RED, badge: "● ", blink: true, mode: "blocked" };
66
+ }
67
+ if (unread) return { color: AMBER, badge: "● ", blink: false, mode: "unread" };
68
+ // « working » n'a ni couleur de pip ni badge : le favicon est animé (spinner)
69
+ // par l'appelant tant que le mode vaut "working".
70
+ if (working) return { color: null, badge: "", blink: false, mode: "working" };
71
+ return { color: null, badge: "", blink: false, mode: null };
64
72
  }