@wendongfly/myhi 1.3.197 → 1.3.198

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
@@ -11,6 +11,7 @@
11
11
  <title>myhi 会话</title>
12
12
  <script src="/lib/machine.js" data-session></script>
13
13
  <script src="/lib/prefs.js"></script>
14
+ <script src="/lib/attn.js"></script>
14
15
  <script src="/sio-client/socket.io.min.js"></script>
15
16
  <script src="/theme.js"></script>
16
17
  <script>
@@ -24,6 +25,9 @@
24
25
  if(em==='1') r.classList.add('embed');
25
26
  // 首页电脑端工作台右侧嵌入:保留本页顶栏,左侧已有任务列表 → 藏掉分屏/切换/返回;
26
27
  // 页面里的链接(如「首页 ⚙ 设置」)在整个窗口打开,别在右侧小框里再套一个首页
28
+ // 手机(触屏且窄屏,平板横屏 >768px 按电脑处理)且未关「移动端精简」→ 精简布局
29
+ var cs={};try{cs=JSON.parse(localStorage.getItem('myhi_chat_settings')||'{}')||{}}catch(e){}
30
+ if(!em&&cs.mobileCompact!==false&&matchMedia('(pointer: coarse) and (max-width: 768px)').matches) r.classList.add('m');
27
31
  if(em==='ws'){ r.classList.add('embed-ws'); var bs=document.createElement('base'); bs.target='_top'; document.head.appendChild(bs); }
28
32
  }catch(e){}})();
29
33
  </script>
@@ -77,6 +81,26 @@
77
81
  .top-btn:hover { background: var(--surface2); color: var(--text); }
78
82
  .top-btn:active { transform: scale(0.92); }
79
83
 
84
+ /* ── 移动端精简(html.m):顶栏只留 ← / ●任务名▾ / ⋯,状态行收进 ⋯ ── */
85
+ #title-row { display: flex; align-items: center; gap: 0.35rem; min-width: 0; }
86
+ #m-dot, #m-back, #m-more, #m-ro { display: none; }
87
+ html.m #m-back, html.m #m-more { display: inline-flex; }
88
+ html.m #m-dot { display: inline-block; }
89
+ html.m #m-ro.show { display: inline-flex; }
90
+ html.m .top-hide-m, html.m #status-bar, html.m #session-cwd { display: none !important; }
91
+ html.m #title-group { cursor: pointer; }
92
+ html.m #session-title::after { content: ' ▾'; color: var(--muted); font-size: 0.7rem; font-weight: 400; }
93
+ html.m #ctx-chip:not(.hi) { display: none !important; }
94
+ html.m #sk-builtin-skills, html.m #sk-custom-skills { display: none !important; }
95
+ html.m #top-bar { transition: margin-top 0.2s ease; }
96
+ #m-more { position: relative; font-size: 1.1rem; letter-spacing: 1px; }
97
+ #m-more.dot::after { content: ''; position: absolute; top: 6px; right: 5px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); }
98
+ #m-ro { font-size: 0.68rem; color: var(--muted); border: 1px solid var(--border); border-radius: 10px; padding: 0.1rem 0.45rem; white-space: nowrap; flex-shrink: 0; cursor: pointer; }
99
+ /* 其他任务完成 / 等你回复的提醒条 */
100
+ #m-toast { position: fixed; left: 0.6rem; right: 0.6rem; top: calc(3.2rem + env(safe-area-inset-top)); z-index: 110; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-left: 4px solid var(--green); border-radius: 10px; padding: 0.6rem 0.8rem; font-size: 0.82rem; box-shadow: 0 6px 20px rgba(0,0,0,0.35); display: none; cursor: pointer; }
101
+ #m-toast.show { display: block; }
102
+ #m-toast .sub { font-size: 0.72rem; color: var(--muted); margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
103
+
80
104
  /* ── 状态栏 ─────────────────────────────── */
81
105
  #status-bar { display: flex; align-items: center; gap: 0.6rem; padding: 0.3rem 0.75rem; background: var(--bg); border-bottom: 1px solid var(--surface2); font-size: 0.72rem; overflow-x: auto; flex-shrink: 0; scrollbar-width: none; }
82
106
  #status-bar::-webkit-scrollbar { display: none; }
@@ -651,21 +675,27 @@
651
675
 
652
676
  <div id="top-bar">
653
677
  <button id="grid-btn" class="top-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)" style="font-size:1.05rem;padding:0 0.35rem;color:var(--blue2)">▦</button>
654
- <div id="title-group">
655
- <div id="session-title">加载中...</div>
678
+ <button id="m-back" class="top-btn" onclick="goHome()" title="首页">
679
+ <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="M15 18l-6-6 6-6"/></svg>
680
+ </button>
681
+ <div id="title-group" onclick="if(document.documentElement.classList.contains('m'))openSwitchSheet()">
682
+ <div id="title-row"><span id="m-dot" class="sb-dot idle"></span><div id="session-title">加载中...</div></div>
656
683
  <div id="session-cwd"></div>
657
684
  </div>
658
- <button class="top-btn" onclick="openChatHelp()" title="使用说明" style="font-size:0.7rem;color:var(--muted)">?</button>
659
- <button class="top-btn" onclick="openSettings()" title="设置">
685
+ <span id="m-ro" onclick="toggleControl()"></span>
686
+ <button class="top-btn top-hide-m" onclick="openChatHelp()" title="使用说明" style="font-size:0.7rem;color:var(--muted)">?</button>
687
+ <button class="top-btn top-hide-m" onclick="openSettings()" title="设置">
660
688
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
661
689
  </button>
662
690
  <button class="top-btn" id="ctx-chip" onclick="openCtxSheet()" title="会话数据:上下文水位 / 成本 / token 明细" style="display:none;width:auto;padding:0 0.5rem;white-space:nowrap;font-size:0.7rem;color:var(--muted)">◈ --</button>
663
- <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
664
- <button class="top-btn" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
665
- <button class="top-btn" onclick="goBack()" title="返回">
691
+ <button class="top-btn top-hide-m" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
692
+ <button class="top-btn top-hide-m" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
693
+ <button class="top-btn top-hide-m" onclick="goBack()" title="返回">
666
694
  <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>
667
695
  </button>
696
+ <button id="m-more" class="top-btn" onclick="openMobileMore()" title="更多">⋯</button>
668
697
  </div>
698
+ <div id="m-toast"></div>
669
699
 
670
700
  <div id="status-bar">
671
701
  <span class="sb-item" id="work-status"><span class="sb-dot idle"></span> 空闲</span>
@@ -770,7 +800,7 @@
770
800
  <button class="sk sk-claude" data-sk="followup" onclick="openFollowUpSheet()" style="color:var(--blue2)">🔄 跟进</button>
771
801
  <button class="sk sk-claude" data-sk="git" onclick="openGitSheet()" style="color:var(--green)">提交</button>
772
802
  <button class="sk sk-claude" data-sk="memory" onclick="showMemory()">记忆</button>
773
- <button class="sk sk-claude" data-sk="skills" onclick="openSkillSheet()" style="color:var(--purple2)">技能</button>
803
+ <button class="sk sk-claude" data-sk="skills" onclick="skillBtnTap()" style="color:var(--purple2)">技能</button>
774
804
  <span id="sk-builtin-skills" data-sk="skills"></span>
775
805
  <span id="sk-custom-skills" data-sk="skills"></span>
776
806
  </div>
@@ -824,6 +854,7 @@
824
854
  <div class="set-line"><span>思考过程</span><div id="seg-think" class="seg-row"><button class="seg" data-v="hide">隐藏</button><button class="seg" data-v="fold">折叠</button><button class="seg" data-v="open">展开</button></div></div>
825
855
  <div class="set-line"><span>工具调用</span><div id="seg-tools" class="seg-row"><button class="seg" data-v="fold">折叠</button><button class="seg" data-v="open">展开</button></div></div>
826
856
  <div class="set-line"><span>工具输出<small>AI 执行命令/读文件的原始结果</small></span><div id="seg-toolout" class="seg-row"><button class="seg" data-v="hide">隐藏</button><button class="seg" data-v="clip">前8行</button><button class="seg" data-v="all">全部</button></div></div>
857
+ <div class="set-line"><span>手机布局<small>仅手机生效:精简只留任务名、状态与切换</small></span><div id="seg-mobile" class="seg-row"><button class="seg" data-v="on">精简</button><button class="seg" data-v="off">完整</button></div></div>
827
858
  <label class="setting-toggle" id="row-parse"><span>把终端里的 Claude 输出整理成聊天样式</span><input type="checkbox" id="set-parse" checked></label>
828
859
  </div>
829
860
  <div class="setting-group" data-stab="chat" style="display:none">
@@ -1167,7 +1198,7 @@
1167
1198
  <div id="cmd-sheet" class="action-sheet">
1168
1199
  <div class="action-sheet-backdrop" onclick="closeCmdSheet()"></div>
1169
1200
  <div class="action-sheet-box" style="max-height:75vh;display:flex;flex-direction:column">
1170
- <div class="action-sheet-title">自定义命令</div>
1201
+ <div class="action-sheet-title" id="cmd-sheet-title">自定义命令</div>
1171
1202
  <div style="padding:0.3rem 0.2rem">
1172
1203
  <input id="cmd-filter" class="slash-inp" placeholder="搜索命令..." autocomplete="off">
1173
1204
  </div>
@@ -2798,6 +2829,7 @@
2798
2829
  };
2799
2830
  const s = STATES[state] || STATES.idle;
2800
2831
  el.innerHTML = `<span class="sb-dot ${s.dot}"></span> ${s.text}`;
2832
+ const md = document.getElementById('m-dot'); if (md) { md.className = 'sb-dot ' + s.dot; md.title = s.text; }
2801
2833
  // 回归 idle 由 result / agent:busy false / session-exit 驱动,
2802
2834
  // 不做定时兜底——长工具调用期间兜底会让取消按钮提前消失。
2803
2835
  if (currentSession?.mode === 'agent') {
@@ -3442,12 +3474,13 @@
3442
3474
  if (tokens > 0) _lastCtxTokens = tokens;
3443
3475
  const chip = document.getElementById('ctx-chip');
3444
3476
  if (!chip) return;
3445
- if (!tokens || tokens <= 0) { chip.style.display = 'none'; return; }
3477
+ if (!tokens || tokens <= 0) { chip.style.display = 'none'; chip.classList.remove('hi'); return; }
3446
3478
  const limit = ctxLimit(tokens);
3447
3479
  const pct = Math.min(100, Math.round(tokens / limit * 100));
3448
3480
  chip.style.display = '';
3449
3481
  chip.textContent = `◈ ${pct}%`;
3450
3482
  chip.style.color = pct >= 90 ? 'var(--red)' : pct >= 75 ? 'var(--yellow)' : 'var(--muted)';
3483
+ chip.classList.toggle('hi', pct >= 75); // 手机精简布局只在偏高时才露出来
3451
3484
  // 压缩快捷按钮同步高亮(agent 栏是 doSlashCmd('/compact'),PTY 栏是 doCompact())
3452
3485
  for (const sel of [
3453
3486
  '#sk-claude-agent button[onclick*="/compact"]',
@@ -3938,6 +3971,9 @@
3938
3971
  document.querySelectorAll('.sk').forEach(b => b.disabled = !canTakeControl());
3939
3972
  }
3940
3973
  updateShortcutBar();
3974
+ // 手机顶栏:没控制权时露出一个小标(只读 / 获取控制 / X 控制中),点了等同控制徽标
3975
+ const ro = document.getElementById('m-ro');
3976
+ if (ro) { ro.textContent = controlling ? '' : badge.textContent; ro.classList.toggle('show', !controlling); }
3941
3977
  }
3942
3978
 
3943
3979
  // ── 设置面板 ──────────────────────────────────
@@ -4575,6 +4611,14 @@
4575
4611
  if (b.dataset.v !== 'hide') settings.thinkCollapsed = b.dataset.v === 'fold';
4576
4612
  saveSettings(); syncDisplaySegs();
4577
4613
  }));
4614
+ const syncMobileSeg = () => document.querySelectorAll('#seg-mobile .seg').forEach(b => b.classList.toggle('active', b.dataset.v === (settings.mobileCompact === false ? 'off' : 'on')));
4615
+ syncMobileSeg();
4616
+ document.querySelectorAll('#seg-mobile .seg').forEach(b => b.addEventListener('click', () => {
4617
+ const v = b.dataset.v !== 'off';
4618
+ if ((settings.mobileCompact !== false) === v) return;
4619
+ settings.mobileCompact = v; saveSettings(); syncMobileSeg();
4620
+ location.reload(); // 布局类在页面头部就定了,切换后重载最干净
4621
+ }));
4578
4622
  document.querySelectorAll('#seg-tools .seg').forEach(b => b.addEventListener('click', () => {
4579
4623
  settings.collapseTools = b.dataset.v === 'fold'; saveSettings(); syncDisplaySegs();
4580
4624
  }));
@@ -4603,6 +4647,104 @@
4603
4647
  window.goHome = goHome;
4604
4648
  window.goBack = function() { goHome(); };
4605
4649
 
4650
+ // ── 手机精简布局:⋯ 更多菜单 / 技能按钮 / 未读标记 / 其他任务提醒 ──────────
4651
+ const IS_M = document.documentElement.classList.contains('m');
4652
+ // 技能按钮:手机上快捷栏不平铺技能,点「技能」直接出可运行列表(底部有「管理技能」)
4653
+ window.skillBtnTap = function() { if (IS_M) openCmdSheet(); else openSkillSheet(); };
4654
+
4655
+ // ⋯ 菜单:状态行(在线/控制、项目说明、升级提示)和顶栏低频按钮都收在这;有待处理提示时 ⋯ 带红点
4656
+ const _shown = (id) => { const e = document.getElementById(id); return e && e.style.display !== 'none' && e.textContent.trim() ? e : null; };
4657
+ function mMoreDot() {
4658
+ const g = _shown('guide-chip');
4659
+ document.getElementById('m-more')?.classList.toggle('dot', !!(_shown('update-badge') || _shown('claude-update-badge') || (g && g.textContent.includes('偏长'))));
4660
+ }
4661
+ if (IS_M) new MutationObserver(mMoreDot).observe(document.getElementById('status-bar'), { subtree: true, childList: true, attributes: true, characterData: true });
4662
+ window.openMobileMore = function() {
4663
+ const items = [];
4664
+ const add = (icon, text, fn, color) => items.push({ icon, text, fn, color });
4665
+ add('📁', '文件', () => openFilePanel());
4666
+ const ctx = _shown('ctx-chip'); if (ctx) add('◈', '会话数据(上下文 ' + ctx.textContent.replace(/[^\d%]/g, '') + ')', () => openCtxSheet());
4667
+ const vc = _shown('viewer-count'), cb = _shown('control-badge');
4668
+ if (vc || cb) add('👥', [vc?.textContent, cb?.textContent].filter(Boolean).join(' · '), () => cb && toggleControl());
4669
+ const g = _shown('guide-chip'); if (g) add('', g.textContent, () => showMemory('project'), g.textContent.includes('偏长') ? 'var(--yellow)' : '');
4670
+ const u = _shown('update-badge'); if (u) add('', 'myhi 有更新:' + u.textContent.replace('⬆', '').trim(), () => doUpdate(), 'var(--yellow)');
4671
+ const cu = _shown('claude-update-badge'); if (cu) add('', 'Claude 有更新:' + cu.textContent.replace('⬆', '').trim(), () => doClaudeUpdate(), 'var(--blue)');
4672
+ add('⚙', '设置', () => openSettings());
4673
+ add('❓', '使用说明', () => openChatHelp());
4674
+ const ov = document.createElement('div');
4675
+ ov.style.cssText = 'position:fixed;inset:0;z-index:120;background:rgba(0,0,0,0.5);display:flex;align-items:flex-end';
4676
+ const esc2 = (t) => { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; };
4677
+ ov.innerHTML = '<div style="background:var(--surface);border-radius:18px 18px 0 0;width:100%;padding:0.4rem 0 max(0.8rem,env(safe-area-inset-bottom))">' +
4678
+ '<div style="padding:0.5rem 1.1rem 0.3rem;font-size:0.72rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(document.getElementById('session-cwd')?.textContent || '') + '</div>' +
4679
+ items.map((it, i) => '<div data-i="' + i + '" style="padding:0.8rem 1.1rem;border-top:1px solid var(--surface2);font-size:0.9rem;cursor:pointer;color:' + (it.color || 'var(--text)') + '">' + (it.icon ? it.icon + '&nbsp; ' : '') + esc2(it.text) + '</div>').join('') +
4680
+ '</div>';
4681
+ ov.onclick = (e) => { const r = e.target.closest('[data-i]'); ov.remove(); if (r) items[+r.dataset.i].fn(); };
4682
+ document.body.appendChild(ov);
4683
+ };
4684
+
4685
+ // 手机:手指往上推(往下看内容)收起顶栏,往下拉再出现;只跟手势走,AI 输出时的自动滚底不触发
4686
+ if (IS_M) {
4687
+ const bar = document.getElementById('top-bar');
4688
+ let y0 = null, hidden = false;
4689
+ const setHidden = (h) => {
4690
+ if (h === hidden) return; hidden = h;
4691
+ bar.style.marginTop = h ? -bar.offsetHeight + 'px' : '';
4692
+ };
4693
+ chatArea.addEventListener('touchstart', (e) => { y0 = e.touches[0].clientY; }, { passive: true });
4694
+ chatArea.addEventListener('touchmove', (e) => {
4695
+ if (y0 == null) return;
4696
+ const dy = e.touches[0].clientY - y0;
4697
+ if (Math.abs(dy) < 12) return;
4698
+ setHidden(dy < 0 && chatArea.scrollTop > 40);
4699
+ y0 = e.touches[0].clientY;
4700
+ }, { passive: true });
4701
+ chatArea.addEventListener('touchend', () => { y0 = null; }, { passive: true });
4702
+ chatArea.addEventListener('scroll', () => { if (chatArea.scrollTop < 10) setHidden(false); }, { passive: true });
4703
+ }
4704
+
4705
+ // 本页在看 = 本任务已读(切换面板 / 首页据此算「新完成」)
4706
+ let _seenAt = 0;
4707
+ function markSeenNow(force) {
4708
+ if (document.hidden || !window.myhiAttn) return;
4709
+ if (!force && Date.now() - _seenAt < 2000) return;
4710
+ _seenAt = Date.now(); myhiAttn.markSeen(SESSION_ID);
4711
+ }
4712
+ markSeenNow(true);
4713
+ document.addEventListener('visibilitychange', () => markSeenNow(true));
4714
+ window.addEventListener('pagehide', () => markSeenNow(true));
4715
+ socket.on('agent:message', () => markSeenNow());
4716
+
4717
+ // 其他任务的提醒:手机上每 8 秒拉一次列表(页面可见时),有任务转为「等你回复」或刚跑完就弹提示条,点了直接切过去
4718
+ let _attnSnap = null, _toastTimer = null;
4719
+ function mToast(s, kind) {
4720
+ const el = document.getElementById('m-toast'); if (!el) return;
4721
+ const esc2 = (t) => { const d = document.createElement('div'); d.textContent = t || ''; return d.innerHTML; };
4722
+ el.style.borderLeftColor = myhiAttn.color[kind] || 'var(--green)';
4723
+ el.innerHTML = '<div>「' + esc2(s.title || '(无标题)') + '」' + (kind === 'wait' ? '在等你回复' : '已完成') + ' <span style="color:var(--muted);font-size:0.72rem">点此切换</span></div>' +
4724
+ (s.lastText ? '<div class="sub">' + esc2(s.lastText) + '</div>' : '');
4725
+ el.onclick = () => { window.location.href = '/terminal/' + s.id; };
4726
+ el.classList.add('show');
4727
+ clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove('show'), 8000);
4728
+ }
4729
+ function attnCheck(list) {
4730
+ if (!IS_M || !window.myhiAttn || !Array.isArray(list)) return;
4731
+ myhiAttn.init(list);
4732
+ const me = list.find(x => x.id === SESSION_ID)?.owner;
4733
+ const snap = {};
4734
+ for (const s of list) {
4735
+ const st = s.pending > 0 ? 'wait' : s.busy ? 'busy' : '';
4736
+ snap[s.id] = st;
4737
+ // 只提醒自己的任务(管理员的列表里有别人的任务,别人的完成不该弹给你)
4738
+ if (!_attnSnap || s.id === SESSION_ID || !(s.id in _attnSnap) || (me && s.owner && s.owner !== me)) continue;
4739
+ const was = _attnSnap[s.id];
4740
+ if (st === 'wait' && was !== 'wait') mToast(s, 'wait');
4741
+ else if (was === 'busy' && st === '') mToast(s, 'done');
4742
+ }
4743
+ _attnSnap = snap;
4744
+ }
4745
+ socket.on('sessions', attnCheck);
4746
+ if (IS_M) setInterval(() => { if (!document.hidden && socket.connected) socket.emit('list'); }, 8000);
4747
+
4606
4748
  // ── 快速切换会话(免退出到首页)──────────────────────────
4607
4749
  function switchRelTime(ms) {
4608
4750
  if (!ms) return '';
@@ -4624,6 +4766,8 @@
4624
4766
  sessions = await res.json();
4625
4767
  } catch { addStatusMessage && addStatusMessage('会话列表加载失败'); return; }
4626
4768
  sessions.sort((a, b) => switchSessTime(b) - switchSessTime(a)); // 最新在前
4769
+ const A = window.myhiAttn; if (A) A.init(sessions);
4770
+ const attnOf = (s) => (A && s.id !== SESSION_ID ? A.of(s) : (s.pending > 0 ? 'wait' : s.busy ? 'busy' : ''));
4627
4771
  const esc2 = (s) => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
4628
4772
  let mode = localStorage.getItem('myhi_group_mode') || 'time'; // 与首页共用记忆
4629
4773
 
@@ -4631,12 +4775,15 @@
4631
4775
  const cur = s.id === SESSION_ID;
4632
4776
  const proj = (s.cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).slice(-1)[0] || '';
4633
4777
  const tags = (s.tags || []).map(x => '<span style="font-size:0.62rem;color:var(--text3);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(' ');
4634
- const sub = [proj, switchRelTime(switchSessTime(s)), s.busy ? '运行中' : '', s.controlHolderName ? (s.controlHolderName + ' 控制中') : ''].filter(Boolean).join(' · ');
4778
+ const at = attnOf(s);
4779
+ const sub = [A && at ? A.label[at] : (s.busy ? '运行中' : ''), proj, switchRelTime(switchSessTime(s)), s.controlHolderName ? (s.controlHolderName + ' 控制中') : ''].filter(Boolean).join(' · ');
4780
+ const dotC = at && A ? A.color[at] : (s.alive ? 'var(--green)' : 'var(--dim)');
4635
4781
  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 var(--surface2);cursor:pointer;' + (cur ? 'background:rgba(124,58,237,0.12)' : '') + '">' +
4636
- '<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + (s.alive ? 'var(--green)' : 'var(--dim)') + '"></span>' +
4782
+ '<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + dotC + (at === 'wait' || at === 'busy' ? ';animation:pulse 1s infinite' : '') + '"></span>' +
4637
4783
  '<div style="flex:1;min-width:0">' +
4638
4784
  '<div style="font-size:0.9rem;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(s.title || '(无标题)') + ' ' + (cur ? '<span style="font-size:0.68rem;color:var(--purple)">· 当前</span>' : '') + '</div>' +
4639
4785
  '<div style="font-size:0.72rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(sub) + '</div>' +
4786
+ (s.lastText ? '<div style="font-size:0.72rem;color:var(--text2, var(--muted));opacity:0.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">🤖 ' + esc2(s.lastText) + '</div>' : '') +
4640
4787
  (tags ? '<div style="display:flex;flex-wrap:wrap;gap:0.25rem;margin-top:0.25rem">' + tags + '</div>' : '') +
4641
4788
  '</div></div>';
4642
4789
  };
@@ -4659,7 +4806,12 @@
4659
4806
  if (untag.length) h += groupHdr('未分类', untag.length) + untag.map(rowHTML).join('');
4660
4807
  return h;
4661
4808
  }
4662
- return sessions.map(rowHTML).join('');
4809
+ // 最近:需要你关注的排前面(等你回复 > 执行中 > 新完成),其余按时间
4810
+ const buckets = { wait: [], busy: [], done: [], '': [] };
4811
+ for (const s of sessions) buckets[attnOf(s)].push(s);
4812
+ if (!buckets.wait.length && !buckets.busy.length && !buckets.done.length) return sessions.map(rowHTML).join('');
4813
+ return ['wait', 'busy', 'done', ''].filter(k => buckets[k].length)
4814
+ .map(k => groupHdr(k ? ({ wait: '🟠 等你回复', busy: '🔵 执行中', done: '🟢 新完成' })[k] : '其他', buckets[k].length) + buckets[k].map(rowHTML).join('')).join('');
4663
4815
  };
4664
4816
 
4665
4817
  const overlay = document.createElement('div');
@@ -6147,19 +6299,23 @@
6147
6299
  filterInp.value = '';
6148
6300
  const render = (keyword = '') => {
6149
6301
  const kw = keyword.toLowerCase();
6150
- // 命令面板只列用户/项目技能(内置技能始终在快捷栏直接显示)
6302
+ // 命令面板只列用户/项目技能(内置技能在快捷栏直接显示);手机精简布局快捷栏不平铺技能 → 内置也列进来
6303
+ const mob = document.documentElement.classList.contains('m');
6304
+ document.getElementById('cmd-sheet-title').textContent = mob ? '技能' : '自定义命令';
6151
6305
  const filtered = _customSkills.filter(s =>
6152
- s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
6153
- listEl.innerHTML = filtered.length
6306
+ (mob || s.source !== 'builtin') && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
6307
+ filtered.sort((a, b) => (a.source === 'builtin') - (b.source === 'builtin'));
6308
+ listEl.innerHTML = (filtered.length
6154
6309
  ? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}', '${s.kind || 'command'}')">
6155
- <div><div style="color:var(--purple3)">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
6310
+ <div><div style="color:${s.source === 'builtin' ? 'var(--teal)' : 'var(--purple3)'}">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
6156
6311
  </div>`).join('')
6157
- : '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>';
6312
+ : '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>')
6313
+ + (mob ? `<div class="action-sheet-item" onclick="closeCmdSheet();openSkillSheet()"><div><div style="color:var(--muted)">⚙ 管理技能(生成 / 编辑 / 分享 / 导入)</div></div></div>` : '');
6158
6314
  };
6159
6315
  render();
6160
6316
  filterInp.oninput = (e) => render(e.target.value);
6161
6317
  sheet.classList.add('open');
6162
- setTimeout(() => filterInp.focus(), 100);
6318
+ if (!document.documentElement.classList.contains('m')) setTimeout(() => filterInp.focus(), 100); // 手机上自动聚焦会弹键盘挡住列表
6163
6319
  };
6164
6320
  window.closeCmdSheet = function() {
6165
6321
  document.getElementById('cmd-sheet').classList.remove('open');
package/dist/index.html CHANGED
@@ -17,6 +17,7 @@
17
17
  <title>myhi</title>
18
18
  <script src="/lib/machine.js"></script>
19
19
  <script src="/lib/prefs.js"></script>
20
+ <script src="/lib/attn.js"></script>
20
21
  <script src="/sio-client/socket.io.min.js"></script>
21
22
  <script src="/theme.js"></script>
22
23
  <script>
@@ -74,8 +75,8 @@
74
75
  justify-content: space-between;
75
76
  gap: 0.75rem;
76
77
  padding: 0 0.75rem;
77
- padding-top: max(1rem, env(safe-area-inset-top));
78
- padding-bottom: 0.75rem;
78
+ padding-top: max(0.45rem, env(safe-area-inset-top));
79
+ padding-bottom: 0.45rem;
79
80
  background: var(--bg);
80
81
  position: sticky;
81
82
  top: 0;
@@ -402,6 +403,10 @@
402
403
  .st-busy::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: stPulse 1.2s ease-in-out infinite; }
403
404
  @keyframes stPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.7); } }
404
405
  .st-dead { color: var(--muted); }
406
+ .st-wait { color: #db6d28; font-weight: 600; }
407
+ .st-wait::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #db6d28; animation: stPulse 0.8s ease-in-out infinite; }
408
+ .st-done { color: var(--green); font-weight: 600; }
409
+ .st-done::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--green); }
405
410
  .viewers {
406
411
  font-size: 0.7rem;
407
412
  color: var(--muted);
@@ -738,8 +743,6 @@
738
743
  <button id="ws-collapse" onclick="wsSetCollapsed(true)" title="收起任务栏(收起后左侧保留窄栏)">«</button>
739
744
  <div class="hd-actions">
740
745
  <button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦<span class="hd-txt"> 分屏</span></button>
741
- <button id="doctor-btn" class="hd-btn" onclick="openDoctor()" title="会话体检">🩺<span class="hd-txt"> 体检</span></button>
742
- <button id="usage-btn" class="hd-btn" onclick="showUsage()" title="用量">📊<span class="hd-txt"> 用量</span></button>
743
746
  <button id="help-btn" class="hd-btn" onclick="openHelpSheet()" title="使用说明 / 快速上手">❓<span class="hd-txt"> 帮助</span></button>
744
747
  <button id="settings-btn" class="hd-btn" onclick="openSettings()" title="设置(外观 / 本机名称 / 语音 / 服务)">⚙<span class="hd-txt"> 设置</span></button>
745
748
  <button id="user-btn" class="hd-btn" onclick="toggleUserMenu(event)" title="账户:修改密码 / 退出登录" style="display:none">👤<span id="user-name" class="hd-txt"></span></button>
@@ -924,6 +927,12 @@
924
927
  <div id="set-asr-status" class="set-status"></div>
925
928
  </div>
926
929
 
930
+ <div class="set-group-t">工具</div>
931
+ <div class="set-card">
932
+ <button class="set-link" onclick="closeSettings();openDoctor()">🩺 会话体检 <small>查中毒 / 过大的会话,一键重开</small></button>
933
+ <button class="set-link" onclick="closeSettings();showUsage()">📊 用量 <small>费用 / token 统计</small></button>
934
+ </div>
935
+
927
936
  <div id="set-svc">
928
937
  <div class="set-group-t" id="set-svc-t">服务</div>
929
938
  <div class="set-card">
@@ -1561,7 +1570,11 @@
1561
1570
  const sub = esc([fmtCwd(s.cwd), fmtRelTime(sessionTime(s)), s.viewers > 0 ? `${s.viewers} 在线` : '', s.controlHolderName ? `${s.controlHolderName} 控制中` : ''].filter(Boolean).join(' · '));
1562
1571
  const tags = (s.tags || []).map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
1563
1572
  // 会话名片:一堆会话都叫 claude,靠第一条真人消息认出「这个任务当初是干啥的」
1564
- const gist = s.firstUserText
1573
+ const at = window.myhiAttn ? myhiAttn.of(s) : '';
1574
+ // 等你回复 / 新完成:比起「当初问了啥」,AI 最后说了啥更有用
1575
+ const gist = (at === 'wait' || at === 'done') && s.lastText
1576
+ ? `<div class="card-gist" style="font-size:0.72rem;color:var(--muted,var(--muted));margin-top:0.18rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:0.85">🤖 ${esc(s.lastText)}</div>`
1577
+ : s.firstUserText
1565
1578
  ? `<div class="card-gist" style="font-size:0.72rem;color:var(--muted,var(--muted));margin-top:0.18rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:0.85">💬 ${esc(s.firstUserText)}</div>`
1566
1579
  : '';
1567
1580
  const nDraft = _draftCounts[s.id] || 0;
@@ -1579,8 +1592,10 @@
1579
1592
  </div>
1580
1593
  <div class="card-right">
1581
1594
  <button class="card-more" title="更多操作(重命名 / 标签 / 终止)">⋯</button>
1582
- ${!s.alive ? '<div class="st st-dead" title="会话进程已退出,点进去会自动恢复">已停止</div>'
1583
- : s.busy ? '<div class="st st-busy" title="AI 正在执行">执行中</div>' : ''}
1595
+ ${at === 'wait' ? '<div class="st st-wait" title="AI 在等你回答提问 / 审批方案">等你回复</div>'
1596
+ : !s.alive ? '<div class="st st-dead" title="会话进程已退出,点进去会自动恢复">已停止</div>'
1597
+ : s.busy ? '<div class="st st-busy" title="AI 正在执行">执行中</div>'
1598
+ : at === 'done' ? '<div class="st st-done" title="你上次看过之后有了新回复">新完成</div>' : ''}
1584
1599
  ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
1585
1600
  </div>
1586
1601
  </div>`;
@@ -1608,8 +1623,11 @@
1608
1623
  </div>`;
1609
1624
  }
1610
1625
 
1626
+ // 手机(触屏窄屏)的「最近」视图按关注度分组;电脑工作台左侧列表保持纯时间序
1627
+ const _attnFirst = () => !!window.myhiAttn && matchMedia('(pointer: coarse) and (max-width: 768px)').matches;
1611
1628
  function renderSessions(sessions) {
1612
1629
  if (Array.isArray(sessions)) _allSessions = sessions;
1630
+ if (window.myhiAttn) myhiAttn.init(_allSessions);
1613
1631
  // 正在原地改名:列表每 5 秒刷新一次会把输入框冲掉,先攒着,改完再刷
1614
1632
  if (_renaming) { _renderPending = true; return; }
1615
1633
  renderTagFilter();
@@ -1670,6 +1688,14 @@
1670
1688
  html += groupHeaderHTML('未分类', untagged.length, 't:__none__');
1671
1689
  if (!isCollapsed('t:__none__') || q) html += untagged.map(cardHTML).join('');
1672
1690
  }
1691
+ } else if (_attnFirst()) {
1692
+ // 手机:需要你关注的排前面(等你回复 > 执行中 > 新完成),其余按时间
1693
+ const b = { wait: [], busy: [], done: [], '': [] };
1694
+ for (const s of items) b[myhiAttn.of(s)].push(s);
1695
+ const lbl = { wait: '🟠 等你回复', busy: '🔵 执行中', done: '🟢 新完成', '': '其他' };
1696
+ const any = b.wait.length || b.busy.length || b.done.length;
1697
+ html = !any ? items.map(cardHTML).join('')
1698
+ : ['wait', 'busy', 'done', ''].filter(k => b[k].length).map(k => groupHeaderHTML(lbl[k], b[k].length) + b[k].map(cardHTML).join('')).join('');
1673
1699
  } else {
1674
1700
  html = items.map(cardHTML).join('');
1675
1701
  }