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 +1 -1
- package/public/index.html +70 -3
- package/public/live-text.js +39 -0
package/package.json
CHANGED
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(--
|
|
221
|
-
|
|
222
|
-
background:
|
|
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
|
+
}
|