@wendongfly/myhi 1.3.184 → 1.3.186
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 +165 -72
- package/dist/index.html +83 -162
- package/dist/index.js +10 -10
- package/dist/index.min.js +123 -121
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
.sub-act .k { color: var(--muted); }
|
|
158
158
|
.sub-summary { background: var(--surface); border: 1px solid var(--surface2); border-radius: 6px; padding: 0.4rem 0.5rem; margin-top: 0.35rem; font-size: 0.75rem; white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow-y: auto; }
|
|
159
159
|
|
|
160
|
-
/*
|
|
160
|
+
/* 往上翻看历史(含 ☰ 定位跳转后)→ 底部浮动「↓ 回到底部」,期间来了新消息变「↓ 新消息」;点击回到底部 */
|
|
161
161
|
#new-msg-btn { position: fixed; left: 50%; transform: translateX(-50%); bottom: 92px; z-index: 60; background: var(--blue2, #4a9eff); color: #fff; border: none; border-radius: 16px; padding: 0.35rem 0.95rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; box-shadow: 0 3px 10px rgba(0,0,0,0.35); display: none; }
|
|
162
162
|
#new-msg-btn.show { display: block; }
|
|
163
163
|
#new-msg-btn:active { opacity: 0.8; }
|
|
@@ -198,10 +198,16 @@
|
|
|
198
198
|
.raw-toggle:hover { color: var(--text); border-color: var(--blue); }
|
|
199
199
|
/* 工具输出复制:右上角,桌面端悬停才现身,触屏常显 */
|
|
200
200
|
.msg-raw { position: relative; }
|
|
201
|
-
.raw-copy { position: absolute; top: 0.35rem; right: 0.35rem; z-index: 1; font-size: 0.68rem; color: var(--muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 0.1rem 0.45rem; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
|
|
201
|
+
.raw-copy, .cp-btn { position: absolute; top: 0.35rem; right: 0.35rem; z-index: 1; font-size: 0.68rem; color: var(--muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 0.1rem 0.45rem; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
|
|
202
202
|
.msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
|
|
203
|
-
.raw-copy:hover { color: var(--text); border-color: var(--blue); }
|
|
203
|
+
.raw-copy:hover, .cp-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
204
204
|
@media (hover: none) { .raw-copy { opacity: 0.85; } }
|
|
205
|
+
/* 消息里其他带框内容(代码块/表格/diff/工具详情…)同款复制按钮,见 attachCopyBtns */
|
|
206
|
+
.cpw, .cp-host { position: relative; }
|
|
207
|
+
.cp-btn { user-select: none; }
|
|
208
|
+
.cpw:hover > .cp-btn, .cp-host:hover > .cp-btn, .cp-btn.done { opacity: 1; }
|
|
209
|
+
.tool-body:not(.open) + .cp-btn { display: none; }
|
|
210
|
+
@media (hover: none) { .cp-btn { opacity: 0.85; } }
|
|
205
211
|
|
|
206
212
|
/* 历史和状态 */
|
|
207
213
|
.msg-history .code-block { background: var(--bg); border: 1px solid var(--surface2); border-radius: 8px; padding: 0.6rem 0.75rem; font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.75rem; line-height: 1.3; white-space: pre-wrap; word-break: break-all; color: var(--muted); max-height: 300px; overflow-y: auto; }
|
|
@@ -667,7 +673,7 @@
|
|
|
667
673
|
</div>
|
|
668
674
|
<div class="setting-group" data-stab="more" style="display:none">
|
|
669
675
|
<h4>全局设置</h4>
|
|
670
|
-
<div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5"
|
|
676
|
+
<div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5">本机名称、语音识别、账户等对所有任务生效的设置,统一放在首页的 ⚙ 设置里。这里的「外观 / 聊天 / 快捷栏」只影响当前这台设备。</div>
|
|
671
677
|
<button onclick="location.href='/?settings=1'" style="width:100%;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.55rem;font-size:0.82rem;cursor:pointer">前往首页 ⚙ 设置 →</button>
|
|
672
678
|
</div>
|
|
673
679
|
<div class="setting-group" data-stab="more" style="display:none">
|
|
@@ -685,8 +691,8 @@
|
|
|
685
691
|
</div>
|
|
686
692
|
</div>
|
|
687
693
|
|
|
688
|
-
<!--
|
|
689
|
-
<button id="new-msg-btn" onclick="jumpToBottom()">↓
|
|
694
|
+
<!-- 往上翻历史时的浮动按钮(来了新消息时提示「新消息」),点击回到底部 -->
|
|
695
|
+
<button id="new-msg-btn" onclick="jumpToBottom()">↓ 回到底部</button>
|
|
690
696
|
|
|
691
697
|
<!-- 模型选择面板 -->
|
|
692
698
|
<!-- 会话数据面板:上下文水位 + 本会话成本/token 明细(点顶栏水位 chip 打开)-->
|
|
@@ -2318,6 +2324,51 @@
|
|
|
2318
2324
|
return ok;
|
|
2319
2325
|
}
|
|
2320
2326
|
|
|
2327
|
+
function copyFlash(btn, ok) {
|
|
2328
|
+
btn.textContent = ok ? '已复制' : '复制失败';
|
|
2329
|
+
btn.classList.add('done');
|
|
2330
|
+
clearTimeout(btn._t);
|
|
2331
|
+
btn._t = setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('done'); }, 1500);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// 消息里所有「带框」内容右上角都给复制按钮:代码块、表格(复制成制表符分隔,可直接粘进 Excel)、
|
|
2335
|
+
// diff、工具调用详情、子代理汇总、历史块、压缩摘要。工具原始输出(.msg-raw)有自己的按钮。
|
|
2336
|
+
// 渲染点很多(流式每个 chunk 都重写 innerHTML),统一用 MutationObserver 补挂,不逐处改。
|
|
2337
|
+
const COPY_SEL = '.msg-assistant .content pre, .md-table, .msg-diff, .tool-body, .sub-summary, .msg-synthetic .syn-body, .msg-history .code-block';
|
|
2338
|
+
function copyableText(el) {
|
|
2339
|
+
if (el.tagName === 'TABLE') return [...el.rows].map(r => [...r.cells].map(c => c.innerText.trim()).join('\t')).join('\n');
|
|
2340
|
+
const nodes = [...el.childNodes].filter(n => !n.classList?.contains('cp-btn'));
|
|
2341
|
+
// diff 每行是一个 <div>,按行拼;其余(pre / pre-wrap 文本)textContent 自带换行
|
|
2342
|
+
if (el.classList.contains('msg-diff')) return nodes.map(n => n.textContent).join('\n');
|
|
2343
|
+
return nodes.map(n => n.textContent).join('').replace(/\n+$/, '');
|
|
2344
|
+
}
|
|
2345
|
+
function attachCopyBtns() {
|
|
2346
|
+
chatArea.querySelectorAll(COPY_SEL).forEach(el => {
|
|
2347
|
+
if (el._cpBtn?.isConnected || el.closest('.msg-raw')) return;
|
|
2348
|
+
const btn = document.createElement('button');
|
|
2349
|
+
btn.className = 'cp-btn';
|
|
2350
|
+
btn.textContent = '复制';
|
|
2351
|
+
btn.title = el.tagName === 'TABLE' ? '复制表格(可直接粘贴到 Excel)' : '复制';
|
|
2352
|
+
btn.onclick = async (e) => { e.stopPropagation(); copyFlash(btn, await copyText(copyableText(el))); };
|
|
2353
|
+
if (el.classList.contains('msg')) {
|
|
2354
|
+
// 本身就是一条 .msg(diff):不能套壳,否则 trimMessages / lastElementChild 认不出
|
|
2355
|
+
el.classList.add('cp-host'); el.appendChild(btn);
|
|
2356
|
+
} else {
|
|
2357
|
+
// 套一层壳挂按钮:代码块/工具详情自己会横竖滚动,按钮放里面会跟着滚走
|
|
2358
|
+
const w = document.createElement('div');
|
|
2359
|
+
w.className = 'cpw';
|
|
2360
|
+
el.before(w); w.append(el, btn);
|
|
2361
|
+
}
|
|
2362
|
+
el._cpBtn = btn;
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
let _cpPending = false;
|
|
2366
|
+
new MutationObserver(() => {
|
|
2367
|
+
if (_cpPending) return;
|
|
2368
|
+
_cpPending = true;
|
|
2369
|
+
requestAnimationFrame(() => { _cpPending = false; attachCopyBtns(); });
|
|
2370
|
+
}).observe(chatArea, { childList: true, subtree: true });
|
|
2371
|
+
|
|
2321
2372
|
// 工具输出局部/全部显示。默认跟设置 rawClip;用户点过按钮的块记在 dataset.open,内容追加时保持。
|
|
2322
2373
|
// 只有内容真的超出 8 行高度才出按钮;回放时元素可能还在 fragment 里没上屏,等上屏再量。
|
|
2323
2374
|
function rawClipSync(msg, tries = 0) {
|
|
@@ -2329,13 +2380,7 @@
|
|
|
2329
2380
|
cp.className = 'raw-copy';
|
|
2330
2381
|
cp.textContent = '复制';
|
|
2331
2382
|
cp.title = '复制全部输出';
|
|
2332
|
-
cp.onclick = async () =>
|
|
2333
|
-
const ok = await copyText(block.textContent.replace(/\n+$/, ''));
|
|
2334
|
-
cp.textContent = ok ? '已复制' : '复制失败';
|
|
2335
|
-
cp.classList.add('done');
|
|
2336
|
-
clearTimeout(cp._t);
|
|
2337
|
-
cp._t = setTimeout(() => { cp.textContent = '复制'; cp.classList.remove('done'); }, 1500);
|
|
2338
|
-
};
|
|
2383
|
+
cp.onclick = async () => copyFlash(cp, await copyText(block.textContent.replace(/\n+$/, '')));
|
|
2339
2384
|
msg.appendChild(cp);
|
|
2340
2385
|
}
|
|
2341
2386
|
const clip = msg.dataset.open ? msg.dataset.open !== '1' : settings.rawClip !== false;
|
|
@@ -2911,9 +2956,12 @@
|
|
|
2911
2956
|
|
|
2912
2957
|
chatArea.addEventListener('scroll', () => {
|
|
2913
2958
|
// 用户主动往上翻 → 禁用自动 trim,避免被删
|
|
2914
|
-
|
|
2959
|
+
const dist = chatArea.scrollHeight - chatArea.clientHeight - chatArea.scrollTop;
|
|
2960
|
+
if (dist > 100) {
|
|
2915
2961
|
_userScrolledUp = true;
|
|
2916
|
-
|
|
2962
|
+
// 离底部超过 300px(翻看历史 / ☰ 定位跳到旧消息)→ 露出「↓ 回到底部」,一键回来
|
|
2963
|
+
if (dist > 300) setBottomBtn(true);
|
|
2964
|
+
} else if (dist < 20) {
|
|
2917
2965
|
// 回到接近底部 → 恢复 trim + 隐藏新消息提示
|
|
2918
2966
|
_userScrolledUp = false;
|
|
2919
2967
|
hideNewMsgHint();
|
|
@@ -3207,7 +3255,7 @@
|
|
|
3207
3255
|
// 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
|
|
3208
3256
|
// 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
|
|
3209
3257
|
// 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
|
|
3210
|
-
const _mention = { open: false, start: -1, items: [], idx: 0, browse: null, seq: 0 };
|
|
3258
|
+
const _mention = { open: false, start: -1, items: [], idx: 0, browse: null, seq: 0, lock: '', token: null };
|
|
3211
3259
|
const _mentionBox = document.createElement('div');
|
|
3212
3260
|
_mentionBox.id = 'mention-box';
|
|
3213
3261
|
document.body.appendChild(_mentionBox);
|
|
@@ -3217,6 +3265,10 @@
|
|
|
3217
3265
|
function mentionToken() {
|
|
3218
3266
|
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3219
3267
|
const val = cmdInput.value;
|
|
3268
|
+
// 逐层进了带空格的文件夹(@My Docs/):认我们自己写进去的路径前缀,不按空白截断
|
|
3269
|
+
const L = _mention.open && _mention.lock;
|
|
3270
|
+
if (L && val[_mention.start] === '@' && val.startsWith(L, _mention.start + 1) && pos >= _mention.start + 1 + L.length
|
|
3271
|
+
&& !/\s/.test(val.slice(_mention.start + 1 + L.length, pos))) return { start: _mention.start };
|
|
3220
3272
|
const from = val.lastIndexOf('@', Math.max(0, pos - 1));
|
|
3221
3273
|
if (from < 0) return null;
|
|
3222
3274
|
if (from > 0 && /[A-Za-z0-9._%-]/.test(val[from - 1])) return null; // 邮箱等普通文本不触发
|
|
@@ -3228,12 +3280,21 @@
|
|
|
3228
3280
|
_mention.open = false;
|
|
3229
3281
|
_mention.browse = null;
|
|
3230
3282
|
_mention.items = [];
|
|
3283
|
+
_mention.lock = '';
|
|
3284
|
+
_mention.token = null;
|
|
3231
3285
|
_mentionBox.style.display = 'none';
|
|
3232
3286
|
}
|
|
3233
3287
|
|
|
3234
|
-
//
|
|
3235
|
-
// @
|
|
3236
|
-
|
|
3288
|
+
// 仿 Claude Code:@ 后是路径形态(含 / 或 \、C:、~)→ 按路径逐层浏览(本机任意目录,成员限本项目/绑定目录);
|
|
3289
|
+
// 只打 @ = 从本项目第一层开始浏览;@ 后打了普通字 = 本项目递归搜索。
|
|
3290
|
+
function mentionPathParts(token) {
|
|
3291
|
+
const t = token.replace(/\\/g, '/');
|
|
3292
|
+
if (/^[A-Za-z]:$/.test(t)) return { dir: t + '/', q: '' };
|
|
3293
|
+
if (t === '~') return { dir: '~/', q: '' };
|
|
3294
|
+
const i = t.lastIndexOf('/');
|
|
3295
|
+
return i < 0 ? null : { dir: t.slice(0, i + 1), q: t.slice(i + 1) };
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3237
3298
|
function mentionRowHTML(f, i) {
|
|
3238
3299
|
const on = i === _mention.idx ? 'on' : '';
|
|
3239
3300
|
if (f.act === 'up') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">⬆</span><span class="m-name">上一级</span></div>`;
|
|
@@ -3242,21 +3303,28 @@
|
|
|
3242
3303
|
const cut = f.label ? -1 : path.lastIndexOf('/');
|
|
3243
3304
|
const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
|
|
3244
3305
|
const name = cut >= 0 ? path.slice(cut + 1) : path;
|
|
3245
|
-
const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
|
|
3306
|
+
const ico = f.dir ? (f.drive ? '💽' : '📁') : (isEditableFile(name) ? '📝' : '📄');
|
|
3246
3307
|
const hint = f.nav ? '进入 ›' : f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
|
|
3247
3308
|
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>`;
|
|
3248
3309
|
}
|
|
3249
3310
|
|
|
3250
3311
|
function mentionRender() {
|
|
3251
3312
|
const b = _mention.browse;
|
|
3252
|
-
let html =
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
html +=
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
if (b
|
|
3313
|
+
let html = '';
|
|
3314
|
+
if (b) {
|
|
3315
|
+
const crumb = b.rel ? '本项目' + (b.dir ? ' / ' + b.dir.replace(/\/+$/, '').split('/').join(' / ') : '')
|
|
3316
|
+
: (b.abs === '/' && b.win ? '本机磁盘' : b.abs || b.dir);
|
|
3317
|
+
html += `<div class="mi-head">📂 ${escHtml(crumb)}</div>`;
|
|
3318
|
+
}
|
|
3319
|
+
_mention.items.forEach((f, i) => { html += mentionRowHTML(f, i); });
|
|
3320
|
+
if (b?.err) html += `<div class="mi-empty" style="color:var(--red)">${escHtml(b.err)}</div>`;
|
|
3321
|
+
else if (!_mention.items.some(f => !f.act)) html += `<div class="mi-empty">${b && !b.q ? '这个文件夹是空的' : '没有匹配的文件'}</div>`;
|
|
3322
|
+
if (b && !b.err) {
|
|
3323
|
+
const tips = [];
|
|
3324
|
+
if (_mention.items.some(f => f.nav)) tips.push('回车进入文件夹 · Tab 直接引用文件夹');
|
|
3325
|
+
if (!b.dir) tips.push('打字搜索本项目 · 项目外打 ' + (b.win ? '@D: 或 @\\' : '@/') + '、@~/ 逐层浏览');
|
|
3326
|
+
if (tips.length) html += `<div class="mi-tip">${escHtml(tips.join(' · '))}</div>`;
|
|
3327
|
+
}
|
|
3260
3328
|
_mentionBox.innerHTML = html;
|
|
3261
3329
|
const r = cmdInput.getBoundingClientRect();
|
|
3262
3330
|
_mentionBox.style.left = Math.max(8, r.left) + 'px';
|
|
@@ -3269,77 +3337,94 @@
|
|
|
3269
3337
|
|
|
3270
3338
|
function mentionSearch(token) {
|
|
3271
3339
|
clearTimeout(_mentionTimer);
|
|
3340
|
+
const seq = _mention.seq;
|
|
3272
3341
|
_mentionTimer = setTimeout(async () => {
|
|
3273
3342
|
try {
|
|
3274
3343
|
const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
|
|
3275
3344
|
const d = await r.json();
|
|
3276
|
-
if (
|
|
3277
|
-
_mention.items =
|
|
3345
|
+
if (seq !== _mention.seq || !_mention.open || !d.ok) return;
|
|
3346
|
+
_mention.items = d.files || [];
|
|
3278
3347
|
_mention.idx = 0;
|
|
3279
3348
|
mentionRender();
|
|
3280
3349
|
} catch { mentionClose(); }
|
|
3281
3350
|
}, 140);
|
|
3282
3351
|
}
|
|
3283
3352
|
|
|
3284
|
-
//
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
const
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3353
|
+
// 按路径列一层:dir 为 @ 后最后一个斜杠及之前(原样,服务端解析 ~ / C: / 相对路径),q 为最后一段
|
|
3354
|
+
function mentionList(dir, q) {
|
|
3355
|
+
clearTimeout(_mentionTimer);
|
|
3356
|
+
const seq = _mention.seq;
|
|
3357
|
+
_mentionTimer = setTimeout(async () => {
|
|
3358
|
+
try {
|
|
3359
|
+
const r = await fetch(`/api/files/${SESSION_ID}/at-ls?dir=${encodeURIComponent(dir)}&q=${encodeURIComponent(q)}`);
|
|
3360
|
+
const d = await r.json();
|
|
3361
|
+
if (seq !== _mention.seq || !_mention.open) return;
|
|
3362
|
+
if (!d.ok) { _mention.browse = { dir, q, err: d.error || '读取失败' }; _mention.items = []; mentionRender(); return; }
|
|
3363
|
+
// 插入输入框的路径:本项目内用相对路径;项目外一律绝对路径(~ 已由服务端展开),AI 直接按它读
|
|
3364
|
+
const absBase = d.abs === '/' ? '' : d.abs.replace(/\/+$/, '');
|
|
3365
|
+
const pickName = (label) => d.rel ? dir + label : (d.abs === '/' && d.win ? label : absBase + '/' + label);
|
|
3366
|
+
const items = [];
|
|
3367
|
+
if (!q && dir) {
|
|
3368
|
+
items.push({ act: 'up' });
|
|
3369
|
+
if (!(d.abs === '/' && d.win)) items.push({ act: 'self', name: d.rel ? dir.replace(/\/+$/, '') : absBase || '/', dir: true, abs: !d.rel }); // 盘符列表本身不能引用
|
|
3370
|
+
}
|
|
3371
|
+
d.entries.forEach(e => items.push({ label: e.label, name: pickName(e.label), dir: e.dir, nav: e.dir, abs: !d.rel, drive: d.abs === '/' && d.win, typed: dir + e.label + '/' }));
|
|
3372
|
+
_mention.browse = { dir, q, ...d };
|
|
3373
|
+
_mention.items = items;
|
|
3374
|
+
_mention.idx = Math.max(0, items.findIndex(f => !f.act)); // 默认落在第一个条目上,不是「上一级」
|
|
3375
|
+
mentionRender();
|
|
3376
|
+
} catch { mentionClose(); }
|
|
3377
|
+
}, 90);
|
|
3308
3378
|
}
|
|
3309
3379
|
|
|
3310
3380
|
function mentionUpdate() {
|
|
3311
3381
|
const t = mentionToken();
|
|
3312
3382
|
if (!t) return mentionClose();
|
|
3313
3383
|
const token = cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length);
|
|
3314
|
-
const
|
|
3384
|
+
const same = _mention.open && _mention.start === t.start && _mention.token === token;
|
|
3385
|
+
if (_mention.start !== t.start) _mention.lock = '';
|
|
3315
3386
|
_mention.open = true;
|
|
3316
3387
|
_mention.start = t.start;
|
|
3317
|
-
if (
|
|
3318
|
-
|
|
3319
|
-
if (!(wasOpen && _mention.browse)) { _mention.browse = { root: null, path: '', crumb: '' }; mentionBrowse(null, ''); }
|
|
3320
|
-
return;
|
|
3321
|
-
}
|
|
3322
|
-
_mention.browse = null;
|
|
3388
|
+
if (same) return; // keyup 等事件会反复进来,token 没变不重复请求
|
|
3389
|
+
_mention.token = token;
|
|
3323
3390
|
_mention.seq++;
|
|
3391
|
+
const pp = token ? mentionPathParts(token) : { dir: '', q: '' };
|
|
3392
|
+
if (pp) return mentionList(pp.dir, pp.q);
|
|
3393
|
+
_mention.browse = null;
|
|
3324
3394
|
mentionSearch(token);
|
|
3325
3395
|
}
|
|
3326
3396
|
|
|
3327
|
-
//
|
|
3397
|
+
// 把 @ 后面(到光标)替换成新的路径并继续浏览——「进入文件夹 / 上一级」都是改输入框里的路径,和手打等价
|
|
3398
|
+
function mentionGoto(tok) {
|
|
3399
|
+
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3400
|
+
const before = cmdInput.value.slice(0, _mention.start + 1);
|
|
3401
|
+
cmdInput.value = before + tok + cmdInput.value.slice(pos);
|
|
3402
|
+
const caret = (before + tok).length;
|
|
3403
|
+
cmdInput.setSelectionRange(caret, caret);
|
|
3404
|
+
_mention.lock = /\s/.test(tok) ? tok : '';
|
|
3405
|
+
cmdInput.focus();
|
|
3406
|
+
mentionUpdate();
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// 回车/点击:文件夹进入、上一级返回、其余引用;Tab = 文件夹也直接引用
|
|
3328
3410
|
function mentionActivate(f, pickDir) {
|
|
3329
3411
|
if (!f) return;
|
|
3412
|
+
const b = _mention.browse;
|
|
3330
3413
|
if (f.act === 'up') {
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3414
|
+
if (b.rel) {
|
|
3415
|
+
const segs = b.dir.split('/').filter(Boolean);
|
|
3416
|
+
return mentionGoto(!segs.length || segs[segs.length - 1] === '..' ? b.dir + '../' : (segs.pop(), segs.length ? segs.join('/') + '/' : ''));
|
|
3417
|
+
}
|
|
3418
|
+
if (!b.parent) return;
|
|
3419
|
+
return mentionGoto(b.parent === '/' ? '/' : b.parent.replace(/\/+$/, '') + '/');
|
|
3335
3420
|
}
|
|
3336
|
-
if (f.nav && !pickDir) return
|
|
3421
|
+
if (f.nav && !pickDir) return mentionGoto(f.typed);
|
|
3337
3422
|
mentionPick(f);
|
|
3338
3423
|
}
|
|
3339
3424
|
|
|
3340
3425
|
function mentionPick(f) {
|
|
3341
3426
|
// 目录带尾斜杠插入(「迁移到 docs/」一眼看出是文件夹),文件带空格结尾
|
|
3342
|
-
const path = f.name + (f.dir ? '/' : '');
|
|
3427
|
+
const path = f.name.replace(/\/+$/, '') + (f.dir ? '/' : '');
|
|
3343
3428
|
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3344
3429
|
const before = cmdInput.value.slice(0, _mention.start);
|
|
3345
3430
|
const after = cmdInput.value.slice(pos);
|
|
@@ -3458,10 +3543,10 @@
|
|
|
3458
3543
|
box.innerHTML = `
|
|
3459
3544
|
<div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
|
|
3460
3545
|
<b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
|
|
3461
|
-
<b>让它读文件</b>:打 <b>@</b>
|
|
3546
|
+
<b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
|
|
3462
3547
|
<b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
|
|
3463
3548
|
<b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
|
|
3464
|
-
<b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰
|
|
3549
|
+
<b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。<br>
|
|
3465
3550
|
<b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
|
|
3466
3551
|
<span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
|
|
3467
3552
|
const closeBtn = document.createElement('button');
|
|
@@ -4139,8 +4224,16 @@
|
|
|
4139
4224
|
if (_userScrolledUp) { showNewMsgHint(); return; }
|
|
4140
4225
|
requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; });
|
|
4141
4226
|
}
|
|
4142
|
-
|
|
4143
|
-
function
|
|
4227
|
+
let _newBelow = false; // 翻看历史期间底下来了新消息 → 按钮文案变「↓ 新消息」
|
|
4228
|
+
function setBottomBtn(show) {
|
|
4229
|
+
const b = document.getElementById('new-msg-btn'); if (!b) return;
|
|
4230
|
+
b.textContent = _newBelow ? '↓ 新消息' : '↓ 回到底部';
|
|
4231
|
+
// 贴着聊天区底边上方:输入框/快捷栏高度随设备和内容变,写死 bottom 在手机上会压住输入框
|
|
4232
|
+
if (show) b.style.bottom = Math.max(12, window.innerHeight - chatArea.getBoundingClientRect().bottom + 12) + 'px';
|
|
4233
|
+
b.classList.toggle('show', show);
|
|
4234
|
+
}
|
|
4235
|
+
function showNewMsgHint() { _newBelow = true; setBottomBtn(true); }
|
|
4236
|
+
function hideNewMsgHint() { _newBelow = false; setBottomBtn(false); }
|
|
4144
4237
|
window.jumpToBottom = function() { _userScrolledUp = false; hideNewMsgHint(); requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); };
|
|
4145
4238
|
function trimMessages() {
|
|
4146
4239
|
if (_userScrolledUp) return; // 用户在往上翻历史,禁用自动删
|