wezard 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/chat.css CHANGED
@@ -1,4 +1,4 @@
1
- /* Rolepage 外壳。消息正文 (turn 卡片) 的样式来自 detail-render 的 SHARED_CSS/TURN_CSS,
1
+ /* Rolepage 外壳。消息正文 (气泡 / 过程框) 的样式来自 detail-render 的 SHARED_CSS/TURN_CSS,
2
2
  由 /chat/app.css 拼在本文件之前 —— 这里管骨架、会话列表、左右气泡与页脚总账。
3
3
  视觉只押一处: 「视角」。我说的靠右、染成视角色; 别人说的靠左、带头像; 气泡对面
4
4
  那片空白是一道门 —— 点下去就换成对方的视角。其余一律安静。 */
@@ -211,13 +211,17 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
211
211
  .si .pv:empty { display: none }
212
212
 
213
213
  /* 执行中的 wizard: 头像角上一盏呼吸灯 (同关系图的 .wav .live)。 */
214
- .ci > .av, .si > .av { position: relative }
215
- .ci > .av .live, .si > .av .live {
214
+ .ci > .av { position: relative }
215
+ .ci > .av .live {
216
216
  position: absolute; right: 1px; bottom: -3px; width: 9px; height: 9px;
217
217
  border-radius: 50%; background: #1a7f37; border: 2px solid #fff;
218
218
  animation: pulse 1.4s infinite;
219
219
  }
220
- .si > .av .live { right: -1px; width: 8px; height: 8px }
220
+ /* 子项的头像太小, 角上挂不住一盏看得清的灯 —— 挪到名字右边, 与回复头上的 .tg-dot 同大。 */
221
+ .si .l1 .live {
222
+ flex: none; align-self: center; width: 7px; height: 7px;
223
+ border-radius: 50%; background: #1a7f37; animation: pulse 1.4s infinite;
224
+ }
221
225
 
222
226
  /* ── 右栏头: 这个群聊 / 私聊是什么 ───────────────────────────────── */
223
227
  .main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--card) }
@@ -227,13 +231,8 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
227
231
  }
228
232
  .chathead .back { display: none; border: 0; background: none; font-size: 22px; color: var(--mute); cursor: pointer; padding: 0 4px }
229
233
  .ch-who { display: flex; gap: 11px; align-items: center; min-width: 0; flex: 1 }
230
- .ch-who > .av:not(.mosaic) { font-size: 26px; line-height: 1; flex: none }
231
- .ch-who .l { min-width: 0; display: flex; flex-direction: column; gap: 1px }
232
234
  .ch-who .t { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
233
- .ch-who > .t { flex: none }
234
- .ch-who .tr { display: flex; align-items: center; gap: 6px; min-width: 0 }
235
- .ch-who .jump { flex: none; display: inline-flex; padding: 3px; border-radius: 5px; color: var(--mute) }
236
- .ch-who .jump:hover { color: var(--accent); background: var(--line) }
235
+ .ch-who > .t:not(:only-child) { flex: none }
237
236
  .ch-who .sub { font-size: 11.5px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
238
237
  .ch-acts { display: flex; gap: 8px; align-items: center; flex: none; font-size: 12px; color: var(--mute) }
239
238
  .vb {
@@ -276,10 +275,11 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
276
275
  /* 卡片里那份时刻是详情页要的, 这里已经写在 .mwho 上了 —— 同一条消息不报两次时间。 */
277
276
  .mb .tg-time { display: none }
278
277
 
279
- /* 入消息 (问句) 是一颗气泡; 出消息 (回复) 是一张带工具细节的卡片 —— 两者都按视角着色。 */
280
- .mb > .bubble.mq { border-radius: 14px; border-color: var(--line) }
278
+ /* 入消息 (问句) 是一颗气泡; 出消息 (回复) 是一串气泡, 中间夹着虚线的过程框 ——
279
+ 没有外层卡片, 直接排在对话底上。 */
280
+ .mb > .bubble.mq { border-radius: 14px }
281
+ .mb :is(.bubble.mq, .say > .bubble) { border-color: var(--line) }
281
282
  .mb .mq .mmeta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; padding: 8px 14px 0; font-size: 11px; color: var(--faint) }
282
- .mb .mq .md-body { padding: 10px 14px }
283
283
  .mb .mq .mmeta + .md-body { padding-top: 4px }
284
284
  .mchip { border-radius: 4px; padding: 0 5px; background: #f1f3f6; color: var(--mute) }
285
285
  .mchip.priv { color: var(--priv); background: #9a5b1014 }
@@ -287,24 +287,14 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
287
287
  .mchip.graph { color: #6639ba; background: #8250df12 }
288
288
  .mchip.sys { color: #0a7d6b; background: #0a7d6b12 }
289
289
 
290
- /* 自己发出的那条: 整块就是视角色。TURN_CSS 里写死的白卡 / 灰底 (嵌套气泡、
291
- 代码块、折叠头) 会在蓝底上显成一张张贴纸 —— 这里把它们收回到气泡自己的
292
- 底色上, 一条消息读起来是一块颜色, 需要再沉一档的才用 --mine-soft。 */
293
- .mrow.mine .mb > .bubble.mq, .mrow.mine .mb > .turn-group { background: var(--mine); border-color: var(--mine-line) }
294
- .mrow.mine .mb .bubble, .mrow.mine .mb .typing { background: transparent; border-color: var(--mine-line) }
295
- .mrow.mine .mb .bubble.user, .mrow.mine .mb .bubble.approval,
296
- .mrow.mine .mb .md-body code, .mrow.mine .mb .md-body pre,
297
- .mrow.mine .mb details:not(.tool-call) > summary,
298
- .mrow.mine .mb .tool-body section > h2, .mrow.mine .mb .diff-meta,
299
- .mrow.mine .mb .tool-call > summary:hover { background: var(--mine-soft); border-color: var(--mine-line) }
300
- .mrow.mine .mb .md-body pre code { background: transparent }
301
- /* 展开的工具正文 (command / diff / 文件内容) 与嵌在父轮里的 subagent 卡片都是裸
302
- section, 吃的是 TURN_CSS 的白底 —— 同样收回气泡底色; subagent 的绿色左边线留着。 */
303
- .mrow.mine .mb .tool-body section { background: transparent; border-color: var(--mine-line) }
304
- .mrow.mine .mb .turn-group .turn-group { background: transparent; box-shadow: none; border-color: var(--mine-line) }
305
- .mrow.mine .mb .turn-group .turn-group.subagent { border-left-color: #1a7f3733 }
306
- .mrow.mine .mb .bubble details, .mrow.mine .mb .tg-head { border-color: var(--mine-line) }
307
- .mrow.mine .mb .tool-body { border-left-color: var(--mine-line) }
290
+ /* 自己说的话染成视角色 —— 只染气泡; 过程框是底上的一道虚线, 不跟着染。气泡里
291
+ TURN_CSS 写死的灰底 (行内代码 / 代码块) 沉一档到 --mine-soft, 免得显成贴纸。 */
292
+ .mrow.mine .mb :is(.bubble.mq, .say > .bubble) { background: var(--mine); border-color: var(--mine-line) }
293
+ .mrow.mine .mb :is(.bubble.mq, .say) .md-body :is(code, pre) { background: var(--mine-soft); border-color: var(--mine-line) }
294
+ .mrow.mine .mb :is(.bubble.mq, .say) .md-body pre code { background: transparent }
295
+ /* 气泡跟着行靠右, 时刻翻到气泡左边; 嵌在过程框里的子 agent 不翻 —— 那不是我说的。 */
296
+ .mrow.mine .mb > .turn-group > .bubbles > .say { align-self: flex-end; flex-direction: row-reverse }
297
+ .mrow.mine .mb > .turn-group > .tg-head { justify-content: flex-end }
308
298
  /* 工具正文是展开时才取的 (chat.js loadTool) —— 空壳期间说一句在干什么。 */
309
299
  .tool-body[data-lazy-turn]:empty::before { content: "加载中…"; color: #8c959f; font-size: 12px }
310
300
  .tool-body[data-lazy-turn][data-err]:empty::before { content: "加载失败 · 收起再展开重试"; color: #cf222e }
@@ -342,38 +332,27 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
342
332
  .flip { transition: none }
343
333
  }
344
334
 
345
- /* ── turn 分组 ──────────────────────────────────────────────────── */
346
- /* section 的默认样式给了白底+边框, 但没有内边距 —— 气泡/工具行会直接贴着卡片
347
- 边缘, 所以这里补上一轮对话自己的呼吸空间。 */
335
+ /* ── 一轮回复 ───────────────────────────────────────────────────── */
336
+ /* 只是一根竖轴, 不是卡片: section 的默认白底 / 边框在这里收掉。本轮的账 (模型 /
337
+ token / 耗时) 压成末尾一行小字 —— 它是这条消息的脚注, 不是标题。 */
348
338
  .turn-group {
349
- display: flex; flex-direction: column; gap: 10px;
350
- padding: 14px 16px 16px; border-radius: 12px; border-color: #e1e6ec;
351
- box-shadow: 0 1px 2px #1f23280a;
339
+ display: flex; flex-direction: column; gap: 8px;
340
+ padding: 0; border: 0; border-radius: 0; background: none;
352
341
  }
353
- .tg-head {
354
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
355
- padding-bottom: 6px; border-bottom: 1px dashed #d8dee4;
342
+ .tg-head { order: 9; display: flex; align-items: center; gap: 4px 10px; flex-wrap: wrap; padding: 0 2px }
343
+ .tg-head .chip.model, .tg-head .chip.tier, .tg-tok {
344
+ padding: 0; border: 0; border-radius: 0; background: none; font-weight: 400;
345
+ font-size: 10.5px; color: var(--faint); font-family: ui-monospace, Menlo, monospace;
356
346
  }
357
- .tg-dot { width: 7px; height: 7px; border-radius: 50%; background: #b1bac4; flex: none }
358
- .tg-dot.live { background: #1a7f37; animation: pulse 1.4s infinite }
347
+ .tg-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; display: none }
348
+ .tg-dot.live { display: block; background: #1a7f37; animation: pulse 1.4s infinite }
359
349
  .tg-time { font-size: 11.5px; color: #8c959f; font-family: ui-monospace, Menlo, monospace }
360
- .tg-tok {
361
- font-size: 11px; color: #57606a; background: #f1f3f6; border-radius: 9px;
362
- padding: 1px 7px; font-family: ui-monospace, Menlo, monospace;
363
- }
364
- .turn-group .bubbles { margin-top: 0 }
365
- /* 上下文断点 (/clear、/new、会话轮换): 断点条来自 TURN_CSS, 这里再给整张卡片
366
- 多留一口气 + 描边, 让"这一轮读不到上一轮"在滚动中一眼可辨。 */
367
- .turn-group.cut { margin-top: 12px }
368
- .turn-group.cut-clear, .turn-group.cut-new {
369
- border-color: #d4a72c66; box-shadow: 0 1px 2px #1f23280a, 0 0 0 3px #d4a72c14;
370
- }
371
- /* graph 派的一轮: 卡片本身也着色, 滚动时不必读归因条就知道这不是人发的。 */
372
- .turn-group.graph { border-color: #8250df44 }
373
350
 
374
351
  /* ── 🏓 keepalive 折叠 ──────────────────────────────────────────────
375
352
  保温 ping 不是对话, 但它是真花销 —— 所以留在时间轴上, 只是收成一行虚线。 */
376
- .ping-fold { border: 1px dashed #d8dee4; border-radius: 10px; background: #fbfcfd }
353
+ /* summary 的灰底 / 底线是 SHARED_CSS 里通用 details 的 —— 圆角得自己裁, 底线只在展开时才是分割线。 */
354
+ .ping-fold { border: 1px dashed #d8dee4; border-radius: 10px; background: #fbfcfd; overflow: hidden }
355
+ .ping-fold:not([open]) > .ping-sum { border-bottom: 0 }
377
356
  .ping-fold > .ping-sum {
378
357
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
379
358
  cursor: pointer; list-style: none; font-size: 11.5px; color: #8c959f;
@@ -392,8 +371,6 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
392
371
  .ping-fold .ping-body {
393
372
  display: flex; flex-direction: column; gap: 10px; padding: 2px 10px 10px;
394
373
  }
395
- /* 展开后的 ping 轮次压一档: 它们是背景噪声, 不该和真对话卡片一样醒目。 */
396
- .ping-fold .turn-group { box-shadow: none }
397
374
  .ping-fold .mcol { max-width: 100% }
398
375
  .ping-fold .flip { display: none }
399
376
 
@@ -408,7 +385,6 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
408
385
  .thread { padding: 12px }
409
386
  .mcol { max-width: calc(86% + var(--av)) }
410
387
  .flip { min-width: 16px; padding: 0 4px; font-size: 0 }
411
- .turn-group { padding: 12px }
412
388
  .thread { padding-bottom: calc(12px + env(safe-area-inset-bottom)) }
413
389
  .usage { padding: 8px 12px; gap: 6px 14px }
414
390
  .usage .u-kvs, .usage .leg { flex-wrap: wrap }
@@ -729,5 +705,5 @@ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
729
705
  会话标题条与页脚总账只属于对话视图。 */
730
706
  .pane { flex: 1; min-height: 0; display: flex; flex-direction: column; background: #fafbfc }
731
707
 
732
- /* 回复卡片里的工具行是 nowrap 的 —— 不裁一刀会把整列撑出横向滚动。 */
708
+ /* 过程框里的工具行是 nowrap 的 —— 不裁一刀会把整列撑出横向滚动。 */
733
709
  .mb > .turn-group { overflow: hidden }
package/web/chat.js CHANGED
@@ -192,6 +192,12 @@
192
192
  for (var i = 0; i < el.children.length; i++) if (el.children[i].classList.contains(cls)) return el.children[i];
193
193
  return null;
194
194
  };
195
+ // 一轮回复 (.tg-head) 与过程框 (.steps-head) 是同一副骨架: 头 + .bubbles。
196
+ var headOf = function (el) { return childBy(el, 'tg-head') || childBy(el, 'steps-head'); };
197
+ var swapHead = function (ex, nc) {
198
+ var eh = headOf(ex), nh = headOf(nc);
199
+ if (eh && nh) eh.innerHTML = nh.innerHTML;
200
+ };
195
201
  var reconcile = function (cur, next) {
196
202
  var open = snapOpen(cur), existing = {};
197
203
  Array.prototype.forEach.call(cur.children, function (c) {
@@ -203,8 +209,7 @@
203
209
  if (ex.getAttribute('data-sig') === nc.getAttribute('data-sig')) return ex;
204
210
  var eb = childBy(ex, 'bubbles'), nb = childBy(nc, 'bubbles');
205
211
  if (eb && nb) {
206
- var eh = childBy(ex, 'tg-head'), nh = childBy(nc, 'tg-head');
207
- if (eh && nh) eh.innerHTML = nh.innerHTML;
212
+ swapHead(ex, nc);
208
213
  reconcile(eb, nb);
209
214
  ex.setAttribute('data-sig', nc.getAttribute('data-sig') || '');
210
215
  return ex;
@@ -249,6 +254,14 @@
249
254
  if (d.open && d.classList && d.classList.contains('tool-call')) loadTool(d);
250
255
  }, true);
251
256
 
257
+ // 过程框的收起: 框头与框内空白是开关; 落在某一行工具上的点击归那一行自己。
258
+ // 收起态只是节点上的一个 class —— reconcile 认键留节点, 往框里添调用时它跟着留下。
259
+ inner.addEventListener('click', function (e) {
260
+ var s = e.target.closest && e.target.closest('.steps');
261
+ if (!s) return;
262
+ if (e.target.closest('.steps-head') || e.target === s || e.target === childBy(s, 'bubbles')) s.classList.toggle('folded');
263
+ });
264
+
252
265
  // ── 左栏: 当前 role 的名片 + 会话列表 ──
253
266
  var convOf = function (key) { return R.convs.filter(function (c) { return c.key === key; })[0]; };
254
267
 
@@ -283,8 +296,8 @@
283
296
  ms.map(function (m) { return '<i>' + esc(m.label || roleLabel(m.role)) + '</i>'; }).join('') + '</span>';
284
297
  };
285
298
 
286
- var line = function (title, ts, pv) {
287
- return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' +
299
+ var line = function (title, ts, pv, lamp) {
300
+ return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' + (lamp || '') +
288
301
  '<span class="ts">' + esc(fmtAgo(ts)) + '</span></span>' +
289
302
  '<span class="pv">' + esc(pv) + '</span></span>';
290
303
  };
@@ -299,8 +312,8 @@
299
312
  var sel = s.role === WITH;
300
313
  return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
301
314
  'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
302
- goSpan('av', s.role, esc(s.label) + lampOf(s.status)) +
303
- line(nm(s.role, s.name, true), s.lastTs, s.preview) + '</button>';
315
+ goSpan('av', s.role, esc(s.label)) +
316
+ line(nm(s.role, s.name, true), s.lastTs, s.preview, lampOf(s.status)) + '</button>';
304
317
  }).join('') + '</div>'
305
318
  : '';
306
319
  var sel = on && !WITH;
@@ -485,14 +498,6 @@
485
498
  putUsage($('#ch-usage'), !wiz && WITH && VIEW === 'msgs' ? R.winStats : null);
486
499
  };
487
500
 
488
- // 只有真群 (`chat:<roomid>`) 才有跳转: `user:` 是人与 bot 的单聊, userid 指向的是人自己而不是这段会话。
489
- var JUMP = '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M9.5 2.5h4v4M13.5 2.5 7.5 8.5M11.5 9.5v3a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
490
- var openInWecom = function (base) {
491
- return base.indexOf('chat:') === 0
492
- ? '<a class="jump" href="wxwork://openconversation?roomid=' + encodeURIComponent(base.slice(5)) + '" title="在企业微信中打开这个群聊">' + JUMP + '</a>'
493
- : '';
494
- };
495
-
496
501
  // ── 右栏头: 这个群聊 / 私聊是什么 (关系 / 日程视图时是视图名) ──
497
502
  var renderHead = function () {
498
503
  var who = $('#ch-who'), acts = $('#ch-acts');
@@ -505,16 +510,7 @@
505
510
  }
506
511
  var c = convOf(CONV);
507
512
  if (!c) { who.innerHTML = ''; acts.innerHTML = ''; return; }
508
- if (c.kind === 'wizard') {
509
- who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name, true) + '</span>' +
510
- '<span class="sub">私聊 · ' + c.count + ' 条</span></span>';
511
- } else {
512
- var ms = membersOf(c);
513
- who.innerHTML = avatarOf(c) + '<span class="l"><span class="tr"><span class="t">' + esc(c.name) + '</span>' + openInWecom(c.base) + '</span>' +
514
- '<span class="sub">群聊 · ' + ms.length + ' 位成员 · ' +
515
- ms.slice(0, 8).map(function (m) { return esc(m.label || roleLabel(m.role)); }).join('') +
516
- (ms.length > 8 ? '…' : '') + '</span></span>';
517
- }
513
+ who.innerHTML = '<span class="t">' + (c.kind === 'wizard' ? nm(c.peer, c.name, true) : esc(c.name)) + '</span>';
518
514
  acts.innerHTML = WITH
519
515
  ? '<span class="with">只看我与 ' + nm(WITH, '', true) + '</span><button class="vb" id="ch-all">看全部</button>'
520
516
  : '';
@@ -654,8 +650,7 @@
654
650
  var ec = eb && eb.firstElementChild, nc = nb && nb.firstElementChild;
655
651
  var ebb = ec && childBy(ec, 'bubbles'), nbb = nc && childBy(nc, 'bubbles');
656
652
  if (ebb && nbb) {
657
- var eh = childBy(ec, 'tg-head'), nh = childBy(nc, 'tg-head');
658
- if (eh && nh) eh.innerHTML = nh.innerHTML;
653
+ swapHead(ec, nc);
659
654
  reconcile(ebb, nbb);
660
655
  ['data-sig', 'data-stale-at'].forEach(function (a) { cur.setAttribute(a, next.getAttribute(a)); });
661
656
  } else {