@wendongfly/myhi 1.3.192 → 1.3.193

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/index.html CHANGED
@@ -249,6 +249,36 @@
249
249
  /* 窄栏悬停提示(原生 title 有延迟,这里即时显示) */
250
250
  #ws-tip { position: fixed; z-index: 60; left: 62px; background: var(--text); color: var(--bg); font-size: 0.75rem; padding: 0.3rem 0.55rem; border-radius: 6px;
251
251
  white-space: nowrap; pointer-events: none; display: none; max-width: 320px; overflow: hidden; text-overflow: ellipsis; }
252
+ /* 任务操作:⋯ 按钮(电脑上悬停才出现)+ 小菜单 / 原地改名 / 标签编辑 */
253
+ .card-more { display: none; background: none; border: none; color: var(--muted); font-size: 1.05rem; line-height: 1; padding: 0.15rem 0.35rem; border-radius: 6px; cursor: pointer; }
254
+ .card-more:hover, .card-more.on { background: var(--border); color: var(--text); }
255
+ @media (hover: hover) and (pointer: fine) { .card:hover .card-more, .card-more.on { display: block; } }
256
+ #card-menu:not(.sheet-mode), #tag-pop:not(.sheet-mode) { position: fixed; z-index: 85; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.35); padding: 0.3rem; }
257
+ #card-menu:not(.sheet-mode) { min-width: 10.5rem; }
258
+ #card-menu button { display: block; width: 100%; text-align: left; background: none; border: none; color: var(--text); font-size: 0.84rem; padding: 0.5rem 0.65rem; border-radius: 7px; cursor: pointer; }
259
+ #card-menu button:hover { background: var(--bg); }
260
+ #card-menu button.danger { color: var(--red); }
261
+ #card-menu.sheet-mode, #tag-pop.sheet-mode { position: fixed; inset: auto 0 0 0; z-index: 85; }
262
+ #card-menu.sheet-mode { inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: flex-end; justify-content: center; }
263
+ #card-menu .cm-box { background: var(--surface); border-radius: 18px 18px 0 0; width: 100%; max-width: 640px; padding: 0.5rem 0 max(1rem, env(safe-area-inset-bottom)); }
264
+ #card-menu .cm-title { padding: 0.6rem 1.25rem 0.4rem; color: var(--muted); font-size: 0.8rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
265
+ #card-menu.sheet-mode button { font-size: 0.95rem; padding: 0.9rem 1.25rem; border-radius: 0; border-top: 1px solid var(--border); }
266
+ #card-menu .cm-cancel { color: var(--muted) !important; text-align: center !important; border-top: 6px solid var(--bg) !important; }
267
+ .rename-inp { width: 100%; font-size: 0.9rem; font-weight: 600; padding: 0.15rem 0.35rem; border: 1px solid var(--accent); border-radius: 6px; background: var(--bg); color: var(--text); outline: none; }
268
+ #tag-pop:not(.sheet-mode) { width: 17rem; padding: 0.7rem; }
269
+ #tag-pop.sheet-mode { background: var(--surface); border-radius: 18px 18px 0 0; padding: 1rem 1rem max(1rem, env(safe-area-inset-bottom)); box-shadow: 0 -8px 30px rgba(0,0,0,0.35); max-width: 640px; margin: 0 auto; }
270
+ #tag-pop .tp-title { font-size: 0.8rem; font-weight: 600; color: var(--text2); margin-bottom: 0.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
271
+ #tag-pop .tp-cur, #tag-pop .tp-all { display: flex; flex-wrap: wrap; gap: 0.3rem; }
272
+ #tag-pop .tp-chip { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.75rem; background: rgba(124,58,237,0.14); border: 1px solid rgba(124,58,237,0.35); border-radius: 999px; padding: 0.1rem 0.25rem 0.1rem 0.55rem; }
273
+ #tag-pop .tp-chip b { cursor: pointer; color: var(--muted); font-weight: 400; padding: 0 0.2rem; }
274
+ #tag-pop .tp-chip b:hover { color: var(--red); }
275
+ #tag-pop .tp-none { font-size: 0.74rem; color: var(--muted); }
276
+ #tag-pop .tp-inp { width: 100%; margin: 0.6rem 0 0.2rem; font-size: 0.82rem; padding: 0.4rem 0.55rem; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); outline: none; }
277
+ #tag-pop .tp-inp:focus { border-color: var(--accent); }
278
+ #tag-pop .tp-sub { font-size: 0.7rem; color: var(--muted); margin: 0.45rem 0 0.3rem; }
279
+ #tag-pop .tp-sug { font-size: 0.74rem; color: var(--text2); border: 1px dashed var(--border); border-radius: 999px; padding: 0.1rem 0.5rem; cursor: pointer; }
280
+ #tag-pop .tp-sug:hover { border-color: var(--accent); color: var(--accent); }
281
+ #tag-pop .tp-done { display: block; margin: 0.65rem 0 0 auto; background: var(--accent); color: #fff; border: none; border-radius: 7px; padding: 0.35rem 0.9rem; font-size: 0.78rem; cursor: pointer; }
252
282
  /* 当前使用模式(单用户 / 多用户·管理员 / 多用户·成员),让成员知道为什么有些项是只读 */
253
283
  .set-mode { font-size: 0.72rem; color: var(--muted); margin: 0.35rem 0.25rem 0; }
254
284
  .set-mode a { color: var(--accent); text-decoration: none; }
@@ -809,6 +839,7 @@
809
839
  <div class="hs-tile"><span>🛡</span><div><b>护栏 / 密钥</b><p>敏感信息不外泄</p></div></div>
810
840
  <div class="hs-tile"><span>⚙️</span><div><b>会话设置</b><p>快捷栏、折叠、主题字号</p></div></div>
811
841
  </div>
842
+ <div class="hs-tip" style="margin-bottom:0.45rem">💡 进入任务后点顶栏 <kbd>?</kbd> 可看每个按钮是做什么的(模型、计划、压缩、清除…);电脑上鼠标停在按钮上也有提示。</div>
812
843
  <div class="hs-tip">📱 手机浏览器打开同一地址即可(局域网 / Tailscale),建议「添加到主屏幕」当 App 用。</div>
813
844
  <div class="hs-actions">
814
845
  <button class="hs-ghost" onclick="closeHelpSheet()">我知道了</button>
@@ -1477,6 +1508,7 @@
1477
1508
 
1478
1509
  // 视图状态:分组方式 + 标签筛选
1479
1510
  let _allSessions = [];
1511
+ let _renaming = null, _renderPending = false; // 原地改名中的任务 id / 期间被推迟的刷新
1480
1512
  // 电脑端工作台状态(声明放这里:列表渲染会用到,必须先于任何一次渲染)
1481
1513
  let _wsCur = null; // 右侧当前打开的任务
1482
1514
  let _wsPending = new URLSearchParams(location.search).get('s'); // 刷新后回到上次的任务
@@ -1539,6 +1571,7 @@
1539
1571
  ${draftChip}
1540
1572
  </div>
1541
1573
  <div class="card-right">
1574
+ <button class="card-more" title="更多操作(重命名 / 标签 / 终止)">⋯</button>
1542
1575
  ${!s.alive ? '<div class="st st-dead" title="会话进程已退出,点进去会自动恢复">已停止</div>'
1543
1576
  : s.busy ? '<div class="st st-busy" title="AI 正在执行">执行中</div>' : ''}
1544
1577
  ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
@@ -1570,6 +1603,8 @@
1570
1603
 
1571
1604
  function renderSessions(sessions) {
1572
1605
  if (Array.isArray(sessions)) _allSessions = sessions;
1606
+ // 正在原地改名:列表每 5 秒刷新一次会把输入框冲掉,先攒着,改完再刷
1607
+ if (_renaming) { _renderPending = true; return; }
1573
1608
  renderTagFilter();
1574
1609
  updateGroupSeg();
1575
1610
 
@@ -1648,8 +1683,9 @@
1648
1683
  list.querySelectorAll('.card').forEach(card => {
1649
1684
  const id = card.dataset.id;
1650
1685
  const title = card.dataset.title;
1651
- card.addEventListener('click', () => openSession(id));
1686
+ card.addEventListener('click', (e) => { if (!e.target.closest('.card-more, .rename-inp')) openSession(id); });
1652
1687
  card.addEventListener('contextmenu', (e) => showActions(e, id, title));
1688
+ card.querySelector('.card-more')?.addEventListener('click', (e) => { e.stopPropagation(); showActions(e, id, title, e.currentTarget); });
1653
1689
  // 移动端长按
1654
1690
  let lpTimer = null;
1655
1691
  card.addEventListener('touchstart', (e) => { lpTimer = setTimeout(() => showActions(e, id, title), 550); }, { passive: true });
@@ -2305,38 +2341,131 @@
2305
2341
  // ── Kill ─────────────────────────────────────────────────────
2306
2342
  let _killId = null;
2307
2343
  // 会话操作菜单:打开 / 重命名 / 设置标签 / 终止
2308
- function showActions(e, id, title) {
2344
+ // 任务操作菜单:电脑上(细指针)在 ⋯ 按钮或鼠标旁弹小菜单;手机上长按弹底部菜单。
2345
+ // 原来电脑右键也弹手机式底部菜单(离鼠标很远、整屏变暗),重命名 / 标签用浏览器自带 prompt。
2346
+ function showActions(e, id, title, anchor) {
2309
2347
  e.preventDefault();
2310
- const s = _allSessions.find(x => x.id === id);
2311
- const curTags = (s?.tags || []).join(', ');
2348
+ closeCardMenu();
2349
+ const items = [
2350
+ ['✏️ 重命名', () => inlineRename(id)],
2351
+ ['🏷 设置标签', () => openTagEditor(id, anchor || { x: e.clientX, y: e.clientY })],
2352
+ ['↗ 在新窗口打开', () => window.open('/terminal/' + encodeURIComponent(id), '_blank')],
2353
+ ['🗑 终止会话', () => showKill(e, id, title), true],
2354
+ ];
2355
+ if (matchMedia('(pointer: fine)').matches) {
2356
+ const m = document.createElement('div');
2357
+ m.id = 'card-menu';
2358
+ m.innerHTML = items.map(([l, , danger], i) => `<button data-i="${i}" class="${danger ? 'danger' : ''}">${l}</button>`).join('');
2359
+ document.body.appendChild(m);
2360
+ m.querySelectorAll('button').forEach(b => b.onclick = (ev) => { ev.stopPropagation(); closeCardMenu(); items[b.dataset.i][1](); });
2361
+ placePopover(m, anchor || { x: e.clientX, y: e.clientY });
2362
+ if (anchor) anchor.classList.add('on');
2363
+ m._anchor = anchor;
2364
+ return;
2365
+ }
2366
+ // 手机:底部菜单
2312
2367
  const overlay = document.createElement('div');
2313
- 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';
2314
- const box = document.createElement('div');
2315
- box.style.cssText = 'background:var(--surface,var(--surface));border-radius:18px 18px 0 0;width:100%;max-width:640px;padding:0.5rem 0 max(1rem,env(safe-area-inset-bottom))';
2316
- const mkBtn = (label, color) => `<button style="display:block;width:100%;text-align:left;background:none;border:none;border-top:1px solid var(--border,var(--border));color:${color || 'var(--text,var(--text))'};font-size:0.95rem;padding:0.9rem 1.25rem;cursor:pointer">${label}</button>`;
2317
- box.innerHTML =
2318
- `<div style="padding:0.6rem 1.25rem 0.4rem;color:var(--muted,var(--muted));font-size:0.8rem;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(title)}</div>` +
2319
- mkBtn('打开') + mkBtn('重命名') + mkBtn('设置标签') + mkBtn('终止会话', 'var(--red,var(--red))') +
2320
- `<button style="display:block;width:100%;background:none;border:none;border-top:6px solid var(--bg,var(--bg));color:var(--muted,var(--muted));font-size:0.9rem;padding:0.85rem;cursor:pointer">取消</button>`;
2321
- overlay.appendChild(box);
2322
- const close = () => overlay.remove();
2323
- overlay.addEventListener('click', (ev) => { if (ev.target === overlay) close(); });
2324
- const btns = box.querySelectorAll('button');
2325
- btns[0].onclick = () => { close(); openSession(id); };
2326
- btns[1].onclick = () => {
2327
- close();
2328
- const nv = prompt('重命名会话', title);
2329
- if (nv && nv.trim()) socket.emit('rename', { sessionId: id, title: nv.trim() });
2368
+ overlay.id = 'card-menu';
2369
+ overlay.className = 'sheet-mode';
2370
+ overlay.innerHTML = `<div class="cm-box"><div class="cm-title">${esc(title)}</div>` +
2371
+ items.map(([l, , danger], i) => `<button data-i="${i}" class="${danger ? 'danger' : ''}">${l}</button>`).join('') +
2372
+ `<button class="cm-cancel">取消</button></div>`;
2373
+ document.body.appendChild(overlay);
2374
+ overlay.addEventListener('click', (ev) => { if (ev.target === overlay) closeCardMenu(); });
2375
+ overlay.querySelector('.cm-cancel').onclick = closeCardMenu;
2376
+ overlay.querySelectorAll('button[data-i]').forEach(b => b.onclick = () => { closeCardMenu(); items[b.dataset.i][1](); });
2377
+ }
2378
+ function closeCardMenu() {
2379
+ const m = document.getElementById('card-menu');
2380
+ if (m) { m._anchor?.classList.remove('on'); m.remove(); }
2381
+ }
2382
+ // 弹层贴着锚点(按钮或鼠标位置)放,超出视口就往回收
2383
+ function placePopover(el, anchor) {
2384
+ const r = anchor.getBoundingClientRect ? anchor.getBoundingClientRect() : { left: anchor.x, right: anchor.x, top: anchor.y, bottom: anchor.y };
2385
+ const w = el.offsetWidth, h = el.offsetHeight;
2386
+ let x = r.left, y = r.bottom + 4;
2387
+ if (x + w > innerWidth - 8) x = Math.max(8, r.right - w);
2388
+ if (y + h > innerHeight - 8) y = Math.max(8, r.top - h - 4);
2389
+ el.style.left = x + 'px'; el.style.top = y + 'px';
2390
+ }
2391
+ document.addEventListener('mousedown', (e) => {
2392
+ if (!e.target.closest('#card-menu') && !e.target.closest('.card-more')) closeCardMenu();
2393
+ if (!e.target.closest('#tag-pop') && !e.target.closest('#card-menu')) closeTagEditor();
2394
+ });
2395
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeCardMenu(); closeTagEditor(); } });
2396
+
2397
+ // 原地改名:标题变输入框,回车 / 失焦保存,Esc 取消
2398
+ function inlineRename(id) {
2399
+ const card = document.querySelector(`#list .card[data-id="${CSS.escape(id)}"]`);
2400
+ const t = card?.querySelector('.card-title');
2401
+ if (!t) return;
2402
+ const old = _allSessions.find(x => x.id === id)?.title || t.textContent;
2403
+ _renaming = id;
2404
+ const inp = document.createElement('input');
2405
+ inp.className = 'rename-inp';
2406
+ inp.value = old;
2407
+ inp.maxLength = 80;
2408
+ t.replaceWith(inp);
2409
+ inp.focus(); inp.select();
2410
+ let done = false;
2411
+ const finish = (save) => {
2412
+ if (done) return; done = true;
2413
+ const v = inp.value.trim();
2414
+ if (save && v && v !== old) {
2415
+ socket.emit('rename', { sessionId: id, title: v });
2416
+ const s = _allSessions.find(x => x.id === id); if (s) s.title = v;
2417
+ }
2418
+ _renaming = null;
2419
+ _renderPending = false;
2420
+ renderSessions();
2330
2421
  };
2331
- btns[2].onclick = () => {
2332
- close();
2333
- const nv = prompt('设置标签(多个用逗号分隔,留空清除)', curTags);
2334
- if (nv !== null) socket.emit('set-tags', { sessionId: id, tags: nv.split(/[,,]/).map(t => t.trim()).filter(Boolean) });
2422
+ inp.addEventListener('keydown', (ev) => {
2423
+ if (ev.key === 'Enter' && !ev.isComposing) { ev.preventDefault(); finish(true); }
2424
+ else if (ev.key === 'Escape') { ev.stopPropagation(); finish(false); }
2425
+ });
2426
+ inp.addEventListener('blur', () => finish(true));
2427
+ inp.addEventListener('click', (ev) => ev.stopPropagation());
2428
+ }
2429
+
2430
+ // 标签编辑:当前标签(点 × 去掉)+ 其他任务用过的标签(点一下加上)+ 输入新标签回车;每次改动立即保存
2431
+ function openTagEditor(id, anchor) {
2432
+ closeTagEditor();
2433
+ const s = _allSessions.find(x => x.id === id);
2434
+ if (!s) return;
2435
+ const pop = document.createElement('div');
2436
+ pop.id = 'tag-pop';
2437
+ if (!matchMedia('(pointer: fine)').matches) pop.classList.add('sheet-mode');
2438
+ document.body.appendChild(pop);
2439
+ const save = (tags) => {
2440
+ s.tags = tags;
2441
+ socket.emit('set-tags', { sessionId: id, tags });
2442
+ draw();
2335
2443
  };
2336
- btns[3].onclick = () => { close(); showKill(e, id, title); };
2337
- btns[4].onclick = close;
2338
- document.body.appendChild(overlay);
2444
+ function draw() {
2445
+ const cur = s.tags || [];
2446
+ const all = [...new Set(_allSessions.flatMap(x => x.tags || []))].filter(t => !cur.includes(t)).sort((a, b) => a.localeCompare(b, 'zh'));
2447
+ pop.innerHTML = `<div class="tp-title">🏷 「${esc(s.title || '')}」的标签</div>` +
2448
+ `<div class="tp-cur">${cur.length ? cur.map(t => `<span class="tp-chip">${esc(t)}<b data-del="${esc(t)}">×</b></span>`).join('') : '<span class="tp-none">还没有标签</span>'}</div>` +
2449
+ `<input class="tp-inp" placeholder="输入新标签,回车添加" maxlength="20">` +
2450
+ (all.length ? `<div class="tp-sub">点一下添加:</div><div class="tp-all">${all.map(t => `<span class="tp-sug" data-add="${esc(t)}">+ ${esc(t)}</span>`).join('')}</div>` : '') +
2451
+ `<button class="tp-done">完成</button>`;
2452
+ pop.querySelectorAll('[data-del]').forEach(b => b.onclick = () => save(cur.filter(t => t !== b.dataset.del)));
2453
+ pop.querySelectorAll('[data-add]').forEach(b => b.onclick = () => save([...cur, b.dataset.add]));
2454
+ const inp = pop.querySelector('.tp-inp');
2455
+ inp.addEventListener('keydown', (ev) => {
2456
+ if (ev.key === 'Enter' && !ev.isComposing) {
2457
+ ev.preventDefault();
2458
+ const v = inp.value.trim().replace(/[,,]/g, '');
2459
+ if (v && !cur.includes(v)) save([...cur, v]); else inp.value = '';
2460
+ }
2461
+ });
2462
+ pop.querySelector('.tp-done').onclick = closeTagEditor;
2463
+ if (!pop.classList.contains('sheet-mode')) placePopover(pop, anchor);
2464
+ setTimeout(() => pop.querySelector('.tp-inp')?.focus(), 0);
2465
+ }
2466
+ draw();
2339
2467
  }
2468
+ function closeTagEditor() { document.getElementById('tag-pop')?.remove(); }
2340
2469
 
2341
2470
  function showKill(e, id, title) {
2342
2471
  if (e && e.preventDefault) e.preventDefault();