release v1.2.6: AskUserQuestion preview panel, 401 atomic write fix, mobile scrollbar
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
# 更新记录
|
||||
|
||||
- **v1.2.5**
|
||||
- **v1.2.6**
|
||||
- 新增 AskUserQuestion 选项预览区:左侧选项列表,右侧实时显示选项说明;桌面端 hover 切换,移动端 tap 选中后点确认按钮发送。
|
||||
- 修复 `~/.claude/settings.json` 写入竞争问题:改为原子写入(先写临时文件再 rename),避免 Claude 子进程读到写了一半的文件导致随机 401 认证失败。
|
||||
- 修复 `ANTHROPIC_REASONING_MODEL` 被误删问题:补充到 settings.json 白名单,保留该字段不被覆盖。
|
||||
- 移动端自定义滚动条优化:加宽滑块热区(18px),滚动时自动显示滑块,停止后 1.2 秒淡出,修复 hover 粘滞导致半透明滑块残留问题。
|
||||
|
||||
- 修复删除会话时同步删除 `~/.claude/projects/` 下对应的原生会话历史,遍历所有项目目录确保完整清除。
|
||||
- 新增删除确认弹窗,支持「确认且不再提示」选项,风格与主界面一致。
|
||||
- 用户消息支持多行换行显示。
|
||||
|
||||
@@ -528,29 +528,98 @@
|
||||
card.appendChild(body);
|
||||
|
||||
if (Array.isArray(q.options) && q.options.length > 0) {
|
||||
const hasDesc = q.options.some(o => o.description);
|
||||
|
||||
// 左右分栏容器
|
||||
const layout = document.createElement('div');
|
||||
layout.className = 'ask-options-layout' + (hasDesc ? ' has-preview' : '');
|
||||
|
||||
const opts = document.createElement('div');
|
||||
opts.className = 'ask-question-options';
|
||||
|
||||
// 右侧预览区(仅在有 description 时创建)
|
||||
const preview = hasDesc ? document.createElement('div') : null;
|
||||
if (preview) {
|
||||
preview.className = 'ask-option-preview';
|
||||
// 默认显示第一项
|
||||
preview.textContent = q.options[0].description || '';
|
||||
}
|
||||
|
||||
// 当前选中项(移动端 tap-to-preview 状态)
|
||||
let selectedOpt = null;
|
||||
let selectedBtn = null;
|
||||
|
||||
q.options.forEach((opt, i) => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'ask-option-item';
|
||||
item.addEventListener('click', () => appendAskOptionToInput(q, opt));
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'ask-option-label';
|
||||
title.textContent = `${i + 1}. ${opt.label || ''}`;
|
||||
item.appendChild(title);
|
||||
|
||||
if (opt.description) {
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'ask-option-desc';
|
||||
desc.textContent = opt.description;
|
||||
item.appendChild(desc);
|
||||
// 桌面:hover 切换预览
|
||||
if (preview) {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
preview.textContent = opt.description || '';
|
||||
});
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
const isTouch = item.dataset.touchActivated === '1';
|
||||
item.dataset.touchActivated = '';
|
||||
|
||||
if (isTouch) {
|
||||
// 移动端:第一次 tap = 选中预览,不发送
|
||||
if (selectedBtn !== item) {
|
||||
if (selectedBtn) selectedBtn.classList.remove('ask-option-selected');
|
||||
selectedBtn = item;
|
||||
selectedOpt = opt;
|
||||
item.classList.add('ask-option-selected');
|
||||
if (preview) preview.textContent = opt.description || '';
|
||||
return;
|
||||
}
|
||||
// 第二次 tap 同一项 = 发送
|
||||
}
|
||||
|
||||
// 桌面直接发送
|
||||
appendAskOptionToInput(q, opt);
|
||||
});
|
||||
|
||||
item.addEventListener('touchstart', () => {
|
||||
item.dataset.touchActivated = '1';
|
||||
}, { passive: true });
|
||||
|
||||
opts.appendChild(item);
|
||||
});
|
||||
card.appendChild(opts);
|
||||
|
||||
layout.appendChild(opts);
|
||||
if (preview) {
|
||||
layout.appendChild(preview);
|
||||
// 预览区最小高度 = 左侧选项列表总高度(渲染后同步)
|
||||
requestAnimationFrame(() => {
|
||||
preview.style.minHeight = opts.offsetHeight + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
// 移动端确认按钮
|
||||
if (hasDesc) {
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.type = 'button';
|
||||
confirmBtn.className = 'ask-confirm-btn';
|
||||
confirmBtn.textContent = '确认选择';
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
if (selectedOpt) {
|
||||
appendAskOptionToInput(q, selectedOpt);
|
||||
} else if (q.options.length > 0) {
|
||||
appendAskOptionToInput(q, q.options[0]);
|
||||
}
|
||||
});
|
||||
layout.appendChild(confirmBtn);
|
||||
}
|
||||
|
||||
card.appendChild(layout);
|
||||
}
|
||||
|
||||
wrapper.appendChild(card);
|
||||
@@ -681,7 +750,15 @@
|
||||
thumbEl.style.top = thumbTop + 'px';
|
||||
}
|
||||
|
||||
messagesDiv.addEventListener('scroll', () => updateScrollbar(), { passive: true });
|
||||
messagesDiv.addEventListener('scroll', () => {
|
||||
updateScrollbar();
|
||||
// 移动端:滚动时短暂显示滑块,停止后淡出
|
||||
scrollbarEl.classList.add('scrolling');
|
||||
clearTimeout(scrollbarEl._hideTimer);
|
||||
scrollbarEl._hideTimer = setTimeout(() => {
|
||||
if (!isDragging) scrollbarEl.classList.remove('scrolling');
|
||||
}, 1200);
|
||||
}, { passive: true });
|
||||
new ResizeObserver(updateScrollbar).observe(messagesDiv);
|
||||
|
||||
// Drag logic
|
||||
|
||||
@@ -49,11 +49,9 @@ body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
|
||||
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #b0a090; }
|
||||
/* 全局隐藏原生滚动条,聊天区由 custom-scrollbar 接管 */
|
||||
::-webkit-scrollbar { display: none; }
|
||||
* { scrollbar-width: none; }
|
||||
|
||||
/* === Login === */
|
||||
.login-overlay {
|
||||
@@ -369,6 +367,16 @@ body {
|
||||
.custom-scrollbar.active {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 移动端触摸后 hover 会粘滞 — 完全禁用 hover 触发,只靠 .active(拖动时)显示 */
|
||||
@media (pointer: coarse) {
|
||||
.messages-wrap:hover .custom-scrollbar {
|
||||
opacity: 0;
|
||||
}
|
||||
/* 移动端滚动时用独立的类显示滑块,不走 .active */
|
||||
.custom-scrollbar.scrolling {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.custom-scrollbar-thumb {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -388,6 +396,28 @@ body {
|
||||
cursor: grab;
|
||||
}
|
||||
.custom-scrollbar-thumb.dragging { cursor: grabbing; }
|
||||
/* 移动端触摸设备:加宽滑块与轨道,便于手指操作;默认隐藏,拖动时显示 */
|
||||
@media (pointer: coarse) {
|
||||
.custom-scrollbar {
|
||||
width: 18px;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.custom-scrollbar.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.custom-scrollbar-thumb {
|
||||
width: 8px;
|
||||
right: 5px;
|
||||
min-height: 40px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar-thumb:hover,
|
||||
.custom-scrollbar-thumb.dragging {
|
||||
width: 14px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
.welcome-msg {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
@@ -640,10 +670,64 @@ body {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
/* 左右分栏布局 */
|
||||
.ask-options-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.ask-options-layout.has-preview {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
.ask-question-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
}
|
||||
/* 右侧预览区 */
|
||||
.ask-option-preview {
|
||||
grid-row: 1;
|
||||
grid-column: 2;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
min-height: 60px;
|
||||
transition: background 0.15s;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* 确认按钮 — 仅移动端 */
|
||||
.ask-confirm-btn {
|
||||
display: none;
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
padding: 9px 0;
|
||||
border: 1.5px solid var(--accent);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.ask-confirm-btn:active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
.ask-confirm-btn { display: block; }
|
||||
}
|
||||
.ask-option-item {
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -659,6 +743,10 @@ body {
|
||||
background: var(--accent-light);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.ask-option-item.ask-option-selected {
|
||||
background: var(--accent-light);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.ask-option-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
12
server.js
12
server.js
@@ -311,7 +311,8 @@ function loadClaudeJsonModelMap() {
|
||||
// Apply model config to runtime MODEL_MAP only (env vars are injected per-spawn, not here)
|
||||
const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'settings.json');
|
||||
const SETTINGS_API_KEYS = ['ANTHROPIC_AUTH_TOKEN','ANTHROPIC_API_KEY','ANTHROPIC_BASE_URL','ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL','ANTHROPIC_DEFAULT_SONNET_MODEL','ANTHROPIC_DEFAULT_HAIKU_MODEL'];
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL','ANTHROPIC_DEFAULT_SONNET_MODEL','ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
'ANTHROPIC_REASONING_MODEL'];
|
||||
|
||||
function applyCustomTemplateToSettings(tpl) {
|
||||
let settings = {};
|
||||
@@ -327,7 +328,14 @@ function applyCustomTemplateToSettings(tpl) {
|
||||
if (tpl.sonnetModel) cleanedEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = tpl.sonnetModel;
|
||||
if (tpl.haikuModel) cleanedEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = tpl.haikuModel;
|
||||
settings.env = cleanedEnv;
|
||||
try { fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2)); } catch {}
|
||||
// 原子写入:先写临时文件再 rename,避免 Claude 子进程读到写了一半的文件
|
||||
const tmpPath = CLAUDE_SETTINGS_PATH + '.tmp';
|
||||
try {
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2));
|
||||
fs.renameSync(tmpPath, CLAUDE_SETTINGS_PATH);
|
||||
} catch {
|
||||
try { fs.unlinkSync(tmpPath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function applyModelConfig() {
|
||||
|
||||
Reference in New Issue
Block a user