wezard 2.0.2 → 2.0.3

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.
@@ -38,8 +38,8 @@ export const renderChatPage = () => `<!doctype html><html lang="zh-CN"><head><me
38
38
  <aside class="side">
39
39
  <header class="rolebar" id="rolebar">
40
40
  <div class="who" id="rb-who"></div>
41
- <div class="acts" id="rb-acts"></div>
42
41
  </header>
42
+ <div class="acts" id="rb-acts"></div>
43
43
  <nav class="convs" id="convs" aria-label="会话"></nav>
44
44
  <footer class="sidefoot" id="rb-foot" hidden><div id="rb-sess"></div></footer>
45
45
  </aside>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wezard",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Bridge Claude Code and WeCom: PreToolUse approval cards + bidirectional tmux session mirror with image paste",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/web/chat.css CHANGED
@@ -43,7 +43,7 @@ button { font: inherit; color: inherit }
43
43
  display: flex; flex-direction: column; min-height: 0;
44
44
  }
45
45
 
46
- /* ── 左栏顶: 当前 role 的名片 (身份 + 关系/日程入口) ──── */
46
+ /* ── 左栏顶: 当前 role 的名片 (身份); 关系/日程入口是名片下、会话列表上的一排 ──── */
47
47
  .rolebar {
48
48
  position: relative; flex: none; padding: 16px 16px 14px; border-bottom: 1px solid var(--line);
49
49
  display: flex; flex-direction: column; gap: 8px; background: var(--card);
@@ -68,10 +68,12 @@ button { font: inherit; color: inherit }
68
68
  .rolebar .facts span { white-space: nowrap }
69
69
  .rolebar .go { cursor: pointer; color: var(--accent) }
70
70
  .rolebar .go:hover { text-decoration: underline }
71
- .rolebar .acts { display: flex; gap: 6px; flex-wrap: wrap; align-items: center }
72
- .rolebar .acts:empty { display: none }
71
+ .acts { flex: none; display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--line) }
72
+ .acts:empty { display: none }
73
+ /* 旧外壳 (daemon 未重启) 里它还在名片内 —— 那里不需要自己的内边距与分隔线。 */
74
+ .rolebar .acts { padding: 0; border: 0 }
73
75
  .bd {
74
- font-size: 11px; line-height: 18px; padding: 0 8px; border: 0; border-radius: 9px; cursor: pointer;
76
+ flex: 1; font-size: 12.5px; line-height: 28px; padding: 0 10px; border: 0; border-radius: 8px; cursor: pointer;
75
77
  background: #f1f3f6; color: var(--mute); white-space: nowrap;
76
78
  }
77
79
  .bd:hover { color: var(--accent) }
@@ -155,11 +157,12 @@ button { font: inherit; color: inherit }
155
157
  .mosaic.n3, .mosaic.n4 { grid-template: 1fr 1fr / 1fr 1fr }
156
158
  .mosaic.n3 i:first-child { grid-row: span 2 }
157
159
 
158
- .convs { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 8px 16px }
160
+ .convs { flex: 1; min-height: 0; overflow-y: auto; padding: 2px 8px 16px }
159
161
  .convs h2 {
160
162
  margin: 14px 8px 4px; font-size: 12px; font-weight: 600; color: var(--mute);
161
163
  display: flex; align-items: baseline; gap: 6px;
162
164
  }
165
+ .convs h2:first-child { margin-top: 6px }
163
166
  .convs h2 span { font-weight: 400; color: var(--faint); font-variant-numeric: tabular-nums }
164
167
 
165
168
  .ci {
@@ -179,12 +182,14 @@ button { font: inherit; color: inherit }
179
182
  .ci .pv { font-size: 12px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
180
183
  .ci .pv:empty { display: none }
181
184
  .ci .tag { font-size: 10.5px; border-radius: 4px; padding: 0 4px; flex: none }
182
- /* 选中的 role 项上的换视角开关。 */
183
- .sw {
184
- flex: none; align-self: center; display: inline-grid; place-items: center;
185
- width: 20px; height: 20px; border-radius: 5px; color: var(--accent); cursor: pointer;
186
- }
187
- .sw:hover { background: #2f55c91a }
185
+ /* 头像与名字是进入那个 role 视角的入口 —— 侧栏项、会话头、消息行同一种读法。
186
+ 它们嵌在整行那颗按钮里, 所以悬停态必须压过行: 指到入口上时行自己的悬停底色
187
+ 让位 (:has), 否则读起来像是要选中这一行。z-index 保证没有兄弟盖住命中区。 */
188
+ .nm.go, .av.go { position: relative; z-index: 1; cursor: pointer }
189
+ .nm.go:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px }
190
+ span.av.go { border-radius: 8px; transition: background .12s, box-shadow .12s }
191
+ span.av.go:hover { background: #2f55c924; box-shadow: 0 0 0 3px #2f55c924 }
192
+ .ci:not(.on):has(.go:hover), .si:not(.on):has(.go:hover) { background: transparent }
188
193
  .ci .tag.priv { color: var(--priv); background: #9a5b1014 }
189
194
 
190
195
  /* 群里的其他 role —— 挂在群下, 与会话项同一种读法 (名字 · 时刻 / 最近一句), 只压一档。
@@ -286,9 +291,15 @@ button { font: inherit; color: inherit }
286
291
  .mrow.mine .mb .bubble, .mrow.mine .mb .typing, .mrow.mine .mb .stats { background: transparent; border-color: var(--mine-line) }
287
292
  .mrow.mine .mb .bubble.user, .mrow.mine .mb .bubble.approval,
288
293
  .mrow.mine .mb .md-body code, .mrow.mine .mb .md-body pre,
289
- .mrow.mine .mb .tile, .mrow.mine .mb details > summary,
294
+ .mrow.mine .mb .tile, .mrow.mine .mb details:not(.tool-call) > summary,
295
+ .mrow.mine .mb .tool-body section > h2, .mrow.mine .mb .diff-meta,
290
296
  .mrow.mine .mb .tool-call > summary:hover { background: var(--mine-soft); border-color: var(--mine-line) }
291
297
  .mrow.mine .mb .md-body pre code { background: transparent }
298
+ /* 展开的工具正文 (command / diff / 文件内容) 与嵌在父轮里的 subagent 卡片都是裸
299
+ section, 吃的是 TURN_CSS 的白底 —— 同样收回气泡底色; subagent 的绿色左边线留着。 */
300
+ .mrow.mine .mb .tool-body section { background: transparent; border-color: var(--mine-line) }
301
+ .mrow.mine .mb .turn-group .turn-group { background: transparent; box-shadow: none; border-color: var(--mine-line) }
302
+ .mrow.mine .mb .turn-group .turn-group.subagent { border-left-color: #1a7f3733 }
292
303
  .mrow.mine .mb .bubble details, .mrow.mine .mb .tg-head { border-color: var(--mine-line) }
293
304
  .mrow.mine .mb .tool-body { border-left-color: var(--mine-line) }
294
305
  /* 工具正文是展开时才取的 (chat.js loadTool) —— 空壳期间说一句在干什么。 */
@@ -316,7 +327,7 @@ button { font: inherit; color: inherit }
316
327
  .flip:hover, .flip:focus-visible { background: #2f55c90d; color: var(--accent) }
317
328
  .flip[disabled] { cursor: default; background: transparent !important; color: transparent !important }
318
329
  .mwho .av.go { cursor: pointer }
319
- .mwho .av.go:hover { border-color: var(--accent) }
330
+ .mwho .av.go:hover { border-color: var(--accent); background: #2f55c914 }
320
331
 
321
332
  .mrow.mark { justify-content: center }
322
333
  .mrow.mark > * { flex: 1; max-width: 520px }
package/web/chat.js CHANGED
@@ -128,11 +128,23 @@
128
128
  return k.label || (n && n.label) || ({ human: '👤', task: '⏰', system: '🧙' })[kindOf(id)] || '🧙';
129
129
  };
130
130
  // `.name` 的点由 CSS 画 —— 人与定时任务没有地址, 不画点。
131
- var nm = function (id, name) {
132
- return '<span class="nm ' + kindOf(id) + '">' + esc(name || roleName(id)) + '</span>';
133
- };
134
131
  var nameOf = function (id) { return (kindOf(id) === 'wizard' ? '.' : '') + roleName(id); };
135
132
  var canSwitch = function (id) { return !!id && kindOf(id) !== 'task' && kindOf(id) !== 'system' && id !== 'human:'; };
133
+ // 头像与名字就是进入那个 role 视角的入口 —— 侧栏项、会话头、消息行同一种读法。
134
+ // 是 <span> 不是 <button>: 侧栏里它嵌在整行那颗按钮里。
135
+ var goSpan = function (cls, id, inner) {
136
+ var on = canSwitch(id) && id !== ROLE;
137
+ return '<span class="' + cls + (on ? ' go" data-r="' + esc(id) + '" title="' + esc('切到 ' + nameOf(id) + ' 的视角') : '') + '">' + inner + '</span>';
138
+ };
139
+ var nm = function (id, name, go) {
140
+ var cls = 'nm ' + kindOf(id), txt = esc(name || roleName(id));
141
+ return go ? goSpan(cls, id, txt) : '<span class="' + cls + '">' + txt + '</span>';
142
+ };
143
+ var bindGo = function (root, sel) {
144
+ root.querySelectorAll(sel || '.go[data-r]').forEach(function (b) {
145
+ b.onclick = function (e) { e.stopPropagation(); switchRole(b.getAttribute('data-r')); };
146
+ });
147
+ };
136
148
 
137
149
  // ── markdown 渲染 (只对未渲染过的节点做, 复用节点不重绘) ──
138
150
  var md = null;
@@ -265,29 +277,21 @@
265
277
  };
266
278
 
267
279
  var avatarOf = function (c, extra) {
268
- if (c.kind !== 'group') return '<span class="av">' + esc(c.label) + (extra || '') + '</span>';
280
+ if (c.kind !== 'group') return goSpan('av', c.peer, esc(c.label) + (extra || ''));
269
281
  var ms = membersOf(c).slice(0, 4);
270
282
  return '<span class="av mosaic n' + ms.length + '" aria-hidden="true">' +
271
283
  ms.map(function (m) { return '<i>' + esc(m.label || roleLabel(m.role)) + '</i>'; }).join('') + '</span>';
272
284
  };
273
285
 
274
- // 选中的那一项若是个 role (私聊对端 / 群里的某人), 行内给一个换视角的开关。
275
- // 不能是 <button>: 它嵌在整行那颗按钮里。
276
- var SWAP = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3 5.5h9M9.5 3l2.5 2.5L9.5 8M13 10.5H4M6.5 8 4 10.5 6.5 13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
277
- var swapOf = function (role) {
278
- return canSwitch(role) && role !== ROLE
279
- ? '<span class="sw" role="button" tabindex="0" data-sw="' + esc(role) + '" title="' + esc('切到 ' + nameOf(role) + ' 的视角') + '">' + SWAP + '</span>'
280
- : '';
281
- };
282
- var line = function (title, sw, ts, pv) {
283
- return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' + sw +
286
+ var line = function (title, ts, pv) {
287
+ return '<span class="b"><span class="l1"><span class="t">' + title + '</span>' +
284
288
  '<span class="ts">' + esc(fmtAgo(ts)) + '</span></span>' +
285
289
  '<span class="pv">' + esc(pv) + '</span></span>';
286
290
  };
287
291
 
288
292
  var convItem = function (c) {
289
293
  var on = c.key === CONV;
290
- var title = c.kind === 'wizard' ? nm(c.peer, c.name) : '<span class="nm chat">' + esc(c.name) + '</span>';
294
+ var title = c.kind === 'wizard' ? nm(c.peer, c.name, true) : '<span class="nm chat">' + esc(c.name) + '</span>';
291
295
  // 只列与我有往来的: 在群里但没和我说过话的人, 点进去也是空的。
292
296
  var talked = on ? c.subs.filter(function (s) { return s.count; }) : [];
293
297
  var subs = talked.length
@@ -295,13 +299,13 @@
295
299
  var sel = s.role === WITH;
296
300
  return '<button class="si' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '" data-with="' + esc(s.role) + '" ' +
297
301
  'title="' + esc('我与 ' + nameOf(s.role) + ' 在这里的 ' + s.count + ' 条往来') + '">' +
298
- '<span class="av">' + esc(s.label) + lampOf(s.status) + '</span>' +
299
- line(nm(s.role, s.name), sel ? swapOf(s.role) : '', s.lastTs, s.preview) + '</button>';
302
+ goSpan('av', s.role, esc(s.label) + lampOf(s.status)) +
303
+ line(nm(s.role, s.name, true), s.lastTs, s.preview) + '</button>';
300
304
  }).join('') + '</div>'
301
305
  : '';
302
306
  var sel = on && !WITH;
303
307
  return '<button class="ci' + (sel ? ' on' : '') + '" data-conv="' + esc(c.key) + '">' +
304
- avatarOf(c, lampOf(c.status)) + line(title, sel && c.kind === 'wizard' ? swapOf(c.peer) : '', c.lastTs, c.preview) +
308
+ avatarOf(c, lampOf(c.status)) + line(title, c.lastTs, c.preview) +
305
309
  '</button>' + subs;
306
310
  };
307
311
 
@@ -319,11 +323,7 @@
319
323
  convsEl.querySelectorAll('[data-conv]').forEach(function (b) {
320
324
  b.onclick = function () { selectConv(b.getAttribute('data-conv'), b.getAttribute('data-with') || ''); };
321
325
  });
322
- convsEl.querySelectorAll('[data-sw]').forEach(function (x) {
323
- var go = function (e) { e.stopPropagation(); switchRole(x.getAttribute('data-sw')); };
324
- x.onclick = go;
325
- x.onkeydown = function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(e); } };
326
- });
326
+ bindGo(convsEl);
327
327
  };
328
328
 
329
329
  var shortCwd = function (p) {
@@ -378,7 +378,7 @@
378
378
  if (SESS_OPEN && e.key === 'Escape') setSessOpen(false);
379
379
  });
380
380
 
381
- // 名片: 身份 · 出身 · cwd · 出生 · 关系 / 日程入口。
381
+ // 名片: 身份 · 出身 · cwd · 出生; 名片下面一排是关系 / 日程入口。
382
382
  var renderRole = function () {
383
383
  var r = R.role;
384
384
  if (!r) return;
@@ -413,7 +413,7 @@
413
413
  $('#rb-foot').hidden = R.sessions.length < 2;
414
414
  bindSessPicker();
415
415
  var acts = [];
416
- // 入口只在有东西可看时出现, 且只是一枚徽标 —— 名片的主角是身份, 不是按钮。
416
+ // 入口只在有东西可看时出现 —— 挂在名片下、会话列表上, 不挤进名片: 名片的主角是身份。
417
417
  if (R.relations) acts.push('<button class="bd' + (VIEW === 'world' ? ' on' : '') + '" data-view="world">关系图</button>');
418
418
  if (R.schedules) acts.push('<button class="bd' + (VIEW === 'plan' ? ' on' : '') + '" data-view="plan">日程<b>' + R.schedules + '</b></button>');
419
419
  $('#rb-acts').innerHTML = acts.join('');
@@ -497,7 +497,7 @@
497
497
  var c = convOf(CONV);
498
498
  if (!c) { who.innerHTML = ''; acts.innerHTML = ''; return; }
499
499
  if (c.kind === 'wizard') {
500
- who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name) + '</span>' +
500
+ who.innerHTML = avatarOf(c) + '<span class="l"><span class="t">' + nm(c.peer, c.name, true) + '</span>' +
501
501
  '<span class="sub">私聊 · ' + c.count + ' 条</span></span>';
502
502
  } else {
503
503
  var ms = membersOf(c);
@@ -507,8 +507,9 @@
507
507
  (ms.length > 8 ? '…' : '') + '</span></span>';
508
508
  }
509
509
  acts.innerHTML = WITH
510
- ? '<span class="with">只看我与 ' + nm(WITH) + '</span><button class="vb" id="ch-all">看全部</button>'
510
+ ? '<span class="with">只看我与 ' + nm(WITH, '', true) + '</span><button class="vb" id="ch-all">看全部</button>'
511
511
  : '';
512
+ bindGo(who); bindGo(acts);
512
513
  var x = $('#ch-all');
513
514
  if (x) x.onclick = function () { selectConv(CONV, ''); };
514
515
  };
@@ -529,11 +530,11 @@
529
530
  // 看 key 而不是会话列表: 换视角就地重包时, 列表还是上一个 role 的。
530
531
  var group = CONV.indexOf('p:') !== 0;
531
532
  // 群里我不是收信方的那条 (X → Y), 头上写清是说给谁的。
532
- var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName) + '</span>' : '';
533
+ var to = !mine && m.to !== ROLE && group ? '<span class="to">→ ' + nm(m.to, m.toName, true) + '</span>' : '';
533
534
  var priv = !m.channel && group ? '<span class="ch priv">私聊</span>' : '';
534
535
  var who = mine
535
- ? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName) : '') + '</span>' + stamp(m.ts)
536
- : nm(m.from, m.fromName) + to + priv + stamp(m.ts);
536
+ ? '<span class="to">' + (m.to && m.to !== 'human:' ? '→ ' + nm(m.to, m.toName, true) : '') + '</span>' + stamp(m.ts)
537
+ : nm(m.from, m.fromName, true) + to + priv + stamp(m.ts);
537
538
  var sw = canSwitch(other);
538
539
  var flip = '<button class="flip" data-r="' + esc(other) + '"' + (sw ? '' : ' disabled tabindex="-1"') +
539
540
  ' aria-label="' + esc(sw ? '切到 ' + nameOf(other) + ' 的视角' : '') + '">' +
@@ -590,9 +591,7 @@
590
591
  };
591
592
 
592
593
  var bindRow = function (el) {
593
- el.querySelectorAll('.flip[data-r]:not([disabled]), .av.go').forEach(function (b) {
594
- b.onclick = function () { switchRole(b.getAttribute('data-r')); };
595
- });
594
+ bindGo(el, '.flip[data-r]:not([disabled]), .mwho .go[data-r]');
596
595
  };
597
596
  var rowNode = function (id) {
598
597
  var list = inner.querySelectorAll('.mrow');