ec-web 0.0.20 → 0.0.22

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.
@@ -92,6 +92,28 @@ const translations = {
92
92
  'common.online': '在线',
93
93
  'common.offline': '离线',
94
94
  'common.ago': '前',
95
+ 'basicSettings.open': '基础设置',
96
+ 'basicSettings.openTitle': '打开基础设置',
97
+ 'basicSettings.title': '基础设置',
98
+ 'basicSettings.subtitle': '按用途组织参数。保存前会校验并保留未编辑字段。',
99
+ 'basicSettings.reload': '重新读取',
100
+ 'basicSettings.tabs.models': '模型与推理',
101
+ 'basicSettings.tabs.modelsHint': '基座、模型与连接',
102
+ 'basicSettings.tabs.response': '响应与会话',
103
+ 'basicSettings.tabs.responseHint': '响应方式与续接',
104
+ 'basicSettings.tabs.projects': '项目与服务',
105
+ 'basicSettings.tabs.projectsHint': '路径与本机服务',
106
+ 'basicSettings.tabs.identity': '身份与安全',
107
+ 'basicSettings.tabs.identityHint': 'AID 与访问保护',
108
+ 'basicSettings.tabs.runtime': '运行与诊断',
109
+ 'basicSettings.tabs.runtimeHint': '日志与稳定性',
110
+ 'basicSettings.noChanges': '没有需要保存的改动',
111
+ 'basicSettings.viewJson': '查看 JSON',
112
+ 'basicSettings.save': '保存设置',
113
+ 'basicSettings.jsonTitle': '查看 JSON',
114
+ 'basicSettings.jsonSubtitle': '只读预览,敏感值已脱敏。',
115
+ 'basicSettings.daemonFile': 'daemon.json',
116
+ 'basicSettings.defaultsFile': 'agents/defaults.json',
95
117
  'theme.light': '浅色模式',
96
118
  'theme.dark': '深色模式',
97
119
  'theme.system': '跟随系统',
@@ -587,6 +609,8 @@ const translations = {
587
609
  'triggers.history.field.payload': '事件数据',
588
610
  'triggers.history.field.sessionId': '会话 ID',
589
611
  'triggers.history.field.messageId': '消息 ID',
612
+ 'triggers.history.field.operationId': '操作 ID',
613
+ 'triggers.history.field.outboxId': 'Outbox ID',
590
614
  'triggers.history.field.handoffId': 'Handoff ID',
591
615
  'triggers.history.field.attempt': '尝试次数',
592
616
  'triggers.history.field.triggerId': 'Trigger ID',
@@ -609,8 +633,9 @@ const translations = {
609
633
  'triggers.history.outcome.error': '错误',
610
634
  'triggers.history.outcome.interrupted': '已中断',
611
635
  'triggers.history.outcome.timeout': '超时',
636
+ 'triggers.history.outcome.queued': '已入队',
612
637
  'triggers.history.effect.message.send': '发送消息',
613
- 'triggers.history.effect.handoff.enqueue': '创建 Handoff',
638
+ 'triggers.history.effect.handoff.enqueue': 'Handoff 投递',
614
639
  'triggers.history.effect.message.inbound': '注入目标会话',
615
640
  'triggers.history.effect.agent.conversation': 'Agent 对话',
616
641
  'triggers.runStatus.completed': '成功',
@@ -998,6 +1023,28 @@ const translations = {
998
1023
  'common.online': 'Online',
999
1024
  'common.offline': 'Offline',
1000
1025
  'common.ago': 'ago',
1026
+ 'basicSettings.open': 'Settings',
1027
+ 'basicSettings.openTitle': 'Open basic settings',
1028
+ 'basicSettings.title': 'Basic Settings',
1029
+ 'basicSettings.subtitle': 'Settings organized by purpose. Saving validates changes and preserves untouched fields.',
1030
+ 'basicSettings.reload': 'Reload',
1031
+ 'basicSettings.tabs.models': 'Models & Reasoning',
1032
+ 'basicSettings.tabs.modelsHint': 'Base Agents, models, connections',
1033
+ 'basicSettings.tabs.response': 'Responses & Sessions',
1034
+ 'basicSettings.tabs.responseHint': 'Delivery and renewal',
1035
+ 'basicSettings.tabs.projects': 'Projects & Services',
1036
+ 'basicSettings.tabs.projectsHint': 'Paths and local services',
1037
+ 'basicSettings.tabs.identity': 'Identity & Security',
1038
+ 'basicSettings.tabs.identityHint': 'AID and access protection',
1039
+ 'basicSettings.tabs.runtime': 'Runtime & Diagnostics',
1040
+ 'basicSettings.tabs.runtimeHint': 'Logs and stability',
1041
+ 'basicSettings.noChanges': 'No unsaved changes',
1042
+ 'basicSettings.viewJson': 'View JSON',
1043
+ 'basicSettings.save': 'Save Settings',
1044
+ 'basicSettings.jsonTitle': 'View JSON',
1045
+ 'basicSettings.jsonSubtitle': 'Read-only preview with sensitive values redacted.',
1046
+ 'basicSettings.daemonFile': 'daemon.json',
1047
+ 'basicSettings.defaultsFile': 'agents/defaults.json',
1001
1048
  'theme.light': 'Light',
1002
1049
  'theme.dark': 'Dark',
1003
1050
  'theme.system': 'System',
@@ -1493,6 +1540,8 @@ const translations = {
1493
1540
  'triggers.history.field.payload': 'Event Data',
1494
1541
  'triggers.history.field.sessionId': 'Session ID',
1495
1542
  'triggers.history.field.messageId': 'Message ID',
1543
+ 'triggers.history.field.operationId': 'Operation ID',
1544
+ 'triggers.history.field.outboxId': 'Outbox ID',
1496
1545
  'triggers.history.field.handoffId': 'Handoff ID',
1497
1546
  'triggers.history.field.attempt': 'Attempts',
1498
1547
  'triggers.history.field.triggerId': 'Trigger ID',
@@ -1515,8 +1564,9 @@ const translations = {
1515
1564
  'triggers.history.outcome.error': 'Error',
1516
1565
  'triggers.history.outcome.interrupted': 'Interrupted',
1517
1566
  'triggers.history.outcome.timeout': 'Timeout',
1567
+ 'triggers.history.outcome.queued': 'Queued',
1518
1568
  'triggers.history.effect.message.send': 'Send Message',
1519
- 'triggers.history.effect.handoff.enqueue': 'Create Handoff',
1569
+ 'triggers.history.effect.handoff.enqueue': 'Deliver Handoff',
1520
1570
  'triggers.history.effect.message.inbound': 'Inject Target Session',
1521
1571
  'triggers.history.effect.agent.conversation': 'Agent Conversation',
1522
1572
  'triggers.runStatus.completed': 'Completed',
@@ -1857,6 +1907,7 @@ function updateI18n() {
1857
1907
  // 更新 html lang 属性
1858
1908
  document.documentElement.lang = currentLang;
1859
1909
  updateThemeButton();
1910
+ window.refreshBasicSettingsI18n?.();
1860
1911
  }
1861
1912
 
1862
1913
  function toggleLang() {
@@ -1873,7 +1924,16 @@ function toggleLang() {
1873
1924
  // 本地直连时页面在 "/",经 AUN Service Proxy 时页面在 "/ecweb/"。
1874
1925
  // 取当前页面所在目录(含尾斜杠)作为所有 API/WS 的前缀,使绝对路径在两种
1875
1926
  // 部署下都正确(proxy-server 用首段路径选服务,前缀不能丢)。
1876
- const BASE = location.pathname.replace(/[^/]*$/, '');
1927
+ const BASE = (() => {
1928
+ const baseElement = document.querySelector('base[href]');
1929
+ if (baseElement) {
1930
+ try {
1931
+ const basePath = new URL(baseElement.href, location.href).pathname;
1932
+ return basePath.endsWith('/') ? basePath : `${basePath}/`;
1933
+ } catch {}
1934
+ }
1935
+ return location.pathname.replace(/[^/]*$/, '');
1936
+ })();
1877
1937
  const apiUrl = (p) => BASE + p.replace(/^\/+/, '');
1878
1938
  const authApiUrl = (p) => {
1879
1939
  const token = localStorage.getItem(TOKEN_KEY);
@@ -2196,6 +2256,12 @@ function subscribeRoleManagementSubview() {
2196
2256
  function setRoleManagementSubview(subview, options = {}) {
2197
2257
  roleManagementSubview = subview === 'assignments' ? 'assignments' : 'definitions';
2198
2258
  localStorage.setItem(ROLE_SUBVIEW_KEY, roleManagementSubview);
2259
+ const newRoleButton = $('#btn-new-role');
2260
+ if (newRoleButton) {
2261
+ const visible = roleManagementSubview === 'definitions';
2262
+ newRoleButton.hidden = !visible;
2263
+ newRoleButton.setAttribute('aria-hidden', String(!visible));
2264
+ }
2199
2265
  document.querySelectorAll('[data-role-subview]').forEach(tab => {
2200
2266
  const active = tab.dataset.roleSubview === roleManagementSubview;
2201
2267
  tab.classList.toggle('active', active);
@@ -3755,7 +3821,7 @@ function renderBlocks(blocks, options = {}) {
3755
3821
  : '';
3756
3822
  out += `<details class="blk blk-tool" open><summary class="tool-head">${icon} <span class="tool-name">${esc(tool)}</span></summary>${toolParams}</details>`;
3757
3823
  } else if (b.kind === 'tool_result') {
3758
- const rejection = b.rejection?.kind === 'approval_rejected' ? b.rejection : undefined;
3824
+ const rejection = b.rejection && (b.rejection.kind === 'approval_rejected' || b.rejection.kind === 'policy_blocked') ? b.rejection : undefined;
3759
3825
  const cls = b.isError ? `blk-result err${rejection ? ' rejection' : ''}` : 'blk-result';
3760
3826
  const loadFull = b.truncated && b.resultKey
3761
3827
  ? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
@@ -3779,7 +3845,7 @@ function renderBlocks(blocks, options = {}) {
3779
3845
  : '';
3780
3846
  return '<div class="result-rejection-meta">' +
3781
3847
  `<span class="result-rejection-badge">⚠ ${t('sessions.block.rejection.title')}</span>` +
3782
- `<span>${rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
3848
+ `<span>${rejection.kind === 'policy_blocked' || rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
3783
3849
  `<span>${t('sessions.block.rejection.notExecuted')}</span>` +
3784
3850
  `<span>${t(`sessions.block.rejection.evidence.${evidenceKey}`)}</span>` +
3785
3851
  backendReason + auditText +
@@ -5460,6 +5526,13 @@ const TRIGGER_EXECUTION_TYPES = [
5460
5526
  ];
5461
5527
  const TRIGGER_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
5462
5528
  const TRIGGER_PERMISSION_MODES = ['readonly', 'auto', 'request', 'bypass'];
5529
+ function triggerPermissionModes(currentMode = '') {
5530
+ const modes = [...TRIGGER_PERMISSION_MODES];
5531
+ if ((state.triggers?.fullaccessEnabled === true
5532
+ && state.triggers?.canApprovePersistentFullAccess === true)
5533
+ || currentMode === 'fullaccess') modes.push('fullaccess');
5534
+ return modes;
5535
+ }
5463
5536
  const TRIGGER_TARGET_SESSIONS = [
5464
5537
  ['main', () => t('triggers.session.main')],
5465
5538
  ['thread', () => t('triggers.session.thread')],
@@ -5581,8 +5654,9 @@ async function withTriggerOp(id, agent, label, fn) {
5581
5654
  finally { setTriggerOp(id, agent, null); }
5582
5655
  }
5583
5656
 
5584
- function triggerOptionHtml(items, current, emptyLabel) {
5657
+ function triggerOptionHtml(items, current, emptyLabel, disabledValues = null) {
5585
5658
  const selected = String(current || '');
5659
+ const disabled = disabledValues instanceof Set ? disabledValues : new Set(disabledValues || []);
5586
5660
  let html = emptyLabel == null ? '' : `<option value=""${selected ? '' : ' selected'}>${esc(emptyLabel)}</option>`;
5587
5661
  let seen = new Set();
5588
5662
  for (const item of items) {
@@ -5591,10 +5665,10 @@ function triggerOptionHtml(items, current, emptyLabel) {
5591
5665
  const label = typeof rawLabel === 'function' ? rawLabel() : String(rawLabel);
5592
5666
  if (seen.has(value)) continue;
5593
5667
  seen.add(value);
5594
- html += `<option value="${esc(value)}"${value === selected ? ' selected' : ''}>${esc(label)}</option>`;
5668
+ html += `<option value="${esc(value)}"${value === selected ? ' selected' : ''}${disabled.has(value) ? ' disabled' : ''}>${esc(label)}</option>`;
5595
5669
  }
5596
5670
  if (selected && !seen.has(selected)) {
5597
- html += `<option value="${esc(selected)}" selected>${esc(selected)} (${t('common.currentSuffix')})</option>`;
5671
+ html += `<option value="${esc(selected)}" selected${disabled.has(selected) ? ' disabled' : ''}>${esc(selected)} (${t('common.currentSuffix')})</option>`;
5598
5672
  }
5599
5673
  return html;
5600
5674
  }
@@ -6216,7 +6290,9 @@ function triggerHistoryFieldLabel(path, context = '') {
6216
6290
  channelType: t('triggers.th.channelType'),
6217
6291
  channelId: t('triggers.field.channelId'),
6218
6292
  sessionId: t('triggers.history.field.sessionId'),
6293
+ operationId: t('triggers.history.field.operationId'),
6219
6294
  messageId: t('triggers.history.field.messageId'),
6295
+ outboxId: t('triggers.history.field.outboxId'),
6220
6296
  handoffId: t('triggers.history.field.handoffId'),
6221
6297
  attempt: t('triggers.history.field.attempt'),
6222
6298
  startedAt: t('triggers.history.startedAt'),
@@ -6254,6 +6330,7 @@ function triggerHistoryFieldValue(path, value, context = '') {
6254
6330
  }
6255
6331
  if (context === 'effects' && path === 'status') {
6256
6332
  if (value === 'success') return t('triggers.history.outcome.success');
6333
+ if (value === 'queued') return t('triggers.history.outcome.queued');
6257
6334
  if (value === 'failed') return t('triggers.runStatus.failed');
6258
6335
  if (value === 'skipped') return t('triggers.runStatus.skipped');
6259
6336
  }
@@ -6368,7 +6445,7 @@ function triggerHistoryEffectsBlock(effects) {
6368
6445
  'channelId',
6369
6446
  ...(Number(effect?.attempt) === 1 ? ['attempt'] : []),
6370
6447
  ],
6371
- keyOrder: ['type', 'status', 'handoffId', 'sessionId'],
6448
+ keyOrder: ['type', 'status', 'messageId', 'outboxId', 'handoffId', 'sessionId', 'operationId'],
6372
6449
  }) +
6373
6450
  `</div>`,
6374
6451
  ).join('')}</div>`;
@@ -7280,7 +7357,7 @@ function openTriggerEditDrawer(id, agent) {
7280
7357
  `<label class="ag-edit-field"><span>${t('triggers.field.executionType')}</span><select id="trig-edit-type">${triggerOptionHtml(TRIGGER_EXECUTION_TYPES, original.executionType, null)}</select></label>` +
7281
7358
  `<label class="ag-edit-field trig-agent-field"><span>${t('triggers.field.model')}</span><select id="trig-edit-model">${agentSelectOptions([], original.model, t('common.inherited'))}</select></label>` +
7282
7359
  `<label class="ag-edit-field trig-agent-field"><span>${t('triggers.field.effort')}</span><select id="trig-edit-effort">${agentSelectOptions([], original.effort, t('common.inherited'))}</select></label>` +
7283
- `<label class="ag-edit-field trig-agent-field"><span>${t('triggers.field.permission')}</span><select id="trig-edit-permission">${triggerOptionHtml(TRIGGER_PERMISSION_MODES, original.permissionMode, t('common.inherited'))}</select></label>` +
7360
+ `<label class="ag-edit-field trig-agent-field"><span>${t('triggers.field.permission')}</span><select id="trig-edit-permission">${triggerOptionHtml(triggerPermissionModes(original.permissionMode), original.permissionMode, t('common.inherited'), state.triggers?.canApprovePersistentFullAccess === true ? [] : ['fullaccess'])}</select></label>` +
7284
7361
  `<label class="ag-edit-field trig-script-field"><span>${t('triggers.field.scriptRuntime')}</span><select id="trig-edit-script-runtime">${triggerOptionHtml(['node', 'python', 'bash'], original.scriptRuntime, null)}</select></label>` +
7285
7362
  `<label class="ag-edit-field trig-script-field trig-script-path-field"><span>${t('triggers.field.scriptPath')}</span><input id="trig-edit-script-path" type="text" autocomplete="off" spellcheck="false" value="${esc(original.scriptPath)}" placeholder="${t('triggers.placeholder.scriptPath')}"></label>` +
7286
7363
  `</div>` +
@@ -9021,11 +9098,39 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
9021
9098
  }
9022
9099
 
9023
9100
  function renderRoles(data) {
9024
- if (!data) return;
9025
-
9026
9101
  const agentSelect = $('#roles-agent-select');
9102
+ const tbody = document.querySelector('#relations-table tbody');
9103
+ if (!data) {
9104
+ if (tbody) {
9105
+ tbody.innerHTML = '';
9106
+ const row = tbody.insertRow();
9107
+ const cell = row.insertCell();
9108
+ cell.colSpan = 7;
9109
+ cell.textContent = t('common.loading');
9110
+ cell.style.textAlign = 'center';
9111
+ cell.style.color = 'var(--dim)';
9112
+ cell.style.padding = '40px';
9113
+ }
9114
+ return;
9115
+ }
9027
9116
  if (!agentSelect) return;
9028
9117
 
9118
+ if (data.error) {
9119
+ agentSelect.innerHTML = `<option value="">${esc(t('roles.selectAgentPlaceholder'))}</option>`;
9120
+ rolesCurrentAgent = null;
9121
+ if (tbody) {
9122
+ tbody.innerHTML = '';
9123
+ const row = tbody.insertRow();
9124
+ const cell = row.insertCell();
9125
+ cell.colSpan = 7;
9126
+ cell.textContent = `${t('common.error')}: ${String(data.error)}`;
9127
+ cell.style.textAlign = 'center';
9128
+ cell.style.color = 'var(--red)';
9129
+ cell.style.padding = '40px';
9130
+ }
9131
+ return;
9132
+ }
9133
+
9029
9134
  agentSelect.innerHTML = '';
9030
9135
  const placeholderOption = document.createElement('option');
9031
9136
  placeholderOption.value = '';
@@ -9063,6 +9168,7 @@ function roleSourceLabel(source) {
9063
9168
  const labels = {
9064
9169
  'relation-assigned': 'Explicit',
9065
9170
  'group-member': 'Member override',
9171
+ 'relation-actor': 'Private role',
9066
9172
  'private-inherited': 'Private role',
9067
9173
  'group-default': 'Group role',
9068
9174
  default: 'Default',
@@ -9080,6 +9186,34 @@ function peerTypeText(peerType) {
9080
9186
  return '-';
9081
9187
  }
9082
9188
 
9189
+ function roleVerificationStatus(item) {
9190
+ const status = String(item?.verifyStatus || 'unknown').toLowerCase();
9191
+ if (status === 'agentmd' || status === 'verified') return 'verified';
9192
+ if (status === 'agentmd-unverified' || status === 'unverified') return 'agentmd-unverified';
9193
+ if (status === 'invalid') return 'invalid';
9194
+ return 'unknown';
9195
+ }
9196
+
9197
+ function roleVerificationMatches(item, filter) {
9198
+ if (!filter) return true;
9199
+ const status = roleVerificationStatus(item);
9200
+ return filter === 'verified' ? status === 'verified' : status === filter;
9201
+ }
9202
+
9203
+ function roleVerificationBadge(item) {
9204
+ const status = roleVerificationStatus(item);
9205
+ if (status === 'invalid') {
9206
+ return ` <span class="verify-badge verify-invalid" title="${esc(t('roles.verify.invalid'))}">⚠️ ${esc(t('roles.verify.invalidShort'))}</span>`;
9207
+ }
9208
+ if (status === 'agentmd-unverified') {
9209
+ return ` <span class="verify-badge verify-unverified" title="${esc(t('roles.verify.unverified'))}">⚠️</span>`;
9210
+ }
9211
+ if (status === 'unknown') {
9212
+ return ` <span class="verify-badge verify-unknown" title="${esc(t('roles.verify.unknown'))}">❓</span>`;
9213
+ }
9214
+ return '';
9215
+ }
9216
+
9083
9217
  function roleClass(role) {
9084
9218
  return String(role || 'none').replace(/[^a-z0-9_-]/gi, '-');
9085
9219
  }
@@ -9092,7 +9226,9 @@ function assignedRoleFor(scope, item) {
9092
9226
  if (!item) return '';
9093
9227
  if (scope === 'private') return item.source === 'relation-assigned' ? (item.assignment?.role || item.role || '') : '';
9094
9228
  if (scope === 'group') return item.source === 'relation-assigned' ? (item.assignment?.role || item.role || '') : '';
9095
- return item.source === 'group-member' ? (item.assignment?.role || item.role || '') : '';
9229
+ return item.source === 'group-member' || item.source === 'relation-actor'
9230
+ ? (item.assignment?.role || item.role || '')
9231
+ : '';
9096
9232
  }
9097
9233
 
9098
9234
  function assignableRolesForAgent(agentAid) {
@@ -9114,6 +9250,16 @@ function renderRelationsTable(data, filterAid = null) {
9114
9250
  if (!tbody) return;
9115
9251
 
9116
9252
  tbody.innerHTML = '';
9253
+ if (data?.error) {
9254
+ const row = tbody.insertRow();
9255
+ const cell = row.insertCell();
9256
+ cell.colSpan = 7;
9257
+ cell.textContent = `${t('common.error')}: ${String(data.error)}`;
9258
+ cell.style.textAlign = 'center';
9259
+ cell.style.color = 'var(--red)';
9260
+ cell.style.padding = '40px';
9261
+ return;
9262
+ }
9117
9263
  let conversations = data.conversations || [];
9118
9264
 
9119
9265
  if (filterAid) {
@@ -9141,6 +9287,16 @@ function renderRelationsTable(data, filterAid = null) {
9141
9287
  conversations = conversations.filter(conv => conv.chatType === rolesChatTypeFilter);
9142
9288
  }
9143
9289
 
9290
+ if (rolesVerifyFilter) {
9291
+ conversations = conversations
9292
+ .map(conv => conv.chatType === 'group'
9293
+ ? { ...conv, members: (conv.members || []).filter(member => roleVerificationMatches(member, rolesVerifyFilter)) }
9294
+ : conv)
9295
+ .filter(conv => conv.chatType === 'group'
9296
+ ? (conv.members || []).length > 0
9297
+ : roleVerificationMatches(conv, rolesVerifyFilter));
9298
+ }
9299
+
9144
9300
  conversations.sort((a, b) => {
9145
9301
  const lastDiff = (b.lastAt || 0) - (a.lastAt || 0);
9146
9302
  if (lastDiff !== 0) return lastDiff;
@@ -9170,10 +9326,12 @@ function renderPrivateConversationRow(tbody, conv) {
9170
9326
  const peerId = conv.peerId || conv.peerAid || conv.peerKey || conv.conversationId;
9171
9327
  const name = conv.peerName || conv.name || shortAid(peerId);
9172
9328
  const assignedRole = assignedRoleFor('private', conv);
9329
+ const verifyBadge = roleVerificationBadge(conv);
9330
+ const verifyClass = roleVerificationStatus(conv) === 'verified' ? '' : ` peer-${roleVerificationStatus(conv) === 'unknown' ? 'unknown' : 'unverified'}`;
9173
9331
  const row = tbody.insertRow();
9174
9332
  row.innerHTML = `
9175
9333
  <td><span class="chat-tag chat-tag-private">${esc(t('roles.chatType.private') || 'Private')}</span></td>
9176
- <td><strong>${esc(name)}</strong></td>
9334
+ <td class="${verifyClass.trim()}"><strong>${esc(name)}</strong>${verifyBadge}</td>
9177
9335
  <td><code>${esc(peerId)}</code></td>
9178
9336
  <td>${esc(peerTypeText(conv.peerType))}</td>
9179
9337
  <td><span class="role-badge role-${esc(roleClass(conv.role))}">${esc(displayRole(conv.role))}</span></td>
@@ -9238,11 +9396,13 @@ function renderGroupMemberRow(tbody, conv, member) {
9238
9396
  const peerId = member.peerId || member.peerAid || member.peerKey;
9239
9397
  const name = member.peerName || shortAid(peerId);
9240
9398
  const assignedRole = assignedRoleFor('group-member', member);
9399
+ const verifyBadge = roleVerificationBadge(member);
9400
+ const verifyClass = roleVerificationStatus(member) === 'verified' ? '' : ` peer-${roleVerificationStatus(member) === 'unknown' ? 'unknown' : 'unverified'}`;
9241
9401
  const row = tbody.insertRow();
9242
9402
  row.className = 'group-member-row';
9243
9403
  row.innerHTML = `
9244
9404
  <td></td>
9245
- <td><span class="group-member-indent">${esc(name)}</span></td>
9405
+ <td class="${verifyClass.trim()}"><span class="group-member-indent">${esc(name)}</span>${verifyBadge}</td>
9246
9406
  <td><code>${esc(peerId)}</code></td>
9247
9407
  <td>${esc(peerTypeText(member.peerType))}</td>
9248
9408
  <td><span class="role-badge role-${esc(roleClass(member.role))}">${esc(displayRole(member.role))}</span></td>