feat(kebab-tui): p9-fb-11 ask answer markdown rendering #72

Merged
altair823 merged 2 commits from feat/p9-fb-11-markdown into main 2026-05-03 03:37:04 +00:00
Owner

요약

도그푸딩 item 9 — TUI Ask 답변 본문이 raw markdown 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark 로 파싱 → ratatui Span/Line + p9-fb-14 Theme 변환. CLI 는 raw markdown 그대로 (terminal 호환성).

변경

  • kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>> 신규. pulldown-cmark 0.13 위에 build (이미 kebab-parse-md 가 사용 중).

    inline: bold/italic/strikethrough → modifier, inline code → Role::Hint, link → Role::CitationMarker + UNDERLINED.

    block: heading H1/H2 → Role::Heading, H3+ → Role::Title, list (bullet/ordered/nested), fenced code → indent + Role::Hint, blockquote , table |-separated, --- → 가로선.

  • streaming 안전성: 매 frame 재 parse, unterminated ** 등 incomplete inline 은 literal 글자 그대로 (글자 누락 X).

  • ask::push_turn_lines grounded 답변만 markdown 렌더. refusal/streaming 은 role color 시그널 보존 위해 raw. body 들은 indent.

  • CLI kebab ask 는 raw markdown 그대로 (변경 없음).

테스트

  • 신규 14 markdown unit (empty/plain/bold/italic/strike/code/link/H1/list/ordered/code fence/quote/table/streaming-incomplete/composite)
  • 기존 75 TUI 테스트 모두 통과
  • cargo clippy -p kebab-tui --all-targets -- -D warnings clean

문서

  • README kebab tui 행: markdown 렌더 + CLI raw 명시
  • HANDOFF: 2026-05-03 entry
  • spec status planned → in_progress
## 요약 도그푸딩 item 9 — TUI Ask 답변 본문이 raw markdown 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark 로 파싱 → ratatui Span/Line + p9-fb-14 Theme 변환. CLI 는 raw markdown 그대로 (terminal 호환성). ## 변경 - **`kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>`** 신규. pulldown-cmark 0.13 위에 build (이미 kebab-parse-md 가 사용 중). inline: bold/italic/strikethrough → modifier, inline code → `Role::Hint`, link → `Role::CitationMarker + UNDERLINED`. block: heading H1/H2 → `Role::Heading`, H3+ → `Role::Title`, list (bullet/ordered/nested), fenced code → indent + `Role::Hint`, blockquote `▎`, table `|`-separated, `---` → 가로선. - **streaming 안전성**: 매 frame 재 parse, unterminated `**` 등 incomplete inline 은 literal 글자 그대로 (글자 누락 X). - **`ask::push_turn_lines`** grounded 답변만 markdown 렌더. refusal/streaming 은 role color 시그널 보존 위해 raw. body 들은 ` ` indent. - **CLI `kebab ask`** 는 raw markdown 그대로 (변경 없음). ## 테스트 - 신규 14 markdown unit (empty/plain/bold/italic/strike/code/link/H1/list/ordered/code fence/quote/table/streaming-incomplete/composite) - 기존 75 TUI 테스트 모두 통과 - `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean ## 문서 - README `kebab tui` 행: markdown 렌더 + CLI raw 명시 - HANDOFF: 2026-05-03 entry - spec status planned → in_progress
altair823 added 1 commit 2026-05-03 03:33:46 +00:00
도그푸딩 item 9 — TUI Ask 답변 본문이 raw `**bold**` / `# Title` /
` ```code``` ` 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark
파싱 → ratatui Span/Line 변환.

## 핵심 변경

- **`kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>`**
  신규. pulldown-cmark = "0.13" (이미 kebab-parse-md 가 사용 중인
  버전) 위에 build.

  inline:
  - `**bold**` / `__bold__` → `Modifier::BOLD`
  - `*italic*` / `_italic_` → `Modifier::ITALIC`
  - `~~strike~~` → `Modifier::CROSSED_OUT`
  - `` `code` `` → `Role::Hint` (DIM 스타일 — 터미널 호환성 위해 bg
    color 보다 안전)
  - `[text](url)` → `Role::CitationMarker` + `Modifier::UNDERLINED`

  block:
  - heading H1/H2 → `Role::Heading` (Cyan + BOLD), H3-H6 → `Role::Title`
    (White + BOLD)
  - bullet list `-`/`*` → `- ` + 깊이별 indent
  - ordered list `1.` → 실제 번호 prefix + indent
  - fenced code block ``` ``` ``` → `  ` indented + `Role::Hint`
  - blockquote `>` → 좌측 `▎` bar (중첩 시 반복) + `Role::Hint`
  - table `| col |` → `| col1 | col2 |` 식 줄, `|` separator 색 강조
  - horizontal rule `---` → `─` × 40

- **streaming 안전성**: 매 frame 재 parse 가 spec — pulldown
  토크나이저가 µs/KB 라 비용 무시. unterminated `**` (사용자가 한창
  입력 중인 inline 가 닫히기 전) 은 pulldown 이 Text 로 처리 →
  literal `**` 그대로 표시 (글자 누락 X).

- **`ask::push_turn_lines` 통합**: grounded 답변에서만 markdown
  렌더 사용. refusal turn (`Role::Warning` override) 와 streaming
  turn (`Role::Hint`) 은 raw 로 두어 role color 시그널이 markdown
  스타일에 묻히지 않도록. body line 들은 `  ` indent 로 transcript
  에서 답변 본문 시각 구분.

- **CLI `kebab ask` 출력은 raw markdown** — 터미널 호환성 + pipe
  처리 시 안정성 위해 (ANSI escape 없이 plain text).

## 테스트 (markdown.rs 14 unit)

- empty input → 빈 라인 1 줄 (caller scroll/measure 안전)
- plain text → 단일 라인 + paragraph blank
- bold / italic / strikethrough / inline code → 해당 modifier 검증
- link → UNDERLINED 검증
- heading H1 → BOLD 텍스트 span
- bullet list `-` / numbered list `1./2.` → prefix 검증
- code fence body → 줄별 `  ` indent 보존
- blockquote → `▎` prefix
- 2x2 table → `|`-separated 줄 검증
- unterminated `**` → 글자 누락 없음 (streaming 안전성 회귀 방지)
- composite (heading + para + list + code) → 문서 순서 보존

기존 75 TUI 테스트 + 신규 14 markdown = 89 통과. clippy clean.

## 문서

- README `kebab tui` 행에 markdown 렌더 안내 + CLI 는 raw 명시
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-03 03:34:53 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — pulldown-cmark Event 매핑이 정석적이고 streaming 시 매 frame 재 parse 라는 결정도 합리적 (토크나이저 µs/KB). Role 매핑 (heading H1/H2 → Heading, H3+ → Title, link → CitationMarker+UNDERLINE, code → Hint) 도 p9-fb-14 의 role 의도와 잘 맞습니다.

actionable nit 3 건 inline. (a) heading 안의 link/code 가 heading 색에 가려짐, (b) Math/Footnote/TaskList silently drop 으로 답변 글자 사라짐 위험, (c) streaming 회귀 테스트의 OR 어설션이 약함.

회차 1 — pulldown-cmark Event 매핑이 정석적이고 streaming 시 매 frame 재 parse 라는 결정도 합리적 (토크나이저 µs/KB). Role 매핑 (heading H1/H2 → Heading, H3+ → Title, link → CitationMarker+UNDERLINE, code → Hint) 도 p9-fb-14 의 role 의도와 잘 맞습니다. actionable nit 3 건 inline. (a) heading 안의 link/code 가 heading 색에 가려짐, (b) Math/Footnote/TaskList silently drop 으로 답변 글자 사라짐 위험, (c) streaming 회귀 테스트의 OR 어설션이 약함.
@@ -0,0 +195,4 @@
Event::Rule => {
flush_current(&mut current, &mut out);
out.push(Line::from(Span::styled(
"".repeat(40),

Event::FootnoteReference / TaskListMarker / InlineMath / DisplayMath 4 종을 silently drop 하는데, 특히 InlineMathDisplayMath 는 사용자가 답변에서 수학 표현을 봤을 때 아예 사라지는 현상이 됩니다 ($E = mc^2$ 가 빈 자리만 남음). 우선 raw 글자라도 살리는 게 정보 손실 측면에서 안전합니다:

Event::InlineMath(s) | Event::DisplayMath(s) => {
    let style = theme.style(crate::theme::Role::Hint);
    current.push(Span::styled(s.into_string(), style));
}

TaskListMarker(bool)[ ] / [x] 로 그대로 표시하면 되고, FootnoteReference[^1] 형태 raw 가 답변 본문에서 자연스럽습니다.

`Event::FootnoteReference / TaskListMarker / InlineMath / DisplayMath` 4 종을 silently drop 하는데, 특히 `InlineMath` 와 `DisplayMath` 는 사용자가 답변에서 수학 표현을 봤을 때 아예 사라지는 현상이 됩니다 (`$E = mc^2$` 가 빈 자리만 남음). 우선 raw 글자라도 살리는 게 정보 손실 측면에서 안전합니다: ``` Event::InlineMath(s) | Event::DisplayMath(s) => { let style = theme.style(crate::theme::Role::Hint); current.push(Span::styled(s.into_string(), style)); } ``` `TaskListMarker(bool)` 는 `[ ] ` / `[x] ` 로 그대로 표시하면 되고, `FootnoteReference` 도 `[^1]` 형태 raw 가 답변 본문에서 자연스럽습니다.
@@ -0,0 +306,4 @@
}
let line: Vec<Span<'static>> = std::mem::take(current);
out.push(Line::from(line));
}

compose_style 의 분기 우선순위가 heading > link > inline_code > Body 인데, heading 안의 link / inline code 는 heading 스타일에 가려집니다. 예: # Section [docs](url) 에서 docs 가 헤딩 스타일만 받고 underline 이 사라짐.

2 가지 옵션:

  1. 레이어링: heading 이 base 면 그 위에 link 의 UNDERLINE / inline code 의 DIM modifier 만 add. 색은 heading 우선, modifier 만 합성.
  2. 무시: 의도된 동작이라면 doc comment 에 명시 ("heading 안의 link/code 는 heading 색만 사용").

저는 (1) 이 현재 코드 의도와 더 정렬한다고 봅니다 — link 의 UNDERLINE 은 색과 무관한 "클릭 가능한 텍스트" 시그널이라 헤딩에서도 살리는 게 자연스럽습니다.

`compose_style` 의 분기 우선순위가 heading > link > inline_code > Body 인데, heading 안의 link / inline code 는 heading 스타일에 가려집니다. 예: `# Section [docs](url)` 에서 `docs` 가 헤딩 스타일만 받고 underline 이 사라짐. 2 가지 옵션: 1. **레이어링**: heading 이 base 면 그 위에 link 의 UNDERLINE / inline code 의 DIM modifier 만 add. 색은 heading 우선, modifier 만 합성. 2. **무시**: 의도된 동작이라면 doc comment 에 명시 ("heading 안의 link/code 는 heading 색만 사용"). 저는 (1) 이 현재 코드 의도와 더 정렬한다고 봅니다 — link 의 UNDERLINE 은 색과 무관한 "클릭 가능한 텍스트" 시그널이라 헤딩에서도 살리는 게 자연스럽습니다.
@@ -0,0 +448,4 @@
assert!(texts.iter().any(|t| t.starts_with("2. beta")));
}
/// Code fence body is preserved verbatim per line, indented two

테스트 unterminated_bold_renders_literal_asterisks 의 assertion 이 OR 로 묶여 있어 "실제 동작" 을 pin 하지 못합니다 (둘 중 하나만 통과해도 OK):

assert!(
    combined.contains("**still typing") || combined.contains("still typing"),
    ...
);

현 pulldown-cmark 0.13 이 어느 쪽으로 emit 하는지 한 번 확인하고, 그 동작을 pin 하면 streaming 안전성 회귀 (예: pulldown 업그레이드가 글자 누락 새 버그 끌고 옴) 가 잡힙니다. 단순 combined.contains("still typing") (글자만 없으면 fail) 로 강화하면 충���합니다 — ** 보존 여부는 cosmetic, 글자 누락은 진짜 회귀.

테스트 `unterminated_bold_renders_literal_asterisks` 의 assertion 이 OR 로 묶여 있어 "실제 동작" 을 pin 하지 못합니다 (둘 중 하나만 통과해도 OK): ``` assert!( combined.contains("**still typing") || combined.contains("still typing"), ... ); ``` 현 pulldown-cmark 0.13 이 어느 쪽으로 emit 하는지 한 번 확인하고, 그 동작을 pin 하면 streaming 안전성 회귀 (예: pulldown 업그레이드가 글자 누락 새 버그 끌고 옴) 가 잡힙니다. 단순 `combined.contains("still typing")` (글자만 없으면 fail) 로 강화하면 충���합니다 — `**` 보존 여부는 cosmetic, 글자 누락은 진짜 회귀.
altair823 added 1 commit 2026-05-03 03:36:45 +00:00
- `compose_style` 레이어링 정리 — base color 는 가장 구체적인 컨테이너
  (heading > link > inline_code > body) 에서 가져오되 modifier 는
  link 의 UNDERLINED, inline code 의 DIM 도 헤딩 위에 add. `# Section
  [docs](url)` 의 `docs` 가 헤딩 색 + UNDERLINE 둘 다 받음.
- `Event::InlineMath` / `Event::DisplayMath` silently drop 폐기 →
  raw 글자 (예: `E = mc^2`, `\sum_i x_i`) 를 `Role::Hint` 스타일로
  보존. 답변에서 수학 표현이 사라지던 문제 수정.
- `Event::FootnoteReference` → `[^label]`, `Event::TaskListMarker` →
  `[x] ` / `[ ] ` 로 raw 표시 (이전엔 silently drop).
- `unterminated_bold_renders_literal_asterisks` 테스트의 OR 어설션을
  강화 — `still typing` 글자만 누락 안 되면 통과 (literal `**` 보존
  여부는 cosmetic, 글자 누락은 진짜 회귀).
- 신규 unit 3 개: heading 안 link 가 UNDERLINE+BOLD 둘 다, math 보존,
  task list 체크박스 글리프.

35 lib + 17 search + 18 ask + 12 inspect + 10 library 통과. clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-03 03:36:52 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — nit 3 건 깔끔히 반영.

  • compose_style 레이어링: heading 안 link 가 UNDERLINE + heading 색 둘 다, inline code 가 DIM + heading 색 — 신규 테스트 link_inside_heading_layers_underline_on_heading_color 로 핀
  • Math/Footnote/TaskList silently drop 폐기 — 신규 inline_and_display_math_render_as_text + task_list_renders_checkbox_glyphs 로 회귀 방지
  • streaming 회귀 테스트의 OR 어설션 → 글자 누락만 fail (literal 보존은 cosmetic)

추가 지적 없음. 머지 OK.

회차 2 — nit 3 건 깔끔히 반영. - compose_style 레이어링: heading 안 link 가 UNDERLINE + heading 색 둘 다, inline code 가 DIM + heading 색 — 신규 테스트 link_inside_heading_layers_underline_on_heading_color 로 핀 - Math/Footnote/TaskList silently drop 폐기 — 신규 inline_and_display_math_render_as_text + task_list_renders_checkbox_glyphs 로 회귀 방지 - streaming 회귀 테스트의 OR 어설션 → 글자 누락만 fail (literal 보존은 cosmetic) 추가 지적 없음. 머지 OK.
altair823 merged commit cd11897158 into main 2026-05-03 03:37:04 +00:00
altair823 deleted branch feat/p9-fb-11-markdown 2026-05-03 03:37:05 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#72