Skip to content

feat: 그랩 팝오버 ⌥⏎ 단순 복사와 라인넘버 드래그 선택 제거 - #69

Merged
say8425 merged 6 commits into
mainfrom
feat/grab-popover-shortcuts
Aug 29, 2026
Merged

feat: 그랩 팝오버 ⌥⏎ 단순 복사와 라인넘버 드래그 선택 제거#69
say8425 merged 6 commits into
mainfrom
feat/grab-popover-shortcuts

Conversation

@say8425

@say8425 say8425 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

실사용 피드백 4건을 한 표면(grab 팝오버 · 거터 선택)에서 함께 반영하고, 그에 딸린
문서·회귀망 의무까지 닫는다. 설계 근거는 docs/superpowers/specs/2026-08-24-grab-plain-copy-and-gutter-drag-off-design.md(로컬 전용)에 있다.

무엇이 바뀌나

  1. 라인넘버 드래그가 더 이상 줄을 선택하지 않는다. 드래그는 이제 코드 텍스트 선택(그랩)의 제스처다. 클릭 한 줄 선택 · shift클릭 확장 · 재클릭 해제 · "+" 버튼(호버·클릭·"+" 드래그 다중 선택)은 그대로다 — 제거 대상은 드래그 확장만이다.
  2. ⌥⏎(Option/Alt+Enter) = 단순 복사. 프롬프트 · 펜스 · File:/Lines: 머리말 전부 없이 잡은 코드 텍스트만 클립보드로 간다(편집기에 바로 붙여넣는 형식이라 mixed 선택의 +/- 마커도 싣지 않는다). Shift+Enter 개행은 그대로.
  3. 복사 성공 후 자동 닫힘 1200ms → 400ms. "Copied"+초록 체크를 확인시키는 최소 체류만 남긴다.
  4. 하단에 ⌥⏎ Copy code only 각주 상시 고지. .grab-label과 같은 어휘(11px · 정수 행고 15px · --vd-fg-muted)에 키 글리프만 #c2c2ca로 밝힌다.

구현 메모

  • 엔진에 enableLineSelectionDrag?: boolean(기본 true = upstream 동작 유지)을 추가하고 뷰어만 false로 켠다. 핵심은 선택 시점을 옮긴 것이다: pointerdown에서 즉시 선택하던 것을 pendingLineSelect 세션으로 미루고, pointermove가 다른 행으로 건너가면 세션을 취소한다. 같은 행 지터는 무시하고 그대로 놓으면 클릭으로 확정한다 — "드래그의 첫 행이 선택으로 남는" 반쪽 동작이 생기지 않는다. 분기 본문은 extendSelectionFromShiftClick/selectSingleLineFromPoint로 드래그 켠 경로와 공유한다.
  • packages/diffs 수정이므로 Foundation 예외다: [diffdeck] 마커 9곳(InteractionManager 7 · CodeView 2)을 넣고 CLAUDE.md 예외 목록에 5번으로 등재했다.
  • keydown 분기 순서가 계약이다: IME 가드 → altKeyshiftKey → 제출. altKey를 뒤로 미루면 ⌥⏎가 개행으로 새고, IME 가드보다 앞으로 당기면 조합 중 오작동한다.
  • 상시 한 줄이 늘어 POPOVER_SIZE.height 190 → 211(15px 줄 + 6px gap). 이 상수는 open() 때 한 번만 계산되므로 과소 선언하면 화면 아래쪽에서 카드가 잘린다.

Screenshots

dist/cli.js를 실제로 띄워 Playwright로 캡처했다(텍스트 드래그 → 팝오버).

Before (main) After
before.png after.png

Test plan

레인 결과
bun test 703 pass / 0 fail
bun run test:coverage 703 pass / 0 fail, 100% 게이트 통과 (grab/encode.ts·grab/popover.ts 포함)
bun run typecheck 통과
bun run lint 신규 에러 0 (기존 warning만)
bun run format:check 통과
bun run test:e2e 86 passed / 3 failed — 아래 참고

신규 회귀망:

  • 유닛 8종(grab-popover.test.ts·grab-encode.test.ts): Alt+Enter 복사 · 기본동작 취소 · IME 가드 · 400ms 닫힘 · .grab-keys 렌더 · aria/title 고정값 · plainSnippet side/mixed.
  • e2e 3종(grab.e2e.ts): ⑯ 라인넘버 드래그 무선택 + 클릭 선택 유지, ⑰ ⌥⏎ 순수 코드 복사 + 각주 표시, ⑱ 드래그를 끈 거터의 shift클릭 확장 · 재클릭 해제.

⑱은 뮤테이션으로 판별력을 확인했다. commitPendingLineSelect의 shift클릭·재클릭 해제 두 분기를 통째로 지우고 selectSingleLineFromPoint 하나만 남겨도 ⑯·⑰을 포함한 grab e2e 17종이 전부 통과했다. ⑱을 넣은 뒤에야 빨간불이 난다. packages/diffsmarkup-contract.test.ts 하나뿐이라 유닛이 이 경로를 원리적으로 못 잡는 것도 같은 이유다.

알려진 실패 (이 PR과 무관)

e2e 3건(lockfile-freeze · retokenize-cache · worker-highlight)이 로컬에서 실패하는데, 셋 다 실패 지점이 #status"Loading…" 고착이다 — CLAUDE.md에 기록된 기존 Bun $ never-settle 행의 재현이다. main(573ab76)에서 같은 스펙을 돌려도 동일하게 실패하는 것을 확인했으므로 이 브랜치의 회귀가 아니다. 이 브랜치는 server·git 서브프로세스 경로를 전혀 건드리지 않는다.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PaHtxdofJX6EvH7BWXgYKX

say8425 and others added 6 commits August 24, 2026 15:28
enableLineSelectionDrag(기본 true)를 추가한다. false면 라인넘버
pointerdown에서 즉시 선택하는 대신 pendingLineSelect 세션으로 미루고,
pointermove가 다른 행으로 건너가는 순간 세션을 취소한다 — 그 제스처는
드래그고 드래그는 아무것도 선택하지 않는다. 같은 행 안 지터는 무시하고,
그대로 놓으면 클릭으로 확정한다(한 줄 선택·shift클릭 확장·재클릭 해제,
notifySelectionStart/End/Committed 시퀀스까지 기존 pointerdown 경로와
동일 — 분기 본문은 extendSelectionFromShiftClick/
selectSingleLineFromPoint로 공유한다). "+" 버튼 드래그(gutterSelecting)는
그대로다.

기본값이 true라 라이브러리 소비자의 동작은 바뀌지 않는다.
- 뷰어에 enableLineSelectionDrag: false를 적용 — 라인넘버 드래그는 더 이상
  줄 선택을 만들지 않는다. 드래그는 코드 텍스트 선택(그랩)의 제스처다.
- ⌥⏎(Option/Alt+Enter)는 프롬프트·펜스·File:/Lines: 머리말 없이 잡은 코드
  텍스트만 클립보드로 복사한다(plainSnippet). 성공/실패·자동 닫힘은 제출과
  같은 경로를 공유하고 IME 가드도 앞선다. Shift+Enter 개행은 그대로.
- 복사 성공 후 자동 닫힘을 1200ms → 400ms로. 확인("Copied"+초록 체크)으로서
  최소 체류만 남긴다(기존 1200은 copyButton RESET_MS와 맞춘 값이었다).
- 팝오버 하단에 ⌥⏎ Copy code only 각주를 상시 고지한다 — .grab-label과
  같은 어휘(11px·정수 행고·--vd-fg-muted)에 키 글리프만 #c2c2ca로 밝힌다.
  2026-07-30 스펙이 힌트 줄을 지운 건 Enter/Esc 이중 고지 때문이었고, 이번
  각주는 새 액션의 유일한 고지 채널이라 겹침이 없다. 상시 줄이 생겼으므로
  POPOVER_SIZE.height 190 → 211.
- 유닛 8종·e2e 2종(⑯ 드래그 무선택/클릭 유지, ⑰ ⌥⏎ 순수 복사) 추가,
  스펙 문서 작성.
⑯은 "라인넘버 드래그가 아무것도 선택하지 않는다"는 절반만 지킨다. 나머지
절반 — enableLineSelectionDrag:false에서도 클릭 계열 제스처가 드래그 켠
경로와 똑같이 동작한다 — 은 commitPendingLineSelect의 두 분기(shift클릭
확장·재클릭 해제)에 있는데 그 둘을 통째로 지워도 grab e2e 17종이 전부
통과했다(실측). ⑱을 넣은 뒤에야 빨간불이 난다.

선택 개수는 행 수가 아니라 스탬프 수다 — unified는 선택된 행마다
[data-selected-line]을 둘(번호 셀 + 내용 셀) 찍는다(실측). 그래서 한 줄은
2, 세 줄은 6이다. 픽스처의 dragSelect엔 모디파이어 인자가 없어 shift는
스펙에서 keyboard.down/up으로 감싼다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaHtxdofJX6EvH7BWXgYKX
포크 패키지 수정은 [diffdeck] 주석으로 upstream 이탈을 코드에 표기하고
CLAUDE.md 예외 목록에 올려야 하는데, enableLineSelectionDrag는 둘 다
빠져 있었다. InteractionManager에 7곳·CodeView에 2곳 마커를 넣고 예외를
4건 → 5건으로 등재한다(회귀망이 ⑯만으로는 미달인 근거도 함께 적는다).

같은 커밋에서 이번 브랜치가 낡게 만든 CLAUDE.md 서술 5곳을 고친다:
- 진입 경로 ① — 거터는 이제 드래그 확장만 꺼진 상태다
- 자동 닫힘 1.2초 → 0.4초
- 키 분기에 ⌥⏎ 단순 복사와 "IME → altKey → shiftKey → 제출" 순서 계약
- 고지 채널 둘 → 셋. 새 .grab-keys 각주가 "상시 힌트 줄 제거"와 모순이
  아닌 이유와 POPOVER_SIZE 190 → 211
- grab.e2e.ts 15종 → 18종

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaHtxdofJX6EvH7BWXgYKX
⑨는 "복사 전후 창 높이가 같다"(sr-only 힌트가 창을 늘리지 않는다)를 지키는
유일한 회귀망인데 — happy-dom엔 레이아웃이 없어 유닛이 원리적으로 못 잡는다 —
press 뒤에 상태 폴과 boundingBox로 왕복을 두 번 했다. 자동 닫힘이 1200ms일
땐 넉넉했지만 400ms로 줄면서 느린 러너가 그 사이에 팝오버를 닫아 진짜 계약
테스트가 flake가 될 수 있다.

상태와 높이를 한 번의 evaluate로 같은 순간에 읽어 경합을 원리적으로 없앤다.
close()가 data-state를 되돌리지 않아(계속 "ok") 닫힌 뒤에도 폴은 통과하므로
hidden을 함께 실어, 닫혔을 때 "69 → 0"이 아니라 닫혔다고 말하게 한다.
주석의 1.2초도 0.4초로 바로잡는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaHtxdofJX6EvH7BWXgYKX
- [diffdeck] 마커를 4곳 더 붙인다(PointerSession 유니온, has
  ActiveLineSelectionSession, pointerup·pointercancel case, pluck 2곳).
  특히 pointerup이 드래그 없는 선택이 실제로 확정되는 지점인데 upstream
  코드처럼 보였다. 9곳 → 13곳.
- commitPendingLineSelect의 의도적 이탈을 명시한다:
  extendSelectionFromShiftClick이 false를 낼 때 드래그 켠 경로는 통째로
  빠져나가지만 여기선 공통 꼬리가 그대로 돌아 notifySelectionEnd/Committed가
  한 쌍 헛돈다. 이 리포 뷰어는 선택 콜백을 하나도 안 물려 무해하지만,
  기본값 true의 취지가 "라이브러리 소비자 무영향"이므로 적어 둔다.
- 크로스행 취소가 행 인덱스가 아니라 (lineNumber, side)로 키를 잡는다는
  사실을 주석에 적는다 — unified에서 한 행이 old/new 번호 셀을 함께 낼 수
  있어 가로 지터로도 취소된다. 기존 주석은 "다른 행으로 건너가면"이라고만
  적어 이 경우를 설명하지 못했다.
- pointerInfo == null 주석이 "앵커 행으로 돌아와 놓는" 경우만 열거해
  옆으로 빠져나간 드래그도 앵커 행을 확정한다는 사실을 가렸다.
- CLAUDE.md: 팝오버 구성이 여전히 "둘"이라 같은 문단 뒷부분(.grab-keys
  상시 줄, POPOVER_SIZE 190 → 211)과 모순이었다 → 셋.
- CLAUDE.md: "드래그를 ②로 넘긴다"는 기제 주장 완화 — 거터는
  user-select: none이라 거기서 시작한 드래그는 텍스트 선택으로 넘어가지
  않고 그냥 아무 일도 하지 않는다(엔진 주석이 근거).
- main.ts: 이 브랜치가 다시 쓴 주석 블록의 max-height 96px → 90px(실제 CSS).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaHtxdofJX6EvH7BWXgYKX
@say8425
say8425 merged commit eeeff04 into main Aug 29, 2026
5 checks passed
@say8425
say8425 deleted the feat/grab-popover-shortcuts branch August 29, 2026 13:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant