3-10. 프로젝트 검색과 저장소 신뢰 (10/5 ~ 10/9)
앞 편이 같은 문서를 두 칸에 나눠 여는 일이었다면, 이번 닷새는 편집기가 파일 하나 밖을 다루기 시작한 시간이었다. 열린 문서 안에서만 돌던 찾기가 ripgrep을 번들한 프로젝트 검색이 됐고, 언어 서버를 띄울지 묻는 확인은 저장소 단위의 신뢰 표와 사용자 셸 환경으로 다시 짜였다. 그사이 창을 닫거나 재접속하는 순간 앱이 죽던 결함 셋과, 업데이트 뒤 실행할 때마다 빈 창이 뜨던 회귀를 고쳤다. config 파일은 저장하면 모든 창에 같게 다시 읽히고, 모달·메뉴·알림 패널은 보이는 자리와 눌리는 자리가 어긋나던 곳을 하나씩 맞췄다.
이 편은 공개 main에 한국 시간 10월 9일까지 병합된 마지막 PR #4264의 커밋 f6c74f97를 기준으로 썼다. 3-9의 기준점 2597f8f2 이후 커밋은 148개, 3-9의 마지막 PR #4135 이후 main을 대상으로 병합된 PR은 105개다. 날짜는 앞 편과 같이 한국 시간의 병합 시각이며, 10월 10일 0시 이후 병합분은 다음 편으로 넘겼다. 대상이 개발 브랜치인 PR 22개는 마지막 절에 따로 적고 구현 범위에 넣지 않았다.
프로젝트 전체를 찾는다 (10/5 ~ 10/9)
프로젝트 검색은 엔진을 고르는 실험부터 시작했다. 같은 2,048파일·64 MiB에서 ripgrep 전체 검색, 파일 목록 뒤 기존 찾기, 내용 선별 뒤 기존 찾기를 비교하자 기존 평문 찾기가 후보 위치마다 UTF-8을 다시 해독하느라 수백 ms를 쓰고 있었다. 이 비용부터 줄여, 대소문자를 구분하는 드문 평문 검색의 전체 시간 중앙값이 431.89 ms에서 67.90 ms가 됐다. PR은 캐시와 시스템 부하를 격리하지 않은 수치라 화면 반응성의 개선으로 선언하지 않는다고 적었다. 후보 실측 #4140, 평문 디코드 절감 #4145.
검색 규칙도 이 자리에서 다시 정했다. 대소문자 무시 검색이 편집용 소문자 변환을 빌려 쓰느라 켈빈 기호 K(U+212A)와 k, ſ와 s, Σ·σ·ς를 같은 글자로 보지 못했다. 검색용 Unicode 17 접기(simple folding)를 따로 두고 편집기·터미널·WASM 검색이 함께 쓴다. 줄마다 따로 돌던 정규식은 문서 전체를 대상으로 바뀌어 여러 줄 검색과 캡처 치환이 된다. 단어 단위 옵션과 빈 일치 처리는 사용자가 고른 대로 VS Code 기본 동작에 맞췄다. Unicode 접기 #4148, 문서 전체 정규식 #4153, ripgrep 재비교 #4167, VS Code 검색 경계 #4170.
10월 7일 사용자는 VS Code의 구성, 곧 디스크는 ripgrep으로 찾고 열린 문서는 편집 중인 본문을 우선하는 방식을 골랐다. 공식 ripgrep 15.2.0을 두 아키텍처용으로 고정해 앱 번들의 Contents/Helpers/rg에 넣고, PATH에서 찾지 않는다. 그 위에 백그라운드 worker가 rg 출력을 조금씩 해석하고, 열린 문서는 불변 사본으로 넘겨 먼저 검색한다. 열린 문서가 0건이어도 디스크의 옛 본문이 결과에 끼어들지 않는다. 다음 날에는 실제 창의 문서·한글 조합과 연결하고, 여러 루트를 한 요청과 한 예산으로 묶었다. ripgrep 번들 #4196, worker #4208, AppSession 연결 #4218, 여러 루트 #4234.
10월 9일 저녁, ⇧⌘F나 팔레트의 Search: Find in Workspace로 도크를 열어 현재 창의 로컬 프로젝트를 검색하고, 결과를 눌러 그 위치로 이동할 수 있게 됐다. 대소문자·단어·정규식과 포함·제외 glob을 고르고, 입력은 300ms 뒤에 검색하며 앞선 검색은 취소한다. PR은 실제 앱에서 결과 14,048개를 검색·선택·스크롤·취소한 것을 적었다. 같은 날 밤에는 결과를 읽기 전용 탭으로 넓게 보고, 바꾸기 전후를 diff로 미리 보는 탭이 들어왔다. 실제 여러 파일 바꾸기는 이 범위에 없다. 열린 문서에 바꾸기를 적용하는 PR은 10월 10일에 병합돼 다음 편으로 넘긴다. 검색 도크 #4245, 결과 탭·바꾸기 미리보기 #4261.
편집기 둘레에도 길이 생겼다. 도크에 활성 문서의 함수·클래스를 문서 순서로 보이는 아웃라인이 더해졌고, maru://open?path=…&line=42&column=7 같은 앱 URL로 파일과 위치를 연다. 이미 열린 파일은 미저장 내용을 그대로 둔 채 그 위치로 간다. 도크 아웃라인 #4134, 앱 URL #4249.
저장소 단위로 믿는다 (10/7 ~ 10/9)
언어 서버의 신뢰 확인은 9월 중순 1단 구현 때 "trusted workspace 확인"만 옮겨 왔고, 계약 문서에 있던 실행 파일 표시·환경·격리 한계 안내는 결정 기록 없이 빠져 있었다. 10월 7일 계약을 저장소 단위 신뢰 하나와 사용자 셸 환경으로 고치고, 차이를 메울 WT1~WT5 계획을 세웠다. 환경을 줄이지 않기로 한 이유도 적었다. 서버는 어차피 사용자 권한으로 돌아 ~ 아래를 읽을 수 있어 보안 이득이 작고, 허용 목록은 RUSTUP_HOME·GOPATH 같은 툴체인 설정을 깨뜨린다. 격리가 필요해지면 OS 샌드박스의 일이다. 신뢰 계획 #4217.
예전 확인 문구는 사실과 달랐다. 서버 하나를 묻지만 답은 저장소 전체에 기억됐고, 영향이 저장소 안에 머무는 것처럼 읽혔다. 새 신뢰 시트는 서버가 사용자 권한으로 격리 없이 저장소 밖 파일과 네트워크에 닿고, 빌드 스크립트를 실행할 수 있다는 대가를 줄을 나눠 밝힌다. 이어 창마다 따로 캐시하던 신뢰를 앱 전역 표 하나로 올려, 한 창의 답이 다른 창에도 적용되고 같은 저장소는 한 창에서만 묻는다. 키는 경로 문자열이 아니라 실제 경로와 볼륨이라 /tmp와 /private/tmp, 심볼릭 링크로 거부를 피할 수 없다. 신뢰 시트 WT1 #4221, 앱 전역 표 WT2a #4227.
동작이 바뀐 곳도 있다. 홈에 dotfiles용 .git이 있으면 홈 아래 저장소가 아닌 폴더의 파일은 모두 루트가 홈이 되어, 한 번의 허락이 홈 전체를 열었다. 이제 홈이거나 git 저장소 밖인 루트는 묻지도 띄우지도 않고, 상태바에 이유만 보인다. 잘못 허용한 저장소는 팔레트의 목록에서 철회하거나 잊을 수 있고, 철회하면 모든 창에서 서버를 바로 내린다. 같은 결정을 컨트롤 플레인과 CLI로도 조회·철회·잊기 할 수 있지만, 에이전트가 신뢰를 주지 못하게 부여 메서드는 두지 않았다. 저장소 밖 루트 WT2b #4230, 팔레트 관리 WT4a #4232, CLI 관리 WT4b #4244.
환경 쪽은 Finder나 Dock으로 띄운 앱의 짧은 PATH가 문제였다. ~/.cargo/bin·~/go/bin·nvm 아래 서버를 찾지 못해, 신뢰를 묻기도 전에 "없음 — 설치"로 끝났다. 이제 로그인 셸을 앱 전체에서 한 번 읽어 서버를 찾고 띄우며, 읽는 동안은 "없음"을 판정하지 않는다. 셸 설정을 돌리고 싶지 않으면 lsp.shell-environment로 끈다. 셸이 export한 토큰이 서버까지 가는 문제는 패턴으로 지우지 않고, 사용자가 고르는 lsp.environment-exclude 목록과 넘어가는 이름을 보는 팔레트 명령으로 풀었다. 신뢰 시트와 Show Server Info는 실제로 실행될 파일의 경로와 출처, 서버가 알려 준 버전을 보인다. 셸 환경 WT3a #4240, WT3b #4243, 실행 파일 표시 WT5a #4251, 제외 목록 WT5b-1 #4255.
보안 쪽 구멍도 같이 막았다. 신뢰 시트와 브라우저 권한 확인은 비동기로 뜨는데 처음 포커스가 "허용"이고 Y가 단축키여서, 상자가 뜬 순간 치던 Enter나 y가 빌드 스크립트 실행이나 쿠키 접근을 허락할 수 있었다. 이제 거부에 포커스를 두고, 키보드로 고른 허용은 한 번 더 ⌘⏎로만 받는다. 권한 동의문이 긴 URL을 가운데에서 줄이며 accounts.google.com.…attacker.io의 등록 도메인을 잘라 실제 사이트를 숨기던 것도 고쳤다. git 읽기는 저장소의 log.showSignature와 gpg.program 설정 때문에 저장소가 정한 프로그램을 실행하던 것을 끈다. 신뢰에 따라 git 동작을 가르는 WT6b는 첫 조각이 10월 10일에 병합돼 다음 편으로 넘긴다. 키 보호 #4238, URL 호스트 #4188, 서명 검증 차단 WT6a #4260.
서버의 수명도 손봤다. rust-analyzer가 띄운 cargo check가 서버를 내린 뒤 고아로 남던 것을 프로세스 그룹째 내리게 하고, initialize에 답하지 않는 서버나 stdin을 읽지 않는 서버가 매달리거나 쌓이지 않게 했다. CLI에는 maru editor open 'src/file 한글.zig' -l 42 -c 7처럼 파일을 여는 명령과 신뢰 관리 명령이 maru editor 아래로 모였다. 기준 커밋의 #4264는 --help가 설치 파일을 교체하거나 초과 인자가 출력 파일을 덮어쓰던 CLI 결함을 고친 PR이다. 서버 수명 #4213, maru editor #4258, CLI 쓰기 거절 #4264.
앱이 죽던 자리 (10/6 ~ 10/8)
10월 6일 오후 창을 닫다가 앱이 SIGSEGV로 죽었다. 창 닫기와 복원은 탭을 차례로 풀면서 목록에서 빼지 않았고, 뒤 탭의 Term을 풀 때 저장 충돌 비교를 정리하는 함수가 모든 탭을 훑다가 이미 해제된 앞 탭을 읽었다. 탭 하나만 닫는 경로는 원래 "빼고 푼다" 순서라 안전했다. 세 자리를 같은 순서로 바꿨다. TAB-UAF #4186.
전날 저녁에는 SIGABRT가 있었다. 세션 호스트와 다시 연결되는 순간, 재접속 작업이 호스트의 runtime을 모두 얼린 같은 프레임에서 창 tick이 그 runtime을 읽었다. 8월부터 있던 잠복 결함이었다. 얼린 동안은 읽기를 쉬고 비변경 요청은 거절하게 했고, 같은 상황에서 사용자가 ⌘W나 에이전트 행의 ✕로 탭을 닫으면 abort하던 경로는 backend가 닫기를 맡았다가 재접속이 끝난 뒤 마저 닫게 했다. 재접속 drain #4184, 재접속 중 닫기 #4187.
같은 날 고친 회귀는 더 눈에 띄었다. 10월 4일의 복구 ID 변경이 저장 헤더를 maru.workspace.v2로 올리면서 기존 v1 파일을 거절해, 업데이트한 앱이 실행할 때마다 빈 창으로 시작했다. 세션은 살아 있었다. 이는 7월 8일 사용자가 정한 "헤더를 올리지 않는다"는 결정을 어긴 것이었다. 헤더를 v1로 되돌리고 그사이 v2로 쓴 파일은 읽기만 하며, 저장 파일을 읽지 못하면 로그를 남기게 했다. 헤더 복원 #4162.
잠자기 뒤 재접속도 고쳤다. 덮개를 닫았다 연 뒤 앱이 다시 실행할 때까지 세션 호스트에 붙지 않는 일이 10월 4일과 7일에 났다. 10월 5일 새벽 재접속 작업이 끝날 때마다 사유를 남기게 한 로그가 원인을 확정했다. 데드라인 초과를 종결로 처리했고, 다시 넣어도 처음 받은 5초 데드라인을 끌고 갔다. 이제 데드라인 초과도 새 데드라인으로 다시 시도한다. 10월 7일 저녁의 다른 끊김은 원인을 둘 중 하나로 좁힐 수 있게 계측만 넣었다. 사유 기록 #4138, 잠자기 재접속 #4222, 정체 계측 #4219.
네 수정 모두 PR은 실제 앱에서의 재현 흐름(창 닫기, 재접속 직후의 프레임, 덮개 닫기)을 자동 검증하지 못한 영역으로 적고, 설치 뒤 수동 확인으로 남겼다. 이 밖에 kitty 이미지를 나눠 받는 도중 메모리 할당이 실패하면 버퍼를 잘못된 길이로 해제하던 힙 손상 경로도 고쳤다. kitty OOM #4150.
설정은 모든 창에 같게 (10/5 ~ 10/7)
config 파일을 외부 편집기로 고쳐도 Reload Config 메뉴를 눌러야 반영되던 것이 바뀌었다. behavior.auto-reload(기본 켜짐)는 폴링 없이 FSEvents로 config가 든 폴더를 보고, 앱이 앞으로 올 때 한 번 더 확인한다. 로컬 디스크가 아닌 config는 감시하지 않는다. PR은 실제 FSEvents부터 다시 읽기까지의 종단은 자동 검증하지 못했다고 적었다. 자동 다시 읽기 #4137.
자동 경로가 생기자 창 사이의 비대칭이 드러났다. 저장 한 번에 창 수만큼 같은 로그가 찍혔고, 메뉴의 Reload Config는 활성 창 하나만 다시 읽었으며, 전체 리셋은 리셋한 창만 스크롤백을 줄였다. 적대적 검증을 실제 창 둘로 돌려, 메뉴 Reload 뒤 활성 창은 1,000줄인데 다른 창은 5,000줄로 남거나, 리셋 뒤 다른 창의 글꼴 확대가 풀리지 않는 경우까지 맞췄다. 못 읽는 config로 다시 읽으면 그 창만 설정이 통째로 기본값이 되던 것은, 사용자에게 확인한 뒤 지금 설정을 지키는 쪽으로 바꿨다. 로그 한 번 #4151, 모든 창 Reload #4155, 모든 창 리셋 #4158, 미룬 적용 #4189, 못 읽는 config #4194.
로그에서는 다른 문제도 나왔다. 끝이 =인 base64 토큰을 잘못 붙여 넣으면 로더가 그 줄을 키로 보고 app.log에 그대로 남겼다. 키 모양일 때만 키를 찍고, 숫자와 대문자가 섞인 토큰은 가린다. 오타 키는 고치려면 보여야 하므로 계속 찍는다. 토큰 가리기 #4200, 점 없는 키 #4211.
보이는 것과 눌리는 것 (10/5 ~ 10/9)
화면 쪽 수정은 대개 보이는 것과 실제 상태가 어긋나는 자리였다. 확인 모달은 긴 메시지와 버튼을 상자 안에서 줄을 나눠 그리고, 좁고 낮은 창에서도 버튼 행을 패널 안에 둔다. 브라우저 권한 동의문은 URL이 길면 질문이 통째로 잘려, 사용자가 질문 없는 URL 조각을 보고 답할 수 있었다. 우클릭 메뉴는 창보다 길면 폭을 자르고, 깨진 UTF-8이 섞인 라벨과 결합 부호가 붙은 이름도 비거나 갈라지지 않게 했다. 잘린 문장에는 "…"를 붙여 잘렸음을 보인다. 모달 줄바꿈 #4163, 동의문 질문 #4168, 메뉴 폭 #4174, 긴 notice #4183, 좁은 창 모달 #4190, 보간 잘림 #4193, 이름 자르기 #4199.
알림 패널에서는 스크롤이 줄 중간에 걸쳐 있으면 클릭과 호버가 한 줄 위 카드를 잡아, ✕ 칸을 누르면 엉뚱한 알림이 지워졌다. 스크롤바나 우클릭도 카드를 지웠다. 알림 패널과 notice가 한 화면에 겹쳐 글자가 섞이던 것, 찾기 막대를 연 채 우클릭하면 메뉴가 패딩과 그림자 없이 그려지던 것도 이 흐름이다. 걸친 카드 #4173, 패널과 notice #4177, 걸친 스크롤 클릭 #4182, 오버레이 둘 #4225, 겹친 오버레이 #4228.
편집기의 팝업 이름 상자(심볼 이름 바꾸기와 이름 없는 문서의 저장 이름)는 보이지 않는 상자가 키를 먹는 결함이 연달아 나왔다. 낱말을 휠로 화면 밖에 보내거나 메뉴로 탭을 옮기면 상자가 사라지거나 다른 탭 위에 떴는데, 친 글자는 그 상자로 갔다. 앱이 포커스만 잃어도 이름 바꾸기가 확정됐다. 이름 없는 문서에서 ⌘S를 누르면 저장 상자가 아예 그려지지 않아 "⌘S가 아무 일도 안 한다"로 보였는데, 그사이 친 글자는 보이지 않는 상자로 들어가 Enter를 누르면 그 이름으로 파일이 만들어졌다. 다른 오버레이 아래에 숨은 자동완성 목록이 클릭을 받아 문서를 바꾸던 것도 막았다. 보이지 않는 상자 #4236, 상자 안 누름 #4237, caret 이동 #4242, 숨은 목록 #4247, 저장 상자 #4250, 숨은 헬퍼 닫기 #4263.
저장 상자 결함은 판정자 문제이기도 했다. 오버레이는 손으로 쓴 목록의 관문을 통과해야 그려지는데, 그리는 출처가 늘 때 목록에 넣지 않아 생긴 결함이 이번이 세 번째였다. 세 번 모두 판정자가 관문을 건너뛰고 그리기 함수를 직접 불러 초록이었다. 3-9의 "효과를 재라"는 규율대로, 그리는 출처마다 관문이 서는지 대조하고 실제로 오버레이를 하나씩 열어 재는 판정자를 세웠다. 관문 대조 #4252, 판정자 구멍 #4254, 동작으로 재기 #4259.
터미널과 에이전트 (10/5 ~ 10/9)
tmux 안에서 terminal-browser를 띄우면 왼쪽 위 30열×30행만 그림이 뜨고 나머지는 줄무늬와 결합 문자 격자가 됐다. kitty의 unicode placeholder는 칸마다 결합 문자 둘로 이미지 조각의 행·열을 적는데, Maru는 그 297개 중 30번째부터 214개를 폭 1로 세어 칸을 밀었다. 결합 부호 Mn·Me 전체를 0폭으로 센다. PR은 수정 빌드로 실제 tmux 화면을 본 것은 아직이라고 적었다. vim 같은 alt 화면 앱을 쓰는 중 창을 줄이면, 잘린 셸 화면의 kitty 이미지가 나중에 빈 줄 위에 되살아나던 것도 고쳤다. 결합 부호 폭 #4257, alt 화면 이미지 #4157.
같은 폴더에서 Codex를 두 pane으로 띄우면 사이드바의 상태와 제목이 뒤바뀌었다. Codex 0.157부터 모든 TUI의 훅을 공유 데몬 하나가 돌리는데, 데몬이 먼저 뜬 pane의 환경을 물려받아 Maru가 그 pane을 증거로 믿었다. 데몬을 유지한 채 훅을 세션 ID로 귀속한다. 에이전트 활동 도크는 새 활동이 앞에 붙어도 보던 항목이 맨 위에 남는다. Codex 데몬 귀속 #4139, 활동 목록 위치 #4156.
테스트가 실제 홈에 닿지 않게 (10/5 ~ 10/7)
3-9에서 테스트 빌드가 실제 백업 폴더와 캐시에 쓰는 것을 막았지만, 테스트가 띄우는 제품 실행 파일은 테스트 빌드가 아니어서 그 가드를 타지 않았다. 테스트가 띄운 세션 호스트는 실제 ~/.cache/maru에서 죽은 호스트 칸을 지우고 있었다. 공용 테스트 러너에서 HOME을 임시 자리로 옮겨 자식 프로세스가 함께 옮겨 가게 했다. 이후 적대적 검증이 세 바퀴 돌며 pid 재사용으로 이전 임시 홈을 물려받는 것, HOME=/에서 러너가 끝없이 자기 자신을 exec 하던 것, 그 고침이 루프를 조용한 누출로 바꾼 것까지 차례로 잡았다. HOME 격리 #4141, 임시 홈 #4207, 무한 exec #4209, 루트 HOME #4215.
3-9에서 남긴 공유 분할의 검증도 이어졌다. 공유 뷰를 복원한 뒤 일부를 닫을 때의 백업 보존, 실제 IME 전환과 조합 중 닫기, 실제 한자 후보창을 연 채 pane을 전환하고 닫는 경우를 AppKit에서 판정했다. 자연스럽게 늦게 도착하는 OS 콜백은 재현되지 않았고 전환·닫기 각 5회가 통과했다. 공유 뷰 닫기 #4185, IME 전환 #4203, 한자 후보창 #4223.
개발 브랜치의 Chromium 탭 (10/5 ~ 10/9)
앱 안의 Chromium 웹 패널은 여전히 docs/web-panel-osr-backend 브랜치에서 진행 중이다. 10월 5일부터 9일까지 그 브랜치를 대상으로 22개 PR이 병합됐고, main으로 합치는 통합 PR #3867은 10월 10일 확인 시점에도 열려 있다. 따라서 아래 내용은 main에 반영되지 않은 개발 브랜치의 작업이다.
- 브라우저다운 동작: 원래 페이지와 이어진 팝업을 maru 탭으로 열고, 뒤에 있는 창의 첫 클릭을 페이지로 보내며, 링크·이미지·동영상 우클릭 메뉴를 채웠다. 탭을 닫을 때 페이지의 떠나기 확인을 묻고, 링크를 탭 막대·주소 띠에 끌어 놓고 그림 데이터는 파일로 놓으며,
datalist제안 목록을 macOS 네이티브 창으로 띄운다. W6f② #4144, W6g #4152, W6h① #4159, W6h② #4166, W6j #4175, W6l① #4191, W6l② #4195, W6m② #4212. - 다운로드: 조용히 취소되던 다운로드를
~/Downloads에 받고 목록 창(⇧⌘J)을 둔다. 실행될 수 있는 파일은 보류하고 묻고, "매번 묻기" 설정과 받는 중 탭을 닫거나 앱을 끝낼 때의 처리가 이어졌다. W10a #4226, W10b #4231, W10c #4241, W10d #4246, W10e #4248. - 세션 호스트: 마지막 셸이 끝나 앱이 종료되거나 "종료 및 세션 끝내기"를 고르면 앱이
proof_loss(종료 코드 86)로 죽던 결함을 이 브랜치에서 고쳤다. 웹 패널과 무관한 결함이지만 역시main에는 아직 없다. 끝난 셸 닫기 #4256, 세션 끝내기 #4262.
10월 9일 시점
이 닷새 동안 편집기는 파일 하나를 넘어 프로젝트를 찾고, 언어 서버는 저장소 단위로 신뢰를 묻게 됐다. 남은 것도 분명하다. 프로젝트 검색은 찾기와 미리 보기까지이고 여러 파일 바꾸기는 후속이며, 신뢰 계획은 WT6a까지 들어왔다. 앱이 죽던 세 경로와 잠자기 재접속은 PR이 실제 앱에서의 재현을 설치 뒤 수동 확인으로 남겼다. 종료할 때 앱이 죽던 결함의 수정은 개발 브랜치에만 있다. Markdown 소스 이관 검토안과 Windows 편집기·파일 처리 PR은 열린 채 미병합이다. Markdown 이관 검토 #3967, Windows 보완 #4084.