wezard 2.1.0 → 2.1.2
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/CLAUDE.md +1 -1
- package/dist/daemon/detail.js +13 -2
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/index.js +1 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +50 -27
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/notices.js +3 -5
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/peers.js +117 -87
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/session-name.js +1 -1
- package/dist/daemon/session-name.js.map +1 -1
- package/dist/shared/config.js +6 -6
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +22 -5
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/reminder.js +56 -0
- package/dist/shared/reminder.js.map +1 -0
- package/dist/shared/role-render.js +6 -3
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +24 -15
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/session-label.js +9 -3
- package/dist/shared/session-label.js.map +1 -1
- package/hooks/pre-tool-use.sh +1 -1
- package/package.json +1 -1
- package/web/chat.css +23 -13
- package/web/chat.js +126 -21
package/web/chat.css
CHANGED
|
@@ -211,16 +211,14 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
211
211
|
.si .ts { margin-left: auto; flex: none; font-size: 10.5px; color: var(--faint); font-variant-numeric: tabular-nums }
|
|
212
212
|
.si .pv { display: block; font-size: 11.5px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
213
213
|
.si .pv:empty { display: none }
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.ci > .av .live {
|
|
218
|
-
position: absolute; right: 1px; bottom: -3px; width: 9px; height: 9px;
|
|
219
|
-
border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
|
|
220
|
-
animation: pulse 1.4s infinite;
|
|
214
|
+
.si-more {
|
|
215
|
+
display: block; width: 100%; text-align: center; padding: 4px 8px; border: 0;
|
|
216
|
+
background: transparent; cursor: pointer; font-size: 11.5px; color: var(--mute);
|
|
221
217
|
}
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
.si-more:hover { color: var(--accent); font-weight: 600 }
|
|
219
|
+
|
|
220
|
+
/* 执行中的 wizard: 名字右边一盏呼吸灯, 会话与子项同一位置, 与回复头上的 .tg-dot 同大。 */
|
|
221
|
+
.ci .l1 .live, .si .l1 .live {
|
|
224
222
|
flex: none; align-self: center; width: 7px; height: 7px;
|
|
225
223
|
border-radius: 50%; background: #1a7f37; animation: pulse 1.4s infinite;
|
|
226
224
|
}
|
|
@@ -303,6 +301,8 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
303
301
|
.mrow .mb .tool-body section > h2, .mrow .mb .diff-meta,
|
|
304
302
|
.mrow .mb .tool-call > summary:hover { background: var(--bub-soft); border-color: var(--bub-line) }
|
|
305
303
|
.mrow .mb .md-body pre code { background: transparent }
|
|
304
|
+
/* 不是说给人听的 (工具过程框、保温 ping、reminder) 一律白底 —— 底色只留给真正的对话。 */
|
|
305
|
+
.mrow .mb .steps, .ping-fold .mrow { --bub: #fff; --bub-soft: #f6f8fa; --bub-line: #d0d7de }
|
|
306
306
|
/* 展开的工具正文 (command / diff / 文件内容) 是裸 section, 吃的是 TURN_CSS 的白底。 */
|
|
307
307
|
.mrow .mb .tool-body section { background: transparent; border-color: var(--bub-line) }
|
|
308
308
|
.mrow .mb .bubble details { border-color: var(--bub-line) }
|
|
@@ -338,7 +338,7 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
338
338
|
.mrow.mark { justify-content: center }
|
|
339
339
|
.mrow.mark > * { flex: 1; max-width: 520px }
|
|
340
340
|
|
|
341
|
-
/*
|
|
341
|
+
/* 换视角后一条旧消息都不在了 (换到别的会话): 整列淡入。有交集的走 chat.js 的逐条 FLIP (位移 + 底色)。 */
|
|
342
342
|
.thread-in.flipping { animation: flip .26s ease }
|
|
343
343
|
@keyframes flip { 0% { opacity: 1 } 45% { opacity: 0; transform: scaleX(.985) } 100% { opacity: 1 } }
|
|
344
344
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -366,12 +366,13 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
366
366
|
/* ── 🏓 keepalive 折叠 ──────────────────────────────────────────────
|
|
367
367
|
保温 ping 不是对话, 但它是真花销 —— 所以留在时间轴上, 只是收成一行虚线。 */
|
|
368
368
|
/* summary 的灰底 / 底线是 SHARED_CSS 里通用 details 的 —— 圆角得自己裁, 底线只在展开时才是分割线。 */
|
|
369
|
-
.ping-fold { border: 1px dashed #d8dee4; border-radius: 10px; background: #
|
|
369
|
+
.ping-fold { border: 1px dashed #d8dee4; border-radius: 10px; background: #fff; overflow: hidden }
|
|
370
370
|
.ping-fold:not([open]) > .ping-sum { border-bottom: 0 }
|
|
371
371
|
.ping-fold > .ping-sum {
|
|
372
|
-
display: flex; align-items: center; gap: 8px; padding: 6px 12px;
|
|
372
|
+
display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: #fff;
|
|
373
373
|
cursor: pointer; list-style: none; font-size: 11.5px; color: #8c959f;
|
|
374
374
|
font-family: ui-monospace, Menlo, monospace; font-variant-numeric: tabular-nums;
|
|
375
|
+
text-transform: none; letter-spacing: 0; /* SHARED_CSS 的 details summary 会转大写 —— 这里是 wizard 名, 大小写敏感 */
|
|
375
376
|
}
|
|
376
377
|
.ping-fold > .ping-sum::-webkit-details-marker { display: none }
|
|
377
378
|
.ping-fold > .ping-sum::after {
|
|
@@ -380,7 +381,7 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
380
381
|
}
|
|
381
382
|
.ping-fold[open] > .ping-sum::after { transform: rotate(90deg) }
|
|
382
383
|
.ping-fold > .ping-sum:hover { color: #57606a }
|
|
383
|
-
.ping-fold .pb { color: #57606a; font-weight: 700; flex:
|
|
384
|
+
.ping-fold .pb { color: #57606a; font-weight: 700; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
384
385
|
.ping-fold .ps { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
|
|
385
386
|
.ping-fold .pt { flex: none; opacity: .8 }
|
|
386
387
|
.ping-fold .ping-body {
|
|
@@ -722,3 +723,12 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
|
|
|
722
723
|
|
|
723
724
|
/* 过程框里的工具行是 nowrap 的 —— 不裁一刀会把整列撑出横向滚动。 */
|
|
724
725
|
.mb > .turn-group { overflow: hidden }
|
|
726
|
+
|
|
727
|
+
/* 输入尾巴上的 system-reminder: 默认一个 <reminder> 标签, 点一下展开原文 (见 detail-render.splitReminders)。 */
|
|
728
|
+
.rems { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 14px 10px }
|
|
729
|
+
.rem { cursor: pointer }
|
|
730
|
+
.rem-tag { display: inline-block; border-radius: 4px; padding: 0 5px; font: 11px/1.6 ui-monospace, monospace; color: var(--faint); background: #0000000a }
|
|
731
|
+
.rem-raw { display: none; margin: 0; padding: 3px 8px; border-radius: 6px; background: #0000000a; color: var(--mute); white-space: pre-wrap; word-break: break-word; font: 11px/1.5 ui-monospace, monospace; cursor: text }
|
|
732
|
+
.rem.raw { flex-basis: 100% }
|
|
733
|
+
.rem.raw .rem-raw { display: block }
|
|
734
|
+
.rem.raw .rem-tag { display: none }
|
package/web/chat.js
CHANGED
|
@@ -210,6 +210,11 @@
|
|
|
210
210
|
var eb = childBy(ex, 'bubbles'), nb = childBy(nc, 'bubbles');
|
|
211
211
|
if (eb && nb) {
|
|
212
212
|
swapHead(ex, nc);
|
|
213
|
+
// 终句刚到: 过程框收起一次。标记留在节点上, 用户之后再点开不会被下一帧收回去。
|
|
214
|
+
if (nc.hasAttribute('data-fold') && !ex.hasAttribute('data-fold')) {
|
|
215
|
+
ex.setAttribute('data-fold', '');
|
|
216
|
+
ex.classList.add('folded');
|
|
217
|
+
}
|
|
213
218
|
reconcile(eb, nb);
|
|
214
219
|
ex.setAttribute('data-sig', nc.getAttribute('data-sig') || '');
|
|
215
220
|
return ex;
|
|
@@ -261,6 +266,12 @@
|
|
|
261
266
|
if (!s) return;
|
|
262
267
|
if (e.target.closest('.steps-head') || e.target === s || e.target === childBy(s, 'bubbles')) s.classList.toggle('folded');
|
|
263
268
|
});
|
|
269
|
+
// reminder 块: 摘要 ⇄ 原文。刚划选了一段原文的那次松手不算点击。
|
|
270
|
+
inner.addEventListener('click', function (e) {
|
|
271
|
+
var r = e.target.closest && e.target.closest('.rem');
|
|
272
|
+
if (!r || String(window.getSelection() || '')) return;
|
|
273
|
+
r.classList.toggle('raw');
|
|
274
|
+
});
|
|
264
275
|
|
|
265
276
|
// ── 左栏: 当前 role 的名片 + 会话列表 ──
|
|
266
277
|
var convOf = function (key) { return R.convs.filter(function (c) { return c.key === key; })[0]; };
|
|
@@ -305,6 +316,8 @@
|
|
|
305
316
|
// 展开态按会话各记各的: 点开一个不收起别的, 轮询重画也不动它。
|
|
306
317
|
// OPEN_AT = 已经替它自动展开过的那个 CONV —— 选中新会话时展开一次, 之后折不折由人说了算。
|
|
307
318
|
var OPEN = {}, OPEN_AT = '';
|
|
319
|
+
// 子项默认只露前 SUB_FOLD 个, 其余收在一条展开/折叠条后面; 展开态同样按会话各记各的。
|
|
320
|
+
var SUB_FOLD = 5, MORE = {};
|
|
308
321
|
var reveal = function () {
|
|
309
322
|
if (CONV && CONV !== OPEN_AT) { OPEN[CONV] = true; OPEN_AT = CONV; }
|
|
310
323
|
};
|
|
@@ -324,18 +337,25 @@
|
|
|
324
337
|
var title = c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>';
|
|
325
338
|
// 只列与我有往来的: 在群里但没和我说过话的人, 点进去也是空的。
|
|
326
339
|
var talked = OPEN[c.key] ? c.subs.filter(function (s) { return s.count; }) : [];
|
|
340
|
+
var hidden = talked.length - SUB_FOLD;
|
|
341
|
+
var bar = hidden > 0
|
|
342
|
+
? '<button class="si-more" data-more="' + esc(c.key) + '">' + (MORE[c.key] ? '折叠' : '展开更多') + ' × ' + hidden + '</button>'
|
|
343
|
+
: '';
|
|
344
|
+
// 展开条钉在第 SUB_FOLD+1 位, 展开与折叠都不挪: 其余子项展开后接在它下面。
|
|
345
|
+
var sub = function (s) {
|
|
346
|
+
var sel = on && s.role === WITH;
|
|
347
|
+
return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
|
|
348
|
+
'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
|
|
349
|
+
goSpan('av', s.role, esc(s.label)) +
|
|
350
|
+
line(nm(s.role, s.name, true), s.lastTs, s.preview, lampOf(s.status)) + '</button>';
|
|
351
|
+
};
|
|
352
|
+
var rest = MORE[c.key] ? talked.slice(SUB_FOLD).map(sub).join('') : '';
|
|
327
353
|
var subs = talked.length
|
|
328
|
-
? '<div class="subs">' + talked.
|
|
329
|
-
var sel = on && s.role === WITH;
|
|
330
|
-
return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
|
|
331
|
-
'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
|
|
332
|
-
goSpan('av', s.role, esc(s.label)) +
|
|
333
|
-
line(nm(s.role, s.name, true), s.lastTs, s.preview, lampOf(s.status)) + '</button>';
|
|
334
|
-
}).join('') + '</div>'
|
|
354
|
+
? '<div class="subs">' + talked.slice(0, SUB_FOLD).map(sub).join('') + bar + rest + '</div>'
|
|
335
355
|
: '';
|
|
336
356
|
var sel = on && !WITH;
|
|
337
357
|
return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' +
|
|
338
|
-
avatarOf(c
|
|
358
|
+
avatarOf(c) + line(title, c.lastTs, c.preview, lampOf(c.status)) +
|
|
339
359
|
'</button>' + subs;
|
|
340
360
|
};
|
|
341
361
|
|
|
@@ -354,6 +374,10 @@
|
|
|
354
374
|
var key = b.getAttribute('data-conv'), w = b.getAttribute('data-with') || '';
|
|
355
375
|
b.onclick = function () { clickItem(key, w); };
|
|
356
376
|
});
|
|
377
|
+
convsEl.querySelectorAll('[data-more]').forEach(function (b) {
|
|
378
|
+
var key = b.getAttribute('data-more');
|
|
379
|
+
b.onclick = function () { MORE[key] = !MORE[key]; renderConvs(); };
|
|
380
|
+
});
|
|
357
381
|
bindGo(convsEl);
|
|
358
382
|
};
|
|
359
383
|
|
|
@@ -416,8 +440,9 @@
|
|
|
416
440
|
// 名字下面那一行就是出身 —— 有父亲的直接写成「谁的什么」(可点过去), 不再另起一条重复。
|
|
417
441
|
// 分身 = 从父亲某个 session 节点 fork 出来的 (带着那一刻的上下文);
|
|
418
442
|
// 子 wizard = 父亲 spawn 的白板, 只有出身、没有继承。
|
|
419
|
-
|
|
420
|
-
|
|
443
|
+
// 人不挂身份行 —— 名字本身就够了。
|
|
444
|
+
var kind = r.kind === 'human' ? ''
|
|
445
|
+
: !r.parent ? 'wizard'
|
|
421
446
|
: r.forkedFrom
|
|
422
447
|
? '<span class="go" data-r="' + esc(r.parent.id) + '" title="从它的 session ' + esc(r.forkedFrom) + ' fork, 开局带着那一刻的上下文">⧉ .' + esc(r.parent.name) + ' 的分身 · @' + esc(r.forkedFrom.slice(0, 8)) + '</span>'
|
|
423
448
|
: '<span class="go" data-r="' + esc(r.parent.id) + '" title="它 spawn 的白板, 没有继承上下文">↳ .' + esc(r.parent.name) + ' 的子 wizard</span>';
|
|
@@ -433,7 +458,7 @@
|
|
|
433
458
|
$('#rb-who').innerHTML =
|
|
434
459
|
'<div class="id"><span class="av">' + esc(r.label) + '</span>' +
|
|
435
460
|
'<span class="l">' + nm(r.id, r.name) +
|
|
436
|
-
'<span class="k">' + kind + (r.kind === 'wizard' ? '<span class="st" id="rb-st"></span>' : '') + '</span
|
|
461
|
+
(kind ? '<span class="k">' + kind + (r.kind === 'wizard' ? '<span class="st" id="rb-st"></span>' : '') + '</span>' : '') + '</span></div>' +
|
|
437
462
|
(r.description ? '<p class="job">' + esc(r.description) + '</p>' : '') +
|
|
438
463
|
'<div class="facts">' + facts.join('') + '</div>';
|
|
439
464
|
paintStatus();
|
|
@@ -566,7 +591,7 @@
|
|
|
566
591
|
(sw ? '<span class="fi"><span class="fn">' + esc(nameOf(other)) + '</span>' + CHEVRON + '</span>' : '') + '</button>';
|
|
567
592
|
var av = mine ? '' : '<button class="av' + (canSwitch(m.from) ? ' go' : '') + '" data-r="' + esc(m.from) + '" title="' + esc(nameOf(m.from)) + '"' + (canSwitch(m.from) ? '' : ' disabled') + '>' + esc(m.fromLabel || roleLabel(m.from)) + '</button>';
|
|
568
593
|
return '<div class="mrow ' + (mine ? 'mine' : 'them') + '" data-id="' + esc(m.id) + '" data-turn="' + esc(m.turnId || m.id) + '" data-ts="' + m.ts + '"' +
|
|
569
|
-
(m.ping ? ' data-ping="1"' : '') + ' data-sig="' + esc(m.sig) + '" data-stale-at="' + (m.staleAt || 0) + '">' +
|
|
594
|
+
(m.ping ? ' data-ping="1" data-ping-who="' + esc(m.dir === 'in' ? m.toName : m.fromName) + '"' : '') + ' data-sig="' + esc(m.sig) + '" data-stale-at="' + (m.staleAt || 0) + '">' +
|
|
570
595
|
'<div class="mcol"><div class="mwho">' + av + who + '</div><div class="mb">' + m.html + '</div></div>' +
|
|
571
596
|
flip +
|
|
572
597
|
'</div>';
|
|
@@ -587,18 +612,24 @@
|
|
|
587
612
|
var run = [];
|
|
588
613
|
var flush = function (before) {
|
|
589
614
|
if (!run.length) { return; }
|
|
590
|
-
|
|
615
|
+
// 群窗里几个 wizard 同时挂机, 它们的 ping 会交错成一串 —— 按 wizard 分开计数, 不相加。
|
|
616
|
+
var seen = {}, per = {}, order = [];
|
|
591
617
|
run.forEach(function (r) {
|
|
592
618
|
var t = r.getAttribute('data-turn') || r.getAttribute('data-id');
|
|
593
|
-
|
|
619
|
+
var w = r.getAttribute('data-ping-who') || '';
|
|
620
|
+
if (seen[t]) { return; }
|
|
621
|
+
seen[t] = 1;
|
|
622
|
+
if (!per[w]) { per[w] = 0; order.push(w); }
|
|
623
|
+
per[w]++;
|
|
594
624
|
});
|
|
625
|
+
var tally = order.map(function (w) { return (w ? '.' + w + ' ' : '') + '×' + per[w]; }).join(' · ');
|
|
595
626
|
var t0 = Number(run[0].getAttribute('data-ts') || 0);
|
|
596
627
|
var t1 = Number(run[run.length - 1].getAttribute('data-ts') || 0);
|
|
597
628
|
var span = fmtHM(t0) + (fmtHM(t1) !== fmtHM(t0) ? ' – ' + fmtHM(t1) : '');
|
|
598
629
|
var d = document.createElement('details');
|
|
599
630
|
d.className = 'ping-fold';
|
|
600
631
|
d.open = PING_OPEN;
|
|
601
|
-
d.innerHTML = '<summary class="ping-sum"><span class="pb">🏓
|
|
632
|
+
d.innerHTML = '<summary class="ping-sum"><span class="pb">🏓 ' + esc(tally) + '</span>' +
|
|
602
633
|
'<span class="ps">保温 ping</span><span class="pt">' + esc(span) + '</span></summary>' +
|
|
603
634
|
'<div class="ping-body"></div>';
|
|
604
635
|
root.insertBefore(d, before);
|
|
@@ -624,7 +655,8 @@
|
|
|
624
655
|
return null;
|
|
625
656
|
};
|
|
626
657
|
|
|
627
|
-
|
|
658
|
+
// land: 换视角时由它来定位 (锚住一条旧消息), 返回 false 才照常吸底。
|
|
659
|
+
var loadMsgs = function (limit, land) {
|
|
628
660
|
var gen = S.gen;
|
|
629
661
|
if (!CONV) { inner.innerHTML = '<div class="empty">选一个会话</div>'; return Promise.resolve(); }
|
|
630
662
|
return api('api/msgs', viewParams(limit ? { limit: limit } : {})).then(function (d) {
|
|
@@ -642,6 +674,7 @@
|
|
|
642
674
|
render(inner);
|
|
643
675
|
foldPings(inner);
|
|
644
676
|
expireRows();
|
|
677
|
+
if (land && land()) return;
|
|
645
678
|
S.pinned = true; toBottom(true);
|
|
646
679
|
// CDN 字体/代码高亮加载完会改变高度, 再吸一次底。
|
|
647
680
|
setTimeout(function () { toBottom(); }, 60);
|
|
@@ -731,10 +764,11 @@
|
|
|
731
764
|
};
|
|
732
765
|
|
|
733
766
|
/** 视角或窗口变了: 摘要、正文、SSE 全部按新参数重来。 */
|
|
734
|
-
var refresh = function () {
|
|
767
|
+
var refresh = function (land) {
|
|
735
768
|
S.gen++;
|
|
736
769
|
syncUrl();
|
|
737
|
-
|
|
770
|
+
// 要做 FLIP 就把旧行留到新行到来 —— 中间闪一下「加载中」就量不到旧位置了。
|
|
771
|
+
if (!land) inner.innerHTML = '<div class="empty">加载中…</div>';
|
|
738
772
|
return api('api/role', viewParams()).then(function (d) {
|
|
739
773
|
if (!d.ok) {
|
|
740
774
|
document.body.innerHTML = '<div class="empty" style="padding:80px">' + esc(d.error || 'not found') + '</div>';
|
|
@@ -743,7 +777,7 @@
|
|
|
743
777
|
applyRole(d);
|
|
744
778
|
syncUrl();
|
|
745
779
|
if (VIEW === 'world' || VIEW === 'plan') { W.sel = ROLE; loadWorld(); }
|
|
746
|
-
return loadMsgs().then(connect);
|
|
780
|
+
return loadMsgs(undefined, land).then(connect);
|
|
747
781
|
});
|
|
748
782
|
};
|
|
749
783
|
|
|
@@ -773,14 +807,84 @@
|
|
|
773
807
|
var withBack = keep && WITH ? from : '';
|
|
774
808
|
ROLE = id; WITH = withBack; SESSION = '';
|
|
775
809
|
CONV = keep || (CONV.indexOf('p:') === 0 ? 'p:' + from : '');
|
|
776
|
-
|
|
777
|
-
|
|
810
|
+
if (same) { flipWindow(); return; }
|
|
811
|
+
var snap = VIEW === 'msgs' && !calm() ? snapRows() : {};
|
|
812
|
+
var kept = VIEW === 'msgs' ? keepBodies() : {};
|
|
813
|
+
refresh(function () { adoptBodies(kept); return landFlip(snap); });
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// ── 换视角的 FLIP: 按 msgid 记下每条气泡的位置与底色, 重包后从旧处滑到新处、旧色渐到新色 ──
|
|
817
|
+
// 位置动在 .mcol 上, 底色按行内第几颗气泡对上 —— 就地重包与整窗重拉两条路一样用。
|
|
818
|
+
// 只量视窗里的行 —— 屏外的动了也没人看, 长窗口里逐行 getComputedStyle 是真开销。
|
|
819
|
+
var BUB = '.bubble.mq, .say > .bubble';
|
|
820
|
+
var calm = function () { return window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches; };
|
|
821
|
+
var visibleRows = function () {
|
|
822
|
+
var vp = thread.getBoundingClientRect();
|
|
823
|
+
return [].slice.call(inner.querySelectorAll('.mrow:not(.mark)')).filter(function (r) {
|
|
824
|
+
var b = r.getBoundingClientRect();
|
|
825
|
+
return b.height && b.bottom > vp.top && b.top < vp.bottom;
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
var snapRows = function () {
|
|
829
|
+
return visibleRows().reduce(function (acc, r) {
|
|
830
|
+
var col = r.querySelector('.mcol');
|
|
831
|
+
acc[r.getAttribute('data-id')] = {
|
|
832
|
+
rect: col.getBoundingClientRect(),
|
|
833
|
+
bubs: [].slice.call(r.querySelectorAll(BUB)).map(function (b) { return getComputedStyle(b).backgroundColor; }),
|
|
834
|
+
};
|
|
835
|
+
return acc;
|
|
836
|
+
}, {});
|
|
837
|
+
};
|
|
838
|
+
var playFlip = function (snap) {
|
|
839
|
+
var ease = { duration: 340, easing: 'cubic-bezier(.2,.7,.2,1)' };
|
|
840
|
+
Object.keys(snap).forEach(function (id) {
|
|
841
|
+
var row = rowNode(id), was = snap[id];
|
|
842
|
+
var col = row && row.querySelector('.mcol');
|
|
843
|
+
if (!col || !col.animate) return;
|
|
844
|
+
var now = col.getBoundingClientRect();
|
|
845
|
+
var dx = was.rect.left - now.left, dy = was.rect.top - now.top;
|
|
846
|
+
if (dx || dy) col.animate([{ transform: 'translate(' + dx + 'px,' + dy + 'px)' }, { transform: 'none' }], ease);
|
|
847
|
+
var bubs = row.querySelectorAll(BUB);
|
|
848
|
+
was.bubs.forEach(function (from, i) {
|
|
849
|
+
var b = bubs[i], to = b && getComputedStyle(b).backgroundColor;
|
|
850
|
+
if (to && to !== from) b.animate([{ backgroundColor: from }, { backgroundColor: to }], ease);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
// 整窗重拉会把正文换成服务端的新片段 —— 过程框按「已有终句就收起」重新出生, 点开的工具
|
|
855
|
+
// 与取过的正文全丢, 看上去就是工具那一截在切换中突然没了。sig 没变的行把旧 .mb 整块搬回来,
|
|
856
|
+
// 与 flipWindow 同一个做法: 折叠 / 展开态、懒取的工具正文、已渲染的 markdown 一并保住。
|
|
857
|
+
var keepBodies = function () {
|
|
858
|
+
return [].slice.call(inner.querySelectorAll('.mrow:not(.mark)')).reduce(function (acc, r) {
|
|
859
|
+
var mb = r.querySelector('.mb');
|
|
860
|
+
if (mb) acc[r.getAttribute('data-id')] = { mb: mb, sig: r.getAttribute('data-sig') };
|
|
861
|
+
return acc;
|
|
862
|
+
}, {});
|
|
863
|
+
};
|
|
864
|
+
var adoptBodies = function (kept) {
|
|
865
|
+
Object.keys(kept).forEach(function (id) {
|
|
866
|
+
var row = rowNode(id), k = kept[id];
|
|
867
|
+
var nb = row && row.getAttribute('data-sig') === k.sig && row.querySelector('.mb');
|
|
868
|
+
if (nb) nb.replaceWith(k.mb);
|
|
869
|
+
});
|
|
870
|
+
};
|
|
871
|
+
// 整窗重拉后: 滚动锚在第一条新旧都在的消息上, 其余相对它滑动; 一条都不在 (换到了别的会话) 才整列淡入。
|
|
872
|
+
var landFlip = function (snap) {
|
|
873
|
+
var anchor = Object.keys(snap).filter(function (id) {
|
|
874
|
+
var r = rowNode(id); return r && r.getBoundingClientRect().height;
|
|
875
|
+
})[0];
|
|
876
|
+
if (!anchor) { inner.classList.remove('flipping'); void inner.offsetWidth; inner.classList.add('flipping'); return false; }
|
|
877
|
+
thread.scrollTop += rowNode(anchor).querySelector('.mcol').getBoundingClientRect().top - snap[anchor].rect.top;
|
|
878
|
+
S.pinned = atBottom();
|
|
879
|
+
playFlip(snap);
|
|
880
|
+
return true;
|
|
778
881
|
};
|
|
779
882
|
|
|
780
883
|
/** 同一窗口换视角: 拿手里的片段就地重包左右, 已渲染的正文整块搬过去; 侧栏交给 SSE 的首个 role 事件。 */
|
|
781
884
|
var flipWindow = function () {
|
|
782
885
|
S.gen++;
|
|
783
886
|
syncUrl();
|
|
887
|
+
var snap = calm() ? {} : snapRows();
|
|
784
888
|
unfoldPings(inner);
|
|
785
889
|
[].slice.call(inner.querySelectorAll('.mrow')).forEach(function (row) {
|
|
786
890
|
var m = S.frags[row.getAttribute('data-id')];
|
|
@@ -794,6 +898,7 @@
|
|
|
794
898
|
});
|
|
795
899
|
foldPings(inner);
|
|
796
900
|
expireRows();
|
|
901
|
+
playFlip(snap);
|
|
797
902
|
connect();
|
|
798
903
|
var gen = S.gen;
|
|
799
904
|
api('api/msgs', viewParams()).then(function (d) {
|