@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 +101 -0
- package/dist/index.html +219 -36
- package/dist/index.js +1 -1
- package/dist/index.min.js +121 -121
- package/package.json +1 -1
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
|
|
455
|
-
<input id="inp-title" type="text" placeholder="
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
674
|
-
}
|
|
744
|
+
}
|
|
675
745
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
//
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|