shadok-ai 0.2.30 → 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.30",
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
@@ -1308,10 +1308,6 @@
1308
1308
  <span class="label">Name</span>
1309
1309
  <input type="text" id="nameInput" placeholder="agent" autocomplete="off" spellcheck="false">
1310
1310
  </div>
1311
- <div class="field">
1312
- <span class="label">Working directory</span>
1313
- <input type="text" id="cwdInput" placeholder="/path/to/project">
1314
- </div>
1315
1311
  <label class="check" id="worktreeField">
1316
1312
  <input type="checkbox" id="worktreeInput" checked>
1317
1313
  <span>Isolate in a git worktree <span class="check-hint">— the agent edits a fresh branch; review &amp; merge from the Diff panel. Auto-removed on close if unused, kept if it has changes. (new sessions only)</span></span>
@@ -1321,7 +1317,11 @@
1321
1317
  <span>Mirror to Telegram <span class="check-hint">— gives this agent its own topic in the board group, so you can follow it and reply from your phone. Off by default; switchable later from the channel menu.</span></span>
1322
1318
  </label>
1323
1319
  <details class="fold" id="advancedField">
1324
- <summary>Advanced — resume an existing session</summary>
1320
+ <summary>Advanced — working directory, resume an existing session</summary>
1321
+ <div class="field">
1322
+ <span class="label">Working directory <span class="check-hint">— defaults to where shadok-ai runs; point at another repo for a multi-project agent.</span></span>
1323
+ <input type="text" id="cwdInput" placeholder="/path/to/project">
1324
+ </div>
1325
1325
  <div class="field">
1326
1326
  <div class="radio-row">
1327
1327
  <label><input type="radio" name="mode" value="new" checked> new session</label>
@@ -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) {
@@ -3323,10 +3351,16 @@
3323
3351
  * à la place de la conversation qu'on regardait.
3324
3352
  */
3325
3353
  function openSetup() {
3326
- // Ne jamais écraser ce qui est déjà tapé (la popin peut se rouvrir).
3327
- if (!$("cwdInput").value) {
3328
- $("cwdInput").value = (active && active.cwd) || localStorage.getItem("cp.cwd") || serverCwd || "";
3329
- }
3354
+ // Défauts PRÉVISIBLES à chaque ouverture. Le navigateur restaure l'état des
3355
+ // champs de formulaire (un mode "continue"/"resume" choisi une fois, une case
3356
+ // worktree décochée, un cwd de worktree) c'est ce qui faisait retomber
3357
+ // « créer un agent » dans le répertoire de base, en silence. On réaffirme le
3358
+ // défaut : session NEUVE, worktree isolé, depuis la base.
3359
+ const newRadio = document.querySelector('input[name="mode"][value="new"]');
3360
+ newRadio.checked = true;
3361
+ newRadio.dispatchEvent(new Event("change")); // réactive worktree/profil, cache le resume
3362
+ $("worktreeInput").checked = true;
3363
+ $("cwdInput").value = serverCwd || "";
3330
3364
  const last = localStorage.getItem("cp.session");
3331
3365
  if (last && !$("resumeInput").value) $("resumeInput").value = last;
3332
3366
  $("startBtn").disabled = false;
@@ -3341,7 +3375,9 @@
3341
3375
  refreshRecoverList();
3342
3376
  refreshLiveList();
3343
3377
  $("setupOverlay").hidden = false;
3344
- $("cwdInput").focus();
3378
+ // Le cwd est désormais dans le fold « Advanced » (replié) : on focus le nom,
3379
+ // premier champ du flux principal.
3380
+ $("nameInput").focus();
3345
3381
  }
3346
3382
  function closeSetup() { $("setupOverlay").hidden = true; }
3347
3383
 
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
  }