shadok-ai 0.1.136 → 0.1.138

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.1.136",
3
+ "version": "0.1.138",
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
@@ -216,11 +216,19 @@
216
216
  .tab .ctxfill.warn { background: var(--amber); }
217
217
  .tab .ctxfill.crit { background: var(--err); }
218
218
  .tab:hover { color: var(--text); background: var(--bg-inset); }
219
+ /* Onglet actif (sélectionné). Marqué fort ET robuste : le fond amber et le
220
+ bord gauche sont écrasés par les styles d'état (.working/.needs-answer/
221
+ .unread, définis plus bas, même spécificité → l'ordre source gagne), donc
222
+ l'accent de sélection passe surtout par des canaux qu'AUCUN état ne touche —
223
+ box-shadow (barre gauche vive) et font-weight — pour rester visible quel que
224
+ soit l'état de l'onglet. */
219
225
  .tab.active {
220
- color: var(--amber);
221
- border-left-color: var(--amber);
222
- background: var(--amber-soft);
226
+ color: var(--text);
227
+ font-weight: 600;
228
+ background: rgba(240, 168, 72, 0.24);
229
+ border-left-color: var(--phosphor);
223
230
  border-radius: 0 4px 4px 0;
231
+ box-shadow: inset 3px 0 0 var(--phosphor);
224
232
  }
225
233
  .tab .name {
226
234
  flex: 1;
@@ -449,6 +457,18 @@
449
457
  }
450
458
  /* History replayed from the .jsonl: raw markdown → HTML rendering. */
451
459
  .turn.hist { opacity: 0.8; }
460
+ /* Aperçu live du bloc en cours, extrait de l'écran TUI (brut, non-markdown).
461
+ Grisé + pré-formaté pour signaler que c'est provisoire ; remplacé par le
462
+ bloc .jsonl (stream-text) dès qu'il arrive. */
463
+ .turn.claude.live-preview .bubble {
464
+ opacity: 0.55;
465
+ white-space: pre-wrap;
466
+ font-family: var(--mono);
467
+ }
468
+ .turn.claude.live-preview .bubble::after {
469
+ content: "▍";
470
+ animation: breathe 1s step-start infinite;
471
+ }
452
472
  .bubble.md {
453
473
  white-space: normal;
454
474
  word-break: break-word;
@@ -1031,6 +1051,10 @@
1031
1051
  </main>
1032
1052
 
1033
1053
  <script src="/vendor/marked.js"></script>
1054
+ <script type="module">
1055
+ import { extractLiveText } from "/live-text.js";
1056
+ window.extractLiveText = extractLiveText;
1057
+ </script>
1034
1058
  <script>
1035
1059
  const $ = (id) => document.getElementById(id);
1036
1060
  const tabbar = $("tabbar"), newTabBtn = $("newTab"), newGroupBtn = $("newGroup");
@@ -1669,6 +1693,40 @@
1669
1693
  return bubble;
1670
1694
  }
1671
1695
 
1696
+ /* ── Aperçu live du texte en cours ───────────────────────
1697
+ Le .jsonl n'écrit un bloc de texte que TERMINÉ, donc rien ne s'affiche
1698
+ pendant sa génération. On comble avec le dernier bloc lu sur l'écran TUI
1699
+ (window.extractLiveText), dans une bulle provisoire, remplacée par le rendu
1700
+ markdown autoritatif dès que `stream-text` livre le bloc complet.
1701
+
1702
+ Déduplication : après un `stream-text`, l'écran continue de montrer le même
1703
+ bloc `⏺` ; on mémorise sa forme-écran (`lastFinalizedScreenText`) pour ne
1704
+ pas recréer un provisoire qui doublonnerait la bulle déjà finalisée. */
1705
+ function updateLivePreview(tab, screen) {
1706
+ if (!window.extractLiveText) return;
1707
+ const txt = extractLiveText(screen);
1708
+ if (!txt) return; // trou d'extraction : garder l'existant tel quel
1709
+ if (txt === tab.lastFinalizedScreenText) { clearLivePreview(tab); return; }
1710
+ if (!tab.livePreviewEl) {
1711
+ const turn = document.createElement("div");
1712
+ turn.className = "turn claude live-preview";
1713
+ const bubble = document.createElement("div");
1714
+ bubble.className = "bubble";
1715
+ turn.appendChild(bubble);
1716
+ tab.transcriptEl.appendChild(turn);
1717
+ tab.livePreviewEl = turn;
1718
+ tab.livePreviewBubble = bubble;
1719
+ }
1720
+ tab.livePreviewBubble.textContent = txt;
1721
+ if (tab === active) tab.transcriptEl.scrollTop = tab.transcriptEl.scrollHeight;
1722
+ }
1723
+
1724
+ function clearLivePreview(tab) {
1725
+ if (tab.livePreviewEl) tab.livePreviewEl.remove();
1726
+ tab.livePreviewEl = null;
1727
+ tab.livePreviewBubble = null;
1728
+ }
1729
+
1672
1730
  /* ── Activity log ────────────────────────────────────────
1673
1731
  Tool traffic is grouped instead of stacked. A *block* is a run of tool
1674
1732
  events between two pieces of assistant text; inside it, consecutive calls
@@ -2286,6 +2344,8 @@
2286
2344
  if (!t.turnStart) startTurnTimer(t, msg.startedAt);
2287
2345
  setTabMood(t, "working");
2288
2346
  setTabState(t, "busy", "responding…");
2347
+ clearLivePreview(t);
2348
+ t.lastFinalizedScreenText = null;
2289
2349
  break;
2290
2350
  case "prompt-echo":
2291
2351
  // Prompt sent by another tab/interface — or by the server itself
@@ -2307,6 +2367,10 @@
2307
2367
  // one, so the transcript keeps the real text/tools interleaving.
2308
2368
  retireChoices(t);
2309
2369
  closeActivity(t);
2370
+ // Mémoriser la forme-écran du bloc qu'on finalise, pour empêcher un
2371
+ // provisoire doublon tant que l'écran le montre encore, puis remplacer.
2372
+ if (window.extractLiveText) t.lastFinalizedScreenText = extractLiveText(t.screenText);
2373
+ clearLivePreview(t);
2310
2374
  addTurn(t, "claude", "claude", msg.text, "live");
2311
2375
  break;
2312
2376
  case "stream-tool":
@@ -2319,6 +2383,7 @@
2319
2383
  break;
2320
2384
  case "turn-done":
2321
2385
  retireChoices(t);
2386
+ clearLivePreview(t);
2322
2387
  // Activity recedes now the turn is over — errors excepted.
2323
2388
  sealTurn(t);
2324
2389
  if (msg.sessionId && msg.sessionId !== t.sessionId) {
@@ -2333,6 +2398,7 @@
2333
2398
  case "dialog":
2334
2399
  // Re-rendered in place when a dialog is already shown (multi toggle).
2335
2400
  closeActivity(t);
2401
+ clearLivePreview(t);
2336
2402
  renderDialog(t, msg);
2337
2403
  stopTurnTimer(t);
2338
2404
  setTabMood(t, "needs-answer");
@@ -2341,6 +2407,7 @@
2341
2407
  case "screen":
2342
2408
  t.screenText = msg.text;
2343
2409
  if (t === active) screenEl.textContent = msg.text;
2410
+ if (msg.working) updateLivePreview(t, msg.text);
2344
2411
  break;
2345
2412
  case "gone":
2346
2413
  // Session's directory is gone (e.g. a removed worktree): stop trying
@@ -0,0 +1,39 @@
1
+ // Extraction best-effort du bloc de texte assistant en cours, depuis l'écran
2
+ // TUI (@xterm/headless) — voir docs/superpowers/specs/2026-07-28-live-text-preview-design.md.
3
+ //
4
+ // Chargé tel quel par le navigateur (ESM) et importé par les tests node/tsx.
5
+ // Le transcript .jsonl n'écrit un bloc de texte que TERMINÉ ; l'écran, lui, le
6
+ // montre au fil de la frappe → seule source token-granulaire.
7
+ //
8
+ // Un bloc de texte assistant = une ligne "⏺ <prose>" (U+23FA + espace) en
9
+ // colonne 0, suivie de continuations indentées de 2 espaces. Un tool_use rend
10
+ // "⏺ Nom(args)" ; un résultat d'outil rend " ⎿ …".
11
+
12
+ const MARKER = "⏺ "; // "⏺ "
13
+
14
+ /** Le dernier bloc de texte assistant visible, dé-wrappé ; "" sinon. */
15
+ export function extractLiveText(screen) {
16
+ if (typeof screen !== "string" || !screen) return "";
17
+ const lines = screen.split("\n");
18
+
19
+ // Trouver le dernier marqueur de bloc "⏺ " en colonne 0.
20
+ let start = -1;
21
+ for (let i = lines.length - 1; i >= 0; i--) {
22
+ if (lines[i].startsWith(MARKER)) { start = i; break; }
23
+ }
24
+ if (start < 0) return "";
25
+
26
+ const head = lines[start].slice(MARKER.length).trim();
27
+ // tool_use : "⏺ Nom(...)" — identifiant collé à une parenthèse.
28
+ if (/^[\w.-]+\(/.test(head)) return "";
29
+
30
+ const parts = [head];
31
+ for (let i = start + 1; i < lines.length; i++) {
32
+ const l = lines[i];
33
+ if (!l.startsWith(" ") || l.trim() === "") break; // spinner/blanc/séparateur/fin
34
+ const t = l.trim();
35
+ if (t.startsWith("⎿") || /^Ran\b/.test(t)) break; // sous-ligne d'outil
36
+ parts.push(t);
37
+ }
38
+ return parts.join(" ");
39
+ }