ec-web 0.0.4 → 0.0.6

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.
@@ -248,6 +248,7 @@ const translations = {
248
248
  'messages.empty.noAgents': '暂无 Agent',
249
249
  'messages.empty.noPeers': '暂无聊天对端',
250
250
  'messages.empty.noMessages': '暂无消息',
251
+ 'messages.loadOlder': '加载更早消息',
251
252
  'messages.tag.group': '群聊',
252
253
  'messages.tag.groupShort': '群',
253
254
  'messages.tag.main': '主',
@@ -288,6 +289,8 @@ const translations = {
288
289
  'sessions.empty.noMatch': '无匹配会话',
289
290
  'sessions.empty.noSessions': '该项目暂无会话',
290
291
  'sessions.empty.noContent': '该会话暂无内容',
292
+ 'sessions.loadOlder': '加载更早内容',
293
+ 'sessions.loadFullResult': '加载完整结果',
291
294
  'sessions.header.project': '项目',
292
295
  'sessions.header.session': '会话',
293
296
  'sessions.stat.context': '📐 {tokens} ctx',
@@ -296,10 +299,19 @@ const translations = {
296
299
  'sessions.turnType.toolUse': '工具使用',
297
300
  'sessions.turnType.toolResult': '工具结果',
298
301
  'sessions.turnType.msgSend': '发送消息',
299
- 'sessions.count': '{filtered} / {total} 个会话',
302
+ 'sessions.count': '{count} 个会话',
300
303
  'sessions.filter.valid': '有效',
301
304
  'sessions.filter.validTitle': '只显示有效会话(≥1 条用户消息)',
305
+ 'sessions.filter.all': '全部',
306
+ 'sessions.filter.allTitle': '显示全部会话(包括无用户消息的会话)',
307
+ 'sessions.filter.sourceTitle': '按会话类型筛选',
308
+ 'sessions.filter.sourceAll': '全部类型',
302
309
  'sessions.messageCountTitle': '用户输入 {user} 条 / 共 {total} 条消息',
310
+ 'sessions.source.trigger': 'Trigger',
311
+ 'sessions.source.agent': 'Agent',
312
+ 'sessions.source.cli': 'CLI',
313
+ 'sessions.source.user': '用户',
314
+ 'sessions.source.unknown': 'Unknown',
303
315
  'sessions.selectLog': '选择会话查看 CC 日志',
304
316
  'sessions.chatView': '💬 对话视图',
305
317
  'sessions.fullView': '📜 完整视图',
@@ -309,10 +321,21 @@ const translations = {
309
321
  'sessions.empty.noDialogue': '该会话没有用户对话消息',
310
322
  'sessions.turnType.userInput': '用户输入',
311
323
  'sessions.turnType.toolCall': '工具调用',
324
+ 'sessions.turnType.subtask': '子任务',
312
325
  'sessions.turnType.system': '系统',
326
+ 'sessions.subtask.status.running': '运行中',
327
+ 'sessions.subtask.status.completed': '已完成',
328
+ 'sessions.subtask.status.failed': '失败',
329
+ 'sessions.subtask.status.stopped': '已停止',
330
+ 'sessions.subtask.status.killed': '已终止',
331
+ 'sessions.subtask.tools': '{count} 次工具',
332
+ 'sessions.subtask.tokens': '{count} tokens',
333
+ 'sessions.subtask.reason': '原因:{reason}',
313
334
  'sessions.block.thinking': '思考',
314
335
  'sessions.block.result': '结果',
315
336
  'sessions.block.resultError': '结果',
337
+ 'sessions.content.expand': '展开内容',
338
+ 'sessions.content.collapse': '收起内容',
316
339
  'sessions.contextTitle': '最后一轮喂给模型的完整上下文大小',
317
340
  'sessions.costTitle': '累计费用(按模型定价估算)',
318
341
  'sessions.msgUnit': '条',
@@ -639,6 +662,7 @@ const translations = {
639
662
  'triggers.feedback.defaultFailure': '默认:触发器失败:{{error.message}}',
640
663
  'triggers.field.model': '模型',
641
664
  'triggers.field.effort': '推理强度',
665
+ 'triggers.baseagent.mismatch': 'Trigger 固定:{trigger},Agent 当前:{agent}',
642
666
  'triggers.field.permission': '权限模式',
643
667
  'triggers.delivery.on': '启用',
644
668
  'triggers.delivery.off': '禁用',
@@ -1104,6 +1128,7 @@ const translations = {
1104
1128
  'messages.empty.noAgents': 'No agents',
1105
1129
  'messages.empty.noPeers': 'No chats',
1106
1130
  'messages.empty.noMessages': 'No messages',
1131
+ 'messages.loadOlder': 'Load older messages',
1107
1132
  'messages.tag.group': 'Group',
1108
1133
  'messages.tag.groupShort': 'G',
1109
1134
  'messages.tag.main': 'Main',
@@ -1144,6 +1169,8 @@ const translations = {
1144
1169
  'sessions.empty.noMatch': 'No matching sessions',
1145
1170
  'sessions.empty.noSessions': 'No sessions in this project',
1146
1171
  'sessions.empty.noContent': 'No content in this session',
1172
+ 'sessions.loadOlder': 'Load older content',
1173
+ 'sessions.loadFullResult': 'Load full result',
1147
1174
  'sessions.header.project': 'Project',
1148
1175
  'sessions.header.session': 'Session',
1149
1176
  'sessions.stat.context': '📐 {tokens} ctx',
@@ -1152,10 +1179,19 @@ const translations = {
1152
1179
  'sessions.turnType.toolUse': 'Tool Use',
1153
1180
  'sessions.turnType.toolResult': 'Tool Result',
1154
1181
  'sessions.turnType.msgSend': 'Send Message',
1155
- 'sessions.count': '{filtered} / {total} sessions',
1182
+ 'sessions.count': '{count} sessions',
1156
1183
  'sessions.filter.valid': 'Valid',
1157
1184
  'sessions.filter.validTitle': 'Only show valid sessions (at least 1 user message)',
1185
+ 'sessions.filter.all': 'All',
1186
+ 'sessions.filter.allTitle': 'Show all sessions, including sessions without user messages',
1187
+ 'sessions.filter.sourceTitle': 'Filter by session type',
1188
+ 'sessions.filter.sourceAll': 'All types',
1158
1189
  'sessions.messageCountTitle': '{user} user messages / {total} total messages',
1190
+ 'sessions.source.trigger': 'Trigger',
1191
+ 'sessions.source.agent': 'Agent',
1192
+ 'sessions.source.cli': 'CLI',
1193
+ 'sessions.source.user': 'User',
1194
+ 'sessions.source.unknown': 'Unknown',
1159
1195
  'sessions.selectLog': 'Select a session to view the CC log',
1160
1196
  'sessions.chatView': '💬 Chat View',
1161
1197
  'sessions.fullView': '📜 Full View',
@@ -1165,10 +1201,21 @@ const translations = {
1165
1201
  'sessions.empty.noDialogue': 'No user dialogue messages in this session',
1166
1202
  'sessions.turnType.userInput': 'User Input',
1167
1203
  'sessions.turnType.toolCall': 'Tool Call',
1204
+ 'sessions.turnType.subtask': 'Subtask',
1168
1205
  'sessions.turnType.system': 'System',
1206
+ 'sessions.subtask.status.running': 'Running',
1207
+ 'sessions.subtask.status.completed': 'Completed',
1208
+ 'sessions.subtask.status.failed': 'Failed',
1209
+ 'sessions.subtask.status.stopped': 'Stopped',
1210
+ 'sessions.subtask.status.killed': 'Killed',
1211
+ 'sessions.subtask.tools': '{count} tool uses',
1212
+ 'sessions.subtask.tokens': '{count} tokens',
1213
+ 'sessions.subtask.reason': 'Reason: {reason}',
1169
1214
  'sessions.block.thinking': 'Thinking',
1170
1215
  'sessions.block.result': 'Result',
1171
1216
  'sessions.block.resultError': 'Result',
1217
+ 'sessions.content.expand': 'Expand content',
1218
+ 'sessions.content.collapse': 'Collapse content',
1172
1219
  'sessions.contextTitle': 'Full context size sent to the model in the last turn',
1173
1220
  'sessions.costTitle': 'Estimated cumulative cost by model pricing',
1174
1221
  'sessions.msgUnit': 'msgs',
@@ -1495,6 +1542,7 @@ const translations = {
1495
1542
  'triggers.feedback.defaultFailure': 'Default: 触发器失败:{{error.message}}',
1496
1543
  'triggers.field.model': 'Model',
1497
1544
  'triggers.field.effort': 'Effort',
1545
+ 'triggers.baseagent.mismatch': 'Trigger fixed: {trigger}; current Agent: {agent}',
1498
1546
  'triggers.field.permission': 'Permission Mode',
1499
1547
  'triggers.delivery.on': 'Enabled',
1500
1548
  'triggers.delivery.off': 'Disabled',
@@ -1896,7 +1944,7 @@ function connect() {
1896
1944
  console.log('[ecweb] Available baseagents:', availableBaseagents, 'Selected:', sessSel.baseagent);
1897
1945
  // 重新订阅当前视图(带上正确的参数)
1898
1946
  if (currentView === 'session') {
1899
- subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
1947
+ subscribe('session', sessSel);
1900
1948
  } else if (currentView === 'roleManagement') {
1901
1949
  subscribeRoleManagementSubview();
1902
1950
  } else if (currentView === 'system' && systemContentMode === 'monitor') {
@@ -1931,6 +1979,11 @@ function connect() {
1931
1979
  if (pend) { delete _ipcPending[msg.requestId]; pend.resolve(msg.data); }
1932
1980
  return;
1933
1981
  }
1982
+ if (msg.type === 'session.result') {
1983
+ const pending = _sessionResultPending[msg.requestId];
1984
+ if (pending) { delete _sessionResultPending[msg.requestId]; pending.resolve(msg.data); }
1985
+ return;
1986
+ }
1934
1987
  if (msg.type === 'snapshot' || msg.type === 'delta') {
1935
1988
  console.log('[ecweb] Received', msg.type, 'for view:', msg.view, 'currentView:', currentView);
1936
1989
  // 系统基础快照不主动执行健康检查;保留用户点击按钮后得到的 check/upgrade 结果。
@@ -1940,6 +1993,24 @@ function connect() {
1940
1993
  check: msg.data.check ?? state.system.check,
1941
1994
  upgrade: state.system.upgrade ?? msg.data.upgrade,
1942
1995
  };
1996
+ } else if (msg.view === 'msg'
1997
+ && msg.data?.messagePage?.before != null
1998
+ && state.msg?.scope === msg.data.scope
1999
+ && state.msg?.peer === msg.data.peer) {
2000
+ const existing = state.msg.messages || [];
2001
+ const incoming = msg.data.messages || [];
2002
+ const incomingKeys = new Set(incoming.map(item => item.msgId || `${item.ts}:${item.dir}:${item.content}`));
2003
+ const merged = incoming.concat(existing.filter(item => !incomingKeys.has(item.msgId || `${item.ts}:${item.dir}:${item.content}`)));
2004
+ state.msg = { ...msg.data, messages: merged };
2005
+ } else if (msg.view === 'session'
2006
+ && msg.data?.turnPage?.before != null
2007
+ && state.session?.sessionId === msg.data.sessionId
2008
+ && state.session?.project === msg.data.project) {
2009
+ const existing = state.session.turns || [];
2010
+ const incoming = msg.data.turns || [];
2011
+ const incomingKeys = new Set(incoming.map(turn => turn.uuid || `${turn.ts}:${turn.category}:${JSON.stringify(turn.blocks || [])}`));
2012
+ const merged = incoming.concat(existing.filter(turn => !incomingKeys.has(turn.uuid || `${turn.ts}:${turn.category}:${JSON.stringify(turn.blocks || [])}`)));
2013
+ state.session = { ...msg.data, turns: merged };
1943
2014
  } else {
1944
2015
  state[msg.view] = msg.data;
1945
2016
  }
@@ -2002,6 +2073,8 @@ function menuSend(payload) {
2002
2073
 
2003
2074
  const _ipcPending = {};
2004
2075
  let _ipcSeq = 0;
2076
+ const _sessionResultPending = {};
2077
+ let _sessionResultSeq = 0;
2005
2078
  function ipcSend(payload, timeoutMs) {
2006
2079
  return new Promise((resolve, reject) => {
2007
2080
  if (!ws || ws.readyState !== WebSocket.OPEN) { reject(new Error(t('common.notConnected'))); return; }
@@ -2015,14 +2088,26 @@ function ipcSend(payload, timeoutMs) {
2015
2088
  });
2016
2089
  }
2017
2090
 
2091
+ function sessionResultSend(params) {
2092
+ return new Promise((resolve, reject) => {
2093
+ if (!ws || ws.readyState !== WebSocket.OPEN) { reject(new Error(t('common.notConnected'))); return; }
2094
+ const requestId = 'ecw-session-result-' + (++_sessionResultSeq);
2095
+ _sessionResultPending[requestId] = { resolve, reject };
2096
+ setTimeout(() => {
2097
+ if (_sessionResultPending[requestId]) { delete _sessionResultPending[requestId]; reject(new Error('timeout')); }
2098
+ }, 10000);
2099
+ ws.send(JSON.stringify({ type: 'session.result', requestId, params }));
2100
+ });
2101
+ }
2102
+
2018
2103
  // 心跳
2019
2104
  setInterval(() => {
2020
2105
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
2021
2106
  }, 20000);
2022
2107
 
2023
2108
  // ── Tab 切换 ──
2024
- let msgSel = { aid: null, peer: null };
2025
- let sessSel = { sessionId: null, project: null, baseagent: null };
2109
+ let msgSel = { aid: null, peer: null, before: null };
2110
+ let sessSel = { sessionId: null, project: null, baseagent: null, turnBefore: null };
2026
2111
  let trigSel = { agent: null };
2027
2112
  let trigAgentsCollapsed = false;
2028
2113
  const _trigOps = new Map(); // Map<triggerId, string>
@@ -2030,7 +2115,8 @@ let _triggerEdit = null;
2030
2115
  let _triggerHistory = null;
2031
2116
  let _triggerTest = null;
2032
2117
  let sessSearch = '';
2033
- let sessFilterNormal = false; // true=只显示有效会话(userMsgs >= 1)
2118
+ let sessShowAll = false; // false=只显示有效会话;true=显示全部
2119
+ let sessSourceFilter = 'all'; // all / trigger / agent / cli / user / unknown
2034
2120
  let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
2035
2121
  let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
2036
2122
  let logSel = { source: null, level: 'all', module: 'all', search: '', limit: 500, follow: true };
@@ -2087,8 +2173,8 @@ function switchView(view) {
2087
2173
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
2088
2174
  document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
2089
2175
  // 切换时按当前选择恢复订阅
2090
- if (view === 'msg') subscribe('msg', { aid: msgSel.aid, peer: msgSel.peer });
2091
- else if (view === 'session') subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
2176
+ if (view === 'msg') subscribe('msg', msgSel);
2177
+ else if (view === 'session') subscribe('session', sessSel);
2092
2178
  else if (view === 'cache') subscribe('cache', {});
2093
2179
  else if (view === 'system' && systemContentMode === 'monitor') subscribe('monitor', { range: monRange });
2094
2180
  else if (view === 'system') subscribe('system', {});
@@ -2414,8 +2500,13 @@ const MSG_TYPE_META = {
2414
2500
  file: { icon: '📎', label: () => t('messages.msgType.file') },
2415
2501
  command: { icon: '⌘', label: () => t('messages.msgType.command') }
2416
2502
  };
2417
- function msgTagsHtml(kind, encrypt, chatmode, dir) {
2418
- let h = '';
2503
+ // 渠道标:AUN 是默认渠道,不标;其余渠道标出来,免得混排后分不清来路
2504
+ function channelTagHtml(channel, cls) {
2505
+ if (!channel || channel === 'aun') return '';
2506
+ return `<span class="${cls}">${esc(channel)}</span>`;
2507
+ }
2508
+ function msgTagsHtml(kind, encrypt, chatmode, dir, channel) {
2509
+ let h = channelTagHtml(channel, 'mtag mtag-channel');
2419
2510
  // 'send' 仅出向才是「回复」;入向是用户输入,不打回复标记
2420
2511
  const km = (kind === 'send' && dir === 'in') ? null : MSG_KIND_META[kind];
2421
2512
  if (km) h += `<span class="mtag${kind === 'send' ? ' mtag-reply' : ''}">${km.icon}${km.label()}</span>`;
@@ -2425,24 +2516,39 @@ function msgTagsHtml(kind, encrypt, chatmode, dir) {
2425
2516
  }
2426
2517
 
2427
2518
  // 消息行:方向箭头 + 标记 + 对端 + 文字
2519
+ function latestAgentPreviewMessage(s) {
2520
+ const recent = Array.isArray(s.recentMessages) ? s.recentMessages[0] : null;
2521
+ if (recent?.text) return recent;
2522
+ const recvTs = s.lastReceivedAt || 0;
2523
+ const sentTs = s.lastSentAt || 0;
2524
+ if (sentTs > recvTs && s.lastSentText) {
2525
+ return {
2526
+ dir: 'out', peer: s.lastSentTo, text: s.lastSentText, kind: s.lastSentKind,
2527
+ encrypt: s.lastSentEncrypt, chatmode: s.lastSentChatmode, channel: s.lastSentChannel,
2528
+ };
2529
+ }
2530
+ if (s.lastReceivedText) {
2531
+ return {
2532
+ dir: 'in', peer: s.lastReceivedFrom, text: s.lastReceivedText, kind: s.lastReceivedKind,
2533
+ encrypt: s.lastReceivedEncrypt, chatmode: s.lastReceivedChatmode, channel: s.lastReceivedChannel,
2534
+ };
2535
+ }
2536
+ return null;
2537
+ }
2538
+
2428
2539
  function agentPreviewHtml(s) {
2429
2540
  const clip = (t) => esc(String(t).replace(/\n/g, ' ').slice(0, 80));
2430
- const line = (dir, peer, text, kind, encrypt, chatmode) => {
2541
+ const line = (dir, peer, text, kind, encrypt, chatmode, channel) => {
2431
2542
  const arrow = dir === 'in' ? '<span class="arrow-in">↓</span>' : '<span class="arrow-out">↑</span>';
2432
- const tags = msgTagsHtml(kind, encrypt, chatmode, dir);
2543
+ const tags = msgTagsHtml(kind, encrypt, chatmode, dir, channel);
2433
2544
  const peerHtml = peer ? `<span class="peer">${esc(shortAid(peer))}</span>: ` : '';
2434
2545
  const textCls = dir === 'in' ? 'text-in' : 'text-out';
2435
2546
  return `${arrow}${tags ? ' ' + tags + ' ' : ' '}${peerHtml}<span class="${textCls}">${clip(text)}</span>`;
2436
2547
  };
2437
- if ((s.processing || 0) > 0 && s.lastReceivedText)
2438
- return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
2439
- const recvTs = s.lastReceivedAt || 0, sentTs = s.lastSentAt || 0;
2440
- if (!recvTs && !sentTs) return '';
2441
- if (sentTs > recvTs && s.lastSentText)
2442
- return line('out', s.lastSentTo, s.lastSentText, s.lastSentKind, s.lastSentEncrypt, s.lastSentChatmode);
2443
- if (s.lastReceivedText)
2444
- return line('in', s.lastReceivedFrom, s.lastReceivedText, s.lastReceivedKind, s.lastReceivedEncrypt, s.lastReceivedChatmode);
2445
- return '';
2548
+ const latest = latestAgentPreviewMessage(s);
2549
+ return latest
2550
+ ? line(latest.dir, latest.peer, latest.text, latest.kind, latest.encrypt, latest.chatmode, latest.channel)
2551
+ : '';
2446
2552
  }
2447
2553
 
2448
2554
  // HTML tooltip(最近 N 轮):时间 + 彩色箭头 + 方式 + 对端 + 文字
@@ -2458,10 +2564,11 @@ function recentMsgTooltipHtml(recent) {
2458
2564
  const kh = km ? `<span class="tip-kind${m.kind === 'send' ? ' tip-kind-reply' : ''}">${km.icon}${km.label()}</span>` : '';
2459
2565
  const enc = m.encrypt != null ? `<span class="tip-flag">${m.encrypt ? t('messages.tag.encrypted') : t('messages.tag.plain')}</span>` : '';
2460
2566
  const mode = m.chatmode ? `<span class="tip-flag">${m.chatmode === 'proactive' ? t('messages.tag.proactive') : (m.chatmode === 'inject' ? t('messages.tag.inject') : t('messages.tag.responsive'))}</span>` : '';
2567
+ const chan = channelTagHtml(m.channel, 'tip-flag tip-flag-channel');
2461
2568
  const peer = m.peer ? esc(shortAid(m.peer)) : '';
2462
2569
  const text = esc(String(m.text).replace(/\n/g, ' ').slice(0, 140));
2463
2570
  const time = m.ts ? `<span class="tip-time">${fmtTime(m.ts)}</span>` : '';
2464
- h += `<div class="tip-row ${rcls}">${time}${arrow}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
2571
+ h += `<div class="tip-row ${rcls}">${time}${arrow}${chan}${kh}${enc}${mode} <b>${peer}</b> ${text}</div>`;
2465
2572
  }
2466
2573
  return h + '</div>';
2467
2574
  }
@@ -2857,6 +2964,14 @@ function card(label, value, valCls, sub) {
2857
2964
  }
2858
2965
 
2859
2966
  // ── Messages 视图 ──
2967
+ function msgChannelTone(channelType) {
2968
+ const normalized = String(channelType || '').trim().toLowerCase();
2969
+ if (normalized === 'qq') return 'qqbot';
2970
+ return ['aun', 'wechat', 'wecom', 'feishu', 'dingtalk', 'qqbot', 'daemon'].includes(normalized)
2971
+ ? normalized
2972
+ : 'other';
2973
+ }
2974
+
2860
2975
  function renderMsg(data) {
2861
2976
  if (!data) return;
2862
2977
  const aids = data.scopes || data.aids || [];
@@ -2867,7 +2982,7 @@ function renderMsg(data) {
2867
2982
  if (!selectedAidExists) {
2868
2983
  const nextAid = aids[0]?.aid || null;
2869
2984
  const selectionChanged = msgSel.aid !== nextAid || msgSel.peer !== null;
2870
- msgSel = { aid: nextAid, peer: null };
2985
+ msgSel = { aid: nextAid, peer: null, before: null };
2871
2986
  if (selectionChanged && nextAid) subscribe('msg', msgSel);
2872
2987
  }
2873
2988
  const scopeMatches = data.scope === msgSel.aid;
@@ -2877,7 +2992,7 @@ function renderMsg(data) {
2877
2992
  if (!selectedPeerExists) {
2878
2993
  const nextPeer = peers[0]?.peerId || null;
2879
2994
  const selectionChanged = msgSel.peer !== nextPeer;
2880
- msgSel = { aid: msgSel.aid, peer: nextPeer };
2995
+ msgSel = { aid: msgSel.aid, peer: nextPeer, before: null };
2881
2996
  if (selectionChanged && nextPeer) subscribe('msg', msgSel);
2882
2997
  }
2883
2998
  }
@@ -2889,7 +3004,7 @@ function renderMsg(data) {
2889
3004
  let aidsHtml = `<div class="col-title"><span>${t('messages.colTitle.aid')}</span><span class="col-count">${aids.length}</span></div>`;
2890
3005
  for (const a of aids) {
2891
3006
  const sel = a.aid === msgSel.aid ? ' sel' : '';
2892
- const name = a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown';
3007
+ const name = a.displayName || (a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown');
2893
3008
  const privateCount = Math.max(0, (a.peerCount || 0) - (a.groupCount || 0));
2894
3009
  const privateBit = tf('messages.privateCount', { count: privateCount });
2895
3010
  const groupBit = a.groupCount ? ` · ${tf('messages.groupCount', { count: a.groupCount })}` : '';
@@ -2902,7 +3017,7 @@ function renderMsg(data) {
2902
3017
  $('#msg-aids').querySelectorAll('.list-item').forEach(item => {
2903
3018
  item.onclick = () => {
2904
3019
  if (item.dataset.aid === msgSel.aid) return;
2905
- msgSel = { aid: item.dataset.aid, peer: null };
3020
+ msgSel = { aid: item.dataset.aid, peer: null, before: null };
2906
3021
  subscribe('msg', msgSel);
2907
3022
  };
2908
3023
  });
@@ -2933,7 +3048,9 @@ function renderMsg(data) {
2933
3048
  ? t('messages.tag.topic')
2934
3049
  : p.partition === 'legacy' ? t('messages.tag.legacy') : t('messages.tag.main');
2935
3050
  const partitionMark = `<span class="msg-partition-mark msg-partition-${esc(p.partition || 'main')}">${esc(partitionLabel)}</span>`;
2936
- const channelTag = channelLabel ? `<span class="msg-tag">${esc(channelLabel)}</span>` : '';
3051
+ const channelTag = channelLabel
3052
+ ? `<span class="msg-tag msg-channel msg-channel-${msgChannelTone(p.channelType)}">${esc(channelLabel)}</span>`
3053
+ : '';
2937
3054
  peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
2938
3055
  `<div class="name">${groupMark}${partitionMark}<span class="msg-peer-title" title="${esc(chatTitle)}">${esc(shortId(chatTitle))}</span>${channelTag}</div>` +
2939
3056
  `<div class="sub">${p.partition === 'thread' ? `${esc(shortId(displayName))} · ` : ''}↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
@@ -2943,7 +3060,7 @@ function renderMsg(data) {
2943
3060
  $('#msg-peers').querySelectorAll('.list-item').forEach(item => {
2944
3061
  item.onclick = () => {
2945
3062
  if (item.dataset.peer === msgSel.peer) return;
2946
- msgSel = { aid: msgSel.aid, peer: item.dataset.peer };
3063
+ msgSel = { aid: msgSel.aid, peer: item.dataset.peer, before: null };
2947
3064
  subscribe('msg', msgSel);
2948
3065
  };
2949
3066
  });
@@ -2961,6 +3078,9 @@ function renderMsg(data) {
2961
3078
  }
2962
3079
  const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
2963
3080
  let msgHtml = '<div class="msg-stream-body">';
3081
+ if (data.messagePage?.hasMore && data.messagePage.nextBefore != null) {
3082
+ msgHtml += `<button class="ctrl-btn msg-load-more" type="button" data-before="${data.messagePage.nextBefore}">${t('messages.loadOlder') || '加载更早消息'}</button>`;
3083
+ }
2964
3084
  for (const m of messages) {
2965
3085
  const cls = m.dir === 'in' ? 'in' : 'out';
2966
3086
  const arrow = m.dir === 'in' ? '↓' : '↑';
@@ -2983,6 +3103,11 @@ function renderMsg(data) {
2983
3103
  }
2984
3104
  msgHtml += messages.length ? '</div>' : `<div class="empty">${t('messages.empty.noMessages')}</div></div>`;
2985
3105
  stream.innerHTML = renderMsgSessionHeader(sessionContext) + msgHtml;
3106
+ const loadMore = stream.querySelector('.msg-load-more');
3107
+ if (loadMore) loadMore.onclick = () => {
3108
+ msgSel = { aid: msgSel.aid, peer: msgSel.peer, before: Number(loadMore.dataset.before) };
3109
+ subscribe('msg', msgSel);
3110
+ };
2986
3111
  if (atBottom) stream.scrollTop = stream.scrollHeight;
2987
3112
  }
2988
3113
 
@@ -3020,6 +3145,20 @@ function renderMsgSessionHeader(context) {
3020
3145
  }
3021
3146
 
3022
3147
  // ── Sessions 视图 ──
3148
+ function filterSessionTranscripts(transcripts, showAll, sourceType = 'all') {
3149
+ return (transcripts || []).filter(session => {
3150
+ const valid = showAll || (session.userMsgs || 0) >= 1;
3151
+ const sourceMatches = sourceType === 'all' || (session.sourceType || 'unknown') === sourceType;
3152
+ return valid && sourceMatches;
3153
+ });
3154
+ }
3155
+
3156
+ function renderSessionSourceBadge(sourceType) {
3157
+ const type = ['trigger', 'agent', 'cli', 'user'].includes(sourceType) ? sourceType : 'unknown';
3158
+ const label = t(`sessions.source.${type}`);
3159
+ return `<span class="session-source-badge source-${type}" title="${esc(label)}">${esc(label)}</span>`;
3160
+ }
3161
+
3023
3162
  function renderSession(data) {
3024
3163
  if (!data) return;
3025
3164
  const projects = data.projects || [];
@@ -3034,8 +3173,7 @@ function renderSession(data) {
3034
3173
  }
3035
3174
 
3036
3175
  // 有效筛选
3037
- const filtered = transcripts
3038
- .filter(t => !sessFilterNormal || (t.userMsgs || 0) >= 1);
3176
+ const filtered = filterSessionTranscripts(transcripts, sessShowAll, sessSourceFilter);
3039
3177
 
3040
3178
  // 左栏:过滤条 + 列表
3041
3179
  const projOpts = projects.map(p =>
@@ -3050,18 +3188,25 @@ function renderSession(data) {
3050
3188
  if (availableBaseagents.codex) {
3051
3189
  baseagentOpts += `<option value="codex"${sessSel.baseagent === 'codex' ? ' selected' : ''}>Codex</option>`;
3052
3190
  }
3191
+ const sourceTypes = ['all', 'user', 'agent', 'trigger', 'cli', 'unknown'];
3192
+ const sourceOpts = sourceTypes.map(type => {
3193
+ const label = type === 'all' ? t('sessions.filter.sourceAll') : t(`sessions.source.${type}`);
3194
+ return `<option value="${type}"${sessSourceFilter === type ? ' selected' : ''}>${esc(label)}</option>`;
3195
+ }).join('');
3053
3196
 
3054
- const normalCount = transcripts.filter(t => (t.userMsgs || 0) >= 1).length;
3197
+ const filterLabelKey = sessShowAll ? 'sessions.filter.all' : 'sessions.filter.valid';
3198
+ const filterTitleKey = sessShowAll ? 'sessions.filter.allTitle' : 'sessions.filter.validTitle';
3055
3199
  let listHtml = '<div class="sess-filter">' +
3056
3200
  '<div class="sess-select-row">' +
3057
3201
  `<select id="sess-baseagent" title="Base Agent">${baseagentOpts}</select>` +
3058
3202
  `<select id="sess-project" title="${t('sessions.header.project')}">${projOpts}</select>` +
3203
+ `<select id="sess-source-type" title="${t('sessions.filter.sourceTitle')}">${sourceOpts}</select>` +
3059
3204
  '</div>' +
3060
3205
  '<div class="sess-filter-row">' +
3061
- `<div class="sess-count">${tf('sessions.count', { filtered: filtered.length, total: transcripts.length })}</div>` +
3062
- `<label class="sess-switch" title="${t('sessions.filter.validTitle')}">` +
3063
- `<span>${t('sessions.filter.valid')}</span>` +
3064
- `<input id="sess-filter-toggle" type="checkbox"${sessFilterNormal ? ' checked' : ''}>` +
3206
+ `<div class="sess-count">${tf('sessions.count', { count: filtered.length })}</div>` +
3207
+ `<label class="sess-switch" title="${t(filterTitleKey)}">` +
3208
+ `<span>${t(filterLabelKey)}</span>` +
3209
+ `<input id="sess-filter-toggle" type="checkbox"${sessShowAll ? ' checked' : ''}>` +
3065
3210
  '<span class="sess-switch-track" aria-hidden="true"><span class="sess-switch-thumb"></span></span>' +
3066
3211
  '</label>' +
3067
3212
  '</div></div>' +
@@ -3076,12 +3221,13 @@ function renderSession(data) {
3076
3221
  let badge = '';
3077
3222
  if (t.bound) {
3078
3223
  const dot = t.online ? '<span class="dot on"></span>' : '<span class="dot idle"></span>';
3079
- badge = `<span class="bind-badge">${dot}${esc(t.boundChannel || '')}·${esc(shortAid(t.boundPeer || ''))}</span>`;
3224
+ badge = `<span class="bind-badge">${dot}${esc(t.boundChannel || '')}·${esc(shortAid(t.boundSelf || ''))}</span>`;
3080
3225
  }
3226
+ const sourceBadge = renderSessionSourceBadge(t.sourceType);
3081
3227
  const msgs = `<span class="msg-count" title="${tf('sessions.messageCountTitle', { user: t.userMsgs || 0, total: t.totalMsgs || 0 })}">💬 ${t.userMsgs || 0}/${t.totalMsgs || 0}</span>`;
3082
3228
  listHtml += `<div class="list-item${sel}" data-sid="${esc(t.id)}">` +
3083
3229
  `<div class="name">${esc(title)}</div>` +
3084
- `<div class="sub">${fmtAgo(t.lastActivity)} · ${msgs}${t.gitBranch ? ' · ' + esc(t.gitBranch) : ''}${badge}</div>` +
3230
+ `<div class="sub">${fmtAgo(t.lastActivity)} · ${msgs} · ${sourceBadge}${badge}</div>` +
3085
3231
  '</div>';
3086
3232
  }
3087
3233
  listHtml += '</div>';
@@ -3091,19 +3237,24 @@ function renderSession(data) {
3091
3237
  const baseagentSel = $('#sess-baseagent');
3092
3238
  if (baseagentSel) baseagentSel.onchange = () => {
3093
3239
  console.log('[ecweb] Baseagent changed to:', baseagentSel.value);
3094
- sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value };
3240
+ sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value, turnBefore: null };
3095
3241
  console.log('[ecweb] Subscribing to session with baseagent:', sessSel.baseagent);
3096
3242
  subscribe('session', { baseagent: sessSel.baseagent });
3097
3243
  };
3098
3244
  const projSel = $('#sess-project');
3099
3245
  if (projSel) projSel.onchange = () => {
3100
- sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent };
3246
+ sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent, turnBefore: null };
3101
3247
  subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
3102
3248
  };
3103
3249
  const filterToggle = $('#sess-filter-toggle');
3104
- if (filterToggle) filterToggle.onchange = () => { sessFilterNormal = filterToggle.checked; renderSession(state.session); };
3250
+ if (filterToggle) filterToggle.onchange = () => { sessShowAll = filterToggle.checked; renderSession(state.session); };
3251
+ const sourceTypeSelect = $('#sess-source-type');
3252
+ if (sourceTypeSelect) sourceTypeSelect.onchange = () => {
3253
+ sessSourceFilter = sourceTypeSelect.value;
3254
+ renderSession(state.session);
3255
+ };
3105
3256
  $('#sess-list').querySelectorAll('.list-item').forEach(item => {
3106
- item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent }; subscribe('session', sessSel); };
3257
+ item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent, turnBefore: null }; subscribe('session', sessSel); };
3107
3258
  });
3108
3259
 
3109
3260
  // 右:transcript 详情
@@ -3113,11 +3264,22 @@ function renderSession(data) {
3113
3264
  const h = data.header || {};
3114
3265
  const atBottom = detail.scrollHeight - detail.scrollTop - detail.clientHeight < 60;
3115
3266
  let html = renderSessHeader(h, data.baseagent || sessSel.baseagent);
3267
+ if (data.turnPage?.hasMore && data.turnPage.nextBefore != null) {
3268
+ html += `<button class="ctrl-btn sess-load-more" type="button" data-before="${data.turnPage.nextBefore}">${t('sessions.loadOlder') || '加载更早内容'}</button>`;
3269
+ }
3116
3270
  html += '<div class="turn-list">' + (sessChatMode ? renderChatView(turns) : renderFullView(turns)) + '</div>';
3117
3271
  detail.innerHTML = html;
3118
3272
 
3273
+ const loadMore = detail.querySelector('.sess-load-more');
3274
+ if (loadMore) loadMore.onclick = () => {
3275
+ sessSel = { ...sessSel, turnBefore: Number(loadMore.dataset.before) };
3276
+ subscribe('session', sessSel);
3277
+ };
3278
+
3119
3279
  const toggle = $('#chat-toggle');
3120
3280
  if (toggle) toggle.onclick = () => { sessChatMode = !sessChatMode; renderSession(state.session); };
3281
+ setupContentCollapses(detail);
3282
+ setupTruncatedSessionResults(detail, data);
3121
3283
  if (atBottom) detail.scrollTop = detail.scrollHeight;
3122
3284
  }
3123
3285
 
@@ -3126,13 +3288,19 @@ function renderFullView(turns) {
3126
3288
  let html = '';
3127
3289
  for (const t of turns) {
3128
3290
  const cat = t.category || t.role;
3291
+ const blocks = t.blocks || [];
3292
+ // Hide already-cached empty assistant turns until their transcript is reparsed.
3293
+ if (cat === 'model_output' && !blocks.length) continue;
3129
3294
  const c = CAT_META[cat] || CAT_META.system;
3130
3295
  const usage = (t.inputTokens || t.outputTokens)
3131
3296
  ? `<span class="turn-usage">${esc(t.model || '')} · in ${t.inputTokens || 0} / out ${t.outputTokens || 0}</span>` : '';
3132
3297
  html += `<div class="turn cat-${cat}">` +
3133
3298
  `<div class="turn-head"><span class="turn-role">${c.icon} ${c.label()}</span>` +
3134
3299
  `<span class="turn-time">${t.ts ? fmtTime(t.ts) : ''}</span>${usage}</div>` +
3135
- `<div class="turn-blocks">${renderBlocks(t.blocks || [])}</div></div>`;
3300
+ `<div class="turn-blocks">${renderBlocks(blocks, {
3301
+ collapseText: cat === 'model_output' || cat === 'user_input',
3302
+ collapseKind: cat === 'user_input' ? 'user-input' : 'model-output',
3303
+ })}</div></div>`;
3136
3304
  }
3137
3305
  return html;
3138
3306
  }
@@ -3179,7 +3347,9 @@ function renderChatView(turns) {
3179
3347
  while (next < items.length && items[next].type === 'proc') { group.push(items[next]); next++; }
3180
3348
  const peer = it.peer ? shortAid(it.peer) : '';
3181
3349
  const time = `${it.ts ? fmtTime(it.ts) : ''}${peer ? ' → ' + esc(peer) : ''}`;
3182
- html += `<div class="chat-row ${it.type}"><div class="chat-bubble">${esc(it.text)}</div>` +
3350
+ const kind = it.type === 'out' ? 'model-output' : 'user-input';
3351
+ const bubble = renderCollapsibleText(it.text, `chat-bubble ${kind}-content`, kind);
3352
+ html += `<div class="chat-row ${it.type}">${bubble}` +
3183
3353
  '<div class="chat-meta-row">' +
3184
3354
  (group.length ? renderProcGroup(group, true, time) : `<div class="chat-time">${time}</div>`) +
3185
3355
  '</div></div>';
@@ -3201,7 +3371,10 @@ function renderProcGroup(group, inline = false, time = '') {
3201
3371
  const c = CAT_META[g.cat] || CAT_META.system;
3202
3372
  inner += `<div class="turn cat-${g.cat}"><div class="turn-head"><span class="turn-role">${c.icon} ${c.label()}</span>` +
3203
3373
  `<span class="turn-time">${g.ts ? fmtTime(g.ts) : ''}</span></div>` +
3204
- `<div class="turn-blocks">${renderBlocks(g.blocks)}</div></div>`;
3374
+ `<div class="turn-blocks">${renderBlocks(g.blocks, {
3375
+ collapseText: g.cat === 'model_output' || g.cat === 'user_input',
3376
+ collapseKind: g.cat === 'user_input' ? 'user-input' : 'model-output',
3377
+ })}</div></div>`;
3205
3378
  }
3206
3379
  const summary = tf('sessions.procSummary', { count: group.length });
3207
3380
  if (inline) {
@@ -3220,6 +3393,7 @@ const CAT_META = {
3220
3393
  tool_call: { label: () => t('sessions.turnType.toolCall'), icon: '🟣' },
3221
3394
  tool_result: { label: () => t('sessions.turnType.toolResult'), icon: '🟠' },
3222
3395
  msg_send: { label: () => t('sessions.turnType.msgSend'), icon: '📤' },
3396
+ subtask: { label: () => t('sessions.turnType.subtask'), icon: '◆' },
3223
3397
  system: { label: () => t('sessions.turnType.system'), icon: '⚪' },
3224
3398
  };
3225
3399
 
@@ -3236,7 +3410,7 @@ function renderSessHeader(h, baseagent) {
3236
3410
  let bind = '';
3237
3411
  if (h.bound) {
3238
3412
  const dot = h.online ? `<span class="dot on"></span>${t('common.online')}` : `<span class="dot idle"></span>${t('common.offline')}`;
3239
- bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundPeer || ''))} ${dot}</span>`;
3413
+ bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundSelf || ''))} ${dot}</span>`;
3240
3414
  }
3241
3415
  return '<div class="sess-header">' +
3242
3416
  '<div class="sh-title-row">' +
@@ -3269,6 +3443,7 @@ function renderCatBar(counts) {
3269
3443
  ['tool_call', counts.toolCall],
3270
3444
  ['tool_result', counts.toolResult],
3271
3445
  ['msg_send', counts.msgSend],
3446
+ ['subtask', counts.subtask],
3272
3447
  ];
3273
3448
  let s = '<div class="sh-cats">';
3274
3449
  for (const [cat, n] of items) {
@@ -3295,11 +3470,50 @@ function toolParamValue(value) {
3295
3470
  try { return JSON.stringify(value, null, 2); } catch { return String(value); }
3296
3471
  }
3297
3472
 
3298
- function renderBlocks(blocks) {
3473
+ function setupContentCollapses(container) {
3474
+ const contents = container.querySelectorAll('.collapse-content');
3475
+ contents.forEach(content => {
3476
+ const body = content.querySelector('.collapse-body');
3477
+ const arrowToggle = content.querySelector('.collapse-arrow-toggle');
3478
+ if (!body || !arrowToggle) return;
3479
+
3480
+ body.classList.remove('is-collapsed');
3481
+ content.classList.remove('is-collapsed');
3482
+ body.style.maxHeight = '';
3483
+ arrowToggle.hidden = true;
3484
+
3485
+ const styles = getComputedStyle(body);
3486
+ const previewLines = Number.parseInt(styles.getPropertyValue('--collapse-preview-lines'), 10) || 12;
3487
+ const lineHeight = Number.parseFloat(styles.lineHeight) || 17;
3488
+ const padding = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
3489
+ const previewHeight = previewLines * lineHeight + padding;
3490
+ if (body.scrollHeight <= previewHeight + 1) return;
3491
+
3492
+ const setExpanded = expanded => {
3493
+ body.classList.toggle('is-collapsed', !expanded);
3494
+ content.classList.toggle('is-collapsed', !expanded);
3495
+ body.style.maxHeight = `${expanded ? body.scrollHeight : previewHeight}px`;
3496
+ arrowToggle.setAttribute('aria-expanded', String(expanded));
3497
+ arrowToggle.setAttribute('aria-label', t(expanded ? 'sessions.content.collapse' : 'sessions.content.expand'));
3498
+ };
3499
+ setExpanded(false);
3500
+ arrowToggle.hidden = false;
3501
+ arrowToggle.onclick = () => setExpanded(body.classList.contains('is-collapsed'));
3502
+ });
3503
+ }
3504
+
3505
+ function renderCollapsibleText(text, classes, kind = 'model-output') {
3506
+ return `<div class="${classes} collapse-content"><div class="collapse-body ${kind}-body">${esc(text)}</div>` +
3507
+ '<button class="collapse-arrow-toggle" type="button" hidden aria-expanded="false"><span class="collapse-arrow-icon" aria-hidden="true"></span></button></div>';
3508
+ }
3509
+
3510
+ function renderBlocks(blocks, options = {}) {
3299
3511
  let out = '';
3300
3512
  for (const b of blocks) {
3301
3513
  if (b.kind === 'text') {
3302
- out += `<div class="blk blk-text">${esc(b.text)}</div>`;
3514
+ out += options.collapseText
3515
+ ? renderCollapsibleText(b.text, `blk blk-text ${(options.collapseKind || 'model-output')}-content`, options.collapseKind)
3516
+ : `<div class="blk blk-text">${esc(b.text)}</div>`;
3303
3517
  } else if (b.kind === 'thinking') {
3304
3518
  out += `<details class="blk blk-thinking" open><summary>💭 ${t('sessions.block.thinking')}</summary><div class="blk-thinking-body">${esc(b.text)}</div></details>`;
3305
3519
  } else if (b.kind === 'tool_use') {
@@ -3312,15 +3526,73 @@ function renderBlocks(blocks) {
3312
3526
  for (const p of entries) {
3313
3527
  params += `<div class="tool-param"><span class="pk">${esc(p.k)}</span><code class="pv">${esc(p.v)}</code></div>`;
3314
3528
  }
3315
- out += `<details class="blk blk-tool" open><summary class="tool-head">${icon} <span class="tool-name">${esc(tool)}</span></summary><div class="tool-params">${params}</div></details>`;
3529
+ const toolParams = params
3530
+ ? `<div class="tool-params collapse-content"><div class="tool-params-body collapse-body">${params}</div>` +
3531
+ '<button class="collapse-arrow-toggle" type="button" hidden aria-expanded="false"><span class="collapse-arrow-icon" aria-hidden="true"></span></button></div>'
3532
+ : '';
3533
+ out += `<details class="blk blk-tool" open><summary class="tool-head">${icon} <span class="tool-name">${esc(tool)}</span></summary>${toolParams}</details>`;
3316
3534
  } else if (b.kind === 'tool_result') {
3317
3535
  const cls = b.isError ? 'blk-result err' : 'blk-result';
3318
- out += `<details class="blk ${cls}" open><summary>${b.isError ? '✗ ' + t('sessions.block.resultError') : '↳ ' + t('sessions.block.result')}</summary><pre class="result-body">${esc(b.text)}</pre></details>`;
3536
+ const loadFull = b.truncated && b.resultKey
3537
+ ? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
3538
+ : '';
3539
+ out += `<details class="blk ${cls}" open><summary>${b.isError ? '✗ ' + t('sessions.block.resultError') : '↳ ' + t('sessions.block.result')}</summary>` +
3540
+ `<div class="result-content collapse-content"><pre class="result-body collapse-body">${esc(b.text)}</pre>${loadFull}<button class="collapse-arrow-toggle" type="button" hidden aria-expanded="false"><span class="collapse-arrow-icon" aria-hidden="true"></span></button></div></details>`;
3541
+ } else if (b.kind === 'subtask') {
3542
+ const knownStatuses = ['running', 'completed', 'failed', 'stopped', 'killed'];
3543
+ const status = knownStatuses.includes(String(b.status)) ? String(b.status) : 'running';
3544
+ const statusLabel = t(`sessions.subtask.status.${status}`);
3545
+ const meta = [];
3546
+ if (b.subagentType) meta.push(esc(b.subagentType));
3547
+ if (b.toolUses) meta.push(esc(tf('sessions.subtask.tools', { count: fmtNum(b.toolUses) })));
3548
+ if (b.totalTokens) meta.push(esc(tf('sessions.subtask.tokens', { count: fmtNum(b.totalTokens) })));
3549
+ if (b.durationMs) meta.push(esc(fmtDur(b.durationMs / 1000)));
3550
+ if (b.terminalReason) meta.push(esc(tf('sessions.subtask.reason', { reason: b.terminalReason })));
3551
+ const progress = b.progress && b.progress !== b.description
3552
+ ? `<div class="subtask-progress">${esc(b.progress)}</div>`
3553
+ : '';
3554
+ out += `<div class="blk blk-subtask status-${status}">` +
3555
+ '<div class="subtask-main">' +
3556
+ `<span class="subtask-status-dot" aria-hidden="true"></span>` +
3557
+ `<span class="subtask-status">${esc(statusLabel)}</span>` +
3558
+ `<span class="subtask-title">${esc(b.description || b.taskId || '')}</span>` +
3559
+ '</div>' +
3560
+ (meta.length ? `<div class="subtask-meta">${meta.join('<span>·</span>')}</div>` : '') +
3561
+ progress +
3562
+ '</div>';
3319
3563
  }
3320
3564
  }
3321
3565
  return out;
3322
3566
  }
3323
3567
 
3568
+ function setupTruncatedSessionResults(container, data) {
3569
+ container.querySelectorAll('.result-load-full').forEach(button => {
3570
+ button.onclick = async () => {
3571
+ const key = button.dataset.resultKey;
3572
+ if (!key || button.disabled) return;
3573
+ button.disabled = true;
3574
+ button.textContent = t('common.loading');
3575
+ try {
3576
+ const result = await sessionResultSend({
3577
+ baseagent: data.baseagent || sessSel.baseagent,
3578
+ project: data.project || sessSel.project,
3579
+ sessionId: data.sessionId || sessSel.sessionId,
3580
+ resultKey: key,
3581
+ });
3582
+ const body = button.parentElement?.querySelector('.result-body');
3583
+ if (!body || !result?.text) throw new Error('not found');
3584
+ body.textContent = result.text;
3585
+ body.classList.remove('is-collapsed');
3586
+ button.parentElement?.classList.remove('is-collapsed');
3587
+ button.remove();
3588
+ } catch {
3589
+ button.disabled = false;
3590
+ button.textContent = t('sessions.loadFullResult') || '加载完整结果';
3591
+ }
3592
+ };
3593
+ });
3594
+ }
3595
+
3324
3596
  // ── 通用 Menu 协议辅助(mResp / toast,供 Agents / System / Triggers 复用)──
3325
3597
 
3326
3598
  // 提取 menu.response 的 data/error
@@ -5474,12 +5746,18 @@ function triggerOriginalEditState(t) {
5474
5746
  const id = triggerIdOf(t);
5475
5747
  const agentAid = triggerAgentOf(t);
5476
5748
  const rawFeedbackStrategy = String(feedback.strategy ?? t?.feedbackStrategy ?? 'target');
5749
+ const executionType = String(execution.type ?? t?.executionType ?? 'target_session');
5750
+ const agentBaseagent = String(triggerAgentInfo(agentAid)?.baseagent ?? triggerActiveBaseagentForAgent(agentAid));
5751
+ const persistedBaseagent = typeof execution.baseagent === 'string' && execution.baseagent.trim()
5752
+ ? execution.baseagent.trim()
5753
+ : (typeof t?.baseagent === 'string' && t.baseagent.trim() ? t.baseagent.trim() : '');
5754
+ const baseagent = persistedBaseagent || agentBaseagent;
5477
5755
  return {
5478
5756
  id,
5479
5757
  agentAid,
5480
5758
  name: String(definition?.name ?? t?.name ?? t?.label ?? ''),
5481
5759
  enabled: triggerIsEnabled(t),
5482
- executionType: String(execution.type ?? t?.executionType ?? 'target_session'),
5760
+ executionType,
5483
5761
  scriptPath: String(execution.script?.path ?? ''),
5484
5762
  scriptRuntime: String(execution.script?.runtime ?? 'node'),
5485
5763
  scriptTimeoutMs: Number(execution.script?.timeoutMs ?? 30_000),
@@ -5487,7 +5765,11 @@ function triggerOriginalEditState(t) {
5487
5765
  scheduleValue: source.scheduleValue,
5488
5766
  eventFilter: definition?.source?.type === 'event' ? (definition.source.filter ?? null) : null,
5489
5767
  timezone: String(source.timezone ?? ''),
5490
- baseagent: String(triggerAgentInfo(agentAid)?.baseagent ?? t?.baseagent ?? triggerActiveBaseagentForAgent(agentAid)),
5768
+ baseagent,
5769
+ agentBaseagent,
5770
+ baseagentMismatch: executionType === 'trigger_session'
5771
+ && Boolean(persistedBaseagent)
5772
+ && persistedBaseagent !== agentBaseagent,
5491
5773
  prompt: execution.type === 'script' ? '' : triggerPrompt(definition, t),
5492
5774
  model: String(execution.model ?? t?.model ?? ''),
5493
5775
  effort: String(execution.effort ?? t?.effort ?? ''),
@@ -6710,6 +6992,7 @@ function openTriggerEditDrawer(id, agent) {
6710
6992
  `<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>` +
6711
6993
  `<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>` +
6712
6994
  `</div>` +
6995
+ (original.baseagentMismatch ? `<div class="trig-baseagent-note">${esc(tf('triggers.baseagent.mismatch', { trigger: original.baseagent, agent: original.agentBaseagent }))}</div>` : '') +
6713
6996
  `<div class="trig-script-feedback">` +
6714
6997
  `<div class="trig-feedback-templates-title">${t('triggers.field.outputTemplate')}</div>` +
6715
6998
  `<div class="trig-feedback-templates">` +
@@ -6990,6 +7273,13 @@ function updateThemeButton() {
6990
7273
  }
6991
7274
 
6992
7275
  function disposeWindowChart(key) {
7276
+ const observerKey = '__ecwebChartResizeObserver_' + key;
7277
+ const observer = window[observerKey];
7278
+ if (observer) {
7279
+ observer.disconnect();
7280
+ window[observerKey] = null;
7281
+ }
7282
+
6993
7283
  const chart = window[key];
6994
7284
  if (chart && typeof chart.dispose === 'function') {
6995
7285
  chart.dispose();
@@ -7048,6 +7338,28 @@ function initTheme() {
7048
7338
  }
7049
7339
  }
7050
7340
 
7341
+ function chartThemeValue(name, fallback) {
7342
+ var value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
7343
+ return value || fallback;
7344
+ }
7345
+
7346
+ function chartTooltipOptions(isDark) {
7347
+ return {
7348
+ backgroundColor: chartThemeValue('--bg2', isDark ? '#161b22' : '#ffffff'),
7349
+ borderColor: chartThemeValue('--border', isDark ? '#30363d' : '#e2e6ed'),
7350
+ borderWidth: 1,
7351
+ padding: [10, 12],
7352
+ textStyle: {
7353
+ color: chartThemeValue('--fg', isDark ? '#e6edf3' : '#1a202c'),
7354
+ fontSize: 12,
7355
+ lineHeight: 20,
7356
+ },
7357
+ extraCssText: 'border-radius:8px;box-shadow:' + (isDark
7358
+ ? '0 14px 34px rgba(0,0,0,0.42)'
7359
+ : '0 14px 34px rgba(31,41,55,0.16)') + ';',
7360
+ };
7361
+ }
7362
+
7051
7363
  // ── Usage 相关函数 ──
7052
7364
  function fmtTokens(n) {
7053
7365
  if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
@@ -7255,12 +7567,19 @@ function formatDatetimeLocal(date) {
7255
7567
  }
7256
7568
 
7257
7569
  // 模型访问明细查询
7570
+ var _detailInited = false;
7571
+
7258
7572
  function initDetailQuery() {
7259
7573
  // 填充Agent选择器
7260
7574
  loadDetailAgentList();
7261
7575
  // 填充Model选择器(按上面总览的时间范围)
7262
7576
  loadDetailModelList();
7263
7577
 
7578
+ // 事件只绑一次:initOverviewFilters 会在每次切回 Overview 子 Tab 时重复调用,
7579
+ // 重复绑定会让一次 change 触发 N 次并发查询,慢的旧响应可能覆盖新响应(表现为“切换无效”)。
7580
+ if (_detailInited) return;
7581
+ _detailInited = true;
7582
+
7264
7583
  // 绑定分页大小变化
7265
7584
  const pageSizeEl = $('#detail-page-size');
7266
7585
  if (pageSizeEl) {
@@ -7347,10 +7666,8 @@ async function loadDetailAgentList() {
7347
7666
  option.textContent = a.agent_name || a.agent_aid.split('.')[0];
7348
7667
  selectEl.appendChild(option);
7349
7668
  });
7350
- // 默认选中第一个agent
7351
- if (agents.length > 0) {
7352
- selectEl.value = agents[0].agent_aid;
7353
- }
7669
+ // 默认查询全部智能体(首项 value=""),不要自动切到第一个 Agent。
7670
+ selectEl.value = '';
7354
7671
  // 加载完成后自动查询一次
7355
7672
  queryDetailUsage();
7356
7673
  }
@@ -7394,7 +7711,11 @@ async function loadDetailModelList() {
7394
7711
  } catch {}
7395
7712
  }
7396
7713
 
7714
+ var _detailQuerySeq = 0;
7715
+
7397
7716
  async function queryDetailUsage() {
7717
+ // 请求序号:并发查询时只认最后一次的响应,避免慢的旧响应覆盖新结果
7718
+ const seq = ++_detailQuerySeq;
7398
7719
  // 使用总览的时间范围
7399
7720
  const timeRange = window._currentOverviewTimeRange || {};
7400
7721
  const fromTs = timeRange.fromTs;
@@ -7421,13 +7742,16 @@ async function queryDetailUsage() {
7421
7742
  const resp = await fetch(authApiUrl('api/stats/detail?' + params.toString()), {
7422
7743
  headers: { Authorization: 'Bearer ' + localStorage.getItem(TOKEN_KEY) }
7423
7744
  });
7745
+ if (seq !== _detailQuerySeq) return; // 已有更新的查询发出,丢弃本次结果
7424
7746
  if (!resp.ok) {
7425
7747
  showDetailError(t('usage.detail.error'));
7426
7748
  return;
7427
7749
  }
7428
7750
  const result = await resp.json();
7751
+ if (seq !== _detailQuerySeq) return;
7429
7752
  renderDetailTable(result.data, result.total, page, pageSize);
7430
7753
  } catch {
7754
+ if (seq !== _detailQuerySeq) return;
7431
7755
  showDetailError(t('usage.detail.error'));
7432
7756
  }
7433
7757
  }
@@ -7627,9 +7951,11 @@ function initExplorer() {
7627
7951
  // 初始化时间范围选择
7628
7952
  initExplorerTimeFilters();
7629
7953
 
7630
- // 绑定查询按钮
7631
- var btn = $('#exp-query-btn');
7632
- if (btn) btn.onclick = runExplorerQuery;
7954
+ // 模型、粒度下拉框变更后立即查询,不再需要额外的查询按钮。
7955
+ var modelFilter = $('#exp-model');
7956
+ if (modelFilter) modelFilter.addEventListener('change', runExplorerQuery);
7957
+ var granularityFilter = $('#exp-granularity');
7958
+ if (granularityFilter) granularityFilter.addEventListener('change', runExplorerQuery);
7633
7959
 
7634
7960
  // Load sidebar lists
7635
7961
  loadExplorerSidebar();
@@ -8076,7 +8402,10 @@ async function runExplorerQuery() {
8076
8402
  _explorerChart = echarts.init(chartEl, isDark ? 'dark' : null);
8077
8403
  var periods = data.map(function(r) { return r.period; });
8078
8404
  _explorerChart.setOption({
8079
- tooltip: { trigger: 'axis' },
8405
+ tooltip: {
8406
+ trigger: 'axis',
8407
+ ...chartTooltipOptions(isDark),
8408
+ },
8080
8409
  legend: { data: [t('usage.card.input'), t('usage.card.output')], top: 0, textStyle: { fontSize: 11 } },
8081
8410
  grid: { top: 30, bottom: 30, left: 60, right: 16 },
8082
8411
  xAxis: { type: 'category', data: periods, axisLabel: { fontSize: 10, rotate: 30 } },
@@ -8122,6 +8451,52 @@ function disposeMonitorCharts() {
8122
8451
  ['_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
8123
8452
  }
8124
8453
 
8454
+ function scheduleAfterLayout(callback) {
8455
+ if (typeof requestAnimationFrame === 'function') return requestAnimationFrame(callback);
8456
+ return setTimeout(callback, 16);
8457
+ }
8458
+
8459
+ function resizeChartAfterLayout(chart) {
8460
+ scheduleAfterLayout(function () {
8461
+ if (!chart || (typeof chart.isDisposed === 'function' && chart.isDisposed())) return;
8462
+ chart.resize();
8463
+ });
8464
+ }
8465
+
8466
+ function observeMonitorChartSize(el, varKey, chart) {
8467
+ const observerKey = '__ecwebChartResizeObserver_' + varKey;
8468
+ const previous = window[observerKey];
8469
+ if (previous) previous.disconnect();
8470
+
8471
+ if (typeof ResizeObserver === 'function') {
8472
+ const observer = new ResizeObserver(function () {
8473
+ if (window[varKey] !== chart || (typeof chart.isDisposed === 'function' && chart.isDisposed())) {
8474
+ observer.disconnect();
8475
+ return;
8476
+ }
8477
+ chart.resize();
8478
+ });
8479
+ observer.observe(el);
8480
+ window[observerKey] = observer;
8481
+ }
8482
+
8483
+ resizeChartAfterLayout(chart);
8484
+ }
8485
+
8486
+ function deferMonitorChartUntilVisible(el, render) {
8487
+ let attempts = 0;
8488
+ const retry = function () {
8489
+ if (!el.isConnected) return;
8490
+ if (el.clientWidth > 0 && el.clientHeight > 0) {
8491
+ render();
8492
+ return;
8493
+ }
8494
+ // The AUN proxy can deliver the first snapshot before its page layout settles.
8495
+ if (++attempts < 30) scheduleAfterLayout(retry);
8496
+ };
8497
+ scheduleAfterLayout(retry);
8498
+ }
8499
+
8125
8500
  function bindMonRangeTabs(wrap) {
8126
8501
  var tabs = wrap.querySelectorAll('.mon-range');
8127
8502
  if (!tabs.length) return;
@@ -8289,8 +8664,18 @@ function renderMonitor(data) {
8289
8664
  function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8290
8665
  var el = $('#' + elId);
8291
8666
  if (!el) return;
8292
- if (!window[varKey]) window[varKey] = echarts.init(el, isDark ? 'dark' : null);
8293
- window[varKey].setOption({
8667
+ const render = function () {
8668
+ var chart = window[varKey];
8669
+ if (chart && typeof chart.getDom === 'function' && chart.getDom() !== el) {
8670
+ disposeWindowChart(varKey);
8671
+ chart = null;
8672
+ }
8673
+ if (!chart) {
8674
+ chart = echarts.init(el, isDark ? 'dark' : null);
8675
+ window[varKey] = chart;
8676
+ observeMonitorChartSize(el, varKey, chart);
8677
+ }
8678
+ chart.setOption({
8294
8679
  title: { text: title, left: 'center', top: 4, textStyle: { fontSize: 12, color: isDark ? '#e6edf3' : '#1a202c' } },
8295
8680
  legend: {
8296
8681
  show: series.length > 0,
@@ -8303,6 +8688,7 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8303
8688
  },
8304
8689
  tooltip: {
8305
8690
  trigger: 'axis',
8691
+ ...chartTooltipOptions(isDark),
8306
8692
  formatter: function (params) {
8307
8693
  var lines = [params[0].axisValue];
8308
8694
  params.forEach(function (pt) {
@@ -8330,7 +8716,15 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
8330
8716
  };
8331
8717
  }),
8332
8718
  animation: false,
8333
- });
8719
+ });
8720
+ resizeChartAfterLayout(chart);
8721
+ };
8722
+
8723
+ if (el.clientWidth <= 0 || el.clientHeight <= 0) {
8724
+ deferMonitorChartUntilVisible(el, render);
8725
+ return;
8726
+ }
8727
+ render();
8334
8728
  }
8335
8729
 
8336
8730
  function renderRoles(data) {