Code Explorer Diff 뷰 설계
Code Explorer Diff 뷰 설계
- 작성일: 2026-05-28
- 대상: VibeLign GUI Code Explorer (read-only 소스 뷰어, v2.2.19+)
- 목표: 뷰어에서 수정된 코드(녹색)/기존 코드(빨강)를 unified inline diff로 표시 (Cursor/Antigravity 스타일)
1. 배경
현재 Code Explorer는 CodeFileViewer → CodeLine으로 파일 내용을 줄 단위
평면 렌더링하는 순수 읽기 전용 뷰어다. diff 기능이 없어 “마지막 커밋 이후
무엇이 바뀌었는지”를 뷰어 안에서 볼 수 없다.
Rust 백엔드(code_access.rs)에는 이미 경로 정규화·보안 가드와
read_code_file_under가 있고, git을 std::process::Command::new("git")로
호출하는 패턴(onboarding/mod.rs, commands/project_summary.rs)도 존재한다.
VibeLign은 git과 독립적으로 .vibelign/checkpoints/<ts>_<msg>/files/...에
파일 전체 스냅샷을 저장한다.
2. 핵심 결정 (확정)
| 항목 | 결정 |
|---|---|
| diff 기준선(baseline) | 계층형: Git HEAD → 최신 VibeLign checkpoint → 없음 |
| 렌더링 스타일 | Unified inline (제거=빨강 배경, 추가=녹색 배경, ± 마커, old/new 2단 줄번호 gutter) |
| diff 계산 위치 | Rust 백엔드 (baseline 확보 + 보안 가드가 이미 Rust에 있음) |
| diff 알고리즘 | Rust similar 크레이트 (line-level) |
| 토글 기본값 | baseline 존재 + 변경 있음 → 자동 ON, 아니면 평면 뷰 |
3. 아키텍처
3.1 신규 Tauri 커맨드 read_code_file_diff(root, path)
처리 흐름:
1. code_access 가드로 path 정규화·검증 (기존 normalize_relative_input /
parent-escape / ignored-dir / 확장자 가드 재사용)
2. 현재 파일 내용 읽기 (read_code_file_under 경로 재사용)
3. baseline 확보 — 계층형:
a. git rev-parse --is-inside-work-tree 성공 AND `git ls-files --error-unmatch path` 성공
→ `git -C root show HEAD:<path>` ⇒ baseline_source = "git"
b. 위 실패 시 → 최신 .vibelign/checkpoints/<ts>/files/<path> 존재하면 읽기
→ baseline_source = "checkpoint"
c. 둘 다 없음 → baseline 없음 ⇒ baseline_source = "none"
4. baseline 있으면 similar로 line diff 계산, 없으면 전체를 context로 반환
git 추적 여부 확인에
git ls-files --error-unmatch를 쓰는 이유: 신규 (untracked) 파일은 HEAD에 없으므로git show HEAD:path가 실패한다. 이 경우 git baseline을 건너뛰고 checkpoint fallback으로 내려간다.
3.2 반환 shape (serde 직렬화)
{
"path": "src/foo.ts",
"language": "TypeScript",
"baseline_source": "git", // "git" | "checkpoint" | "none"
"added": 3,
"removed": 1,
"lines": [
{ "kind": "context", "old_no": 12, "new_no": 12, "text": "const x = 1;" },
{ "kind": "removed", "old_no": 13, "new_no": null, "text": "return old(x);" },
{ "kind": "added", "old_no": null, "new_no": 13, "text": "return next(x);" }
]
}
kind: context(동일) | added(추가, 녹색) | removed(제거, 빨강).
old_no/new_no는 해당 쪽에 존재할 때만 값을 가진다.
3.3 baseline_source = “none” 조건
- git 저장소가 아님 + checkpoint 없음
- 또는 git/checkpoint 어디에도 해당 경로가 없는 신규 파일
이 경우 lines는 전부 context로 채워 평면 뷰와 동일하게 보이고,
프론트의 Diff 토글은 비활성화된다.
4. 프론트엔드 컴포넌트
4.1 신규 DiffLine.tsx
- props:
kind,oldNo?,newNo?,text - 배경색: added=녹색, removed=빨강, context=기본
-
좌측 2단 gutter(old 줄번호 new 줄번호) + +/-/마커 - 색상은 기존 디자인 토큰/CSS 변수에 맞춰 다크 테마와 조화
4.2 CodeFileViewer 확장
- 신규 prop
diffMode: boolean diffMode === true:read_code_file_diff결과를DiffLine[]으로 렌더diffMode === false: 기존CodeLine평면 뷰 유지 (회귀 없음)
4.3 뷰어 헤더 토글
- 헤더에 “Diff” 토글 버튼 +
+N −M뱃지 baseline_source === "none"→ 토글 비활성 + tooltip “비교할 기준선이 없습니다”- 기본값: baseline 존재 + (added+removed > 0) → 자동 ON, 아니면 OFF(평면)
4.4 상태 관리 (CodeExplorer.tsx)
- 파일 선택 시
read_code_file_diff호출(또는 기존read_code_file와 통합) diffMode상태 보관, 파일 전환 시 위 기본값 규칙으로 재계산- 로딩/에러 처리는 기존
selectedFile패턴 답습
5. 데이터 흐름
CodeExplorer (page)
└ 파일 선택 → invoke("read_code_file_diff", {root, path})
└ Rust: 가드 검증 → 현재 읽기 → baseline(git|checkpoint|none) → similar diff
← { baseline_source, added, removed, lines[] }
└ diffMode 기본값 결정 → CodeFileViewer(diffMode)
└ diffMode ? lines.map(DiffLine) : CodeLine 평면 뷰
6. 보안
- baseline 읽기는 사용자 노출 relpath를 기존 가드로 먼저 검증한 뒤 사용.
- checkpoint의 경우 내부적으로만
.vibelign/checkpoints/<ts>/files/<relpath>로 매핑..vibelign는 explorer ignored-dir이지만 이 매핑은 정규화된 relpath만 결합하므로 경로 탈출(.., 절대경로, 심볼릭 링크 탈출)이 불가능하다. git show HEAD:<path>도 정규화된 relpath만 인자로 사용.- 확장자 allowlist(
.swift포함)·크기 캡 등 기존 가드는 현재 파일 읽기 경로에서 그대로 적용된다.
7. 의존성
vibelign-gui/src-tauri/Cargo.toml에similar = "2"추가 (+ Cargo.lock 갱신).- 직접 LCS 구현 대안도 있으나 정확도/유지보수상
similar채택.
8. 테스트
code_access.rs 테스트 스타일(TempDir 기반)을 따른다:
- git baseline: temp repo
git init→commit→파일 수정 → 커맨드 호출 시 removed/added 라인이 기대대로 나오고baseline_source == "git". - checkpoint baseline: git 없는 디렉토리 +
.vibelign/checkpoints/<ts>/files/에 원본 사본 배치 →baseline_source == "checkpoint", diff 정상. - baseline 없음: 신규 파일(git/checkpoint 모두 없음) →
baseline_source == "none",lines전부context,added == 0및removed == 0. - 경로 가드: parent-escape·절대경로·ignored-dir 입력이 diff 커맨드에서도 거부.
.swift등 지원 확장자에서 diff 동작.
프론트는 필요 시 DiffLine 렌더 스냅샷/단위 테스트(기존 vitest 셋업) 추가.
9. 범위 밖 (YAGNI)
- side-by-side 분할 뷰 (단일 컬럼 unified로 충분)
- word/char-level intra-line diff (line-level만)
- 임의 두 커밋/체크포인트 선택 비교 UI (기준선은 자동 1순위만)
- diff 편집/스테이징/되돌리기 (읽기 전용 유지)