feat: confirm creating missing session cwd
This commit is contained in:
@@ -102,6 +102,7 @@
|
|||||||
let currentSessionRunning = false;
|
let currentSessionRunning = false;
|
||||||
let fileBrowserState = null;
|
let fileBrowserState = null;
|
||||||
let directoryPickerState = null;
|
let directoryPickerState = null;
|
||||||
|
let pendingNewSessionRequest = null;
|
||||||
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
|
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
|
||||||
let pendingInitialSessionLoad = false;
|
let pendingInitialSessionLoad = false;
|
||||||
|
|
||||||
@@ -1747,6 +1748,7 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'session_info':
|
case 'session_info':
|
||||||
|
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
|
||||||
const snapshot = normalizeSessionSnapshot(msg);
|
const snapshot = normalizeSessionSnapshot(msg);
|
||||||
if (activeSessionLoad?.sessionId === msg.sessionId) {
|
if (activeSessionLoad?.sessionId === msg.sessionId) {
|
||||||
activeSessionLoad.snapshot = snapshot;
|
activeSessionLoad.snapshot = snapshot;
|
||||||
@@ -1902,6 +1904,39 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
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);
|
appendError(msg.message);
|
||||||
clearSessionLoading();
|
clearSessionLoading();
|
||||||
if (!isGenerating && currentSessionId) {
|
if (!isGenerating && currentSessionId) {
|
||||||
@@ -2732,6 +2767,47 @@
|
|||||||
return '删除本会话将同步删去本地 Claude 中的会话历史,不可恢复。确认删除?';
|
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 ? `<div style="font-size:1em;font-weight:700;color:var(--text-primary);margin-bottom:10px">${escapeHtml(options.title)}</div>` : '';
|
||||||
|
const confirmText = options.confirmText || '确认';
|
||||||
|
const cancelText = options.cancelText || '取消';
|
||||||
|
|
||||||
|
box.innerHTML = `
|
||||||
|
${title}
|
||||||
|
<div style="font-size:0.9em;color:var(--text-primary);margin-bottom:20px;line-height:1.7;word-break:break-word;white-space:pre-line">${escapeHtml(options.message || '')}</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
<button id="simple-confirm-ok" style="width:100%;padding:10px;border:none;border-radius:10px;background:var(--accent);color:#fff;font-size:0.95em;font-weight:600;cursor:pointer;font-family:inherit">${escapeHtml(confirmText)}</button>
|
||||||
|
<button id="simple-confirm-cancel" style="width:100%;padding:9px;border:none;border-radius:10px;background:transparent;color:var(--text-muted);font-size:0.85em;cursor:pointer;font-family:inherit">${escapeHtml(cancelText)}</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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) {
|
function showDeleteConfirm(agent, onConfirm) {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'settings-overlay';
|
overlay.className = 'settings-overlay';
|
||||||
@@ -4639,10 +4715,11 @@
|
|||||||
// --- New Session Modal ---
|
// --- New Session Modal ---
|
||||||
let _onCwdSuggestions = null;
|
let _onCwdSuggestions = null;
|
||||||
|
|
||||||
function showNewSessionModal() {
|
function showNewSessionModal(options = {}) {
|
||||||
const targetAgent = currentAgent;
|
const targetAgent = normalizeAgent(options.agent || currentAgent);
|
||||||
const targetLabel = AGENT_LABELS[targetAgent] || AGENT_LABELS.claude;
|
const targetLabel = AGENT_LABELS[targetAgent] || AGENT_LABELS.claude;
|
||||||
const recentCwds = getRecentCwds();
|
const recentCwds = getRecentCwds();
|
||||||
|
const requestedMode = ['default', 'plan', 'yolo'].includes(options.mode) ? options.mode : currentMode;
|
||||||
let suggestionsRequested = false;
|
let suggestionsRequested = false;
|
||||||
let suggestionState = {
|
let suggestionState = {
|
||||||
defaultPath: '',
|
defaultPath: '',
|
||||||
@@ -4689,7 +4766,7 @@
|
|||||||
const createBtn = overlay.querySelector('#ns-create-btn');
|
const createBtn = overlay.querySelector('#ns-create-btn');
|
||||||
const pickDirBtn = overlay.querySelector('#ns-pick-dir-btn');
|
const pickDirBtn = overlay.querySelector('#ns-pick-dir-btn');
|
||||||
|
|
||||||
cwdInput.value = recentCwds[0] || '';
|
cwdInput.value = String(options.cwd || recentCwds[0] || '').trim();
|
||||||
|
|
||||||
function getMergedCwdSuggestions() {
|
function getMergedCwdSuggestions() {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -4759,9 +4836,16 @@
|
|||||||
|
|
||||||
function createSession() {
|
function createSession() {
|
||||||
const cwd = getEffectiveCwd();
|
const cwd = getEffectiveCwd();
|
||||||
|
const rawCwd = cwdInput.value.trim();
|
||||||
|
pendingNewSessionRequest = {
|
||||||
|
cwd,
|
||||||
|
rawCwd,
|
||||||
|
agent: targetAgent,
|
||||||
|
mode: requestedMode,
|
||||||
|
};
|
||||||
close();
|
close();
|
||||||
if (cwd) saveRecentCwd(cwd);
|
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', () => {
|
pickDirBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -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');
|
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');
|
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)}`);
|
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.currentPath === pickerRoot, 'Directory picker should return requested absolute path');
|
||||||
assert(directoryPayload.defaultPath === homeDir, 'Directory picker should expose HOME as default path');
|
assert(directoryPayload.defaultPath === homeDir, 'Directory picker should expose HOME as default path');
|
||||||
|
|||||||
64
server.js
64
server.js
@@ -937,6 +937,58 @@ function getDefaultSessionCwd() {
|
|||||||
|| path.resolve(process.cwd());
|
|| 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) {
|
function collectRecentSessionCwds(limit = 12) {
|
||||||
const results = [];
|
const results = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
@@ -2564,10 +2616,16 @@ function handleNewSession(ws, msg) {
|
|||||||
const cwd = (msg && msg.cwd) ? String(msg.cwd) : null;
|
const cwd = (msg && msg.cwd) ? String(msg.cwd) : null;
|
||||||
const agent = normalizeAgent(msg?.agent);
|
const agent = normalizeAgent(msg?.agent);
|
||||||
const requestedMode = ['default', 'plan', 'yolo'].includes(msg?.mode) ? msg.mode : 'yolo';
|
const requestedMode = ['default', 'plan', 'yolo'].includes(msg?.mode) ? msg.mode : 'yolo';
|
||||||
if (cwd && !normalizeExistingDirPath(cwd)) {
|
const cwdResult = resolveSessionCwd(cwd, { createMissing: !!msg?.createCwd });
|
||||||
return wsSend(ws, { type: 'error', message: '工作目录不存在或不可访问,请重新选择。' });
|
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 id = crypto.randomUUID();
|
||||||
const session = {
|
const session = {
|
||||||
id,
|
id,
|
||||||
|
|||||||
Reference in New Issue
Block a user