dsh-paste-code-block 0.2.5 → 0.2.7

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/src/client.js CHANGED
@@ -17,6 +17,20 @@ window.__ModuleLoader__.load({
17
17
  const DETAIL_NS = 'dsh-pcb-detail'
18
18
  const CHIP_X_CLASS = 'dsh-pcb-chip-x'
19
19
 
20
+ // ===== diagnostics =======================================================
21
+ // Off by default. Set `window.__PCB_DEBUG__ = true` in the console (then
22
+ // paste / send) to trace paste parsing and fold decisions. The on-screen
23
+ // badge that used to sit above the composer was scaffolding for one
24
+ // debugging session and is gone.
25
+ const PCB_DEBUG = (() => {
26
+ try { return typeof window !== 'undefined' && window.__PCB_DEBUG__ === true } catch (e) { return false }
27
+ })()
28
+ function pcbDiag(kind, info) {
29
+ if (!PCB_DEBUG) return
30
+ try { console.log('[pcb-diag]', kind + (info ? ': ' + info : '')) } catch (e) {}
31
+ }
32
+ // =========================================================================
33
+
20
34
  // ----- shared chip-DOM helpers ------------------------------------------
21
35
  // Module level on purpose: the chip sync scan and the controller's removal
22
36
  // path both need to map a chip host element back to its label (and its
@@ -218,6 +232,25 @@ window.__ModuleLoader__.load({
218
232
  return ''
219
233
  }
220
234
 
235
+ /**
236
+ * The single complete fence of `text`, or null when the text is not exactly
237
+ * one fenced block (blank lines around it tolerated, real prose not).
238
+ * Keep in sync with src/parse.js#wholeFence.
239
+ */
240
+ function wholeFence(text) {
241
+ const segs = splitFencedSegments(text)
242
+ let fence = null
243
+ for (const seg of segs) {
244
+ if (seg.kind === 'fence') {
245
+ if (fence) return null
246
+ fence = seg
247
+ } else if (seg.text.trim() !== '') {
248
+ return null
249
+ }
250
+ }
251
+ return fence
252
+ }
253
+
221
254
  /**
222
255
  * Parse pasted text into a block, or null when it is ordinary short prose
223
256
  * that should paste plainly. `isCode` drives both the chip label
@@ -228,12 +261,42 @@ window.__ModuleLoader__.load({
228
261
  const text = String(raw || '').replace(/^\uFEFF/, '')
229
262
  if (!text) return null
230
263
 
231
- const fm = /^\s*```([\w+-]*)[^\n]*\n([\s\S]*?)\n?\s*```\s*$/.exec(text)
232
- if (fm) {
233
- const lang = (fm[1] || '').trim()
234
- const content = fm[2].replace(/\n+$/, '')
264
+ // (1) exactly one complete fence: this plugin's own output shape. The
265
+ // fence's info string decides the type, so an explicit ```text paste is
266
+ // a TEXT block and round-trips instead of degrading into bare code.
267
+ const whole = wholeFence(text)
268
+ if (whole) {
269
+ const content = whole.body.replace(/\n+$/, '')
235
270
  if (!content) return null
236
- return { lang, content, lines: content.split(/\r?\n/), isCode: true }
271
+ const infoLang = (whole.lang || '').toLowerCase()
272
+ return {
273
+ lang: infoLang === 'text' ? '' : whole.lang,
274
+ content,
275
+ lines: content.split(/\r?\n/),
276
+ isCode: infoLang !== 'text',
277
+ }
278
+ }
279
+
280
+ // (2) a paste that OPENS with a fence but carries trailing prose — the
281
+ // copy-a-whole-previous-message case. Keep it as ONE block and take the
282
+ // type from the leading fence's info string, so the chip and the
283
+ // transcript agree. (0.2.5 treated it as code and re-wrapped the
284
+ // already-fenced text in a bare fence: two nested fences in the bubble.)
285
+ const segs = splitFencedSegments(text)
286
+ let leadFence = -1
287
+ for (let i = 0; i < segs.length; i += 1) {
288
+ if (segs[i].kind === 'fence') { leadFence = i; break }
289
+ if (segs[i].text.trim() !== '') break
290
+ }
291
+ if (leadFence >= 0) {
292
+ const leadLang = (segs[leadFence].lang || '').toLowerCase()
293
+ const content = text.replace(/\n+$/, '')
294
+ return {
295
+ lang: leadLang === 'text' ? '' : segs[leadFence].lang,
296
+ content,
297
+ lines: content.split(/\r?\n/),
298
+ isCode: leadLang !== 'text',
299
+ }
237
300
  }
238
301
 
239
302
  const multi = /\r?\n/.test(text)
@@ -277,9 +340,23 @@ window.__ModuleLoader__.load({
277
340
  return n
278
341
  }
279
342
 
343
+ /**
344
+ * Backtick marker long enough to wrap `content` unambiguously: longer than
345
+ * the longest backtick run inside it. Keep in sync with
346
+ * src/parse.js#fenceMarkerFor.
347
+ */
348
+ function fenceMarkerFor(content) {
349
+ const src = String(content == null ? '' : content)
350
+ let longest = 0
351
+ const runs = src.match(/`+/g)
352
+ if (runs) for (const run of runs) if (run.length > longest) longest = run.length
353
+ return '`'.repeat(Math.max(3, longest + 1))
354
+ }
355
+
280
356
  function serializeBlock(block) {
281
357
  const lang = block.isCode ? (block.lang || '') : 'text'
282
- return `\n\`\`\`${lang}\n${block.content}\n\`\`\`\n`
358
+ const marker = fenceMarkerFor(block.content)
359
+ return `\n${marker}${lang}\n${block.content}\n${marker}\n`
283
360
  }
284
361
 
285
362
  // ----- fence segmentation (embedded copy of src/parse.js) ----------------
@@ -328,6 +405,80 @@ window.__ModuleLoader__.load({
328
405
  return segments
329
406
  }
330
407
 
408
+ /**
409
+ * Character ranges of every complete fence in `text`, in order, as
410
+ * { start, end } (end exclusive, both fence lines included). Same pairing
411
+ * rules as splitFencedSegments. Keep in sync with src/parse.js#fenceRanges.
412
+ */
413
+ function fenceRanges(text) {
414
+ const src = String(text == null ? '' : text)
415
+ const out = []
416
+ if (!src) return out
417
+ const starts = [0]
418
+ for (let i = 0; i < src.length; i += 1) if (src.charCodeAt(i) === 10) starts.push(i + 1)
419
+ const lineEnd = (n) => (n + 1 < starts.length ? starts[n + 1] - 1 : src.length)
420
+ const lineText = (n) => src.slice(starts[n], lineEnd(n)).replace(/\r$/, '')
421
+ const OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/
422
+ let i = 0
423
+ while (i < starts.length) {
424
+ const m = OPEN.exec(lineText(i))
425
+ if (!m) { i += 1; continue }
426
+ const marker = m[1]
427
+ const ch = marker[0]
428
+ const info = m[2].trim()
429
+ if (ch === '`' && info.includes('`')) { i += 1; continue }
430
+ const closeRe = new RegExp('^ {0,3}' + (ch === '`' ? '`' : '~') + '{' + marker.length + ',}[ \\t]*$')
431
+ let j = i + 1
432
+ let closed = false
433
+ while (j < starts.length) {
434
+ if (closeRe.test(lineText(j))) { closed = true; break }
435
+ j += 1
436
+ }
437
+ if (!closed) { i += 1; continue }
438
+ out.push({ start: starts[i], end: starts[j] + lineText(j).length })
439
+ i = j + 1
440
+ }
441
+ return out
442
+ }
443
+
444
+ /**
445
+ * How the sent-message fold scan should fold `text`, given the run texts
446
+ * DSH actually rendered it as. Keep in sync with src/parse.js#planFoldRuns.
447
+ * Returns null when nothing is foldable, else
448
+ * { mode:'single', segments, runs } — every fence lies inside ONE run
449
+ * { mode:'bubble', segments, fences } — a fence straddles run boundaries
450
+ */
451
+ function planFoldRuns(text, runTexts) {
452
+ const src = String(text == null ? '' : text)
453
+ const segments = splitFencedSegments(src)
454
+ const fenceSegs = segments.filter((seg) => seg.kind === 'fence')
455
+ const spans = fenceRanges(src)
456
+ if (fenceSegs.length !== spans.length) return null
457
+ const fences = []
458
+ fenceSegs.forEach((seg, k) => { if (seg.body.trim() !== '') fences.push(spans[k]) })
459
+ if (!fences.length) return null
460
+
461
+ const bounds = []
462
+ let at = 0
463
+ for (const rt of (runTexts || [])) {
464
+ const len = String(rt == null ? '' : rt).length
465
+ bounds.push([at, at + len])
466
+ at += len
467
+ }
468
+ const runOf = (off) => {
469
+ for (let k = 0; k < bounds.length; k += 1) if (off >= bounds[k][0] && off < bounds[k][1]) return k
470
+ return -1
471
+ }
472
+ const runs = []
473
+ for (const f of fences) {
474
+ const a = runOf(f.start)
475
+ const b = runOf(Math.max(f.start, f.end - 1))
476
+ if (a < 0 || a !== b) return { mode: 'bubble', segments, fences }
477
+ if (!runs.includes(a)) runs.push(a)
478
+ }
479
+ return { mode: 'single', segments, runs }
480
+ }
481
+
331
482
  // ===== Recovery mirror (draft re-seed after workspace switch / reload) ===
332
483
  // DSH persists a session's composer draft as PLAIN TEXT
333
484
  // (`dsh.conversation.<sid>` in localStorage) and re-seeds the editor from
@@ -515,7 +666,11 @@ window.__ModuleLoader__.load({
515
666
  insertReference(sessionId, block) {
516
667
  const { actx, shell } = this.scope(sessionId)
517
668
  const input = shell.snapshot
518
- if (!input || input.phase !== 'plain') return false
669
+ // DSH's SessionInputShell.insertReference accepts both `plain` and
670
+ // `claimed` phases; mirror that so a paste while a command draft is
671
+ // still pending (claimed) folds into a chip instead of falling back to
672
+ // raw text. Submitting/adjudicating (mid-submit) stay rejected.
673
+ if (!input || (input.phase !== 'plain' && input.phase !== 'claimed')) return false
519
674
  // Each chip contributes exactly one char to the editor's detect text
520
675
  // (the "" placeholder) and one char to the plain `draft` projection
521
676
  // (clipboardText = zero-width space, chosen because String#trim() does
@@ -1183,14 +1338,20 @@ window.__ModuleLoader__.load({
1183
1338
  // React-side and never sees this DOM.
1184
1339
  const FOLD_HOST_CLASS = 'dsh-pcb-fold-host'
1185
1340
  const FOLD_CARD_CLASS = 'dsh-pcb-fold'
1186
- const FOLD_RUN_ATTR = 'data-dsh-pcb-run' // '1|<stamp>' folded · '0|<stamp>' checked, plain
1187
- const FOLD_STAMP_ATTR = 'data-dsh-pcb-stamp' // on the host, matches the run's stamp
1341
+ const FOLD_STAMP_ATTR = 'data-dsh-pcb-stamp' // on a host: the content stamp it was built from
1342
+ const FOLD_HIDE_ATTR = 'data-dsh-pcb-hide' // on every run the scan hid: the same stamp
1343
+ const FOLD_SIG_ATTR = 'data-dsh-pcb-sig' // on the bubble: '<mode>|<stamp>' it was folded for
1344
+ // Match the message bubble container case-insensitively: DSH renamed its
1345
+ // chat bubble class from `...bubble...` to `_367UGW_chatBubble` (capital B),
1346
+ // and a bare [class*="bubble"] comparison is case-sensitive — it matched
1347
+ // nothing on the new shell, so sent fenced blocks were never folded and
1348
+ // stayed expanded in the transcript. The `i` flag makes it match both.
1188
1349
  const FOLD_BUBBLE_SEL = [
1189
1350
  '[data-chat-flow-kind="user"]',
1190
1351
  '[data-chat-flow-kind="steering"]',
1191
1352
  '[data-submission-echo]',
1192
1353
  '[data-pending-steering]',
1193
- ].map((s) => `${s} [class*="bubble"]`).join(',')
1354
+ ].map((s) => `${s} [class*="bubble" i]`).join(',')
1194
1355
  const foldOpenKeys = new Set() // user-toggled-open cards, stable across rebuilds
1195
1356
  let foldRev = 0 // bumped on locale switch so cards re-title
1196
1357
 
@@ -1423,6 +1584,7 @@ window.__ModuleLoader__.load({
1423
1584
  const inject = ['slots', 'sessions', 'conversation', 'inputTriggers', 'locale']
1424
1585
 
1425
1586
  function apply(ctx) {
1587
+ try { pcbDiag('LOADED', 'v0.2.5-diag') } catch (e) {}
1426
1588
  // Bump alongside package.json on every release — the load log is the
1427
1589
  // only proof of WHICH bundle generation the browser actually loaded.
1428
1590
  try { console.log('[dsh-paste-code-block] client loaded v0.2.5') } catch (e) { /* noop */ }
@@ -1494,25 +1656,95 @@ window.__ModuleLoader__.load({
1494
1656
  }
1495
1657
 
1496
1658
  // ----- sent-message fold: the scan ------------------------------------
1497
- function foldRun(run, plan, keyBase, stamp) {
1498
- run.setAttribute(FOLD_RUN_ATTR, `1|${stamp}`)
1499
- run.style.display = 'none'
1500
- const next = run.nextElementSibling
1501
- if (next && next.classList.contains(FOLD_HOST_CLASS)) {
1502
- if (next.getAttribute(FOLD_STAMP_ATTR) === stamp) return // already current
1503
- next.remove()
1659
+ // DSH renders a sent message as plain pre-wrap runs and SPLITS those runs
1660
+ // at every @mention chip, so one serialized block can arrive as several
1661
+ // sibling runs with the fence opener in one and its closer in another.
1662
+ // A per-run scan then sees only an unterminated half (nothing to fold) —
1663
+ // or, worse, a fragment that happens to look like a complete fence, which
1664
+ // is how a bogus card built from the tail of the message used to appear.
1665
+ // So fold at BUBBLE scope: rebuild the text from the runs, plan the
1666
+ // fences over that, and keep the per-run path only when every fence sits
1667
+ // inside a single run (which leaves DSH's mention chips clickable).
1668
+ const foldedBubbles = new WeakSet()
1669
+
1670
+ /** Hand a bubble back to DSH's own rendering. */
1671
+ function clearFolds(bubble) {
1672
+ for (const el of [...bubble.children]) {
1673
+ if (el.classList.contains(FOLD_HOST_CLASS)) { el.remove(); continue }
1674
+ if (el.getAttribute(FOLD_HIDE_ATTR) !== null) {
1675
+ el.style.display = ''
1676
+ el.removeAttribute(FOLD_HIDE_ATTR)
1677
+ }
1504
1678
  }
1505
- const host = buildFoldHost(plan, keyBase, t)
1506
- host.setAttribute(FOLD_STAMP_ATTR, stamp)
1507
- run.insertAdjacentElement('afterend', host)
1679
+ bubble.removeAttribute(FOLD_SIG_ATTR)
1680
+ foldedBubbles.delete(bubble)
1681
+ }
1682
+
1683
+ /** True when the DOM still matches what `sig` claims (React remounts runs). */
1684
+ function foldsIntact(bubble, mode, stamp, wanted) {
1685
+ const kids = [...bubble.children]
1686
+ const hosts = kids.filter((el) => el.classList.contains(FOLD_HOST_CLASS))
1687
+ if (hosts.length !== (mode === 'bubble' ? 1 : wanted)) return false
1688
+ for (const host of hosts) if (host.getAttribute(FOLD_STAMP_ATTR) !== stamp) return false
1689
+ const hidden = kids.filter((el) => el.getAttribute(FOLD_HIDE_ATTR) === stamp)
1690
+ if (hidden.length !== wanted) return false
1691
+ for (const el of hidden) if (el.style.display !== 'none') return false
1692
+ return true
1508
1693
  }
1509
1694
 
1510
- /** Restore a previously folded run to DSH's own rendering. */
1511
- function unfoldRun(run, stamp) {
1512
- const next = run.nextElementSibling
1513
- if (next && next.classList.contains(FOLD_HOST_CLASS)) next.remove()
1514
- run.style.display = ''
1515
- run.setAttribute(FOLD_RUN_ATTR, `0|${stamp}`)
1695
+ function foldBubble(bubble) {
1696
+ const holder = bubble.closest('[data-chat-flow-key]')
1697
+ const flowKey = holder ? holder.getAttribute('data-chat-flow-key') || '' : ''
1698
+ const runs = [...bubble.children].filter((el) => !el.classList.contains(FOLD_HOST_CLASS))
1699
+ const texts = runs.map((el) => el.textContent || '')
1700
+ const full = texts.join('')
1701
+ const stamp = foldStamp(full)
1702
+ // Cheap guard first: most messages carry no fence at all.
1703
+ const plan = runs.length && FENCE_HINT.test(full) ? planFoldRuns(full, texts) : null
1704
+ let mode = plan ? plan.mode : ''
1705
+ let subs = []
1706
+ if (plan && mode === 'single') {
1707
+ for (const k of plan.runs) {
1708
+ const sub = foldPlanFor(texts[k])
1709
+ if (sub) subs.push([k, sub])
1710
+ }
1711
+ // A run we cannot plan for (a mention chip, a wrapper element) is not
1712
+ // ours to hide — rebuild the bubble instead.
1713
+ if (!subs.length || plan.runs.some((k) => !isFoldRun(runs[k]))) mode = 'bubble'
1714
+ }
1715
+ const sig = plan ? `${mode}|${stamp}` : ''
1716
+ const wanted = plan ? (mode === 'bubble' ? runs.length : subs.length) : 0
1717
+ if (plan && bubble.getAttribute(FOLD_SIG_ATTR) === sig && foldsIntact(bubble, mode, stamp, wanted)) return
1718
+ if (!plan && !foldedBubbles.has(bubble)) return
1719
+ clearFolds(bubble)
1720
+ if (!plan) return
1721
+ const keyBase = flowKey || 'h' + hash32(full)
1722
+ const fenceCount = plan.fences ? plan.fences.length : (plan.runs || []).length
1723
+ pcbDiag('FOLD', 'mode=' + mode + ' runs=' + runs.length + ' fences=' + fenceCount +
1724
+ ' hide=' + (mode === 'bubble' ? 'all' : subs.map((s) => s[0]).join(',')) + ' len=' + full.length)
1725
+ if (mode === 'bubble') {
1726
+ // Hide every run and render the whole message from the plan, so the
1727
+ // fence that straddled them is paired again. Mention chips inside the
1728
+ // folded text lose their click target — they are collapsed content.
1729
+ const host = buildFoldHost(plan.segments, `${keyBase}#b`, t)
1730
+ host.setAttribute(FOLD_STAMP_ATTR, stamp)
1731
+ for (const el of runs) {
1732
+ el.style.display = 'none'
1733
+ el.setAttribute(FOLD_HIDE_ATTR, stamp)
1734
+ }
1735
+ bubble.appendChild(host)
1736
+ } else {
1737
+ for (const [k, sub] of subs.slice().sort((a, b) => a[0] - b[0])) {
1738
+ const el = runs[k]
1739
+ el.style.display = 'none'
1740
+ el.setAttribute(FOLD_HIDE_ATTR, stamp)
1741
+ const host = buildFoldHost(sub, `${keyBase}#${k}`, t)
1742
+ host.setAttribute(FOLD_STAMP_ATTR, stamp)
1743
+ el.insertAdjacentElement('afterend', host)
1744
+ }
1745
+ }
1746
+ bubble.setAttribute(FOLD_SIG_ATTR, sig)
1747
+ foldedBubbles.add(bubble)
1516
1748
  }
1517
1749
 
1518
1750
  function scanFolds() {
@@ -1521,38 +1753,7 @@ window.__ModuleLoader__.load({
1521
1753
  try { bubbles = document.querySelectorAll(FOLD_BUBBLE_SEL) } catch (e) { return }
1522
1754
  for (const bubble of bubbles) {
1523
1755
  if (!bubble.isConnected) continue
1524
- const holder = bubble.closest('[data-chat-flow-key]')
1525
- const flowKey = holder ? holder.getAttribute('data-chat-flow-key') || '' : ''
1526
- const kids = [...bubble.children]
1527
- for (let i = 0; i < kids.length; i += 1) {
1528
- const el = kids[i]
1529
- if (el.classList.contains(FOLD_HOST_CLASS)) continue
1530
- if (!isFoldRun(el)) continue
1531
- const text = el.textContent || ''
1532
- const stamp = foldStamp(text)
1533
- const marked = el.getAttribute(FOLD_RUN_ATTR) || ''
1534
- if (marked === `1|${stamp}` || marked === `0|${stamp}`) continue // current
1535
- const plan = foldPlanFor(text)
1536
- if (!plan) {
1537
- if (marked.startsWith('1|')) unfoldRun(el, stamp)
1538
- else el.setAttribute(FOLD_RUN_ATTR, `0|${stamp}`)
1539
- continue
1540
- }
1541
- // Echo/pending rows have no flow key — fall back to a content hash so
1542
- // two simultaneous unkeyed rows never share one open-state slot.
1543
- const keyBase = `${flowKey || 'h' + hash32(text)}#${i}`
1544
- foldRun(el, plan, keyBase, stamp)
1545
- }
1546
- // Drop orphan hosts (e.g. React remounted the run span underneath us).
1547
- for (const el of [...bubble.children]) {
1548
- if (!el.classList.contains(FOLD_HOST_CLASS)) continue
1549
- const prev = el.previousElementSibling
1550
- const mark = prev && prev.getAttribute ? prev.getAttribute(FOLD_RUN_ATTR) : null
1551
- const keep = !!mark && mark.startsWith('1|') &&
1552
- prev.style.display === 'none' &&
1553
- el.getAttribute(FOLD_STAMP_ATTR) === mark.slice(2)
1554
- if (!keep) el.remove()
1555
- }
1756
+ try { foldBubble(bubble) } catch (e) { /* never break DSH's render loop */ }
1556
1757
  }
1557
1758
  }
1558
1759
 
@@ -1586,10 +1787,11 @@ window.__ModuleLoader__.load({
1586
1787
  ctx.effect(() => () => {
1587
1788
  try {
1588
1789
  for (const host of document.querySelectorAll(`.${FOLD_HOST_CLASS}`)) host.remove()
1589
- for (const run of document.querySelectorAll(`[${FOLD_RUN_ATTR}]`)) {
1790
+ for (const run of document.querySelectorAll(`[${FOLD_HIDE_ATTR}]`)) {
1590
1791
  run.style.display = ''
1591
- run.removeAttribute(FOLD_RUN_ATTR)
1792
+ run.removeAttribute(FOLD_HIDE_ATTR)
1592
1793
  }
1794
+ for (const bubble of document.querySelectorAll(`[${FOLD_SIG_ATTR}]`)) bubble.removeAttribute(FOLD_SIG_ATTR)
1593
1795
  } catch (e) { /* noop */ }
1594
1796
  foldOpenKeys.clear()
1595
1797
  }, 'paste-code-block: fold restore')
@@ -1651,14 +1853,15 @@ window.__ModuleLoader__.load({
1651
1853
  const target = document.activeElement
1652
1854
  if (!target || !(target instanceof Element)) return
1653
1855
  const sid = sessionFor(target)
1654
- if (!sid) return
1856
+ if (!sid) { pcbDiag("EXIT-SID-NULL", "activeEl=" + target.tagName + "." + (target.className || "") + " mark=" + !!document.querySelector("[data-dsh-pcb]")); return }
1655
1857
  const block = parseBlock(text)
1656
- if (!block) { console.log('[dsh-paste-code-block] paste NOT block', text.length); return }
1858
+ if (!block) { pcbDiag("EXIT-NOT-BLOCK", "len=" + text.length); console.log('[dsh-paste-code-block] paste NOT block', text.length); return }
1657
1859
  if (pasteStorm.seen(text, Date.now())) {
1658
1860
  // Consume the event (never let the raw text fall through to the
1659
1861
  // composer) but create nothing.
1660
1862
  ev.preventDefault()
1661
1863
  ev.stopPropagation()
1864
+ pcbDiag("EXIT-STORM", "len=" + text.length);
1662
1865
  console.log('[dsh-paste-code-block] storm paste ignored (<' + STORM_WINDOW_MS + 'ms repeat)')
1663
1866
  return
1664
1867
  }
@@ -1666,11 +1869,14 @@ window.__ModuleLoader__.load({
1666
1869
  // trusted=false means a page script dispatched this paste (userscript,
1667
1870
  // clipboard bridge, replay glue) rather than a real keystroke — the
1668
1871
  // load-log line that names the source when a storm report comes in.
1872
+ pcbDiag("ENTER-INTERCEPT", "sid=" + sid + " lines=" + block.lines.length + " code=" + block.isCode + " trusted=" + ev.isTrusted);
1669
1873
  console.log('[dsh-paste-code-block] intercept', block.isCode ? 'code' : 'text', block.lines.length, 'lang=', block.lang, 'trusted=', ev.isTrusted)
1670
1874
  ev.preventDefault()
1671
1875
  ev.stopPropagation()
1672
1876
  try {
1673
1877
  if (!controller.attach(sid, block)) {
1878
+ const snapX = (() => { try { const s = controller.scope(sid).shell.snapshot; return "phase=" + (s && s.phase) + " draftLen=" + (s && s.draft ? s.draft.length : -1) + " rev=" + (s && s.draftRev) } catch (e) { return "scope-throw:" + e.message } })();
1879
+ pcbDiag("EXIT-ATTACH-FAIL", snapX);
1674
1880
  console.log('[dsh-paste-code-block] attach failed; plain insert instead')
1675
1881
  const { actx, shell } = controller.scope(sid)
1676
1882
  const input = shell.snapshot
@@ -1685,6 +1891,17 @@ window.__ModuleLoader__.load({
1685
1891
  console.warn('[dsh-paste-code-block] could not insert block text')
1686
1892
  }
1687
1893
  }
1894
+ pcbDiag("OK-ATTACHED", "id=" + block.id);
1895
+ setTimeout(() => { try {
1896
+ const sc = controller.scope(sid).shell.snapshot || {};
1897
+ const occ = (sc.occurrences || []).filter((o) => o.source === 'paste-code-block').length;
1898
+ var __chip = document.querySelector('[data-composer-chip="paste-code-block"]'); var __r = __chip && __chip.getBoundingClientRect(); var __cs = __chip && getComputedStyle(__chip); var __csi = __chip && __chip.firstElementChild && getComputedStyle(__chip.firstElementChild); pcbDiag("POST-ATTACH", "phase=" + sc.phase + " draftLen=" + (sc.draft ? sc.draft.length : -1) + " occ=" + occ + " chips=" + document.querySelectorAll('[data-composer-chip="paste-code-block"]').length);
1899
+ var __hosts = []; var __seen = new Set();
1900
+ document.querySelectorAll('[data-composer-chip]').forEach(function (c) { var k = c.getAttribute('data-composer-chip') + '|' + (c.closest('[data-composer-seat]') ? 'seat' : (c.closest('[data-message-id],[data-message-id] *,.dsh-msg,.message') ? 'msg' : '?')); if (!__seen.has(k)) { __seen.add(k); __hosts.push(k) } });
1901
+ pcbDiag("CHIP-HOSTS", __hosts.join(',') || 'none');
1902
+ var __seats = [].map.call(document.querySelectorAll('[data-composer-seat]'), function (s) { return s.getBoundingClientRect().width + 'x' + Math.round(s.getBoundingClientRect().height) }).join(',');
1903
+ pcbDiag("SEATS", "n=" + document.querySelectorAll('[data-composer-seat]').length + " sizes=" + (__seats || '-') + " ce=" + document.querySelectorAll('[contenteditable="true"]').length); pcbDiag("CHIP-DOM", __chip ? ("rect=" + Math.round(__r.width) + "x" + Math.round(__r.height) + "@" + Math.round(__r.x) + "," + Math.round(__r.y) + " disp=" + __cs.display + " vis=" + __cs.visibility + " op=" + __cs.opacity + " innerDisp=" + (__csi ? __csi.display : "?") + " txt=(" + __chip.textContent.slice(0, 24) + ") parent=" + (__chip.parentElement ? __chip.parentElement.tagName + "." + String(__chip.parentElement.className).slice(0, 40) : "?")) : "NO-CHIP");
1904
+ } catch (e) { pcbDiag("POST-ATTACH-THROW", e.message) } }, 350);
1688
1905
  } catch (err) {
1689
1906
  console.error('[paste-code-block] attach failed:', err)
1690
1907
  }