ec-web 0.0.5 → 0.0.7

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',
@@ -331,6 +334,8 @@ const translations = {
331
334
  'sessions.block.thinking': '思考',
332
335
  'sessions.block.result': '结果',
333
336
  'sessions.block.resultError': '结果',
337
+ 'sessions.content.expand': '展开内容',
338
+ 'sessions.content.collapse': '收起内容',
334
339
  'sessions.contextTitle': '最后一轮喂给模型的完整上下文大小',
335
340
  'sessions.costTitle': '累计费用(按模型定价估算)',
336
341
  'sessions.msgUnit': '条',
@@ -1123,6 +1128,7 @@ const translations = {
1123
1128
  'messages.empty.noAgents': 'No agents',
1124
1129
  'messages.empty.noPeers': 'No chats',
1125
1130
  'messages.empty.noMessages': 'No messages',
1131
+ 'messages.loadOlder': 'Load older messages',
1126
1132
  'messages.tag.group': 'Group',
1127
1133
  'messages.tag.groupShort': 'G',
1128
1134
  'messages.tag.main': 'Main',
@@ -1163,6 +1169,8 @@ const translations = {
1163
1169
  'sessions.empty.noMatch': 'No matching sessions',
1164
1170
  'sessions.empty.noSessions': 'No sessions in this project',
1165
1171
  'sessions.empty.noContent': 'No content in this session',
1172
+ 'sessions.loadOlder': 'Load older content',
1173
+ 'sessions.loadFullResult': 'Load full result',
1166
1174
  'sessions.header.project': 'Project',
1167
1175
  'sessions.header.session': 'Session',
1168
1176
  'sessions.stat.context': '📐 {tokens} ctx',
@@ -1206,6 +1214,8 @@ const translations = {
1206
1214
  'sessions.block.thinking': 'Thinking',
1207
1215
  'sessions.block.result': 'Result',
1208
1216
  'sessions.block.resultError': 'Result',
1217
+ 'sessions.content.expand': 'Expand content',
1218
+ 'sessions.content.collapse': 'Collapse content',
1209
1219
  'sessions.contextTitle': 'Full context size sent to the model in the last turn',
1210
1220
  'sessions.costTitle': 'Estimated cumulative cost by model pricing',
1211
1221
  'sessions.msgUnit': 'msgs',
@@ -1934,7 +1944,7 @@ function connect() {
1934
1944
  console.log('[ecweb] Available baseagents:', availableBaseagents, 'Selected:', sessSel.baseagent);
1935
1945
  // 重新订阅当前视图(带上正确的参数)
1936
1946
  if (currentView === 'session') {
1937
- subscribe('session', { sessionId: sessSel.sessionId, project: sessSel.project, baseagent: sessSel.baseagent });
1947
+ subscribe('session', sessSel);
1938
1948
  } else if (currentView === 'roleManagement') {
1939
1949
  subscribeRoleManagementSubview();
1940
1950
  } else if (currentView === 'system' && systemContentMode === 'monitor') {
@@ -1969,6 +1979,11 @@ function connect() {
1969
1979
  if (pend) { delete _ipcPending[msg.requestId]; pend.resolve(msg.data); }
1970
1980
  return;
1971
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
+ }
1972
1987
  if (msg.type === 'snapshot' || msg.type === 'delta') {
1973
1988
  console.log('[ecweb] Received', msg.type, 'for view:', msg.view, 'currentView:', currentView);
1974
1989
  // 系统基础快照不主动执行健康检查;保留用户点击按钮后得到的 check/upgrade 结果。
@@ -1978,6 +1993,24 @@ function connect() {
1978
1993
  check: msg.data.check ?? state.system.check,
1979
1994
  upgrade: state.system.upgrade ?? msg.data.upgrade,
1980
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 };
1981
2014
  } else {
1982
2015
  state[msg.view] = msg.data;
1983
2016
  }
@@ -2040,6 +2073,8 @@ function menuSend(payload) {
2040
2073
 
2041
2074
  const _ipcPending = {};
2042
2075
  let _ipcSeq = 0;
2076
+ const _sessionResultPending = {};
2077
+ let _sessionResultSeq = 0;
2043
2078
  function ipcSend(payload, timeoutMs) {
2044
2079
  return new Promise((resolve, reject) => {
2045
2080
  if (!ws || ws.readyState !== WebSocket.OPEN) { reject(new Error(t('common.notConnected'))); return; }
@@ -2053,14 +2088,26 @@ function ipcSend(payload, timeoutMs) {
2053
2088
  });
2054
2089
  }
2055
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
+
2056
2103
  // 心跳
2057
2104
  setInterval(() => {
2058
2105
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'ping' }));
2059
2106
  }, 20000);
2060
2107
 
2061
2108
  // ── Tab 切换 ──
2062
- let msgSel = { aid: null, peer: null };
2063
- 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 };
2064
2111
  let trigSel = { agent: null };
2065
2112
  let trigAgentsCollapsed = false;
2066
2113
  const _trigOps = new Map(); // Map<triggerId, string>
@@ -2126,8 +2173,8 @@ function switchView(view) {
2126
2173
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
2127
2174
  document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
2128
2175
  // 切换时按当前选择恢复订阅
2129
- if (view === 'msg') subscribe('msg', { aid: msgSel.aid, peer: msgSel.peer });
2130
- 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);
2131
2178
  else if (view === 'cache') subscribe('cache', {});
2132
2179
  else if (view === 'system' && systemContentMode === 'monitor') subscribe('monitor', { range: monRange });
2133
2180
  else if (view === 'system') subscribe('system', {});
@@ -2935,7 +2982,7 @@ function renderMsg(data) {
2935
2982
  if (!selectedAidExists) {
2936
2983
  const nextAid = aids[0]?.aid || null;
2937
2984
  const selectionChanged = msgSel.aid !== nextAid || msgSel.peer !== null;
2938
- msgSel = { aid: nextAid, peer: null };
2985
+ msgSel = { aid: nextAid, peer: null, before: null };
2939
2986
  if (selectionChanged && nextAid) subscribe('msg', msgSel);
2940
2987
  }
2941
2988
  const scopeMatches = data.scope === msgSel.aid;
@@ -2945,7 +2992,7 @@ function renderMsg(data) {
2945
2992
  if (!selectedPeerExists) {
2946
2993
  const nextPeer = peers[0]?.peerId || null;
2947
2994
  const selectionChanged = msgSel.peer !== nextPeer;
2948
- msgSel = { aid: msgSel.aid, peer: nextPeer };
2995
+ msgSel = { aid: msgSel.aid, peer: nextPeer, before: null };
2949
2996
  if (selectionChanged && nextPeer) subscribe('msg', msgSel);
2950
2997
  }
2951
2998
  }
@@ -2970,7 +3017,7 @@ function renderMsg(data) {
2970
3017
  $('#msg-aids').querySelectorAll('.list-item').forEach(item => {
2971
3018
  item.onclick = () => {
2972
3019
  if (item.dataset.aid === msgSel.aid) return;
2973
- msgSel = { aid: item.dataset.aid, peer: null };
3020
+ msgSel = { aid: item.dataset.aid, peer: null, before: null };
2974
3021
  subscribe('msg', msgSel);
2975
3022
  };
2976
3023
  });
@@ -3013,7 +3060,7 @@ function renderMsg(data) {
3013
3060
  $('#msg-peers').querySelectorAll('.list-item').forEach(item => {
3014
3061
  item.onclick = () => {
3015
3062
  if (item.dataset.peer === msgSel.peer) return;
3016
- msgSel = { aid: msgSel.aid, peer: item.dataset.peer };
3063
+ msgSel = { aid: msgSel.aid, peer: item.dataset.peer, before: null };
3017
3064
  subscribe('msg', msgSel);
3018
3065
  };
3019
3066
  });
@@ -3031,6 +3078,9 @@ function renderMsg(data) {
3031
3078
  }
3032
3079
  const atBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 60;
3033
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
+ }
3034
3084
  for (const m of messages) {
3035
3085
  const cls = m.dir === 'in' ? 'in' : 'out';
3036
3086
  const arrow = m.dir === 'in' ? '↓' : '↑';
@@ -3053,6 +3103,11 @@ function renderMsg(data) {
3053
3103
  }
3054
3104
  msgHtml += messages.length ? '</div>' : `<div class="empty">${t('messages.empty.noMessages')}</div></div>`;
3055
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
+ };
3056
3111
  if (atBottom) stream.scrollTop = stream.scrollHeight;
3057
3112
  }
3058
3113
 
@@ -3182,13 +3237,13 @@ function renderSession(data) {
3182
3237
  const baseagentSel = $('#sess-baseagent');
3183
3238
  if (baseagentSel) baseagentSel.onchange = () => {
3184
3239
  console.log('[ecweb] Baseagent changed to:', baseagentSel.value);
3185
- sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value };
3240
+ sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value, turnBefore: null };
3186
3241
  console.log('[ecweb] Subscribing to session with baseagent:', sessSel.baseagent);
3187
3242
  subscribe('session', { baseagent: sessSel.baseagent });
3188
3243
  };
3189
3244
  const projSel = $('#sess-project');
3190
3245
  if (projSel) projSel.onchange = () => {
3191
- sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent };
3246
+ sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent, turnBefore: null };
3192
3247
  subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
3193
3248
  };
3194
3249
  const filterToggle = $('#sess-filter-toggle');
@@ -3199,7 +3254,7 @@ function renderSession(data) {
3199
3254
  renderSession(state.session);
3200
3255
  };
3201
3256
  $('#sess-list').querySelectorAll('.list-item').forEach(item => {
3202
- 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); };
3203
3258
  });
3204
3259
 
3205
3260
  // 右:transcript 详情
@@ -3209,11 +3264,22 @@ function renderSession(data) {
3209
3264
  const h = data.header || {};
3210
3265
  const atBottom = detail.scrollHeight - detail.scrollTop - detail.clientHeight < 60;
3211
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
+ }
3212
3270
  html += '<div class="turn-list">' + (sessChatMode ? renderChatView(turns) : renderFullView(turns)) + '</div>';
3213
3271
  detail.innerHTML = html;
3214
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
+
3215
3279
  const toggle = $('#chat-toggle');
3216
3280
  if (toggle) toggle.onclick = () => { sessChatMode = !sessChatMode; renderSession(state.session); };
3281
+ setupContentCollapses(detail);
3282
+ setupTruncatedSessionResults(detail, data);
3217
3283
  if (atBottom) detail.scrollTop = detail.scrollHeight;
3218
3284
  }
3219
3285
 
@@ -3222,13 +3288,19 @@ function renderFullView(turns) {
3222
3288
  let html = '';
3223
3289
  for (const t of turns) {
3224
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;
3225
3294
  const c = CAT_META[cat] || CAT_META.system;
3226
3295
  const usage = (t.inputTokens || t.outputTokens)
3227
3296
  ? `<span class="turn-usage">${esc(t.model || '')} · in ${t.inputTokens || 0} / out ${t.outputTokens || 0}</span>` : '';
3228
3297
  html += `<div class="turn cat-${cat}">` +
3229
3298
  `<div class="turn-head"><span class="turn-role">${c.icon} ${c.label()}</span>` +
3230
3299
  `<span class="turn-time">${t.ts ? fmtTime(t.ts) : ''}</span>${usage}</div>` +
3231
- `<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>`;
3232
3304
  }
3233
3305
  return html;
3234
3306
  }
@@ -3275,7 +3347,9 @@ function renderChatView(turns) {
3275
3347
  while (next < items.length && items[next].type === 'proc') { group.push(items[next]); next++; }
3276
3348
  const peer = it.peer ? shortAid(it.peer) : '';
3277
3349
  const time = `${it.ts ? fmtTime(it.ts) : ''}${peer ? ' → ' + esc(peer) : ''}`;
3278
- 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}` +
3279
3353
  '<div class="chat-meta-row">' +
3280
3354
  (group.length ? renderProcGroup(group, true, time) : `<div class="chat-time">${time}</div>`) +
3281
3355
  '</div></div>';
@@ -3297,7 +3371,10 @@ function renderProcGroup(group, inline = false, time = '') {
3297
3371
  const c = CAT_META[g.cat] || CAT_META.system;
3298
3372
  inner += `<div class="turn cat-${g.cat}"><div class="turn-head"><span class="turn-role">${c.icon} ${c.label()}</span>` +
3299
3373
  `<span class="turn-time">${g.ts ? fmtTime(g.ts) : ''}</span></div>` +
3300
- `<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>`;
3301
3378
  }
3302
3379
  const summary = tf('sessions.procSummary', { count: group.length });
3303
3380
  if (inline) {
@@ -3393,11 +3470,50 @@ function toolParamValue(value) {
3393
3470
  try { return JSON.stringify(value, null, 2); } catch { return String(value); }
3394
3471
  }
3395
3472
 
3396
- 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 = {}) {
3397
3511
  let out = '';
3398
3512
  for (const b of blocks) {
3399
3513
  if (b.kind === 'text') {
3400
- 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>`;
3401
3517
  } else if (b.kind === 'thinking') {
3402
3518
  out += `<details class="blk blk-thinking" open><summary>💭 ${t('sessions.block.thinking')}</summary><div class="blk-thinking-body">${esc(b.text)}</div></details>`;
3403
3519
  } else if (b.kind === 'tool_use') {
@@ -3410,10 +3526,18 @@ function renderBlocks(blocks) {
3410
3526
  for (const p of entries) {
3411
3527
  params += `<div class="tool-param"><span class="pk">${esc(p.k)}</span><code class="pv">${esc(p.v)}</code></div>`;
3412
3528
  }
3413
- 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>`;
3414
3534
  } else if (b.kind === 'tool_result') {
3415
3535
  const cls = b.isError ? 'blk-result err' : 'blk-result';
3416
- 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>`;
3417
3541
  } else if (b.kind === 'subtask') {
3418
3542
  const knownStatuses = ['running', 'completed', 'failed', 'stopped', 'killed'];
3419
3543
  const status = knownStatuses.includes(String(b.status)) ? String(b.status) : 'running';
@@ -3441,6 +3565,34 @@ function renderBlocks(blocks) {
3441
3565
  return out;
3442
3566
  }
3443
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
+
3444
3596
  // ── 通用 Menu 协议辅助(mResp / toast,供 Agents / System / Triggers 复用)──
3445
3597
 
3446
3598
  // 提取 menu.response 的 data/error
@@ -5061,6 +5213,18 @@ const TRIGGER_CONCURRENCY_OPTIONS = [
5061
5213
  ['replace', () => t('triggers.concurrency.replace')],
5062
5214
  ['allow', () => t('triggers.concurrency.allow')],
5063
5215
  ];
5216
+
5217
+ function triggerCanonicalThread(thread) {
5218
+ return thread === 'per_run' || thread === 'by_run' ? 'by_run' : 'by_trigger';
5219
+ }
5220
+
5221
+ function triggerConcurrencyOptions(executionType, thread) {
5222
+ if (executionType === 'script') return TRIGGER_CONCURRENCY_OPTIONS;
5223
+ if (executionType === 'trigger_session' && triggerCanonicalThread(thread) === 'by_run') {
5224
+ return TRIGGER_CONCURRENCY_OPTIONS;
5225
+ }
5226
+ return TRIGGER_CONCURRENCY_OPTIONS.filter(([value]) => value !== 'allow');
5227
+ }
5064
5228
  const TRIGGER_MISSED_POLICY_OPTIONS = [
5065
5229
  ['skip', () => t('triggers.missed.skip')],
5066
5230
  ['run_once', () => t('triggers.missed.run_once')],
@@ -5606,6 +5770,7 @@ function triggerOriginalEditState(t) {
5606
5770
  name: String(definition?.name ?? t?.name ?? t?.label ?? ''),
5607
5771
  enabled: triggerIsEnabled(t),
5608
5772
  executionType,
5773
+ triggerThread: String(execution.thread ?? ''),
5609
5774
  scriptPath: String(execution.script?.path ?? ''),
5610
5775
  scriptRuntime: String(execution.script?.runtime ?? 'node'),
5611
5776
  scriptTimeoutMs: Number(execution.script?.timeoutMs ?? 30_000),
@@ -6402,6 +6567,7 @@ function syncTriggerEditFields(ctx) {
6402
6567
  const valueField = root.querySelector('#trig-edit-schedule-value-field');
6403
6568
  const eventFields = root.querySelectorAll('.trig-event-field');
6404
6569
  const executionType = root.querySelector('#trig-edit-type')?.value || ctx.original.executionType;
6570
+ const concurrencySelect = root.querySelector('#trig-edit-concurrency');
6405
6571
  const deliverySwitch = root.querySelector('#trig-edit-delivery-enabled');
6406
6572
  const deliverySwitchField = root.querySelector('.trig-delivery-switch');
6407
6573
  if (executionType === 'target_session') {
@@ -6481,6 +6647,15 @@ function syncTriggerEditFields(ctx) {
6481
6647
  if (scriptFeedback) scriptFeedback.hidden = executionType !== 'script' || !deliveryEnabled;
6482
6648
  if (threadField) threadField.hidden = !showTarget || targetSession !== 'thread';
6483
6649
  if (threadInput) threadInput.disabled = ctx.saving || !showTarget || targetSession !== 'thread';
6650
+ if (concurrencySelect) {
6651
+ const options = triggerConcurrencyOptions(executionType, ctx.original.triggerThread);
6652
+ const current = options.some(([value]) => value === concurrencySelect.value)
6653
+ ? concurrencySelect.value
6654
+ : 'forbid';
6655
+ concurrencySelect.innerHTML = triggerOptionHtml(options, current, null);
6656
+ concurrencySelect.value = current;
6657
+ concurrencySelect.disabled = Boolean(ctx.saving);
6658
+ }
6484
6659
  }
6485
6660
 
6486
6661
  function readTriggerEditValues(root) {
@@ -6627,7 +6802,7 @@ function triggerDefinitionFromEdit(original, values, eventFilter, feedbackStrate
6627
6802
  execution.prompt = values.prompt;
6628
6803
  delete execution.script;
6629
6804
  if (values.executionType === 'trigger_session') {
6630
- if (previousExecutionType !== 'trigger_session' || !['per_run', 'by_trigger'].includes(execution.thread)) {
6805
+ if (previousExecutionType !== 'trigger_session' || !['per_run', 'by_run', 'by_trigger'].includes(execution.thread)) {
6631
6806
  execution.thread = 'by_trigger';
6632
6807
  }
6633
6808
  } else {
@@ -6872,7 +7047,7 @@ function openTriggerEditDrawer(id, agent) {
6872
7047
  `<div class="ag-edit-grid trig-edit-grid trig-limit-grid">` +
6873
7048
  `<label class="ag-edit-field"><span>${t('triggers.field.maxRuns')}</span><input id="trig-edit-max-runs" type="number" min="1" step="1" value="${esc(original.maxRuns)}" placeholder="${t('common.noLimit')}"></label>` +
6874
7049
  `<label class="ag-edit-field"><span>${t('triggers.field.maxDuration')}</span><input id="trig-edit-max-duration" type="text" autocomplete="off" spellcheck="false" value="${esc(original.maxDuration)}" placeholder="${t('triggers.placeholder.maxDuration')}"></label>` +
6875
- `<label class="ag-edit-field"><span>${t('triggers.field.concurrency')}</span><select id="trig-edit-concurrency">${triggerOptionHtml(TRIGGER_CONCURRENCY_OPTIONS, original.concurrency, null)}</select></label>` +
7050
+ `<label class="ag-edit-field"><span>${t('triggers.field.concurrency')}</span><select id="trig-edit-concurrency">${triggerOptionHtml(triggerConcurrencyOptions(original.executionType, original.triggerThread), original.concurrency, null)}</select></label>` +
6876
7051
  `<label class="ag-edit-field"><span>${t('triggers.field.retryAttempts')}</span><input id="trig-edit-retry-attempts" type="number" min="0" max="20" step="1" value="${esc(original.retryAttempts)}"></label>` +
6877
7052
  `<label class="ag-edit-field"><span>${t('triggers.field.retryBackoff')}</span><input id="trig-edit-retry-backoff" type="text" autocomplete="off" spellcheck="false" value="${esc(original.retryBackoff)}" placeholder="${t('triggers.placeholder.retryBackoff')}"></label>` +
6878
7053
  `<label class="ag-edit-field"><span>${t('triggers.field.missedPolicy')}</span><select id="trig-edit-missed-policy">${triggerOptionHtml(TRIGGER_MISSED_POLICY_OPTIONS, original.missedPolicy, null)}</select></label>` +
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-07-30 05:24
51
+ 2026-08-03 19:27
52
52
  <span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
53
53
  <button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
54
54
  <button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>