shadok-ai 0.2.17 → 0.2.19

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
@@ -63,15 +63,17 @@ Enter to skip; you can add it later from the web UI).
63
63
  the chat can't express. With tmux there is also an **experimental real
64
64
  terminal** (xterm.js over the pane's byte stream) when a snapshot isn't enough.
65
65
  - **Diff panel** — what an agent actually changed, against its base.
66
- - **Tweak Shadok-AI** — a card at the bottom of the agents column. One click
67
- clones shadok-ai's own source into `~/.shadok-ai/self/shadok-ai`, starts an
68
- agent on it in its own worktree, and that agent delivers its change as a
66
+ - **Tweak Shadok-AI** — a card pinned at the bottom of the agents column. One
67
+ click clones shadok-ai's own source into `~/.shadok-ai/self/shadok-ai` and
68
+ starts an agent on it in its own worktree; that agent delivers its change as a
69
69
  **pull request** — a fork under your GitHub account, since you need no rights
70
70
  on the repo. Nothing to configure and no token to paste: the clone is
71
71
  anonymous, so you describe an idea, watch it work and read the diff first, and
72
72
  it only asks for GitHub (via `gh auth login`, device code relayed in the chat)
73
- when there is something worth pushing. Desktop only the agents column is
74
- hidden on phones.
73
+ when there is something worth pushing. It is **one channel, not a launcher**:
74
+ the card becomes that channel's tab, with the usual menu — mute, reload,
75
+ rename, change profile, mirror to Telegram, close — and closing it brings the
76
+ card back. Desktop only: the agents column is hidden on phones.
75
77
  - **Self-update** — polls npm and can update and reload itself in place.
76
78
 
77
79
  ### Telegram (optional)
@@ -122,6 +124,27 @@ the same-origin policy, so without that check any page you happen to visit could
122
124
  drive your agents. Behind a reverse proxy that rewrites `Host`, list the public
123
125
  origin in `SHADOK_ORIGINS`.
124
126
 
127
+ ### Behind TLS (nginx, Caddy…)
128
+
129
+ Serving the cockpit over HTTPS works, with one requirement: **the proxy must
130
+ forward the WebSocket upgrade**. Everything live — the channel list included —
131
+ travels over `/ws`, and the page itself is static HTML, so a proxy that drops
132
+ `Upgrade`/`Connection` produces a cockpit that loads perfectly and then never
133
+ connects. In nginx:
134
+
135
+ ```nginx
136
+ location / {
137
+ proxy_pass http://127.0.0.1:3789;
138
+ proxy_http_version 1.1;
139
+ proxy_set_header Upgrade $http_upgrade;
140
+ proxy_set_header Connection "upgrade";
141
+ proxy_set_header Host $host;
142
+ }
143
+ ```
144
+
145
+ The client picks `wss://` on its own when the page is HTTPS, so there is nothing
146
+ to configure on that side.
147
+
125
148
  ### SSH identity in Docker
126
149
 
127
150
  When shadok-ai runs **in a container** it gives itself an SSH key on first boot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
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
@@ -374,10 +374,27 @@
374
374
  bordure et les + des boutons fantômes, donc une carte pleine se lit comme
375
375
  une action et non comme une session. Épinglée en bas (margin-top:auto +
376
376
  sticky) pour rester atteignable quand la liste des agents défile. */
377
- #tweakCta {
377
+ /* L'emplacement réservé au canal « tweak », épinglé en bas de la colonne. Il
378
+ porte tantôt le CTA (aucune session), tantôt l'ONGLET du canal lui-même —
379
+ qui est un `.tab` ordinaire déplacé ici, donc états, menu contextuel et
380
+ rattachement Telegram marchent sans code en double. */
381
+ #tweakSlot {
378
382
  margin-top: auto;
379
383
  position: sticky;
380
384
  bottom: 0;
385
+ padding-top: 8px;
386
+ background: var(--bg-raised);
387
+ /* Sépare de la liste qui défile dessous, sans ajouter de filet. */
388
+ box-shadow: 0 -8px 10px -8px rgba(0, 0, 0, 0.6);
389
+ }
390
+ /* L'onglet du canal tweak garde la teinte de son emplacement, pour qu'on le
391
+ reconnaisse au même endroit qu'avant de l'avoir démarré. */
392
+ #tweakSlot .tab {
393
+ border: 1px solid var(--amber);
394
+ background: linear-gradient(var(--amber-soft), var(--amber-soft)), var(--bg-raised);
395
+ }
396
+ #tweakSlot .tab .name { color: var(--amber); }
397
+ #tweakCta {
381
398
  display: flex;
382
399
  flex-direction: column;
383
400
  gap: 2px;
@@ -385,13 +402,8 @@
385
402
  padding: 8px;
386
403
  border: 1px solid var(--amber);
387
404
  border-radius: 6px;
388
- /* Empilé sur le fond de la colonne, PAS `var(--amber-soft)` seul : la carte
389
- est sticky, et un fond translucide laisserait les onglets défiler au
390
- travers. */
405
+ width: 100%;
391
406
  background: linear-gradient(var(--amber-soft), var(--amber-soft)), var(--bg-raised);
392
- /* Ombre douce vers le haut : sépare la carte de la liste qui passe dessous,
393
- sans ajouter de filet qui doublerait la bordure. */
394
- box-shadow: 0 -8px 10px -8px rgba(0, 0, 0, 0.6);
395
407
  }
396
408
  #tweakCta .tweak-title {
397
409
  font-family: var(--mono);
@@ -404,10 +416,7 @@
404
416
  /* Aucune animation permanente : un élément qui clignote ici entrerait en
405
417
  concurrence avec les vrais signaux (onglet en attente, jauges) et
406
418
  deviendrait du bruit. */
407
- #tweakCta:hover {
408
- border-color: var(--amber);
409
- box-shadow: 0 -8px 10px -8px rgba(0, 0, 0, 0.6), 0 0 12px rgba(240, 168, 72, 0.18);
410
- }
419
+ #tweakCta:hover { border-color: var(--amber); box-shadow: 0 0 12px rgba(240, 168, 72, 0.18); }
411
420
  #tweakCta:hover .tweak-arrow { transform: translateX(2px); }
412
421
  #tweakCta.busy { opacity: 0.7; cursor: default; }
413
422
  #tweakCta.failed { border-color: var(--err); background: transparent; }
@@ -1411,11 +1420,13 @@
1411
1420
  <div id="groups"></div>
1412
1421
  <button id="newTab" title="New agent">+ new agent</button>
1413
1422
  <button id="newGroup" title="New agent group">+ new group</button>
1414
- <button id="tweakCta" type="button"
1415
- title="Change shadok-ai itself — the agent works on a branch and opens a pull request">
1416
- <span class="tweak-title">🛠 Tweak Shadok-AI <span class="tweak-arrow">›</span></span>
1417
- <span class="tweak-sub">change the cockpit itself</span>
1418
- </button>
1423
+ <div id="tweakSlot">
1424
+ <button id="tweakCta" type="button"
1425
+ title="Change shadok-ai itself the agent works on a branch and opens a pull request">
1426
+ <span class="tweak-title">🛠 Tweak Shadok-AI <span class="tweak-arrow">›</span></span>
1427
+ <span class="tweak-sub">change the cockpit itself</span>
1428
+ </button>
1429
+ </div>
1419
1430
  </nav>
1420
1431
 
1421
1432
  <section class="chat-pane">
@@ -2209,7 +2220,9 @@
2209
2220
  e.dataTransfer.dropEffect = "move";
2210
2221
  if (dragTabEl) {
2211
2222
  const overTab = e.target.closest(".tab");
2212
- if (overTab && overTab !== dragTabEl) {
2223
+ // L'emplacement du bas n'accueille QUE le canal tweak : sans ce garde, un
2224
+ // onglet traîné au-dessus s'y insérerait et se retrouverait épinglé là.
2225
+ if (overTab && overTab !== dragTabEl && !overTab.classList.contains("tweak")) {
2213
2226
  const r = overTab.getBoundingClientRect();
2214
2227
  const after = e.clientY > r.top + r.height / 2;
2215
2228
  overTab.parentNode.insertBefore(dragTabEl, after ? overTab.nextSibling : overTab);
@@ -2330,6 +2343,9 @@
2330
2343
  if (tab.reconnectTimer) clearTimeout(tab.reconnectTimer);
2331
2344
  if (tab.ws && tab.ws.readyState === WebSocket.OPEN) tab.ws.close();
2332
2345
  dropDraft(tab);
2346
+ // Le canal tweak fermé, son emplacement redevient le CTA — sinon le bas de
2347
+ // la colonne resterait vide et la fonctionnalité disparaîtrait sans retour.
2348
+ if (tab.isTweak) document.getElementById("tweakCta").hidden = false;
2333
2349
  tab.btnEl.remove();
2334
2350
  tab.transcriptEl.remove();
2335
2351
  tabs = tabs.filter((t) => t !== tab);
@@ -2383,6 +2399,9 @@
2383
2399
  nt.draft = savedDrafts[c.sessionId] || "";
2384
2400
  if (c.branch) nt.branch = c.branch;
2385
2401
  nt.profile = c.profile || null;
2402
+ // Un canal tweak né ailleurs (autre appareil, Telegram) reprend sa place
2403
+ // en bas plutôt que d'apparaître comme un agent ordinaire de la liste.
2404
+ if (nt.profile === "Shadok-Tweak" && !tweakTab()) markTweakTab(nt);
2386
2405
  setCrons(nt, c.crons);
2387
2406
  if (c.muted) setMuted(nt, true, false);
2388
2407
  if (typeof c.mirror === "boolean" ? c.mirror : !!c.telegram) setMirror(nt, true, false);
@@ -3139,7 +3158,12 @@
3139
3158
  * of stranding the tab on "link lost". */
3140
3159
  function openLink(t) {
3141
3160
  setTabState(t, "connecting", t.reconnectAttempts ? "reconnecting…" : "connecting…");
3142
- const ws = new WebSocket(`ws://${location.host}/ws`);
3161
+ // The scheme MUST follow the page's. On an HTTPS page — the cockpit behind an
3162
+ // nginx/TLS reverse proxy — a `ws://` socket is blocked as mixed content, and
3163
+ // the failure is invisible: the page is static HTML so it paints normally,
3164
+ // while every channel stays stuck "connecting…" forever. Never hardcode `ws://`.
3165
+ const scheme = location.protocol === "https:" ? "wss:" : "ws:";
3166
+ const ws = new WebSocket(`${scheme}//${location.host}/ws`);
3143
3167
  t.ws = ws;
3144
3168
  ws.addEventListener("open", () => {
3145
3169
  t.reconnectAttempts = 0;
@@ -4598,6 +4622,13 @@
4598
4622
  // Un clic démarre un agent sur le code source de shadok-ai lui-même : le
4599
4623
  // dossier, le worktree et le rôle sont tous déjà décidés, donc un formulaire
4600
4624
  // ne ferait que demander d'approuver des choix qu'on ne peut pas juger.
4625
+ //
4626
+ // Le canal tweak est UNIQUE et vit dans son emplacement en bas de colonne : le
4627
+ // CTA n'est visible que tant qu'il n'existe pas. Une fois lancé, son onglet
4628
+ // prend la place du CTA — un `.tab` ordinaire simplement déplacé là, donc il
4629
+ // hérite sans code en double des états, du menu contextuel, du rattachement
4630
+ // Telegram, du renommage et de la fermeture.
4631
+ const tweakSlot = $("tweakSlot");
4601
4632
  const tweakCta = $("tweakCta");
4602
4633
  const tweakTitle = tweakCta.querySelector(".tweak-title");
4603
4634
  const tweakSub = tweakCta.querySelector(".tweak-sub");
@@ -4619,8 +4650,26 @@
4619
4650
  }
4620
4651
  }
4621
4652
 
4653
+ /** L'onglet du canal tweak, s'il existe. */
4654
+ function tweakTab() { return tabs.find((t) => t.isTweak); }
4655
+
4656
+ /** Déplace l'onglet dans l'emplacement du bas et masque le CTA : à partir de
4657
+ * là, c'est LUI qu'on voit et qu'on clique. `appendChild` déplace le nœud,
4658
+ * donc l'onglet quitte la liste de gauche sans être recréé. */
4659
+ function markTweakTab(t) {
4660
+ t.isTweak = true;
4661
+ t.btnEl.classList.add("tweak");
4662
+ t.btnEl.draggable = false; // il n'a pas à finir dans un groupe
4663
+ tweakSlot.appendChild(t.btnEl);
4664
+ tweakCta.hidden = true;
4665
+ }
4666
+
4622
4667
  tweakCta.addEventListener("click", async () => {
4623
4668
  if (tweakBusy) return;
4669
+ // Ceinture : le CTA est masqué dès qu'un canal tweak existe, mais s'il
4670
+ // réapparaissait on active l'existant plutôt que d'en ouvrir un second.
4671
+ const already = tweakTab();
4672
+ if (already) return activate(already);
4624
4673
  tweakBusy = true;
4625
4674
  setTweakCta("busy");
4626
4675
  let data;
@@ -4637,6 +4686,7 @@
4637
4686
  t.customName = true; // launchTab ne nomme qu'un onglet qui n'a pas de nom
4638
4687
  t.nameEl.textContent = "tweak";
4639
4688
  t.profile = "Shadok-Tweak";
4689
+ markTweakTab(t);
4640
4690
  launchTab(t, { cwd: data.cwd, worktree: true, profile: "Shadok-Tweak" });
4641
4691
  });
4642
4692
  newGroupBtn.addEventListener("click", () => {
@@ -4775,6 +4825,8 @@
4775
4825
  t.draft = savedDrafts[s.sessionId] || "";
4776
4826
  if (s.branch) t.branch = s.branch;
4777
4827
  t.profile = s.profile || null;
4828
+ // Au rechargement, le canal tweak retrouve son emplacement du bas.
4829
+ if (t.profile === "Shadok-Tweak" && !tweakTab()) markTweakTab(t);
4778
4830
  setCrons(t, s.crons);
4779
4831
  if (s.muted) setMuted(t, true, false); // restoring: don't push back
4780
4832
  // The mirror TOO, otherwise the first persist pushes `mirror:false`