diff --git a/CLAUDE.md b/CLAUDE.md index f4901b0..e25e16e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,20 +10,29 @@ diffdeck/ │ ├── path-store/ # @diffdeck/path-store — 트리 순수 로직 (flatten·sort·projection·store), 18 src, deps 없음 │ ├── theming/ # @diffdeck/theming — 테마 시스템 + 테마 데이터, 14 src + themes/*.json 10개 │ ├── diffs/ # @diffdeck/diffs — CodeView diff 렌더 엔진, 144 src (CodeView.ts 3,563줄) -│ └── trees/ # @diffdeck/trees — FileTree 엔진, 46 src (vanilla 렌더, de-preact 완료) +│ └── trees/ # @diffdeck/trees — FileTree 엔진, 45 src (vanilla 렌더, de-preact 완료) ├── apps/ -│ └── viewer/ # @say8425/diffdeck — server/(데이터 API) + browser/(뷰어 프론트) + cli/(CLI 엔트리) + build.ts·cli.ts +│ └── viewer/ # @say8425/diffdeck — server/(데이터 API) + browser/(뷰어 프론트) + cli/(CLI 엔트리) + build.ts·cli.ts·README.md(npm 페이지) +├── skills/diffdeck/ # SKILL.md — 에이전트가 참조하는 유일한 구동 문서. build.ts가 dist/skills/로 복사한다 +├── docs/ # README 번역 4종(ko·ja·zh·es) + demo.gif·screenshot.png·grab.png +│ # (docs/superpowers/의 plan·spec은 .gitignore — 로컬 전용) ├── scripts/ │ ├── extract-sources.ts # 소스맵 sourcesContent → 원본 TS 복원 도구 (Foundation 일회성) │ ├── extract-sources.test.ts # 합성 소스맵 fixture로 hermetic (외부 체크아웃 의존 없음) │ ├── css-inline-plugin.ts # *.css?inline import용 Bun 플러그인 (런타임/번들러 2분리) -│ └── parity/ # 포크 렌더 패리티 하니스 (fixture·smoke test·main·build) +│ └── parity/ # 포크 렌더 패리티 하니스 (fixture·preload·smoke test·main·build·index.html) +├── .github/workflows/ # pr-check.yml (5잡 — 아직 required 아님, CI 항목 참고) + release.yml (release-please → npm) +├── .claude-plugin/ .codex-plugin/ # 플러그인 매니페스트 — release-please extra-files가 버전을 bump +├── CHANGELOG.md # 레포 루트에 있다 (release-please changelog-path의 앞 슬래시 — CI 항목 참고) +├── bunfig.toml # 테스트 preload + 커버리지 게이트/제외 (테스트 3레인 항목 참고) ├── tsconfig.base.json # 공유 base (jsxImportSource: preact, @diffdeck/* path alias) ├── .oxlintrc.json / .oxfmtrc.json ├── NOTICE # pierre (Apache-2.0) 유래 고지 └── package.json # Bun workspace 루트 (workspaces: packages/*, apps/*) ``` +**위 `N src` 숫자의 정의**: `src/` 아래 **테스트를 뺀 `.ts` 파일 수**다(`src/` 안의 `style.css`는 빠지고, `themes/*.json` 10개는 `src/` 밖이라 애초에 안 세어진다 — 그래서 따로 적었다). 규칙을 박아두는 이유는 예전에 trees만 `style.css`를 포함해 세는 바람에 46으로 적혀 있었기 때문이다. 재계산: `find packages//src -name '*.ts' -not -path '*__tests__*' | wc -l`. 이 숫자들은 게이트가 없으므로 드리프트한다 — 규모의 감을 주는 값으로만 읽고, 정확한 값이 필요하면 위 명령이 답한다. + **기술 스택**: Bun (런타임·번들·테스트·workspace), TypeScript 7 (Go 네이티브 컴파일러), oxlint/oxfmt **패키지 의존 그래프**: @@ -48,7 +57,7 @@ cc-statusline에 포함됐던 로컬 diff 뷰어를 독립 제품으로 분리 + "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에 직접 `