---
name: tui-chat-design
description: "masc TUI 채팅 화면이 무엇을 어떤 기호로 그리는지 정리한 표와 규칙이다. 말한 사람 마크, 턴 레일, 왼쪽 여백의 순서, 시계 칸, Ctrl-F 밀도 단계, 줄 문구를 바꾸거나 새 기호를 넣기 전에 쓴다. 바꾼 화면을 실제로 띄워 확인하는 방법은 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` | 오류 |

### 이 표를 만든 규칙 둘

- **기호는 도형이다.** `?` 같은 문장 부호는 쓰지 않는다. 끝난 줄에 물음표가 남으면 아무도 답하지
  않은 것처럼 읽힌다. 화면의 메모가 `○` 인 이유다.
- **채운 것과 빈 것이 짝이다.** `●`/`○`, `◆`/`◇` 에서 채운 쪽은 그 일 자체이고, 빈 쪽은 그 일에
  대한 화면의 메모다. 새 상태가 필요하면 이 짝으로 말할 수 있는지 먼저 본다.

### 색은 구분의 근거가 못 된다

`NO_COLOR` 가 켜지면 색이 사라진다. 그래서 누가 말했는지는 색이 아니라 마크가 답해야 한다.
색으로만 구분되는 상태를 만들면 색 없는 터미널에서는 그 상태가 안 보인다.

## 턴 레일 — `turn_rail_glyph`

레일은 "이 줄이 어느 턴에 속하나" 한 가지에만 답한다. 누가 말했나는 마크가 답한다.
한 기호에 두 질문을 싣지 않는다.

| 기호 | 값 | 뜻 |
|---|---|---|
| `╭` | `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칸이다. 칸 폭이 줄마다 다르면 본문 줄바꿈도 줄마다 달라진다.
3. **마크 + 띄움 + 라벨** — 라벨 칸은 `chat_role_label_column = 10`. 마크 칸은 라벨 칸 안에서 뺀다.
   이름이 넘치면 가운데를 줄인다(`fit_middle`). 마크는 줄이는 범위 밖에 있어서 긴 이름에서도 남는다.
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` | 전체 타임스탬프와 request id 를 자기 줄에 따로 그린다 |
| `Origin_bare` | 시계를 떼고 마크와 이름만 남긴다 |

Ctrl-F 는 `Origin_inline → Origin_row → Origin_bare → Origin_inline` 으로 돈다
(`Masc_tui_types.next_origin_display`). 기본값은 `Masc_tui_types` 의 상태 생성에서 정한다.

기본값 하나만으로 읽을 수 있어야 한다. "Ctrl-F 누르면 보인다" 는 기본값이 모자란다는 뜻이다.

## 줄의 문구

- 게이트 줄은 가운뎃점으로 소식과 대상을 잇는다. 앞이 소식, 뒤가 대상이다
  (`bin/masc_tui_gate_text.ml`: `승인됨 · 적용 예정`, `판정 중 · 이 호출은 미뤄짐`).
- 가운뎃점은 서로 다른 칸을 가르는 데만 쓴다. 한 상태에 사실이 둘이면 쉼표로 잇는다
  (`bin/masc_tui_keeper_chat_transcript.ml`: `읽음, 전달 확인 중`, `전달됨, 도구 안 씀`).
  `읽음 · 전달 기록 없음` 이라고 쓰면 상태 하나인지 둘인지 읽는 사람이 알 수 없다.
- 상태 이름은 모델이 무엇을 했는지로 쓴다(`읽음`, `전달됨`, `도구 씀`). 서버가 보내고 받은 동사로
  쓰면 누가 누구에게 보냈는지 운영자가 알 수 없다.
- 한국어는 한국 사람이 실제로 쓰는 말로 쓴다. 영어 라벨을 품사째 옮기지 않는다.
- 한글 문구는 소스에 리터럴로 쓴다. 바이트 이스케이프(`\xeb\x83\xa8`)로 손으로 적지 않는다.
  손으로 맞추다 틀리기 쉽다.

## 고치고 나면

- 화면은 코드를 읽어서 판정하지 않는다. `tui-pty-scenario` 스킬로 실제로 띄워 본다.
- 문구·기호·폭을 바꿨으면 `test/*.py` 가 기다리는 글자열(needle)에서 옛 문구와 옛 기호의 바이트열을
  찾아 같이 고친다. 렌더 파일만 고친 PR 은 PR CI 가 키보드 산책을 돌리지 않는다.
- 하네스가 못 보는 것은 사람에게 묻는다: 색, 사용자 글꼴이 `╶ ╌ ┊ ◈ ○` 를 그리는지, 실제 폭에서 읽히는지.
