## 2026-05-04 — 세션 스크린샷 호스트 OS 클립보드 페이스트 (프로젝트 트리에 파일 0)

### 배경

이전엔 세션 터미널에서 스크린샷/이미지를 붙여넣으면 서버가 `<project>/.clitrigger/paste-images/img{N}.<ext>`로 비트맵을 저장한 뒤 그 상대경로를 PTY에 텍스트로 보내 Claude/Codex/Gemini의 path-to-image 디텍터가 픽업하도록 했다. 두 가지 문제가 있었다 — (a) 파일이 사용자 프로젝트 트리 안에 남아서 gitignore가 되어 있어도 IDE 탐색기, OS 인덱서, 파일 다이얼로그에 노출됨, (b) 워크트리/세션 단위 cleanup이 같은 디렉터리를 공유하다 보니 다른 세션이 방금 쓴 이미지를 cleanup이 지워버리는 레이스. 새 흐름은 비트맵을 서버에서 호스트 OS 클립보드로 직접 푸시한 다음 PTY에 ESC+v(Alt+V) 한 시퀀스만 보내서 CLI의 네이티브 클립보드-읽기 핸들러가 자체적으로 `[Image #N]`을 렌더하도록 위임 → 사용자 프로젝트 트리에 디스크 파일 0개.

### 주요 변경

#### 1. 클립보드 쓰기 서비스 (`dcac8db`)

- **`src/server/services/clipboard-writer.ts`** (신규) — `writeImageToClipboard(buffer)` 단일 export. 플랫폼별 분기:
  - **Windows**: PowerShell stdin으로 base64 → `MemoryStream` → `System.Drawing.Image` → `System.Windows.Forms.Clipboard.SetImage`. 메모리에서 끝나고 임시 파일 생성 안 함. `windowsHide: true`로 콘솔 창 노출도 방지.
  - **macOS**: `osascript`가 path 기반 API만 받으므로 `os.tmpdir()`에 짧게 임시 파일을 만들고 (`clitrigger-clip-{ts}-{pid}.png`), `set the clipboard to (read (POSIX file ...) as «class PNGf»)` 실행 직후 `fs.unlinkSync`로 정리. 사용자 프로젝트 트리 경로는 절대 거치지 않음.
  - **Linux**: `WAYLAND_DISPLAY` env가 있으면 `wl-copy --type image/png`을 1순위로 시도하고 실패하면 `xclip -selection clipboard -t image/png -i`로 폴백. X11 세션에서는 순서가 반대. 둘 다 stdin으로 raw bytes 주입.
- **프로세스 단위 mutex**: 모듈 스코프 `let mutex: Promise<void>` 체이닝으로 동시 호출 직렬화. 두 세션이 거의 동시에 paste하면 한쪽 바이트가 다른 쪽이 읽기 전에 클립보드를 덮어쓰는 레이스가 있어서 직렬화는 안전성에 필수.
- **10초 spawn timeout**: PowerShell/osascript/xclip 중 어느 것이라도 응답이 없으면 `SIGKILL` + 에러로 reject.

#### 2. paste-image 라우트 단순화 (`dcac8db`)

- **`POST /api/sessions/:id/paste-image`** — 디스크 쓰기/프로젝트 lookup/디렉터리 순차 넘버링/`workDir` 결정 로직 모두 제거. data URL 파싱 + 10MB 가드만 남기고 곧장 `writeImageToClipboard(buffer)` 호출 후 `{ pasted: true }` 응답. 응답 타입이 바뀌었으므로 클라이언트 API 시그니처도 함께 변경 (`Promise<{ path: string }>` → `Promise<{ pasted: true }>`).
- **`DELETE /sessions/:id`, `POST /sessions/:id/cleanup`** — paste-images 디렉터리 cleanup 블록 삭제. 디스크에 파일이 더 이상 안 만들어지므로 cleanup 자체가 불필요.

#### 3. 레거시 paste-images 부팅 cleanup (`dcac8db`)

- **`src/server/index.ts`** — stale-session 정리 직후 1회성 cleanup 추가. 모든 프로젝트의 `<project>/.clitrigger/paste-images/`와 `<project>/.worktrees/*/.clitrigger/paste-images/`를 best-effort로 `fs.rmSync({recursive, force})`. 모든 fs 호출은 try/catch로 swallow되어 권한 거부나 깨진 심볼릭 링크가 부팅을 막지 못함. 구버전에서 새 버전으로 업그레이드한 사용자가 명시적 작업 없이 기존 파일이 자연스럽게 사라지도록 의도.

#### 4. 클라이언트 키스트로크/브라우저 paste 핸들러 (`dcac8db`)

- **`src/client/src/components/SessionTerminal.tsx`** — 두 paste 진입점(키보드 입력 시점의 `clipboardData` 처리, 브라우저 `paste` 이벤트 처리)이 모두 동일한 흐름:
  1. `pasteImage(sessionId, dataUrl)` 호출 — 서버가 OS 클립보드에 비트맵 push 끝낼 때까지 await
  2. 성공하면 `sessionId` + `input: '\x1bv'`으로 `session:terminal-input` 전송 — `ESC+v`는 터미널이 보내는 Alt+V의 raw 시퀀스라 Claude/Codex/Gemini가 자체 paste 핸들러를 트리거
- 기존엔 응답으로 받은 상대경로를 PTY에 그대로 텍스트로 흘렸음 (CLI의 path-to-image 디텍터에 의존). 새 흐름은 path가 PTY에 입력되지 않으므로 사용자가 입력 중이던 프롬프트가 의도치 않게 깨질 가능성도 줄어듬.
- **`src/client/src/api/sessions.ts`** — `pasteImage(...)` 응답 타입을 `{ path: string }` → `{ pasted: true }`로 갱신.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/server/services/clipboard-writer.ts` | NEW — Win32/macOS/Linux 3-플랫폼 클립보드 writer + mutex + 10s timeout |
| `src/server/routes/sessions.ts` | paste-image 라우트가 클립보드 쓰기로 교체, 응답 `{pasted:true}`. delete/cleanup의 paste-images 정리 블록 삭제 |
| `src/server/index.ts` | 부팅 시 레거시 `<project>/.clitrigger/paste-images/` + 워크트리 동일 경로 1회 cleanup |
| `src/client/src/components/SessionTerminal.tsx` | 키스트로크 & 브라우저 paste 핸들러가 업로드 후 `\x1bv` (Alt+V) 전송 (받은 path 대신) |
| `src/client/src/api/sessions.ts` | `pasteImage` 응답 타입 `{path}` → `{pasted: true}` |

### 아키텍처 결정

1. **CLI path 디텍터에 의존 vs OS 클립보드 위임**: 이전 흐름은 CLI마다 path-to-image 디텍터가 동작 방식이 다르고(절대/상대, 인용 부호, 줄바꿈 처리), 사용자 프롬프트와 path가 같은 PTY 입력 줄에 섞이는 케이스가 있었다. 클립보드를 매개체로 두면 CLI가 어댑터별 차이 없이 자체 paste UI를 트리거하게 되어 어댑터 분기/유지보수가 줄고, PTY에 들어가는 건 단 1바이트 시퀀스 (`\x1bv`)뿐이라 사용자 입력과 충돌하지 않는다.
2. **프로세스 단위 promise-chain mutex**: OS 클립보드는 단일 리소스라 두 세션이 동시에 paste 요청을 보내면 한쪽 바이트가 다른 쪽 CLI가 읽기 전에 덮여 쓰일 수 있다. queue/lib 추가 없이 모듈 스코프 `let mutex` 체이닝(이전 promise를 await한 뒤 다음 promise를 mutex로 노출)으로 정확히 직렬화. spawn 자체는 비동기이므로 await만 들어가는 형태로 충분.
3. **macOS 임시 파일은 `os.tmpdir()`만**: osascript의 PNG 클립보드 API가 path-only라 파일 우회가 불가피하지만, 사용자 프로젝트 트리에는 절대 만들지 않고 OS-tmp에만 생성 후 즉시 unlink. tmp 디렉터리는 OS가 알아서 청소하고 IDE/탐색기에 노출되지 않음.
4. **부팅 cleanup은 best-effort, swallow-all**: 마이그레이션 흔적 정리는 부가적인 친절함이지 필수 동작이 아니므로 어떤 fs 에러도 boot를 막아선 안 됨. 권한이 막히거나 디렉터리가 없거나 깨진 worktree symlink가 있어도 `try/{ /* ignore */ }`로 흘려보냄.

---
