fix: 코드앱 모드별 상태 분리 마무리 + 경로 처리 버그 2건

앞선 커밋(df56376)에서 프로젝트 목록만 모드별로 갈랐는데, 재점검하니
모드를 타야 할 상태가 둘 더 남아 있었다.

  code_proj_session_ / code_proj_history_
      프로젝트 **이름만**으로 키를 만들어서, 같은 이름이 양쪽에 있으면
      (로컬에도 서버에도 `solar`) 채팅 세션과 히스토리가 한 통으로 섞였다.
      서버 모드에만 `@srv:`를 끼운다 — 프로젝트 이름은 [a-zA-Z0-9가-힣_-]로
      정제되어 `:`가 못 들어가므로 이름 충돌이 원천 불가능하다.
      `_server_` 같은 접두사는 `server_solar`라는 로컬 프로젝트와 부딪힌다.
      키 접두사는 그대로라 전체 삭제 스캔은 계속 동작한다.

  csb_open_dirs
      트리 펼침 상태. 두 모드는 디렉토리 구성이 달라서 공유하면 한쪽에서
      펼쳐둔 경로가 다른 쪽에서 엉뚱하게 적용된다.

에디터 설정·테마·토큰 15개는 공유가 맞아서 그대로 뒀다.

경로 버그 2건:

  1) `f.name.replace(activeProjectName + '/', '')`는 String 인자를 받으면
     위치를 안 가리고 **첫 등장**을 지운다. 프로젝트가 `solar`일 때
     `src/solar/main.py`가 `src/main.py`로 뭉개졌다. startsWith로 앞부분만
     떼는 _clientBareFileName으로 교체.
  2) 모드 판정에 localDirHandle을 쓰던 곳 — 로컬 폴더를 열어둔 채 서버
     모드로 바꾸면 핸들이 살아 있어 로컬 분기로 잘못 빠진다. 모드와 핸들이
     독립인 지금은 실제로 재현되는 조건이다. codeIsServerMode()로 교체.

모델에게 보내는 파일 목록 라벨이 서버 모드에서도 "[로컬 파일 …]"로 나가던
것도 고쳤다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-08-18 11:37:16 +09:00
co-authored by Claude Opus 5
parent df56376ded
commit 3a8438d7a7
+31 -14
View File
@@ -315,8 +315,8 @@ async function restoreLocalDirHandle() {
updateLocalModeUI();
// Restore per-project AI session
if (activeProjectName) {
const savedId = localStorage.getItem('code_proj_session_' + activeProjectName);
const savedHist = localStorage.getItem('code_proj_history_' + activeProjectName);
const savedId = localStorage.getItem(_projStoreKey('session', activeProjectName));
const savedHist = localStorage.getItem(_projStoreKey('history', activeProjectName));
if (savedId) codeAiSessionId = savedId;
try { if (savedHist) codeAiHistory = JSON.parse(savedHist); } catch {}
// 채팅 패널 렌더링 (switchProject와 동일)
@@ -2778,7 +2778,9 @@ async function _codeAiSendImpl() {
if (activeProjectName && f.name.includes('/')) {
const isCurrentProjPrefix = f.name.startsWith(activeProjectName + '/');
if (!isCurrentProjPrefix) {
if (!localDirHandle) { continue; } // 서버 모드: 프로젝트 prefix 없으면 다른 프로젝트
// 모드 판정은 codeIsServerMode()로 한다. localDirHandle로 갈랐더니, 로컬 폴더를
// 열어둔 채 서버 모드로 바꾸면 핸들이 살아 있어서 로컬 분기로 잘못 빠졌다.
if (codeIsServerMode()) { continue; } // 서버 모드: 프로젝트 prefix 없으면 다른 프로젝트
// 로컬 모드: 첫 세그먼트가 알려진 다른 프로젝트 이름이면 제외
const firstSeg = f.name.split('/')[0];
const knownProjects = _knownProjectsGet();
@@ -2789,7 +2791,10 @@ async function _codeAiSendImpl() {
const bare = f.name.split('/').pop() || '';
if (SKIP_NAMES.test(bare)) continue;
seen.add(f.name);
const rel = activeProjectName ? f.name.replace(activeProjectName + '/', '') : f.name;
// String.replace는 문자열 인자를 받으면 **위치를 안 가리고 첫 등장**을 지운다.
// `src/solar/main.py`에서 프로젝트가 `solar`면 `src/main.py`가 되어 경로가 망가졌다.
// _clientBareFileName은 startsWith로 앞부분만 뗀다.
const rel = _clientBareFileName(f.name);
const content = f.model ? f.model.getValue() : f.content;
if (!content) continue;
addFile(rel, content);
@@ -2798,7 +2803,7 @@ async function _codeAiSendImpl() {
for (const [cachedName, cachedContent] of Object.entries(_readFileCache)) {
const currentBare = currentFileName ? _clientBareFileName(currentFileName) : '';
if (cachedName === currentBare) continue;
if (seen.has(cachedName) || seen.has(activeProjectName + '/' + cachedName)) continue;
if (seen.has(cachedName) || (activeProjectName && seen.has(activeProjectName + '/' + cachedName))) continue;
seen.add(cachedName);
addFile(cachedName, cachedContent);
}
@@ -2817,7 +2822,7 @@ async function _codeAiSendImpl() {
})
.map(f => activeProjectName ? f.slice(activeProjectName.length + 1) : f);
if (manifest.length)
otherFilesCtx += `[로컬 파일 (Read 툴로 요청 가능): ${manifest.join(', ')}]\n\n`;
otherFilesCtx += `[${codeIsServerMode() ? '서버' : '로컬'} 파일 (Read 툴로 요청 가능): ${manifest.join(', ')}]\n\n`;
} catch {}
}
}
@@ -4548,10 +4553,19 @@ function codeAiScrollBottom() {
}
// ── Project session management ────────────────────────────────────────────
// 프로젝트별 채팅 세션·히스토리 키도 저장소 모드로 갈라야 한다. 예전엔 프로젝트 **이름만**으로
// 키를 만들어서, 같은 이름이 양쪽에 있으면(예: 로컬에도 서버에도 `solar`) 대화가 한 통으로
// 섞였다. 서버 모드에만 `@srv:`를 끼운다 — 프로젝트 이름은 [a-zA-Z0-9가-힣_-]로 정제되므로
// `:`가 들어갈 수 없어 이름 충돌이 원천 불가능하다(`_server_` 같은 접두사는 `server_solar`
// 라는 로컬 프로젝트와 부딪힌다).
// 접두사 `code_proj_session_`/`code_proj_history_`는 그대로라, 전체 삭제 스캔은 계속 동작한다.
function _projStoreKey(kind, name) {
return `code_proj_${kind}_${codeIsServerMode() ? '@srv:' : ''}${name}`;
}
function saveProjectSession(projectName) {
if (!projectName) return;
localStorage.setItem('code_proj_session_' + projectName, codeAiSessionId);
try { localStorage.setItem('code_proj_history_' + projectName, JSON.stringify(codeAiHistory.slice(-40))); } catch {}
localStorage.setItem(_projStoreKey('session', projectName), codeAiSessionId);
try { localStorage.setItem(_projStoreKey('history', projectName), JSON.stringify(codeAiHistory.slice(-40))); } catch {}
}
// 알려진 프로젝트 목록은 **저장소 모드별로 따로** 둔다.
@@ -4591,8 +4605,8 @@ async function deleteActiveProject() {
const name = activeProjectName;
// localStorage 정리
_knownProjectsRemove(name);
localStorage.removeItem('code_proj_session_' + name);
localStorage.removeItem('code_proj_history_' + name);
localStorage.removeItem(_projStoreKey('session', name));
localStorage.removeItem(_projStoreKey('history', name));
// 프로젝트 폴더 삭제 (서버/로컬 공통)
await projectRemoveDir(name);
// 탭 닫기 및 프로젝트 초기화
@@ -4617,8 +4631,8 @@ function switchProject(name) {
localStorage.setItem(_activeProjectKey(), activeProjectName || '');
if (activeProjectName) {
_knownProjectsAdd(activeProjectName);
const savedId = localStorage.getItem('code_proj_session_' + activeProjectName);
const savedHist = localStorage.getItem('code_proj_history_' + activeProjectName);
const savedId = localStorage.getItem(_projStoreKey('session', activeProjectName));
const savedHist = localStorage.getItem(_projStoreKey('history', activeProjectName));
codeAiSessionId = savedId || ('code_ai_' + Math.random().toString(36).slice(2, 10));
try { codeAiHistory = savedHist ? JSON.parse(savedHist) : []; } catch { codeAiHistory = []; _prevTurnDoneTools = []; _readFileCache = {}; _historySummary = ''; }
_readFileCache = {}; _historySummary = '';
@@ -5841,11 +5855,14 @@ async function codeSbRefresh() {
}
}
// 트리 펼침 상태도 모드별로 둔다 — 두 모드는 디렉토리 구성이 서로 달라서, 공유하면
// 한쪽에서 펼쳐둔 경로가 다른 쪽에서 엉뚱하게 적용되거나 아무 데도 안 맞는다.
const _csbOpenDirsKey = () => codeIsServerMode() ? 'csb_open_dirs_server' : 'csb_open_dirs';
function csbGetOpenDirs() {
try { return new Set(JSON.parse(localStorage.getItem('csb_open_dirs') || '[]')); } catch { return new Set(); }
try { return new Set(JSON.parse(localStorage.getItem(_csbOpenDirsKey()) || '[]')); } catch { return new Set(); }
}
function csbSaveOpenDirs(set) {
try { localStorage.setItem('csb_open_dirs', JSON.stringify([...set])); } catch {}
try { localStorage.setItem(_csbOpenDirsKey(), JSON.stringify([...set])); } catch {}
}
function csbToggleDir(id) {
const el = document.getElementById(id);