feat(tui): Ask conversation transcript UI (p9-fb-16) #62

Merged
altair823 merged 2 commits from feat/p9-fb-16-tui-conv into main 2026-05-03 00:03:22 +00:00
Owner

Summary

도그푸딩 항목 13 (꼬리 물기) 의 TUI surface. AskState 재설계 + transcript layout + ask_with_history wiring + Ctrl-L reset. p9-fb-15 (#60) 의 RAG facade 위에 build.

변경

kebab-tui::app::AskState

pub struct AskState {
    /* 기존 input / explain / streaming / partial / thread / rx / scroll / last_error */
    pub turns: Vec<Turn>,
    pub current_question: Option<String>,
    pub conversation_id: Option<String>,
    pub last_answer: Option<Answer>,  // (was: answer)
}

kebab-tui::ask

  • spawn_ask_worker: 첫 submit 에 conversation_id = Some(make_conversation_id()) 자동 생성 (timestamp-based: conv_<unix_nanos_hex>). inputcurrent_question, input clear. history = turns.clone(), turn_index = turns.len() as u32. worker 가 kebab-app::ask_with_config (facade) 호출, opts 에 history/conversation_id/turn_index 모두 채움 → RagPipeline::ask_with_history 까지 thread.
  • poll_worker: Answer 받으면 새 Turn 만들어 turns.push, last_answer 도 보존. UI 는 다음 frame 부터 새 turn 보여줌.
  • handle_key_ask: Ctrl-L 가 turns + conversation_id + last_answer + partial + current_question + scroll 모두 초기화 (in-flight worker 는 그대로 finish, 결과는 새 conv 의 stale turn 으로 silently 폐기).
  • render_answer: 완료 turns chronological 출력 + in-flight turn (있으면) 가장 아래. Q/A 라벨 색상 구분 (Q cyan bold, A green bold), in-flight answer 는 ▍ cursor + dim. transcript title 에 turn count.

Test

  • 17 PASS (3 신규):
    • ctrl_l_clears_conversation_state — Ctrl-L 가 모든 conversation field 초기화.
    • render_transcript_shows_completed_turns_in_order — Q1 < Q2 chronological.
    • render_streaming_inflight_turn_appears_below_completed_turns — 완료 turn 다음에 ▍ cursor 가진 in-flight turn.
  • 기존 14 회귀 0 (s.answer → s.last_answer + Turn fixture push).

README + HANDOFF

TUI 행에 multi-turn 동작 추가 ("Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. Ctrl-L 로 새 conversation 시작").

후속

  • 머지 후 spec status in_progresscompleted.
  • p9-fb-17 (V004 chat sessions storage) 가 turns 영속화 위에 build.
  • p9-fb-18 (CLI --session / --repl) 가 같은 facade 위에 build.
## Summary 도그푸딩 항목 13 (꼬리 물기) 의 TUI surface. AskState 재설계 + transcript layout + ask_with_history wiring + Ctrl-L reset. p9-fb-15 (#60) 의 RAG facade 위에 build. ## 변경 ### `kebab-tui::app::AskState` ```rust pub struct AskState { /* 기존 input / explain / streaming / partial / thread / rx / scroll / last_error */ pub turns: Vec<Turn>, pub current_question: Option<String>, pub conversation_id: Option<String>, pub last_answer: Option<Answer>, // (was: answer) } ``` ### `kebab-tui::ask` - `spawn_ask_worker`: 첫 submit 에 `conversation_id = Some(make_conversation_id())` 자동 생성 (timestamp-based: `conv_<unix_nanos_hex>`). `input` → `current_question`, `input` clear. `history = turns.clone()`, `turn_index = turns.len() as u32`. worker 가 `kebab-app::ask_with_config` (facade) 호출, opts 에 history/conversation_id/turn_index 모두 채움 → RagPipeline::ask_with_history 까지 thread. - `poll_worker`: Answer 받으면 새 Turn 만들어 `turns.push`, `last_answer` 도 보존. UI 는 다음 frame 부터 새 turn 보여줌. - `handle_key_ask`: `Ctrl-L` 가 turns + conversation_id + last_answer + partial + current_question + scroll 모두 초기화 (in-flight worker 는 그대로 finish, 결과는 새 conv 의 stale turn 으로 silently 폐기). - `render_answer`: 완료 turns chronological 출력 + in-flight turn (있으면) 가장 아래. Q/A 라벨 색상 구분 (Q cyan bold, A green bold), in-flight answer 는 ▍ cursor + dim. transcript title 에 turn count. ### Test - 17 PASS (3 신규): - `ctrl_l_clears_conversation_state` — Ctrl-L 가 모든 conversation field 초기화. - `render_transcript_shows_completed_turns_in_order` — Q1 < Q2 chronological. - `render_streaming_inflight_turn_appears_below_completed_turns` — 완료 turn 다음에 ▍ cursor 가진 in-flight turn. - 기존 14 회귀 0 (s.answer → s.last_answer + Turn fixture push). ### README + HANDOFF TUI 행에 multi-turn 동작 추가 ("Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. Ctrl-L 로 새 conversation 시작"). ## 후속 - 머지 후 spec status `in_progress` → `completed`. - p9-fb-17 (V004 chat sessions storage) 가 turns 영속화 위에 build. - p9-fb-18 (CLI `--session` / `--repl`) 가 같은 facade 위에 build.
altair823 added 1 commit 2026-05-02 23:58:31 +00:00
Multi-turn ask pane. AskState 가 turns: Vec<Turn> + current_question
+ conversation_id + last_answer 로 재설계. answer area 가 transcript
형식 (Q1/A1, Q2/A2, ...) 로 갈음, 매 Enter 가 이전 turns 를 history
로 worker 에 전달 — RagPipeline::ask_with_history 호출.

신규 (kebab-tui::app):
- AskState 에 turns / current_question / conversation_id / last_answer
  4 field 추가. 기존 answer field 제거 (last_answer 가 갈음).

신규 (kebab-tui::ask):
- spawn_ask_worker: 첫 submit 시 conversation_id 자동 생성
  (conv_<unix_nanos_hex>), input → current_question, input clear.
  history = turns.clone(), turn_index = turns.len(). worker 가
  ask_with_history 호출 (kebab-app facade 가 _cancellable 통해
  RagPipeline::ask_with_history 까지 thread).
- poll_worker: Answer 받으면 Turn { question: current_question,
  answer, citations, created_at } 만들어 turns 에 push, last_answer
  도 보존.
- handle_key_ask: Ctrl-L 가 turns + conversation_id 초기화 (in-flight
  worker 는 그대로 finish — 결과는 새 conversation 의 stale turn 으로
  silently 폐기, 사용자 의도와 일치).
- render_answer: 모든 completed turns + (있으면) in-flight turn
  chronological 출력. Q/A 라벨 색상 구분 (Q cyan bold, A green bold).
  in-flight answer 는 ▍ cursor + dim. transcript title 에 turn count.
- render_status / render_citations_or_explain: s.last_answer 사용.

Test:
- 17 PASS (3 신규: ctrl_l_clears_conversation_state /
  render_transcript_shows_completed_turns_in_order /
  render_streaming_inflight_turn_appears_below_completed_turns).
- 기존 14 회귀 0 (기존 s.answer → s.last_answer + Turn fixture
  push).

README + HANDOFF: TUI 행에 multi-turn 동작 추가. 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 00:00:20 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — UX 회귀 1건 (transcript refusal yellow 손실) + race risk 1건 (Ctrl-L 이 thread/rx clear 안 함 → 새 conv 에 stale turn 끼임) + 칭찬 2건. fix: push_turn_lines 가 last turn 색상 분기 + Ctrl-L 분기에 thread=None/rx=None/streaming=false 추가.

회차 1 — UX 회귀 1건 (transcript refusal yellow 손실) + race risk 1건 (Ctrl-L 이 thread/rx clear 안 함 → 새 conv 에 stale turn 끼임) + 칭찬 2건. fix: push_turn_lines 가 last turn 색상 분기 + Ctrl-L 분기에 thread=None/rx=None/streaming=false 추가.

(칭찬) AskState 의 4 신규 field (turns / current_question / conversation_id / last_answer) 모두 doc comment 가 lifecycle 명시 — 각 field 가 언제 set / clear 되는지. 미래 reader 가 "turns 는 언제 push?" / "current_question 은 언제 None?" 의문 코드 옆에서 즉시 답. Default derive 유지 (모두 Vec::new / None 기본값) — 기존 caller (App::new 의 ask: None) 변경 0.

(칭찬) `AskState` 의 4 신규 field (`turns` / `current_question` / `conversation_id` / `last_answer`) 모두 doc comment 가 lifecycle 명시 — 각 field 가 언제 set / clear 되는지. 미래 reader 가 "turns 는 언제 push?" / "current_question 은 언제 None?" 의문 코드 옆에서 즉시 답. `Default` derive 유지 (모두 Vec::new / None 기본값) — 기존 caller (App::new 의 `ask: None`) 변경 0.
@@ -96,1 +129,4 @@
out.push(Line::from(vec![
Span::styled(
q_label,
Style::default()

(칭찬) transcript title format transcript (N turn{s}) 가 user-visible state 노출 — 사용자 한눈에 "지금 N 번째 follow-up" 인지 파악. 영어 plural 정확 처리 (turn vs turns). pre-submission 빈 상태는 plain "transcript" 로 fallback — (0 turns) 같은 noise 없음.

(칭찬) transcript title format `transcript (N turn{s})` 가 user-visible state 노출 — 사용자 한눈에 "지금 N 번째 follow-up" 인지 파악. 영어 plural 정확 처리 (`turn` vs `turns`). pre-submission 빈 상태는 plain "transcript" 로 fallback — `(0 turns)` 같은 noise 없음.

(UX 회귀 / refusal style 손실) 이전 single-shot s.answer 렌더링은 refusal (grounded=false) turn 을 yellow text 로 표시 → 사용자가 sourced 답변과 거절 즉시 구분. 새 transcript 는 모든 turn 동일 default style — refusal 도 normal 답변과 시각 구분 0.

Why: 도그푸딩 항목 13 의 multi-turn 요구는 transcript 누적 + history wiring — 시각 구분을 jeo 변경 의도 X. 이전 yellow refusal 색상 contract (P9-3 의 design choice) 가 silently dropped — 회귀.

How to apply: push_turn_linesgrounded: bool 인자 추가 (또는 Turn 구조체 안에 grounded 정보가 없으니 caller 가 알 수 있는 방식 — last_answer.grounded 와 turn 의 매칭). 가장 단순:

  • Turn struct 자체에는 grounded 없음 (kebab-core).
  • transcript 의 가장 최근 turn 만 last_answer 와 연결되어 grounded 알 수 있음.
  • 과거 turns 는 grounded 정보 없음 — 모두 grounded 가정 (실제로 refused turn 은 history 에 들어가긴 하지만 사용자가 다음 질문 이미 던진 상태라 색상 의미 ↓).

권장: last_answer.grounded == false 일 때 마지막 turn 의 A 라인만 yellow. 또는 전체 turn yellow (강한 시각 — 사용자 친화). 가장 작은 surface: 마지막 turn 만.

(UX 회귀 / refusal style 손실) 이전 single-shot `s.answer` 렌더링은 refusal (`grounded=false`) turn 을 yellow text 로 표시 → 사용자가 sourced 답변과 거절 즉시 구분. 새 transcript 는 모든 turn 동일 default style — refusal 도 normal 답변과 시각 구분 0. Why: 도그푸딩 항목 13 의 multi-turn 요구는 transcript 누적 + history wiring — 시각 구분을 jeo 변경 의도 X. 이전 yellow refusal 색상 contract (P9-3 의 design choice) 가 silently dropped — 회귀. How to apply: `push_turn_lines` 에 `grounded: bool` 인자 추가 (또는 `Turn` 구조체 안에 grounded 정보가 없으니 caller 가 알 수 있는 방식 — last_answer.grounded 와 turn 의 매칭). 가장 단순: - `Turn` struct 자체에는 grounded 없음 (kebab-core). - transcript 의 가장 최근 turn 만 last_answer 와 연결되어 grounded 알 수 있음. - 과거 turns 는 grounded 정보 없음 — 모두 grounded 가정 (실제로 refused turn 은 history 에 들어가긴 하지만 사용자가 다음 질문 이미 던진 상태라 색상 의미 ↓). 권장: `last_answer.grounded == false` 일 때 마지막 turn 의 A 라인만 yellow. 또는 전체 turn yellow (강한 시각 — 사용자 친화). 가장 작은 surface: 마지막 turn 만.
@@ -203,3 +257,4 @@
}
(KeyCode::Esc, _) => {
// Best-effort cancellation per spec — worker keeps running
// but its result is dropped. Detach by clearing rx /

(race / Ctrl-L 후 stale turn) Ctrl-L 가 turns/conversation_id 등 6 field 초기화 하지만 thread / rx / streaming 은 그대로 둠. in-flight worker 가 다음 frame 에 finish → poll_worker 가 새 Turn 만들어 빈 turns 에 push. 결과: 새 conversation 의 첫 turn 이 사실 이전 conversation 의 답변 (질문 ‌current_question 도 clear 됐는데 worker 가 받은 query 는 여전히 old). 사용자에게 confusing.

Why: spec p9-fb-16 의 "Ctrl-L 가 turns + conversation_id 초기화 (in-flight worker 는 그대로 finish, 결과는 새 conversation 의 stale turn 으로 silently 폐기)" — "silently 폐기" 가 핵심이지만 현재 구현은 폐기 안 됨, 새 conversation 으로 들어감.

How to apply: Ctrl-L 분기에 s.thread = None; s.rx = None; s.streaming = false; 추가. JoinHandle Drop on None 할당 = detach (기존 P9-3 Esc 핸들러와 같은 패턴). worker thread 는 background 에서 계속 돌아 SQLite answers 에 commit (정상 — "실패한 turn" 흔적 보존), TUI 는 결과 무시.

(race / Ctrl-L 후 stale turn) Ctrl-L 가 turns/conversation_id 등 6 field 초기화 하지만 `thread` / `rx` / `streaming` 은 그대로 둠. in-flight worker 가 다음 frame 에 finish → poll_worker 가 새 Turn 만들어 빈 turns 에 push. 결과: 새 conversation 의 첫 turn 이 사실 이전 conversation 의 답변 (질문 ‌current_question 도 clear 됐는데 worker 가 받은 query 는 여전히 old). 사용자에게 confusing. Why: spec p9-fb-16 의 "Ctrl-L 가 turns + conversation_id 초기화 (in-flight worker 는 그대로 finish, 결과는 새 conversation 의 stale turn 으로 silently 폐기)" — "silently 폐기" 가 핵심이지만 현재 구현은 폐기 안 됨, 새 conversation 으로 들어감. How to apply: Ctrl-L 분기에 `s.thread = None; s.rx = None; s.streaming = false;` 추가. JoinHandle Drop on `None` 할당 = detach (기존 P9-3 Esc 핸들러와 같은 패턴). worker thread 는 background 에서 계속 돌아 SQLite `answers` 에 commit (정상 — "실패한 turn" 흔적 보존), TUI 는 결과 무시.
altair823 added 1 commit 2026-05-03 00:02:56 +00:00
회차 1 actionable 2건 반영.

- (UX 회귀) push_turn_lines 가 answer_color_override: Option<Color>
  추가 받음. render_answer 가 마지막 turn 에 한해 last_answer.grounded
  == false 면 Yellow override 전달 → P9-3 의 refusal 시각 구분
  contract 가 transcript 안에서도 보존. test:
  render_refusal_turn_in_transcript_uses_yellow_when_last_answer_ungrounded
  가 buffer 의 Yellow R 셀 검사로 검증.
- (race) Ctrl-L 가 turns/conversation_id/last_answer/partial/
  current_question/scroll 외에도 thread/rx/streaming 까지 detach.
  in-flight worker 가 다음 frame 에 finish 해도 새 conv 의 stale
  Turn 으로 graduate 안 됨 — JoinHandle Drop 으로 detach (P9-3 Esc
  cancel pattern 동일). worker 자체는 background 에서 SQLite
  answers 에 \"실패한 conv\" 흔적 commit. ctrl_l_clears_conversation_state
  test 가 streaming/thread/rx 도 함께 검증.

18 PASS. clippy clean.

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

회차 2 — 회차 1 actionable 2건 (refusal yellow / Ctrl-L race) 정확히 반영. 18 PASS (1 신규: refusal yellow buffer cell 검사). APPROVE.

회차 2 — 회차 1 actionable 2건 (refusal yellow / Ctrl-L race) 정확히 반영. 18 PASS (1 신규: refusal yellow buffer cell 검사). APPROVE.
altair823 merged commit d3b6cc9407 into main 2026-05-03 00:03:22 +00:00
altair823 deleted branch feat/p9-fb-16-tui-conv 2026-05-03 00:03:23 +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#62