ec-web 0.0.28 → 0.0.29

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.
@@ -83,6 +83,12 @@ const translations = {
83
83
  'common.loadingSuffix': '加载中',
84
84
  'common.noChanges': '没有需要保存的改动',
85
85
  'common.close': '关闭',
86
+ 'common.back': '返回',
87
+ 'common.openDetail': '打开详情',
88
+ 'navigation.open': '打开导航',
89
+ 'navigation.close': '关闭导航',
90
+ 'navigation.pages': '页面导航',
91
+ 'navigation.moreFilters': '更多筛选',
86
92
  'common.rawJsonPreview': '原始 JSON 预览',
87
93
  'common.inherited': '继承',
88
94
  'common.noLimit': '无限制',
@@ -116,6 +122,7 @@ const translations = {
116
122
  'theme.light': '浅色模式',
117
123
  'theme.dark': '深色模式',
118
124
  'theme.system': '跟随系统',
125
+ 'theme.drawerLabel': '主题:{mode}',
119
126
  'theme.buttonTitle': '主题:{mode};点击切换',
120
127
  'language.buttonTitle': '切换语言',
121
128
 
@@ -133,6 +140,7 @@ const translations = {
133
140
  'logs.lines.1000': '1000 行',
134
141
  'logs.lines.2000': '2000 行',
135
142
  'logs.follow': '跟随尾部',
143
+ 'logs.moreFilters': '更多筛选',
136
144
  'logs.live': '自动刷新',
137
145
  'logs.meta': '{files} 个文件 · {size} · 更新于 {time}',
138
146
  'logs.filtered': '筛选结果 {visible} 行(已加载 {total} 行)',
@@ -277,10 +285,13 @@ const translations = {
277
285
  // Messages view
278
286
  'messages.colTitle.aid': 'Agent',
279
287
  'messages.colTitle.peers': 'Chats',
288
+ 'messages.mobile.selection': '消息选择',
289
+ 'messages.mobile.aid': 'Agent',
290
+ 'messages.mobile.peer': 'Chats',
280
291
  'messages.empty.selectAid': '← 选择一个 Agent',
281
292
  'messages.empty.selectToView': '选择 Agent 查看消息',
282
293
  'messages.empty.noAgents': '暂无 Agent',
283
- 'messages.empty.noPeers': '暂无聊天对端',
294
+ 'messages.empty.noPeers': '暂无 Chats',
284
295
  'messages.empty.noMessages': '暂无消息',
285
296
  'messages.loadOlder': '加载更早消息',
286
297
  'messages.tag.group': '群聊',
@@ -304,7 +315,7 @@ const translations = {
304
315
  'messages.msgType.command': '命令',
305
316
  'messages.groupCount': '群 {count}',
306
317
  'messages.privateCount': '单 {count}',
307
- 'messages.session.field.sessionId': 'EvolCore 会话 ID',
318
+ 'messages.session.field.sessionId': 'EC会话ID',
308
319
  'messages.session.field.baseagent': 'Baseagent',
309
320
  'messages.session.field.chatMode': '模式',
310
321
  'messages.session.field.permissionMode': '权限',
@@ -315,6 +326,8 @@ const translations = {
315
326
  'messages.session.status.idle': '空闲',
316
327
  'messages.session.mode.interactive': '响应',
317
328
  'messages.session.mode.proactive': '自主',
329
+ 'messages.session.details.expand': '展开会话状态',
330
+ 'messages.session.details.collapse': '收起会话状态',
318
331
 
319
332
  // Sessions view
320
333
  'sessions.filter.normal': '🔍 仅有效',
@@ -358,6 +371,8 @@ const translations = {
358
371
  'sessions.fullView': '📜 完整视图',
359
372
  'sessions.chatHint': '只看用户与 Agent 的对话,处理过程已折叠',
360
373
  'sessions.fullHint': '显示全部消息',
374
+ 'sessions.details.expand': '展开会话信息',
375
+ 'sessions.details.collapse': '收起会话信息',
361
376
  'sessions.procSummary': '⋯ {count} 条处理过程(思考·工具·结果)',
362
377
  'sessions.empty.noDialogue': '该会话没有用户对话消息',
363
378
  'sessions.turnType.userInput': '用户输入',
@@ -749,10 +764,13 @@ const translations = {
749
764
  'triggers.lastUnrecorded': '未记录',
750
765
  'triggers.collapse.expand': '展开 Agent 列表',
751
766
  'triggers.collapse.collapse': '收起 Agent 列表',
767
+ 'triggers.agent.daemon': 'DAEMON',
752
768
  'triggers.empty.noAgents': '暂无 Agent',
753
769
  'triggers.empty.noTriggers': '暂无触发器',
754
770
  'triggers.empty.noAgentTriggers': '该 Agent 暂无触发器',
755
771
  'triggers.th.status': '状态',
772
+ 'triggers.th.agent': 'Agent',
773
+ 'triggers.th.id': 'ID',
756
774
  'triggers.th.subscription': '订阅',
757
775
  'triggers.th.name': '名称',
758
776
  'triggers.th.type': '类型',
@@ -1023,6 +1041,12 @@ const translations = {
1023
1041
  'common.loadingSuffix': 'loading',
1024
1042
  'common.noChanges': 'No changes to save',
1025
1043
  'common.close': 'Close',
1044
+ 'common.back': 'Back',
1045
+ 'common.openDetail': 'Open details',
1046
+ 'navigation.open': 'Open navigation',
1047
+ 'navigation.close': 'Close navigation',
1048
+ 'navigation.pages': 'Page navigation',
1049
+ 'navigation.moreFilters': 'More filters',
1026
1050
  'common.rawJsonPreview': 'Raw JSON Preview',
1027
1051
  'common.inherited': 'Inherited',
1028
1052
  'common.noLimit': 'No limit',
@@ -1056,6 +1080,7 @@ const translations = {
1056
1080
  'theme.light': 'Light',
1057
1081
  'theme.dark': 'Dark',
1058
1082
  'theme.system': 'System',
1083
+ 'theme.drawerLabel': 'Theme: {mode}',
1059
1084
  'theme.buttonTitle': 'Theme: {mode}; click to switch',
1060
1085
  'language.buttonTitle': 'Switch language',
1061
1086
 
@@ -1073,6 +1098,7 @@ const translations = {
1073
1098
  'logs.lines.1000': '1000 Lines',
1074
1099
  'logs.lines.2000': '2000 Lines',
1075
1100
  'logs.follow': 'Follow tail',
1101
+ 'logs.moreFilters': 'More filters',
1076
1102
  'logs.live': 'Auto refresh',
1077
1103
  'logs.meta': '{files} files · {size} · updated {time}',
1078
1104
  'logs.filtered': '{visible} filtered ({total} loaded)',
@@ -1217,6 +1243,9 @@ const translations = {
1217
1243
  // Messages view
1218
1244
  'messages.colTitle.aid': 'Agent',
1219
1245
  'messages.colTitle.peers': 'Chats',
1246
+ 'messages.mobile.selection': 'Message selection',
1247
+ 'messages.mobile.aid': 'Agent',
1248
+ 'messages.mobile.peer': 'Chats',
1220
1249
  'messages.empty.selectAid': '← Select Agent',
1221
1250
  'messages.empty.selectToView': 'Select Agent to view messages',
1222
1251
  'messages.empty.noAgents': 'No agents',
@@ -1244,7 +1273,7 @@ const translations = {
1244
1273
  'messages.msgType.command': 'Command',
1245
1274
  'messages.groupCount': '{count} groups',
1246
1275
  'messages.privateCount': '{count} private',
1247
- 'messages.session.field.sessionId': 'EvolCore Session ID',
1276
+ 'messages.session.field.sessionId': 'EC Session ID',
1248
1277
  'messages.session.field.baseagent': 'Baseagent',
1249
1278
  'messages.session.field.chatMode': 'Mode',
1250
1279
  'messages.session.field.permissionMode': 'Perm',
@@ -1255,6 +1284,8 @@ const translations = {
1255
1284
  'messages.session.status.idle': 'Idle',
1256
1285
  'messages.session.mode.interactive': 'Interactive',
1257
1286
  'messages.session.mode.proactive': 'Proactive',
1287
+ 'messages.session.details.expand': 'Expand session status',
1288
+ 'messages.session.details.collapse': 'Collapse session status',
1258
1289
 
1259
1290
  // Sessions view
1260
1291
  'sessions.filter.normal': '🔍 Valid Only',
@@ -1298,6 +1329,8 @@ const translations = {
1298
1329
  'sessions.fullView': '📜 Full View',
1299
1330
  'sessions.chatHint': 'Only user and Agent messages; processing is collapsed',
1300
1331
  'sessions.fullHint': 'Show all messages',
1332
+ 'sessions.details.expand': 'Expand session details',
1333
+ 'sessions.details.collapse': 'Collapse session details',
1301
1334
  'sessions.procSummary': '⋯ {count} processing steps (thinking · tools · results)',
1302
1335
  'sessions.empty.noDialogue': 'No user dialogue messages in this session',
1303
1336
  'sessions.turnType.userInput': 'User Input',
@@ -1689,10 +1722,13 @@ const translations = {
1689
1722
  'triggers.lastUnrecorded': 'Not recorded',
1690
1723
  'triggers.collapse.expand': 'Expand Agent List',
1691
1724
  'triggers.collapse.collapse': 'Collapse Agent List',
1725
+ 'triggers.agent.daemon': 'DAEMON',
1692
1726
  'triggers.empty.noAgents': 'No agents',
1693
1727
  'triggers.empty.noTriggers': 'No triggers',
1694
1728
  'triggers.empty.noAgentTriggers': 'No triggers for this agent',
1695
1729
  'triggers.th.status': 'Status',
1730
+ 'triggers.th.agent': 'Agent',
1731
+ 'triggers.th.id': 'ID',
1696
1732
  'triggers.th.subscription': 'Subscription',
1697
1733
  'triggers.th.name': 'Name',
1698
1734
  'triggers.th.type': 'Type',
@@ -1921,9 +1957,15 @@ function updateI18n() {
1921
1957
  const key = el.getAttribute('data-i18n-title');
1922
1958
  el.title = t(key);
1923
1959
  });
1960
+ document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {
1961
+ const key = el.getAttribute('data-i18n-aria-label');
1962
+ el.setAttribute('aria-label', t(key));
1963
+ });
1924
1964
  // 更新 html lang 属性
1925
1965
  document.documentElement.lang = currentLang;
1926
1966
  updateThemeButton();
1967
+ updateMobilePageTitle();
1968
+ renderMobileNavigation();
1927
1969
  window.refreshBasicSettingsI18n?.();
1928
1970
  }
1929
1971
 
@@ -2282,7 +2324,13 @@ setInterval(() => {
2282
2324
 
2283
2325
  // ── Tab 切换 ──
2284
2326
  let msgSel = { aid: null, peer: null, before: null };
2327
+ let msgCollapsedAids = new Set();
2328
+ // 消息详情扩展信息只在当前页面生命周期内记忆,不写入 sessionStorage。
2329
+ // 以 Agent/Chats/EC 会话 ID 区分,切换会话时仍默认展开。
2330
+ const msgSessionDetailsExpanded = new Map();
2285
2331
  let sessSel = { sessionId: null, project: null, baseagent: null, turnBefore: null };
2332
+ // 会话详情顶部信息只在当前页面生命周期内记忆展开/收起状态。
2333
+ const sessHeaderExpanded = new Map();
2286
2334
  let trigSel = { agent: null };
2287
2335
  let trigAgentsCollapsed = false;
2288
2336
  const _trigOps = new Map(); // Map<triggerId, string>
@@ -2292,9 +2340,98 @@ let _triggerTest = null;
2292
2340
  let sessSearch = '';
2293
2341
  let sessShowAll = false; // false=只显示有效会话;true=显示全部
2294
2342
  let sessSourceFilter = 'all'; // all / trigger / agent / cli / user / unknown
2343
+ let sessListScroll = { layout: 0, items: 0 };
2295
2344
  let sessChatMode = false; // false=完整视图,true=对话视图(折叠处理过程)
2296
2345
  let monRange = '2m'; // Monitor 时间窗口:2m / 10m / 1h
2297
2346
  let logSel = { source: null, level: 'all', module: 'all', search: '', limit: 500, follow: true };
2347
+ const SESSION_UI_KEY = 'ecWebUiState';
2348
+ let mobileNavOpen = false;
2349
+ let detailViewState = { msg: false, session: false };
2350
+ let historySyncing = false;
2351
+ let sessScrollSaveFrame = null;
2352
+
2353
+ function readSessionUiState() {
2354
+ try {
2355
+ const raw = sessionStorage.getItem(SESSION_UI_KEY);
2356
+ if (!raw) return;
2357
+ const saved = JSON.parse(raw);
2358
+ if (saved.msg && typeof saved.msg === 'object') msgSel = { ...msgSel, ...saved.msg, before: null };
2359
+ if (saved.session && typeof saved.session === 'object') sessSel = { ...sessSel, ...saved.session, turnBefore: null };
2360
+ if (saved.trigger && typeof saved.trigger === 'object') trigSel = { ...trigSel, ...saved.trigger };
2361
+ if (typeof saved.sessShowAll === 'boolean') sessShowAll = saved.sessShowAll;
2362
+ if (typeof saved.sessSourceFilter === 'string') sessSourceFilter = saved.sessSourceFilter;
2363
+ if (saved.sessionListScroll && typeof saved.sessionListScroll === 'object') {
2364
+ sessListScroll = {
2365
+ layout: Math.max(0, Number(saved.sessionListScroll.layout) || 0),
2366
+ items: Math.max(0, Number(saved.sessionListScroll.items) || 0),
2367
+ };
2368
+ }
2369
+ if (saved.log && typeof saved.log === 'object') logSel = { ...logSel, ...saved.log };
2370
+ if (saved.detail && typeof saved.detail === 'object') detailViewState = { ...detailViewState, ...saved.detail };
2371
+ } catch {
2372
+ // sessionStorage may be unavailable in private or embedded contexts.
2373
+ }
2374
+ }
2375
+
2376
+ function saveSessionUiState() {
2377
+ try {
2378
+ captureSessionListScroll();
2379
+ sessionStorage.setItem(SESSION_UI_KEY, JSON.stringify({
2380
+ msg: { aid: msgSel.aid, peer: msgSel.peer },
2381
+ session: { ...sessSel, turnBefore: null },
2382
+ trigger: { agent: trigSel.agent },
2383
+ sessShowAll,
2384
+ sessSourceFilter,
2385
+ sessionListScroll: sessListScroll,
2386
+ log: { ...logSel },
2387
+ detail: { ...detailViewState },
2388
+ }));
2389
+ } catch {
2390
+ // Keep the in-memory state when storage is unavailable.
2391
+ }
2392
+ }
2393
+
2394
+ function captureSessionListScroll() {
2395
+ const layout = document.querySelector('.sess-layout');
2396
+ const items = document.querySelector('#sess-list .sess-items');
2397
+ if (layout) sessListScroll.layout = Math.max(0, layout.scrollTop || 0);
2398
+ if (items) sessListScroll.items = Math.max(0, items.scrollTop || 0);
2399
+ }
2400
+
2401
+ function restoreSessionListScroll() {
2402
+ const layout = document.querySelector('.sess-layout');
2403
+ const items = document.querySelector('#sess-list .sess-items');
2404
+ if (layout) layout.scrollTop = sessListScroll.layout;
2405
+ if (items) items.scrollTop = sessListScroll.items;
2406
+ }
2407
+
2408
+ function bindSessionListScroll() {
2409
+ const layout = document.querySelector('.sess-layout');
2410
+ if (layout && layout.dataset.scrollBound !== '1') {
2411
+ layout.dataset.scrollBound = '1';
2412
+ layout.addEventListener('scroll', () => {
2413
+ captureSessionListScroll();
2414
+ scheduleSessionListStateSave();
2415
+ }, { passive: true });
2416
+ }
2417
+ const items = document.querySelector('#sess-list .sess-items');
2418
+ if (items) items.addEventListener('scroll', () => {
2419
+ captureSessionListScroll();
2420
+ scheduleSessionListStateSave();
2421
+ }, { passive: true });
2422
+ }
2423
+
2424
+ function scheduleSessionListStateSave() {
2425
+ if (sessScrollSaveFrame != null) return;
2426
+ const flush = () => {
2427
+ sessScrollSaveFrame = null;
2428
+ saveSessionUiState();
2429
+ };
2430
+ if (typeof window.requestAnimationFrame === 'function') sessScrollSaveFrame = window.requestAnimationFrame(flush);
2431
+ else sessScrollSaveFrame = window.setTimeout(flush, 0);
2432
+ }
2433
+
2434
+ readSessionUiState();
2298
2435
  // ECWeb 界面的 base agent 展示名统一走这里的 i18n 键,而不是父包别名表:
2299
2436
  // 别名表面向 CLI/日志(claude → "Claude Code"、ecagent → "ecagent"),
2300
2437
  // 面板里应保持简洁一致(Claude / Codex / Gemini / EC Agent)。
@@ -2371,7 +2508,133 @@ function setRoleManagementSubview(subview, options = {}) {
2371
2508
  if (options.render !== false) renderRoleManagementSubview();
2372
2509
  }
2373
2510
 
2374
- function switchView(view) {
2511
+ const VIEW_LABEL_KEYS = {
2512
+ agents: 'tab.agents', msg: 'tab.messages', session: 'tab.sessions', triggers: 'tab.triggers',
2513
+ cache: 'tab.cache', system: 'tab.system', logs: 'tab.logs', usage: 'tab.usage', roleManagement: 'tab.roleManagement',
2514
+ };
2515
+
2516
+ function normalizedView(view) {
2517
+ if (view === 'monitor') return 'system';
2518
+ if (view === 'roles' || view === 'roleDefinitions') return 'roleManagement';
2519
+ return ['agents', 'msg', 'session', 'triggers', 'cache', 'system', 'logs', 'usage', 'roleManagement'].includes(view)
2520
+ ? view
2521
+ : 'agents';
2522
+ }
2523
+
2524
+ function historySnapshot() {
2525
+ return {
2526
+ ecweb: true,
2527
+ view: currentView,
2528
+ detail: { ...detailViewState },
2529
+ roleManagementSubview,
2530
+ };
2531
+ }
2532
+
2533
+ function updateMobilePageTitle() {
2534
+ const title = $('#mobile-page-title');
2535
+ if (title) title.textContent = t(VIEW_LABEL_KEYS[currentView] || 'tab.agents');
2536
+ }
2537
+
2538
+ function applyDetailClasses() {
2539
+ ['msg', 'session'].forEach(view => {
2540
+ const el = $('#view-' + view);
2541
+ if (!el) return;
2542
+ el.classList.toggle('detail-fullscreen', currentView === view && detailViewState[view] === true);
2543
+ });
2544
+ saveSessionUiState();
2545
+ }
2546
+
2547
+ function enterDetail(view, options = {}) {
2548
+ if (!['msg', 'session'].includes(view) || currentView !== view) return;
2549
+ if (detailViewState[view]) return;
2550
+ detailViewState[view] = true;
2551
+ applyDetailClasses();
2552
+ if (options.history !== false && !historySyncing) history.pushState(historySnapshot(), '', location.href);
2553
+ renderView(view);
2554
+ }
2555
+
2556
+ function leaveDetail(view, options = {}) {
2557
+ if (!['msg', 'session'].includes(view) || !detailViewState[view]) return;
2558
+ if (options.history !== false && !historySyncing && history.state?.ecweb && history.state.detail?.[view]) {
2559
+ history.back();
2560
+ return;
2561
+ }
2562
+ detailViewState[view] = false;
2563
+ applyDetailClasses();
2564
+ renderView(view);
2565
+ }
2566
+
2567
+ function restoreHistoryState(snapshot) {
2568
+ const next = snapshot && snapshot.ecweb ? snapshot : { ecweb: true, view: currentView, detail: {} };
2569
+ historySyncing = true;
2570
+ if (next.roleManagementSubview) roleManagementSubview = next.roleManagementSubview;
2571
+ detailViewState = { msg: false, session: false };
2572
+ switchView(next.view || 'agents', { history: false });
2573
+ detailViewState = { msg: false, session: false, ...(next.detail || {}) };
2574
+ historySyncing = false;
2575
+ applyDetailClasses();
2576
+ if (currentView === 'msg' || currentView === 'session') renderView(currentView);
2577
+ }
2578
+
2579
+ function initHistory() {
2580
+ const initial = history.state && history.state.ecweb ? history.state : historySnapshot();
2581
+ history.replaceState(initial, '', location.href);
2582
+ window.addEventListener('popstate', event => restoreHistoryState(event.state));
2583
+ }
2584
+
2585
+ function renderMobileNavigation() {
2586
+ const list = $('.mobile-nav-list');
2587
+ if (!list) return;
2588
+ list.innerHTML = [...document.querySelectorAll('.tabs .tab')].map(tab => {
2589
+ const view = tab.dataset.view;
2590
+ const label = t(tab.dataset.i18n || VIEW_LABEL_KEYS[view] || view);
2591
+ return `<button type="button" class="mobile-nav-item${view === currentView ? ' active' : ''}" data-mobile-view="${esc(view)}">${esc(label)}</button>`;
2592
+ }).join('');
2593
+ list.querySelectorAll('[data-mobile-view]').forEach(button => {
2594
+ button.onclick = () => {
2595
+ closeMobileNavigation();
2596
+ switchView(button.dataset.mobileView);
2597
+ };
2598
+ });
2599
+ }
2600
+
2601
+ function setMobileNavigation(open) {
2602
+ mobileNavOpen = Boolean(open);
2603
+ const drawer = $('#mobile-nav-drawer');
2604
+ const backdrop = $('#mobile-nav-backdrop');
2605
+ const menu = $('#mobile-menu-btn');
2606
+ if (drawer) {
2607
+ drawer.classList.toggle('open', mobileNavOpen);
2608
+ drawer.setAttribute('aria-hidden', String(!mobileNavOpen));
2609
+ }
2610
+ if (backdrop) {
2611
+ backdrop.hidden = !mobileNavOpen;
2612
+ backdrop.classList.toggle('open', mobileNavOpen);
2613
+ }
2614
+ if (menu) menu.setAttribute('aria-expanded', String(mobileNavOpen));
2615
+ document.body.classList.toggle('mobile-nav-open', mobileNavOpen);
2616
+ }
2617
+
2618
+ function closeMobileNavigation() { setMobileNavigation(false); }
2619
+
2620
+ function initMobileNavigation() {
2621
+ renderMobileNavigation();
2622
+ $('#mobile-menu-btn')?.addEventListener('click', () => setMobileNavigation(!mobileNavOpen));
2623
+ $('#mobile-nav-close')?.addEventListener('click', closeMobileNavigation);
2624
+ $('#mobile-nav-backdrop')?.addEventListener('click', closeMobileNavigation);
2625
+ document.querySelectorAll('[data-mobile-tool]').forEach(button => {
2626
+ button.addEventListener('click', () => {
2627
+ const tool = button.dataset.mobileTool;
2628
+ closeMobileNavigation();
2629
+ if (tool === 'lang') toggleLang();
2630
+ else if (tool === 'theme') $('#theme-btn')?.click();
2631
+ else if (tool === 'settings') $('#basic-settings-btn')?.click();
2632
+ else if (tool === 'logout') $('#logout-btn')?.click();
2633
+ });
2634
+ });
2635
+ }
2636
+
2637
+ function switchView(view, options = {}) {
2375
2638
  if (view === 'monitor') {
2376
2639
  systemContentMode = 'monitor';
2377
2640
  view = 'system';
@@ -2380,10 +2643,19 @@ function switchView(view) {
2380
2643
  roleManagementSubview = view === 'roles' ? 'assignments' : 'definitions';
2381
2644
  view = 'roleManagement';
2382
2645
  }
2646
+ view = normalizedView(view);
2647
+ if (currentView !== view) {
2648
+ detailViewState = { msg: false, session: false };
2649
+ }
2383
2650
  currentView = view;
2384
2651
  localStorage.setItem(VIEW_KEY, view);
2385
2652
  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
2386
2653
  document.querySelectorAll('.view').forEach(v => v.classList.toggle('active', v.id === 'view-' + view));
2654
+ updateMobilePageTitle();
2655
+ renderMobileNavigation();
2656
+ applyDetailClasses();
2657
+ if (options.replaceHistory) history.replaceState(historySnapshot(), '', location.href);
2658
+ else if (options.history !== false && !historySyncing && options.fromHistory !== true) history.pushState(historySnapshot(), '', location.href);
2387
2659
  // 切换时按当前选择恢复订阅
2388
2660
  if (view === 'msg') subscribe('msg', msgSel);
2389
2661
  else if (view === 'session') subscribe('session', sessSel);
@@ -2415,6 +2687,7 @@ function initTabs() {
2415
2687
  document.querySelector(`[data-role-subview="${next}"]`)?.focus();
2416
2688
  };
2417
2689
  });
2690
+ initMobileNavigation();
2418
2691
  }
2419
2692
 
2420
2693
  function renderView(view) {
@@ -2518,37 +2791,49 @@ function bindLogControls() {
2518
2791
  const search = $('#log-search');
2519
2792
  const limit = $('#log-limit');
2520
2793
  const follow = $('#log-follow');
2794
+ const moreFilters = $('#log-more-filters');
2521
2795
  if (!source || source.dataset.bound === '1') return;
2522
2796
  source.dataset.bound = '1';
2523
2797
  source.addEventListener('change', () => {
2524
2798
  logSel.source = source.value || null;
2799
+ saveSessionUiState();
2525
2800
  $('#log-stream').innerHTML = `<div class="empty">${t('common.loading')}</div>`;
2526
2801
  subscribe('logs', { source: logSel.source, limit: logSel.limit });
2527
2802
  });
2528
2803
  level.addEventListener('change', () => {
2529
2804
  logSel.level = level.value || 'all';
2805
+ saveSessionUiState();
2530
2806
  renderLogs(state.logs);
2531
2807
  });
2532
2808
  module.addEventListener('change', () => {
2533
2809
  logSel.module = module.value || 'all';
2810
+ saveSessionUiState();
2534
2811
  renderLogs(state.logs);
2535
2812
  });
2536
2813
  search.addEventListener('input', () => {
2537
2814
  logSel.search = search.value || '';
2815
+ saveSessionUiState();
2538
2816
  renderLogs(state.logs);
2539
2817
  });
2540
2818
  limit.addEventListener('change', () => {
2541
2819
  logSel.limit = Number(limit.value) || 500;
2820
+ saveSessionUiState();
2542
2821
  $('#log-stream').innerHTML = `<div class="empty">${t('common.loading')}</div>`;
2543
2822
  subscribe('logs', { source: logSel.source, limit: logSel.limit });
2544
2823
  });
2545
2824
  follow.addEventListener('change', () => {
2546
2825
  logSel.follow = follow.checked;
2826
+ saveSessionUiState();
2547
2827
  if (logSel.follow) {
2548
2828
  const stream = $('#log-stream');
2549
2829
  stream.scrollTop = stream.scrollHeight;
2550
2830
  }
2551
2831
  });
2832
+ if (moreFilters) moreFilters.addEventListener('click', () => {
2833
+ const toolbar = moreFilters.closest('.log-toolbar');
2834
+ const expanded = toolbar?.classList.toggle('filters-expanded') || false;
2835
+ moreFilters.setAttribute('aria-expanded', String(expanded));
2836
+ });
2552
2837
  }
2553
2838
 
2554
2839
  function renderLogs(data) {
@@ -2558,6 +2843,7 @@ function renderLogs(data) {
2558
2843
  const search = $('#log-search');
2559
2844
  const limit = $('#log-limit');
2560
2845
  const follow = $('#log-follow');
2846
+ const moreFilters = $('#log-more-filters');
2561
2847
  const meta = $('#log-meta-info');
2562
2848
  const filterResult = $('#log-filter-result');
2563
2849
  const stream = $('#log-stream');
@@ -2568,6 +2854,7 @@ function renderLogs(data) {
2568
2854
  search.value = logSel.search;
2569
2855
  limit.value = String(logSel.limit);
2570
2856
  follow.checked = logSel.follow;
2857
+ if (moreFilters) moreFilters.textContent = t('logs.moreFilters');
2571
2858
  if (!data) {
2572
2859
  stream.innerHTML = `<div class="empty">${t('common.loading')}</div>`;
2573
2860
  return;
@@ -2891,6 +3178,13 @@ function initMsgTipFloat() {
2891
3178
  if (to && (wrap.contains(to) || (floatEl && floatEl.contains(to)))) return;
2892
3179
  scheduleHide();
2893
3180
  });
3181
+ document.addEventListener('click', (e) => {
3182
+ if (!floatEl || !floatEl.classList.contains('show')) return;
3183
+ if (floatEl.contains(e.target)) return;
3184
+ const wrap = e.target.closest && e.target.closest('.ag-msg-wrap');
3185
+ if (wrap && wrap === curWrap) return;
3186
+ hideNow();
3187
+ }, true);
2894
3188
  // 滚动时隐藏,避免浮层与行脱节
2895
3189
  window.addEventListener('scroll', hideNow, true);
2896
3190
  }
@@ -3140,7 +3434,9 @@ function renderAgents(data) {
3140
3434
  if (!disabledAgents.length) {
3141
3435
  html += `<div class="empty">${t('agents.empty.disabled')}</div>`;
3142
3436
  } else {
3143
- html += `<table><thead><tr><th>${t('agents.th.agent')}</th><th>${t('agents.th.projectPath')}</th><th>${t('agents.th.operations')}</th></tr></thead><tbody>`;
3437
+ html += '<table class="agents-table agents-table-disabled"><thead><tr>' +
3438
+ `<th>${t('agents.th.agent')}</th><th>${t('agents.th.operations')}</th>` +
3439
+ '</tr></thead><tbody>';
3144
3440
  for (const ag of disabledAgents) {
3145
3441
  const busy = _agentOps.has(ag.aid);
3146
3442
  const ops = busy
@@ -3148,7 +3444,6 @@ function renderAgents(data) {
3148
3444
  : agentDisabledOpsHtml(ag.aid);
3149
3445
  html += `<tr class="ag-main">` +
3150
3446
  `<td><div class="ag-id"><span class="dot off"></span><span class="ag-id-text"><span class="ag-name">${esc(ag.displayName || shortAid(ag.aid))}${agentLifecycleBadge(ag.lifecycle)}</span><span class="ag-aid">${esc(ag.aid)}</span></span></div></td>` +
3151
- `<td style="font-size:11px;font-family:monospace">${esc(ag.projectPath || '—')}</td>` +
3152
3447
  `<td class="agent-ops-cell">${ops}</td></tr>`;
3153
3448
  }
3154
3449
  html += '</tbody></table>';
@@ -3173,7 +3468,7 @@ function renderAgents(data) {
3173
3468
  return;
3174
3469
  }
3175
3470
 
3176
- html += '<table><thead><tr>' +
3471
+ html += '<table class="agents-table agents-table-enabled"><thead><tr>' +
3177
3472
  `<th>${t('agents.th.aid')}</th><th>${t('agents.th.work')}</th><th>${t('agents.th.queue')}</th><th>${t('agents.th.model')}</th><th>${t('agents.th.runtime')}</th>` +
3178
3473
  `<th>${t('agents.th.received')}</th><th>${t('agents.th.sent')}</th><th>${t('agents.th.errors')}</th><th>${t('agents.th.interrupts')}</th><th>${t('agents.th.completed')}</th>` +
3179
3474
  `<th>${t('agents.th.lastActivity')}</th><th>${t('agents.th.operations')}</th>` +
@@ -3214,10 +3509,11 @@ function renderAgents(data) {
3214
3509
  const recent = (s.recentMessages || []);
3215
3510
  const tipHtml = recentMsgTooltipHtml(recent);
3216
3511
 
3217
- html += `<tr class="ag-sub"><td colspan="12"><div class="ag-info">` +
3218
- (ag.projectPath ? `<div class="ag-path">${esc(ag.projectPath)}</div>` : '') +
3219
- (preview ? `<div class="ag-msg-wrap">${tipHtml}<div class="ag-msg">${preview}</div></div>` : '') +
3220
- '</div></td></tr>';
3512
+ if (preview) {
3513
+ html += `<tr class="ag-sub"><td colspan="12"><div class="ag-info">` +
3514
+ `<div class="ag-msg-wrap">${tipHtml}<div class="ag-msg">${preview}</div></div>` +
3515
+ '</div></td></tr>';
3516
+ }
3221
3517
  }
3222
3518
  html += '</tbody></table>';
3223
3519
  if (data.daemonRunning) {
@@ -3360,6 +3656,83 @@ function msgChannelTone(channelType) {
3360
3656
  : 'other';
3361
3657
  }
3362
3658
 
3659
+ function renderMsgMobileControls(aids, peers, scopeMatches) {
3660
+ const el = $('#msg-mobile-controls');
3661
+ if (!el) return;
3662
+ const aidOptions = aids.map(item => {
3663
+ const name = item.displayName || (item.selfAID && item.selfAID !== 'unknown' ? shortAid(item.selfAID) : 'unknown');
3664
+ return `<option value="${esc(item.aid)}"${item.aid === msgSel.aid ? ' selected' : ''}>${esc(name)}</option>`;
3665
+ }).join('');
3666
+ const peerOptions = peers.map(item => {
3667
+ const name = item.chatType === 'group'
3668
+ ? (item.groupName || item.peerName || item.groupId || item.peerId)
3669
+ : (item.peerName || item.peerId);
3670
+ return `<option value="${esc(item.peerId)}"${item.peerId === msgSel.peer ? ' selected' : ''}>${esc(shortId(name))}</option>`;
3671
+ }).join('');
3672
+ el.innerHTML = `<label><span>${esc(t('messages.mobile.aid'))}</span>` +
3673
+ `<select id="msg-mobile-aid"${aids.length ? '' : ' disabled'}>${aidOptions || '<option value="">—</option>'}</select>` +
3674
+ `</label><label><span>${esc(t('messages.mobile.peer'))}</span>` +
3675
+ `<select id="msg-mobile-peer"${scopeMatches && peers.length ? '' : ' disabled'}>${peerOptions || '<option value="">—</option>'}</select>` +
3676
+ '</label>';
3677
+ const aidSelect = $('#msg-mobile-aid');
3678
+ if (aidSelect) aidSelect.onchange = () => {
3679
+ msgSel = { aid: aidSelect.value || null, peer: null, before: null };
3680
+ detailViewState.msg = false;
3681
+ saveSessionUiState();
3682
+ subscribe('msg', msgSel);
3683
+ };
3684
+ const peerSelect = $('#msg-mobile-peer');
3685
+ if (peerSelect) peerSelect.onchange = () => {
3686
+ msgSel = { aid: msgSel.aid, peer: peerSelect.value || null, before: null };
3687
+ saveSessionUiState();
3688
+ subscribe('msg', msgSel);
3689
+ };
3690
+ }
3691
+
3692
+ function msgAgentIsExpanded(aid) {
3693
+ return aid === msgSel.aid && !msgCollapsedAids.has(aid);
3694
+ }
3695
+
3696
+ function renderMsgPeerItem(peer, extraClass = '') {
3697
+ const sel = peer.peerId === msgSel.peer ? ' sel' : '';
3698
+ const isGroup = peer.chatType === 'group';
3699
+ const displayName = peer.chatType === 'group'
3700
+ ? (peer.groupName || peer.peerName || peer.groupId || peer.peerId)
3701
+ : (peer.peerName || peer.peerId);
3702
+ const chatTitle = peer.partition === 'thread'
3703
+ ? (peer.threadName || peer.threadId || displayName)
3704
+ : peer.partition === 'legacy'
3705
+ ? t('messages.legacy.title')
3706
+ : displayName;
3707
+ const channelLabel = peer.channelName && peer.channelName !== 'main'
3708
+ ? `${peer.channelType}/${peer.channelName}`
3709
+ : (peer.channelType || '');
3710
+ const groupMark = isGroup ? `<span class="msg-group-mark">${esc(t('messages.tag.groupShort'))}</span>` : '';
3711
+ const partitionLabel = peer.partition === 'thread'
3712
+ ? t('messages.tag.topic')
3713
+ : peer.partition === 'legacy' ? t('messages.tag.legacy') : t('messages.tag.main');
3714
+ const partitionMark = `<span class="msg-partition-mark msg-partition-${esc(peer.partition || 'main')}">${esc(partitionLabel)}</span>`;
3715
+ const channelTag = channelLabel
3716
+ ? `<span class="msg-tag msg-channel msg-channel-${msgChannelTone(peer.channelType)}">${esc(channelLabel)}</span>`
3717
+ : '';
3718
+ const topicStatus = ({ pending: '待讨论', in_progress: '进行中', completed: '已完成', archived: '已归档' })[peer.topicStatus] || '待讨论';
3719
+ const sub = peer.partition === 'thread' ? `${esc(topicStatus)} · ` : '';
3720
+ const label = peer.label ? `${esc(peer.label)} · ` : '';
3721
+ const threadPeer = peer.partition === 'thread' ? `${esc(shortId(displayName))} · ` : '';
3722
+ return `<div class="list-item msg-peer-item ${extraClass}${sel}" data-peer="${esc(peer.peerId)}">` +
3723
+ `<div class="name">${groupMark}${partitionMark}<span class="msg-peer-title" title="${esc(chatTitle)}">${esc(shortId(chatTitle))}</span>${channelTag}</div>` +
3724
+ `<div class="sub">${sub}${label}${threadPeer}↓${peer.inbound} ↑${peer.outbound} · ${fmtAgo(peer.lastAt)}</div></div>`;
3725
+ }
3726
+
3727
+ function bindDetailPreview(container, view) {
3728
+ if (!container) return;
3729
+ container.onclick = event => {
3730
+ if (event.target.closest('button, a, input, select, textarea, summary, details')) return;
3731
+ if (detailViewState[view]) leaveDetail(view);
3732
+ else enterDetail(view);
3733
+ };
3734
+ }
3735
+
3363
3736
  function renderMsg(data) {
3364
3737
  if (!data) return;
3365
3738
  const aids = data.scopes || data.aids || [];
@@ -3371,6 +3744,7 @@ function renderMsg(data) {
3371
3744
  const nextAid = aids[0]?.aid || null;
3372
3745
  const selectionChanged = msgSel.aid !== nextAid || msgSel.peer !== null;
3373
3746
  msgSel = { aid: nextAid, peer: null, before: null };
3747
+ saveSessionUiState();
3374
3748
  if (selectionChanged && nextAid) subscribe('msg', msgSel);
3375
3749
  }
3376
3750
  const scopeMatches = data.scope === msgSel.aid;
@@ -3381,6 +3755,7 @@ function renderMsg(data) {
3381
3755
  const nextPeer = peers[0]?.peerId || null;
3382
3756
  const selectionChanged = msgSel.peer !== nextPeer;
3383
3757
  msgSel = { aid: msgSel.aid, peer: nextPeer, before: null };
3758
+ saveSessionUiState();
3384
3759
  if (selectionChanged && nextPeer) subscribe('msg', msgSel);
3385
3760
  }
3386
3761
  }
@@ -3388,24 +3763,78 @@ function renderMsg(data) {
3388
3763
  const selectedAid = aids.find(item => item.aid === msgSel.aid);
3389
3764
  const peerTotal = scopeMatches ? peers.length : (selectedAid?.peerCount || 0);
3390
3765
 
3391
- // 左:AID 列表
3392
- let aidsHtml = `<div class="col-title"><span>${t('messages.colTitle.aid')}</span><span class="col-count">${aids.length}</span></div>`;
3766
+ // 左:桌面端 AID 列表;双列布局使用同一栏里的 Agent → Chats 树。
3767
+ let aidListHtml = `<div class="col-title"><span>${t('messages.colTitle.aid')}</span><span class="col-count">${aids.length}</span></div>`;
3393
3768
  for (const a of aids) {
3394
3769
  const sel = a.aid === msgSel.aid ? ' sel' : '';
3395
3770
  const name = a.displayName || (a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown');
3396
3771
  const privateCount = Math.max(0, (a.peerCount || 0) - (a.groupCount || 0));
3397
3772
  const privateBit = tf('messages.privateCount', { count: privateCount });
3398
3773
  const groupBit = a.groupCount ? ` · ${tf('messages.groupCount', { count: a.groupCount })}` : '';
3399
- aidsHtml += `<div class="list-item${sel}" data-aid="${esc(a.aid)}">` +
3774
+ aidListHtml += `<div class="list-item msg-aid-item${sel}" data-aid="${esc(a.aid)}">` +
3400
3775
  `<div class="name">${esc(name)}</div>` +
3401
3776
  `<div class="sub">↓${a.totalIn} ↑${a.totalOut} · ${privateBit}${groupBit} · ${fmtAgo(a.lastAt)}</div></div>`;
3402
3777
  }
3403
- if (!aids.length) aidsHtml += `<div class="empty">${t('messages.empty.noAgents')}</div>`;
3404
- $('#msg-aids').innerHTML = aidsHtml;
3405
- $('#msg-aids').querySelectorAll('.list-item').forEach(item => {
3778
+ if (!aids.length) aidListHtml += `<div class="empty">${t('messages.empty.noAgents')}</div>`;
3779
+
3780
+ let aidTreeHtml = `<div class="msg-tree-title"><span>${t('messages.colTitle.aid')}</span><span class="col-count">${aids.length}</span></div>`;
3781
+ for (const a of aids) {
3782
+ const selected = a.aid === msgSel.aid;
3783
+ const expanded = msgAgentIsExpanded(a.aid);
3784
+ const name = a.displayName || (a.selfAID && a.selfAID !== 'unknown' ? shortAid(a.selfAID) : 'unknown');
3785
+ const peerCount = a.peerCount || 0;
3786
+ const groupBit = a.groupCount ? ` · ${tf('messages.groupCount', { count: a.groupCount })}` : '';
3787
+ let chatsHtml = '';
3788
+ if (expanded) {
3789
+ if (!scopeMatches) chatsHtml = `<div class="msg-tree-empty">${t('common.loading')}</div>`;
3790
+ else if (!peers.length) chatsHtml = `<div class="msg-tree-empty">${t('messages.empty.noPeers')}</div>`;
3791
+ else chatsHtml = peers.map(peer => renderMsgPeerItem(peer, 'msg-tree-chat')).join('');
3792
+ }
3793
+ aidTreeHtml += `<div class="msg-tree-agent${selected ? ' sel' : ''}${expanded ? ' expanded' : ''}">` +
3794
+ `<button type="button" class="msg-tree-agent-toggle" data-aid="${esc(a.aid)}" aria-expanded="${expanded ? 'true' : 'false'}">` +
3795
+ `<span class="msg-tree-chevron" aria-hidden="true">${expanded ? '▾' : '▸'}</span>` +
3796
+ `<span class="msg-tree-agent-main"><span class="name">${esc(name)}</span>` +
3797
+ `<span class="sub">${t('messages.colTitle.peers')} ${peerCount} · ↓${a.totalIn} ↑${a.totalOut}${groupBit ? `${groupBit}` : ''} · ${fmtAgo(a.lastAt)}</span>` +
3798
+ `</span>` +
3799
+ `</button>` +
3800
+ `<div class="msg-tree-chats"${expanded ? '' : ' hidden'}>${chatsHtml}</div>` +
3801
+ `</div>`;
3802
+ }
3803
+ if (!aids.length) aidTreeHtml += `<div class="empty">${t('messages.empty.noAgents')}</div>`;
3804
+ $('#msg-aids').innerHTML = `<div class="msg-aids-list">${aidListHtml}</div><div class="msg-tree-view">${aidTreeHtml}</div>`;
3805
+ $('#msg-aids').querySelectorAll('.msg-aid-item').forEach(item => {
3406
3806
  item.onclick = () => {
3407
3807
  if (item.dataset.aid === msgSel.aid) return;
3808
+ msgCollapsedAids.delete(item.dataset.aid);
3408
3809
  msgSel = { aid: item.dataset.aid, peer: null, before: null };
3810
+ detailViewState.msg = false;
3811
+ saveSessionUiState();
3812
+ subscribe('msg', msgSel);
3813
+ };
3814
+ });
3815
+
3816
+ $('#msg-aids').querySelectorAll('.msg-tree-agent-toggle').forEach(item => {
3817
+ item.onclick = () => {
3818
+ const aid = item.dataset.aid;
3819
+ if (aid !== msgSel.aid) {
3820
+ msgCollapsedAids.delete(aid);
3821
+ msgSel = { aid, peer: null, before: null };
3822
+ detailViewState.msg = false;
3823
+ saveSessionUiState();
3824
+ subscribe('msg', msgSel);
3825
+ return;
3826
+ }
3827
+ if (msgCollapsedAids.has(aid)) msgCollapsedAids.delete(aid);
3828
+ else msgCollapsedAids.add(aid);
3829
+ renderMsg(state.msg);
3830
+ };
3831
+ });
3832
+ $('#msg-aids').querySelectorAll('.msg-tree-chat').forEach(item => {
3833
+ item.onclick = event => {
3834
+ event.stopPropagation();
3835
+ if (item.dataset.peer === msgSel.peer) return;
3836
+ msgSel = { aid: msgSel.aid, peer: item.dataset.peer, before: null };
3837
+ saveSessionUiState();
3409
3838
  subscribe('msg', msgSel);
3410
3839
  };
3411
3840
  });
@@ -3419,40 +3848,20 @@ function renderMsg(data) {
3419
3848
  } else if (!peers.length) {
3420
3849
  peersHtml += `<div class="empty">${t('messages.empty.noPeers')}</div>`;
3421
3850
  } else {
3422
- for (const p of peers) {
3423
- const sel = p.peerId === msgSel.peer ? ' sel' : '';
3424
- const isGroup = p.chatType === 'group';
3425
- const displayName = p.chatType === 'group'
3426
- ? (p.groupName || p.peerName || p.groupId || p.peerId)
3427
- : (p.peerName || p.peerId);
3428
- const chatTitle = p.partition === 'thread'
3429
- ? (p.threadName || p.threadId || displayName)
3430
- : p.partition === 'legacy'
3431
- ? t('messages.legacy.title')
3432
- : displayName;
3433
- const channelLabel = p.channelName && p.channelName !== 'main' ? `${p.channelType}/${p.channelName}` : (p.channelType || '');
3434
- const groupMark = isGroup ? `<span class="msg-group-mark">${esc(t('messages.tag.groupShort'))}</span>` : '';
3435
- const partitionLabel = p.partition === 'thread'
3436
- ? t('messages.tag.topic')
3437
- : p.partition === 'legacy' ? t('messages.tag.legacy') : t('messages.tag.main');
3438
- const partitionMark = `<span class="msg-partition-mark msg-partition-${esc(p.partition || 'main')}">${esc(partitionLabel)}</span>`;
3439
- const channelTag = channelLabel
3440
- ? `<span class="msg-tag msg-channel msg-channel-${msgChannelTone(p.channelType)}">${esc(channelLabel)}</span>`
3441
- : '';
3442
- peersHtml += `<div class="list-item${sel}" data-peer="${esc(p.peerId)}">` +
3443
- `<div class="name">${groupMark}${partitionMark}<span class="msg-peer-title" title="${esc(chatTitle)}">${esc(shortId(chatTitle))}</span>${channelTag}</div>` +
3444
- `<div class="sub">${p.lable ? `${esc(p.lable)} · ` : ''}${p.partition === 'thread' ? `${esc(shortId(displayName))} · ` : ''}↓${p.inbound} ↑${p.outbound} · ${fmtAgo(p.lastAt)}</div></div>`;
3445
- }
3851
+ for (const p of peers) peersHtml += renderMsgPeerItem(p);
3446
3852
  }
3447
3853
  $('#msg-peers').innerHTML = peersHtml;
3448
- $('#msg-peers').querySelectorAll('.list-item').forEach(item => {
3854
+ $('#msg-peers').querySelectorAll('.msg-peer-item').forEach(item => {
3449
3855
  item.onclick = () => {
3450
3856
  if (item.dataset.peer === msgSel.peer) return;
3451
3857
  msgSel = { aid: msgSel.aid, peer: item.dataset.peer, before: null };
3858
+ saveSessionUiState();
3452
3859
  subscribe('msg', msgSel);
3453
3860
  };
3454
3861
  });
3455
3862
 
3863
+ renderMsgMobileControls(aids, peers, scopeMatches);
3864
+
3456
3865
  // 右:消息流
3457
3866
  const stream = $('#msg-stream');
3458
3867
  if (!msgSel.aid) { stream.innerHTML = `<div class="empty">${t('messages.empty.selectToView')}</div>`; return; }
@@ -3491,16 +3900,83 @@ function renderMsg(data) {
3491
3900
  }
3492
3901
  msgHtml += messages.length ? '</div>' : `<div class="empty">${t('messages.empty.noMessages')}</div></div>`;
3493
3902
  stream.innerHTML = renderMsgSessionHeader(sessionContext) + msgHtml;
3903
+ bindDetailPreview(stream, 'msg');
3904
+ const sessionDetailsRow = stream.querySelector('.msg-session-id-row');
3905
+ const sessionDetails = stream.querySelector('.msg-session-grid');
3906
+ if (sessionDetailsRow && sessionDetails) {
3907
+ const detailsKey = [msgSel.aid || '', msgSel.peer || '', sessionContext?.sessionId || ''].join('\u001f');
3908
+ const expanded = msgSessionDetailsExpanded.has(detailsKey)
3909
+ ? msgSessionDetailsExpanded.get(detailsKey) !== false
3910
+ : true;
3911
+ sessionDetails.hidden = !expanded;
3912
+ sessionDetailsRow.setAttribute('aria-expanded', String(expanded));
3913
+ sessionDetails.setAttribute('aria-expanded', String(expanded));
3914
+ const initialLabel = t(`messages.session.details.${expanded ? 'collapse' : 'expand'}`);
3915
+ sessionDetailsRow.setAttribute('aria-label', initialLabel);
3916
+ sessionDetailsRow.title = initialLabel;
3917
+ sessionDetails.setAttribute('aria-label', initialLabel);
3918
+ const initialIcon = sessionDetailsRow.querySelector('.msg-session-toggle-icon');
3919
+ if (initialIcon) initialIcon.textContent = expanded ? '▾' : '▸';
3920
+ const setSessionDetailsExpanded = () => {
3921
+ const nextExpanded = sessionDetails.hidden;
3922
+ sessionDetails.hidden = !nextExpanded;
3923
+ msgSessionDetailsExpanded.set(detailsKey, nextExpanded);
3924
+ sessionDetailsRow.setAttribute('aria-expanded', String(nextExpanded));
3925
+ sessionDetails.setAttribute('aria-expanded', String(nextExpanded));
3926
+ const label = t(`messages.session.details.${nextExpanded ? 'collapse' : 'expand'}`);
3927
+ sessionDetailsRow.setAttribute('aria-label', label);
3928
+ sessionDetailsRow.title = label;
3929
+ sessionDetails.setAttribute('aria-label', label);
3930
+ const icon = sessionDetailsRow.querySelector('.msg-session-toggle-icon');
3931
+ if (icon) icon.textContent = nextExpanded ? '▾' : '▸';
3932
+ };
3933
+ sessionDetailsRow.onclick = event => {
3934
+ event.stopPropagation();
3935
+ setSessionDetailsExpanded();
3936
+ };
3937
+ sessionDetailsRow.onkeydown = event => {
3938
+ if (event.key !== 'Enter' && event.key !== ' ') return;
3939
+ event.preventDefault();
3940
+ event.stopPropagation();
3941
+ setSessionDetailsExpanded();
3942
+ };
3943
+ sessionDetails.onclick = event => {
3944
+ event.stopPropagation();
3945
+ setSessionDetailsExpanded();
3946
+ };
3947
+ sessionDetails.onkeydown = event => {
3948
+ if (event.key !== 'Enter' && event.key !== ' ') return;
3949
+ event.preventDefault();
3950
+ event.stopPropagation();
3951
+ setSessionDetailsExpanded();
3952
+ };
3953
+ }
3494
3954
  const loadMore = stream.querySelector('.msg-load-more');
3495
3955
  if (loadMore) loadMore.onclick = () => {
3496
3956
  msgSel = { aid: msgSel.aid, peer: msgSel.peer, before: Number(loadMore.dataset.before) };
3957
+ saveSessionUiState();
3497
3958
  subscribe('msg', msgSel);
3498
3959
  };
3499
3960
  if (atBottom) stream.scrollTop = stream.scrollHeight;
3500
3961
  }
3501
3962
 
3963
+ function renderDetailToolbar(view, title = '') {
3964
+ return title ? `<div class="detail-toolbar"><span class="detail-toolbar-title">${esc(title)}</span></div>` : '';
3965
+ }
3966
+
3967
+ function renderSessionDetailToolbar() {
3968
+ return '<div class="detail-toolbar sess-detail-toolbar">' +
3969
+ `<span class="detail-toolbar-title">${esc(t('tab.sessions'))}</span>` +
3970
+ '<div class="sess-view-control">' +
3971
+ `<button class="view-toggle${sessChatMode ? ' active' : ''}" id="chat-toggle">` +
3972
+ `${sessChatMode ? t('sessions.chatView') : t('sessions.fullView')}</button>` +
3973
+ `<span class="toolbar-hint">${sessChatMode ? t('sessions.chatHint') : t('sessions.fullHint')}</span>` +
3974
+ '</div>' +
3975
+ '</div>';
3976
+ }
3977
+
3502
3978
  function renderMsgSessionHeader(context) {
3503
- if (!context) return '';
3979
+ if (!context) return renderDetailToolbar('msg');
3504
3980
  const status = context.status === 'processing' ? 'processing' : 'idle';
3505
3981
  const statusLabel = t(`messages.session.status.${status}`);
3506
3982
  const modeKey = context.chatMode === 'proactive' ? 'proactive' : (context.chatMode === 'interactive' ? 'interactive' : '');
@@ -3521,13 +3997,16 @@ function renderMsgSessionHeader(context) {
3521
3997
  `${esc(value)}</span>` +
3522
3998
  '</span>'
3523
3999
  ).join('');
3524
- return '<div class="sess-header msg-session-header">' +
4000
+ const detailsLabel = t('messages.session.details.collapse');
4001
+ return renderDetailToolbar('msg') +
4002
+ '<div class="sess-header msg-session-header">' +
3525
4003
  '<div class="msg-session-context-row">' +
3526
- '<div class="msg-session-id-row">' +
4004
+ `<div class="msg-session-id-row" role="button" tabindex="0" aria-expanded="false" aria-label="${esc(detailsLabel)}" title="${esc(detailsLabel)}">` +
3527
4005
  `<span class="msg-session-label">${t('messages.session.field.sessionId')}</span>` +
3528
4006
  `<span class="msg-session-id" title="${esc(context.sessionId || '—')}">${esc(context.sessionId || '—')}</span>` +
4007
+ '<span class="msg-session-toggle" aria-hidden="true"><span class="msg-session-toggle-icon">▸</span></span>' +
3529
4008
  '</div>' +
3530
- `<div class="msg-session-grid">${stats}</div>` +
4009
+ `<div class="msg-session-grid" role="button" tabindex="0" hidden>${stats}</div>` +
3531
4010
  '</div>' +
3532
4011
  '</div>';
3533
4012
  }
@@ -3665,7 +4144,7 @@ function renderSession(data) {
3665
4144
  // 有效筛选
3666
4145
  const filtered = filterSessionTranscripts(transcripts, sessShowAll, sessSourceFilter);
3667
4146
 
3668
- // 左栏:过滤条 + 列表
4147
+ // 会话列表顶部:过滤条 + 标题/计数;详情保持右侧第二列。
3669
4148
  const projOpts = projects.map(p =>
3670
4149
  `<option value="${esc(p.encoded)}"${p.encoded === sessSel.project ? ' selected' : ''}>${esc(p.label)} (${p.count})</option>`
3671
4150
  ).join('');
@@ -3680,7 +4159,7 @@ function renderSession(data) {
3680
4159
 
3681
4160
  const filterLabelKey = sessShowAll ? 'sessions.filter.all' : 'sessions.filter.valid';
3682
4161
  const filterTitleKey = sessShowAll ? 'sessions.filter.allTitle' : 'sessions.filter.validTitle';
3683
- let listHtml = '<div class="sess-filter">' +
4162
+ const contextHtml = '<div class="sess-filter">' +
3684
4163
  '<div class="sess-select-row">' +
3685
4164
  `<select id="sess-baseagent" title="Base Agent">${baseagentOpts}</select>` +
3686
4165
  `<select id="sess-project" title="${t('sessions.header.project')}">${projOpts}</select>` +
@@ -3693,8 +4172,9 @@ function renderSession(data) {
3693
4172
  `<input id="sess-filter-toggle" type="checkbox"${sessShowAll ? ' checked' : ''}>` +
3694
4173
  '<span class="sess-switch-track" aria-hidden="true"><span class="sess-switch-thumb"></span></span>' +
3695
4174
  '</label>' +
3696
- '</div></div>' +
3697
- '<div class="sess-items">';
4175
+ '</div></div>';
4176
+
4177
+ let listHtml = contextHtml + '<div class="sess-items">';
3698
4178
 
3699
4179
  if (!filtered.length) {
3700
4180
  const emptyText = data.noConfiguredBaseagent
@@ -3719,47 +4199,72 @@ function renderSession(data) {
3719
4199
  }
3720
4200
  listHtml += '</div>';
3721
4201
  $('#sess-list').innerHTML = listHtml;
4202
+ bindSessionListScroll();
4203
+ restoreSessionListScroll();
4204
+ window.requestAnimationFrame?.(restoreSessionListScroll);
3722
4205
 
3723
4206
  // 绑定交互
3724
4207
  const baseagentSel = $('#sess-baseagent');
3725
4208
  if (baseagentSel) baseagentSel.onchange = () => {
3726
4209
  console.log('[ecweb] Baseagent changed to:', baseagentSel.value);
3727
4210
  sessSel = { sessionId: null, project: null, baseagent: baseagentSel.value, turnBefore: null };
4211
+ detailViewState.session = false;
4212
+ saveSessionUiState();
3728
4213
  console.log('[ecweb] Subscribing to session with baseagent:', sessSel.baseagent);
3729
4214
  subscribe('session', { baseagent: sessSel.baseagent });
3730
4215
  };
3731
4216
  const projSel = $('#sess-project');
3732
4217
  if (projSel) projSel.onchange = () => {
3733
4218
  sessSel = { sessionId: null, project: projSel.value, baseagent: sessSel.baseagent, turnBefore: null };
4219
+ detailViewState.session = false;
4220
+ saveSessionUiState();
3734
4221
  subscribe('session', { project: sessSel.project, baseagent: sessSel.baseagent });
3735
4222
  };
3736
4223
  const filterToggle = $('#sess-filter-toggle');
3737
- if (filterToggle) filterToggle.onchange = () => { sessShowAll = filterToggle.checked; renderSession(state.session); };
4224
+ if (filterToggle) filterToggle.onchange = () => { sessShowAll = filterToggle.checked; saveSessionUiState(); renderSession(state.session); };
3738
4225
  const sourceTypeSelect = $('#sess-source-type');
3739
4226
  if (sourceTypeSelect) sourceTypeSelect.onchange = () => {
3740
4227
  sessSourceFilter = sourceTypeSelect.value;
4228
+ saveSessionUiState();
3741
4229
  renderSession(state.session);
3742
4230
  };
3743
4231
  $('#sess-list').querySelectorAll('.list-item').forEach(item => {
3744
- item.onclick = () => { sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent, turnBefore: null }; subscribe('session', sessSel); };
4232
+ item.onclick = () => {
4233
+ sessSel = { sessionId: item.dataset.sid, project: sessSel.project, baseagent: sessSel.baseagent, turnBefore: null };
4234
+ saveSessionUiState();
4235
+ if (window.matchMedia?.('(max-width: 479px)').matches) enterDetail('session');
4236
+ subscribe('session', sessSel);
4237
+ };
3745
4238
  });
3746
4239
 
3747
4240
  // 右:transcript 详情
3748
4241
  const detail = $('#sess-detail');
3749
- if (!sessSel.sessionId) { detail.innerHTML = `<div class="empty">${t('sessions.selectLog')}</div>`; return; }
3750
- if (!turns.length) { detail.innerHTML = `<div class="empty">${t('sessions.empty.noContent')}</div>`; return; }
4242
+ if (!sessSel.sessionId) {
4243
+ detail.innerHTML = `<div class="detail-toolbar"><span class="detail-placeholder">${t('sessions.selectLog')}</span></div><div class="empty">${t('sessions.selectLog')}</div>`;
4244
+ bindDetailPreview(detail, 'session');
4245
+ return;
4246
+ }
4247
+ if (!turns.length) {
4248
+ detail.innerHTML = renderDetailToolbar('session') + `<div class="empty">${t('sessions.empty.noContent')}</div>`;
4249
+ bindDetailPreview(detail, 'session');
4250
+ return;
4251
+ }
3751
4252
  const h = data.header || {};
3752
4253
  const atBottom = detail.scrollHeight - detail.scrollTop - detail.clientHeight < 60;
3753
- let html = renderSessHeader(h, data.baseagent || sessSel.baseagent);
4254
+ let html = renderSessionDetailToolbar();
4255
+ html += renderSessHeader(h, data.baseagent || sessSel.baseagent);
3754
4256
  if (data.turnPage?.hasMore && data.turnPage.nextBefore != null) {
3755
4257
  html += `<button class="ctrl-btn sess-load-more" type="button" data-before="${data.turnPage.nextBefore}">${t('sessions.loadOlder') || '加载更早内容'}</button>`;
3756
4258
  }
3757
4259
  html += '<div class="turn-list">' + (sessChatMode ? renderChatView(turns) : renderFullView(turns)) + '</div>';
3758
4260
  detail.innerHTML = html;
4261
+ bindDetailPreview(detail, 'session');
4262
+ bindSessionHeaderToggle(detail, [sessSel.project || '', data.baseagent || sessSel.baseagent || '', h.sessionId || ''].join('\u001f'));
3759
4263
 
3760
4264
  const loadMore = detail.querySelector('.sess-load-more');
3761
4265
  if (loadMore) loadMore.onclick = () => {
3762
4266
  sessSel = { ...sessSel, turnBefore: Number(loadMore.dataset.before) };
4267
+ saveSessionUiState();
3763
4268
  subscribe('session', sessSel);
3764
4269
  };
3765
4270
 
@@ -3770,6 +4275,41 @@ function renderSession(data) {
3770
4275
  if (atBottom) detail.scrollTop = detail.scrollHeight;
3771
4276
  }
3772
4277
 
4278
+ function bindSessionHeaderToggle(container, key) {
4279
+ const toggleRow = container?.querySelector('.sess-header-toggle-row');
4280
+ const details = container?.querySelector('.sess-header-details');
4281
+ if (!toggleRow || !details) return;
4282
+ const remembered = sessHeaderExpanded.get(key);
4283
+ const initialExpanded = remembered == null ? true : remembered !== false;
4284
+ const apply = expanded => {
4285
+ details.hidden = !expanded;
4286
+ const label = t(`sessions.details.${expanded ? 'collapse' : 'expand'}`);
4287
+ toggleRow.setAttribute('aria-expanded', String(expanded));
4288
+ toggleRow.setAttribute('aria-label', label);
4289
+ toggleRow.title = label;
4290
+ details.setAttribute('aria-expanded', String(expanded));
4291
+ details.setAttribute('aria-label', label);
4292
+ const icon = toggleRow.querySelector('.sess-header-toggle-icon');
4293
+ if (icon) icon.textContent = expanded ? '▾' : '▸';
4294
+ };
4295
+ const toggle = event => {
4296
+ event.preventDefault();
4297
+ event.stopPropagation();
4298
+ const expanded = details.hidden;
4299
+ sessHeaderExpanded.set(key, expanded);
4300
+ apply(expanded);
4301
+ };
4302
+ toggleRow.onclick = toggle;
4303
+ details.onclick = toggle;
4304
+ toggleRow.onkeydown = event => {
4305
+ if (event.key === 'Enter' || event.key === ' ') toggle(event);
4306
+ };
4307
+ details.onkeydown = event => {
4308
+ if (event.key === 'Enter' || event.key === ' ') toggle(event);
4309
+ };
4310
+ apply(initialExpanded);
4311
+ }
4312
+
3773
4313
  // 完整视图:所有轮次按 4 类渲染
3774
4314
  function renderFullView(turns) {
3775
4315
  let html = '';
@@ -3899,24 +4439,23 @@ function renderSessHeader(h, baseagent) {
3899
4439
  const dot = h.online ? `<span class="dot on"></span>${t('common.online')}` : `<span class="dot idle"></span>${t('common.offline')}`;
3900
4440
  bind = `<span class="sh-stat">🔗 ${esc(h.boundChannel || '')} · ${esc(shortAid(h.boundSelf || ''))} ${dot}</span>`;
3901
4441
  }
3902
- return '<div class="sess-header">' +
3903
- '<div class="sh-title-row">' +
4442
+ const detailsLabel = t('sessions.details.collapse');
4443
+ return '<div class="sess-header sess-header-collapsible">' +
4444
+ `<div class="sess-header-toggle-row" role="button" tabindex="0" aria-expanded="true" aria-label="${esc(detailsLabel)}" title="${esc(detailsLabel)}">` +
3904
4445
  `<div class="sh-title" title="${esc(title)}">${esc(title)}</div>` +
4446
+ '<span class="sess-header-toggle" aria-hidden="true"><span class="sess-header-toggle-icon">▾</span></span>' +
3905
4447
  '</div>' +
3906
- '<div class="sh-stats">' +
3907
- `<span class="sh-stat" title="${tf('sessions.messageCountTitle', { user: h.userMsgs || 0, total: h.totalMsgs || 0 })}">💬 ${h.userMsgs || 0}/${h.totalMsgs || 0} ${t('sessions.msgUnit')}</span>` +
3908
- (h.model ? `<span class="sh-stat">🤖 ${esc(h.model)}</span>` : '') +
3909
- tok + ctx + cost +
3910
- (h.gitBranch ? `<span class="sh-stat">🌿 ${esc(h.gitBranch)}</span>` : '') +
3911
- (h.version ? `<span class="sh-stat">cc ${esc(h.version)}</span>` : '') +
3912
- bind +
3913
- '</div>' +
3914
- '<div class="sh-footer-row">' +
3915
- renderCatBar(h.counts) +
3916
- '<div class="sess-view-control">' +
3917
- `<button class="view-toggle${sessChatMode ? ' active' : ''}" id="chat-toggle">` +
3918
- `${sessChatMode ? t('sessions.chatView') : t('sessions.fullView')}</button>` +
3919
- `<span class="toolbar-hint">${sessChatMode ? t('sessions.chatHint') : t('sessions.fullHint')}</span>` +
4448
+ '<div class="sess-header-details" role="button" tabindex="0" aria-expanded="true" aria-label="' + esc(detailsLabel) + '">' +
4449
+ '<div class="sh-stats">' +
4450
+ `<span class="sh-stat" title="${tf('sessions.messageCountTitle', { user: h.userMsgs || 0, total: h.totalMsgs || 0 })}">💬 ${h.userMsgs || 0}/${h.totalMsgs || 0} ${t('sessions.msgUnit')}</span>` +
4451
+ (h.model ? `<span class="sh-stat">🤖 ${esc(h.model)}</span>` : '') +
4452
+ tok + ctx + cost +
4453
+ (h.gitBranch ? `<span class="sh-stat">🌿 ${esc(h.gitBranch)}</span>` : '') +
4454
+ (h.version ? `<span class="sh-stat">cc ${esc(h.version)}</span>` : '') +
4455
+ bind +
4456
+ '</div>' +
4457
+ '<div class="sh-footer-row">' +
4458
+ renderCatBar(h.counts) +
3920
4459
  '</div>' +
3921
4460
  '</div>' +
3922
4461
  '</div>';
@@ -5346,7 +5885,7 @@ function openAgentConfigDrawer(aid, data) {
5346
5885
  const fullaccessState = enabled => currentLang === 'zh-CN'
5347
5886
  ? (enabled ? '开启' : '关闭')
5348
5887
  : (enabled ? 'Enabled' : 'Disabled');
5349
- const fullaccessSection = fullaccessStatus.hostEnabled ?
5888
+ const fullaccessSection =
5350
5889
  `<section class="ag-edit-section">` +
5351
5890
  `<div class="ag-edit-fullaccess-row">` +
5352
5891
  `<span class="ag-edit-field-label">Owner Fullaccess<span class="basic-settings-tip" tabindex="0" role="img" aria-label="${esc(fullaccessHelpLabel)}" data-tooltip="${esc(fullaccessHelp)}">?</span></span>` +
@@ -5356,7 +5895,7 @@ function openAgentConfigDrawer(aid, data) {
5356
5895
  `<span class="sess-switch-track" aria-hidden="true"><span class="sess-switch-thumb"></span></span>` +
5357
5896
  `</label>` +
5358
5897
  `</div>` +
5359
- `</section>` : '';
5898
+ `</section>`;
5360
5899
 
5361
5900
  const html = `<div class="ag-edit-backdrop" id="ag-edit-backdrop">` +
5362
5901
  `<aside class="ag-edit-drawer" role="dialog" aria-modal="true" aria-labelledby="ag-edit-title">` +
@@ -5439,7 +5978,6 @@ function openAgentConfigDrawer(aid, data) {
5439
5978
  document.addEventListener('keydown', ctx.onKeydown);
5440
5979
  syncAgentBaseagentFields(ctx);
5441
5980
  syncAgentBaseagentOptions(ctx);
5442
- setTimeout(() => backdrop.querySelector('#ag-edit-owners')?.focus(), 0);
5443
5981
  }
5444
5982
 
5445
5983
  async function agentOpEdit(aid) {
@@ -5789,8 +6327,7 @@ const TRIGGER_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
5789
6327
  const TRIGGER_PERMISSION_MODES = ['readonly', 'auto', 'request', 'bypass'];
5790
6328
  function triggerPermissionModes(currentMode = '') {
5791
6329
  const modes = [...TRIGGER_PERMISSION_MODES];
5792
- if ((state.triggers?.fullaccessEnabled === true
5793
- && state.triggers?.canApprovePersistentFullAccess === true)
6330
+ if (state.triggers?.canApprovePersistentFullAccess === true
5794
6331
  || currentMode === 'fullaccess') modes.push('fullaccess');
5795
6332
  return modes;
5796
6333
  }
@@ -6332,6 +6869,18 @@ function triggerAgentInfo(agentAid) {
6332
6869
  return (state.triggers?.agents || []).find(item => item.aid === agentAid || item.value === agentAid) || null;
6333
6870
  }
6334
6871
 
6872
+ function isDaemonTriggerAgent(aid, label, channels) {
6873
+ const normalizedAid = String(aid || '').trim().toLowerCase();
6874
+ const normalizedLabel = String(label || '').trim().toLowerCase();
6875
+ return normalizedAid === 'daemon'
6876
+ || normalizedAid === '__daemon__'
6877
+ || normalizedAid.endsWith('#daemon')
6878
+ || normalizedAid.endsWith('.daemon')
6879
+ || normalizedLabel === 'daemon'
6880
+ || (Array.isArray(channels) && channels.some(channel => String(channel).trim().toLowerCase() === 'daemon'))
6881
+ || /\(daemon\)$/i.test(normalizedLabel);
6882
+ }
6883
+
6335
6884
  function triggerChannelOptions(agentAid, current) {
6336
6885
  const channels = [...(triggerAgentInfo(agentAid)?.channels || [])].map(String).filter(Boolean);
6337
6886
  if (current && !channels.includes(current)) channels.push(current);
@@ -6468,31 +7017,22 @@ function triggerRunJson(value) {
6468
7017
  try { return JSON.stringify(value, null, 2); } catch { return String(value); }
6469
7018
  }
6470
7019
 
6471
- function triggerRunPreview(run) {
6472
- return String(
6473
- run?.error?.message
6474
- || run?.reply?.textPreview
6475
- || run?.script?.result?.text
6476
- || run?.script?.stderrPreview
6477
- || run?.script?.stdoutPreview
6478
- || run?.reason
6479
- || '',
6480
- );
6481
- }
6482
-
6483
- function triggerHistoryListHtml(runs, selectedRunId) {
7020
+ function triggerHistoryListHtml(runs, selectedRunId, inlineDetails, renderDetail) {
6484
7021
  if (!runs.length) return `<div class="trig-history-empty">${t('triggers.history.empty')}</div>`;
6485
7022
  return runs.map(run => {
6486
7023
  const runId = String(run.runId || '');
7024
+ const selected = runId === selectedRunId;
7025
+ const expanded = inlineDetails && selected;
6487
7026
  const duration = Number(run.finishedAt || 0) - Number(run.startedAt || 0);
6488
7027
  const source = run.source?.eventName || run.source?.type || '—';
6489
7028
  const execution = run.processing?.mode || run.definition?.name || '—';
6490
- const preview = triggerRunPreview(run);
6491
- return `<button type="button" class="trig-history-item${runId === selectedRunId ? ' selected' : ''}" data-runid="${esc(runId)}">` +
6492
- `<div class="trig-history-item-head"><span>${run.startedAt ? fmtTime(run.startedAt) : '—'}</span>${triggerRunStatusBadge(run.status)}</div>` +
7029
+ return `<article class="trig-history-entry${expanded ? ' expanded' : ''}">` +
7030
+ `<button type="button" class="trig-history-item${selected ? ' selected' : ''}" data-runid="${esc(runId)}" aria-expanded="${expanded}">` +
7031
+ `<div class="trig-history-item-head"><span>${run.startedAt ? fmtTime(run.startedAt) : '—'}</span>${triggerRunStatusBadge(run.status)}<span class="trig-history-chevron" aria-hidden="true">›</span></div>` +
6493
7032
  `<div class="trig-history-item-meta"><span>${esc(source)}</span><span>${esc(execution)}</span><span>${esc(triggerRunDurationLabel(duration))}</span></div>` +
6494
- (preview ? `<div class="trig-history-item-preview">${esc(preview)}</div>` : '') +
6495
- `</button>`;
7033
+ `</button>` +
7034
+ (expanded ? `<div class="trig-history-inline-detail">${renderDetail(run)}</div>` : '') +
7035
+ `</article>`;
6496
7036
  }).join('');
6497
7037
  }
6498
7038
 
@@ -6843,29 +7383,38 @@ function triggerHistoryDetailHtml(run, definition) {
6843
7383
  function renderTriggerHistoryDrawer(ctx) {
6844
7384
  if (_triggerHistory !== ctx) return;
6845
7385
  const list = ctx.backdrop.querySelector('#trig-history-list');
6846
- const detail = ctx.backdrop.querySelector('#trig-history-detail');
6847
7386
  const count = ctx.backdrop.querySelector('#trig-history-count');
7387
+ const detail = ctx.backdrop.querySelector('#trig-history-detail');
7388
+ const inlineDetails = ctx.layoutQuery.matches;
6848
7389
  if (count) count.textContent = tf('triggers.history.runCount', { count: ctx.runs.length });
7390
+ const renderDetail = run => {
7391
+ const revision = String(run?.definition?.revision || '');
7392
+ const historicalDefinition = ctx.definitionsByRevision.get(revision)
7393
+ || (revision && revision === String(ctx.currentRevision || '') ? ctx.currentDefinition : undefined);
7394
+ return triggerHistoryDetailHtml(run, historicalDefinition);
7395
+ };
6849
7396
  if (list) {
6850
- list.innerHTML = triggerHistoryListHtml(ctx.runs, ctx.selectedRunId);
7397
+ list.innerHTML = triggerHistoryListHtml(ctx.runs, ctx.selectedRunId, inlineDetails, renderDetail);
6851
7398
  list.querySelectorAll('.trig-history-item').forEach(item => {
6852
7399
  item.addEventListener('click', () => {
6853
- ctx.selectedRunId = item.dataset.runid || '';
7400
+ const runId = item.dataset.runid || '';
7401
+ ctx.selectedRunId = inlineDetails && ctx.selectedRunId === runId ? '' : runId;
6854
7402
  renderTriggerHistoryDrawer(ctx);
6855
7403
  });
6856
7404
  });
6857
7405
  }
6858
- const selected = ctx.runs.find(run => String(run.runId || '') === ctx.selectedRunId) || null;
6859
- const revision = String(selected?.definition?.revision || '');
6860
- const historicalDefinition = ctx.definitionsByRevision.get(revision)
6861
- || (revision && revision === String(ctx.currentRevision || '') ? ctx.currentDefinition : undefined);
6862
- if (detail) detail.innerHTML = triggerHistoryDetailHtml(selected, historicalDefinition);
7406
+ const selectedRun = ctx.runs.find(run => String(run.runId || '') === ctx.selectedRunId) || null;
7407
+ if (detail) detail.innerHTML = !inlineDetails && selectedRun
7408
+ ? `<div class="trig-history-detail">${renderDetail(selectedRun)}</div>`
7409
+ : `<div class="trig-history-detail-empty">${t('triggers.history.selectRun')}</div>`;
6863
7410
  }
6864
7411
 
6865
7412
  function closeTriggerHistoryDrawer() {
6866
7413
  const ctx = _triggerHistory;
6867
7414
  if (!ctx) return;
6868
7415
  document.removeEventListener('keydown', ctx.onKeydown);
7416
+ if (ctx.layoutQuery?.removeEventListener) ctx.layoutQuery.removeEventListener('change', ctx.onLayoutChange);
7417
+ else if (ctx.layoutQuery?.removeListener) ctx.layoutQuery.removeListener(ctx.onLayoutChange);
6869
7418
  try { ctx.backdrop.remove(); } catch {}
6870
7419
  _triggerHistory = null;
6871
7420
  }
@@ -6884,10 +7433,10 @@ async function openTriggerHistoryDrawer(id, agent) {
6884
7433
  `<button class="ag-edit-close" id="trig-history-close" type="button" aria-label="${t('common.close')}">×</button>` +
6885
7434
  `</div>` +
6886
7435
  `<div class="trig-history-content">` +
6887
- `<div class="trig-history-list-pane"><div id="trig-history-count" class="trig-history-pane-title">${t('triggers.history.loading')}</div><div id="trig-history-list" class="trig-history-list"><div class="trig-history-empty">${t('triggers.history.loading')}</div></div></div>` +
6888
- `<div id="trig-history-detail" class="trig-history-detail"><div class="trig-history-detail-empty">${t('triggers.history.selectRun')}</div></div>` +
7436
+ `<div class="trig-history-list-pane"><div id="trig-history-list" class="trig-history-list"><div class="trig-history-empty">${t('triggers.history.loading')}</div></div></div>` +
7437
+ `<div id="trig-history-detail" class="trig-history-detail-pane"><div class="trig-history-detail-empty">${t('triggers.history.selectRun')}</div></div>` +
6889
7438
  `</div>` +
6890
- `<div class="ag-edit-actions"><button class="ctrl-btn" id="trig-history-done" type="button">${t('common.close')}</button></div>` +
7439
+ `<div class="ag-edit-actions trig-history-actions"><span id="trig-history-count" class="trig-history-count" aria-live="polite">${t('triggers.history.loading')}</span><button class="ctrl-btn" id="trig-history-done" type="button">${t('common.close')}</button></div>` +
6891
7440
  `</aside>` +
6892
7441
  `</div>`;
6893
7442
  const wrap = document.createElement('div');
@@ -6898,16 +7447,20 @@ async function openTriggerHistoryDrawer(id, agent) {
6898
7447
  backdrop,
6899
7448
  runs: [],
6900
7449
  selectedRunId: '',
7450
+ layoutQuery: window.matchMedia('(orientation: portrait)'),
6901
7451
  currentDefinition: item.definition,
6902
7452
  currentRevision: item.revision,
6903
7453
  definitionsByRevision: new Map(),
6904
7454
  onKeydown: e => { if (e.key === 'Escape') closeTriggerHistoryDrawer(); },
7455
+ onLayoutChange: () => renderTriggerHistoryDrawer(ctx),
6905
7456
  };
6906
7457
  _triggerHistory = ctx;
6907
7458
  backdrop.addEventListener('click', e => { if (e.target === backdrop) closeTriggerHistoryDrawer(); });
6908
7459
  backdrop.querySelector('#trig-history-close').addEventListener('click', closeTriggerHistoryDrawer);
6909
7460
  backdrop.querySelector('#trig-history-done').addEventListener('click', closeTriggerHistoryDrawer);
6910
7461
  document.addEventListener('keydown', ctx.onKeydown);
7462
+ if (ctx.layoutQuery.addEventListener) ctx.layoutQuery.addEventListener('change', ctx.onLayoutChange);
7463
+ else if (ctx.layoutQuery.addListener) ctx.layoutQuery.addListener(ctx.onLayoutChange);
6911
7464
 
6912
7465
  try {
6913
7466
  const response = await ipcSend({ type: 'trigger.history', agentAid: agent, triggerId: id, limit: 500 }, 10000);
@@ -7760,7 +8313,6 @@ function openTriggerEditDrawer(id, agent) {
7760
8313
  if (_triggerEdit === ctx) toast(e.message || String(e), true);
7761
8314
  });
7762
8315
  if (triggerUsesModelOptions(ctx)) syncTriggerOptionFields(ctx, original);
7763
- setTimeout(() => backdrop.querySelector('#trig-edit-name')?.focus(), 0);
7764
8316
  }
7765
8317
 
7766
8318
  function renderTriggers(data) {
@@ -7771,7 +8323,7 @@ function renderTriggers(data) {
7771
8323
 
7772
8324
  // 左列:agent 列表(仿 msg list-item 风格)
7773
8325
  let aHtml = '<div class="col-title trig-title">' +
7774
- '<span>Agent</span>' +
8326
+ `<span>${t('triggers.th.agent')}</span>` +
7775
8327
  `<button id="trig-agents-toggle" class="trig-collapse-btn" title="${trigAgentsCollapsed ? t('triggers.collapse.expand') : t('triggers.collapse.collapse')}">${trigAgentsCollapsed ? '›' : '‹'}</button>` +
7776
8328
  '</div>';
7777
8329
  if (!trigAgentsCollapsed) {
@@ -7779,9 +8331,12 @@ function renderTriggers(data) {
7779
8331
  for (const ag of agents) {
7780
8332
  const aid = ag.aid || ag.value || '';
7781
8333
  const sel = aid === selAid ? ' sel' : '';
8334
+ const isDaemonAgent = isDaemonTriggerAgent(aid, ag.label, ag.channels);
8335
+ const label = isDaemonAgent ? t('triggers.agent.daemon') : ag.label;
7782
8336
  aHtml += `<div class="list-item${sel}" data-aid="${esc(aid)}">` +
7783
- `<div class="name">${esc(ag.label)}</div>` +
7784
- `<div class="sub">${esc(aid)}</div></div>`;
8337
+ `<div class="name">${esc(label)}</div>` +
8338
+ (isDaemonAgent ? '' : `<div class="sub">${esc(aid)}</div>`) +
8339
+ '</div>';
7785
8340
  }
7786
8341
  }
7787
8342
  const agentCol = $('#trig-agents');
@@ -7799,11 +8354,13 @@ function renderTriggers(data) {
7799
8354
  if (trigAgentsCollapsed) return;
7800
8355
  if (!trigSel.agent) return;
7801
8356
  trigSel.agent = null;
8357
+ saveSessionUiState();
7802
8358
  subscribe('triggers', {});
7803
8359
  };
7804
8360
  agentCol.querySelectorAll('.list-item').forEach(item => {
7805
8361
  item.onclick = () => {
7806
8362
  trigSel.agent = item.dataset.aid;
8363
+ saveSessionUiState();
7807
8364
  subscribe('triggers', { agent: trigSel.agent });
7808
8365
  };
7809
8366
  });
@@ -7811,14 +8368,14 @@ function renderTriggers(data) {
7811
8368
  // 右列:table,每字段一列
7812
8369
  const el = $('#trig-table');
7813
8370
  if (!triggers.length) {
7814
- el.innerHTML = `<div class="empty" style="padding:16px">${selAid ? t('triggers.empty.noAgentTriggers') : t('triggers.empty.noTriggers')}</div>`;
8371
+ el.innerHTML = `<div class="trig-table-empty empty" style="padding:16px">${selAid ? t('triggers.empty.noAgentTriggers') : t('triggers.empty.noTriggers')}</div>`;
7815
8372
  return;
7816
8373
  }
7817
8374
 
7818
8375
  let html = '<table><thead><tr>' +
7819
- `<th>Agent</th><th>${t('triggers.th.status')}</th><th>${t('triggers.th.subscription')}</th><th>${t('triggers.th.name')}</th><th>ID</th><th>${t('triggers.th.type')}</th><th>${t('triggers.th.expression')}</th>` +
8376
+ `<th class="trig-agent-col">${t('triggers.th.agent')}</th><th>${t('triggers.th.status')}</th><th class="trig-subscription-col">${t('triggers.th.subscription')}</th><th>${t('triggers.th.name')}</th><th class="trig-id-col">${t('triggers.th.id')}</th><th>${t('triggers.th.type')}</th><th>${t('triggers.th.expression')}</th>` +
7820
8377
  `<th>${t('triggers.th.lastFire')}</th><th>${t('triggers.th.nextFire')}</th><th>${t('triggers.th.fireCount')}</th><th>${t('triggers.th.failCount')}</th><th>${t('triggers.th.lastResult')}</th><th>${t('triggers.field.executionType')}</th>` +
7821
- `<th>${t('triggers.th.targetChannel')}</th><th>${t('triggers.field.channelId')}</th><th>${t('triggers.th.channelType')}</th>` +
8378
+ `<th class="trig-target-channel-col">${t('triggers.th.targetChannel')}</th><th class="trig-channel-id-col">${t('triggers.field.channelId')}</th><th class="trig-channel-type-col">${t('triggers.th.channelType')}</th>` +
7822
8379
  `<th>${t('triggers.th.createdBy')}</th><th>${t('triggers.th.createdChannel')}</th><th>${t('triggers.th.createdAt')}</th><th class="trig-ops-col">${t('triggers.th.operations')}</th>` +
7823
8380
  '</tr></thead><tbody>';
7824
8381
  for (const trigger of triggers) {
@@ -7827,42 +8384,62 @@ function renderTriggers(data) {
7827
8384
  const lastFireTime = trigger.lastFiredAt || trigger.lastScheduledAt;
7828
8385
  const lastResult = trigger.lastResult || (trigger.lastScheduledAt ? t('triggers.lastUnrecorded') : '');
7829
8386
  const agentAid = trigger.agentAid || trigger.schedulerAid || selAid || '';
7830
- const agentLabel = trigger.agentLabel || (agentAid ? shortAid(agentAid) : '');
8387
+ const agentInfo = triggerAgentInfo(agentAid);
8388
+ const agentLabel = isDaemonTriggerAgent(agentAid, trigger.agentLabel, agentInfo?.channels)
8389
+ ? t('triggers.agent.daemon')
8390
+ : (trigger.agentLabel || (agentAid ? shortAid(agentAid) : ''));
7831
8391
  const sourceType = triggerSourceType(trigger);
7832
- html += `<tr class="${active ? '' : 'trig-disabled'}">` +
7833
- `<td title="${esc(agentAid)}">${esc(agentLabel)}</td>` +
7834
- `<td>${trigStatusBadge(status)}</td>` +
7835
- `<td>${trigSubscriptionBadge(trigger.subscription, sourceType)}</td>` +
7836
- `<td>${esc(trigger.name ?? trigger.label ?? '')}</td>` +
7837
- `<td>${esc(trigger.id ?? trigger.value ?? '')}</td>` +
7838
- `<td>${esc(triggerScheduleLabel(sourceType || trigger.scheduleType || ''))}</td>` +
7839
- `<td>${esc(triggerScheduleValueLabel(trigger.scheduleType, trigger.scheduleValue))}</td>` +
7840
- `<td>${lastFireTime ? fmtTime(lastFireTime) : '—'}</td>` +
7841
- `<td>${trigger.nextFireAt ? fmtTime(trigger.nextFireAt) : '—'}</td>` +
7842
- `<td>${trigger.fireCount ?? 0}</td>` +
7843
- `<td>${trigger.failCount ? `<span style="color:var(--red)">${trigger.failCount}</span>` : '0'}</td>` +
7844
- `<td>${lastResult ? esc(lastResult) : '—'}</td>` +
7845
- `<td>${esc(triggerExecutionTypeLabel(trigger.definition?.execution?.type ?? trigger.executionType ?? trigger.execution?.type))}</td>` +
7846
- `<td>${esc(trigger.targetChannel ?? '')}</td>` +
7847
- `<td>${esc(trigger.targetChannelId ?? '')}</td>` +
7848
- `<td>${esc(trigger.targetChannelType ?? '')}</td>` +
7849
- `<td>${esc(trigger.createdByPeerId ?? '')}</td>` +
7850
- `<td>${esc(trigger.createdByChannel ?? '')}</td>` +
7851
- `<td>${trigger.createdAt ? fmtTime(trigger.createdAt) : '—'}</td>` +
7852
- `<td class="trig-ops-col">${triggerOpsHtml(trigger)}</td>` +
8392
+ const mobileLabel = key => ` data-mobile-label="${esc(t(key))}"`;
8393
+ html += `<tr class="trig-row${active ? '' : ' trig-disabled'}" tabindex="0" aria-expanded="false">` +
8394
+ `<td class="trig-agent-col"${mobileLabel('triggers.th.agent')} title="${esc(agentAid)}">${esc(agentLabel)}</td>` +
8395
+ `<td${mobileLabel('triggers.th.status')}>${trigStatusBadge(status)}</td>` +
8396
+ `<td class="trig-subscription-col"${mobileLabel('triggers.th.subscription')}>${trigSubscriptionBadge(trigger.subscription, sourceType)}</td>` +
8397
+ `<td${mobileLabel('triggers.th.name')}>${esc(trigger.name ?? trigger.label ?? '')}</td>` +
8398
+ `<td class="trig-id-col"${mobileLabel('triggers.th.id')}>${esc(trigger.id ?? trigger.value ?? '')}</td>` +
8399
+ `<td${mobileLabel('triggers.th.type')}>${esc(triggerScheduleLabel(sourceType || trigger.scheduleType || ''))}</td>` +
8400
+ `<td${mobileLabel('triggers.th.expression')}>${esc(triggerScheduleValueLabel(trigger.scheduleType, trigger.scheduleValue))}</td>` +
8401
+ `<td${mobileLabel('triggers.th.lastFire')}>${lastFireTime ? fmtTime(lastFireTime) : '—'}</td>` +
8402
+ `<td${mobileLabel('triggers.th.nextFire')}>${trigger.nextFireAt ? fmtTime(trigger.nextFireAt) : '—'}</td>` +
8403
+ `<td${mobileLabel('triggers.th.fireCount')}>${trigger.fireCount ?? 0}</td>` +
8404
+ `<td${mobileLabel('triggers.th.failCount')}>${trigger.failCount ? `<span style="color:var(--red)">${trigger.failCount}</span>` : '0'}</td>` +
8405
+ `<td${mobileLabel('triggers.th.lastResult')}>${lastResult ? esc(lastResult) : '—'}</td>` +
8406
+ `<td${mobileLabel('triggers.field.executionType')}>${esc(triggerExecutionTypeLabel(trigger.definition?.execution?.type ?? trigger.executionType ?? trigger.execution?.type))}</td>` +
8407
+ `<td class="trig-target-channel-col"${mobileLabel('triggers.th.targetChannel')}>${esc(trigger.targetChannel ?? '')}</td>` +
8408
+ `<td class="trig-channel-id-col"${mobileLabel('triggers.field.channelId')}>${esc(trigger.targetChannelId ?? '')}</td>` +
8409
+ `<td class="trig-channel-type-col"${mobileLabel('triggers.th.channelType')}>${esc(trigger.targetChannelType ?? '')}</td>` +
8410
+ `<td${mobileLabel('triggers.th.createdBy')}>${esc(trigger.createdByPeerId ?? '')}</td>` +
8411
+ `<td${mobileLabel('triggers.th.createdChannel')}>${esc(trigger.createdByChannel ?? '')}</td>` +
8412
+ `<td${mobileLabel('triggers.th.createdAt')}>${trigger.createdAt ? fmtTime(trigger.createdAt) : '—'}</td>` +
8413
+ `<td class="trig-ops-col"${mobileLabel('triggers.th.operations')}>${triggerOpsHtml(trigger)}</td>` +
7853
8414
  '</tr>';
7854
8415
  }
7855
8416
  html += '</tbody></table>';
7856
- el.innerHTML = html;
8417
+ el.innerHTML = `<div class="trig-table-wide">${html}</div>`;
7857
8418
  ensureOpsOutsideClose();
8419
+ bindTriggerOps(el);
8420
+ el.querySelectorAll('.trig-row').forEach(row => {
8421
+ const toggle = event => {
8422
+ if (event.target.closest('button, a, input, select, textarea, summary, details')) return;
8423
+ const expanded = row.classList.toggle('trig-expanded');
8424
+ row.setAttribute('aria-expanded', String(expanded));
8425
+ };
8426
+ row.addEventListener('click', toggle);
8427
+ row.addEventListener('keydown', event => {
8428
+ if (event.key !== 'Enter' && event.key !== ' ') return;
8429
+ event.preventDefault();
8430
+ toggle(event);
8431
+ });
8432
+ });
8433
+ }
7858
8434
 
7859
- el.querySelectorAll('.trig-ops button[data-trig-op]').forEach(btn => {
8435
+ function bindTriggerOps(container) {
8436
+ container.querySelectorAll('.trig-ops button[data-trig-op]').forEach(btn => {
7860
8437
  btn.addEventListener('click', async (e) => {
7861
8438
  const op = btn.dataset.trigOp;
7862
8439
  if (op === 'more') {
7863
8440
  const more = btn.closest('.ops-more');
7864
8441
  const wasOpen = more?.classList.contains('open');
7865
- document.querySelectorAll('.ops-more.open').forEach(m => m.classList.remove('open'));
8442
+ document.querySelectorAll('.ops-more.open').forEach(item => item.classList.remove('open'));
7866
8443
  if (more && !wasOpen) more.classList.add('open');
7867
8444
  e.stopPropagation();
7868
8445
  return;
@@ -7883,8 +8460,9 @@ function startApp() {
7883
8460
  initTabs();
7884
8461
  initRolesTab();
7885
8462
  initRoleDefinitionsTab();
8463
+ initHistory();
7886
8464
  // 恢复保存的 tab 视图
7887
- switchView(currentView);
8465
+ switchView(currentView, { history: false });
7888
8466
  connect();
7889
8467
  $('#logout-btn').onclick = () => {
7890
8468
  localStorage.removeItem(TOKEN_KEY);
@@ -7912,13 +8490,17 @@ function resolveTheme(preference) {
7912
8490
 
7913
8491
  function updateThemeButton() {
7914
8492
  const btn = $('#theme-btn');
7915
- if (!btn) return;
7916
8493
  const icons = { light: '☀️', dark: '🌙', system: '🖥️' };
7917
- const label = t('theme.buttonTitle').replace('{mode}', t('theme.' + themePreference));
7918
- btn.textContent = icons[themePreference] || icons.light;
7919
- btn.title = label;
7920
- btn.setAttribute('aria-label', label);
7921
- btn.dataset.themeMode = themePreference;
8494
+ const modeLabel = t('theme.' + themePreference);
8495
+ const label = tf('theme.buttonTitle', { mode: modeLabel });
8496
+ if (btn) {
8497
+ btn.textContent = icons[themePreference] || icons.light;
8498
+ btn.title = label;
8499
+ btn.setAttribute('aria-label', label);
8500
+ btn.dataset.themeMode = themePreference;
8501
+ }
8502
+ const mobileLabel = $('[data-mobile-theme-label]');
8503
+ if (mobileLabel) mobileLabel.textContent = tf('theme.drawerLabel', { mode: modeLabel });
7922
8504
  }
7923
8505
 
7924
8506
  function disposeWindowChart(key) {
@@ -10039,6 +10621,7 @@ const COMMAND_PERMISSION_CATEGORY_ORDER = ['read', 'diagnose', 'write-own', 'wri
10039
10621
  const COMMAND_PERMISSION_BOOLEAN_CONSTRAINTS = [
10040
10622
  'ownPeerOnly',
10041
10623
  'ownAgentOnly',
10624
+ 'currentRelationOnly',
10042
10625
  'privateOnly',
10043
10626
  'groupOnly',
10044
10627
  'requireDaemonOwner',
@@ -10106,6 +10689,7 @@ function commandConstraintLabel(key) {
10106
10689
  const labels = {
10107
10690
  ownPeerOnly: currentLang === 'zh-CN' ? '仅本人 Peer' : 'Own peer only',
10108
10691
  ownAgentOnly: currentLang === 'zh-CN' ? '仅当前 Agent' : 'Own agent only',
10692
+ currentRelationOnly: currentLang === 'zh-CN' ? '仅当前关系' : 'Current relation only',
10109
10693
  privateOnly: currentLang === 'zh-CN' ? '仅私聊' : 'Private only',
10110
10694
  groupOnly: currentLang === 'zh-CN' ? '仅群聊' : 'Group only',
10111
10695
  requireDaemonOwner: currentLang === 'zh-CN' ? '需要 daemon owner' : 'Require daemon owner',