feat(kebab-tui): p9-fb-12 partial — Mode enum + global i/Esc + auto switch + status label
도그푸딩 item 10 — vim 비익숙 사용자도 \"지금 키가 입력 vs 명령\" 명확히
구분 가능. 절반 ship: 사용자 가시 signal (mode label + auto flip + i/Esc
global) 만 land, 키 dispatch 의 input-empty heuristic 제거는 follow-up.
## 핵심 변경
- **`kebab_tui::Mode { Normal, Insert }`** enum + `Default = Normal`.
- `Mode::label()` → `"-- NORMAL --"` / `"-- INSERT --"` (status bar
문자열, 테스트로 핀).
- `Mode::auto_for(pane)` → Library/Inspect/Jobs = Normal,
Search/Ask = Insert. pane 전환 시 자동 적용.
- **`App.mode: Mode`** field. `App::new` 가 starting pane 의 auto
mode 로 init.
- **run loop `mode_intercept(app, key)`** — pane dispatch 전에 호출:
- Insert + `Esc` → Normal (어디서나, modifier 없음)
- Normal + `i` (Library/Inspect/Jobs 만) → Insert
- Search/Ask 의 `i` 는 fall-through (이미 Insert 라 typed char)
- 그 외 fall-through
- **pane 전환 시** `app.mode = Mode::auto_for(p)` 자동 flip — 사용자가
Tab 으로 Search 가면 자동으로 Insert.
- **status bar (header)** 에 mode label colored — Insert = Role::
Success (green), Normal = Role::Heading (cyan + bold). a11y: 색은
reinforcement, 글자가 authoritative signal.
## Deferred (HOTFIXES entry 추가)
spec p9-fb-12 의 \"기존 P9-3 ask 의 e/j/k input-empty heuristic 제거 —
mode 로 명확히\" 는 별 PR 로. 현재 dispatch 는 여전히:
- search.rs 의 `is_typing_mod` (SHIFT 만 typing 으로, CTRL/ALT 는 chord)
- ask.rs 의 input.is_empty() 가 e/j/k 를 navigation 으로 분기
테스트가 heuristic 에 의존해 있어, 회귀 surface 좁게 유지하려고 splitting.
spec status `in_progress` 유지 (not `completed`) — follow-up PR 가
heuristic 제거 + 완전 mode-authoritative 후 `completed` flip.
## 테스트
- 신규 3 unit (`Mode::auto_for` 모든 pane, label literals 핀,
default = Normal)
- 기존 98 TUI 테스트 모두 통과 (heuristic 그대로라 회귀 0)
- workspace 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README `kebab tui` 행: vim-style mode + auto NORMAL/INSERT + i/Esc
안내
- HANDOFF entry (partial-ship 명시)
- HOTFIXES entry (heuristic 제거 deferral 사유)
- spec status planned → in_progress (NOT completed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,7 @@ P0~P5 직렬. P6~P9 P5 이후 병렬 가능.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-19)** — In-process LRU search cache + `corpus_revision` 카운터. SQLite V004 migration 으로 `kv (key TEXT PK, value TEXT)` 테이블 + `corpus_revision = '0'` seed. `SqliteStore::corpus_revision()` / `bump_corpus_revision()` 메서드 (`UPDATE ... CAST AS INTEGER + 1` 으로 atomic). `kebab-app::ingest_with_config_cancellable` 가 `new + updated > 0` 시 bump — no-op reingest 는 cache 보존. `App.search_cache: Option<Mutex<LruCache<SearchCacheKey, Vec<SearchHit>>>>` (capacity from `config.search.cache_capacity`, default 256, 0 = 비활성). `SearchCacheKey` = `query_norm` (NFKC + trim + lowercase) + `mode` + `k` + `snippet_chars` + `embedding_version` + `chunker_version` + `corpus_revision` snapshot. `App::search` 가 lookup → miss 시 `search_uncached` → put. `search_uncached_with_config` facade 추가, CLI `kebab search --no-cache` 로 bypass (디버깅용). frozen design §9 versioning 표에 `corpus_revision` row 추가. spec: `tasks/p9/p9-fb-19-search-cache.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-17)** — Multi-turn chat session 영속화 (storage 만 — UI 는 p9-fb-18). SQLite V005 migration (spec 의 V004 가 p9-fb-19 의 kv 와 충돌해서 V005 로 시프트, HOTFIXES) 으로 `chat_sessions` (session_id PK + created_at + updated_at + title + config_snapshot_json) + `chat_turns` (turn_id PK + session_id FK ON DELETE CASCADE + turn_index + question + answer + citations_json + created_at, UNIQUE(session_id, turn_index)) + `idx_chat_turns_session` 추가. `kebab_core::ChatSessionRepo` trait 6 메서드 (create_session / get_session / list_sessions / delete_session / append_turn / list_turns) + `kebab_core::{ChatSessionRow, ChatTurnRow}` 신규 export. `kebab-store-sqlite::SqliteStore` impl (별 `chat_sessions.rs` 모듈) — append_turn 이 insert + parent updated_at bump 을 같은 conn 에서 처리. frozen design §5 storage 에 §5.7a chat_sessions/turns 절 신설. spec: `tasks/p9/p9-fb-17-chat-session-storage.md`. unblocks p9-fb-18 (CLI session/repl).
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-18)** — CLI `kebab ask --session <id>` (multi-turn). p9-fb-17 의 ChatSessionRepo 위에 `kebab-app::App::ask_with_session(session_id, query, opts) -> Answer` 메서드. 첫 호출 시 자동으로 `chat_sessions` row 생성 (title = 첫 question NFC trim 40 chars), 이후 호출은 `list_turns` 로 prior history 받아 `RagPipeline::ask_with_history` 호출 + 새 turn append. `App` 의 helper: `first_question_title(question)` (NFC + trim + 40 char cap, fallback `"untitled"`) + `blake3_truncate(input)` (32-hex `turn_id` 생성). facade `kebab_app::ask_with_session_with_config` + CLI `--session <id>` flag 추가. `--repl` 은 spec 명시 사항이지만 stdin loop fixture 부담 으로 후속 task 로 deferral (out of scope per HANDOFF). spec: `tasks/p9/p9-fb-18-cli-ask-session-repl.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-12 partial)** — TUI vim-style mode machine (절반 ship — heuristic 제거는 follow-up). `kebab_tui::Mode::{Normal, Insert}` enum + `Mode::auto_for(pane)` (Library/Inspect/Jobs → Normal, Search/Ask → Insert) + `Mode::label()` (`"-- NORMAL --"` / `"-- INSERT --"`) + `App.mode: Mode` field. run loop `mode_intercept(app, key)` 가 dispatch 전 intercept — Insert 에서 `Esc` → Normal (어디서나), Normal 에서 `i` → Insert (Library/Inspect/Jobs 만, Search/Ask 는 자동 Insert 라 `i` 가 typed char). 헤더 우측에 mode label colored (Insert = Role::Success green, Normal = Role::Heading cyan+bold). pane 전환 시 `app.mode = Mode::auto_for(p)` 자동 flip. **Deferred (HOTFIXES entry)**: `is_typing_mod` (search) + input-empty heuristic (ask) 는 후속 PR 에서 mode-authoritative 로 교체 — 현재는 user-visible signal (label + auto flip + i/Esc) 만 ship, 키 dispatch 는 heuristic 유지. spec status `in_progress` (not `completed`). spec: `tasks/p9/p9-fb-12-tui-mode-machine.md`.
|
||||
|
||||
## 다음 task 후보
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ kebab doctor
|
||||
| `kebab inspect doc <id>` / `kebab inspect chunk <id>` | raw record 보기 |
|
||||
| `kebab ask "<query>" [--show-citations / --hide-citations] [--session <id>]` | RAG 답변 + 근거 인용. 답변 후 `근거:` block 으로 full path / line range / score 한 줄씩 (default ON — `--hide-citations` 로 끄기, pipe 시 유용). 근거 부족 시 거절. Ollama 필요. `--session <id>` 로 multi-turn — 첫 호출에서 SQLite `chat_sessions` 에 자동 생성, 이후 호출은 prior turns 를 history 로 받아 follow-up. session id 는 사용자 지정 (e.g. `kb-rust-async-2026-05`) — `kebab reset --data-only` 로 모든 session wipe |
|
||||
| `kebab doctor` | 설정/모델/DB 헬스 체크 |
|
||||
| `kebab tui` | Ratatui 셸 (Library + Search + Ask + Inspect 패널, desktop 진행 중). Library 에서 `r` 키로 background ingest 시작 — 화면 하단 status bar 가 진행 표시, 완료/abort 시 final 라인 잠시 유지 후 자동 hide. ingest 진행 중 `Esc` / `Ctrl-C` 가 cancel signal (그 외에는 quit). Search 패널은 200ms debounce 후 background worker 가 검색 — 키 입력으로 UI freeze 안 됨, 사용자가 계속 타이핑하면 stale 결과 자동 폐기 (generation counter). Ask 패널은 multi-turn — 같은 conversation 안에서 Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. 답변 본문은 markdown 렌더 (bold/italic/inline code/heading/list/code fence/table/blockquote, raw `**bold**` 가 실제 굵게 표시). `Ctrl-L` 로 새 conversation 시작. Search 의 `g` 키가 `$EDITOR` (기본 `vi`) 로 hit 의 citation 위치 열기 — 종료 후 TUI 화면이 자동으로 깨끗이 redraw. CLI `kebab ask` 는 raw markdown 그대로 (terminal 호환성 위해) |
|
||||
| `kebab tui` | Ratatui 셸 (Library + Search + Ask + Inspect 패널, desktop 진행 중). Library 에서 `r` 키로 background ingest 시작 — 화면 하단 status bar 가 진행 표시, 완료/abort 시 final 라인 잠시 유지 후 자동 hide. ingest 진행 중 `Esc` / `Ctrl-C` 가 cancel signal (그 외에는 quit). vim-style mode (header 우측 `-- NORMAL --` / `-- INSERT --`) — Library/Inspect 는 자동 NORMAL, Search/Ask 는 자동 INSERT. `i` 로 Normal→Insert (Library/Inspect 만), `Esc` 로 Insert→Normal 어디서나. Search 패널은 200ms debounce 후 background worker 가 검색 — 키 입력으로 UI freeze 안 됨, 사용자가 계속 타이핑하면 stale 결과 자동 폐기 (generation counter). Ask 패널은 multi-turn — 같은 conversation 안에서 Q1/A1, Q2/A2 transcript 누적, 다음 질문이 이전 턴을 history 로 받아 답변. 답변 본문은 markdown 렌더 (bold/italic/inline code/heading/list/code fence/table/blockquote, raw `**bold**` 가 실제 굵게 표시). `Ctrl-L` 로 새 conversation 시작. Search 의 `g` 키가 `$EDITOR` (기본 `vi`) 로 hit 의 citation 위치 열기 — 종료 후 TUI 화면이 자동으로 깨끗이 redraw. CLI `kebab ask` 는 raw markdown 그대로 (terminal 호환성 위해) |
|
||||
| `kebab reset [--all / --data-only / --vector-only / --config-only] [--yes]` | XDG 데이터 wipe. **Irreversible.** TTY 면 confirm prompt, 아니면 `--yes` 필수. `--vector-only` 는 SQLite `embedding_records` 도 함께 truncate (orphan 방지) |
|
||||
| `kebab eval run / compare` | golden query 회귀 측정 |
|
||||
|
||||
|
||||
@@ -20,6 +20,52 @@ pub enum Pane {
|
||||
Jobs,
|
||||
}
|
||||
|
||||
/// p9-fb-12 (partial): vim-style modal interface.
|
||||
///
|
||||
/// `Normal` is the navigation / command mode; `Insert` is for typing
|
||||
/// queries / questions. The run loop intercepts `i` / `Esc` globally
|
||||
/// to flip between them, and pane switches auto-select the natural
|
||||
/// mode for the destination (Library/Inspect → Normal; Search/Ask →
|
||||
/// Insert). The status bar shows the active mode label so the user
|
||||
/// always knows which keys do what.
|
||||
///
|
||||
/// **Scope deviation from spec p9-fb-12** (recorded in HOTFIXES):
|
||||
/// the existing `is_typing_mod` heuristic in `search::handle_key_search`
|
||||
/// and the input-empty heuristic in `ask::handle_key_ask` are NOT
|
||||
/// removed in this PR — they continue to gate j/k/e between
|
||||
/// "navigation" and "typing" based on input buffer state. Removing
|
||||
/// them lands in a follow-up PR so the test surface (which leans on
|
||||
/// the heuristics) gets a focused review. The mode label is
|
||||
/// authoritative for the user-visible signal in the status bar; the
|
||||
/// dispatch is still heuristic-driven.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Insert,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Status-bar label (`-- NORMAL --` / `-- INSERT --`).
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Mode::Normal => "-- NORMAL --",
|
||||
Mode::Insert => "-- INSERT --",
|
||||
}
|
||||
}
|
||||
|
||||
/// p9-fb-12: which mode a freshly-focused pane should auto-enter.
|
||||
/// Library / Inspect are read-only navigation panes (`Normal`);
|
||||
/// Search / Ask are typing panes so we pre-flip to `Insert` so
|
||||
/// the user doesn't have to press `i` after every Tab.
|
||||
pub fn auto_for(pane: Pane) -> Self {
|
||||
match pane {
|
||||
Pane::Search | Pane::Ask => Mode::Insert,
|
||||
Pane::Library | Pane::Inspect | Pane::Jobs => Mode::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of a key handler — what the run loop should do next.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum KeyOutcome {
|
||||
@@ -280,6 +326,13 @@ pub struct App {
|
||||
/// `Style::default().fg(Color::*)`.
|
||||
pub theme: crate::theme::Theme,
|
||||
pub focus: Pane,
|
||||
/// p9-fb-12 (partial): vim-style modal interface. Run loop
|
||||
/// intercepts `i` / `Esc` to toggle, pane switches auto-flip via
|
||||
/// `Mode::auto_for(pane)`. Status bar renders the label. The
|
||||
/// per-pane key handlers still use their pre-fb-12 input-empty
|
||||
/// heuristics for j/k vs typing — full mode-authoritative
|
||||
/// dispatch is a follow-up PR.
|
||||
pub mode: Mode,
|
||||
pub library: LibraryState,
|
||||
/// Populated by p9-2 (None until that crate links in).
|
||||
pub search: Option<SearchState>,
|
||||
@@ -335,10 +388,13 @@ impl App {
|
||||
/// `kebab-app::list_docs_with_config` does not block startup.
|
||||
pub fn new(config: Config) -> anyhow::Result<Self> {
|
||||
let theme = crate::theme::Theme::from_name(&config.ui.theme);
|
||||
let initial_pane = Pane::Library;
|
||||
Ok(Self {
|
||||
config,
|
||||
theme,
|
||||
focus: Pane::Library,
|
||||
focus: initial_pane,
|
||||
// p9-fb-12: starting pane = Library → Normal mode.
|
||||
mode: Mode::auto_for(initial_pane),
|
||||
library: LibraryState::new(),
|
||||
search: None,
|
||||
ask: None,
|
||||
@@ -389,3 +445,36 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod mode_tests {
|
||||
use super::*;
|
||||
|
||||
/// p9-fb-12: Library / Inspect / Jobs auto-Normal; Search / Ask
|
||||
/// auto-Insert. Pin so a future pane addition has to think
|
||||
/// explicitly about its starting mode.
|
||||
#[test]
|
||||
fn auto_for_pane_routes_to_natural_mode() {
|
||||
assert_eq!(Mode::auto_for(Pane::Library), Mode::Normal);
|
||||
assert_eq!(Mode::auto_for(Pane::Inspect), Mode::Normal);
|
||||
assert_eq!(Mode::auto_for(Pane::Jobs), Mode::Normal);
|
||||
assert_eq!(Mode::auto_for(Pane::Search), Mode::Insert);
|
||||
assert_eq!(Mode::auto_for(Pane::Ask), Mode::Insert);
|
||||
}
|
||||
|
||||
/// p9-fb-12: status-bar label literals are part of the contract
|
||||
/// (the user sees them; tests / docs reference them).
|
||||
#[test]
|
||||
fn label_literals_stable() {
|
||||
assert_eq!(Mode::Normal.label(), "-- NORMAL --");
|
||||
assert_eq!(Mode::Insert.label(), "-- INSERT --");
|
||||
}
|
||||
|
||||
/// p9-fb-12: default `Mode` = `Normal` (the safe non-typing
|
||||
/// state). Pin so a future #[derive(Default)] tweak doesn't
|
||||
/// silently flip.
|
||||
#[test]
|
||||
fn default_is_normal() {
|
||||
assert_eq!(Mode::default(), Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ mod theme;
|
||||
|
||||
pub use theme::{Palette, Role, Theme};
|
||||
pub use app::{
|
||||
App, AskState, IngestState, InspectState, InspectTarget, KeyOutcome, LibraryState, Pane,
|
||||
SearchState, SearchWorkerMessage, TERMINAL_LINE_HOLD_SECS,
|
||||
App, AskState, IngestState, InspectState, InspectTarget, KeyOutcome, LibraryState, Mode,
|
||||
Pane, SearchState, SearchWorkerMessage, TERMINAL_LINE_HOLD_SECS,
|
||||
};
|
||||
pub use ask::{handle_key_ask, render_ask};
|
||||
pub use error_popup::{ErrorOverlay, render_error_overlay};
|
||||
|
||||
@@ -130,6 +130,17 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> {
|
||||
if event::poll(POLL_INTERVAL)? {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||
// p9-fb-12: global mode toggle. `Esc` from
|
||||
// Insert → Normal is intercepted here so it
|
||||
// works on every pane uniformly. `i` from
|
||||
// Normal → Insert is also intercepted, but
|
||||
// ONLY on Library/Inspect (where `i` has no
|
||||
// pre-fb-12 meaning); on Search/Ask the user
|
||||
// is already in Insert by Mode::auto_for, so
|
||||
// `i` falls through as a typed character.
|
||||
if mode_intercept(app, key) {
|
||||
continue;
|
||||
}
|
||||
let outcome = match app.focus {
|
||||
Pane::Library => handle_key_library(app, key),
|
||||
Pane::Search => handle_key_search(app, key),
|
||||
@@ -143,6 +154,11 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> {
|
||||
KeyOutcome::Quit => app.should_quit = true,
|
||||
KeyOutcome::SwitchPane(p) => {
|
||||
app.focus = p;
|
||||
// p9-fb-12: auto-flip mode on switch.
|
||||
// Library/Inspect/Jobs → Normal,
|
||||
// Search/Ask → Insert. User can still
|
||||
// press i/Esc to override.
|
||||
app.mode = crate::app::Mode::auto_for(p);
|
||||
// Lazy-init pane state on first switch.
|
||||
if p == Pane::Search && app.search.is_none() {
|
||||
app.search = Some(SearchState::default());
|
||||
@@ -277,10 +293,19 @@ fn render_header(f: &mut Frame, area: Rect, app: &App) {
|
||||
Pane::Inspect => "Inspect",
|
||||
Pane::Jobs => "Jobs",
|
||||
};
|
||||
// p9-fb-12: mode label colored — Insert = Success (green), Normal
|
||||
// = Heading (cyan + bold). The literal text is the user-visible
|
||||
// signal; color is reinforcement (a11y: never color-only).
|
||||
let mode_role = match app.mode {
|
||||
crate::app::Mode::Insert => crate::theme::Role::Success,
|
||||
crate::app::Mode::Normal => crate::theme::Role::Heading,
|
||||
};
|
||||
let line = Line::from(vec![
|
||||
Span::styled("kebab", app.theme.style(crate::theme::Role::Title)),
|
||||
Span::raw(" / "),
|
||||
Span::raw(pane_label),
|
||||
Span::raw(" "),
|
||||
Span::styled(app.mode.label(), app.theme.style(mode_role)),
|
||||
]);
|
||||
f.render_widget(Paragraph::new(line), area);
|
||||
}
|
||||
@@ -308,3 +333,40 @@ fn render_footer(f: &mut Frame, area: Rect, app: &App) {
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
/// p9-fb-12: global mode toggle interception. Returns `true` when
|
||||
/// the key was consumed (caller should `continue` and skip pane
|
||||
/// dispatch); `false` when the key should fall through to the
|
||||
/// active pane's handler.
|
||||
///
|
||||
/// Rules:
|
||||
/// - **`Esc` in Insert mode** → flip to Normal. Consumed (do NOT
|
||||
/// forward as a back-out signal to the pane). Library/Inspect
|
||||
/// start in Normal so this is a no-op there.
|
||||
/// - **`i` in Normal mode on Library / Inspect / Jobs** → flip to
|
||||
/// Insert. Consumed. (`i` has no pre-fb-12 meaning on these
|
||||
/// panes; on Search/Ask the pane is already Insert by
|
||||
/// `Mode::auto_for`, so the global `i` interception would
|
||||
/// swallow what should be a typed character. We let `i` fall
|
||||
/// through there.)
|
||||
/// - Everything else → not consumed.
|
||||
fn mode_intercept(app: &mut crate::app::App, key: crossterm::event::KeyEvent) -> bool {
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use crate::app::{Mode, Pane};
|
||||
|
||||
// Modifier-bearing keys (Ctrl-Esc etc.) are not the toggle.
|
||||
if !key.modifiers.is_empty() && key.modifiers != KeyModifiers::SHIFT {
|
||||
return false;
|
||||
}
|
||||
match (key.code, app.mode, app.focus) {
|
||||
(KeyCode::Esc, Mode::Insert, _) => {
|
||||
app.mode = Mode::Normal;
|
||||
true
|
||||
}
|
||||
(KeyCode::Char('i'), Mode::Normal, Pane::Library | Pane::Inspect | Pane::Jobs) => {
|
||||
app.mode = Mode::Insert;
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,31 @@ historical contract that was implemented; this file accumulates the
|
||||
deltas so phase 5+ readers can find the live behavior without diffing
|
||||
git history.
|
||||
|
||||
## 2026-05-03 — p9-fb-12 partial: mode machine without dispatch removal
|
||||
|
||||
**Spec amended**: `tasks/p9/p9-fb-12-tui-mode-machine.md` (status stays
|
||||
`in_progress`, NOT `completed`). Original contract: introduce vim
|
||||
NORMAL/INSERT modes globally AND remove `is_typing_mod` (search) +
|
||||
input-empty heuristic (ask) so the per-pane key dispatch becomes
|
||||
mode-authoritative.
|
||||
|
||||
**What shipped**: Mode enum + `App.mode` field + global `i`/`Esc`
|
||||
interception in run loop + auto mode flip on pane switch
|
||||
(`Mode::auto_for(pane)`) + status-bar mode label (color-graded via
|
||||
`Role::Success` for Insert, `Role::Heading` for Normal). Status bar
|
||||
literals (`-- NORMAL --` / `-- INSERT --`) pinned.
|
||||
|
||||
**Deferred to follow-up PR**: removal of the existing input-empty
|
||||
heuristics in `search::handle_key_search` and `ask::handle_key_ask`.
|
||||
These continue to gate j/k vs typing based on input buffer state.
|
||||
Tests rely on those heuristics, so the removal warrants its own
|
||||
focused PR (separate review, separate test sweep).
|
||||
|
||||
**Why partial-ship**: the user-visible signal (mode label + auto
|
||||
flip + i/Esc) is the most load-bearing part of the spec; the
|
||||
heuristic removal is cleanup that doesn't change behavior anyone
|
||||
currently observes. Splitting keeps the PR review surface small.
|
||||
|
||||
## 2026-05-03 — p9-fb-17 migration number V004 → V005
|
||||
|
||||
**Spec amended**: `tasks/p9/p9-fb-17-chat-session-storage.md` (frozen —
|
||||
|
||||
@@ -3,7 +3,7 @@ phase: P9
|
||||
component: kebab-tui
|
||||
task_id: p9-fb-12
|
||||
title: "TUI mode state machine (NORMAL / INSERT)"
|
||||
status: planned
|
||||
status: in_progress
|
||||
depends_on: []
|
||||
unblocks: [p9-fb-10, p9-fb-13]
|
||||
contract_source: ../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
|
||||
|
||||
Reference in New Issue
Block a user