@wendongfly/myhi 1.3.197 → 1.3.199

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,32 @@
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
+ /* 安全区只在「添加到主屏幕」全屏运行时才需要;普通浏览器标签页上面已有地址栏,
97
+ 部分国产浏览器仍报状态栏高度 → 顶栏上方白空一行,这里不留 */
98
+ @media (display-mode: browser) {
99
+ #top-bar { padding-top: 0.5rem; }
100
+ #m-toast { top: 3.2rem; }
101
+ }
102
+ #m-more { position: relative; font-size: 1.1rem; letter-spacing: 1px; }
103
+ #m-more.dot::after { content: ''; position: absolute; top: 6px; right: 5px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); }
104
+ #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; }
105
+ /* 其他任务完成 / 等你回复的提醒条 */
106
+ #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; }
107
+ #m-toast.show { display: block; }
108
+ #m-toast .sub { font-size: 0.72rem; color: var(--muted); margin-top: 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
+
80
110
  /* ── 状态栏 ─────────────────────────────── */
81
111
  #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
112
  #status-bar::-webkit-scrollbar { display: none; }
@@ -420,7 +450,10 @@
420
450
  #fedit-ta:focus { border-color: var(--purple); }
421
451
  /* 预览复用记忆面板的 .md 渲染样式,只解开它自带的 max-height/隐藏 */
422
452
  #fedit-preview { display: none; max-height: none; flex: 1; min-height: 0; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); }
453
+ #fedit-preview.html { padding: 0; overflow: hidden; background: #fff; }
454
+ #fedit-preview.html iframe { width: 100%; height: 100%; border: 0; display: block; background: #fff; }
423
455
  .fedit-tab { flex-shrink: 0; }
456
+ #fedit-sheet .fp-btn { white-space: nowrap; }
424
457
  .fedit-tab.on { border-color: var(--purple); background: var(--purple); color: #fff; }
425
458
  .fp-btn-warn { background: var(--yellow2); border-color: #d2992280; }
426
459
  .fp-btn-warn:active { background: var(--yellow2); }
@@ -651,21 +684,27 @@
651
684
 
652
685
  <div id="top-bar">
653
686
  <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>
687
+ <button id="m-back" class="top-btn" onclick="goHome()" title="首页">
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"><path d="M15 18l-6-6 6-6"/></svg>
689
+ </button>
690
+ <div id="title-group" onclick="if(document.documentElement.classList.contains('m'))openSwitchSheet()">
691
+ <div id="title-row"><span id="m-dot" class="sb-dot idle"></span><div id="session-title">加载中...</div></div>
656
692
  <div id="session-cwd"></div>
657
693
  </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="设置">
694
+ <span id="m-ro" onclick="toggleControl()"></span>
695
+ <button class="top-btn top-hide-m" onclick="openChatHelp()" title="使用说明" style="font-size:0.7rem;color:var(--muted)">?</button>
696
+ <button class="top-btn top-hide-m" onclick="openSettings()" title="设置">
660
697
  <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
698
  </button>
662
699
  <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="返回">
700
+ <button class="top-btn top-hide-m" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
701
+ <button class="top-btn top-hide-m" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
702
+ <button class="top-btn top-hide-m" onclick="goBack()" title="返回">
666
703
  <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
704
  </button>
705
+ <button id="m-more" class="top-btn" onclick="openMobileMore()" title="更多">⋯</button>
668
706
  </div>
707
+ <div id="m-toast"></div>
669
708
 
670
709
  <div id="status-bar">
671
710
  <span class="sb-item" id="work-status"><span class="sb-dot idle"></span> 空闲</span>
@@ -770,7 +809,7 @@
770
809
  <button class="sk sk-claude" data-sk="followup" onclick="openFollowUpSheet()" style="color:var(--blue2)">🔄 跟进</button>
771
810
  <button class="sk sk-claude" data-sk="git" onclick="openGitSheet()" style="color:var(--green)">提交</button>
772
811
  <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>
812
+ <button class="sk sk-claude" data-sk="skills" onclick="skillBtnTap()" style="color:var(--purple2)">技能</button>
774
813
  <span id="sk-builtin-skills" data-sk="skills"></span>
775
814
  <span id="sk-custom-skills" data-sk="skills"></span>
776
815
  </div>
@@ -824,6 +863,7 @@
824
863
  <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
864
  <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
865
  <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>
866
+ <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
867
  <label class="setting-toggle" id="row-parse"><span>把终端里的 Claude 输出整理成聊天样式</span><input type="checkbox" id="set-parse" checked></label>
828
868
  </div>
829
869
  <div class="setting-group" data-stab="chat" style="display:none">
@@ -1140,10 +1180,13 @@
1140
1180
  <div class="action-sheet-backdrop" onclick="closeFileEditor()"></div>
1141
1181
  <div class="action-sheet-box" style="height:92vh;max-height:92vh;display:flex;flex-direction:column">
1142
1182
  <div class="action-sheet-title" style="display:flex;align-items:center;gap:0.5rem">
1143
- <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">✏️ <span id="fedit-title">文件</span></span>
1183
+ <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><span id="fedit-icon">✏️</span> <span id="fedit-title">文件</span></span>
1144
1184
  <span id="fedit-state" style="font-size:0.64rem;color:var(--muted);flex-shrink:0;font-weight:400"></span>
1145
1185
  </div>
1146
- <div id="fedit-path" style="font-size:0.66rem;color:var(--muted);padding:0 0.2rem 0.4rem;word-break:break-all"></div>
1186
+ <div style="display:flex;align-items:flex-start;gap:0.4rem;padding:0 0.2rem 0.4rem">
1187
+ <div id="fedit-path" style="flex:1;min-width:0;font-size:0.68rem;color:var(--muted);word-break:break-all;font-family:'SF Mono',Consolas,monospace" title="文件所在位置"></div>
1188
+ <button class="fp-btn" onclick="feditCopyPath(this)" style="flex-shrink:0;font-size:0.66rem;padding:0.15rem 0.45rem" title="复制完整路径">📋 复制路径</button>
1189
+ </div>
1147
1190
  <div style="flex:1;min-height:0;display:flex;flex-direction:column;gap:0.4rem;padding:0 0.2rem">
1148
1191
  <div id="fedit-loading" style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:0.82rem">加载中...</div>
1149
1192
  <textarea id="fedit-ta" spellcheck="false"></textarea>
@@ -1152,6 +1195,7 @@
1152
1195
  <div style="display:flex;gap:0.4rem;padding:0.5rem 0.2rem 0;align-items:center">
1153
1196
  <button class="fp-btn fedit-tab" id="fedit-tab-edit" onclick="feditSetMode('edit')">✏️ 编辑</button>
1154
1197
  <button class="fp-btn fedit-tab" id="fedit-tab-preview" onclick="feditSetMode('preview')" style="display:none">👁 预览</button>
1198
+ <button class="fp-btn" id="fedit-newtab" onclick="feditOpenTab()" style="display:none" title="在浏览器新标签页里全屏查看(已保存的版本)">↗ 全屏</button>
1155
1199
  <span style="flex:1"></span>
1156
1200
  <button class="fp-btn" onclick="feditReload()" title="放弃本地修改,重新读磁盘最新内容">🔄 重读</button>
1157
1201
  <button class="fp-btn" onclick="closeFileEditor()">关闭</button>
@@ -1167,7 +1211,7 @@
1167
1211
  <div id="cmd-sheet" class="action-sheet">
1168
1212
  <div class="action-sheet-backdrop" onclick="closeCmdSheet()"></div>
1169
1213
  <div class="action-sheet-box" style="max-height:75vh;display:flex;flex-direction:column">
1170
- <div class="action-sheet-title">自定义命令</div>
1214
+ <div class="action-sheet-title" id="cmd-sheet-title">自定义命令</div>
1171
1215
  <div style="padding:0.3rem 0.2rem">
1172
1216
  <input id="cmd-filter" class="slash-inp" placeholder="搜索命令..." autocomplete="off">
1173
1217
  </div>
@@ -1739,6 +1783,7 @@
1739
1783
  if (h.start > last) frag.appendChild(document.createTextNode(s.slice(last, h.start)));
1740
1784
  const a = document.createElement('span');
1741
1785
  a.className = 'file-link';
1786
+ a.title = /\.(html?|md|markdown)$/i.test(h.text) ? '点击查看(可切到编辑)' : '点击查看 / 编辑';
1742
1787
  a.textContent = h.text;
1743
1788
  a.dataset.path = h.text;
1744
1789
  frag.appendChild(a);
@@ -2798,6 +2843,7 @@
2798
2843
  };
2799
2844
  const s = STATES[state] || STATES.idle;
2800
2845
  el.innerHTML = `<span class="sb-dot ${s.dot}"></span> ${s.text}`;
2846
+ const md = document.getElementById('m-dot'); if (md) { md.className = 'sb-dot ' + s.dot; md.title = s.text; }
2801
2847
  // 回归 idle 由 result / agent:busy false / session-exit 驱动,
2802
2848
  // 不做定时兜底——长工具调用期间兜底会让取消按钮提前消失。
2803
2849
  if (currentSession?.mode === 'agent') {
@@ -3442,12 +3488,13 @@
3442
3488
  if (tokens > 0) _lastCtxTokens = tokens;
3443
3489
  const chip = document.getElementById('ctx-chip');
3444
3490
  if (!chip) return;
3445
- if (!tokens || tokens <= 0) { chip.style.display = 'none'; return; }
3491
+ if (!tokens || tokens <= 0) { chip.style.display = 'none'; chip.classList.remove('hi'); return; }
3446
3492
  const limit = ctxLimit(tokens);
3447
3493
  const pct = Math.min(100, Math.round(tokens / limit * 100));
3448
3494
  chip.style.display = '';
3449
3495
  chip.textContent = `◈ ${pct}%`;
3450
3496
  chip.style.color = pct >= 90 ? 'var(--red)' : pct >= 75 ? 'var(--yellow)' : 'var(--muted)';
3497
+ chip.classList.toggle('hi', pct >= 75); // 手机精简布局只在偏高时才露出来
3451
3498
  // 压缩快捷按钮同步高亮(agent 栏是 doSlashCmd('/compact'),PTY 栏是 doCompact())
3452
3499
  for (const sel of [
3453
3500
  '#sk-claude-agent button[onclick*="/compact"]',
@@ -3938,6 +3985,9 @@
3938
3985
  document.querySelectorAll('.sk').forEach(b => b.disabled = !canTakeControl());
3939
3986
  }
3940
3987
  updateShortcutBar();
3988
+ // 手机顶栏:没控制权时露出一个小标(只读 / 获取控制 / X 控制中),点了等同控制徽标
3989
+ const ro = document.getElementById('m-ro');
3990
+ if (ro) { ro.textContent = controlling ? '' : badge.textContent; ro.classList.toggle('show', !controlling); }
3941
3991
  }
3942
3992
 
3943
3993
  // ── 设置面板 ──────────────────────────────────
@@ -4205,14 +4255,19 @@
4205
4255
  const sheet = document.getElementById('fedit-sheet');
4206
4256
  sheet.classList.add('open');
4207
4257
  document.getElementById('fedit-title').textContent = rel.split('/').pop();
4208
- document.getElementById('fedit-path').textContent = rel;
4258
+ document.getElementById('fedit-path').textContent = feditAbs(rel);
4209
4259
  document.getElementById('fedit-loading').style.display = 'flex';
4210
4260
  document.getElementById('fedit-loading').textContent = '加载中...';
4211
4261
  document.getElementById('fedit-ta').style.display = 'none';
4212
4262
  document.getElementById('fedit-preview').style.display = 'none';
4213
4263
  feditSetState('');
4214
4264
  try {
4215
- const r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(rel)}`);
4265
+ let r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(rel)}`);
4266
+ // AI 常只写文件名(不带目录):根目录没有就在任务目录里按名字找,唯一命中就打开它
4267
+ if (r.status === 404 && !rel.includes('/')) {
4268
+ const hit = await feditFindByName(rel);
4269
+ if (hit) { r = await fetch(`/api/files/${SESSION_ID}/content?path=${encodeURIComponent(hit)}`); document.getElementById('fedit-path').textContent = feditAbs(hit); }
4270
+ }
4216
4271
  const d = await r.json().catch(() => ({}));
4217
4272
  if (!r.ok || !d.ok) {
4218
4273
  const map = { 404: '文件不存在(可能还没生成,或路径不对)', 403: '路径越界(不在会话目录内)', 413: '文件超过 2MB,请用下载' };
@@ -4220,26 +4275,82 @@
4220
4275
  sheet.classList.remove('open');
4221
4276
  return;
4222
4277
  }
4223
- _fedit = { path: d.path, name: d.name, mtime: d.mtime, md: !!d.md, mode: 'edit', loaded: d.content, dirty: false };
4278
+ const html = /\.html?$/i.test(d.name || d.path || '');
4279
+ _fedit = { path: d.path, name: d.name, mtime: d.mtime, md: !!d.md, html, mode: 'edit', loaded: d.content, dirty: false };
4280
+ document.getElementById('fedit-path').textContent = feditAbs(d.path);
4224
4281
  document.getElementById('fedit-ta').value = d.content;
4225
4282
  document.getElementById('fedit-loading').style.display = 'none';
4226
- document.getElementById('fedit-tab-preview').style.display = _fedit.md ? '' : 'none';
4227
- feditSetMode('edit');
4283
+ document.getElementById('fedit-tab-preview').style.display = (_fedit.md || html) ? '' : 'none';
4284
+ document.getElementById('fedit-newtab').style.display = html ? '' : 'none';
4285
+ // 文档类(md / html)默认「看」,要改再点 ✏️ 编辑
4286
+ feditSetMode(_fedit.md || html ? 'preview' : 'edit');
4228
4287
  } catch (e) {
4229
4288
  addStatusMessage('打开出错: ' + e.message);
4230
4289
  sheet.classList.remove('open');
4231
4290
  }
4232
4291
  };
4233
4292
 
4293
+ // 完整路径(任务目录 + 相对路径),方便知道文件在哪、复制给别的工具
4294
+ function feditAbs(rel) {
4295
+ rel = String(rel || '');
4296
+ if (/^[A-Za-z]:[\\/]/.test(rel) || rel.startsWith('/')) return rel;
4297
+ const cwd = String(currentSession?.cwd || '');
4298
+ if (!cwd) return rel;
4299
+ const sep = cwd.includes('\\') ? '\\' : '/';
4300
+ return cwd.replace(/[\\/]+$/, '') + sep + rel.split('/').join(sep);
4301
+ }
4302
+ window.feditCopyPath = function(btn) {
4303
+ const t = document.getElementById('fedit-path').textContent;
4304
+ const done = () => { const o = btn.textContent; btn.textContent = '✓ 已复制'; setTimeout(() => { btn.textContent = o; }, 1200); };
4305
+ (navigator.clipboard?.writeText(t) || Promise.reject()).then(done, () => { const x = document.createElement('textarea'); x.value = t; document.body.appendChild(x); x.select(); try { document.execCommand('copy'); done(); } catch {} x.remove(); });
4306
+ };
4307
+ async function feditFindByName(name) {
4308
+ try {
4309
+ const d = await (await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(name)}`)).json();
4310
+ const hits = (d.files || []).filter(f => !f.dir && f.name.split('/').pop() === name);
4311
+ return hits.length === 1 ? hits[0].name : null;
4312
+ } catch { return null; }
4313
+ }
4314
+ // HTML 预览令牌(见 server /view/:token):同一任务复用,过期前换新
4315
+ let _viewTok = null;
4316
+ async function feditViewBase() {
4317
+ if (!_viewTok || _viewTok.exp < Date.now()) {
4318
+ const d = await (await fetch(`/api/files/${SESSION_ID}/view-token`, { method: 'POST' })).json();
4319
+ if (!d.ok) throw new Error(d.error || '预览不可用');
4320
+ _viewTok = { token: d.token, exp: Date.now() + 3600_000 };
4321
+ }
4322
+ return '/view/' + _viewTok.token + '/';
4323
+ }
4324
+ const feditEncPath = (p) => String(p).split('/').map(encodeURIComponent).join('/');
4325
+ window.feditOpenTab = async function() {
4326
+ if (!_fedit) return;
4327
+ try { window.open((await feditViewBase()) + feditEncPath(_fedit.path), '_blank'); } catch (e) { addStatusMessage('打开失败: ' + e.message); }
4328
+ };
4329
+ // HTML 渲染:沙箱 iframe(不同源,页面脚本碰不到 myhi);内容用编辑框里的(没保存的修改也能看),
4330
+ // 注入 <base> 让图片 / CSS 等相对路径资源走只读预览链接
4331
+ async function feditRenderHtml(pv, src) {
4332
+ pv.innerHTML = '';
4333
+ let base;
4334
+ try { base = (await feditViewBase()) + feditEncPath(_fedit.path.split('/').slice(0, -1).join('/')) + (_fedit.path.includes('/') ? '/' : ''); }
4335
+ catch (e) { pv.textContent = '预览不可用:' + e.message; return; }
4336
+ const f = document.createElement('iframe');
4337
+ f.setAttribute('sandbox', 'allow-scripts allow-popups allow-forms allow-modals');
4338
+ const tag = '<base href="' + location.origin + base + '">';
4339
+ f.srcdoc = /<head[^>]*>/i.test(src) ? src.replace(/<head[^>]*>/i, (m) => m + tag) : tag + src;
4340
+ pv.appendChild(f);
4341
+ }
4342
+
4234
4343
  window.feditSetMode = function(mode) {
4235
4344
  if (!_fedit) return;
4236
- if (mode === 'preview' && !_fedit.md) mode = 'edit';
4345
+ if (mode === 'preview' && !_fedit.md && !_fedit.html) mode = 'edit';
4237
4346
  _fedit.mode = mode;
4238
4347
  const ta = document.getElementById('fedit-ta');
4239
4348
  const pv = document.getElementById('fedit-preview');
4240
4349
  ta.style.display = mode === 'edit' ? 'block' : 'none';
4241
4350
  pv.style.display = mode === 'preview' ? 'block' : 'none';
4242
- if (mode === 'preview') pv.innerHTML = renderMarkdown(ta.value);
4351
+ pv.classList.toggle('html', mode === 'preview' && !!_fedit.html);
4352
+ document.getElementById('fedit-icon').textContent = mode === 'preview' ? '📄' : '✏️';
4353
+ if (mode === 'preview') { if (_fedit.html) feditRenderHtml(pv, ta.value); else pv.innerHTML = renderMarkdown(ta.value); }
4243
4354
  document.getElementById('fedit-tab-edit').classList.toggle('on', mode === 'edit');
4244
4355
  document.getElementById('fedit-tab-preview').classList.toggle('on', mode === 'preview');
4245
4356
  };
@@ -4575,6 +4686,14 @@
4575
4686
  if (b.dataset.v !== 'hide') settings.thinkCollapsed = b.dataset.v === 'fold';
4576
4687
  saveSettings(); syncDisplaySegs();
4577
4688
  }));
4689
+ const syncMobileSeg = () => document.querySelectorAll('#seg-mobile .seg').forEach(b => b.classList.toggle('active', b.dataset.v === (settings.mobileCompact === false ? 'off' : 'on')));
4690
+ syncMobileSeg();
4691
+ document.querySelectorAll('#seg-mobile .seg').forEach(b => b.addEventListener('click', () => {
4692
+ const v = b.dataset.v !== 'off';
4693
+ if ((settings.mobileCompact !== false) === v) return;
4694
+ settings.mobileCompact = v; saveSettings(); syncMobileSeg();
4695
+ location.reload(); // 布局类在页面头部就定了,切换后重载最干净
4696
+ }));
4578
4697
  document.querySelectorAll('#seg-tools .seg').forEach(b => b.addEventListener('click', () => {
4579
4698
  settings.collapseTools = b.dataset.v === 'fold'; saveSettings(); syncDisplaySegs();
4580
4699
  }));
@@ -4603,6 +4722,104 @@
4603
4722
  window.goHome = goHome;
4604
4723
  window.goBack = function() { goHome(); };
4605
4724
 
4725
+ // ── 手机精简布局:⋯ 更多菜单 / 技能按钮 / 未读标记 / 其他任务提醒 ──────────
4726
+ const IS_M = document.documentElement.classList.contains('m');
4727
+ // 技能按钮:手机上快捷栏不平铺技能,点「技能」直接出可运行列表(底部有「管理技能」)
4728
+ window.skillBtnTap = function() { if (IS_M) openCmdSheet(); else openSkillSheet(); };
4729
+
4730
+ // ⋯ 菜单:状态行(在线/控制、项目说明、升级提示)和顶栏低频按钮都收在这;有待处理提示时 ⋯ 带红点
4731
+ const _shown = (id) => { const e = document.getElementById(id); return e && e.style.display !== 'none' && e.textContent.trim() ? e : null; };
4732
+ function mMoreDot() {
4733
+ const g = _shown('guide-chip');
4734
+ document.getElementById('m-more')?.classList.toggle('dot', !!(_shown('update-badge') || _shown('claude-update-badge') || (g && g.textContent.includes('偏长'))));
4735
+ }
4736
+ if (IS_M) new MutationObserver(mMoreDot).observe(document.getElementById('status-bar'), { subtree: true, childList: true, attributes: true, characterData: true });
4737
+ window.openMobileMore = function() {
4738
+ const items = [];
4739
+ const add = (icon, text, fn, color) => items.push({ icon, text, fn, color });
4740
+ add('📁', '文件', () => openFilePanel());
4741
+ const ctx = _shown('ctx-chip'); if (ctx) add('◈', '会话数据(上下文 ' + ctx.textContent.replace(/[^\d%]/g, '') + ')', () => openCtxSheet());
4742
+ const vc = _shown('viewer-count'), cb = _shown('control-badge');
4743
+ if (vc || cb) add('👥', [vc?.textContent, cb?.textContent].filter(Boolean).join(' · '), () => cb && toggleControl());
4744
+ const g = _shown('guide-chip'); if (g) add('', g.textContent, () => showMemory('project'), g.textContent.includes('偏长') ? 'var(--yellow)' : '');
4745
+ const u = _shown('update-badge'); if (u) add('', 'myhi 有更新:' + u.textContent.replace('⬆', '').trim(), () => doUpdate(), 'var(--yellow)');
4746
+ const cu = _shown('claude-update-badge'); if (cu) add('', 'Claude 有更新:' + cu.textContent.replace('⬆', '').trim(), () => doClaudeUpdate(), 'var(--blue)');
4747
+ add('⚙', '设置', () => openSettings());
4748
+ add('❓', '使用说明', () => openChatHelp());
4749
+ const ov = document.createElement('div');
4750
+ ov.style.cssText = 'position:fixed;inset:0;z-index:120;background:rgba(0,0,0,0.5);display:flex;align-items:flex-end';
4751
+ const esc2 = (t) => { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; };
4752
+ 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))">' +
4753
+ '<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>' +
4754
+ 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('') +
4755
+ '</div>';
4756
+ ov.onclick = (e) => { const r = e.target.closest('[data-i]'); ov.remove(); if (r) items[+r.dataset.i].fn(); };
4757
+ document.body.appendChild(ov);
4758
+ };
4759
+
4760
+ // 手机:手指往上推(往下看内容)收起顶栏,往下拉再出现;只跟手势走,AI 输出时的自动滚底不触发
4761
+ if (IS_M) {
4762
+ const bar = document.getElementById('top-bar');
4763
+ let y0 = null, hidden = false;
4764
+ const setHidden = (h) => {
4765
+ if (h === hidden) return; hidden = h;
4766
+ bar.style.marginTop = h ? -bar.offsetHeight + 'px' : '';
4767
+ };
4768
+ chatArea.addEventListener('touchstart', (e) => { y0 = e.touches[0].clientY; }, { passive: true });
4769
+ chatArea.addEventListener('touchmove', (e) => {
4770
+ if (y0 == null) return;
4771
+ const dy = e.touches[0].clientY - y0;
4772
+ if (Math.abs(dy) < 12) return;
4773
+ setHidden(dy < 0 && chatArea.scrollTop > 40);
4774
+ y0 = e.touches[0].clientY;
4775
+ }, { passive: true });
4776
+ chatArea.addEventListener('touchend', () => { y0 = null; }, { passive: true });
4777
+ chatArea.addEventListener('scroll', () => { if (chatArea.scrollTop < 10) setHidden(false); }, { passive: true });
4778
+ }
4779
+
4780
+ // 本页在看 = 本任务已读(切换面板 / 首页据此算「新完成」)
4781
+ let _seenAt = 0;
4782
+ function markSeenNow(force) {
4783
+ if (document.hidden || !window.myhiAttn) return;
4784
+ if (!force && Date.now() - _seenAt < 2000) return;
4785
+ _seenAt = Date.now(); myhiAttn.markSeen(SESSION_ID);
4786
+ }
4787
+ markSeenNow(true);
4788
+ document.addEventListener('visibilitychange', () => markSeenNow(true));
4789
+ window.addEventListener('pagehide', () => markSeenNow(true));
4790
+ socket.on('agent:message', () => markSeenNow());
4791
+
4792
+ // 其他任务的提醒:手机上每 8 秒拉一次列表(页面可见时),有任务转为「等你回复」或刚跑完就弹提示条,点了直接切过去
4793
+ let _attnSnap = null, _toastTimer = null;
4794
+ function mToast(s, kind) {
4795
+ const el = document.getElementById('m-toast'); if (!el) return;
4796
+ const esc2 = (t) => { const d = document.createElement('div'); d.textContent = t || ''; return d.innerHTML; };
4797
+ el.style.borderLeftColor = myhiAttn.color[kind] || 'var(--green)';
4798
+ el.innerHTML = '<div>「' + esc2(s.title || '(无标题)') + '」' + (kind === 'wait' ? '在等你回复' : '已完成') + ' <span style="color:var(--muted);font-size:0.72rem">点此切换</span></div>' +
4799
+ (s.lastText ? '<div class="sub">' + esc2(s.lastText) + '</div>' : '');
4800
+ el.onclick = () => { window.location.href = '/terminal/' + s.id; };
4801
+ el.classList.add('show');
4802
+ clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove('show'), 8000);
4803
+ }
4804
+ function attnCheck(list) {
4805
+ if (!IS_M || !window.myhiAttn || !Array.isArray(list)) return;
4806
+ myhiAttn.init(list);
4807
+ const me = list.find(x => x.id === SESSION_ID)?.owner;
4808
+ const snap = {};
4809
+ for (const s of list) {
4810
+ const st = s.pending > 0 ? 'wait' : s.busy ? 'busy' : '';
4811
+ snap[s.id] = st;
4812
+ // 只提醒自己的任务(管理员的列表里有别人的任务,别人的完成不该弹给你)
4813
+ if (!_attnSnap || s.id === SESSION_ID || !(s.id in _attnSnap) || (me && s.owner && s.owner !== me)) continue;
4814
+ const was = _attnSnap[s.id];
4815
+ if (st === 'wait' && was !== 'wait') mToast(s, 'wait');
4816
+ else if (was === 'busy' && st === '') mToast(s, 'done');
4817
+ }
4818
+ _attnSnap = snap;
4819
+ }
4820
+ socket.on('sessions', attnCheck);
4821
+ if (IS_M) setInterval(() => { if (!document.hidden && socket.connected) socket.emit('list'); }, 8000);
4822
+
4606
4823
  // ── 快速切换会话(免退出到首页)──────────────────────────
4607
4824
  function switchRelTime(ms) {
4608
4825
  if (!ms) return '';
@@ -4624,6 +4841,8 @@
4624
4841
  sessions = await res.json();
4625
4842
  } catch { addStatusMessage && addStatusMessage('会话列表加载失败'); return; }
4626
4843
  sessions.sort((a, b) => switchSessTime(b) - switchSessTime(a)); // 最新在前
4844
+ const A = window.myhiAttn; if (A) A.init(sessions);
4845
+ const attnOf = (s) => (A && s.id !== SESSION_ID ? A.of(s) : (s.pending > 0 ? 'wait' : s.busy ? 'busy' : ''));
4627
4846
  const esc2 = (s) => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
4628
4847
  let mode = localStorage.getItem('myhi_group_mode') || 'time'; // 与首页共用记忆
4629
4848
 
@@ -4631,12 +4850,15 @@
4631
4850
  const cur = s.id === SESSION_ID;
4632
4851
  const proj = (s.cwd || '').replace(/\\/g, '/').split('/').filter(Boolean).slice(-1)[0] || '';
4633
4852
  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(' · ');
4853
+ const at = attnOf(s);
4854
+ const sub = [A && at ? A.label[at] : (s.busy ? '运行中' : ''), proj, switchRelTime(switchSessTime(s)), s.controlHolderName ? (s.controlHolderName + ' 控制中') : ''].filter(Boolean).join(' · ');
4855
+ const dotC = at && A ? A.color[at] : (s.alive ? 'var(--green)' : 'var(--dim)');
4635
4856
  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>' +
4857
+ '<span style="width:8px;height:8px;border-radius:50%;flex-shrink:0;background:' + dotC + (at === 'wait' || at === 'busy' ? ';animation:pulse 1s infinite' : '') + '"></span>' +
4637
4858
  '<div style="flex:1;min-width:0">' +
4638
4859
  '<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
4860
  '<div style="font-size:0.72rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc2(sub) + '</div>' +
4861
+ (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
4862
  (tags ? '<div style="display:flex;flex-wrap:wrap;gap:0.25rem;margin-top:0.25rem">' + tags + '</div>' : '') +
4641
4863
  '</div></div>';
4642
4864
  };
@@ -4659,7 +4881,12 @@
4659
4881
  if (untag.length) h += groupHdr('未分类', untag.length) + untag.map(rowHTML).join('');
4660
4882
  return h;
4661
4883
  }
4662
- return sessions.map(rowHTML).join('');
4884
+ // 最近:需要你关注的排前面(等你回复 > 执行中 > 新完成),其余按时间
4885
+ const buckets = { wait: [], busy: [], done: [], '': [] };
4886
+ for (const s of sessions) buckets[attnOf(s)].push(s);
4887
+ if (!buckets.wait.length && !buckets.busy.length && !buckets.done.length) return sessions.map(rowHTML).join('');
4888
+ return ['wait', 'busy', 'done', ''].filter(k => buckets[k].length)
4889
+ .map(k => groupHdr(k ? ({ wait: '🟠 等你回复', busy: '🔵 执行中', done: '🟢 新完成' })[k] : '其他', buckets[k].length) + buckets[k].map(rowHTML).join('')).join('');
4663
4890
  };
4664
4891
 
4665
4892
  const overlay = document.createElement('div');
@@ -6147,19 +6374,23 @@
6147
6374
  filterInp.value = '';
6148
6375
  const render = (keyword = '') => {
6149
6376
  const kw = keyword.toLowerCase();
6150
- // 命令面板只列用户/项目技能(内置技能始终在快捷栏直接显示)
6377
+ // 命令面板只列用户/项目技能(内置技能在快捷栏直接显示);手机精简布局快捷栏不平铺技能 → 内置也列进来
6378
+ const mob = document.documentElement.classList.contains('m');
6379
+ document.getElementById('cmd-sheet-title').textContent = mob ? '技能' : '自定义命令';
6151
6380
  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
6381
+ (mob || s.source !== 'builtin') && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
6382
+ filtered.sort((a, b) => (a.source === 'builtin') - (b.source === 'builtin'));
6383
+ listEl.innerHTML = (filtered.length
6154
6384
  ? 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>
6385
+ <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
6386
  </div>`).join('')
6157
- : '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>';
6387
+ : '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>')
6388
+ + (mob ? `<div class="action-sheet-item" onclick="closeCmdSheet();openSkillSheet()"><div><div style="color:var(--muted)">⚙ 管理技能(生成 / 编辑 / 分享 / 导入)</div></div></div>` : '');
6158
6389
  };
6159
6390
  render();
6160
6391
  filterInp.oninput = (e) => render(e.target.value);
6161
6392
  sheet.classList.add('open');
6162
- setTimeout(() => filterInp.focus(), 100);
6393
+ if (!document.documentElement.classList.contains('m')) setTimeout(() => filterInp.focus(), 100); // 手机上自动聚焦会弹键盘挡住列表
6163
6394
  };
6164
6395
  window.closeCmdSheet = function() {
6165
6396
  document.getElementById('cmd-sheet').classList.remove('open');