shadok-ai 0.2.7 → 0.2.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +58 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shadok-ai",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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
@@ -869,6 +869,25 @@
869
869
  #chanBar .gauge { flex-direction: row; align-items: baseline; gap: 4px; }
870
870
  #chanBar .gauge .label { font-size: 10px; }
871
871
  #chanBar #cwdView { display: inline-block; max-width: 200px; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
872
+ /* Sélecteur de canal : masqué sur desktop (la sidebar est la navigation). */
873
+ #chanSelect {
874
+ display: none;
875
+ font-family: var(--mono); font-size: 13px;
876
+ background: var(--bg-inset); color: var(--text);
877
+ border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px;
878
+ max-width: 55vw;
879
+ }
880
+ /* ── Mobile : sidebar cachée, navigation par le sélecteur du header canal ── */
881
+ @media (max-width: 640px) {
882
+ #tabbar { display: none; }
883
+ #chanSelect { display: inline-block; }
884
+ #chanBar #chanBarName { display: none; }
885
+ /* Bandeau canal condensé : on garde Status + Session, on cache le reste. */
886
+ #chanBar #dirGauge, #chanBar #branchGauge { display: none !important; }
887
+ #chanBar .gauge .label { display: none; } /* valeurs seules, plus compact */
888
+ #chanBar { gap: 6px; padding: 6px 10px; }
889
+ header { flex-wrap: wrap; row-gap: 4px; }
890
+ }
872
891
  #xtermHost { flex: 1; min-height: 0; overflow: auto; padding: 8px 10px; }
873
892
  #xtermHost .xterm { height: 100%; }
874
893
  /* Aucun agent ouvert. Le panneau central n'affiche plus un formulaire à demi
@@ -1320,11 +1339,13 @@
1320
1339
  séparé, donc ses actions n'ont pas de raison d'apparaître avec lui. -->
1321
1340
  <div id="chanBar" hidden>
1322
1341
  <span class="label" id="chanBarName"></span>
1342
+ <!-- Mobile: sidebar cachée → ce sélecteur change de canal / en crée un. -->
1343
+ <select id="chanSelect" title="Canal"></select>
1323
1344
  <div class="gauge">
1324
1345
  <span class="label">Status</span>
1325
1346
  <span class="value"><span id="led" class="led"></span><span id="state">—</span></span>
1326
1347
  </div>
1327
- <div class="gauge">
1348
+ <div class="gauge" id="dirGauge">
1328
1349
  <span class="label">Directory</span>
1329
1350
  <span class="value" id="cwdView">—</span>
1330
1351
  </div>
@@ -1646,6 +1667,7 @@
1646
1667
  tab.nameEl.textContent = v;
1647
1668
  saveName(tab, v);
1648
1669
  persistChannels();
1670
+ rebuildChanSelect(); // le nom change → mettre à jour le sélecteur mobile
1649
1671
  });
1650
1672
  }
1651
1673
 
@@ -2329,7 +2351,33 @@
2329
2351
  * un formulaire à la place. Tout ce qui décrit un canal retombe alors sur « — »
2330
2352
  * et le panneau central montre l'invitation.
2331
2353
  */
2354
+ /** (Re)construit le sélecteur mobile : une option par canal + « + New agent ». */
2355
+ function rebuildChanSelect() {
2356
+ const sel = $("chanSelect");
2357
+ if (!sel) return;
2358
+ const cur = active ? String(active.id) : "";
2359
+ sel.innerHTML = "";
2360
+ for (const t of tabs) {
2361
+ const o = document.createElement("option");
2362
+ o.value = String(t.id);
2363
+ o.textContent = t.nameEl ? t.nameEl.textContent : "agent " + t.id;
2364
+ sel.appendChild(o);
2365
+ }
2366
+ const nw = document.createElement("option");
2367
+ nw.value = "__new__";
2368
+ nw.textContent = "+ New agent";
2369
+ sel.appendChild(nw);
2370
+ sel.value = cur;
2371
+ }
2372
+ $("chanSelect").addEventListener("change", () => {
2373
+ const v = $("chanSelect").value;
2374
+ if (v === "__new__") { createTab(); return; }
2375
+ const t = tabs.find((x) => String(x.id) === v);
2376
+ if (t) activate(t);
2377
+ });
2378
+
2332
2379
  function refreshChrome() {
2380
+ rebuildChanSelect(); // garde le sélecteur mobile en phase (liste + actif)
2333
2381
  const t = active;
2334
2382
  if (!t) {
2335
2383
  $("led").className = "led ";
@@ -3596,6 +3644,14 @@
3596
3644
  for (let i = 0; i < bin.length; i++) a[i] = bin.charCodeAt(i);
3597
3645
  return a;
3598
3646
  }
3647
+ // Keystroke → base64, UTF-8 first. btoa() alone treats the string as Latin1,
3648
+ // so "é" (U+00E9) went out as the single byte 0xE9 instead of its UTF-8 pair
3649
+ // 0xC3 0xA9 → mojibake in the pane; anything above U+00FF (emoji…) threw.
3650
+ function strToB64Utf8(s) {
3651
+ let bin = "";
3652
+ for (const b of new TextEncoder().encode(s)) bin += String.fromCharCode(b);
3653
+ return btoa(bin);
3654
+ }
3599
3655
  function updateTermBtn() {
3600
3656
  const on = !!(active && active.termOn);
3601
3657
  const b = $("chanTerm");
@@ -3621,7 +3677,7 @@
3621
3677
  term.open($("xtermHost"));
3622
3678
  term.onData((d) => {
3623
3679
  if (term && termSession && termSession.ws)
3624
- termSession.ws.send(JSON.stringify({ type: "term-input", data: btoa(d) }));
3680
+ termSession.ws.send(JSON.stringify({ type: "term-input", data: strToB64Utf8(d) }));
3625
3681
  });
3626
3682
  }
3627
3683
  if (termSession !== active) {