diff --git a/CLAUDE.md b/CLAUDE.md index e25e16e..e8dd187 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ cc-statusline에 포함됐던 로컬 diff 뷰어를 독립 제품으로 분리 + 뷰어 토글(untracked 포함·watch 자동갱신·flatten·파일트리 좌우·unified/split·파일트리 숨김·트리 접기 동기화)은 `--untracked`/`--watch`/`--no-flatten`/`--tree-right`/`--split`/`--hide-tree`/`--fold-with-tree` CLI 플래그로 구동 시점에 미리 설정할 수 있다(session-only — 저장된 localStorage 프리퍼런스는 건드리지 않음). 인앱 토글의 초기 표시 상태는 항상 실제 launch 값과 일치하도록 sync되며, 우선순위 계산(URL 파라미터 → localStorage → 기본값)은 `apps/viewer/browser/prefs.ts`의 순수 resolver 함수(`resolveUntracked`/`resolveWatch`/`resolveFlatten`/`resolveTreeSide`/`resolveDiffStyle`/`resolveTreeHidden`/`resolveFoldWithTree`)로 분리해 단위 테스트한다. unified↔split 전환은 읽던 스크롤 위치를 그대로 유지한다(엔진 앵커링 — 아래 "CodeView 인스턴스 수명" 항목 참고). 파일트리 숨김은 `localStorage` 폴백이 없는 session-only 토글(`resolveUntracked`와 동일 패턴)로, 툴바 아이콘 버튼(`#tree-toggle-btn`)과 오버플로 메뉴 체크박스(`#toggle-tree-hidden`) 둘 다에서 조작 가능하며 항상 서로 동기화된다. 사이드바에서 디렉토리를 접으면 diff 화면의 해당 파일들도 자동으로 접히는 "Fold with tree" 토글은 `flatten`/`treeSide`와 동일하게 `localStorage`(`cc-statusline:fold-with-tree`)에 영속화되며, 오버플로 메뉴 체크박스(`#toggle-fold-with-tree`)로만 조작한다(전용 툴바 버튼 없음). 트리에서 접힌 디렉토리 아래 파일을 diff 헤더 클릭으로 개별 펼치면 그 파일은 사용자가 다시 접기 전까지(토글 on/off와 무관하게) 계속 펼쳐진 채 유지된다. 파일 트리 사이드바 폭은 `#tree-resizer`를 드래그하거나(포커스 후 방향키로도 10px 단위 조정 가능) 180~600px 범위에서 조정할 수 있으며, 조정한 폭은 `localStorage`(session-only 토글들과 달리 지속)에 저장된다. -"Grab" 기능은 diff의 특정 구간을 프롬프트와 함께 클립보드로 복사해 AI 에이전트에게 바로 붙여넣을 수 있게 한다. 진입 경로는 둘: ① 엔진 거터의 GitHub식 라인 선택 + 상주 "+" 버튼(CodeView의 `enableLineSelection`/`enableGutterUtility` 옵션으로 활성화), ② 코드 텍스트 드래그 릴리스 시 즉시 열리는 프롬프트 팝오버. 둘 다 `#grab-popover` 프롬프트 입력창을 열고, Enter로 "참조(파일 경로·상태·base)+스니펫+프롬프트"를 한 텍스트로 인코딩해 클립보드에 복사한다. `enableLineSelection`/`enableGutterUtility`는 전역 활성화라 모든 라인 호버에 "+"가 뜨고 find 매치 라인에도 상주하는데, 이는 의도된 UX 변화다. 텍스트 경로는 pointerup에서 "실제 드래그였는지"부터 게이트한다 — pointerDown→pointerup 이동 거리가 `DRAG_THRESHOLD`(6px)를 넘어야 하며(헤더 폴드 토글과 동일한 `movedBeyondThreshold` 관례), 이 게이트가 없으면 더블/트리플클릭의 네이티브 단어·문단 선택(마우스 이동 없이도 비어있지 않은 Selection을 만든다)이 곧장 팝오버를 열어버린다. 통과하면 스냅샷 의미론: `pointerup` 시점(한 틱 뒤)에 선택·파일·스니펫을 전부 고정해 이후 워커 하이라이트 DOM 교체나 recycle이 선택을 죽여도 안전하고, 팝오버는 스크롤로는 닫히지 않으며 Esc·팝오버 바깥 pointerdown·`renderPatch`(파일 목록 갱신) 발생 시, 그리고 복사 성공 후 1.2초 뒤 자동으로 닫힌다. 선택 소유권: 팝오버가 엔진 라인 선택(`codeView.selectedLines` — `data-selected-line`을 낳는 슬롯)을 "소유"한 경우에만 — 즉 거터 "+" 경로(`onGutterUtilityClick`)로 열렸을 때만 — 닫히거나 복사가 성공할 때 `codeView.clearSelectedLines()`로 그 선택을 해제한다(`onGutterUtilityClick`이 세우는 `grabOwnsLineSelection` 플래그를 `onCopied`/`onClosed`가 공통으로 가드). 텍스트 드래그 경로는 네이티브 브라우저 Selection만 읽을 뿐 이 슬롯을 건드리지 않으므로 플래그를 세우지 않는다 — 무조건 해제했다면 find 바가 `revealMatch`/`selectMatch`로 같은 슬롯에 세워 둔 매치 하이라이트가 텍스트 경로 팝오버의 Esc나 복사 성공만으로 지워지고, 팝오버를 연 적 없는 "파킹된"(드래그만 하고 아직 "+"를 안 누른) 거터 선택도 `renderPatch`(팝오버가 열렸든 아니든 진입부에서 항상 `close()`를 호출)마다 지워지는 회귀가 생긴다. 거터 경로에서 스테일 선택이 남으면 안 되는 이유: 엔진의 `InteractionManager.placeUtility()`는 활성 선택이 있으면 호버를 무시하고 "+"를 선택 하단 행에 고정하며(그 행이 더 이상 렌더 대상이 아니면 아예 숨김) 이후 다른 행 호버에서도 "+"가 뜨지 않게 된다. 텍스트 경로 팝오버가 열리면(`input.focus()`) 네이티브 드래그 선택은 붕괴한다 — 포커스가 문서 선택을 팝오버 input으로 **옮기는** 것이라(실측: `getComposedRanges`가 `#grab-popover`의 자식을 가리킨다) 페인팅만 멈추는 게 아니고, 강제로 되돌려도 첫 타이핑에 다시 붕괴한다. 그래서 잡은 라인은 **grab 하이라이트**가 대신 보여준다: `unsafeCSS`로 넣은 `::highlight(diffdeck-grab)`(파랑) + `CSS.highlights` 레지스트리로 칠하는 독립 채널이다. 앱이 shadow root에 직접 `