3-9. 한 문서를 두 칸에서 (9/26 ~ 10/4)
앞 편이 작업을 끊었다가 돌아오는 자리를 메웠다면, 이번에는 같은 것을 두 번 갖지 않는 일이 중심이었다. 같은 파일을 두 칸에 열어도 본문과 Undo는 하나만 두고, 같은 경로의 독립 문서는 백업을 서로 덮지 않게 경로 대신 ID로 나눴다. 로그인 세션마다 하나여야 할 세션 호스트가 설치할 때마다 늘던 문제도 잡았다. 그 사이 한글 조합, 정규식 찾기, 터미널 크기 통지가 이어졌고, 테스트가 "있다"만 재고 "효과가 난다"는 재지 않던 자리를 규율 문서로 세웠다.
이 편은 공개 main에 한국 시간 10월 4일까지 병합된 마지막 PR #4135의 커밋 2597f8f2를 기준으로 썼다. 3-8의 기준점 6485c447 이후 커밋은 279개, 3-8의 마지막 PR #3963 이후 main을 대상으로 병합된 PR은 141개다. 날짜는 앞 편과 같이 한국 시간의 병합 시각이며, 10월 5일 0시 이후 병합분은 다음 편으로 넘겼다. 대상이 개발 브랜치인 PR은 마지막 절에 따로 적고 구현 범위에 넣지 않았다.
같은 파일, 하나의 정본 (9/30 ~ 10/4)
같은 파일을 두 pane에서 편집하는 계약은 이전부터 있었지만, 제품의 문서와 Undo는 Term마다 따로 소유하고 있었다. 9월 30일 편집기의 남은 작업을 최신 코드와 대조해 다시 정리했고, 10월 1일 VS Code의 공개 동작을 기준으로 공유 문서와 독립 뷰를 설계했다. 설계 PR은 제품 코드를 바꾸지 않았고, 현재 소유와 소비처를 조사하는 문서가 뒤를 이었다. 잔여 작업 정리 #4022, 공유 문서 설계 #4032, 소유 경계 조사 #4036.
구현은 같은 날부터 작은 조각으로 들어갔다. Undo·Redo 자원과 문서 본문·저장 정보를 먼저 한 객체로 묶고, 슬롯이 움직이지 않는 문서 핸들과 참조 수명을 붙였다. 마지막 뷰를 닫아도 읽기나 요청 참조가 남아 있으면 문서를 해제하지 않는다. 그 위에서 두 뷰를 잇고, 한쪽 입력이 다른 쪽의 줄과 선택 좌표에 반영되게 했다. Undo 소유 분리 #4038, 본문·저장 정보 분리 #4042, 문서 핸들 #4044, 연결된 뷰 편집 #4055, 뷰 연결 #4054.
Undo 기록 공간을 확보하지 못했을 때의 규칙도 이 자리에서 바뀌었다. 종전에는 편집만 남기고 기록을 버려, 오래된 역연산이 기록 없는 변경을 건너 잘못된 위치에 적용될 수 있었다. 이제는 본문을 바꾸기 전에 공간을 확보하고, 실패하면 그 편집을 적용하지 않는다. 편집을 적용한 뒤 기록에 실패하는 경우에는 편집을 남기고 양쪽 이력을 비운다.
10월 4일 저녁, 편집기에서 ⌘\를 누르면 오른쪽에 같은 문서가 열리게 됐다. View 메뉴와 팔레트에서는 네 방향을 고른다. 본문과 Undo는 공유하고, 선택·검색·스크롤·접힘·줄바꿈은 복사한 뒤 각자 갖는다. 공유 뷰 일부만 다른 창으로 옮기면 양쪽 입력이 거절되던 결함도 함께 고쳐, 이제는 안내한 뒤 이동을 거절한다. 그 직전에는 큰 문서를 다시 열거나 언어 서버가 늦게 준비될 때 저장한 접힘이 풀리고 보던 줄이 밀리던 복원 결함을 고쳤다. 공유 IME #4065, 뷰별 검색 #4070, 접힘 유지 #4078, 공유 pane #4080, 지연 분석 복원 #4125, 분할 명령 #4126.
지원 범위는 같은 창 안의 일반 로컬 파일이다. 이름 없는 문서, 원격·비교·병합 문서의 공유 분할과 두 창에 걸친 공유 편집은 지원하지 않는다. 재시작 뒤 복원은 실제 앱으로 확인했지만, 물리 키보드의 ⌘\ 조합과 두 OS 창 사이의 실제 메뉴·드래그 이동은 PR에 검사하지 않은 범위로 남아 있다.
백업은 경로가 아니라 ID로 (10/2 ~ 10/4)
3-8에서 만든 미저장 백업에도 구멍이 있었다. 이름 없는 문서를 복원할 때 앱은 내용을 새 탭에 넣자마자 백업 레코드를 지웠는데, 창 복원은 트랜잭션이라 뒤의 탭 하나만 실패해도 앞서 만든 탭을 모두 되돌린다. 실험에서는 실패 직후 백업 폴더가 완전히 비어 있었다. 이제는 창 복원이 확정될 때까지 레코드를 남기고, 남은 레코드의 번호를 새 문서가 다시 쓰지 않게 했다. 이름 없는 백업 보존 #4067.
같은 경로를 따로 연 두 문서는 경로 기반 백업 하나를 서로 덮거나, 한쪽을 정리하며 다른 쪽 백업까지 지울 수 있었다. 먼저 복원 직후의 유실과 같은 경로 충돌을 테스트로 재현하고, 복구 백업은 새 보호가 성공할 때까지 남기게 했다. 그다음 문서마다 복구 ID를 주고 workspace가 문서 표와 뷰 참조를 함께 저장하게 했다. 같은 경로의 독립 문서는 따로 복원되고, 명시적으로 공유한 뷰는 한 정본을 유지한다. 유실 재현 #4093, 복구 백업 보존 #4094, 경로 충돌 재현 #4095, 복구 ID codec #4103, ID 기반 복원 #4113.
자동 복원이 놓친 백업을 직접 찾는 길도 생겼다. 백업은 저장됐지만 workspace checkpoint가 게시되기 전에 앱이 끝나면, 디스크에 남은 편집을 기존 복원이 찾지 못했다. 팔레트의 Editor: Recover Unsaved Edits(검색어 "미저장 편집 복구")에서 백업을 고르면 미저장 상태의 이름 없는 문서로 열리고, 저장 위치는 Save As에서 정한다. 수동 복구 #4118.
그 과정에서 엉뚱한 사실도 드러났다. 일부 편집기 테스트가 백업 폴더를 고정하지 않아 사용자의 실제 백업 폴더에 레코드를 쓰고 있었다. 10월 4일에 확인한 4,200개 레코드는 전부 테스트 산물이었다. 테스트 빌드가 실제 폴더로 돌아가지 않게 막고, 같은 날 테스트 안에서 도는 세션 호스트가 실제 사용자 캐시에 훅 로그 폴더를 만들던 것도 막았다. 테스트 백업 폴더 #4115, 훅 로그 캐시 #4119.
한글을 입력하는 자리 (9/27 ~ 10/2)
편집기에서 한글 조합을 Enter로 확정하면, 공통 IME 경로가 편집기를 터미널처럼 다뤄 확정 문자열을 PTY 큐로 보냈다. 문서는 글자를 받지 못하고 조합 표시만 지워졌다. 이어서 여러 줄 선택의 재변환, ⌥Return의 한자 후보, 선택 끝이 접힌 줄에 있을 때의 조합을 고쳤다. Enter 확정 #3968, 재변환·한자 #3981.
다중 커서에서는 확정 전 ㅎ → 하 → 한이 주 커서에만 보였다. 이제 같은 조합 글자를 모든 커서 위치에 표시하고, 확정은 한 번의 편집과 한 번의 Undo로 처리한다. 다만 입력기에는 여전히 하나의 marked range만 보고하므로, 보조 커서의 조합은 화면에 비춘 것이다. 확정이 읽기 전용이나 메모리 부족으로 거절되면 조합과 입력 포커스를 그대로 두게 한 것도 이 흐름이다. 모든 커서에 조합 표시 #4000, 확정 거절 #4062.
명령 팔레트는 글자 순서대로 찾는 fuzzy 필터와 한국어 검색 별칭을 얻었다. nwt로 New Terminal을 찾고, 한글로 "새 터미널"을 조합하는 동안 결과가 좁혀진다. 초성 검색은 지원하지 않는다. 사이드바의 워크스페이스 이름을 바꿀 때 ←·→로 커서가 움직이지 않던 문제도 고쳤다. 팔레트 검색 #3995, 이름 변경 커서 #4003.
찾기와 그리기 예산 (9/27 ~ 10/4)
편집기와 터미널 스크롤백의 찾기에 PCRE2 정규식 모드를 선택적으로 붙였다. 기본은 계속 평문이고, 입력칸 옆 .* 버튼이나 ⌥⌘R로 전환한다. 편집기에서는 캡처 그룹으로 바꾸기까지 되며, 정규식은 한 논리 줄 안에서만 찾는다. 원격의 구버전 호스트에서는 정규식을 쓸 수 없다고 표시한다. 읽기 전용 비교 뷰는 좌우 열이 서로 다른 검색어를 갖게 됐고, 대소문자 무시 찾기는 Ł·Ž 같은 Latin Extended-A 글자를 접는다. 편집기 정규식 #3988, 스크롤백 정규식 #3993, 비교 뷰 찾기 #4027, 대소문자 접기 #4117.
검색 결과가 빽빽한 화면에서는 다른 한계가 나왔다. 3-8의 줄 번호 결함이 텍스트 저장소의 몫이었다면, 이번에는 그리기 명령 저장소다. 편집기 pane의 고정 저장소는 2,560개인데, 80행 × 160열 재현 화면은 온전히 그리려면 9,762개가 필요했다. 그런데도 화면이 잘렸다는 신호는 거짓이었다. 먼저 누락을 보고하게 하고, 저장 방식 후보를 실험으로 비교했다. 이어진 검색 강조는 하나로 합쳐 줄였지만, 떨어진 검색과 진단이 많은 화면은 여전히 큰 저장소가 필요하다. B2의 용량 해결은 후속이다. 누락 보고 #4002, 후보 비교 #4004, 전체 프레임 실험 #4016, 강조 병합 #4010.
괄호 판정 통일 #3964, 세로 이동 #3966, ⌘⌫ #3970, 커서 깜빡임 #3971, 공백 표시 #4006, 심볼 미리 보기 #4131.
세션 호스트는 하나여야 한다 (9/27 ~ 10/1)
앱을 종료해도 셸을 살려 두는 세션 호스트는 로그인 세션마다 하나여야 한다. 그런데 9월 30일 실측에서는 호스트 넷이 각자 다른 빌드로 세션을 쥐고 있었다. 새 빌드는 기존 호스트를 제자리에서 교체해야 하는데, 교체 스캔은 늘 실패하는 호스트 하나를 만나면 끝났고, 실패하면 앱이 조용히 새 호스트를 하나 더 띄웠다. 9월 28일에는 셸 17개가 붙은 호스트가 미리 잰 크기보다 3바이트 커진 상태 때문에 승계를 통째로 버리고 새 호스트를 띄우기도 했다. 이제 승계 예약에는 여유를 두고, 스캔은 실패한 호스트를 넘어 계속 돌며, 교체에 실패해도 살아 있는 호환 호스트가 있으면 거기에 새 탭을 연다. 예약 여유 #3994, 스캔 수정 #4023, 호스트 재사용 #4030.
연결이 끊기는 경로도 하나씩 좁혔다. 9월 28일에는 런타임 16개를 든 호스트가 GUI 연결을 통째로 닫아, 셸은 살아 있는데 탭 16개가 한꺼번에 죽은 것처럼 보였다. 창 크기가 A에서 B로 갔다가 A로 돌아오면 격자는 같지만 세대 번호는 2가 올라, 서버가 이를 어긋남으로 판정할 수 있었다. 세대가 다르면 스냅샷을 보내게 고쳤고, 그런 어긋남이 다시 생겨도 그 스트림 하나만 다시 맞추게 피해 범위를 가뒀다. 다만 9/28 사건이 이 원인이었다는 직접 증거는 없다고 PR에 적었다. 런타임이 8개 이상인 호스트가 연결 오류 뒤 재접속을 영영 시작하지 못하던 예산 문제도 고쳤다. 세대 비교 #4001, 스트림 격리 #4008, 재접속 예산 #4011.
원인을 고치기 전에 원인을 볼 수 있게 한 PR도 많았다. 승계 실패가 BadMagic이나 upgrade_busy 한 이름으로 접히던 자리, 기대값과 실제값을 버리던 자리, 로그 없이 끝나던 세 자리에 정보를 실었다. 승계한 호스트가 /tmp 정리로 owner.lock 경로를 잃던 문제와, 호스트가 시작할 때 자기 실행 파일을 다섯 번 해시하던 비용도 이 주에 정리됐다. 실패한 호스트를 나중에 다시 교체하는 정책은 문서로만 정했고 구현은 남아 있다. preflight 진단 #3976, 승계 거절 사유 #3980, 어긋남 수치 #3997, 로그 없는 종료 #4014, owner.lock #3979, 해시 한 번 #4053, 재교체 정책 #3942.
터미널이 알리는 것 (9/27 ~ 10/4)
Claude Code 같은 TUI가 OSC 8 링크를 아예 보내지 않던 원인은 렌더링이 아니라 이름이었다. 앱들은 TERM_PROGRAM 화이트리스트로 하이퍼링크 지원을 짐작하는데 maru는 어느 명단에도 없었다. 신원을 위장하는 대신 FORCE_HYPERLINK=1로 능력을 알리고, 알림처럼 그런 채널이 없는 기능을 위해 term-program 설정을 탈출구로 열었다. 하이퍼링크 능력 #3972, term-program 설정 #3974.
크기 통지 프로토콜 DECSET 2048도 지원했다. SIGWINCH는 PTY를 직접 쥔 자식만 받기 때문에, ssh 너머나 멀티플렉서 안의 앱에는 바이트로 오는 통지가 확실하다. 글꼴 크기를 바꾸면 격자와 셀 픽셀이 따로 도착해 실재하지 않은 크기로 통지가 한 번 더 가던 문제를, resize에 셀 픽셀을 함께 싣는 방식으로 고쳤다. 옛 호스트와는 호환을 위해 통지가 두 번 간다. DECSET 2048 #4085, 통지 한 번 #4088, 명령 순서 #4098.
kitty 그래픽 이미지는 글자 칸에 붙어 있어야 하는데, 창 폭이 바뀌어 줄이 다시 잘려도 아무도 옮기지 않았다. reflow와 스크롤백 재배치, 스크롤 영역과 줄 삽입·삭제를 따라가게 하고, 화면별로 배치를 나눴다. reflow 추종 #4074, 화면별 배치 #4108, 스크롤 영역 #4123.
몇 시간 켜 둔 앱에서 한글이 엉뚱한 글리프로 그려진다는 제보도 있었다. 사이드바만 다시 그리는 부분 투영이 글리프 아틀라스를 다 써 버리면 그 배치를 버리는데, 아틀라스는 그 글리프를 "올라감"으로 기억해 다음 전체 투영이 올리지 않았다. GPU 텍스처의 그 자리에는 이전 세대의 다른 글리프가 남아 있었다. PR은 한글과 탭이 많을수록, 보이지 않는 탭의 출력이 부분 투영으로 들어오며 이 경로를 자주 지난다고 적었다. 이미 깨진 창은 글자 크기를 바꿨다 되돌리면 복구된다. 아틀라스 수정 #4009.
알림과 링크의 빈자리도 메웠다. 앱을 다시 실행한 뒤 이전 실행의 알림을 누르면 재사용된 surface ID 때문에 다른 터미널이 선택될 수 있었고, Quick 패널이 숨어 있는 동안 셸이 보낸 알림은 패널을 다시 열어야 전달됐다. 모바일 터미널에서는 화면의 웹 링크를 탭하면 열리게 됐다. 알림 클릭 대상 #4105, 숨은 Quick 알림 #4116, 모바일 링크 탭 #4064.
설정과 에이전트 (9/28 ~ 10/4)
설정 쪽에서는 미뤄 둔 토글이 열렸다. 환경 변수로만 끌 수 있던 작업공간 자동 복원은 workspace.restore로 끄고, 복원이 실패하면 몇 번째 탭·pane에서 왜 실패했는지를 로그에 남긴다. ⌘+·⌘-의 보폭을 increase_font_size:N으로 바인딩에 싣고, window.quit-after-last-window-closed = false면 마지막 창을 닫아도 앱이 Dock에 남는다. 글꼴 보폭·대소문자 접기·Dock 유지 세 PR에 적대적 검증을 여덟 번 더 돌려, 답하지 않고 사라진 ⌘Q 확인이 종료를 영영 막던 길도 막았다. 복원 토글 #4060, 복원 실패 기록 #4063, 글꼴 보폭 #4112, Dock 유지 #4120, ⌘Q 후속 #4129.
에이전트 쪽은 Codex가 많았다. 질문이 입력 대기로 잡히지 않던 것과 완료 알림이 다음 작업 중에 가던 것을 고쳤다. 턴 ID가 빠진 새 프롬프트 뒤 이전 턴의 늦은 종료 이벤트가 새 턴을 완료로 처리하던 것, 다른 tmux pane이 실행 중이면 Codex 질문 알림이 버려지던 것도 막았다. 시작 정리가 방금 만든 훅 기록 폴더까지 지워, Codex가 질문을 기다리는데도 사이드바에 에이전트 행이 나타나지 않던 경로도 있었다. 원격 훅 설치가 시한을 넘기면 그 목적지를 영구히 막던 것은 백오프 재시도로 바꿨고, 원격 갤러리가 느리거나 실패할 때 아무 기록도 남지 않던 자리에는 측정과 실패 사유를 실었다. Codex 알림 #4073, 턴 ID 격리 #4079, pane 질문 알림 #4083, 사이드바 행 #4092, 원격 훅 재시도 #4087, 원격 그림 측정 #3991, 원격 그림 실패 #3992.
초록이 거짓말할 때 (9/27 ~ 10/4)
경계 판정자는 소스를 글자로 훑어 배선이 아직 있는지 잰다. 9월 27일 두 PR에 적대적 검증을 세 바퀴 더 돌리자 돌연변이 열 개가 초록으로 살아남았다. 제품이 함수를 쓰지 않고 인라인하거나, 호출을 감싼 조건을 뒤집어도 게이트가 빨개지지 않았다. 판정자를 효과를 재는 쪽으로 고친 뒤, kitty 그래픽 계획서 한 문단에 묻혀 있던 규칙을 PR 체크리스트의 단일 출처로 세웠다. "있다"가 아니라 "효과가 난다"를 재라는 것이다. 판정자 수정 #3982, 판정자 규율 #3983, .skip 판정 #3987.
CI도 손봤다. 9월 27일부터 30일까지 main push 51번 중 22번이 실패했고, 가장 긴 구간은 약 22시간 동안 빨간 채로 아무도 몰랐다. main이 빨개지면 이슈 하나로 알리게 했다. 외부 다운로드가 끊겨 잡이 실패하던 것은 의존성 20개의 tarball을 저장소에 두어 없앴고, PR에서 같은 tree를 이미 검증했으면 main push의 macOS 잡을 건너뛴다. 고정된 짧은 대기로 실제 작업을 기다리던 판정자들은 준비됐다는 사실을 확인한 뒤 단언하게 바꿨다. 실패 알림 #4028, 의존성 동봉 #4050, 같은 tree 건너뛰기 #4057, 준비 대기 #4128.
개발 브랜치의 Chromium 탭 (9/26 ~ 10/4)
앱 안의 Chromium 웹 패널은 여전히 docs/web-panel-osr-backend 브랜치에서 진행 중이다. #3963 이후 10월 4일까지 그 브랜치를 대상으로 21개 PR이 병합됐고(바로 직전인 9월 26일 오후의 W5c를 더하면 22개), main으로 합치는 통합 PR #3867은 10월 5일 확인 시점에도 열려 있다. 따라서 아래 내용은 main에 반영되지 않은 개발 브랜치의 작업이다.
- 엔진 배포: 엔진
maru-chromium을 Homebrew로 설치해도 서명이 깨지지 않게 설치물을 미리 고쳐 서명하고, 실행 전 검증과 크래시 처리를 더했다. W7a1 #4005, W7a2 #4015, W7b #4031. - 브라우저다운 동작: 웹 알림,
select목록, 툴팁, macOS 우클릭 메뉴, 안팎으로 끌어 놓기, 페이지가 연 새 탭을 차례로 붙였다. 원래 페이지와 이어진 팝업은 제품 동작이 바뀌지 않는 sidecar 쪽 첫 조각(W6f①)까지 들어왔다. W5c #3961, W6a② #4061, W6b #4069, W6c② #4089, W6d① #4104, W6d② #4114, W6e #4130, W6f① #4136.
10월 4일 시점
이 주에는 같은 대상을 두 번 갖지 않도록 구조를 고쳤다. 문서는 정본 하나에 뷰 여럿, 백업은 문서마다 ID 하나, 세션 호스트는 로그인 세션마다 하나다. 남은 것도 분명하다. 공유 분할은 같은 창의 로컬 파일까지이고, 그리기 저장소 B2의 용량과 실패한 호스트의 재교체는 후속이다. Markdown 소스를 네이티브 편집기로 옮기는 계약 검토안과 Windows 편집기·파일 처리 PR은 열린 채 미병합이다. Markdown 이관 검토 #3967, Windows 보완 #4084.