@wendongfly/myhi 1.3.168 → 1.3.170

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; } }
@@ -276,10 +292,11 @@
276
292
  .fp-list { flex: 1; overflow-y: auto; padding: 0.25rem 0; }
277
293
  .fp-item { display: flex; align-items: center; padding: 0.5rem 0.75rem; gap: 0.5rem; cursor: pointer; font-size: 0.82rem; color: var(--text); }
278
294
  .fp-item:active { background: var(--surface2); }
279
- .fp-item .fp-icon { width: 1.2rem; text-align: center; flex-shrink: 0; }
280
- .fp-item .fp-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
281
- .fp-item .fp-size { color: var(--muted); font-size: 0.7rem; white-space: nowrap; }
282
- .fp-item .fp-dl { color: var(--blue); font-size: 0.7rem; text-decoration: none; padding: 0.2rem 0.4rem; }
295
+ .fp-item .fp-icon { width: 1.2rem; text-align: center; flex-shrink: 0; align-self: flex-start; padding-top: 0.05rem; }
296
+ /* 文件名允许换行显全名(长报告名不再被掐断);动作收成图标省宽度 */
297
+ .fp-item .fp-name { flex: 1; min-width: 0; word-break: break-all; line-height: 1.35; }
298
+ .fp-item .fp-size { color: var(--muted); font-size: 0.7rem; white-space: nowrap; align-self: flex-start; padding-top: 0.05rem; }
299
+ .fp-item .fp-dl { color: var(--blue); font-size: 0.78rem; text-decoration: none; padding: 0.2rem 0.3rem; flex-shrink: 0; align-self: flex-start; }
283
300
  .fp-empty { text-align: center; color: var(--muted); padding: 2rem 0.75rem; font-size: 0.85rem; }
284
301
  .fp-drop-zone {
285
302
  margin: 0.5rem 0.75rem; padding: 1.5rem; border: 2px dashed var(--border); border-radius: 8px;
@@ -771,14 +788,11 @@
771
788
  <div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
772
789
  <div class="action-sheet-title">📥 灵感收件箱 · 草稿</div>
773
790
  <div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
774
- 临时想法随时<b>记在这儿</b>(可带图片,能连着记多条,带记录时间);AI 正忙时在消息框按回车也会自动转存进来。
775
- <b>取回</b>=把内容放回消息框并<b>出队删除</b>。每条草稿都<b>记着属于哪个任务</b>,过几天也认得出。草稿只存在本机,没有发给模型。
776
- <br>勾「<b>自动接力</b>」的草稿会在本轮干完、会话空闲后<b>自动发出</b>(你人不在也跑,按记录先后排队);
777
- <span style="color:var(--yellow)">默认关</span>,不勾就只能手动取回。被中断或出错收尾的轮次不接力。
791
+ 随手记想法:先存这儿,AI 干完后<b>取回</b>发送;勾「<b>自动接力</b>」则干完自动发出。只存本机,细节见各条目按钮。
778
792
  </div>
779
793
  <!-- 就地新增:不必先在消息框打字再"提取",直接在这儿写 -->
780
794
  <div style="border-top:1px solid var(--surface2);padding:0.5rem 0.2rem">
781
- <textarea id="inbox-new-text" rows="3" spellcheck="false" placeholder="写下临时想法… 截图可直接 Ctrl+V 粘进来 (能连着记多条)"
795
+ <textarea id="inbox-new-text" rows="3" spellcheck="false" placeholder="写下临时想法…(截图可直接 Ctrl+V 粘进来)"
782
796
  style="width:100%;box-sizing:border-box;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:0.5rem;font-size:0.82rem;resize:vertical"></textarea>
783
797
  <div id="inbox-new-img" style="display:none;align-items:center;gap:0.4rem;margin-top:0.35rem;font-size:0.7rem;color:var(--text2)">
784
798
  <span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
@@ -2871,7 +2885,115 @@
2871
2885
  cmdInput.focus();
2872
2886
  }
2873
2887
 
2888
+ // ── @ 文件引用补全 ─────────────────────────────────────────
2889
+ // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
2890
+ // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
2891
+ // 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
2892
+ const _mention = { open: false, start: -1, items: [], idx: 0 };
2893
+ const _mentionBox = document.createElement('div');
2894
+ _mentionBox.id = 'mention-box';
2895
+ document.body.appendChild(_mentionBox);
2896
+ let _mentionTimer = null;
2897
+
2898
+ // 光标处是否正打着一个 @词 —— 是则返回 { start }(@ 的下标)
2899
+ function mentionToken() {
2900
+ const pos = cmdInput.selectionStart ?? cmdInput.value.length;
2901
+ const val = cmdInput.value;
2902
+ const from = val.lastIndexOf('@', Math.max(0, pos - 1));
2903
+ if (from < 0) return null;
2904
+ if (from > 0 && /[A-Za-z0-9._%-]/.test(val[from - 1])) return null; // 邮箱等普通文本不触发
2905
+ if (/[\s\r\n]/.test(val.slice(from + 1, pos))) return null; // @ 后已有空白 = 早结束了
2906
+ return { start: from };
2907
+ }
2908
+
2909
+ function mentionClose() {
2910
+ _mention.open = false;
2911
+ _mention.items = [];
2912
+ _mentionBox.style.display = 'none';
2913
+ }
2914
+
2915
+ function mentionRowHTML(f, i) {
2916
+ const path = f.name;
2917
+ const cut = path.lastIndexOf('/');
2918
+ const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
2919
+ const name = cut >= 0 ? path.slice(cut + 1) : path;
2920
+ const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
2921
+ const hint = f.dir ? '文件夹' : '引用';
2922
+ 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>`;
2923
+ }
2924
+
2925
+ function mentionRender() {
2926
+ _mentionBox.innerHTML = _mention.items.length
2927
+ ? _mention.items.map((f, i) => mentionRowHTML(f, i)).join('')
2928
+ : '<div class="mi-empty">没有匹配的文件</div>';
2929
+ const r = cmdInput.getBoundingClientRect();
2930
+ _mentionBox.style.left = Math.max(8, r.left) + 'px';
2931
+ _mentionBox.style.width = Math.max(r.width, 240) + 'px';
2932
+ _mentionBox.style.bottom = (window.innerHeight - r.top + 6) + 'px';
2933
+ _mentionBox.style.display = 'block';
2934
+ const on = _mentionBox.querySelector('.mi.on');
2935
+ if (on) on.scrollIntoView({ block: 'nearest' });
2936
+ }
2937
+
2938
+ function mentionSearch(token) {
2939
+ clearTimeout(_mentionTimer);
2940
+ _mentionTimer = setTimeout(async () => {
2941
+ try {
2942
+ const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
2943
+ const d = await r.json();
2944
+ if (!_mention.open || !d.ok) return;
2945
+ _mention.items = d.files || [];
2946
+ _mention.idx = 0;
2947
+ mentionRender();
2948
+ } catch { mentionClose(); }
2949
+ }, 140);
2950
+ }
2951
+
2952
+ function mentionUpdate() {
2953
+ const t = mentionToken();
2954
+ if (!t) return mentionClose();
2955
+ _mention.open = true;
2956
+ _mention.start = t.start;
2957
+ mentionSearch(cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length));
2958
+ }
2959
+
2960
+ function mentionPick(f) {
2961
+ // 目录带尾斜杠插入(「迁移到 docs/」一眼看出是文件夹),文件带空格结尾
2962
+ const path = f.name + (f.dir ? '/' : '');
2963
+ const pos = cmdInput.selectionStart ?? cmdInput.value.length;
2964
+ const before = cmdInput.value.slice(0, _mention.start);
2965
+ const after = cmdInput.value.slice(pos);
2966
+ cmdInput.value = before + path + ' ' + after;
2967
+ const caret = (before + path + ' ').length;
2968
+ cmdInput.setSelectionRange(caret, caret);
2969
+ mentionClose();
2970
+ cmdInput.style.height = 'auto';
2971
+ cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
2972
+ cmdInput.focus();
2973
+ }
2974
+
2975
+ cmdInput.addEventListener('input', (e) => { if (!e.isComposing) mentionUpdate(); });
2976
+ cmdInput.addEventListener('keyup', (e) => { if (!['ArrowUp','ArrowDown','Escape','Enter'].includes(e.key)) mentionUpdate(); });
2977
+ cmdInput.addEventListener('blur', () => setTimeout(mentionClose, 120)); // 等候选中的 pointerdown 先落地
2978
+ _mentionBox.addEventListener('pointerdown', (e) => {
2979
+ const row = e.target.closest('.mi[data-i]');
2980
+ if (row) { e.preventDefault(); mentionPick(_mention.items[Number(row.dataset.i)]); }
2981
+ });
2982
+ window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
2983
+
2874
2984
  cmdInput.addEventListener('keydown', (e) => {
2985
+ if (_mention.open) {
2986
+ if (e.key === 'ArrowDown' && _mention.items.length) {
2987
+ e.preventDefault(); _mention.idx = (_mention.idx + 1) % _mention.items.length; mentionRender(); return;
2988
+ }
2989
+ if (e.key === 'ArrowUp' && _mention.items.length) {
2990
+ e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
2991
+ }
2992
+ if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
2993
+ e.preventDefault(); mentionPick(_mention.items[_mention.idx]); return; // 补全层开着时回车=选中,不是发送
2994
+ }
2995
+ if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
2996
+ }
2875
2997
  if (e.key === 'Escape') {
2876
2998
  e.preventDefault();
2877
2999
  if (currentSession?.mode === 'agent' && workState !== 'idle') {
@@ -2992,15 +3114,16 @@
2992
3114
  for (const e of entries) {
2993
3115
  const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
2994
3116
  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>`;
3117
+ html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="引用:把目录路径插入消息框">@</span></div>`;
2996
3118
  } else {
2997
3119
  const editable = isEditableFile(e.name);
2998
3120
  html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
2999
3121
  <span class="fp-icon">${editable ? '📝' : '📄'}</span>
3000
- <span class="fp-name">${escHtml(e.name)}</span>
3122
+ <span class="fp-name" title="${escAttr(rel)}">${escHtml(e.name)}</span>
3001
3123
  <span class="fp-size">${fpFormatSize(e.size)}</span>
3002
- ${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">编辑</span>` : ''}
3003
- <a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">下载</a>
3124
+ <span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}')" title="引用:把文件路径插入消息框,让 AI 读这个文件">@</span>
3125
+ ${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">✏️</span>` : ''}
3126
+ <a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">⬇</a>
3004
3127
  </div>`;
3005
3128
  }
3006
3129
  }
@@ -3012,6 +3135,29 @@
3012
3135
 
3013
3136
  window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
3014
3137
 
3138
+ // 把文本插进消息框光标处(前面没空白就补一个,后面留一个空格方便接着打字)
3139
+ function insertIntoInput(text) {
3140
+ const ta = cmdInput;
3141
+ const s = ta.selectionStart ?? ta.value.length;
3142
+ const e = ta.selectionEnd ?? ta.value.length;
3143
+ const before = ta.value.slice(0, s);
3144
+ const after = ta.value.slice(e);
3145
+ const sep = before && !/\s$/.test(before) ? ' ' : '';
3146
+ ta.value = before + sep + text + ' ' + after;
3147
+ const pos = (before + sep + text + ' ').length;
3148
+ ta.setSelectionRange(pos, pos);
3149
+ ta.style.height = 'auto';
3150
+ ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
3151
+ ta.focus();
3152
+ }
3153
+
3154
+ // 文件面板「引用」:把相对路径插进消息框 —— 「让 AI 读某个文件」不用再手打文件名。
3155
+ // 引用完关掉面板,直接回到输入框继续写要求。
3156
+ window.fpRefer = function(rel) {
3157
+ closeFilePanel();
3158
+ insertIntoInput(rel);
3159
+ };
3160
+
3015
3161
  function fpFormatSize(bytes) {
3016
3162
  if (bytes == null) return '';
3017
3163
  if (bytes < 1024) return bytes + ' B';