@wendongfly/myhi 1.3.91 → 1.3.93

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.
package/dist/chat.html CHANGED
@@ -281,6 +281,7 @@
281
281
  </button>
282
282
  <button class="top-btn" onclick="showUsage()" title="用量" style="font-size:0.7rem;color:var(--muted)">用量</button>
283
283
  <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
284
+ <button class="top-btn" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
284
285
  <button class="top-btn" onclick="goBack()" title="返回">
285
286
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
286
287
  </button>
@@ -1122,6 +1123,10 @@
1122
1123
  let _toolGroupCount = 0;
1123
1124
 
1124
1125
  function addToolMessage(raw, toolName) {
1126
+ // 关键:结束当前 assistant 文本流。否则 _streamEl 仍指向工具前那个气泡,
1127
+ // 下一个 LLM call 的流式文本会并进那个(位于工具组之上的)旧气泡,
1128
+ // 造成「文本堆在上面、工具回显落在后面」的错序——重进后按 history 顺序回放才正常。
1129
+ endStream();
1125
1130
  removeThinking();
1126
1131
  const icon = TOOL_ICONS[toolName] || '🔧';
1127
1132
  const collapsed = settings.collapseTools;
@@ -2303,6 +2308,101 @@
2303
2308
  // ── 辅助 ──────────────────────────────────────
2304
2309
  function updateViewers(count) { document.getElementById('viewer-count').textContent = count > 1 ? `${count} 人在线` : ''; }
2305
2310
  window.goBack = function() { window.location.href = '/'; };
2311
+
2312
+ // ── 快速切换会话(免退出到首页)──────────────────────────
2313
+ function switchRelTime(ms) {
2314
+ if (!ms) return '';
2315
+ const diff = Date.now() - ms;
2316
+ if (diff < 60000) return '刚刚';
2317
+ if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
2318
+ if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
2319
+ const d = new Date(ms);
2320
+ if (diff < 172800000) return '昨天';
2321
+ return (d.getMonth() + 1) + '月' + d.getDate() + '日';
2322
+ }
2323
+ function switchSessTime(s) {
2324
+ return (s.lastActivityAt || (s.createdAt ? new Date(s.createdAt).getTime() : 0)) || 0;
2325
+ }
2326
+ window.openSwitchSheet = async function() {
2327
+ let sessions = [];
2328
+ try {
2329
+ const res = await fetch('/api/sessions', { headers: { 'Cache-Control': 'no-cache' } });
2330
+ sessions = await res.json();
2331
+ } catch { addStatusMessage && addStatusMessage('会话列表加载失败'); return; }
2332
+ sessions.sort((a, b) => switchSessTime(b) - switchSessTime(a)); // 最新在前
2333
+ const esc2 = (s) => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
2334
+ let mode = localStorage.getItem('myhi_group_mode') || 'time'; // 与首页共用记忆
2335
+
2336
+ const rowHTML = (s) => {
2337
+ const cur = s.id === SESSION_ID;
2338
+ const proj = (s.cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).slice(-1)[0] || '';
2339
+ const tags = (s.tags || []).map(x => '<span style="font-size:0.62rem;color:#a9b6c2;background:rgba(124,58,237,0.14);border:1px solid rgba(124,58,237,0.35);border-radius:999px;padding:0 0.4rem">' + esc2(x) + '</span>').join(' ');
2340
+ const sub = [proj, switchRelTime(switchSessTime(s)), s.busy ? '运行中' : '', s.controlHolderName ? (s.controlHolderName + ' 控制中') : ''].filter(Boolean).join(' · ');
2341
+ return '<div data-sid="' + esc2(s.id) + '" style="display:flex;align-items:center;gap:0.6rem;padding:0.7rem 1.1rem;border-top:1px solid #21262d;cursor:pointer;' + (cur ? 'background:rgba(124,58,237,0.12)' : '') + '">' +
2342
+ '<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + (s.alive ? '#3fb950' : '#6e7681') + '"></span>' +
2343
+ '<div style="flex:1;min-width:0">' +
2344
+ '<div style="font-size:0.9rem;color:#e6edf3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(s.title || '(无标题)') + ' ' + (cur ? '<span style="font-size:0.68rem;color:#7c3aed">· 当前</span>' : '') + '</div>' +
2345
+ '<div style="font-size:0.72rem;color:#8b949e;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(sub) + '</div>' +
2346
+ (tags ? '<div style="display:flex;flex-wrap:wrap;gap:0.25rem;margin-top:0.25rem">' + tags + '</div>' : '') +
2347
+ '</div></div>';
2348
+ };
2349
+ const groupHdr = (label, n) => '<div style="padding:0.5rem 1.1rem 0.3rem;color:#8b949e;font-size:0.72rem;font-weight:600;background:#0d1117">' + esc2(label) + ' · <span style="font-weight:400">' + n + '</span></div>';
2350
+ const pushInto = (map, key, s) => { if (!map.has(key)) map.set(key, []); map.get(key).push(s); };
2351
+
2352
+ const buildBody = () => {
2353
+ if (!sessions.length) return '<div style="padding:1rem;color:#8b949e;text-align:center">暂无其他会话</div>';
2354
+ if (mode === 'project') {
2355
+ const g = new Map();
2356
+ for (const s of sessions) pushInto(g, (s.cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).slice(-1)[0] || '(默认目录)', s);
2357
+ return [...g.entries()].sort((a, b) => switchSessTime(b[1][0]) - switchSessTime(a[1][0]))
2358
+ .map(([name, arr]) => groupHdr('📁 ' + name, arr.length) + arr.map(rowHTML).join('')).join('');
2359
+ }
2360
+ if (mode === 'tag') {
2361
+ const g = new Map(); const untag = [];
2362
+ for (const s of sessions) { const ts = s.tags || []; if (!ts.length) { untag.push(s); continue; } for (const t of ts) pushInto(g, t, s); }
2363
+ let h = [...g.entries()].sort((a, b) => switchSessTime(b[1][0]) - switchSessTime(a[1][0]))
2364
+ .map(([name, arr]) => groupHdr('🏷 ' + name, arr.length) + arr.map(rowHTML).join('')).join('');
2365
+ if (untag.length) h += groupHdr('未分类', untag.length) + untag.map(rowHTML).join('');
2366
+ return h;
2367
+ }
2368
+ return sessions.map(rowHTML).join('');
2369
+ };
2370
+
2371
+ const overlay = document.createElement('div');
2372
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:120;background:rgba(0,0,0,0.6);display:flex;align-items:flex-end;justify-content:center';
2373
+ const box = document.createElement('div');
2374
+ box.style.cssText = 'background:#161b22;border-radius:18px 18px 0 0;width:100%;max-width:640px;max-height:78vh;display:flex;flex-direction:column;padding-bottom:max(0.8rem,env(safe-area-inset-bottom))';
2375
+ const segBtn = (m, label) => '<button class="sw-seg" data-m="' + m + '" style="background:' + (mode === m ? '#7c3aed' : 'none') + ';color:' + (mode === m ? '#fff' : '#8b949e') + ';border:none;font-size:0.74rem;padding:0.3rem 0.7rem;cursor:pointer;' + (m !== 'time' ? 'border-left:1px solid #30363d' : '') + '">' + label + '</button>';
2376
+ box.innerHTML = '<div style="padding:0.75rem 1.1rem 0.5rem;display:flex;justify-content:space-between;align-items:center;gap:0.5rem">' +
2377
+ '<span style="font-size:0.85rem;font-weight:600;color:#e6edf3">切换会话(' + sessions.length + ')</span>' +
2378
+ '<div style="display:flex;align-items:center;gap:0.5rem">' +
2379
+ '<div style="display:inline-flex;border:1px solid #30363d;border-radius:7px;overflow:hidden">' + segBtn('time', '最近') + segBtn('project', '项目') + segBtn('tag', '标签') + '</div>' +
2380
+ '<button id="sw-home" style="background:none;border:1px solid #30363d;color:#8b949e;font-size:0.72rem;padding:0.25rem 0.6rem;border-radius:6px;cursor:pointer">+ 新建</button>' +
2381
+ '</div>' +
2382
+ '</div>' +
2383
+ '<div id="sw-body" style="overflow:auto">' + buildBody() + '</div>' +
2384
+ '<button id="sw-cancel" style="margin:0.5rem 1.1rem 0;background:none;border:1px solid #30363d;color:#8b949e;font-size:0.85rem;padding:0.7rem;border-radius:10px;cursor:pointer">取消</button>';
2385
+ overlay.appendChild(box);
2386
+ const close = () => overlay.remove();
2387
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
2388
+ box.querySelector('#sw-cancel').onclick = close;
2389
+ box.querySelector('#sw-home').onclick = () => { window.location.href = '/'; };
2390
+ const bindRows = () => box.querySelectorAll('[data-sid]').forEach(row => row.onclick = () => {
2391
+ const sid = row.dataset.sid;
2392
+ if (sid && sid !== SESSION_ID) window.location.href = '/terminal/' + sid;
2393
+ else close();
2394
+ });
2395
+ bindRows();
2396
+ box.querySelectorAll('.sw-seg').forEach(b => b.onclick = () => {
2397
+ mode = b.dataset.m;
2398
+ localStorage.setItem('myhi_group_mode', mode);
2399
+ box.querySelectorAll('.sw-seg').forEach(x => { const on = x.dataset.m === mode; x.style.background = on ? '#7c3aed' : 'none'; x.style.color = on ? '#fff' : '#8b949e'; });
2400
+ box.querySelector('#sw-body').innerHTML = buildBody();
2401
+ bindRows();
2402
+ });
2403
+ document.body.appendChild(overlay);
2404
+ };
2405
+
2306
2406
  function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
2307
2407
  function trimMessages() {
2308
2408
  if (_userScrolledUp) return; // 用户在往上翻历史,禁用自动删
@@ -2323,6 +2423,7 @@
2323
2423
  // 当前主力(Anthropic 最新一代,2026-04 时点)
2324
2424
  { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: '默认推荐 · 速度+智能平衡 · 1M ctx' },
2325
2425
  { id: 'claude-opus-4-7', label: 'Opus 4.7', desc: '最强推理 · agentic coding · 1M ctx' },
2426
+ { id: 'claude-fable-5', label: 'Fable 5', desc: 'Claude 5 家族 · Max plan 标配(消耗较快)' },
2326
2427
  { id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: '最快最便宜 · 200k ctx' },
2327
2428
  // 备选(上一代,仍可用)
2328
2429
  { id: 'claude-opus-4-6', label: 'Opus 4.6', desc: '上一代 Opus(备选)' },
package/dist/index.html CHANGED
@@ -140,6 +140,16 @@
140
140
  overflow: hidden;
141
141
  text-overflow: ellipsis;
142
142
  }
143
+ .tag-chip {
144
+ display: inline-block;
145
+ font-size: 0.66rem;
146
+ color: #a9b6c2;
147
+ background: rgba(124,58,237,0.14);
148
+ border: 1px solid rgba(124,58,237,0.35);
149
+ border-radius: 999px;
150
+ padding: 0.05rem 0.45rem;
151
+ line-height: 1.5;
152
+ }
143
153
 
144
154
  .card-right {
145
155
  display: flex;
@@ -427,6 +437,15 @@
427
437
 
428
438
  <!-- 升级管理已移至 /admin -->
429
439
 
440
+ <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem">
441
+ <div id="group-seg" style="display:inline-flex;align-self:flex-start;background:var(--bg,#0d1117);border:1px solid var(--border,#21262d);border-radius:8px;overflow:hidden">
442
+ <button class="grp-btn" data-group="time" style="background:none;border:none;color:var(--muted,#8b949e);font-size:0.78rem;padding:0.35rem 0.8rem;cursor:pointer">最近</button>
443
+ <button class="grp-btn" data-group="project" style="background:none;border:none;color:var(--muted,#8b949e);font-size:0.78rem;padding:0.35rem 0.8rem;cursor:pointer;border-left:1px solid var(--border,#21262d)">项目</button>
444
+ <button class="grp-btn" data-group="tag" style="background:none;border:none;color:var(--muted,#8b949e);font-size:0.78rem;padding:0.35rem 0.8rem;cursor:pointer;border-left:1px solid var(--border,#21262d)">标签</button>
445
+ </div>
446
+ <div id="tag-filter" style="display:none;flex-wrap:wrap;gap:0.35rem"></div>
447
+ </div>
448
+
430
449
  <div id="list">
431
450
  <div class="empty-state">
432
451
  <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@@ -451,8 +470,8 @@
451
470
  <div class="presets" id="presets"></div>
452
471
 
453
472
  <div class="field">
454
- <label>名称</label>
455
- <input id="inp-title" type="text" placeholder="自定义名称…" autocomplete="off" spellcheck="false">
473
+ <label>名称 <span style="font-weight:400;text-transform:none;color:var(--red,#f85149)">必填</span></label>
474
+ <input id="inp-title" type="text" placeholder="请输入任务名称" autocomplete="off" spellcheck="false">
456
475
  </div>
457
476
  <div class="field">
458
477
  <label>启动命令 <span style="font-weight:400;text-transform:none">(可选)</span></label>
@@ -661,47 +680,171 @@
661
680
  return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
662
681
  }
663
682
 
664
- function renderSessions(sessions) {
665
- if (!sessions.length) {
666
- list.innerHTML = `<div class="empty-state">
667
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
668
- <rect x="2" y="4" width="20" height="16" rx="2"/>
669
- <path d="M8 9l4 3-4 3M13 15h3"/>
670
- </svg>
671
- <p>还没有终端会话</p>
683
+ // 会话的「最后任务时间」:优先最后活动时间,回退创建时间。ms 数或 ISO 皆可。
684
+ function sessionTime(s) {
685
+ const v = s.lastActivityAt || s.createdAt || 0;
686
+ const ms = typeof v === 'number' ? v : new Date(v).getTime();
687
+ return isNaN(ms) ? 0 : ms;
688
+ }
689
+
690
+ // 相对时间:刚刚 / N分钟前 / N小时前 / 昨天 HH:MM / M月D日
691
+ function fmtRelTime(ms) {
692
+ if (!ms) return '';
693
+ const now = Date.now();
694
+ const diff = now - ms;
695
+ if (diff < 60_000) return '刚刚';
696
+ if (diff < 3_600_000) return Math.floor(diff / 60_000) + '分钟前';
697
+ if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + '小时前';
698
+ const d = new Date(ms);
699
+ const hm = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
700
+ if (diff < 172_800_000) return '昨天 ' + hm;
701
+ return (d.getMonth() + 1) + '月' + d.getDate() + '日';
702
+ }
703
+
704
+ // 项目名 = cwd 最后一段
705
+ function projectName(cwd) {
706
+ if (!cwd) return '(默认目录)';
707
+ const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
708
+ return parts[parts.length - 1] || '(根)';
709
+ }
710
+
711
+ // 视图状态:分组方式 + 标签筛选
712
+ let _allSessions = [];
713
+ let _groupMode = localStorage.getItem('myhi_group_mode') || 'time'; // time | project | tag
714
+ let _tagFilter = null; // 仅 tag 模式下:只看某标签
715
+
716
+ const EMPTY_HTML = `<div class="empty-state">
717
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
718
+ <rect x="2" y="4" width="20" height="16" rx="2"/>
719
+ <path d="M8 9l4 3-4 3M13 15h3"/>
720
+ </svg>
721
+ <p>还没有终端会话</p>
722
+ </div>`;
723
+
724
+ function cardHTML(s) {
725
+ const safeTitle = esc(s.title || '');
726
+ const safeId = esc(s.id);
727
+ const color = avatarColor(s.title);
728
+ const letter = esc((s.title || '?')[0].toUpperCase());
729
+ const sub = esc([fmtCwd(s.cwd), fmtRelTime(sessionTime(s)), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
730
+ const tags = (s.tags || []).map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
731
+ return `
732
+ <div class="card" data-id="${safeId}" data-title="${safeTitle}">
733
+ <div class="avatar" style="background:${color}">${letter}</div>
734
+ <div class="card-body">
735
+ <div class="card-title">${safeTitle}</div>
736
+ <div class="card-sub">${sub}</div>
737
+ ${tags ? `<div class="card-tags" style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-top:0.3rem">${tags}</div>` : ''}
738
+ </div>
739
+ <div class="card-right">
740
+ <div class="status-dot ${s.alive ? 'alive' : 'dead'}"></div>
741
+ ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
742
+ </div>
672
743
  </div>`;
673
- return;
674
- }
744
+ }
675
745
 
676
- list.innerHTML = sessions.map(s => {
677
- const safeTitle = esc(s.title || '');
678
- const safeId = esc(s.id);
679
- const color = avatarColor(s.title);
680
- const letter = esc((s.title || '?')[0].toUpperCase());
681
- const sub = esc([fmtCwd(s.cwd), fmtTime(s.createdAt), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
682
- return `
683
- <div class="card" data-id="${safeId}" data-title="${safeTitle}">
684
- <div class="avatar" style="background:${color}">${letter}</div>
685
- <div class="card-body">
686
- <div class="card-title">${safeTitle}</div>
687
- <div class="card-sub">${sub}</div>
688
- </div>
689
- <div class="card-right">
690
- <div class="status-dot ${s.alive ? 'alive' : 'dead'}"></div>
691
- ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
692
- </div>
693
- </div>`;
694
- }).join('');
746
+ function groupHeaderHTML(label, count) {
747
+ return `<div class="group-header" style="display:flex;align-items:center;gap:0.5rem;margin:0.85rem 0.2rem 0.4rem;color:var(--muted,#8b949e);font-size:0.75rem;font-weight:600">
748
+ <span>${esc(label)}</span>
749
+ <span style="color:var(--border,#30363d)">·</span>
750
+ <span style="font-weight:400">${count}</span>
751
+ </div>`;
752
+ }
753
+
754
+ function renderSessions(sessions) {
755
+ if (Array.isArray(sessions)) _allSessions = sessions;
756
+ renderTagFilter();
757
+ updateGroupSeg();
758
+
759
+ const items = [..._allSessions].sort((a, b) => sessionTime(b) - sessionTime(a)); // 最新在前
760
+ if (!items.length) { list.innerHTML = EMPTY_HTML; return; }
761
+
762
+ let html = '';
763
+ if (_groupMode === 'project') {
764
+ const groups = new Map();
765
+ for (const s of items) {
766
+ const k = projectName(s.cwd);
767
+ if (!groups.has(k)) groups.set(k, []);
768
+ groups.get(k).push(s);
769
+ }
770
+ // 组按组内最新会话时间排序
771
+ const ordered = [...groups.entries()].sort((a, b) => sessionTime(b[1][0]) - sessionTime(a[1][0]));
772
+ for (const [name, arr] of ordered) {
773
+ html += groupHeaderHTML('📁 ' + name, arr.length);
774
+ html += arr.map(cardHTML).join('');
775
+ }
776
+ } else if (_groupMode === 'tag') {
777
+ let shown = items;
778
+ if (_tagFilter) shown = items.filter(s => (s.tags || []).includes(_tagFilter));
779
+ const tagged = new Map();
780
+ const untagged = [];
781
+ for (const s of shown) {
782
+ const tags = s.tags || [];
783
+ if (!tags.length) { untagged.push(s); continue; }
784
+ for (const t of tags) {
785
+ if (_tagFilter && t !== _tagFilter) continue;
786
+ if (!tagged.has(t)) tagged.set(t, []);
787
+ tagged.get(t).push(s);
788
+ }
789
+ }
790
+ const ordered = [...tagged.entries()].sort((a, b) => sessionTime(b[1][0]) - sessionTime(a[1][0]));
791
+ for (const [name, arr] of ordered) {
792
+ html += groupHeaderHTML('🏷 ' + name, arr.length);
793
+ html += arr.map(cardHTML).join('');
794
+ }
795
+ if (!_tagFilter && untagged.length) {
796
+ html += groupHeaderHTML('未分类', untagged.length);
797
+ html += untagged.map(cardHTML).join('');
798
+ }
799
+ } else {
800
+ html = items.map(cardHTML).join('');
801
+ }
802
+ list.innerHTML = html;
695
803
 
696
- // 用事件委托替代内联 onclick/oncontextmenu
804
+ // 事件委托:点击打开、右键/长按出菜单
697
805
  list.querySelectorAll('.card').forEach(card => {
698
806
  const id = card.dataset.id;
699
807
  const title = card.dataset.title;
700
808
  card.addEventListener('click', () => openSession(id));
701
- card.addEventListener('contextmenu', (e) => showKill(e, id, title));
809
+ card.addEventListener('contextmenu', (e) => showActions(e, id, title));
810
+ // 移动端长按
811
+ let lpTimer = null;
812
+ card.addEventListener('touchstart', (e) => { lpTimer = setTimeout(() => showActions(e, id, title), 550); }, { passive: true });
813
+ card.addEventListener('touchend', () => clearTimeout(lpTimer));
814
+ card.addEventListener('touchmove', () => clearTimeout(lpTimer));
702
815
  });
703
816
  }
704
817
 
818
+ function renderTagFilter() {
819
+ const bar = document.getElementById('tag-filter');
820
+ if (_groupMode !== 'tag') { bar.style.display = 'none'; return; }
821
+ const allTags = [...new Set(_allSessions.flatMap(s => s.tags || []))].sort();
822
+ if (!allTags.length) { bar.style.display = 'none'; return; }
823
+ bar.style.display = 'flex';
824
+ const mk = (label, val, active) =>
825
+ `<button class="tagf-btn" data-tag="${val === null ? '' : esc(val)}" style="background:${active ? 'var(--accent,#7c3aed)' : 'var(--bg,#0d1117)'};color:${active ? '#fff' : 'var(--muted,#8b949e)'};border:1px solid ${active ? 'var(--accent,#7c3aed)' : 'var(--border,#21262d)'};border-radius:999px;font-size:0.72rem;padding:0.2rem 0.6rem;cursor:pointer">${esc(label)}</button>`;
826
+ bar.innerHTML = mk('全部', null, !_tagFilter) + allTags.map(t => mk(t, t, _tagFilter === t)).join('');
827
+ bar.querySelectorAll('.tagf-btn').forEach(b => b.addEventListener('click', () => {
828
+ _tagFilter = b.dataset.tag || null;
829
+ renderSessions();
830
+ }));
831
+ }
832
+
833
+ function updateGroupSeg() {
834
+ document.querySelectorAll('.grp-btn').forEach(b => {
835
+ const on = b.dataset.group === _groupMode;
836
+ b.style.background = on ? 'var(--accent,#7c3aed)' : 'none';
837
+ b.style.color = on ? '#fff' : 'var(--muted,#8b949e)';
838
+ });
839
+ }
840
+
841
+ document.querySelectorAll('.grp-btn').forEach(b => b.addEventListener('click', () => {
842
+ _groupMode = b.dataset.group;
843
+ _tagFilter = null;
844
+ localStorage.setItem('myhi_group_mode', _groupMode);
845
+ renderSessions();
846
+ }));
847
+
705
848
  // ── Navigation ──────────────────────────────────────────────
706
849
  function openSession(id) {
707
850
  window.location.href = `/terminal/${id}`;
@@ -806,7 +949,7 @@
806
949
  document.querySelectorAll('.chip').forEach((c, j) =>
807
950
  c.classList.toggle('selected', j === i));
808
951
  const p = PRESETS[i];
809
- document.getElementById('inp-title').value = p.title;
952
+ // 不再自动把名称填成 claude/gemini/shell —— 名称必填、留给用户输入任务名
810
953
  document.getElementById('inp-cmd').value = p.cmd || '';
811
954
  }
812
955
 
@@ -830,7 +973,13 @@
830
973
  });
831
974
 
832
975
  function createSession() {
833
- const title = document.getElementById('inp-title').value.trim() || 'shell';
976
+ const titleInput = document.getElementById('inp-title');
977
+ const title = titleInput.value.trim();
978
+ if (!title) {
979
+ alert('请输入任务名称');
980
+ titleInput.focus();
981
+ return;
982
+ }
834
983
  const initCmd = document.getElementById('inp-cmd').value.trim() || undefined;
835
984
  const cwd = document.getElementById('inp-cwd').value.trim() || undefined;
836
985
  if (cwd) recordDir(cwd);
@@ -1040,8 +1189,42 @@
1040
1189
 
1041
1190
  // ── Kill ─────────────────────────────────────────────────────
1042
1191
  let _killId = null;
1043
- function showKill(e, id, title) {
1192
+ // 会话操作菜单:打开 / 重命名 / 设置标签 / 终止
1193
+ function showActions(e, id, title) {
1044
1194
  e.preventDefault();
1195
+ const s = _allSessions.find(x => x.id === id);
1196
+ const curTags = (s?.tags || []).join(', ');
1197
+ const overlay = document.createElement('div');
1198
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:80;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
1199
+ const box = document.createElement('div');
1200
+ box.style.cssText = 'background:var(--surface,#161b22);border-radius:18px 18px 0 0;width:100%;max-width:640px;padding:0.5rem 0 max(1rem,env(safe-area-inset-bottom))';
1201
+ const mkBtn = (label, color) => `<button style="display:block;width:100%;text-align:left;background:none;border:none;border-top:1px solid var(--border,#21262d);color:${color || 'var(--text,#e6edf3)'};font-size:0.95rem;padding:0.9rem 1.25rem;cursor:pointer">${label}</button>`;
1202
+ box.innerHTML =
1203
+ `<div style="padding:0.6rem 1.25rem 0.4rem;color:var(--muted,#8b949e);font-size:0.8rem;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(title)}</div>` +
1204
+ mkBtn('打开') + mkBtn('重命名') + mkBtn('设置标签') + mkBtn('终止会话', 'var(--red,#f85149)') +
1205
+ `<button style="display:block;width:100%;background:none;border:none;border-top:6px solid var(--bg,#0d1117);color:var(--muted,#8b949e);font-size:0.9rem;padding:0.85rem;cursor:pointer">取消</button>`;
1206
+ overlay.appendChild(box);
1207
+ const close = () => overlay.remove();
1208
+ overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); });
1209
+ const btns = box.querySelectorAll('button');
1210
+ btns[0].onclick = () => { close(); openSession(id); };
1211
+ btns[1].onclick = () => {
1212
+ close();
1213
+ const nv = prompt('重命名会话', title);
1214
+ if (nv && nv.trim()) socket.emit('rename', { sessionId: id, title: nv.trim() });
1215
+ };
1216
+ btns[2].onclick = () => {
1217
+ close();
1218
+ const nv = prompt('设置标签(多个用逗号分隔,留空清除)', curTags);
1219
+ if (nv !== null) socket.emit('set-tags', { sessionId: id, tags: nv.split(/[,,]/).map(t => t.trim()).filter(Boolean) });
1220
+ };
1221
+ btns[3].onclick = () => { close(); showKill(e, id, title); };
1222
+ btns[4].onclick = close;
1223
+ document.body.appendChild(overlay);
1224
+ }
1225
+
1226
+ function showKill(e, id, title) {
1227
+ if (e && e.preventDefault) e.preventDefault();
1045
1228
  _killId = id;
1046
1229
  document.getElementById('kill-title').textContent = title;
1047
1230
  document.getElementById('kill-menu').classList.add('open');