From 0a42007101ae83436a76d48f9f7e7ecdd402ca3f Mon Sep 17 00:00:00 2001 From: cc-dan Date: Wed, 11 Mar 2026 15:15:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20v1.2.7=20-=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0CLI=E4=BC=9A=E8=AF=9D=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=BB=BA=E4=BC=9A=E8=AF=9D=E6=8C=87=E5=AE=9A=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E3=80=81=E6=A3=80=E6=9F=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +- public/app.js | 231 ++++++++++++++++++++++++++++++++++++++- public/index.html | 9 +- public/style.css | 234 ++++++++++++++++++++++++++++++++++++++++ server.js | 267 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 738 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0dfbf9..a9f6ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新记录 +- **v1.2.7** + - 新增导入本地 CLI 会话:扫描 `~/.claude/projects/` 下的 `.jsonl`,解析后导入到 cc-web,可续接历史对话。 + - 新增新建会话指定工作目录:创建会话时弹窗设置 cwd,spawn 子进程时使用该目录,header 显示当前工作路径。 + - 新增检查更新功能:设置面板底部「检查更新」按钮,对比本地 CHANGELOG 与 GitHub 最新版本。 + - 修复导入历史会话中工具调用显示动态动画的问题:已完成的工具调用统一显示为静态状态。 + - **v1.2.6** - 工具调用折叠显示:长任务中散落的工具调用块达到5个时自动折叠入唯一父节点,之后每满5个再次移入同一父节点(两级结构),输出结束后剩余散落项也一并收入;总数不超过5个则完整显示不折叠。 - 新增编辑模板弹窗「获取上游模型列表」:通过 `/v1/models` 端点拉取可用模型,填充到四个模型输入框的下拉建议列表,支持自定义端点地址。 @@ -8,9 +14,6 @@ - 新增 AskUserQuestion 选项预览区:左侧选项列表,右侧实时显示选项说明;桌面端 hover 切换,移动端 tap 选中后点确认按钮发送。 - 修复 `~/.claude/settings.json` 写入竞争问题:改为原子写入(先写临时文件再 rename),避免 Claude 子进程读到写了一半的文件导致随机 401 认证失败。 - 修复 `ANTHROPIC_REASONING_MODEL` 被误删问题:补充到 settings.json 白名单,保留该字段不被覆盖。 - - 移动端自定义滚动条优化:加宽滑块热区(18px),滚动时自动显示滑块,停止后 1.2 秒淡出,修复 hover 粘滞导致半透明滑块残留问题。 - - 修复历史消息分批渲染时 prepend 导致的视口跳动问题:通过补偿 scrollTop 保持可见区域稳定。 - - 新增 HTML/SVG 代码块实时预览:代码块右上角新增 Preview 按钮,点击在 iframe 中渲染效果,可切换回 Source 查看代码。 - 修复删除会话时同步删除 `~/.claude/projects/` 下对应的原生会话历史,遍历所有项目目录确保完整清除。 - 新增删除确认弹窗,支持「确认且不再提示」选项,风格与主界面一致。 diff --git a/public/app.js b/public/app.js index f7b9387..73286b0 100644 --- a/public/app.js +++ b/public/app.js @@ -49,6 +49,7 @@ let currentMode = localStorage.getItem('cc-web-mode') || 'yolo'; let currentModel = 'opus'; let loginPasswordValue = ''; // store login password for force-change flow + let currentCwd = null; let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1'; // --- DOM --- @@ -63,8 +64,12 @@ const sidebarOverlay = $('#sidebar-overlay'); const menuBtn = $('#menu-btn'); const newChatBtn = $('#new-chat-btn'); + const newChatArrow = $('#new-chat-arrow'); + const newChatDropdown = $('#new-chat-dropdown'); + const importSessionBtn = $('#import-session-btn'); const sessionList = $('#session-list'); const chatTitle = $('#chat-title'); + const chatCwd = $('#chat-cwd'); const costDisplay = $('#cost-display'); const messagesDiv = $('#messages'); const msgInput = $('#msg-input'); @@ -225,6 +230,18 @@ currentSessionId = msg.sessionId; localStorage.setItem('cc-web-session', currentSessionId); chatTitle.textContent = msg.title || '新会话'; + // 显示 cwd + currentCwd = msg.cwd || null; + if (currentCwd) { + const parts = currentCwd.replace(/\/+$/, '').split('/'); + const short = parts.slice(-2).join('/') || currentCwd; + chatCwd.textContent = '~/' + short; + chatCwd.title = currentCwd; + chatCwd.hidden = false; + } else { + chatCwd.hidden = true; + chatCwd.textContent = ''; + } // 同步 session 的 mode(如有) if (msg.mode && MODE_LABELS[msg.mode]) { currentMode = msg.mode; @@ -347,6 +364,18 @@ case 'password_changed': handlePasswordChanged(msg); break; + + case 'native_sessions': + if (typeof _onNativeSessions === 'function') _onNativeSessions(msg.groups || []); + break; + + case 'cwd_suggestions': + if (typeof _onCwdSuggestions === 'function') _onCwdSuggestions(msg.paths || []); + break; + + case 'update_info': + if (typeof window._ccOnUpdateInfo === 'function') window._ccOnUpdateInfo(msg); + break; } } @@ -476,7 +505,7 @@ bubble.style.whiteSpace = 'pre-wrap'; bubble.textContent = content; } else { - bubble.innerHTML = content ? renderMarkdown(content) : '
'; + bubble.innerHTML = content ? renderMarkdown(content) : ''; } div.appendChild(avatar); @@ -1308,7 +1337,24 @@ }); sidebarOverlay.addEventListener('click', closeSidebar); - newChatBtn.addEventListener('click', () => send({ type: 'new_session' })); + + // Split new-chat button + newChatBtn.addEventListener('click', () => showNewSessionModal()); + newChatArrow.addEventListener('click', (e) => { + e.stopPropagation(); + newChatDropdown.hidden = !newChatDropdown.hidden; + }); + importSessionBtn.addEventListener('click', () => { + newChatDropdown.hidden = true; + showImportSessionModal(); + }); + document.addEventListener('click', (e) => { + if (!newChatDropdown.hidden && + !newChatDropdown.contains(e.target) && + e.target !== newChatArrow) { + newChatDropdown.hidden = true; + } + }); sendBtn.addEventListener('click', sendMessage); abortBtn.addEventListener('click', () => send({ type: 'abort' })); @@ -1474,10 +1520,11 @@
-
修改密码
-
+
+
+
`; overlay.appendChild(panel); @@ -1852,6 +1899,35 @@ const pwOpenModalBtn = panel.querySelector('#pw-open-modal-btn'); pwOpenModalBtn.addEventListener('click', openPasswordModal); + // Check update button + const checkUpdateBtn = panel.querySelector('#check-update-btn'); + const updateStatusEl = panel.querySelector('#update-status'); + let _onUpdateInfo = null; + checkUpdateBtn.addEventListener('click', () => { + updateStatusEl.textContent = '正在检查...'; + updateStatusEl.className = 'settings-status'; + _onUpdateInfo = (info) => { + _onUpdateInfo = null; + if (info.error) { + updateStatusEl.textContent = '检查失败: ' + info.error; + updateStatusEl.className = 'settings-status error'; + return; + } + if (info.hasUpdate) { + updateStatusEl.innerHTML = `有新版本 v${escapeHtml(info.latestVersion)}(当前 v${escapeHtml(info.localVersion)}) 查看更新`; + updateStatusEl.className = 'settings-status success'; + } else { + updateStatusEl.textContent = `已是最新版本 v${info.localVersion}`; + updateStatusEl.className = 'settings-status success'; + } + }; + send({ type: 'check_update' }); + }); + + // Wire _onUpdateInfo into WS handler via closure + const _origOnUpdateInfo = window._ccOnUpdateInfo; + window._ccOnUpdateInfo = (info) => { if (_onUpdateInfo) _onUpdateInfo(info); }; + function openPasswordModal() { const pwOverlay = document.createElement('div'); pwOverlay.className = 'settings-overlay'; @@ -1961,6 +2037,7 @@ _onNotifyTestResult = null; _onModelConfig = null; _onFetchModelsResult = null; + window._ccOnUpdateInfo = null; document.removeEventListener('keydown', _settingsEscape); } @@ -2112,6 +2189,152 @@ } } + // --- New Session Modal --- + let _onCwdSuggestions = null; + + function showNewSessionModal() { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.id = 'new-session-overlay'; + + overlay.innerHTML = ` + + `; + + document.body.appendChild(overlay); + + const cwdInput = overlay.querySelector('#ns-cwd-input'); + const cwdList = overlay.querySelector('#ns-cwd-list'); + + // Fetch suggestions on focus + cwdInput.addEventListener('focus', () => { + _onCwdSuggestions = (paths) => { + cwdList.innerHTML = paths.map(p => ``).join(''); + }; + send({ type: 'list_cwd_suggestions' }); + }); + + function close() { + overlay.remove(); + _onCwdSuggestions = null; + } + + overlay.querySelector('#ns-close-btn').addEventListener('click', close); + overlay.querySelector('#ns-cancel-btn').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + overlay.querySelector('#ns-create-btn').addEventListener('click', () => { + const cwd = cwdInput.value.trim() || null; + close(); + send({ type: 'new_session', cwd }); + }); + + cwdInput.focus(); + } + + // --- Import Native Session Modal --- + let _onNativeSessions = null; + + function showImportSessionModal() { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.id = 'import-session-overlay'; + + overlay.innerHTML = ` + + `; + + document.body.appendChild(overlay); + + function close() { + overlay.remove(); + _onNativeSessions = null; + } + + overlay.querySelector('#is-close-btn').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + _onNativeSessions = (groups) => { + const body = overlay.querySelector('#is-body'); + if (!body) return; + if (!groups || groups.length === 0) { + body.innerHTML = ''; + return; + } + body.innerHTML = ''; + for (const group of groups) { + const groupEl = document.createElement('div'); + groupEl.className = 'import-group'; + // Convert slug dir to readable path + let readablePath = group.dir.replace(/-/g, '/'); + if (!readablePath.startsWith('/')) readablePath = '/' + readablePath; + readablePath = readablePath.replace(/\/+/g, '/'); + const groupTitle = document.createElement('div'); + groupTitle.className = 'import-group-title'; + groupTitle.textContent = readablePath; + groupEl.appendChild(groupTitle); + for (const sess of group.sessions) { + const item = document.createElement('div'); + item.className = 'import-item'; + const info = document.createElement('div'); + info.className = 'import-item-info'; + const titleEl = document.createElement('div'); + titleEl.className = 'import-item-title'; + titleEl.textContent = sess.title; + const meta = document.createElement('div'); + meta.className = 'import-item-meta'; + const cwdText = sess.cwd ? sess.cwd : ''; + const timeText = sess.updatedAt ? timeAgo(sess.updatedAt) : ''; + meta.textContent = [cwdText, timeText].filter(Boolean).join(' · '); + info.appendChild(titleEl); + info.appendChild(meta); + const btn = document.createElement('button'); + btn.className = 'import-item-btn'; + btn.textContent = sess.alreadyImported ? '重新导入' : '导入'; + btn.addEventListener('click', () => { + if (sess.alreadyImported) { + if (!confirm('已导入过此会话,重新导入将覆盖已有内容。确认继续?')) return; + } else { + if (!confirm('由于 cc-web 与本地 CLI 的逻辑不同,导入会话需要解析后方可展示,导入后将覆盖已有内容。确认继续?')) return; + } + close(); + send({ type: 'import_native_session', sessionId: sess.sessionId, projectDir: group.dir }); + }); + item.appendChild(info); + item.appendChild(btn); + groupEl.appendChild(item); + } + body.appendChild(groupEl); + } + }; + + send({ type: 'list_native_sessions' }); + } + // --- Helpers --- function escapeHtml(str) { if (!str) return ''; diff --git a/public/index.html b/public/index.html index 46156d5..66acf54 100644 --- a/public/index.html +++ b/public/index.html @@ -32,7 +32,13 @@