PMDesk(Claude Code / Codex를 여러 프로젝트에서 구동하기 위한 macOS 앱. 필자가 개발 중입니다)의 터미널에는, 줄 맨 앞에 @를 입력하면 파일 목록이 나타나는 기능이 있습니다. 이전에는 검색 기능만 있었지만, 0.1.24 버전에서는 '폴더' 탭(트리 보기)을 추가했고, 0.1.25 버전에서는 점 폴더(dot folder)가 숨겨지도록 했습니다. 구현 과정에서 생각했던 점들을 정리합니다.
무엇을 추가했는가
@를 입력하면 프로젝트 파일과 연결된 고객 자료의 폴더 목록이 함께 나타납니다. 검색 탭은 이름을 입력하여 필터링하는 방식으로 사용됩니다.
@ を打つと出る検索タブ
파일 이름이 기억나지 않을 때는 폴더 형태로 찾고 싶을 때가 있습니다. 그래서 ⌘→를 누르면 '폴더' 탭으로 전환되며, 프로젝트와 자료 폴더가 각각 하나의 루트로 된 트리가 나타납니다.
フォルダタブ(ツリー)
MentionTree
추출된
- 공유 모듈 @ 목록은 터미널의 TermMention과 자료/요구 페이지의 MentionPicker 두 곳에서 사용됩니다. 트리를 구현하는 코드를 두 곳에 작성하면 반드시 불일치가 발생하므로, mock/mention-tree.js에 단 한 곳만 배치했습니다.
방침은 'DOM을 건드리는 함수를 하나로 제한'하는 것입니다.
buildTree/visibleRows/navigate/revealPath/treeKey/tabKey는 순수 함수(pure function)이며, DOM을 건드리는 것은 mountView만 담당합니다.
순수 함수로 만들었기 때문에 브라우저 없이도 단위 테스트가 가능합니다(mock-mention-tree.test.ts). 키 조작의 '오른쪽으로 열기, 열려 있다면 첫 번째 자식으로 이동. 왼쪽으로 닫기, 닫혀 있다면 부모로 이동'과 같은 동작 역시 navigate(rows, idx, action, open)에 배열 형태의 행과 현재 위치를 전달하여 결과를 확인하는 것만으로 테스트할 수 있습니다.
// 상하 모두 양 끝에서 순환
if (action === 'down' || action === 'up')
return { idx: ((idx + (action === 'down' ? 1 : -1)) % n + n) % n, open: o };
2. 루트 행을 sticky하게 만들기
트리가 길어지면 스크롤할 때 '지금 보고 있는 것이 어느 루트 안에 있는지' 알기 어려워집니다. 프로젝트와 자료 폴더의 두 개의 루트가 있기 때문에 이는 사소하지만 큰 불편함이었습니다.
루트 행에 CSS의 position: sticky를 적용하여 위쪽에 고정했습니다(0.1.24에 반영된 부분입니다).
.mt-tree { overflow-y: auto; max-height: 260px; scroll-padding-top: 30px; }
.mt-root { font-weight: 600; position: sticky; top: 0; z-index: 1;
background: var(--panel); box-shadow: 0 1px 0 var(--line); }
スクロールしてもルート行が上に残る
한 가지 함정이 있었습니다. 키보드로 선택 행을 scrollIntoView({block:'nearest'})로 스크롤하면, 선택된 행이 고정된 루트 행 아래에 가려지게 됩니다. 그래서 .mt-tree에 scroll-padding-top을 추가하여 루트 행 높이만큼 공간을 확보했습니다.
3. 점 폴더(Dot folder) 숨기기 (0.1.25)
처음에는 .git이나 .claude 같은 내부 파일까지 모두 트리에 나타나서, 정작 중요한 파일들이 묻히는 문제가 있었습니다. 그래서 경로의 폴더 부분에 .으로 시작하는 것이 있다면 표시하지 않도록 했습니다.
if ((it.dir ? orig : orig.slice(0, -1)).some((s) => s.startsWith('.'))) continue;
핵심은 '파일 이름의 마지막 하나'는 보여주지 않는 것입니다. .gitignore와 같은 점 파일은 표시하고 싶기 때문에, 폴더 부분만 판별하도록 했습니다. 테스트에서도 「.claude/settings.json」과 src/.cache/x.js는 사라지고, .gitignore는 남아있는 것을 확인했습니다. 숨겨진 항목들은 검색 탭에서는 계속해서 찾을 수 있습니다.
4. 사소한 함정
IME 변환 중에는 아무것도 하지 않음: 일본어 입력(IME) 중에 Enter나 화살표 키를 감지하면 오작동합니다. isComposing 상태이거나 keyCode === 229인 경우에는 해당 키 입력을 무시합니다.
- macOS 파일명은 NFD 형태가 있을 수 있음: 전각점 등이 분해된 형태로 올 수 있으므로, 경로 비교(path matching)는 NFC로 정규화하여 수행하고, 표시 및 전송 시에는 원래의 경로를 사용합니다.
- 포커스를 입력 필드에서 벗어나지 않게 함: 행이나 버튼에 대한
mousedown 이벤트를 막아, 클릭해도 입력 필드의 포커스가 벗어나지 않도록 처리했습니다. 포커스가 벗어날 경우 'Esc 키와 동일하게' 취급되어 순수한 @ 기호가 나타나기 때문입니다.
- 파일명은 신뢰하지 않음: 이름 설정 시에는
textContent만을 사용하며, HTML로 해석하지 않습니다.
요약 (Summary)
트리 UI는 순수 함수(pure function)와 DOM 부분을 분리하는 것만으로 테스트가 용이해졌고, 키 조작 추가도 쉬워졌습니다. @ 목록은 아직 검색 중심이기 때문에, 폴더 구조를 통해 탐색하고 싶은 사용자를 위한 진입점을 추가한 위치입니다.
PMDesk는 macOS 전용입니다 (Windows / Linux 미지원). https://pmdesk.dev
토론 (Discussion)