feat: v1.2.7 - 导入本地CLI会话、新建会话指定工作目录、检查更新功能

This commit is contained in:
cc-dan
2026-03-11 15:15:28 +00:00
parent 7b24704c4d
commit 0a42007101
5 changed files with 738 additions and 12 deletions

View File

@@ -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) : '<div class="typing-indicator"><span></span><span></span><span></span></div>';
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 @@
<div class="settings-divider"></div>
<div class="settings-section-title">修改密码</div>
<div class="settings-actions" style="margin-top:0">
<div class="settings-actions" style="margin-top:0;flex-wrap:wrap;gap:10px">
<button class="btn-test" id="pw-open-modal-btn" style="padding:6px 16px">修改密码</button>
<button class="btn-test" id="check-update-btn" style="padding:6px 16px">检查更新</button>
</div>
<div class="settings-status" id="update-status" style="margin-top:8px"></div>
`;
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 = `有新版本 <strong>v${escapeHtml(info.latestVersion)}</strong>(当前 v${escapeHtml(info.localVersion)}&nbsp;<a href="${escapeHtml(info.releaseUrl)}" target="_blank" style="color:var(--accent)">查看更新</a>`;
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 = `
<div class="modal-panel">
<div class="modal-header">
<span class="modal-title">新建会话</span>
<button class="modal-close-btn" id="ns-close-btn">✕</button>
</div>
<div class="modal-body">
<label class="modal-field-label">工作目录</label>
<div class="modal-field-row">
<input type="text" id="ns-cwd-input" class="modal-text-input" placeholder="例如 /home/user/project" list="ns-cwd-list" autocomplete="off">
<datalist id="ns-cwd-list"></datalist>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn-secondary" id="ns-cancel-btn">取消</button>
<button class="modal-btn-primary" id="ns-create-btn">创建</button>
</div>
</div>
`;
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 => `<option value="${escapeHtml(p)}"></option>`).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 = `
<div class="modal-panel modal-panel-wide">
<div class="modal-header">
<span class="modal-title">导入本地 CLI 会话</span>
<button class="modal-close-btn" id="is-close-btn">✕</button>
</div>
<div class="modal-body" id="is-body">
<div class="modal-loading">正在加载…</div>
</div>
</div>
`;
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 = '<div class="modal-empty">未找到本地 CLI 会话</div>';
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 '';

View File

@@ -32,7 +32,13 @@
<!-- Sidebar -->
<aside id="sidebar" class="sidebar">
<div class="sidebar-header">
<button id="new-chat-btn" class="new-chat-btn">+ 新会话</button>
<div class="new-chat-split">
<button id="new-chat-btn" class="new-chat-btn">+ 新会话</button>
<button id="new-chat-arrow" class="new-chat-arrow" title="更多"></button>
</div>
<div id="new-chat-dropdown" class="new-chat-dropdown" hidden>
<button id="import-session-btn">导入本地 CLI 会话</button>
</div>
</div>
<div id="session-list" class="session-list"></div>
<div class="sidebar-footer">
@@ -48,6 +54,7 @@
<header class="chat-header">
<button id="menu-btn" class="menu-btn" title="菜单"></button>
<span id="chat-title" class="chat-title">新会话</span>
<span id="chat-cwd" class="chat-cwd" hidden></span>
<select id="mode-select" class="mode-select" title="权限模式">
<option value="yolo">YOLO</option>
<option value="default">默认</option>

View File

@@ -1309,3 +1309,237 @@ body {
color: var(--text-secondary);
margin-bottom: 14px;
}
/* === New Chat Split Button === */
.new-chat-split {
display: flex;
gap: 4px;
}
.new-chat-split .new-chat-btn {
flex: 1;
border-radius: 10px 0 0 10px;
}
.new-chat-arrow {
padding: 0 10px;
background: var(--accent);
border: none;
border-radius: 0 10px 10px 0;
color: #fff;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
}
.new-chat-arrow:hover { background: var(--accent-hover); }
.new-chat-dropdown {
position: absolute;
top: 54px;
left: 12px;
right: 12px;
background: #fff;
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(45,31,20,0.12);
z-index: 200;
overflow: hidden;
}
.new-chat-dropdown button {
display: block;
width: 100%;
padding: 12px 16px;
background: none;
border: none;
text-align: left;
font-size: 14px;
color: var(--text-primary);
cursor: pointer;
transition: background 0.12s;
}
.new-chat-dropdown button:hover { background: var(--accent-light); }
.sidebar-header { position: relative; }
/* === Chat CWD label === */
.chat-cwd {
font-size: 11px;
color: var(--text-muted);
background: var(--bg-tertiary);
padding: 2px 8px;
border-radius: 6px;
flex-shrink: 0;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
/* === Modal Overlay === */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(45,31,20,0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 500;
padding: 16px;
}
.modal-panel {
background: #fff;
border: 1px solid var(--border-color);
border-radius: 16px;
width: 100%;
max-width: 420px;
box-shadow: 0 8px 32px rgba(45,31,20,0.15);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.modal-panel-wide {
max-width: 600px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.modal-title {
font-weight: 700;
font-size: 16px;
color: var(--text-primary);
}
.modal-close-btn {
background: none;
border: none;
font-size: 16px;
color: var(--text-muted);
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
line-height: 1;
}
.modal-close-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px 16px;
border-top: 1px solid var(--border-color);
flex-shrink: 0;
}
.modal-field-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 8px;
}
.modal-field-row {
display: flex;
gap: 8px;
}
.modal-text-input {
flex: 1;
padding: 10px 12px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 10px;
color: var(--text-primary);
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}
.modal-text-input:focus { border-color: var(--accent); }
.modal-btn-primary {
padding: 9px 20px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.modal-btn-primary:hover { background: var(--accent-hover); }
.modal-btn-secondary {
padding: 9px 20px;
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.modal-btn-secondary:hover { background: var(--bg-secondary); }
.modal-loading, .modal-empty {
text-align: center;
color: var(--text-muted);
padding: 32px 0;
font-size: 14px;
}
/* === Import Session List === */
.import-group {
margin-bottom: 20px;
}
.import-group:last-child { margin-bottom: 0; }
.import-group-title {
font-size: 12px;
font-weight: 700;
color: var(--text-muted);
padding: 4px 0 8px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 8px;
word-break: break-all;
}
.import-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 4px;
border-bottom: 1px solid var(--bg-tertiary);
}
.import-item:last-child { border-bottom: none; }
.import-item-info {
flex: 1;
min-width: 0;
}
.import-item-title {
font-size: 14px;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 2px;
}
.import-item-meta {
font-size: 12px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.import-item-btn {
flex-shrink: 0;
padding: 6px 14px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.import-item-btn:hover { background: var(--accent-hover); }