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,