## 2026-05-03 — 위키 원본 md 동시 주입 + 세션 색상 태그/설정/선택적 제목 + 클립보드 단축키 + 도크 재정렬

### 배경

위키 주입은 그동안 LLM이 압축한 큐레이트 노드(`memory_nodes`)만 프롬프트에 흘려보냈다. 인제스트 시점에 `.clitrigger/raw/`로 원본 md를 보존하고 `memory_nodes.source_path`로 링크해 두는 디스크 인프라는 이미 있었지만 프롬프트 경로에서 닿을 수 없어, 노드 큐레이션 과정에서 떨어져 나간 디테일이 필요한 케이스에서 무력했다. 사용자 요청은 "원본 md도 같이 넣고 싶다" — 큐레이트 노드와 **같이/병렬로** 주입할 수 있어야 하지 둘 중 택일이면 곤란. 그래서 todos/discussions/sessions 모두에 raw 파일 선택 컬럼을 추가하고 inject hook이 두 블록을 합쳐 prepend하도록 확장.

세션 쪽은 운영 중에 쌓인 친마찰들을 한꺼번에 정리. 제목 강제 입력, 워크트리 디폴트가 프로젝트 정책과 따로 노는 것, X 버튼이 무음으로 백그라운드만 보내는 것, 비-워크트리 세션의 브랜치 비표시, 색상 태그 부재로 긴 리스트 스캔 어려움 등. xterm.js의 의도적 "no clipboard integration" 정책 때문에 Ctrl/Cmd+C/V/X가 제어문자로 흘러가던 문제도 같이 수정. dev 모드는 `build-electron.bat`이 better-sqlite3를 Electron ABI로 컴파일해 둔 뒤 `dev.bat`에서 깨지는 회귀가 발견돼 자가-치유 가드 보강.

### 주요 변경

#### 1. 위키 원본 md 파일 동시 주입 (`e5edce9`)

큐레이트 노드(`memory_nodes`)와 **병렬**로 raw md 파일을 주입할 수 있게 함. 새로운 모드가 아니라 기존 mode와 직교하는 selector — `mode='none'`이어도 raw 파일이 있으면 주입 발동.

- **DB 스키마**: `todos`/`discussions`/`sessions` 3개 테이블에 `memory_raw_file_paths TEXT` 컬럼 (JSON array of relative paths). `ALTER TABLE` 호환 마이그레이션.
- **`memory-injector.ts`**: 신규 `buildRawFileBlock(projectRoot, paths, opts?)` — `.clitrigger/raw/` 접두사 path-traversal 가드(`absPath.startsWith(rawRoot + sep)`), 파일당 50KB cap (`[... truncated]` 마커), `<raw_source_files>` XML wrapper + `<raw_source_file path="...">` 항목 직렬화. 신규 `parseRawFilePaths(raw)` 헬퍼 — `parseMemoryNodeIds` 미러.
- **`memory-inject-hook.ts`**: 게이트 조건 변경 — 이전 `if (mode === 'none') return null` → 이제 `if (mode === 'none' && !hasRaw) return null`. 노드 블록과 raw 블록을 각자 빌드한 뒤 `segments` 배열로 합쳐 한 번에 prepend. raw 파일 skip 사유(`outside-raw-dir` / `missing` / `read-error`)는 줄 단위로 task/discussion/session 로그에 기록.
- **오케스트레이터**: `orchestrator.ts`/`discussion-orchestrator.ts`/`session-manager.ts` 모두 호출부에 `rawFilePaths` + `projectRoot` 추가. 게이트 플래그가 `memMode !== 'none' || rawFilePaths.length > 0`으로 바뀌어 raw-only 주입도 가능.
- **라우트**: `routes/todos.ts`/`discussions.ts`/`sessions.ts` create/update가 raw paths를 받아 `.clitrigger/raw/` 접두사 + traversal validation 후 JSON으로 persist. memory `POST /preview` 엔드포인트가 `rawFilePaths`를 받아 합쳐진 블록 + skip 리포트를 반환.
- **클라이언트**: `MemoryInjectControl.tsx`에 **"원본 md 파일"** 섹션 추가 — sourceType별(todo/discussion/manual) grouping + 검색 + 파일 사이즈 + 파생 노드 카운트 칩 + 총 선택 사이즈 합계. 기존 `getProjectRawFiles` API 재사용. preview 모달은 raw 블록 포함.
- **폼 propagation**: `TodoForm`/`DiscussionForm`/`SessionForm` 모두 `memoryRawFilePaths` state. `ProjectDetail`/`TodoList`/`TodoItem`/`TaskGraph`/`SessionList`/`DiscussionDetail` 콜백 체인을 따라 전달.
- **타입**: `Todo`/`Discussion`/`Session` 타입에 `memory_raw_file_paths?: string | null` 추가.
- **i18n**: `rawFilesTitle` / `rawFilesHint` / `rawFilesEmpty` / `rawFilesSelected` / `rawFilesHasNodes` 5개 키 (en/ko).

#### 2. 세션 색상 태그 + 글로벌 설정 패널 + 선택적 제목 (`dc35289`)

긴 세션 리스트에서 시각적 구분 + 신규 세션 생성 마찰 감소.

- **DB 스키마**: 신규 `session_tags(id, name UNIQUE, color, sort_order, created_at, updated_at)` 글로벌 테이블. `sessions.tag_id TEXT` 컬럼 추가 (FK 미설정 — 태그 삭제 시 서버 쿼리에서 명시적으로 NULL clear).
- **신규 라우트**:
  - `/api/session-tags` — CRUD. 색상은 `#RRGGBB` 헥스만 허용, 이름 중복은 case-insensitive로 409. 삭제 시 해당 태그를 사용 중이던 세션의 `tag_id` 자동 NULL clear.
  - `/api/session-settings` — `app_settings` KV 위에 GET/PUT 단일 엔드포인트. 현재 `defaultUseWorktree` 한 키만 노출 ("session.default_use_worktree" → "1"/"0"). 추후 다른 세션 디폴트가 늘어나면 같은 엔드포인트에 필드 추가.
- **세션 제목 옵셔널화**: `SessionForm` 제출 가드(`if (!title.trim()) return`) 제거. 빈 제목으로 저장하면 서버가 `Session YYYY-MM-DD HH:MM` 타임스탬프 fallback으로 채움.
- **`SessionSettingsPanel.tsx`** (신규) — 글로벌 설정 모달의 새 "Sessions" 탭. 태그 CRUD: hex 컬러 픽커, 인라인 이름 편집, 삭제 확인 모달. 워크트리 디폴트 토글.
- **`SettingsModal.tsx`** — 기존 Account/Tunnel 탭 옆에 "Sessions" 탭 추가.
- **`SessionForm.tsx`** — 마운트 시 `getSessionTags()` + (create + Git repo 일 때만) `getSessionSettings()` 병렬 fetch. 태그 dropdown은 태그가 0개면 숨김. 선택된 태그 좌측에 4×4 색 도트 미리보기.
- **`SessionList.tsx`** — 세션 row 제목 옆에 태그 칩(이름 + 배경색) 렌더.
- **i18n**: `settings.tabs.session`, `sessionSettings.*`, `session.tag.none` 등 (en/ko).

#### 3. 세션 창 종료 흐름 + 프로젝트 디폴트 워크트리 상속 + 디폴트 브랜치 칩 (`ceb9a81`, `066efbd`)

세션 X 버튼이 무음으로 백그라운드 보내는 게 멘탈 모델과 어긋난다는 피드백. 최소화 버튼이 이미 백그라운드 역할이라 X = 종료가 자연스러움.

- **`SessionWindowsHost.tsx`**: `confirmRunningStop` 헬퍼 추가 — 그룹에 running 세션이 있으면 `confirm` 1회 + `stopSession()` 호출 후 UI 정리. `close`/`closeGroup` 게이트가 모든 X 경로(탭/단일 스택/멀티 스택/모바일/dock-tray)를 host API 경유하도록 통합. `SessionPane`의 자동 close는 `status !== 'running'`일 때만 트리거되어 confirm 우회.
- **`SessionForm.tsx`**: create 모드 + Git repo일 때 워크트리 체크박스 디폴트를 `project.use_worktree`에서 상속. todos/discussions와의 정책 일관성 회복. 편집 모드는 기존 세션 값 유지.
- **`SessionList.tsx`** (`066efbd`): `branch_name`이 없는 비-워크트리 세션이 Git 저장소면 `project.default_branch`로 fallback해서 GitBranch 칩 렌더. 같은 프로젝트에 워크트리/메인 세션이 섞여 있을 때 한 눈에 어디서 도는지 보임.

#### 4. xterm 세션 클립보드 단축키 + Alt+V 페이스트 + macOS Option-as-Meta (`a0b577b`)

xterm.js core가 일부러 클립보드 통합을 안 하는 정책이라 Ctrl/Cmd+C/V/X가 raw 제어문자(`^C`/`^V`/`^X`)로 PTY에 들어가던 회귀. iTerm2 / Windows Terminal 동작과 정렬.

- **`SessionTerminal.tsx`** `attachCustomKeyEventHandler`:
  - **Ctrl/Cmd+C**: selection이 있으면 복사, 없으면 SIGINT (per-platform mod 디텍션 — macOS Ctrl+C도 SIGINT 유지).
  - **Ctrl/Cmd+V** + **Alt+V**: `navigator.clipboard.readText()` → PTY로 forward.
  - **Ctrl/Cmd+X**: selection이 있으면 copy + clearSelection, 없으면 pass-through.
  - 기존 Ctrl/Cmd `+`/`=`/`-`/`_` 폰트 사이즈 분기 그대로 유지.
- **터미널 옵션**: `macOptionIsMeta: true` — macOS에서 Option+B/F 등이 raw 특수문자 대신 ESC+key를 emit. readline / tmux Meta 단축키 정상 동작.
- **`setupDesktopInput`**: 컨테이너 paste 이벤트 리스너를 fallback으로 설치 — `http://` LAN-IP origin에서 `clipboard.readText()`가 차단되는 케이스 대비.

#### 5. 클립보드 이미지 페이스트 (디스크 기반) + 도크 트레이 드래그 재정렬 + 최소화 hydration 수정 (`8c88bf6`, `5c86686`, `8371ac3`)

> **참고**: 본 흐름의 **디스크 저장 부분(`<workdir>/.clitrigger/paste-images/imgN.<ext>`)은 다음 날(05-04, `dcac8db`) OS 클립보드 직접 푸시 + Alt+V로 전면 교체**되어 현재 코드베이스에서는 사라졌다. 본 항목은 그날 ship된 상태의 기록.

- 이미지 페이스트: `navigator.clipboard.read()`로 이미지 우선 감지 → 서버 업로드(`POST /paste-image`) → 응답 받은 상대경로(`paste-images/img1.png`)를 PTY에 텍스트로 전송 → CLI의 path-to-image 디텍터가 픽업. 브라우저 `paste` 이벤트 핸들러도 동일 흐름 (HTTP origin fallback).
- **`GET /sessions/:id/clipboard-image-path`** (신규, Windows 전용) — PowerShell `Get-Clipboard -Format FileDropList`로 OS 클립보드의 파일 경로를 직접 읽어 디스크 중복 저장 회피. `8371ac3`에서 추가로 Windows의 `MyPictures\Screenshots`, `OneDrive\사진\스크린샷`, `OneDrive\Pictures\Screenshots` 등에서 최근 10초 내 생성된 png/jpg/jpeg/bmp까지 스캔(타임스탬프 cutoff). 스크린샷 단축키 직후 paste가 그 파일을 그대로 참조.
- **paste-image 라우트**: 응답 절대경로 → 상대경로, 파일명을 `${timestamp}-${uuid}` → `img{N}` 순번 (디렉터리 스캔 max + 1).
- 세션 DELETE 및 `/cleanup` 시 `<workdir>/.clitrigger/paste-images/` 폴더 자동 삭제.
- **도크 트레이 드래그 재정렬** (`8c88bf6`): `SessionWindowsHost.tsx`에 `dockDragRef`/`dockDragOverRef` + HTML5 drag handlers — 최소화된 dock chip을 좌우 끌어 순서 변경. cursor: grab.
- **최소화 hydration 수정** (`8c88bf6`): localStorage에서 그룹 복원 시 `minimized: false`를 하드코딩하던 줄을 `minimized: !!g.minimized`로 — 워크스페이스 전환 후 복귀해도 최소화 상태 유지.

#### 6. 폴리시 + dev 모드 자가-치유 (`56f5efa`, `7b86730`, `e2571e0`, `0a1deb0`)

- **`56f5efa`**: `SessionList.tsx`의 cleanup 버튼 노출 조건을 `worktree_path` 단독 → `worktree_path || branch_name`. 서버 cleanup 라우트는 둘 중 하나만 있어도 처리하므로 (use_worktree=false 세션이나 이전에 `delete_branch=false`로 정리한 세션이 git에 잔존 브랜치 남겼을 때 UI에서 정리 가능).
- **`7b86730`**: `db/queries.ts`의 세션 update 경로가 `use_worktree` 부울값을 그대로 binding → `SQLite3 can only bind numbers, ...` 에러로 편집 저장이 항상 실패. create 경로는 이미 0/1 정규화 중이었음 — update도 동일하게 coerce.
- **`e2571e0`** (split-pane resize drift): `group/LayoutNodeView.tsx`의 splitter `onDrag`가 매 mousemove마다 baseline sizes를 다시 쓰는데 `Splitter`는 mousedown 기준 누적 `deltaPx`를 보내므로, 매 프레임 누적 delta가 이미 시프트된 baseline 위에 적용돼 분할자가 폭주. snapshot을 `initialSizes`(mousedown 시 동결, deltaPx 기준)와 `finalSizes`(라이브 갱신, mouseup 커밋)로 분리.
- **`e2571e0`** (dev mode native ABI): `scripts/dev.bat` 부팅 직전에 `require('better-sqlite3')` 프로브 + 실패 시 자동 `npm rebuild better-sqlite3`. `build-electron.bat` 직후 `dev.bat`로 돌아갈 때 NODE_MODULE_VERSION mismatch가 자가 치유.
- **`0a1deb0`** (probe 강화): `require()`만으로는 lazy native dlopen이 트리거 안 돼 silent no-op으로 끝나던 회귀. probe를 `new Database(':memory:')`로 교체. `npm rebuild` 전에 `build/Release/better_sqlite3.node`를 명시적으로 unlink — prebuild-install이 stale 파일을 보고 short-circuit하는 케이스 차단. rebuild 후 같은 probe 재실행 후 실패 시 수동 복구 힌트로 bail.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/server/db/schema.ts` | 신규 `session_tags` 테이블, `sessions.tag_id`, `todos/discussions/sessions.memory_raw_file_paths` 컬럼 |
| `src/server/services/memory-injector.ts` | NEW `buildRawFileBlock`, `parseRawFilePaths` — `.clitrigger/raw/` 가드 + 50KB cap + `<raw_source_files>` 래퍼 |
| `src/server/services/memory-inject-hook.ts` | 노드 블록 + raw 블록 각자 빌드 후 `segments` 합쳐 한 번에 prepend, raw skip 사유 로깅 |
| `src/server/services/orchestrator.ts`, `discussion-orchestrator.ts`, `session-manager.ts` | `rawFilePaths` + `projectRoot` 전달, 게이트 조건 `memMode !== 'none' || raw>0`로 완화 |
| `src/server/routes/session-tags.ts` | NEW — 태그 CRUD + hex 컬러 검증 + 이름 중복 체크 |
| `src/server/routes/session-settings.ts` | NEW — `app_settings` 위에 GET/PUT, `defaultUseWorktree` 노출 |
| `src/server/routes/sessions.ts` | tag_id 처리, raw paths 검증, 빈 제목 timestamp fallback, (5절) clipboard-image-path GET, paste-image 응답 변경, cleanup 시 paste-images 삭제 — 5절 부분은 다음날 다시 제거 |
| `src/server/db/queries.ts` | session 태그/raw paths CRUD, **세션 update 시 use_worktree 0/1 coerce** |
| `src/client/src/components/MemoryInjectControl.tsx` | "원본 md 파일" 섹션 — sourceType grouping + 검색 + 사이즈/노드 카운트 칩 |
| `src/client/src/components/SessionForm.tsx` | 태그 dropdown, 빈 제목 허용, 디폴트 워크트리 서버 fetch, raw paths state |
| `src/client/src/components/SessionList.tsx` | 태그 칩, default_branch fallback 칩, cleanup 버튼 가드 완화 |
| `src/client/src/components/SessionSettingsPanel.tsx` | NEW — 글로벌 Sessions 탭 (태그 CRUD + 워크트리 디폴트) |
| `src/client/src/components/SettingsModal.tsx` | Account/Tunnel 옆에 Sessions 탭 |
| `src/client/src/components/SessionWindowsHost.tsx` | `confirmRunningStop` 헬퍼, dock tray 드래그 재정렬, hydration `minimized` 보존 |
| `src/client/src/components/SessionTerminal.tsx` | Ctrl/Cmd+C/V/X + Alt+V + macOptionIsMeta, 페이스트 fallback (5절 디스크 path 부분은 다음날 제거) |
| `src/client/src/components/group/LayoutNodeView.tsx` | splitter snapshot을 initialSizes(불변) / finalSizes(라이브)로 분리해 drift 차단 |
| `src/client/src/components/ProjectDetail.tsx`, `TodoList.tsx`, `TodoItem.tsx`, `TaskGraph.tsx`, `TodoForm.tsx`, `DiscussionDetail.tsx`, `DiscussionForm.tsx` | raw paths 콜백 체인 + projectUseWorktree / projectDefaultBranch propagation |
| `src/client/src/api/sessionTags.ts`, `sessionSettings.ts` | NEW — 클라이언트 API wrapper |
| `src/client/src/api/sessions.ts`, `todos.ts`, `discussions.ts`, `memory.ts` | raw paths 시그니처 추가, preview 응답 타입 |
| `src/client/src/types.ts` | `SessionTag`, `Todo/Discussion/Session.memory_raw_file_paths` |
| `src/client/src/i18n.tsx` | `rawFiles*` 5키, `settings.tabs.session`, `sessionSettings.*`, `session.tag.none` (en/ko) |
| `scripts/dev.bat` | better-sqlite3 ABI probe (`new Database`), 실패 시 .node 삭제 + `npm rebuild` 자가-치유 |

### 아키텍처 결정

1. **Raw 파일 주입은 mode와 직교**: `selected`/`auto` 모드에 raw 슬롯을 끼워 넣는 대신 **별도 selector**(`memory_raw_file_paths` 컬럼)로 분리. 큐레이트 노드와 raw md는 추상화 레벨이 달라(요약 vs 원본) 같이 켜고 싶은 케이스가 많고, mode가 `none`이어도 raw만 주입하는 시나리오가 자연스러움. 게이트 조건이 OR가 되어야 하는 이유.
2. **`<raw_source_files>` XML 래퍼**: `<long_term_memory>`와 동일한 "literal XML tag = LLM에게 의미 힌트" 패턴 답습. 라벨로 `Treat them as primary references — they may contain detail that was condensed out` 한 줄 설명을 넣어 모델이 큐레이트 노드와의 우선순위를 판단할 수 있게.
3. **path-traversal 가드**: raw 파일 path는 사용자가 폼에서 선택해 서버로 보내므로 신뢰 입력 아님. `path.resolve(projectRoot, rel)`이 `path.resolve(projectRoot, '.clitrigger/raw') + sep`로 시작하는지 검사. 프로젝트 트리 밖이나 raw 디렉터리 밖은 `outside-raw-dir` skip.
4. **`session_tags`는 글로벌 테이블**: 같은 색깔로 "오늘 작업 중", "Wikipedia rabbit hole" 같은 분류를 프로젝트 경계 없이 일관 적용. Per-project 의존이면 새 프로젝트마다 태그 다시 만들어야 해서 마찰.
5. **세션 디폴트 워크트리 = 프로젝트 정책 상속**: 신규 todos/discussions는 이미 `project.use_worktree`를 디폴트로 쓰는데 sessions만 무관하게 false 시작이라 정책 일관성 깨졌음. 글로벌 `session.default_use_worktree`는 그 위에 사용자 override 한 단계 (체인: 프로젝트 정책 < 글로벌 디폴트 < 폼에서 명시 변경).
6. **xterm 클립보드 통합은 `attachCustomKeyEventHandler`로**: xterm.js가 의도적으로 비워둔 영역이라 키 핸들러 레벨에서 가로채는 것이 표준. iTerm2/Windows Terminal과 동일하게 "selection 있으면 copy, 없으면 pass-through" 정책을 채택해 SIGINT가 죽지 않도록 함.
7. **dev.bat probe는 `new Database()`까지**: better-sqlite3는 `require()` 시점에 native bind를 안 하고 `new Database()`에서 dlopen하는 lazy 패턴이라 `require()` 만으로는 ABI mismatch가 잡히지 않음. probe가 진짜 트리거를 강제하지 않으면 자가-치유가 silent no-op으로 끝남.

---
