@wendongfly/myhi 1.3.168 → 1.3.169

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
@@ -210,6 +210,22 @@
210
210
  #img-preview .img-remove:hover { background: var(--red); color: #fff; }
211
211
  #input-box.focused { border-color: var(--blue); }
212
212
  #input-box.disabled { opacity: 0.5; pointer-events: none; }
213
+
214
+ /* @ 文件引用补全浮层(挂在 body 上,fixed 定位在输入框上方) */
215
+ #mention-box {
216
+ position: fixed; z-index: 95; display: none;
217
+ background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
218
+ box-shadow: 0 8px 30px rgba(0,0,0,0.45);
219
+ max-height: 42vh; overflow-y: auto; overscroll-behavior: contain;
220
+ font-size: 0.8rem; padding: 0.25rem;
221
+ }
222
+ #mention-box .mi { display: flex; align-items: center; gap: 0.45rem; padding: 0.45rem 0.6rem; border-radius: 7px; cursor: pointer; }
223
+ #mention-box .mi.on { background: var(--surface3); }
224
+ #mention-box .mi .m-ico { flex-shrink: 0; }
225
+ #mention-box .mi .m-dir { color: var(--muted); font-size: 0.68rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; }
226
+ #mention-box .mi .m-name { color: var(--text); white-space: nowrap; }
227
+ #mention-box .mi .m-hint { color: var(--muted); font-size: 0.66rem; margin-left: auto; padding-left: 0.5rem; flex-shrink: 0; }
228
+ #mention-box .mi-empty { padding: 0.55rem 0.7rem; color: var(--muted); font-size: 0.75rem; }
213
229
  #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; }
214
230
  #cmd-input::placeholder { color: var(--dimmer); }
215
231
  @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
@@ -2871,7 +2887,110 @@
2871
2887
  cmdInput.focus();
2872
2888
  }
2873
2889
 
2890
+ // ── @ 文件引用补全 ─────────────────────────────────────────
2891
+ // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
2892
+ // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
2893
+ // 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
2894
+ const _mention = { open: false, start: -1, items: [], idx: 0 };
2895
+ const _mentionBox = document.createElement('div');
2896
+ _mentionBox.id = 'mention-box';
2897
+ document.body.appendChild(_mentionBox);
2898
+ let _mentionTimer = null;
2899
+
2900
+ // 光标处是否正打着一个 @词 —— 是则返回 { start }(@ 的下标)
2901
+ function mentionToken() {
2902
+ const pos = cmdInput.selectionStart ?? cmdInput.value.length;
2903
+ const val = cmdInput.value;
2904
+ const from = val.lastIndexOf('@', Math.max(0, pos - 1));
2905
+ if (from < 0) return null;
2906
+ if (from > 0 && /[A-Za-z0-9._%-]/.test(val[from - 1])) return null; // 邮箱等普通文本不触发
2907
+ if (/[\s\r\n]/.test(val.slice(from + 1, pos))) return null; // @ 后已有空白 = 早结束了
2908
+ return { start: from };
2909
+ }
2910
+
2911
+ function mentionClose() {
2912
+ _mention.open = false;
2913
+ _mention.items = [];
2914
+ _mentionBox.style.display = 'none';
2915
+ }
2916
+
2917
+ function mentionRowHTML(path, i) {
2918
+ const cut = path.lastIndexOf('/');
2919
+ const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
2920
+ const name = cut >= 0 ? path.slice(cut + 1) : path;
2921
+ return `<div class="mi ${i === _mention.idx ? 'on' : ''}" data-i="${i}"><span class="m-ico">${isEditableFile(name) ? '📝' : '📄'}</span><span class="m-dir">${escHtml(dir)}</span><span class="m-name">${escHtml(name)}</span><span class="m-hint">引用</span></div>`;
2922
+ }
2923
+
2924
+ function mentionRender() {
2925
+ _mentionBox.innerHTML = _mention.items.length
2926
+ ? _mention.items.map((f, i) => mentionRowHTML(f, i)).join('')
2927
+ : '<div class="mi-empty">没有匹配的文件</div>';
2928
+ const r = cmdInput.getBoundingClientRect();
2929
+ _mentionBox.style.left = Math.max(8, r.left) + 'px';
2930
+ _mentionBox.style.width = Math.max(r.width, 240) + 'px';
2931
+ _mentionBox.style.bottom = (window.innerHeight - r.top + 6) + 'px';
2932
+ _mentionBox.style.display = 'block';
2933
+ const on = _mentionBox.querySelector('.mi.on');
2934
+ if (on) on.scrollIntoView({ block: 'nearest' });
2935
+ }
2936
+
2937
+ function mentionSearch(token) {
2938
+ clearTimeout(_mentionTimer);
2939
+ _mentionTimer = setTimeout(async () => {
2940
+ try {
2941
+ const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
2942
+ const d = await r.json();
2943
+ if (!_mention.open || !d.ok) return;
2944
+ _mention.items = d.files || [];
2945
+ _mention.idx = 0;
2946
+ mentionRender();
2947
+ } catch { mentionClose(); }
2948
+ }, 140);
2949
+ }
2950
+
2951
+ function mentionUpdate() {
2952
+ const t = mentionToken();
2953
+ if (!t) return mentionClose();
2954
+ _mention.open = true;
2955
+ _mention.start = t.start;
2956
+ mentionSearch(cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length));
2957
+ }
2958
+
2959
+ function mentionPick(path) {
2960
+ const pos = cmdInput.selectionStart ?? cmdInput.value.length;
2961
+ const before = cmdInput.value.slice(0, _mention.start);
2962
+ const after = cmdInput.value.slice(pos);
2963
+ cmdInput.value = before + path + ' ' + after;
2964
+ const caret = (before + path + ' ').length;
2965
+ cmdInput.setSelectionRange(caret, caret);
2966
+ mentionClose();
2967
+ cmdInput.style.height = 'auto';
2968
+ cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
2969
+ cmdInput.focus();
2970
+ }
2971
+
2972
+ cmdInput.addEventListener('input', (e) => { if (!e.isComposing) mentionUpdate(); });
2973
+ cmdInput.addEventListener('keyup', (e) => { if (!['ArrowUp','ArrowDown','Escape','Enter'].includes(e.key)) mentionUpdate(); });
2974
+ cmdInput.addEventListener('blur', () => setTimeout(mentionClose, 120)); // 等候选中的 pointerdown 先落地
2975
+ _mentionBox.addEventListener('pointerdown', (e) => {
2976
+ const row = e.target.closest('.mi[data-i]');
2977
+ if (row) { e.preventDefault(); mentionPick(_mention.items[Number(row.dataset.i)]); }
2978
+ });
2979
+ window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
2980
+
2874
2981
  cmdInput.addEventListener('keydown', (e) => {
2982
+ if (_mention.open) {
2983
+ if (e.key === 'ArrowDown' && _mention.items.length) {
2984
+ e.preventDefault(); _mention.idx = (_mention.idx + 1) % _mention.items.length; mentionRender(); return;
2985
+ }
2986
+ if (e.key === 'ArrowUp' && _mention.items.length) {
2987
+ e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
2988
+ }
2989
+ if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
2990
+ e.preventDefault(); mentionPick(_mention.items[_mention.idx]); return; // 补全层开着时回车=选中,不是发送
2991
+ }
2992
+ if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
2993
+ }
2875
2994
  if (e.key === 'Escape') {
2876
2995
  e.preventDefault();
2877
2996
  if (currentSession?.mode === 'agent' && workState !== 'idle') {
@@ -2992,13 +3111,14 @@
2992
3111
  for (const e of entries) {
2993
3112
  const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
2994
3113
  if (e.isDir) {
2995
- html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span></div>`;
3114
+ html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="把目录路径插入消息框">引用</span></div>`;
2996
3115
  } else {
2997
3116
  const editable = isEditableFile(e.name);
2998
3117
  html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
2999
3118
  <span class="fp-icon">${editable ? '📝' : '📄'}</span>
3000
3119
  <span class="fp-name">${escHtml(e.name)}</span>
3001
3120
  <span class="fp-size">${fpFormatSize(e.size)}</span>
3121
+ <span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}')" title="把文件路径插入消息框,让 AI 读这个文件">引用</span>
3002
3122
  ${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">编辑</span>` : ''}
3003
3123
  <a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">下载</a>
3004
3124
  </div>`;
@@ -3012,6 +3132,29 @@
3012
3132
 
3013
3133
  window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
3014
3134
 
3135
+ // 把文本插进消息框光标处(前面没空白就补一个,后面留一个空格方便接着打字)
3136
+ function insertIntoInput(text) {
3137
+ const ta = cmdInput;
3138
+ const s = ta.selectionStart ?? ta.value.length;
3139
+ const e = ta.selectionEnd ?? ta.value.length;
3140
+ const before = ta.value.slice(0, s);
3141
+ const after = ta.value.slice(e);
3142
+ const sep = before && !/\s$/.test(before) ? ' ' : '';
3143
+ ta.value = before + sep + text + ' ' + after;
3144
+ const pos = (before + sep + text + ' ').length;
3145
+ ta.setSelectionRange(pos, pos);
3146
+ ta.style.height = 'auto';
3147
+ ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
3148
+ ta.focus();
3149
+ }
3150
+
3151
+ // 文件面板「引用」:把相对路径插进消息框 —— 「让 AI 读某个文件」不用再手打文件名。
3152
+ // 引用完关掉面板,直接回到输入框继续写要求。
3153
+ window.fpRefer = function(rel) {
3154
+ closeFilePanel();
3155
+ insertIntoInput(rel);
3156
+ };
3157
+
3015
3158
  function fpFormatSize(bytes) {
3016
3159
  if (bytes == null) return '';
3017
3160
  if (bytes < 1024) return bytes + ' B';