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) : '
+
+
+
`;
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 = '
未找到本地 CLI 会话
';
+ 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 @@