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/LICENSE +21 -21
- package/README.md +118 -118
- package/README.zh-CN.md +118 -118
- package/cordis.patch.yml +4 -4
- package/docs/design.md +373 -337
- package/docs/images/hero.svg +3 -3
- package/docs/images/languages.svg +3 -3
- package/docs/images/render.cjs +20 -20
- package/docs/images/workflow.svg +3 -3
- package/package.json +3 -2
- package/src/chip-nodes.js +46 -46
- package/src/client.js +279 -62
- package/src/i18n.js +140 -140
- package/src/index.js +11 -11
- package/src/parse.js +159 -6
- package/src/projection.js +54 -54
- package/tests/chip-nodes.test.js +71 -0
- package/tests/fixtures.js +9 -0
- package/tests/fold-smoke.test.js +642 -0
- package/tests/fold.test.js +147 -0
- package/tests/i18n.test.js +112 -0
- package/tests/lexical-chip-probe.mjs +109 -0
- package/tests/parse.test.js +183 -0
- package/tests/projection.test.js +64 -0
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1187
|
-
const
|
|
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
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
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(`[${
|
|
1790
|
+
for (const run of document.querySelectorAll(`[${FOLD_HIDE_ATTR}]`)) {
|
|
1590
1791
|
run.style.display = ''
|
|
1591
|
-
run.removeAttribute(
|
|
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
|
}
|