@wendongfly/myhi 1.3.182 → 1.3.184

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
@@ -22,6 +22,7 @@
22
22
  }catch(e){}})();
23
23
  </script>
24
24
  <style>
25
+ body.no-desktop [onclick="fpOpenFolder()"] { display: none !important; }
25
26
  /* ── 主题 & 字号(默认深色,值=原配色,深色零变化)───────── */
26
27
  :root {
27
28
  --bg:#0d1117; --surface:#161b22; --surface2:#21262d; --surface3:#1c2128; --border:#30363d;
@@ -267,6 +268,8 @@
267
268
  #mention-box .mi .m-name { color: var(--text); white-space: nowrap; }
268
269
  #mention-box .mi .m-hint { color: var(--muted); font-size: 0.66rem; margin-left: auto; padding-left: 0.5rem; flex-shrink: 0; }
269
270
  #mention-box .mi-empty { padding: 0.55rem 0.7rem; color: var(--muted); font-size: 0.75rem; }
271
+ #mention-box .mi-head { padding: 0.4rem 0.6rem 0.15rem; color: var(--muted); font-size: 0.66rem; font-weight: 600; letter-spacing: 0.04em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
272
+ #mention-box .mi-tip { padding: 0.35rem 0.6rem 0.2rem; color: var(--dim, var(--muted)); font-size: 0.64rem; border-top: 1px solid var(--border); margin-top: 0.2rem; }
270
273
  #cmd-input { display: block; width: 100%; background: transparent; color: var(--text); border: none; padding: 0.6rem 0.75rem 0.3rem; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.85rem; outline: none; resize: none; line-height: 1.5; max-height: 140px; overflow-y: auto; }
271
274
  #cmd-input::placeholder { color: var(--dimmer); }
272
275
  @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
@@ -1104,6 +1107,8 @@
1104
1107
  ansi.use_classes = false;
1105
1108
 
1106
1109
  fetch('/api/me').then(r => r.json()).then(data => {
1110
+ // 服务端没有桌面(headless 部署):「打开当前文件夹」必然无效,直接藏掉,只留文件面板
1111
+ if (data?.desktop === false) document.body.classList.add('no-desktop');
1107
1112
  // 打包默认快捷栏预设:用户没自定义过勾选(skVisible 为 null)时采用服务端 config.json 的 skMode
1108
1113
  if (data?.skMode && settings.skVisible == null) {
1109
1114
  settings.skVisible = data.skMode === 'normal' ? skPresetNormal() : skPresetAll();
@@ -3202,7 +3207,7 @@
3202
3207
  // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
3203
3208
  // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
3204
3209
  // 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
3205
- const _mention = { open: false, start: -1, items: [], idx: 0 };
3210
+ const _mention = { open: false, start: -1, items: [], idx: 0, browse: null, seq: 0 };
3206
3211
  const _mentionBox = document.createElement('div');
3207
3212
  _mentionBox.id = 'mention-box';
3208
3213
  document.body.appendChild(_mentionBox);
@@ -3221,24 +3226,38 @@
3221
3226
 
3222
3227
  function mentionClose() {
3223
3228
  _mention.open = false;
3229
+ _mention.browse = null;
3224
3230
  _mention.items = [];
3225
3231
  _mentionBox.style.display = 'none';
3226
3232
  }
3227
3233
 
3234
+ // 两种模式:只打 @ = 分层浏览(_mention.browse 记当前层,点文件夹进入下一层);
3235
+ // @ 后打了字 = 全局递归搜索(服务端已排好:本项目在前、项目外在后,匹配度优先)
3236
+ const MENTION_GROUP = { own: '本项目', ext: '项目外' };
3228
3237
  function mentionRowHTML(f, i) {
3229
- const path = f.name;
3230
- const cut = path.lastIndexOf('/');
3238
+ const on = i === _mention.idx ? 'on' : '';
3239
+ if (f.act === 'up') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">⬆</span><span class="m-name">上一级</span></div>`;
3240
+ if (f.act === 'self') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">✓</span><span class="m-name">引用这个文件夹</span><span class="m-hint">${f.abs ? '项目外目录' : '文件夹'}</span></div>`;
3241
+ const path = f.label || f.name;
3242
+ const cut = f.label ? -1 : path.lastIndexOf('/');
3231
3243
  const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
3232
3244
  const name = cut >= 0 ? path.slice(cut + 1) : path;
3233
3245
  const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
3234
- const hint = f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3235
- return `<div class="mi ${i === _mention.idx ? 'on' : ''}" data-i="${i}"><span class="m-ico">${ico}</span><span class="m-dir">${escHtml(dir)}</span><span class="m-name">${escHtml(name)}</span><span class="m-hint">${hint}</span></div>`;
3246
+ const hint = f.nav ? '进入 ›' : f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3247
+ return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">${ico}</span><span class="m-dir">${escHtml(dir)}</span><span class="m-name">${escHtml(name)}</span><span class="m-hint">${hint}</span></div>`;
3236
3248
  }
3237
3249
 
3238
3250
  function mentionRender() {
3239
- _mentionBox.innerHTML = _mention.items.length
3240
- ? _mention.items.map((f, i) => mentionRowHTML(f, i)).join('')
3241
- : '<div class="mi-empty">没有匹配的文件</div>';
3251
+ const b = _mention.browse;
3252
+ let html = b && b.crumb ? `<div class="mi-head">📂 ${escHtml(b.crumb)}</div>` : '';
3253
+ let group = null;
3254
+ _mention.items.forEach((f, i) => {
3255
+ if (f.group && f.group !== group && !(b && b.crumb)) { group = f.group; html += `<div class="mi-head">${MENTION_GROUP[group]}</div>`; }
3256
+ html += mentionRowHTML(f, i);
3257
+ });
3258
+ if (!_mention.items.some(f => !f.act)) html += `<div class="mi-empty">${b ? '这个文件夹是空的' : '没有匹配的文件'}</div>`;
3259
+ if (b && _mention.items.some(f => f.nav)) html += '<div class="mi-tip">回车/点击进入文件夹 · Tab 直接引用文件夹 · 打字可全局搜索</div>';
3260
+ _mentionBox.innerHTML = html;
3242
3261
  const r = cmdInput.getBoundingClientRect();
3243
3262
  _mentionBox.style.left = Math.max(8, r.left) + 'px';
3244
3263
  _mentionBox.style.width = Math.max(r.width, 240) + 'px';
@@ -3254,20 +3273,68 @@
3254
3273
  try {
3255
3274
  const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
3256
3275
  const d = await r.json();
3257
- if (!_mention.open || !d.ok) return;
3258
- _mention.items = d.files || [];
3276
+ if (!_mention.open || _mention.browse || !d.ok) return;
3277
+ _mention.items = (d.files || []).map(f => ({ ...f, group: f.abs ? 'ext' : 'own' }));
3259
3278
  _mention.idx = 0;
3260
3279
  mentionRender();
3261
3280
  } catch { mentionClose(); }
3262
3281
  }, 140);
3263
3282
  }
3264
3283
 
3284
+ // 分层浏览:root=null 顶层(本项目第一层 + 项目外根目录),-1 本项目内,i 第 i 个项目外根目录内
3285
+ async function mentionBrowse(root, path) {
3286
+ const seq = ++_mention.seq;
3287
+ try {
3288
+ const qs = root === null ? '' : `?root=${root}&path=${encodeURIComponent(path)}`;
3289
+ const r = await fetch(`/api/files/${SESSION_ID}/at-browse${qs}`);
3290
+ const d = await r.json();
3291
+ if (seq !== _mention.seq || !_mention.open || !d.ok) return;
3292
+ const items = [];
3293
+ let crumb = '';
3294
+ if (root !== null) {
3295
+ const rootFull = path ? d.self.slice(0, -(path.length + 1)) : d.self;
3296
+ const rootLabel = root === -1 ? '本项目' : '项目外 · ' + (rootFull.split('/').pop() || rootFull);
3297
+ crumb = rootLabel + (path ? ' / ' + path.split('/').join(' / ') : '');
3298
+ items.push({ act: 'up' });
3299
+ if (path || root !== -1) items.push({ act: 'self', name: d.self, dir: true, abs: root !== -1 });
3300
+ }
3301
+ d.entries.forEach(e => items.push({ ...e, root: root === null ? -1 : root, nav: e.dir, group: root === null ? 'own' : null }));
3302
+ (d.roots || []).forEach(x => items.push({ name: x.name, dir: true, abs: true, root: x.i, rel: '', nav: true, group: 'ext' }));
3303
+ _mention.browse = { root, path, crumb };
3304
+ _mention.items = items;
3305
+ _mention.idx = Math.max(0, items.findIndex(f => !f.act)); // 默认落在第一个条目上,不是「上一级」
3306
+ mentionRender();
3307
+ } catch { mentionClose(); }
3308
+ }
3309
+
3265
3310
  function mentionUpdate() {
3266
3311
  const t = mentionToken();
3267
3312
  if (!t) return mentionClose();
3313
+ const token = cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length);
3314
+ const wasOpen = _mention.open && _mention.start === t.start;
3268
3315
  _mention.open = true;
3269
3316
  _mention.start = t.start;
3270
- mentionSearch(cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length));
3317
+ if (!token) {
3318
+ // 已在浏览某一层就别重置(keyup 等事件会反复进来);新打开或从搜索删回空才回顶层
3319
+ if (!(wasOpen && _mention.browse)) { _mention.browse = { root: null, path: '', crumb: '' }; mentionBrowse(null, ''); }
3320
+ return;
3321
+ }
3322
+ _mention.browse = null;
3323
+ _mention.seq++;
3324
+ mentionSearch(token);
3325
+ }
3326
+
3327
+ // 回车/点击:文件夹(浏览模式)进入、上一级返回、其余引用;Tab = 文件夹也直接引用
3328
+ function mentionActivate(f, pickDir) {
3329
+ if (!f) return;
3330
+ if (f.act === 'up') {
3331
+ const b = _mention.browse;
3332
+ if (b.root !== null && b.path) mentionBrowse(b.root, b.path.includes('/') ? b.path.slice(0, b.path.lastIndexOf('/')) : '');
3333
+ else mentionBrowse(null, '');
3334
+ return;
3335
+ }
3336
+ if (f.nav && !pickDir) return mentionBrowse(f.root, f.rel);
3337
+ mentionPick(f);
3271
3338
  }
3272
3339
 
3273
3340
  function mentionPick(f) {
@@ -3301,7 +3368,7 @@
3301
3368
  if (!d) return;
3302
3369
  if (Math.abs(e.clientX - d.x) >= 8 || Math.abs(e.clientY - d.y) >= 8) return; // 滚动手势
3303
3370
  const row = e.pointerType === 'touch' ? d.row : e.target.closest('.mi[data-i]');
3304
- if (row) mentionPick(_mention.items[Number(row.dataset.i)]);
3371
+ if (row) mentionActivate(_mention.items[Number(row.dataset.i)]);
3305
3372
  });
3306
3373
  _mentionBox.addEventListener('pointercancel', () => { _mentionDown = null; });
3307
3374
  window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
@@ -3315,7 +3382,7 @@
3315
3382
  e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
3316
3383
  }
3317
3384
  if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
3318
- e.preventDefault(); mentionPick(_mention.items[_mention.idx]); return; // 补全层开着时回车=选中,不是发送
3385
+ e.preventDefault(); mentionActivate(_mention.items[_mention.idx], e.key === 'Tab'); return; // 补全层开着时回车=选中/进入,不是发送
3319
3386
  }
3320
3387
  if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
3321
3388
  }
@@ -3434,7 +3501,7 @@
3434
3501
  const d = await r.json();
3435
3502
  if (d.ok) {
3436
3503
  closeFilePanel();
3437
- addStatusMessage('📂 已在本机打开文件夹 ' + (d.cwd || ''));
3504
+ addStatusMessage((d.via === 'task' ? '📂 已在本机桌面打开文件夹(myhi 在后台会话运行,经计划任务打开)' : '📂 已在本机打开文件夹 ') + (d.cwd || ''));
3438
3505
  } else {
3439
3506
  addStatusMessage('打开失败: ' + (d.error || '') + '(若你是远程访问本面板,此按钮本就不生效)');
3440
3507
  }
package/dist/index.html CHANGED
@@ -99,6 +99,11 @@
99
99
  }
100
100
  .hd-btn:hover { color: var(--text); border-color: var(--border2); }
101
101
  #user-name { max-width: 7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
+ /* 顶栏放不下一行时逐级收紧(fitHeader 按实际宽度判断,大字号/窄窗口都适用):先藏用户名,再只留图标 */
103
+ #header.hd-fit1 #user-name { display: none; }
104
+ #header.hd-fit2 .hd-btn .hd-txt { display: none; }
105
+ #header.hd-fit2 .hd-actions { gap: 0.4rem; }
106
+ #header.hd-fit2 .hd-btn { padding: 0.32rem 0.48rem; }
102
107
 
103
108
  /* ── 设置 sheet(全局设置收口:外观 / 本机名称 / @ 引用范围 / 语音 / 账户 / 服务)── */
104
109
  #set-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 30; }
@@ -851,6 +856,7 @@
851
856
  if ((data.exclusive || data.hasUsers) && data.name) {
852
857
  document.getElementById('user-name').textContent = data.name;
853
858
  document.getElementById('set-account-name').textContent = data.name;
859
+ fitHeader();
854
860
  document.getElementById('set-account').style.display = '';
855
861
  document.getElementById('logout-btn').style.display = '';
856
862
  }
@@ -980,7 +986,20 @@
980
986
  r.setAttribute('data-font', f);
981
987
  const meta = document.querySelector('meta[name="theme-color"]');
982
988
  if (meta) meta.setAttribute('content', THEME_META[t] || '#0d1117');
989
+ fitHeader();
983
990
  }
991
+ // 顶栏按钮组折行了就逐级收紧:字号「大」时按钮跟着 rem 变宽,固定断点兜不住
992
+ function fitHeader() {
993
+ const h = document.getElementById('header');
994
+ const acts = h.querySelector('.hd-actions');
995
+ const wraps = () => acts.offsetHeight > document.getElementById('settings-btn').offsetHeight * 1.5;
996
+ h.classList.remove('hd-fit1', 'hd-fit2');
997
+ if (wraps()) h.classList.add('hd-fit1');
998
+ if (wraps()) h.classList.add('hd-fit2');
999
+ }
1000
+ window.addEventListener('resize', fitHeader);
1001
+ document.fonts?.ready.then(fitHeader);
1002
+ fitHeader();
984
1003
  // ── 设置 sheet ─────────────────────────────────────────
985
1004
  const THEMES = [['system','跟随系统'],['dark','深色'],['light','浅色'],['black','纯黑']];
986
1005
  const FONTS = [['small','小'],['medium','中'],['large','大']];