@wendongfly/myhi 1.3.196 → 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 +189 -20
- package/dist/index.html +33 -7
- package/dist/index.js +1 -1
- package/dist/index.min.js +133 -133
- package/dist/lib/attn.js +46 -0
- package/package.json +1 -1
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
|
-
<
|
|
655
|
-
<
|
|
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
|
-
<
|
|
659
|
-
<button class="top-btn" onclick="
|
|
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="
|
|
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') {
|
|
@@ -2906,7 +2938,15 @@
|
|
|
2906
2938
|
}
|
|
2907
2939
|
|
|
2908
2940
|
// ── Socket.IO ──────────────────────────────────
|
|
2941
|
+
// 本页所在位置:随握手上报,服务端在「清除」提示/日志里标明是谁、从哪儿点的
|
|
2942
|
+
const CLIENT_WHERE = (() => {
|
|
2943
|
+
const em = new URLSearchParams(location.search).get('embed');
|
|
2944
|
+
if (em === '1') return 'grid';
|
|
2945
|
+
if (em === 'ws') return 'workbench';
|
|
2946
|
+
return matchMedia('(pointer: coarse)').matches ? 'mobile' : 'desktop';
|
|
2947
|
+
})();
|
|
2909
2948
|
const socket = io({
|
|
2949
|
+
query: { where: CLIENT_WHERE },
|
|
2910
2950
|
transports: ['websocket', 'polling'],
|
|
2911
2951
|
reconnection: true,
|
|
2912
2952
|
reconnectionAttempts: Infinity,
|
|
@@ -3043,6 +3083,11 @@
|
|
|
3043
3083
|
document.querySelectorAll(`[data-inject-tag="${CSS.escape(msg.id || '')}"]`).forEach(t => { t.outerHTML = injectTagHtml({ id: msg.id, state: msg.state, reason: msg.reason }); });
|
|
3044
3084
|
if (msg.state === 'dropped' && msg.reason) addStatusMessage('⚡ ' + msg.reason);
|
|
3045
3085
|
} else if (msg.subtype === 'info' && msg.message) {
|
|
3086
|
+
// 别的页面清除了上下文:本页聊天区还留着旧记录,划条线说明上面的 AI 已经不记得了
|
|
3087
|
+
if (msg.cleared && chatArea.querySelector('.msg-input, .msg-assistant, .msg-tool')) {
|
|
3088
|
+
addStatusMessage('⬆ 以上为清除前的旧对话,AI 已不再记得');
|
|
3089
|
+
_ctxNudgeLevel = 0; _lastCtxTokens = 0; ctxUpdate(0);
|
|
3090
|
+
}
|
|
3046
3091
|
addStatusMessage(msg.message);
|
|
3047
3092
|
// 同步计划模式状态到按钮
|
|
3048
3093
|
if (msg.message.includes('进入计划模式')) {
|
|
@@ -3429,12 +3474,13 @@
|
|
|
3429
3474
|
if (tokens > 0) _lastCtxTokens = tokens;
|
|
3430
3475
|
const chip = document.getElementById('ctx-chip');
|
|
3431
3476
|
if (!chip) return;
|
|
3432
|
-
if (!tokens || tokens <= 0) { chip.style.display = 'none'; return; }
|
|
3477
|
+
if (!tokens || tokens <= 0) { chip.style.display = 'none'; chip.classList.remove('hi'); return; }
|
|
3433
3478
|
const limit = ctxLimit(tokens);
|
|
3434
3479
|
const pct = Math.min(100, Math.round(tokens / limit * 100));
|
|
3435
3480
|
chip.style.display = '';
|
|
3436
3481
|
chip.textContent = `◈ ${pct}%`;
|
|
3437
3482
|
chip.style.color = pct >= 90 ? 'var(--red)' : pct >= 75 ? 'var(--yellow)' : 'var(--muted)';
|
|
3483
|
+
chip.classList.toggle('hi', pct >= 75); // 手机精简布局只在偏高时才露出来
|
|
3438
3484
|
// 压缩快捷按钮同步高亮(agent 栏是 doSlashCmd('/compact'),PTY 栏是 doCompact())
|
|
3439
3485
|
for (const sel of [
|
|
3440
3486
|
'#sk-claude-agent button[onclick*="/compact"]',
|
|
@@ -3925,6 +3971,9 @@
|
|
|
3925
3971
|
document.querySelectorAll('.sk').forEach(b => b.disabled = !canTakeControl());
|
|
3926
3972
|
}
|
|
3927
3973
|
updateShortcutBar();
|
|
3974
|
+
// 手机顶栏:没控制权时露出一个小标(只读 / 获取控制 / X 控制中),点了等同控制徽标
|
|
3975
|
+
const ro = document.getElementById('m-ro');
|
|
3976
|
+
if (ro) { ro.textContent = controlling ? '' : badge.textContent; ro.classList.toggle('show', !controlling); }
|
|
3928
3977
|
}
|
|
3929
3978
|
|
|
3930
3979
|
// ── 设置面板 ──────────────────────────────────
|
|
@@ -4562,6 +4611,14 @@
|
|
|
4562
4611
|
if (b.dataset.v !== 'hide') settings.thinkCollapsed = b.dataset.v === 'fold';
|
|
4563
4612
|
saveSettings(); syncDisplaySegs();
|
|
4564
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
|
+
}));
|
|
4565
4622
|
document.querySelectorAll('#seg-tools .seg').forEach(b => b.addEventListener('click', () => {
|
|
4566
4623
|
settings.collapseTools = b.dataset.v === 'fold'; saveSettings(); syncDisplaySegs();
|
|
4567
4624
|
}));
|
|
@@ -4590,6 +4647,104 @@
|
|
|
4590
4647
|
window.goHome = goHome;
|
|
4591
4648
|
window.goBack = function() { goHome(); };
|
|
4592
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 + ' ' : '') + 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
|
+
|
|
4593
4748
|
// ── 快速切换会话(免退出到首页)──────────────────────────
|
|
4594
4749
|
function switchRelTime(ms) {
|
|
4595
4750
|
if (!ms) return '';
|
|
@@ -4611,6 +4766,8 @@
|
|
|
4611
4766
|
sessions = await res.json();
|
|
4612
4767
|
} catch { addStatusMessage && addStatusMessage('会话列表加载失败'); return; }
|
|
4613
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' : ''));
|
|
4614
4771
|
const esc2 = (s) => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
|
|
4615
4772
|
let mode = localStorage.getItem('myhi_group_mode') || 'time'; // 与首页共用记忆
|
|
4616
4773
|
|
|
@@ -4618,12 +4775,15 @@
|
|
|
4618
4775
|
const cur = s.id === SESSION_ID;
|
|
4619
4776
|
const proj = (s.cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).slice(-1)[0] || '';
|
|
4620
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(' ');
|
|
4621
|
-
const
|
|
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)');
|
|
4622
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)' : '') + '">' +
|
|
4623
|
-
'<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + (
|
|
4782
|
+
'<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + dotC + (at === 'wait' || at === 'busy' ? ';animation:pulse 1s infinite' : '') + '"></span>' +
|
|
4624
4783
|
'<div style="flex:1;min-width:0">' +
|
|
4625
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>' +
|
|
4626
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>' : '') +
|
|
4627
4787
|
(tags ? '<div style="display:flex;flex-wrap:wrap;gap:0.25rem;margin-top:0.25rem">' + tags + '</div>' : '') +
|
|
4628
4788
|
'</div></div>';
|
|
4629
4789
|
};
|
|
@@ -4646,7 +4806,12 @@
|
|
|
4646
4806
|
if (untag.length) h += groupHdr('未分类', untag.length) + untag.map(rowHTML).join('');
|
|
4647
4807
|
return h;
|
|
4648
4808
|
}
|
|
4649
|
-
|
|
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('');
|
|
4650
4815
|
};
|
|
4651
4816
|
|
|
4652
4817
|
const overlay = document.createElement('div');
|
|
@@ -5130,7 +5295,7 @@
|
|
|
5130
5295
|
// Agent 模式:发送 /clear 清除 Claude 上下文
|
|
5131
5296
|
if (currentSession?.mode === 'agent') {
|
|
5132
5297
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
5133
|
-
socket.emit('agent:query', { prompt: '/clear' }); //
|
|
5298
|
+
socket.emit('agent:query', { prompt: '/clear', via: 'button' }); // 服务端回一条「已开始新对话」(带操作人/来源),所有在看的人都能看到
|
|
5134
5299
|
_ctxNudgeLevel = 0;
|
|
5135
5300
|
_lastCtxTokens = 0;
|
|
5136
5301
|
ctxUpdate(0); // 上下文已清空,水位指示复位
|
|
@@ -6134,19 +6299,23 @@
|
|
|
6134
6299
|
filterInp.value = '';
|
|
6135
6300
|
const render = (keyword = '') => {
|
|
6136
6301
|
const kw = keyword.toLowerCase();
|
|
6137
|
-
//
|
|
6302
|
+
// 命令面板只列用户/项目技能(内置技能在快捷栏直接显示);手机精简布局快捷栏不平铺技能 → 内置也列进来
|
|
6303
|
+
const mob = document.documentElement.classList.contains('m');
|
|
6304
|
+
document.getElementById('cmd-sheet-title').textContent = mob ? '技能' : '自定义命令';
|
|
6138
6305
|
const filtered = _customSkills.filter(s =>
|
|
6139
|
-
s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
|
|
6140
|
-
|
|
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
|
|
6141
6309
|
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}', '${s.kind || 'command'}')">
|
|
6142
|
-
<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>
|
|
6143
6311
|
</div>`).join('')
|
|
6144
|
-
: '<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>` : '');
|
|
6145
6314
|
};
|
|
6146
6315
|
render();
|
|
6147
6316
|
filterInp.oninput = (e) => render(e.target.value);
|
|
6148
6317
|
sheet.classList.add('open');
|
|
6149
|
-
setTimeout(() => filterInp.focus(), 100);
|
|
6318
|
+
if (!document.documentElement.classList.contains('m')) setTimeout(() => filterInp.focus(), 100); // 手机上自动聚焦会弹键盘挡住列表
|
|
6150
6319
|
};
|
|
6151
6320
|
window.closeCmdSheet = function() {
|
|
6152
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(
|
|
78
|
-
padding-bottom: 0.
|
|
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
|
|
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
|
-
${
|
|
1583
|
-
: s.
|
|
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
|
}
|