git:20261009.4760c4e to git:20261009.7a8219f

62 added, 165 removed. Audit A to A.

---
name: tui-chat-design
- description: "masc TUI 채팅 화면이 무엇을 어떤 기호로 그리는지 정리한 표와 규칙이다. 말한 사람 마크, 턴 레일, 왼쪽 여백의 순서, 시계 칸, Ctrl-F 밀도 단계, 밝기 위계와 끝난 일의 접힘, 줄 문구를 바꾸거나 새 기호를 넣기 전에 쓴다. 바꾼 화면을 실제로 띄워 확인하는 방법은 tui-pty-scenario 에 있다."
+ description: "MASC TUI 채팅의 기본 대화 레이아웃, 상세 보기, 저널, 입력·진행 정보 배치 규칙. 채팅의 문구·폭·밀도·기호를 바꾸기 전에 읽는다. 실제 실행 검증은 tui-pty-scenario 를 따른다."
---
- # masc 채팅 화면의 기호와 규칙
-
- 이 화면에는 이미 정해진 기호 체계가 있다. 새 기호를 만들기 전에 있는 기호로 말할 수 있는지 먼저 본다.
- 아래 값의 출처는 `bin/masc_tui_message_layout.ml` 과 `bin/masc_tui_render_chat.ml` 이다.
- 이 문서와 코드가 다르면 코드가 맞다. 그 자리에서 이 문서를 고친다.
-
- ## 말한 사람 — `speaker_mark`
-
- | 기호 | 스타일 | 뜻 |
- |---|---|---|
- | `▶` | `User` | 운영자가 보냈다 |
- | `◀` | `Inbound` | 바깥의 누군가가 여기로 보냈다 |
- | `●` | `Keeper` | 키퍼가 말한다 |
- | `○` | `Status` | 그 턴에 대한 화면의 메모. 키퍼가 한 말이 아니다 |
- | `›` | `Local` | 입력줄 프롬프트와 같은 기호. 방금 친 것에 화면이 답한다 |
- | `◈` | `Journal` | 나란히 도는 Memory 저널 레인 |
- | `■` | `Tool` | 도구 |
- | `◇` `◆` `△` `✗` | `Skill _` | 스킬: 도는 중 / 썼음 / 볼 것 / 실패 |
- | `·` | `Thinking` | 생각 중 |
- | `✗` | `Error` | 오류 |
-
- 같은 `✗` 는 Overview 의 TUI Session Events 패널에서도 오류를 뜻한다. `event_type = "error"` 인 행만
- 시계 뒤에 `✗ ` 를 달고, 나머지 레벨은 표식 없이 그린다(`Masc_tui_types.overview_event_mark`).
-
- ### 이 표를 만든 규칙 둘
-
- - **기호는 도형이다.** `?` 같은 문장 부호는 쓰지 않는다. 끝난 줄에 물음표가 남으면 아무도 답하지
- 않은 것처럼 읽힌다. 화면의 메모가 `○` 인 이유다.
- - **채운 것과 빈 것이 짝이다.** `●`/`○`, `◆`/`◇` 에서 채운 쪽은 그 일 자체이고, 빈 쪽은 그 일에
- 대한 화면의 메모다. 새 상태가 필요하면 이 짝으로 말할 수 있는지 먼저 본다.
-
- ### 색은 구분의 근거가 못 된다
-
- `NO_COLOR` 가 켜지면 색이 사라진다. 그래서 누가 말했는지는 색이 아니라 마크가 답해야 한다.
- 색으로만 구분되는 상태를 만들면 색 없는 터미널에서는 그 상태가 안 보인다.
-
- ## 밝기는 위계다
+ # 대화를 먼저 읽는 채팅
- 모양이 무엇인가를, 색이 누구인가·어떤 상태인가를 말한다면, 밝기는 위계만 말한다.
- 밝은 자리는 화면에 몇 개 없어야 한다.
+ 정본은 `bin/masc_tui_message_layout.ml`, `bin/masc_tui_render_chat.ml`,
+ `bin/masc_tui_types.ml`이다. 코드와 다르면 이 문서도 함께 고친다.
- - **말만 전경이다.** `User`·`Inbound`·`Keeper` 본문은 터미널 전경을 유지한다. `Local` 은
- 명령에 대한 답이라 읽는 글이라 누르지 않는다.
- - **일·소식·생각은 한 단계 아래다.** `Tool`·`Skill`·`Journal`·`Thinking` 본문은 dim 이다
- (`Chat_theme.body`, `bin/masc_tui_ansi.ml`). dim 본문 안의 bold·링크가 닫힐 때 dim 을 다시
- 연다 — 스팬 하나가 그 뒤의 줄을 전부 밝히지 못한다.
- - **크롬은 가장 조용하다.** 날짜 레일, 시계, 곁줄, 여백 라벨, 브레드크럼은 `Theme.recede ()`
- 단계다. 스크롤 방향타로는 남지만 밝은 자리를 쓰지 않는다.
- - **실패는 이 규칙의 예외다.** `✗` 와 `failed` 절은 reset→bold→bright red 로 서고, 실패 줄은
- 접히지 않는다. 성공이 가라앉는 만큼 실패가 도드라져야 한다.
+ ## 기본 화면
- ## 턴 레일 — `turn_rail_glyph`
+ - `Origin_bare`가 기본이다. 운영자 `›`, Keeper `●`, 외부 발신자 `◀`를 작은 여백에 둔다.
+ - 외부 발신자는 본문 위에 이름·출처를 남긴다. 운영자와 다른 발신자를 합치지 않는다.
+ - 본문 시작 열은 동일하다. 시계·넓은 역할 열·턴 레일·TOOLS 같은 종류 라벨은 기본으로 그리지 않는다.
+ - 도구 본문은 이미 호출 이름과 결과 기호를 갖는다. 별도 `■ TOOLS` 열을 반복하지 않는다.
+ - 읽기 폭은 최대 100 terminal cells다. 한국어·이모지·결합문자는 display width로 센다.
+ - 대화 사이의 한 줄 여백은 `Spacing`이다. 대화 맨 끝에는 여백을 세지 않으며, 큰 메시지를 잘라 보여줄 때는 첫 줄과 최신 본문을 보존한다.
+ - 사용자와 Keeper 본문은 원문이다. 요청 ID, TURN 번호, 처리 확인·시각 안내를 본문에 덧붙이지 않는다.
+ 시간은 여백·헤더에, 기술 식별자는 본문 뒤에 오는 명시적 진단에 둔다.
+ 그룹 키·인과 관계·검색과 스크롤의 원본 identity는 그대로 유지한다.
+ - 아직 처리되지 않은 입력은 `전송 대기`·`전송 중`·`처리 대기`·`전송 확인 중` 상태를 계속 표시한다. 접수를 턴 반영으로 바꾸어 말하지 않는다.
+ - 글꼴 크기·자간·행간은 터미널 에뮬레이터의 설정이다. 앱은 셀 배치와 여백을 제어한다.
- 레일은 "이 줄이 어느 턴에 속하나" 한 가지에만 답한다. 누가 말했나는 마크가 답한다.
- 한 기호에 두 질문을 싣지 않는다.
+ ## 보기 전환
- | 기호 | 값 | 뜻 |
+ | 키 | 순서 | 기본 |
|---|---|---|
- | `╭` | `Rail_opens` | 턴이 이 줄에서 시작한다 |
- | `│` | `Rail_says` | 턴 안의 말(운영자·키퍼·상태 등) |
- | `├` | `Rail_does` | 턴 안의 작업(도구·스킬·생각) |
- | `╶` | `Rail_stands` | 한 줄짜리 턴인데 그 줄이 작업이다 |
- | `╰` | `Rail_closes` | 턴이 이 줄에서 끝났다 |
- | `┤` | `Rail_joins` | 어느 턴에도 속하지 않은 줄이 선에 닿는다 |
- | 빈칸 | `Rail_none` | 한 줄짜리 말, 또는 턴 밖의 줄 |
-
- - `┤` 앞에는 세 칸짜리 곁줄이 붙는다. 저널은 점선 `╌╌╌`, 다른 사람이 보낸 줄은 실선 `───` 이다.
- 종류는 기호가 아니라 선 모양으로 말한다. 마크와 라벨이 이미 누구인지 말하기 때문이다.
- - 괄호(`╭`…`╰`)는 **붙어 있는 줄에만** 친다. 턴 밖의 줄(저널 기록, 남의 broadcast)은 괄호를 끊지
- 않고 지나간다. 다른 턴의 줄이 끼어들어 한 턴이 둘로 갈라지면, 괄호 대신 외톨이 줄 두 개로 그린다.
- 한 칸짜리 세로줄로는 떨어진 줄을 한 턴이라고 말할 수 없다.
- - 한 줄짜리 말은 레일을 그리지 않는다. 대화의 거의 모든 줄에 레일이 붙으면 아무도 레일을 안 본다.
-
- ## 왼쪽 여백의 순서는 고정이다
-
- 한 줄은 왼쪽부터 이 순서다. 순서를 바꾸면 세로줄이 어긋난다.
-
- 1. **레일 여백** — `turn_rail_cells = 5`. 곁줄 3칸 + 기호 1칸 + 띄움 1칸.
- 곁줄이 없는 줄도 5칸을 다 쓴다. 폭이 줄마다 달라지면 스크롤할 때 본문 줄바꿈이 바뀐다.
- 2. **시계** — `chat_clock_column = 5` 칸 + 띄움 1칸. `Origin_inline` 에서만 그린다.
- 모든 줄의 시계 칸은 같은 5칸이다. 칸 폭이 줄마다 다르면 본문 줄바꿈도 줄마다 달라진다.
- 시계는 recede 단계로 눌려 있고, 마크만 말한 사람의 색을 지킨다
- (시계 칸의 경계는 `gutter_clock_cells`).
- 3. **마크 + 띄움 + 라벨** — 라벨 칸은 `chat_role_label_column = 10`. 마크 칸은 라벨 칸 안에서 뺀다.
- 이름이 넘치면 가운데를 줄인다(`fit_middle`). 마크는 줄이는 범위 밖에 있어서 긴 이름에서도 남는다.
- 도구·스킬·생각 줄은 각각 `TOOLS`·`SKILL`·`THINKING` 라벨을 함께 쓴다.
- 기본 화면에서 기호를 외우지 않아도 레인의 뜻을 읽을 수 있어야 한다.
- 4. **인용 막대** — 남이 쓴 글(`Tool`·`Skill`·`Status`·`Local`)은 흐린 `│`, `Journal` 은 `┊`,
- 말(`User`·`Inbound`·`Keeper`·`Error`·`Thinking`)은 빈칸 두 칸이다.
- 5. **본문**
-
- 폭이 모자라면 레일을 먼저 버린다. 그래도 이름이 안 들어가면 시계와 마크를 버리고 이름을 줄여 남긴다.
- 누가 말했는지가 언제 말했는지보다 중요하다.
-
- ### 같은 값을 반복해서 그리지 않는다
-
- - 시계는 **분이 바뀐 줄에만** 그린다. 같은 분에 여섯 줄이 오면 한 줄만 시각을 달고 나머지는 빈칸이다.
- 칸은 그대로 비워 둬서 아무것도 밀리지 않는다.
- - 같은 사람이 이어서 말하면 이름을 지운다. 마크도 가장 조용한 기호로 바뀐다
- (`continued_mark`: `User`·`Inbound`·`Keeper`·`Tool` 은 `│`, `Thinking` 은 `·`, 나머지는 원래 마크).
-
- ## 밀도는 세 단계이고 Ctrl-F 로 돈다
-
- | 단계 | 모양 |
- |---|---|
- | `Origin_inline` | 출처를 여백에 접고, 분이 바뀐 줄에 짧은 시계를 단다 |
- | `Origin_row` | 헤더 한 줄: 이름 전체, 오른쪽 끝 recede 시계, 사이 rule |
- | `Origin_bare` (기본) | 시각·시간 구분선·진행 타이머를 숨기고 마크와 이름을 남긴다 |
-
- Origin_row 헤더의 시각 칸이 비면(`timeline_bucket = None`) 시계를 안 그리고 `--:--:--`
- 자리표도 쓰지 않는다. 같은 화자가 이어지는 행은 이름 없이 rule 과 시계만 그린다
- (`metadata_row`, `bin/masc_tui_render_chat.ml` 의 `origin_heading`).
+ | Ctrl-F | 대화 → inline metadata → origin heading → 대화 | `Origin_bare` |
+ | Ctrl-N | 숨김 → 요약 → 전체 → 숨김 | `Memory_hidden` |
+ | Ctrl-D | compact → results → full → compact | `Tools_compact` |
+ | Ctrl-R | folded → full → hidden → folded | `Reasoning_folded` |
+ | Ctrl-S | 턴 상세 접기/펼치기 | 기존 상태 유지 |
- Ctrl-F 는 `Origin_bare → Origin_inline → Origin_row → Origin_bare` 으로 돈다
- (`Masc_tui_types.next_origin_display`). 기본값은 `Masc_tui_types` 의 상태 생성에서 정한다.
+ 기본 설정을 알리는 `metadata:off`, `journal:off` 배지를 붙이지 않는다.
+ 사용자가 연 상세 모드만 제목 옆에 표시한다. 모든 단축키는 도움말에서 찾는다.
- 기본값 하나만으로 읽을 수 있어야 한다. "Ctrl-F 누르면 보인다" 는 기본값이 모자란다는 뜻이다.
+ ## 상세 레이아웃
- 사용자와 Keeper의 본문은 기록된 원문이다. 요청 식별자, 턴 번호, 처리 확인 문구,
- 시도·시각 안내를 본문에 덧붙이지 않는다. 원문에 `요청`·`입력 반영됨` 같은 말이
- 있으면 그대로 둔다. 그룹을 묶는 키는 전체 요청 식별자이며 표시용 축약값으로 바꾸지 않는다.
- 시간은 여백·헤더에, 기술 식별자는 명시적으로 펼친 진단에 둔다.
- 펼친 진단 줄은 본문이 다 끝난 뒤에 온다. 대기 입력도 펼쳤을 때 자기 요청 식별자를 진단 줄로 보인다.
+ `Origin_inline`은 시계, 화자 열, 턴 레일을 표시한다. 같은 분의 시계와 같은 화자의 이름은 반복하지 않는다.
+ `Origin_row`는 화자와 시각을 별도 헤더에 표시한다. 소유 Keeper의 이름은 대화 제목과 중복하지 않는다.
- 아직 처리 시작을 확인하지 못한 입력은 `대기 입력 N건` 아래에 두고 `›` 마크를 쓴다.
- 각 입력의 원문 옆 라벨에 `전송 대기`·`전송 중`·`처리 대기`·`전송 확인 중`을 표시한다.
- 서버가 입력을 받았다는 사실은 Keeper가 처리했다는 뜻이 아니다. 영속된 입력 또는
- 해당 실행의 `Run_started`가 확인되면 일반 대화에 한 번만 표시하며 처리 확인 문구는
- 붙이지 않는다. 거절 사유는 본문과 분리된 오류 행에 남긴다.
- Thinking은 기본적으로 접어서 표시한다. `Ctrl-R`은 접힘 → 전체 → 숨김 순서로 돈다.
- Working은 턴이 진행 중이라는 뜻이다. Thinking·답변 델타를 실제로 받으면 진행줄에
- `THINKING`·`STREAMING`을 표시한다.
+ 상세 표식: 운영자 `▶`, 외부 발신자 `◀`, Keeper `●`, 상태 `○`, 로컬 명령 `›`,
+ 저널 `◈`, 도구 `■`, 추론 `·`, 오류 `✗`. 스킬은 상태에 따라 `◇ ◆ △ ✗`를 쓴다.
+ 색이 없어도 의미를 구별할 수 있어야 한다.
- ## 끝난 일은 접힌다
+ 턴 레일은 `╭ │ ├ ╶ ╰`로 같은 턴의 인접 행을 연결한다. 외부 도착은 `───┤`,
+ 저널은 `╌╌╌┤`로 구분한다. 원본 identity로 묶으며 표시 문자열로 그룹을 결정하지 않는다.
- 끝난 일은 화면의 주인공이 아니다. 무엇이 남고 무엇이 접히는지는 정해져 있다.
+ ## 정보의 밝기
- - **끝난 스킬은 요약 한 줄이다.** `전달됨, 도구 씀 · 이름 · N action`
- (`Keeper_chat_transcript.skill_rows`). 전달만 됐는지 실제로 쓰였는지는 기본 밀도에서
- 읽혀야 한다 — 이름만 남기는 접힘은 금지다.
- - **`↳` 호출 줄, `proof · turn=… · use=…`, detail 줄은 Ctrl-D 뒤다.**
- `msg_tool_visibility = Tools_full` 일 때만 그린다. 새 키를 만들지 않고 기존 도구 펼침 축에
- 탄다.
- - **도구 블록은 기본 `Tools_compact` 다.** 반환된 호출은 inventory 헤더 한 줄로 접히고,
- 실패·미반환·진행 중 호출은 헤더의 절이 아니라 자기 마크의 독립 줄로 남는다 — 단 셋
- 이상 모였을 때만. 둘 이하면 쪼개도 Full 이 그리는 두 줄이라 접지 않고, 실패도 헤더가
- `1 failed: web_fetch` 처럼 이름을 불러 한 줄이다 (`project_tool_block` Compact 팔,
- `bin/masc_tui_keeper_chat_transcript.ml`).
- - **Ctrl-D 는 `Tools_compact → Tools_results → Tools_full` 로 돈다.** `Tools_results` 는
- 호출 이름·받은 상태와 짧은 결과를 보여 준다. `↩` 는 결과를 받았다는 뜻이고 성공을 보장하지 않는다.
- 확정된 실패는 `✗` 로 남고, 결과 본문은 상태 색상 문법으로 해석하지 않는다.
- - **돌아가는 동안에도 같은 토글로 그린다.** 라이브 블록과 확정된 블록은 같은
- `project_tool_block` / `skill_rows` 를 지난다. 줄이 접히는 시점은 완료가 아니라
- Ctrl-D 토글이다.
+ - 대화 본문은 전경색이다. 사용자가 보낸 글의 배경은 본문 영역 안에만 둔다.
+ - 도구·스킬·추론·저널은 dim이다. 강조나 링크가 닫힌 뒤에도 원래 밝기를 복원한다.
+ - 제목·시계·상태 크롬은 recede다. 진행줄 전체를 밝은 청록색과 대문자로 강조하지 않는다.
+ - 실패와 조치 요청은 보이게 남긴다. 내용을 보고 오류인지 추측하지 않고 타입을 사용한다.
+ - 실제 도구 결과는 Ctrl-D로 펼친다. 결과 수신 `↩`는 성공 판정이 아니다.
- ## 줄의 문구
+ ## 입력 주변
- - 게이트 줄은 가운뎃점으로 소식과 대상을 잇는다. 앞이 소식, 뒤가 대상이다
- (`bin/masc_tui_gate_text.ml`: `승인됨 · 적용 예정`, `판정 중 · 이 호출은 미뤄짐`).
- - 가운뎃점은 서로 다른 칸을 가르는 데만 쓴다. 한 상태에 사실이 둘이면 쉼표로 잇는다
- (`bin/masc_tui_keeper_chat_transcript.ml`: `읽음, 전달 확인 중`, `전달됨, 도구 안 씀`).
- `읽음 · 전달 기록 없음` 이라고 쓰면 상태 하나인지 둘인지 읽는 사람이 알 수 없다.
- - 상태 이름은 모델이 무엇을 했는지로 쓴다(`읽음`, `전달됨`, `도구 씀`). 서버가 보내고 받은 동사로
- 쓰면 누가 누구에게 보냈는지 운영자가 알 수 없다.
- - 한국어는 한국 사람이 실제로 쓰는 말로 쓴다. 영어 라벨을 품사째 옮기지 않는다.
- - 한글 문구는 소스에 리터럴로 쓴다. 바이트 이스케이프(`\xeb\x83\xa8`)로 손으로 적지 않는다.
- 손으로 맞추다 틀리기 쉽다.
+ - 현재 대화의 진행 상태는 한 줄로 표시한다. 현재 호출·응답 상태만 남긴다.
+ 진행 타이머(요청·호출 경과 시간, 무응답 시간)와 누적 도구 목록·런타임 ID는 상세 레이아웃에 둔다.
+ - 이어서 처리하기를 기다리는 요청은 진행줄을 그리지 않는다. 다음 실행이 시작되면 진행줄이 돌아온다.
+ - 같은 실행을 진행줄과 `기존 작업 처리 중`으로 중복 표시하지 않는다.
+ - 다른 Keeper의 실행과 `/interrupt <다른 이름>`은 입력 주변에 표시하지 않는다.
+ Keepers 목록·Activity에서 관측하고 제어한다. 추적 중인 요청은 삭제하지 않는다.
+ - 전송 결과 미확인, 중단 실패, 관측 실패, 승인 질문은 기본 화면에서도 보인다.
+ `Attention`은 기본 대화에서 상세를 접어도 남는다.
+ - 접힌 승인 기록이 있으면 진행줄의 `Ctrl-S:details`로 접근한다.
+ - 상태줄은 대화 대상의 health·mode·gate와 context를 표시한다. 상세 레이아웃에서는
+ 런타임·세부 사용량도 표시한다. roster 커서는 대화 대상을 바꾸지 않는다.
+ - 기본 footer는 전송·줄바꿈·중단/돌아가기·명령·도움말만 남긴다.
+ - 슬래시 메뉴와 녹음 안내는 관련 상태에서 footer를 대체한다.
+ - 메뉴가 실제로 표시될 때만 방향키·Tab·Enter·Esc를 가로챈다. 초안 보존과 전송 의미는 바꾸지 않는다.
- ## 채팅 입력과 상태의 자리
+ ## 검증
- - 탐색 제목은 대화 위에, runtime·context 값은 입력 프레임 아래에 둔다. 상태줄은 roster와 대화 패널 아래 전체 폭을 쓰며, 현재 대화 대상 Keeper를 이름으로 표시한다. roster 커서가 움직여도 대화 대상이 바뀌기 전에는 상태줄의 대상이 바뀌지 않는다.
- - 슬래시 후보는 입력 위에서 명령 이름과 설명을 두 칸으로 보여 준다. 선택된 행만 강조한다.
- - 방향키는 선택만 바꾼다. Tab·Enter는 초안에 넣고, 그 뒤 Enter가 기존 명령을 실행한다.
- - Esc는 열린 메뉴를 먼저 닫는다. 메뉴가 보이지 않는 폭·높이에서는 입력을 가로채지 않는다.
- - 메뉴는 대화 최소 높이를 보존한다. 전체 후보를 자르지 않고 선택 주위의 페이지를 보여 준다.
- - 사용량은 관측된 값만 표시한다. 상한이 없으면 토큰 수만, 읽을 수 없으면 unavailable을 표시한다.
+ 렌더러와 row budget은 같은 projection을 사용한다. 검색·스크롤·화면 너비·메모 캐시도
+ 같은 origin mode와 실제 행을 사용해야 한다. 복사는 원본 응답에서 한다.
- ## 고치고 나면
+ 폭·기호·기본 모드를 바꾸면 기존 unit/PTY 기대값도 함께 갱신한다. 좁은 화면의 첫/마지막
+ 본문, 외부 발신자 이름, 한글·이모지 줄바꿈, 전송 결과 미확인·중단 실패를 확인한다.
- - 화면은 코드를 읽어서 판정하지 않는다. `tui-pty-scenario` 스킬로 실제로 띄워 본다.
- - 문구·기호·폭을 바꿨으면 `test/*.py` 가 기다리는 글자열(needle)에서 옛 문구와 옛 기호의 바이트열을
- 찾아 같이 고친다. 렌더 파일만 고친 PR 은 PR CI 가 키보드 산책을 돌리지 않는다.
- - 하네스가 못 보는 것은 사람에게 묻는다: 색, 사용자 글꼴이 `╶ ╌ ┊ ◈ ○` 를 그리는지, 실제 폭에서 읽히는지.
+ `tui-pty-scenario`에 따라 실제 바이너리를 띄운 결과만 PTY 증거라고 부른다.
+ 소스 projection 미리보기와 실제 TUI 실행 화면은 구분한다. 로컬 Dune 빌드와 CI 실행 시점은
+ constitution의 execution_protocol을 따른다.