From 86d044f8a90656a5b72988f59e94e46787503d60 Mon Sep 17 00:00:00 2001 From: shiyue Date: Mon, 30 Mar 2026 04:46:13 +0800 Subject: [PATCH] feat: confirm creating missing session cwd --- public/app.js | 92 +++++++++++++++++++++++++++++++++++++++++-- scripts/regression.js | 11 ++++++ server.js | 64 ++++++++++++++++++++++++++++-- 3 files changed, 160 insertions(+), 7 deletions(-) diff --git a/public/app.js b/public/app.js index 8c48d86..c7aff10 100644 --- a/public/app.js +++ b/public/app.js @@ -102,6 +102,7 @@ let currentSessionRunning = false; let fileBrowserState = null; let directoryPickerState = null; + let pendingNewSessionRequest = null; let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1'; let pendingInitialSessionLoad = false; @@ -1747,6 +1748,7 @@ break; case 'session_info': + if (pendingNewSessionRequest) pendingNewSessionRequest = null; const snapshot = normalizeSessionSnapshot(msg); if (activeSessionLoad?.sessionId === msg.sessionId) { activeSessionLoad.snapshot = snapshot; @@ -1902,6 +1904,39 @@ break; case 'error': + if (msg.code === 'new_session_cwd_missing' && pendingNewSessionRequest?.cwd) { + const request = pendingNewSessionRequest; + pendingNewSessionRequest = null; + showSimpleConfirm({ + title: '目录不存在', + message: `${msg.cwd || request.cwd}\n\n要先创建这个目录再进入新会话吗?`, + confirmText: '创建目录', + cancelText: '返回修改', + onConfirm: () => { + pendingNewSessionRequest = { ...request }; + send({ + type: 'new_session', + cwd: request.cwd, + agent: request.agent, + mode: request.mode, + createCwd: true, + }); + }, + onCancel: () => { + showNewSessionModal({ + agent: request.agent, + cwd: request.rawCwd || request.cwd, + mode: request.mode, + }); + }, + }); + clearSessionLoading(); + if (!isGenerating && currentSessionId) { + setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning); + } + break; + } + if (pendingNewSessionRequest) pendingNewSessionRequest = null; appendError(msg.message); clearSessionLoading(); if (!isGenerating && currentSessionId) { @@ -2732,6 +2767,47 @@ return '删除本会话将同步删去本地 Claude 中的会话历史,不可恢复。确认删除?'; } + function showSimpleConfirm(options = {}) { + const overlay = document.createElement('div'); + overlay.className = 'settings-overlay'; + overlay.style.zIndex = '10002'; + + const box = document.createElement('div'); + box.className = 'settings-panel'; + const title = options.title ? `
${escapeHtml(options.title)}
` : ''; + const confirmText = options.confirmText || '确认'; + const cancelText = options.cancelText || '取消'; + + box.innerHTML = ` + ${title} +
${escapeHtml(options.message || '')}
+
+ + +
+ `; + overlay.appendChild(box); + document.body.appendChild(overlay); + + const close = () => { + if (overlay.parentNode) overlay.parentNode.removeChild(overlay); + }; + box.querySelector('#simple-confirm-ok').addEventListener('click', () => { + close(); + if (typeof options.onConfirm === 'function') options.onConfirm(); + }); + box.querySelector('#simple-confirm-cancel').addEventListener('click', () => { + close(); + if (typeof options.onCancel === 'function') options.onCancel(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + close(); + if (typeof options.onCancel === 'function') options.onCancel(); + } + }); + } + function showDeleteConfirm(agent, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'settings-overlay'; @@ -4639,10 +4715,11 @@ // --- New Session Modal --- let _onCwdSuggestions = null; - function showNewSessionModal() { - const targetAgent = currentAgent; + function showNewSessionModal(options = {}) { + const targetAgent = normalizeAgent(options.agent || currentAgent); const targetLabel = AGENT_LABELS[targetAgent] || AGENT_LABELS.claude; const recentCwds = getRecentCwds(); + const requestedMode = ['default', 'plan', 'yolo'].includes(options.mode) ? options.mode : currentMode; let suggestionsRequested = false; let suggestionState = { defaultPath: '', @@ -4689,7 +4766,7 @@ const createBtn = overlay.querySelector('#ns-create-btn'); const pickDirBtn = overlay.querySelector('#ns-pick-dir-btn'); - cwdInput.value = recentCwds[0] || ''; + cwdInput.value = String(options.cwd || recentCwds[0] || '').trim(); function getMergedCwdSuggestions() { const seen = new Set(); @@ -4759,9 +4836,16 @@ function createSession() { const cwd = getEffectiveCwd(); + const rawCwd = cwdInput.value.trim(); + pendingNewSessionRequest = { + cwd, + rawCwd, + agent: targetAgent, + mode: requestedMode, + }; close(); if (cwd) saveRecentCwd(cwd); - send({ type: 'new_session', cwd, agent: targetAgent, mode: currentMode }); + send({ type: 'new_session', cwd, agent: targetAgent, mode: requestedMode }); } pickDirBtn.addEventListener('click', () => { diff --git a/scripts/regression.js b/scripts/regression.js index 2ffc0e8..8955082 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -353,6 +353,17 @@ async function main() { const defaultCodexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.title === 'New Chat'); assert(defaultCodexSession.cwd === homeDir, 'Codex new_session without cwd should default to HOME'); + const missingCwd = path.join(tempRoot, 'missing-space', 'nested-project'); + ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: missingCwd, mode: 'plan' })); + const missingCwdError = await nextMessage(messages, ws, (msg) => msg.type === 'error' && msg.code === 'new_session_cwd_missing'); + assert(missingCwdError.cwd === missingCwd, 'Missing cwd error should return the requested absolute path'); + assert(!fs.existsSync(missingCwd), 'Missing cwd should not be created before explicit confirmation'); + + ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: missingCwd, mode: 'plan', createCwd: true })); + const createdCwdSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === missingCwd); + assert(createdCwdSession.cwd === missingCwd, 'Codex new_session should allow creating a missing cwd'); + assert(fs.existsSync(missingCwd), 'Missing cwd should be created when createCwd is enabled'); + const directoryPayload = await fetchAuthedJson(port, token, `/api/fs/directories?path=${encodeURIComponent(pickerRoot)}`); assert(directoryPayload.currentPath === pickerRoot, 'Directory picker should return requested absolute path'); assert(directoryPayload.defaultPath === homeDir, 'Directory picker should expose HOME as default path'); diff --git a/server.js b/server.js index 1d32915..ab5c2d8 100644 --- a/server.js +++ b/server.js @@ -937,6 +937,58 @@ function getDefaultSessionCwd() { || path.resolve(process.cwd()); } +function resolveSessionCwd(candidate, options = {}) { + if (!candidate || !String(candidate).trim()) { + return { ok: true, path: getDefaultSessionCwd(), created: false }; + } + + const resolvedPath = path.resolve(String(candidate).trim()); + + try { + if (fs.existsSync(resolvedPath)) { + const realPath = fs.realpathSync(resolvedPath); + const stat = fs.statSync(realPath); + if (!stat.isDirectory()) { + return { + ok: false, + code: 'new_session_cwd_invalid', + resolvedPath, + message: '工作目录不是目录,请重新选择。', + }; + } + return { ok: true, path: realPath, created: false }; + } + + if (!options.createMissing) { + return { + ok: false, + code: 'new_session_cwd_missing', + resolvedPath, + message: '工作目录不存在', + }; + } + + fs.mkdirSync(resolvedPath, { recursive: true }); + const createdPath = normalizeExistingDirPath(resolvedPath); + if (!createdPath) { + return { + ok: false, + code: 'new_session_cwd_create_failed', + resolvedPath, + message: '目录已创建,但当前进程无法访问,请检查权限。', + }; + } + return { ok: true, path: createdPath, created: true }; + } catch (err) { + return { + ok: false, + code: options.createMissing ? 'new_session_cwd_create_failed' : 'new_session_cwd_invalid', + resolvedPath, + message: `${options.createMissing ? '创建工作目录失败' : '解析工作目录失败'}: ${err.message}`, + }; + } +} + function collectRecentSessionCwds(limit = 12) { const results = []; const seen = new Set(); @@ -2564,10 +2616,16 @@ function handleNewSession(ws, msg) { const cwd = (msg && msg.cwd) ? String(msg.cwd) : null; const agent = normalizeAgent(msg?.agent); const requestedMode = ['default', 'plan', 'yolo'].includes(msg?.mode) ? msg.mode : 'yolo'; - if (cwd && !normalizeExistingDirPath(cwd)) { - return wsSend(ws, { type: 'error', message: '工作目录不存在或不可访问,请重新选择。' }); + const cwdResult = resolveSessionCwd(cwd, { createMissing: !!msg?.createCwd }); + if (!cwdResult.ok) { + return wsSend(ws, { + type: 'error', + code: cwdResult.code, + cwd: cwdResult.resolvedPath || cwd || null, + message: cwdResult.message, + }); } - const resolvedCwd = normalizeExistingDirPath(cwd) || getDefaultSessionCwd(); + const resolvedCwd = cwdResult.path || getDefaultSessionCwd(); const id = crypto.randomUUID(); const session = { id,