dsh-paste-code-block 0.2.5 → 0.2.6

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,8 +1338,9 @@ 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
1188
1344
  const FOLD_BUBBLE_SEL = [
1189
1345
  '[data-chat-flow-kind="user"]',
1190
1346
  '[data-chat-flow-kind="steering"]',
@@ -1423,6 +1579,7 @@ window.__ModuleLoader__.load({
1423
1579
  const inject = ['slots', 'sessions', 'conversation', 'inputTriggers', 'locale']
1424
1580
 
1425
1581
  function apply(ctx) {
1582
+ try { pcbDiag('LOADED', 'v0.2.5-diag') } catch (e) {}
1426
1583
  // Bump alongside package.json on every release — the load log is the
1427
1584
  // only proof of WHICH bundle generation the browser actually loaded.
1428
1585
  try { console.log('[dsh-paste-code-block] client loaded v0.2.5') } catch (e) { /* noop */ }
@@ -1494,25 +1651,95 @@ window.__ModuleLoader__.load({
1494
1651
  }
1495
1652
 
1496
1653
  // ----- 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()
1654
+ // DSH renders a sent message as plain pre-wrap runs and SPLITS those runs
1655
+ // at every @mention chip, so one serialized block can arrive as several
1656
+ // sibling runs with the fence opener in one and its closer in another.
1657
+ // A per-run scan then sees only an unterminated half (nothing to fold) —
1658
+ // or, worse, a fragment that happens to look like a complete fence, which
1659
+ // is how a bogus card built from the tail of the message used to appear.
1660
+ // So fold at BUBBLE scope: rebuild the text from the runs, plan the
1661
+ // fences over that, and keep the per-run path only when every fence sits
1662
+ // inside a single run (which leaves DSH's mention chips clickable).
1663
+ const foldedBubbles = new WeakSet()
1664
+
1665
+ /** Hand a bubble back to DSH's own rendering. */
1666
+ function clearFolds(bubble) {
1667
+ for (const el of [...bubble.children]) {
1668
+ if (el.classList.contains(FOLD_HOST_CLASS)) { el.remove(); continue }
1669
+ if (el.getAttribute(FOLD_HIDE_ATTR) !== null) {
1670
+ el.style.display = ''
1671
+ el.removeAttribute(FOLD_HIDE_ATTR)
1672
+ }
1504
1673
  }
1505
- const host = buildFoldHost(plan, keyBase, t)
1506
- host.setAttribute(FOLD_STAMP_ATTR, stamp)
1507
- run.insertAdjacentElement('afterend', host)
1674
+ bubble.removeAttribute(FOLD_SIG_ATTR)
1675
+ foldedBubbles.delete(bubble)
1676
+ }
1677
+
1678
+ /** True when the DOM still matches what `sig` claims (React remounts runs). */
1679
+ function foldsIntact(bubble, mode, stamp, wanted) {
1680
+ const kids = [...bubble.children]
1681
+ const hosts = kids.filter((el) => el.classList.contains(FOLD_HOST_CLASS))
1682
+ if (hosts.length !== (mode === 'bubble' ? 1 : wanted)) return false
1683
+ for (const host of hosts) if (host.getAttribute(FOLD_STAMP_ATTR) !== stamp) return false
1684
+ const hidden = kids.filter((el) => el.getAttribute(FOLD_HIDE_ATTR) === stamp)
1685
+ if (hidden.length !== wanted) return false
1686
+ for (const el of hidden) if (el.style.display !== 'none') return false
1687
+ return true
1508
1688
  }
1509
1689
 
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}`)
1690
+ function foldBubble(bubble) {
1691
+ const holder = bubble.closest('[data-chat-flow-key]')
1692
+ const flowKey = holder ? holder.getAttribute('data-chat-flow-key') || '' : ''
1693
+ const runs = [...bubble.children].filter((el) => !el.classList.contains(FOLD_HOST_CLASS))
1694
+ const texts = runs.map((el) => el.textContent || '')
1695
+ const full = texts.join('')
1696
+ const stamp = foldStamp(full)
1697
+ // Cheap guard first: most messages carry no fence at all.
1698
+ const plan = runs.length && FENCE_HINT.test(full) ? planFoldRuns(full, texts) : null
1699
+ let mode = plan ? plan.mode : ''
1700
+ let subs = []
1701
+ if (plan && mode === 'single') {
1702
+ for (const k of plan.runs) {
1703
+ const sub = foldPlanFor(texts[k])
1704
+ if (sub) subs.push([k, sub])
1705
+ }
1706
+ // A run we cannot plan for (a mention chip, a wrapper element) is not
1707
+ // ours to hide — rebuild the bubble instead.
1708
+ if (!subs.length || plan.runs.some((k) => !isFoldRun(runs[k]))) mode = 'bubble'
1709
+ }
1710
+ const sig = plan ? `${mode}|${stamp}` : ''
1711
+ const wanted = plan ? (mode === 'bubble' ? runs.length : subs.length) : 0
1712
+ if (plan && bubble.getAttribute(FOLD_SIG_ATTR) === sig && foldsIntact(bubble, mode, stamp, wanted)) return
1713
+ if (!plan && !foldedBubbles.has(bubble)) return
1714
+ clearFolds(bubble)
1715
+ if (!plan) return
1716
+ const keyBase = flowKey || 'h' + hash32(full)
1717
+ const fenceCount = plan.fences ? plan.fences.length : (plan.runs || []).length
1718
+ pcbDiag('FOLD', 'mode=' + mode + ' runs=' + runs.length + ' fences=' + fenceCount +
1719
+ ' hide=' + (mode === 'bubble' ? 'all' : subs.map((s) => s[0]).join(',')) + ' len=' + full.length)
1720
+ if (mode === 'bubble') {
1721
+ // Hide every run and render the whole message from the plan, so the
1722
+ // fence that straddled them is paired again. Mention chips inside the
1723
+ // folded text lose their click target — they are collapsed content.
1724
+ const host = buildFoldHost(plan.segments, `${keyBase}#b`, t)
1725
+ host.setAttribute(FOLD_STAMP_ATTR, stamp)
1726
+ for (const el of runs) {
1727
+ el.style.display = 'none'
1728
+ el.setAttribute(FOLD_HIDE_ATTR, stamp)
1729
+ }
1730
+ bubble.appendChild(host)
1731
+ } else {
1732
+ for (const [k, sub] of subs.slice().sort((a, b) => a[0] - b[0])) {
1733
+ const el = runs[k]
1734
+ el.style.display = 'none'
1735
+ el.setAttribute(FOLD_HIDE_ATTR, stamp)
1736
+ const host = buildFoldHost(sub, `${keyBase}#${k}`, t)
1737
+ host.setAttribute(FOLD_STAMP_ATTR, stamp)
1738
+ el.insertAdjacentElement('afterend', host)
1739
+ }
1740
+ }
1741
+ bubble.setAttribute(FOLD_SIG_ATTR, sig)
1742
+ foldedBubbles.add(bubble)
1516
1743
  }
1517
1744
 
1518
1745
  function scanFolds() {
@@ -1521,38 +1748,7 @@ window.__ModuleLoader__.load({
1521
1748
  try { bubbles = document.querySelectorAll(FOLD_BUBBLE_SEL) } catch (e) { return }
1522
1749
  for (const bubble of bubbles) {
1523
1750
  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
- }
1751
+ try { foldBubble(bubble) } catch (e) { /* never break DSH's render loop */ }
1556
1752
  }
1557
1753
  }
1558
1754
 
@@ -1586,10 +1782,11 @@ window.__ModuleLoader__.load({
1586
1782
  ctx.effect(() => () => {
1587
1783
  try {
1588
1784
  for (const host of document.querySelectorAll(`.${FOLD_HOST_CLASS}`)) host.remove()
1589
- for (const run of document.querySelectorAll(`[${FOLD_RUN_ATTR}]`)) {
1785
+ for (const run of document.querySelectorAll(`[${FOLD_HIDE_ATTR}]`)) {
1590
1786
  run.style.display = ''
1591
- run.removeAttribute(FOLD_RUN_ATTR)
1787
+ run.removeAttribute(FOLD_HIDE_ATTR)
1592
1788
  }
1789
+ for (const bubble of document.querySelectorAll(`[${FOLD_SIG_ATTR}]`)) bubble.removeAttribute(FOLD_SIG_ATTR)
1593
1790
  } catch (e) { /* noop */ }
1594
1791
  foldOpenKeys.clear()
1595
1792
  }, 'paste-code-block: fold restore')
@@ -1651,14 +1848,15 @@ window.__ModuleLoader__.load({
1651
1848
  const target = document.activeElement
1652
1849
  if (!target || !(target instanceof Element)) return
1653
1850
  const sid = sessionFor(target)
1654
- if (!sid) return
1851
+ if (!sid) { pcbDiag("EXIT-SID-NULL", "activeEl=" + target.tagName + "." + (target.className || "") + " mark=" + !!document.querySelector("[data-dsh-pcb]")); return }
1655
1852
  const block = parseBlock(text)
1656
- if (!block) { console.log('[dsh-paste-code-block] paste NOT block', text.length); return }
1853
+ if (!block) { pcbDiag("EXIT-NOT-BLOCK", "len=" + text.length); console.log('[dsh-paste-code-block] paste NOT block', text.length); return }
1657
1854
  if (pasteStorm.seen(text, Date.now())) {
1658
1855
  // Consume the event (never let the raw text fall through to the
1659
1856
  // composer) but create nothing.
1660
1857
  ev.preventDefault()
1661
1858
  ev.stopPropagation()
1859
+ pcbDiag("EXIT-STORM", "len=" + text.length);
1662
1860
  console.log('[dsh-paste-code-block] storm paste ignored (<' + STORM_WINDOW_MS + 'ms repeat)')
1663
1861
  return
1664
1862
  }
@@ -1666,11 +1864,14 @@ window.__ModuleLoader__.load({
1666
1864
  // trusted=false means a page script dispatched this paste (userscript,
1667
1865
  // clipboard bridge, replay glue) rather than a real keystroke — the
1668
1866
  // load-log line that names the source when a storm report comes in.
1867
+ pcbDiag("ENTER-INTERCEPT", "sid=" + sid + " lines=" + block.lines.length + " code=" + block.isCode + " trusted=" + ev.isTrusted);
1669
1868
  console.log('[dsh-paste-code-block] intercept', block.isCode ? 'code' : 'text', block.lines.length, 'lang=', block.lang, 'trusted=', ev.isTrusted)
1670
1869
  ev.preventDefault()
1671
1870
  ev.stopPropagation()
1672
1871
  try {
1673
1872
  if (!controller.attach(sid, block)) {
1873
+ 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 } })();
1874
+ pcbDiag("EXIT-ATTACH-FAIL", snapX);
1674
1875
  console.log('[dsh-paste-code-block] attach failed; plain insert instead')
1675
1876
  const { actx, shell } = controller.scope(sid)
1676
1877
  const input = shell.snapshot
@@ -1685,6 +1886,17 @@ window.__ModuleLoader__.load({
1685
1886
  console.warn('[dsh-paste-code-block] could not insert block text')
1686
1887
  }
1687
1888
  }
1889
+ pcbDiag("OK-ATTACHED", "id=" + block.id);
1890
+ setTimeout(() => { try {
1891
+ const sc = controller.scope(sid).shell.snapshot || {};
1892
+ const occ = (sc.occurrences || []).filter((o) => o.source === 'paste-code-block').length;
1893
+ 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);
1894
+ var __hosts = []; var __seen = new Set();
1895
+ 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) } });
1896
+ pcbDiag("CHIP-HOSTS", __hosts.join(',') || 'none');
1897
+ var __seats = [].map.call(document.querySelectorAll('[data-composer-seat]'), function (s) { return s.getBoundingClientRect().width + 'x' + Math.round(s.getBoundingClientRect().height) }).join(',');
1898
+ 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");
1899
+ } catch (e) { pcbDiag("POST-ATTACH-THROW", e.message) } }, 350);
1688
1900
  } catch (err) {
1689
1901
  console.error('[paste-code-block] attach failed:', err)
1690
1902
  }