shadok-ai 0.6.3 → 0.6.5

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.6.3",
3
+ "version": "0.6.5",
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 context/*/test/*.test.mjs",
package/public/index.html CHANGED
@@ -1902,7 +1902,7 @@
1902
1902
  request (src/csp.ts). A block without it does not run — which is what makes
1903
1903
  HTML an agent might write into the transcript harmless. -->
1904
1904
  <script type="module" nonce="__CSP_NONCE__">
1905
- import { extractLiveText } from "/live-text.js";
1905
+ import { extractLiveText, livePreviewDecision } from "/live-text.js";
1906
1906
  import {
1907
1907
  profileBlurb,
1908
1908
  profileBadges,
@@ -1918,6 +1918,7 @@
1918
1918
  window.unionRect = unionRect;
1919
1919
  window.visibleSteps = visibleSteps;
1920
1920
  window.extractLiveText = extractLiveText;
1921
+ window.livePreviewDecision = livePreviewDecision;
1921
1922
  window.profileBlurb = profileBlurb;
1922
1923
  window.profileBadges = profileBadges;
1923
1924
  window.defaultAgentName = defaultAgentName;
@@ -3482,11 +3483,20 @@
3482
3483
  // for a stream-text that never comes.
3483
3484
  if (/^[*_`\s]*nothing to show[*_`\s.!]*$/i.test(txt)) { clearLivePreview(tab); return; }
3484
3485
  const nT = normPreview(txt);
3485
- // Still showing the previous turn's answer (captured at turn start) — this
3486
- // turn hasn't produced its own text yet. Suppress until it does.
3487
- if (tab.livePreviewBaseline && nT === tab.livePreviewBaseline) { clearLivePreview(tab); return; }
3488
- if (nT && isFinalizedBlock(tab, nT)) { clearLivePreview(tab); return; }
3489
- tab.livePreviewBaseline = ""; // real new text arrived baseline no longer applies
3486
+ // Decide what to do with the block (pure, tested see livePreviewDecision).
3487
+ // The baseline (previous turn's answer) is kept for the WHOLE turn: once new
3488
+ // text has shown, a redraw frame that briefly makes the OLD block the last
3489
+ // one again is HELD, not flipped back that flip is what made the preview
3490
+ // oscillate new↔old while the model writes.
3491
+ const decision = window.livePreviewDecision
3492
+ ? window.livePreviewDecision({
3493
+ isBaseline: !!tab.livePreviewBaseline && nT === tab.livePreviewBaseline,
3494
+ isFinalized: !!nT && isFinalizedBlock(tab, nT),
3495
+ showing: !!tab.livePreviewBubble,
3496
+ })
3497
+ : "show";
3498
+ if (decision === "keep") return;
3499
+ if (decision === "clear") { clearLivePreview(tab); return; }
3490
3500
  if (!tab.livePreviewEl) {
3491
3501
  const turn = document.createElement("div");
3492
3502
  turn.className = "turn claude live-preview";
@@ -40,6 +40,21 @@ export function extractLiveText(screen) {
40
40
  // - running tool: "⏺ Running 1 shell command" / "⏺ Ran 1 shell command".
41
41
  // Without this, the tool line showed up as a text preview.
42
42
  if (/^[\w.-]+\(/.test(head) || /^(Running|Ran)\b/i.test(head)) return "";
43
+ // Is the next non-empty line after `idx` a "⎿" (tool-result marker)? If so, the
44
+ // line at `idx` is a running tool's STATUS ("Sleeping for 6 seconds\n ⎿ $ sleep
45
+ // 6"), not assistant text. Claude Code flips that status line's leading bullet
46
+ // on and off between redraws, so read as text it made the preview oscillate
47
+ // between the tool status and the real block above it — and glued the status
48
+ // onto the text when the bullet was absent.
49
+ const nextIsToolDetail = (idx) => {
50
+ for (let k = idx + 1; k < lines.length; k++) {
51
+ const t = lines[k].trim();
52
+ if (t === "") continue;
53
+ return t.startsWith("⎿");
54
+ }
55
+ return false;
56
+ };
57
+ if (nextIsToolDetail(start)) return ""; // the head itself is a tool status
43
58
 
44
59
  const parts = [head];
45
60
  const isToolLine = (t) => t.startsWith("⎿") || /^(Ran|Running)\b/.test(t);
@@ -61,7 +76,30 @@ export function extractLiveText(screen) {
61
76
  if (!l.startsWith(" ")) break; // unindented → the end (spinner, box, next block)
62
77
  const t = l.trim();
63
78
  if (isToolLine(t)) break; // a tool's sub-line
79
+ if (nextIsToolDetail(i)) break; // an indented tool STATUS ("… \n ⎿ $ cmd") — stop before it
64
80
  parts.push(t);
65
81
  }
66
82
  return parts.join(" ");
67
83
  }
84
+
85
+ /**
86
+ * What to do with the grey live-preview bubble for a freshly extracted block —
87
+ * pure so the anti-flicker rule is testable; the DOM effect stays in the caller.
88
+ *
89
+ * `isBaseline` = the extracted block equals the PREVIOUS turn's answer, captured
90
+ * at turn start and still (or transiently again) on the screen. `isFinalized` =
91
+ * it is a block already streamed as authoritative this turn. `showing` = a
92
+ * preview bubble is currently up.
93
+ *
94
+ * The flicker this fixes: while the model writes, a redraw frame can briefly make
95
+ * the OLD block the last one on screen. Once new text has shown, that must be
96
+ * HELD (keep), not flipped back to the old text — the old "clear on baseline /
97
+ * drop the baseline once new text appears" oscillated new↔old every few frames.
98
+ *
99
+ * @returns {"show"|"keep"|"clear"}
100
+ */
101
+ export function livePreviewDecision({ isBaseline, isFinalized, showing }) {
102
+ if (isBaseline) return showing ? "keep" : "clear";
103
+ if (isFinalized) return "clear";
104
+ return "show";
105
+ }