From 10d0c0816f5b1b5cc3c8ae6f3c1d061cbd953999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 18 Jul 2026 12:31:52 -0400 Subject: [PATCH] Excise the Changelog feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kigi never publishes CDN changelogs, so the entire inherited feature was dead weight: the welcome-menu Changelog row, the hero-box info slot (bullets + clickable CTA), /release-notes with its /changelog alias, and the ChangelogManager CDN-fetch/disk-cache pipeline (Effect::FetchChangelog, TaskResult::ChangelogFetched, startup and post-login fetch kickoffs, AppView cache fields, mouse hover/click handling). Welcome menu is now [Import] / New worktree / Resume session / Quit; the hero box keeps title + version + subtitle and its layout math simplifies to 3 + menu rows (verified equivalent by the surviving boundary tests). Action::ShowReleaseNotes and the DocViewer modal stay — /docs uses them. builtin.rs keeps deleting stale CHANGELOG.{json,md} caches written by kigi ≤ 0.1.0. kigi-shell-base drops its reqwest 'blocking' feature (only the deleted module used it). -1206 lines net. Gates: fmt clean, workspace check/clippy --all-targets 0/0, kigi-tui lib 6609 / shell-base 56 / shell util:: 258 all passing, welcome pty e2e (3 tests incl. braille logo) passing. --- crates/codegen/kigi-shell-base/Cargo.toml | 2 +- .../kigi-shell-base/src/util/changelog.rs | 362 ---------------- .../codegen/kigi-shell-base/src/util/mod.rs | 1 - crates/codegen/kigi-shell/src/builtin.rs | 4 +- .../docs/user-guide/04-slash-commands.md | 10 - .../docs/user-guide/24-monitoring-usage.md | 2 +- crates/codegen/kigi-tui/src/app/actions.rs | 9 - crates/codegen/kigi-tui/src/app/app_view.rs | 149 +------ .../codegen/kigi-tui/src/app/dispatch/auth.rs | 3 - .../kigi-tui/src/app/dispatch/task_result.rs | 5 - .../kigi-tui/src/app/dispatch/tests/mod.rs | 5 - .../codegen/kigi-tui/src/app/effects/mod.rs | 21 - crates/codegen/kigi-tui/src/app/event_loop.rs | 6 - .../kigi-tui/src/slash/commands/mod.rs | 2 - .../src/slash/commands/release_notes.rs | 60 --- .../kigi-tui/src/views/welcome/hero_box.rs | 186 ++------- .../codegen/kigi-tui/src/views/welcome/mod.rs | 387 +----------------- .../tests/scenarios/release_notes_scroll.yaml | 52 --- .../kigi-tui/tests/scripted_scenarios.rs | 6 - 19 files changed, 66 insertions(+), 1206 deletions(-) delete mode 100644 crates/codegen/kigi-shell-base/src/util/changelog.rs delete mode 100644 crates/codegen/kigi-tui/src/slash/commands/release_notes.rs delete mode 100644 crates/codegen/kigi-tui/tests/scenarios/release_notes_scroll.yaml diff --git a/crates/codegen/kigi-shell-base/Cargo.toml b/crates/codegen/kigi-shell-base/Cargo.toml index 9dff97f..4b5bd59 100644 --- a/crates/codegen/kigi-shell-base/Cargo.toml +++ b/crates/codegen/kigi-shell-base/Cargo.toml @@ -13,7 +13,7 @@ default-bazel = [] [dependencies] anyhow = { workspace = true } chrono = { workspace = true } -reqwest = { workspace = true, features = ["blocking"] } +reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/codegen/kigi-shell-base/src/util/changelog.rs b/crates/codegen/kigi-shell-base/src/util/changelog.rs deleted file mode 100644 index b73a0cd..0000000 --- a/crates/codegen/kigi-shell-base/src/util/changelog.rs +++ /dev/null @@ -1,362 +0,0 @@ -//! Changelog fetching from CDN with local disk cache. -//! -//! Both markdown (`*.external.md`) and JSON (`*.external.json`) changelogs -//! are published per-version alongside the Kigi GitHub distribution. -//! -//! `ChangelogManager::fetch()` retrieves both formats in parallel and -//! returns a `Changelog` with optional markdown + structured entries. -//! Consumers pick the format they need: -//! - `/release-notes` uses `changelog.markdown` for rich scrollback display -//! - Welcome screen uses `changelog.entries` for bullet rendering - -use std::path::PathBuf; - -/// Base URL for published changelogs. Kigi distributes via GitHub, so -/// per-version changelogs live in the release repository. Unreachable or -/// missing files degrade gracefully to the on-disk cache (see `fetch_with`). -const CHANGELOG_BASE: &str = - "https://raw.githubusercontent.com/ZacharyZhang-NY/Kigi-CLI/main/changelogs"; -const FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); - -/// A single structured changelog entry from the published JSON changelog. -/// -/// Shape must match the output of `render_external_json` in `changelog.sh`: -/// `{category, description, breaking_change}` -/// If you change fields here, update `changelog.sh:render_external_json` too. -/// -/// All fields use `#[serde(default)]` so a single malformed entry doesn't -/// kill the entire array parse. Entries with an empty description are -/// filtered out by `bullets_from_entries`. -#[derive(Debug, Clone, serde::Deserialize)] -pub struct ChangelogEntry { - /// Category label (e.g. "features", "fixes", "breaking", "performance"). - #[serde(default)] - pub category: String, - /// Human-readable description (may contain `**bold**` or backticks). - #[serde(default)] - pub description: String, - /// Whether this entry represents a breaking change. - #[serde(default)] - pub breaking_change: bool, -} - -/// Both formats of a version's changelog, fetched together. -pub struct Changelog { - /// Rendered markdown (for `/release-notes` display). - pub markdown: Option, - /// Structured entries (for welcome screen bullets). - pub entries: Option>, -} - -/// Manages changelog retrieval from CDN with local disk caching. -/// -/// Single entry point: `fetch()` returns both markdown and JSON in one -/// `Changelog` struct. Each format is fetched independently with its own -/// cache file, so a failure in one doesn't block the other. -pub struct ChangelogManager { - md_cache: PathBuf, - json_cache: PathBuf, -} - -impl Default for ChangelogManager { - fn default() -> Self { - Self::new() - } -} - -impl ChangelogManager { - pub fn new() -> Self { - // Prefer live `$KIGI_SHARE_DIR` so harness-injected homes (PTY e2e) always - // win over a OnceLock that may have been initialised earlier with a - // different path in the same process graph. - Self::from_env_home() - } - - /// Resolve cache paths from the live process environment (not the - /// `kigi_home()` OnceLock). A seeded `$KIGI_SHARE_DIR` set on the pager - /// process is always honoured even if some earlier init path cached a - /// different home. - fn from_env_home() -> Self { - let home = std::env::var_os("KIGI_SHARE_DIR") - .map(std::path::PathBuf::from) - .filter(|p| !p.as_os_str().is_empty()) - .unwrap_or_else(crate::util::kigi_home::kigi_home); - Self { - md_cache: home.join("CHANGELOG.md"), - json_cache: home.join("CHANGELOG.json"), - } - } - - /// Fetch both markdown and JSON changelogs for the current version. - /// - /// Each format is fetched independently (CDN, 3 s timeout) and cached - /// to disk. On failure, falls back to the cached copy. Either field - /// may be `None` if offline with no cache. - /// - /// When `KIGI_CHANGELOG_OFFLINE` is set (PTY / integration tests), skip - /// the CDN entirely and read only the disk cache so seeded fixtures win - /// deterministically without network races. Paths are re-resolved from - /// `$KIGI_SHARE_DIR` so harness-injected env always applies. - /// - /// JSON is only cached after a successful parse to avoid poisoning the - /// disk cache with malformed content (the markdown cache is write-through - /// since it's consumed as raw text). - pub fn fetch(&self) -> Changelog { - // Always re-resolve from env so a caller holding an older manager - // (or OnceLock lag) still reads the live harness home. - Self::from_env_home().fetch_with(changelog_offline(), CHANGELOG_BASE) - } - - /// Fetch using this manager's already-resolved cache paths, an explicit - /// offline flag, and an explicit CDN base. - /// - /// Split out of [`fetch`] so unit tests can drive it against a temp home - /// without mutating process-global env (`KIGI_SHARE_DIR` / - /// `KIGI_CHANGELOG_OFFLINE`), which races across the parallel test - /// harness. Passing an unreachable `base` lets a test force a - /// deterministic CDN miss instead of depending on whether the sandbox - /// happens to block network. Production callers always go through - /// [`fetch`], so behaviour is unchanged. - fn fetch_with(&self, offline: bool, base: &str) -> Changelog { - if offline { - return Changelog { - markdown: read_cache(&self.md_cache), - entries: self.read_json_cache(), - }; - } - - let version = kigi_version::VERSION; - let md_url = format!("{}/{}.external.md", base, version); - - // Fetch both formats in parallel (3s timeout each → 3s total, not 6s). - let mut markdown = None; - let mut entries = None; - std::thread::scope(|s| { - let md_handle = s.spawn(|| self.fetch_and_cache(&md_url, &self.md_cache)); - let json_handle = s.spawn(|| self.fetch_json(base, version)); - markdown = md_handle.join().ok().flatten(); - entries = json_handle.join().ok().flatten(); - }); - - // If CDN is unreachable (CI sandboxes, airplane mode), fall back to - // any on-disk seed under `$KIGI_SHARE_DIR` even when offline mode was not - // explicitly requested — keeps PTY/integration tests deterministic. - if markdown.is_none() { - markdown = read_cache(&self.md_cache); - } - if entries.is_none() { - entries = self.read_json_cache(); - } - - Changelog { markdown, entries } - } - - /// Fetch and parse JSON changelog, caching only after successful parse. - fn fetch_json(&self, base: &str, version: &str) -> Option> { - let url = format!("{}/{}.external.json", base, version); - - // Try remote first — only cache after successful parse. - if let Ok(raw) = fetch_blocking(&url) - && !raw.trim().is_empty() - { - match serde_json::from_str::>(&raw) { - Ok(entries) => { - if let Err(e) = std::fs::write(&self.json_cache, &raw) { - tracing::debug!(error = %e, "JSON changelog cache write failed"); - } - return Some(entries); - } - Err(e) => { - tracing::debug!(error = %e, "failed to parse JSON changelog from CDN"); - } - } - } - - self.read_json_cache() - } - - fn read_json_cache(&self) -> Option> { - let cached = read_cache(&self.json_cache)?; - match serde_json::from_str(&cached) { - Ok(entries) => Some(entries), - Err(e) => { - tracing::debug!(error = %e, "failed to parse cached JSON changelog"); - None - } - } - } - - /// Shared fetch-and-cache: try remote (3 s timeout), cache on success, - /// fall back to disk cache on failure. - fn fetch_and_cache(&self, url: &str, cache_path: &std::path::Path) -> Option { - if let Ok(content) = fetch_blocking(url) - && !content.trim().is_empty() - { - if let Err(e) = std::fs::write(cache_path, &content) { - tracing::debug!(error = %e, path = %cache_path.display(), "cache write failed"); - } - return Some(content); - } - read_cache(cache_path) - } -} - -/// When set, `ChangelogManager::fetch` skips the CDN and only reads disk cache. -/// Used by PTY harness tests that seed `CHANGELOG.{md,json}` under a temp home. -fn changelog_offline() -> bool { - std::env::var_os("KIGI_CHANGELOG_OFFLINE").is_some_and(|v| !v.is_empty() && v != "0") -} - -fn read_cache(path: &std::path::Path) -> Option { - std::fs::read_to_string(path) - .ok() - .filter(|c| !c.trim().is_empty()) -} - -/// Strip `**bold**` markers and backticks from a description string. -fn strip_markdown_inline(s: &str) -> String { - s.replace("**", "").replace('`', "") -} - -/// Convert changelog entries to plain-text bullet strings. -/// -/// Strips `**bold**` and backtick formatting from each description, -/// skips entries with empty descriptions (from tolerant deserialization), -/// and returns at most `max` entries. -pub fn bullets_from_entries(entries: &[ChangelogEntry], max: usize) -> Vec { - entries - .iter() - .filter(|e| !e.description.is_empty()) - .take(max) - .map(|e| strip_markdown_inline(&e.description)) - .collect() -} - -/// Blocking HTTP fetch. Callers (`std::thread::scope` threads) are already -/// off the tokio runtime, so no extra thread spawn is needed. -fn fetch_blocking(url: &str) -> anyhow::Result { - let client = reqwest::blocking::Client::builder() - .timeout(FETCH_TIMEOUT) - .build()?; - let resp = client.get(url).send()?; - if !resp.status().is_success() { - anyhow::bail!("HTTP {}", resp.status()); - } - Ok(resp.text()?) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Build a manager pointing at `home` directly, bypassing the global - /// `$KIGI_SHARE_DIR` env so tests never race the parallel harness. - fn manager_for(home: &std::path::Path) -> ChangelogManager { - ChangelogManager { - md_cache: home.join("CHANGELOG.md"), - json_cache: home.join("CHANGELOG.json"), - } - } - - #[test] - fn offline_mode_reads_seeded_disk_cache_only() { - let tmp = tempfile::tempdir().unwrap(); - let home = tmp.path().join("kigi-home"); - std::fs::create_dir_all(&home).unwrap(); - std::fs::write(home.join("CHANGELOG.md"), "# seeded offline md\n").unwrap(); - std::fs::write( - home.join("CHANGELOG.json"), - r#"[{"category":"features","description":"seeded entry","breaking_change":false}]"#, - ) - .unwrap(); - - // Offline path: read only the seeded disk cache, no network. - let changelog = manager_for(&home).fetch_with(true, CHANGELOG_BASE); - assert_eq!( - changelog.markdown.as_deref(), - Some("# seeded offline md\n"), - "offline mode must return seeded markdown" - ); - let entries = changelog.entries.expect("seeded json entries"); - assert_eq!(entries.len(), 1); - assert_eq!(entries[0].description, "seeded entry"); - } - - #[test] - fn cdn_miss_falls_back_to_env_home_disk_cache() { - let tmp = tempfile::tempdir().unwrap(); - let home = tmp.path().join("kigi-home-fallback"); - std::fs::create_dir_all(&home).unwrap(); - std::fs::write(home.join("CHANGELOG.md"), "# fallback md\n").unwrap(); - - // Non-offline path with an unreachable CDN base: the remote fetch - // fails deterministically (no dependency on the sandbox blocking - // network), so the on-disk cache must win. - let changelog = manager_for(&home).fetch_with(false, "http://127.0.0.1:1"); - assert_eq!( - changelog.markdown.as_deref(), - Some("# fallback md\n"), - "CDN miss must fall back to the seeded CHANGELOG.md" - ); - } - - #[test] - fn bullets_strips_markdown_and_respects_max() { - let entries = vec![ - ChangelogEntry { - category: "features".into(), - description: "Added **dark mode** support".into(), - breaking_change: false, - }, - ChangelogEntry { - category: "fixes".into(), - description: "Fixed `crash` on startup".into(), - breaking_change: false, - }, - ChangelogEntry { - category: "performance".into(), - description: "Faster **rendering** of `code` blocks".into(), - breaking_change: false, - }, - ]; - - let bullets = bullets_from_entries(&entries, 2); - assert_eq!(bullets.len(), 2); - assert_eq!(bullets[0], "Added dark mode support"); - assert_eq!(bullets[1], "Fixed crash on startup"); - } - - #[test] - fn bullets_skips_empty_descriptions() { - let entries = vec![ - ChangelogEntry { - category: "features".into(), - description: "Good entry".into(), - breaking_change: false, - }, - ChangelogEntry { - category: String::new(), - description: String::new(), // bad entry from tolerant deser - breaking_change: false, - }, - ChangelogEntry { - category: "fixes".into(), - description: "Another good one".into(), - breaking_change: false, - }, - ]; - let bullets = bullets_from_entries(&entries, 10); - assert_eq!(bullets, vec!["Good entry", "Another good one"]); - } - - #[test] - fn tolerant_deserialization_partial_entry() { - // Missing description field → defaults to empty string, not a parse error - let json = r#"[{"category":"features"},{"description":"ok"}]"#; - let entries: Vec = serde_json::from_str(json).unwrap(); - assert_eq!(entries.len(), 2); - assert_eq!(entries[0].description, ""); - assert_eq!(entries[1].category, ""); - assert_eq!(entries[1].description, "ok"); - } -} diff --git a/crates/codegen/kigi-shell-base/src/util/mod.rs b/crates/codegen/kigi-shell-base/src/util/mod.rs index 91a984a..c0bef89 100644 --- a/crates/codegen/kigi-shell-base/src/util/mod.rs +++ b/crates/codegen/kigi-shell-base/src/util/mod.rs @@ -1,4 +1,3 @@ -pub mod changelog; pub mod event_id; pub mod kigi_home; pub mod secure_file; diff --git a/crates/codegen/kigi-shell/src/builtin.rs b/crates/codegen/kigi-shell/src/builtin.rs index ef2d9e7..ebe4334 100644 --- a/crates/codegen/kigi-shell/src/builtin.rs +++ b/crates/codegen/kigi-shell/src/builtin.rs @@ -116,8 +116,8 @@ pub fn extract_bundled_files(kigi_home: &std::path::Path) { let _ = std::fs::create_dir_all(kigi_home); - // Clean up cached changelog files from previous version so - // /release-notes fetches fresh content for the new version. + // Clean up changelog caches written by the removed changelog feature + // (kigi <= 0.1.0 cached CDN release notes in the kigi home). for stale in &["CHANGELOG.json", "CHANGELOG.md"] { let _ = std::fs::remove_file(kigi_home.join(stale)); } diff --git a/crates/codegen/kigi-tui/docs/user-guide/04-slash-commands.md b/crates/codegen/kigi-tui/docs/user-guide/04-slash-commands.md index 1bf708a..908619c 100644 --- a/crates/codegen/kigi-tui/docs/user-guide/04-slash-commands.md +++ b/crates/codegen/kigi-tui/docs/user-guide/04-slash-commands.md @@ -395,16 +395,6 @@ Show terminal capability detection and setup info — including color level, whi Aliases: `/terminal-check`, `/terminal-info` -### `/release-notes` - -View release notes for the current version. - -``` -/release-notes -``` - -Aliases: `/changelog` - ### `/docs` Browse in-TUI How-to Guides, open online Build docs, or jump to a guide by title. diff --git a/crates/codegen/kigi-tui/docs/user-guide/24-monitoring-usage.md b/crates/codegen/kigi-tui/docs/user-guide/24-monitoring-usage.md index 8dfafb3..563d2aa 100644 --- a/crates/codegen/kigi-tui/docs/user-guide/24-monitoring-usage.md +++ b/crates/codegen/kigi-tui/docs/user-guide/24-monitoring-usage.md @@ -2,7 +2,7 @@ > **Status: alpha.** The schema below is versioned (`kigi_code.schema.version = v1`); > additive changes may occur without notice, renames/removals will bump the -> version and be called out in the changelog. +> version. Kigi CLI can export usage **metrics** and **events** to your organization's own OpenTelemetry collector, so platform teams can monitor adoption, token diff --git a/crates/codegen/kigi-tui/src/app/actions.rs b/crates/codegen/kigi-tui/src/app/actions.rs index ddea2e8..5ccc249 100644 --- a/crates/codegen/kigi-tui/src/app/actions.rs +++ b/crates/codegen/kigi-tui/src/app/actions.rs @@ -1471,10 +1471,6 @@ pub enum Effect { /// `SwitchModelComplete` so `IncompatibleAgent` can roll back. prev_model_id: Option, }, - /// Fetch changelog from CDN (both markdown + structured JSON). - /// Runs off the render path via `spawn_blocking`. Result is cached - /// on `AppView` so `/release-notes` and the welcome screen share it. - FetchChangelog, /// Persist memory modal fullscreen preference to `[hints]` in config.toml. PersistMemoryFullscreen { fullscreen: bool }, /// Persist the project-picker opt-out to `[hints] project_picker_disabled`. @@ -2130,11 +2126,6 @@ pub enum TaskResult { /// rollback on `IncompatibleAgent`. prev_model_id: Option, }, - /// Changelog fetched from CDN (both formats). - ChangelogFetched { - markdown: Option, - entries: Vec, - }, /// Cross-session prompt history loaded from ACP. PromptHistoryLoaded { agent_id: AgentId, diff --git a/crates/codegen/kigi-tui/src/app/app_view.rs b/crates/codegen/kigi-tui/src/app/app_view.rs index a923e7a..e740ea3 100644 --- a/crates/codegen/kigi-tui/src/app/app_view.rs +++ b/crates/codegen/kigi-tui/src/app/app_view.rs @@ -596,12 +596,6 @@ pub struct AppView { /// Release-safe FPS HUD (`/debug fps`; `KIGI_FPS` env on release /// builds, where the dev overlay is compiled out) — see the module doc. pub fps_hud: crate::views::fps_hud::FpsHud, - /// Cached changelog markdown (for `/release-notes`). Populated by - /// `FetchChangelog` at startup; `None` until the fetch completes. - pub changelog_markdown: Option, - /// Cached changelog bullets (for welcome screen). Populated by - /// `FetchChangelog` at startup; empty until the fetch completes. - pub changelog_bullets: Vec, /// Resolved tip list from config layers. pub tips: Vec, /// Selected tip for the current launch/session. @@ -709,10 +703,6 @@ pub struct AppView { pub welcome_menu_index: Option, /// Hit-test rects for welcome menu items (populated during render). pub welcome_menu_rects: Vec, - /// Whether the welcome menu currently includes a "Changelog" row (above - /// Quit). Set during render; the input handler uses it to size the menu and - /// map the extra row to the release-notes action. - pub welcome_show_changelog_action: bool, /// Hit-test rect for the import-claude banner on the welcome screen. pub welcome_import_banner_rect: Option, /// Last known mouse position (column, row), updated on every Mouse event. @@ -734,14 +724,8 @@ pub struct AppView { pub welcome_auth_url_rect: Option, /// Whether the mouse pointer was last over the auth URL (for OSC 22 cursor shape). pub welcome_on_auth_url: bool, - /// Mouse last over the changelog block (drives hover color + redraws). - pub welcome_on_changelog_cta: bool, /// Hit-test rect for the "show full URL" fallback link. pub welcome_auth_fallback_rect: Option, - /// Hit-test rect for the "[Refresh]" button on the paywall tier line. - /// Hit-test rect for the gate URL link on the paywall CTA. - /// Hit-test rect for the clickable changelog info block (opens release notes). - pub welcome_changelog_cta_rect: Option, /// Show the raw auth URL with mouse capture disabled for manual copy. pub auth_show_raw_url: bool, /// Whether mouse capture is currently disabled for raw URL mode. @@ -1015,8 +999,6 @@ impl AppView { tracing_rx: None, scroll_debug_hud: crate::views::scroll_debug_hud::ScrollDebugHud::new(), fps_hud: crate::views::fps_hud::FpsHud::new(), - changelog_markdown: None, - changelog_bullets: Vec::new(), tips: Vec::new(), tip: None, welcome_prompt, @@ -1031,7 +1013,6 @@ impl AppView { minimal_state: crate::minimal_api::MinimalState::default(), welcome_menu_index: None, welcome_menu_rects: Vec::new(), - welcome_show_changelog_action: false, welcome_import_banner_rect: None, last_mouse_pos: None, last_scroll_pos: None, @@ -1039,9 +1020,7 @@ impl AppView { welcome_prompt_rect: None, welcome_auth_url_rect: None, welcome_on_auth_url: false, - welcome_on_changelog_cta: false, welcome_auth_fallback_rect: None, - welcome_changelog_cta_rect: None, auth_show_raw_url: false, auth_mouse_disabled: false, session_picker_entries: None, @@ -1635,19 +1614,11 @@ impl AppView { new_worktree_dialog: &mut self.new_worktree_dialog, menu_index: &mut self.welcome_menu_index, menu_rects: &self.welcome_menu_rects, - menu_count: 3 - + if self.has_claude_import { 1 } else { 0 } - + if self.welcome_show_changelog_action { - 1 - } else { - 0 - }, + menu_count: 3 + if self.has_claude_import { 1 } else { 0 }, prompt_rect: self.welcome_prompt_rect.as_ref(), import_banner_rect: self.welcome_import_banner_rect.as_ref(), auth_url_rect: self.welcome_auth_url_rect.as_ref(), auth_fallback_rect: self.welcome_auth_fallback_rect.as_ref(), - changelog_cta_rect: self.welcome_changelog_cta_rect.as_ref(), - on_changelog_cta: &mut self.welcome_on_changelog_cta, show_raw_url: &mut self.auth_show_raw_url, sp_entries: &mut self.session_picker_entries, sp_state: &mut self.session_picker_state, @@ -1657,8 +1628,6 @@ impl AppView { has_claude_import: self.has_claude_import, import_claude_modal: &mut self.import_claude_modal, welcome_doc_viewer: &mut self.welcome_doc_viewer, - changelog_markdown: &self.changelog_markdown, - show_changelog_action: self.welcome_show_changelog_action, has_pending_update: self.pending_update_version.is_some(), has_foreign_resume, cwd_has_git_ancestor: self.cwd_has_git_ancestor, @@ -2175,10 +2144,6 @@ struct WelcomeInputCtx<'a> { import_banner_rect: Option<&'a ratatui::layout::Rect>, auth_url_rect: Option<&'a ratatui::layout::Rect>, auth_fallback_rect: Option<&'a ratatui::layout::Rect>, - /// Hit-test rect for the clickable changelog info block (opens release notes). - changelog_cta_rect: Option<&'a ratatui::layout::Rect>, - /// Sticky hover flag for the changelog block (redraw on enter/leave). - on_changelog_cta: &'a mut bool, show_raw_url: &'a mut bool, sp_entries: &'a mut Option>, sp_state: &'a mut crate::views::picker::PickerState, @@ -2190,10 +2155,6 @@ struct WelcomeInputCtx<'a> { has_claude_import: bool, import_claude_modal: &'a mut Option, welcome_doc_viewer: &'a mut Option, - changelog_markdown: &'a Option, - /// Whether the welcome menu currently includes a "Changelog" row (above - /// Quit), so index→action mapping accounts for it. - show_changelog_action: bool, has_pending_update: bool, /// A recent foreign session is available to resume when no update is pending. has_foreign_resume: bool, @@ -2601,12 +2562,7 @@ fn handle_welcome_input(ev: &Event, ctx: &mut WelcomeInputCtx<'_>) -> InputOutco if key!(Enter).matches(key) && let Some(idx) = *ctx.menu_index { - return dispatch_menu_action( - idx, - ctx.has_claude_import, - ctx.show_changelog_action, - ctx.changelog_markdown.as_deref(), - ); + return dispatch_menu_action(idx, ctx.has_claude_import); } if crate::input::key::is_text_input_key(key) { *ctx.prompt_focused = true; @@ -2762,23 +2718,9 @@ fn handle_welcome_input(ev: &Event, ctx: &mut WelcomeInputCtx<'_>) -> InputOutco { return InputOutcome::Action(Action::DismissClaudeImport); } - return dispatch_menu_action( - i, - ctx.has_claude_import, - ctx.show_changelog_action, - ctx.changelog_markdown.as_deref(), - ); + return dispatch_menu_action(i, ctx.has_claude_import); } } - if let Some(rect) = ctx.changelog_cta_rect - && rect.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) - && let Some(md) = ctx.changelog_markdown.as_deref() - { - return InputOutcome::Action(Action::ShowReleaseNotes { - title: "Release Notes".to_string(), - content: md.trim().to_string(), - }); - } if let Some(rect) = ctx.auth_url_rect && matches!(ctx.auth_state, AuthState::Authenticating { .. }) && rect.contains(ratatui::layout::Position::new(mouse.column, mouse.row)) @@ -2830,12 +2772,6 @@ fn handle_welcome_input(ev: &Event, ctx: &mut WelcomeInputCtx<'_>) -> InputOutco if ctx.has_claude_import && new_index == Some(0) { return InputOutcome::Changed; } - let pos = ratatui::layout::Position::new(mouse.column, mouse.row); - let over_cta = ctx.changelog_cta_rect.is_some_and(|r| r.contains(pos)); - if over_cta != *ctx.on_changelog_cta { - *ctx.on_changelog_cta = over_cta; - return InputOutcome::Changed; - } if matches!(ctx.auth_state, AuthState::Authenticating { .. }) && ctx.auth_url_rect.is_some() { @@ -2885,23 +2821,12 @@ fn dispatch_pending_menu_action(items: &[PendingMenuItem], index: usize) -> Inpu } /// Dispatch an action for a welcome menu item by index. /// -/// Menu order: `[Import]`, New worktree, Resume session, `[Changelog]`, Quit. -/// `show_changelog_action` is true when the Changelog row is rendered; release -/// notes open only once `changelog_md` is available. -fn dispatch_menu_action( - index: usize, - has_claude_import: bool, - show_changelog_action: bool, - changelog_md: Option<&str>, -) -> InputOutcome { +/// Menu order: `[Import]`, New worktree, Resume session, Quit. +fn dispatch_menu_action(index: usize, has_claude_import: bool) -> InputOutcome { let base = if has_claude_import { 1 } else { 0 }; let worktree_idx = base; let resume_idx = base + 1; - let (changelog_idx, quit_idx) = if show_changelog_action { - (Some(base + 2), base + 3) - } else { - (None, base + 2) - }; + let quit_idx = base + 2; if has_claude_import && index == 0 { return InputOutcome::Action(Action::ImportClaudeSettings); } @@ -2911,15 +2836,6 @@ fn dispatch_menu_action( if index == resume_idx { return InputOutcome::Action(Action::FetchSessionList); } - if Some(index) == changelog_idx { - if let Some(md) = changelog_md { - return InputOutcome::Action(Action::ShowReleaseNotes { - title: "Release Notes".to_string(), - content: md.trim().to_string(), - }); - } - return InputOutcome::Unchanged; - } if index == quit_idx { return InputOutcome::Action(Action::Quit); } @@ -3264,8 +3180,6 @@ impl AppView { session_picker_source_filter: self.session_picker_source_filter, chat_mode: self.chat_mode, is_api_key_auth: self.is_api_key_auth, - changelog_bullets: &self.changelog_bullets, - changelog_has_full_notes: self.changelog_markdown.is_some(), }; let result = crate::views::welcome::render_welcome( view_area, @@ -3275,12 +3189,10 @@ impl AppView { &mut self.session_picker_state, ); self.welcome_menu_rects = result.menu_rects; - self.welcome_show_changelog_action = result.changelog_action_present; self.welcome_prompt_rect = result.prompt_rect; self.welcome_import_banner_rect = result.import_banner_rect; self.welcome_auth_url_rect = result.auth_url_rect; self.welcome_auth_fallback_rect = result.auth_fallback_rect; - self.welcome_changelog_cta_rect = result.changelog_cta_rect; self.session_picker_state.hit_areas = result.session_picker_hit_areas; if let Some(modal) = self.import_claude_modal.as_mut() { let theme = crate::theme::Theme::current(); @@ -4324,8 +4236,6 @@ pub(crate) mod tests { pending_notification_escapes: None, deferred_notification: None, tracing_rx: None, - changelog_markdown: None, - changelog_bullets: Vec::new(), tips: Vec::new(), tip: None, cli_model_override: None, @@ -4379,7 +4289,6 @@ pub(crate) mod tests { welcome_tip_typing_dismissed: false, welcome_menu_index: None, welcome_menu_rects: Vec::new(), - welcome_show_changelog_action: false, welcome_import_banner_rect: None, last_mouse_pos: None, last_scroll_pos: None, @@ -4387,9 +4296,7 @@ pub(crate) mod tests { welcome_prompt_rect: None, welcome_auth_url_rect: None, welcome_on_auth_url: false, - welcome_on_changelog_cta: false, welcome_auth_fallback_rect: None, - welcome_changelog_cta_rect: None, auth_show_raw_url: false, auth_mouse_disabled: false, session_picker_entries: None, @@ -5756,64 +5663,40 @@ pub(crate) mod tests { ); } #[test] - fn menu_action_indices_without_changelog() { + fn menu_action_indices() { assert!(matches!( - dispatch_menu_action(0, false, false, None), + dispatch_menu_action(0, false), InputOutcome::Action(Action::OpenNewWorktreeDialog) )); assert!(matches!( - dispatch_menu_action(1, false, false, None), + dispatch_menu_action(1, false), InputOutcome::Action(Action::FetchSessionList) )); assert!(matches!( - dispatch_menu_action(2, false, false, None), + dispatch_menu_action(2, false), InputOutcome::Action(Action::Quit) )); - } - #[test] - fn menu_action_changelog_sits_above_quit() { - let md = Some("# notes"); assert!(matches!( - dispatch_menu_action(1, false, true, md), - InputOutcome::Action(Action::FetchSessionList) - )); - assert!(matches!( - dispatch_menu_action(2, false, true, md), - InputOutcome::Action(Action::ShowReleaseNotes { .. }) - )); - assert!(matches!( - dispatch_menu_action(3, false, true, md), - InputOutcome::Action(Action::Quit) - )); - } - #[test] - fn menu_action_changelog_before_fetch_is_noop() { - assert!(matches!( - dispatch_menu_action(2, false, true, None), + dispatch_menu_action(3, false), InputOutcome::Unchanged )); } #[test] - fn menu_action_indices_with_import_and_changelog() { - let md = Some("# notes"); + fn menu_action_indices_with_import() { assert!(matches!( - dispatch_menu_action(0, true, true, md), + dispatch_menu_action(0, true), InputOutcome::Action(Action::ImportClaudeSettings) )); assert!(matches!( - dispatch_menu_action(1, true, true, md), + dispatch_menu_action(1, true), InputOutcome::Action(Action::OpenNewWorktreeDialog) )); assert!(matches!( - dispatch_menu_action(2, true, true, md), + dispatch_menu_action(2, true), InputOutcome::Action(Action::FetchSessionList) )); assert!(matches!( - dispatch_menu_action(3, true, true, md), - InputOutcome::Action(Action::ShowReleaseNotes { .. }) - )); - assert!(matches!( - dispatch_menu_action(4, true, true, md), + dispatch_menu_action(3, true), InputOutcome::Action(Action::Quit) )); } diff --git a/crates/codegen/kigi-tui/src/app/dispatch/auth.rs b/crates/codegen/kigi-tui/src/app/dispatch/auth.rs index af74fcb..0abd647 100644 --- a/crates/codegen/kigi-tui/src/app/dispatch/auth.rs +++ b/crates/codegen/kigi-tui/src/app/dispatch/auth.rs @@ -387,9 +387,6 @@ pub(super) fn handle_auth_complete( // status only; shell auto-syncs post-auth let mut effects = dispatch(Action::RequestBundleStatus, app); - // Fetch changelog (mirrors startup path for interactive login). - effects.push(Effect::FetchChangelog); - // Replay deferred session startup once BOTH gates are open. Auth // is now Done, so `session_startup_allowed()` here means "is trust // also resolved?" -- if trust is still Pending its question renders diff --git a/crates/codegen/kigi-tui/src/app/dispatch/task_result.rs b/crates/codegen/kigi-tui/src/app/dispatch/task_result.rs index aac40d0..e902fec 100644 --- a/crates/codegen/kigi-tui/src/app/dispatch/task_result.rs +++ b/crates/codegen/kigi-tui/src/app/dispatch/task_result.rs @@ -442,11 +442,6 @@ pub(super) fn dispatch_task_result(result: TaskResult, app: &mut AppView) -> Vec } vec![] } - TaskResult::ChangelogFetched { markdown, entries } => { - app.changelog_markdown = markdown; - app.changelog_bullets = kigi_shell::util::changelog::bullets_from_entries(&entries, 3); - vec![] - } TaskResult::ClipboardAttachmentProbed { ctx, image, diff --git a/crates/codegen/kigi-tui/src/app/dispatch/tests/mod.rs b/crates/codegen/kigi-tui/src/app/dispatch/tests/mod.rs index ab88021..322ce2f 100644 --- a/crates/codegen/kigi-tui/src/app/dispatch/tests/mod.rs +++ b/crates/codegen/kigi-tui/src/app/dispatch/tests/mod.rs @@ -85,8 +85,6 @@ fn test_app() -> AppView { pending_notification_escapes: None, deferred_notification: None, tracing_rx: None, - changelog_markdown: None, - changelog_bullets: Vec::new(), tips: Vec::new(), tip: None, cli_model_override: None, @@ -143,7 +141,6 @@ fn test_app() -> AppView { welcome_tip_typing_dismissed: false, welcome_menu_index: None, welcome_menu_rects: Vec::new(), - welcome_show_changelog_action: false, welcome_import_banner_rect: None, last_mouse_pos: None, last_scroll_pos: None, @@ -151,9 +148,7 @@ fn test_app() -> AppView { welcome_prompt_rect: None, welcome_auth_url_rect: None, welcome_on_auth_url: false, - welcome_on_changelog_cta: false, welcome_auth_fallback_rect: None, - welcome_changelog_cta_rect: None, auth_show_raw_url: false, auth_mouse_disabled: false, session_picker_entries: None, diff --git a/crates/codegen/kigi-tui/src/app/effects/mod.rs b/crates/codegen/kigi-tui/src/app/effects/mod.rs index e496553..061d8c3 100644 --- a/crates/codegen/kigi-tui/src/app/effects/mod.rs +++ b/crates/codegen/kigi-tui/src/app/effects/mod.rs @@ -1712,27 +1712,6 @@ pub(crate) fn execute( TaskResult::PromptImagePreviewPrepared }); } - Effect::FetchChangelog => { - tasks - .spawn(async move { - let changelog = tokio::task::spawn_blocking(|| { - kigi_shell::util::changelog::ChangelogManager::new() - .fetch() - }) - .await - .unwrap_or_else(|e| { - tracing::warn!(error = % e, "changelog fetch task failed"); - kigi_shell::util::changelog::Changelog { - markdown: None, - entries: None, - } - }); - TaskResult::ChangelogFetched { - markdown: changelog.markdown, - entries: changelog.entries.unwrap_or_default(), - } - }); - } Effect::PersistMemoryFullscreen { fullscreen } => { persist_hint( tasks, diff --git a/crates/codegen/kigi-tui/src/app/event_loop.rs b/crates/codegen/kigi-tui/src/app/event_loop.rs index ec152c5..1bb02c1 100644 --- a/crates/codegen/kigi-tui/src/app/event_loop.rs +++ b/crates/codegen/kigi-tui/src/app/event_loop.rs @@ -1136,12 +1136,6 @@ pub(crate) async fn run( if process_effects(effs, &mut tasks, &mut app, &progress_tx) { return Ok(make_run_result(&app)); } - // Fetch changelog off the render path so the welcome screen - // can display bullets and /release-notes uses the cached result. - let effs = vec![super::actions::Effect::FetchChangelog]; - if process_effects(effs, &mut tasks, &mut app, &progress_tx) { - return Ok(make_run_result(&app)); - } } if !post_render_effects.is_empty() diff --git a/crates/codegen/kigi-tui/src/slash/commands/mod.rs b/crates/codegen/kigi-tui/src/slash/commands/mod.rs index 3ae6f8d..a26785c 100644 --- a/crates/codegen/kigi-tui/src/slash/commands/mod.rs +++ b/crates/codegen/kigi-tui/src/slash/commands/mod.rs @@ -41,7 +41,6 @@ pub mod plan; pub mod plugin; pub mod queue; pub mod recap; -pub mod release_notes; pub mod remember; pub mod rename; pub mod resume; @@ -121,7 +120,6 @@ pub fn builtin_commands() -> Vec> { Arc::new(usage::UsageCommand), Arc::new(queue::QueueCommand), Arc::new(tasks::TasksCommand), - Arc::new(release_notes::ReleaseNotesCommand), Arc::new(config_agents::ConfigAgentsCommand), Arc::new(personas::PersonasCommand), Arc::new(gboom::GboomCommand), diff --git a/crates/codegen/kigi-tui/src/slash/commands/release_notes.rs b/crates/codegen/kigi-tui/src/slash/commands/release_notes.rs deleted file mode 100644 index 9d4dedf..0000000 --- a/crates/codegen/kigi-tui/src/slash/commands/release_notes.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! `/release-notes` -- view release notes for the current version. - -use crate::app::actions::Action; -use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand}; - -/// Show release notes for the current pager version. -pub struct ReleaseNotesCommand; - -impl SlashCommand for ReleaseNotesCommand { - fn name(&self) -> &str { - "release-notes" - } - - fn aliases(&self) -> &[&str] { - &["changelog"] - } - - fn description(&self) -> &str { - "View release notes for the current version" - } - - fn usage(&self) -> &str { - "/release-notes" - } - - fn run(&self, _ctx: &mut CommandExecCtx, _args: &str) -> CommandResult { - let changelog = kigi_shell::util::changelog::ChangelogManager::new().fetch(); - match changelog.markdown { - Some(content) => CommandResult::Action(Action::ShowReleaseNotes { - title: "Release Notes".to_string(), - content: content.trim().to_string(), - }), - None => CommandResult::Error("No release notes available (offline).".to_string()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn release_notes_metadata() { - let cmd = ReleaseNotesCommand; - assert_eq!(cmd.name(), "release-notes"); - assert_eq!(cmd.aliases(), &["changelog"]); - assert!(!cmd.takes_args()); - } - - #[test] - fn release_notes_returns_action_or_error() { - let models = crate::acp::model_state::ModelState::default(); - let mut ctx = super::super::tests::make_ctx(&models); - let result = ReleaseNotesCommand.run(&mut ctx, ""); - assert!( - matches!(result, CommandResult::Action(_) | CommandResult::Error(_)), - "expected Action or Error, got {result:?}" - ); - } -} diff --git a/crates/codegen/kigi-tui/src/views/welcome/hero_box.rs b/crates/codegen/kigi-tui/src/views/welcome/hero_box.rs index c36256e..582a5e7 100644 --- a/crates/codegen/kigi-tui/src/views/welcome/hero_box.rs +++ b/crates/codegen/kigi-tui/src/views/welcome/hero_box.rs @@ -1,8 +1,8 @@ //! Hero box component — side-by-side logo + menu inside a bordered box. use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Flex, Layout, Position, Rect}; -use ratatui::style::{Modifier, Style}; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::style::Style; use ratatui::text::Span; use ratatui::widgets::{Block, BorderType, Borders, Widget}; @@ -27,18 +27,11 @@ const HERO_SUBTITLE: &str = "Thanks for trying Kigi, give feedback with /feedbac use super::{PROMPT_HEIGHT, VERSION_GAP}; -/// Rows the "thanks" subtitle occupies. Hidden when the in-box info slot -/// (changelog) is shown, to keep the box compact. -fn subtitle_rows(info_height: u16) -> u16 { - if info_height > 0 { 0 } else { 1 } -} - -/// Height of the hero box's right column: version + optional subtitle + -/// optional info block + the gap before the menu + the menu itself. -fn right_col_height(menu_height: u16, info_height: u16) -> u16 { - let info_gap = if info_height > 0 { 1u16 } else { 0 }; - // version(1) + subtitle + [info_gap + info] + gap-before-menu(1) + menu - 1 + subtitle_rows(info_height) + info_gap + info_height + 1 + menu_height +/// Height of the hero box's right column: version + subtitle + the gap +/// before the menu + the menu itself. +fn right_col_height(menu_height: u16) -> u16 { + // version(1) + subtitle(1) + gap-before-menu(1) + menu + 3 + menu_height } /// Minimum content-area height the hero box needs to render without truncating: @@ -46,13 +39,8 @@ fn right_col_height(menu_height: u16, info_height: u16) -> u16 { /// below (tip + prompt + version). The box always shows the full-height logo, /// so a terminal shorter than this falls back to the stacked layout instead of /// overflowing. -pub(super) fn min_content_height( - error_height: u16, - menu_height: u16, - tip_height: u16, - info_height: u16, -) -> u16 { - let inner = super::logo::full_logo_line_count().max(right_col_height(menu_height, info_height)); +pub(super) fn min_content_height(error_height: u16, menu_height: u16, tip_height: u16) -> u16 { + let inner = super::logo::full_logo_line_count().max(right_col_height(menu_height)); let hero_box_height = 2 + V_PAD * 2 + inner; let gap_after_error = if error_height > 0 { 1u16 } else { 0 }; gap_after_error + error_height + hero_box_height + 1 + WelcomeLayout::fixed_below(tip_height) @@ -70,33 +58,26 @@ fn left_col_width() -> u16 { } /// Compute the hero box layout: bordered box with logo left, version + menu right. -/// -/// Sizes the in-box info slot here (the fixed `changelog_height`) so the -/// renderer just draws into `hero_info`. pub(super) fn compute_hero_box( content_area: Rect, error_height: u16, menu_height: u16, tip_height: u16, - changelog_height: u16, ) -> WelcomeLayout { let zero = Rect::default(); let tip_gap = if tip_height > 0 { 1u16 } else { 0 }; let fixed_below = WelcomeLayout::fixed_below(tip_height); // Column widths are height-independent, so derive them once and reuse for - // both the measurement and the rects: `hero_info.width == info_slot_width`, - // i.e. measured == drawn. + // both the measurement and the rects. let box_width = content_area.width.saturating_sub(6).min(120); let inner_width = box_width.saturating_sub(2); let left_col_width = left_col_width(); let right_width = inner_width.saturating_sub(left_col_width); - let info_slot_width = right_width.saturating_sub(H_INSET); - let info_height = changelog_height; + let menu_slot_width = right_width.saturating_sub(H_INSET); let logo_rows = super::logo::full_logo_line_count(); - let info_gap = if info_height > 0 { 1u16 } else { 0 }; - let inner_height = logo_rows.max(right_col_height(menu_height, info_height)); + let inner_height = logo_rows.max(right_col_height(menu_height)); let hero_box_height = 2 + V_PAD * 2 + inner_height; let gap_after_error = if error_height > 0 { 1 } else { 0 }; @@ -105,7 +86,7 @@ pub(super) fn compute_hero_box( // Top padding for vertical centering (use the default menu height so the // logo position stays constant regardless of picker/focus state). let default_menu_height = 4u16; - let default_inner = logo_rows.max(right_col_height(default_menu_height, info_height)); + let default_inner = logo_rows.max(right_col_height(default_menu_height)); let default_hero = 2 + V_PAD * 2 + default_inner; let remaining = content_area.height.saturating_sub(fixed_above); let top_pad = remaining @@ -190,39 +171,22 @@ pub(super) fn compute_hero_box( height: 1, }; - // Subtitle line below version — hidden when the info slot is shown. - let hero_subtitle = if subtitle_rows(info_height) > 0 { - Rect { - x: right_x, - y: inner.y + 1, - width: right_width, - height: 1, - } - } else { - zero + // Subtitle line below version. + let hero_subtitle = Rect { + x: right_x, + y: inner.y + 1, + width: right_width, + height: 1, }; - // Info block (changelog) below version + optional subtitle. - let info_y = inner.y + 1 + subtitle_rows(info_height) + info_gap; - let hero_info = if info_height > 0 { - Rect { - x: right_x, - y: info_y, - width: info_slot_width, - height: info_height, - } - } else { - zero - }; - - // version + subtitle + info_gap + info + gap-before-menu - let right_header_rows = 1 + subtitle_rows(info_height) + info_gap + info_height + 1; + // version + subtitle + gap-before-menu + let right_header_rows = 3; // Menu below the header rows, left-aligned in right column. let hero_menu = Rect { x: right_x, y: inner.y + right_header_rows, - width: info_slot_width, + width: menu_slot_width, height: menu_height.min(inner.height.saturating_sub(right_header_rows)), }; @@ -230,7 +194,6 @@ pub(super) fn compute_hero_box( logo: zero, error, menu: zero, - changelog: zero, tip, prompt, version: version_slot, @@ -238,26 +201,12 @@ pub(super) fn compute_hero_box( hero_logo, hero_version, hero_subtitle, - hero_info, hero_menu, } } -/// Changelog content shown in the hero box info slot. -pub(super) struct ChangelogDisplay<'a> { - pub(super) bullets: &'a [String], - pub(super) has_full_notes: bool, -} - -/// Hit-test rects produced by [`render_hero_box`]. -pub(super) struct HeroBoxRects { - /// Hit-test rect per menu item row (for click/hover). - pub(super) menu_rects: Vec, - /// Clickable changelog info block, if drawn. - pub(super) changelog_cta_rect: Option, -} - /// Render the bordered hero box with logo left, version + subtitle + menu right. +/// Returns the hit-test rect per menu item row (for click/hover). pub(super) fn render_hero_box( layout: &WelcomeLayout, buf: &mut Buffer, @@ -265,8 +214,7 @@ pub(super) fn render_hero_box( menu_items: &[(&str, &str)], selected: Option, mouse_pos: Option<(u16, u16)>, - changelog: ChangelogDisplay<'_>, -) -> HeroBoxRects { +) -> Vec { // Dim the box border toward the background for a softer, dimmer gray. let border_color = crate::render::color::blend_color(theme.bg_base, theme.gray_dim, 0.45) .unwrap_or(theme.gray_dim); @@ -289,30 +237,15 @@ pub(super) fn render_hero_box( ); // Subtitle line below the version. - if layout.hero_subtitle.height > 0 { - let subtitle_style = Style::default().fg(theme.gray); - buf.set_span( - layout.hero_subtitle.x, - layout.hero_subtitle.y, - &Span::styled(HERO_SUBTITLE, subtitle_style), - layout.hero_subtitle.width, - ); - } + let subtitle_style = Style::default().fg(theme.gray); + buf.set_span( + layout.hero_subtitle.x, + layout.hero_subtitle.y, + &Span::styled(HERO_SUBTITLE, subtitle_style), + layout.hero_subtitle.width, + ); - // In-box info slot: the changelog, always in this same position. - let mut changelog_cta_rect = None; - if layout.hero_info.height > 0 && !changelog.bullets.is_empty() { - changelog_cta_rect = render_hero_changelog( - buf, - theme, - layout.hero_info, - changelog.bullets, - changelog.has_full_notes, - mouse_pos, - ); - } - - let menu_rects = super::menu::render_menu( + super::menu::render_menu( layout.hero_menu, buf, theme, @@ -320,58 +253,5 @@ pub(super) fn render_hero_box( selected, mouse_pos, layout.hero_menu.width, - ); - HeroBoxRects { - menu_rects, - changelog_cta_rect, - } -} - -/// Render the changelog block (header + bullets) in the info slot. When -/// `clickable` (full notes exist), the whole block opens the notes on click and -/// brightens while hovered; returns that clickable rect. -fn render_hero_changelog( - buf: &mut Buffer, - theme: &Theme, - area: Rect, - bullets: &[String], - clickable: bool, - mouse_pos: Option<(u16, u16)>, -) -> Option { - if area.width == 0 || area.height == 0 { - return None; - } - - let hovered = - clickable && mouse_pos.is_some_and(|(mx, my)| area.contains(Position::new(mx, my))); - - let header_style = super::hover_style( - theme, - hovered, - Style::default() - .fg(theme.gray_bright) - .add_modifier(Modifier::DIM), - ); - let title = "Changelog"; - buf.set_span( - area.x, - area.y, - &Span::styled(title, header_style), - area.width, - ); - - // Bullets start 2 rows down (header + blank), matching the height budget. - let bullet_style = super::hover_style(theme, hovered, Style::default().fg(theme.gray_bright)); - let max_text_width = area.width.saturating_sub(4) as usize; // " • " prefix + pad - for (i, bullet) in bullets.iter().enumerate() { - let row = area.y + 2 + i as u16; - if row >= area.y + area.height { - break; - } - let truncated = crate::render::line_utils::truncate_str(bullet, max_text_width); - let text = format!(" \u{2022} {truncated}"); - buf.set_span(area.x, row, &Span::styled(text, bullet_style), area.width); - } - - clickable.then_some(area) + ) } diff --git a/crates/codegen/kigi-tui/src/views/welcome/mod.rs b/crates/codegen/kigi-tui/src/views/welcome/mod.rs index e751e36..4996f4b 100644 --- a/crates/codegen/kigi-tui/src/views/welcome/mod.rs +++ b/crates/codegen/kigi-tui/src/views/welcome/mod.rs @@ -7,7 +7,7 @@ //! - Bottom margin use ratatui::buffer::Buffer; -use ratatui::layout::{Alignment, Constraint, Flex, Layout, Position, Rect}; +use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Padding, Paragraph, Widget, Wrap}; @@ -56,22 +56,12 @@ fn quit_hint_spans(theme: &Theme) -> Vec> { ] } -/// Style for a clickable welcome block: bright primary while `hovered`, else -/// `base`. Shared by the changelog renderer. -pub(super) fn hover_style(theme: &Theme, hovered: bool, base: Style) -> Style { - if hovered { - Style::default().fg(theme.text_primary) - } else { - base - } -} - /// Horizontal margin (left and right) in normal mode. const H_MARGIN: u16 = 2; /// Horizontal margin in compact mode. const H_MARGIN_COMPACT: u16 = 1; -/// Minimum width for menu + changelog sections so they don't resize when the import row toggles. +/// Minimum width for the menu section so it doesn't resize when the import row toggles. /// Derivation: "[ " (2) + import-claude label (22) + gap (4) + "ctrl+i [x]" (11) + " ]" (2) = 41. /// Bumped to 51 for comfortable breathing room. const MENU_MIN_WIDTH: u16 = 51; @@ -103,13 +93,6 @@ pub struct WelcomeRenderResult { pub auth_url_rect: Option, /// Hit-test rect for the "show full URL" fallback link. pub auth_fallback_rect: Option, - /// Hit-test rect for the "[Refresh]" button on the paywall tier line. - /// Whether a "Changelog" menu action was rendered (above Quit), so the - /// input handler can map the extra menu row to the release-notes action - /// once markdown is available. - pub changelog_action_present: bool, - /// Hit-test rect for the clickable changelog info block (opens release notes). - pub changelog_cta_rect: Option, } use hero_box::HERO_BOX_MIN_WIDTH; @@ -124,9 +107,6 @@ pub(super) struct WelcomeLayout { pub(super) logo: Rect, pub(super) error: Rect, pub(super) menu: Rect, - /// Stacked info slot below the menu (narrow layout only) — shows the - /// changelog. Zero in the hero box layout, which uses `hero_info` instead. - pub(super) changelog: Rect, pub(super) tip: Rect, pub(super) prompt: Rect, pub(super) version: Rect, @@ -135,8 +115,6 @@ pub(super) struct WelcomeLayout { pub(super) hero_logo: Rect, pub(super) hero_version: Rect, pub(super) hero_subtitle: Rect, - /// In-box info slot — shows the changelog. - pub(super) hero_info: Rect, pub(super) hero_menu: Rect, } @@ -151,9 +129,7 @@ struct WelcomeLayoutInput { error_height: u16, menu_height: u16, tip_height: u16, - /// Desired changelog height (collapsed to 0 if the terminal is too short). - changelog_height: u16, - /// Vertical compaction (session picker visible): skip the logo + info slot. + /// Vertical compaction (session picker visible): skip the logo. compact: bool, /// Horizontal-inset compaction (appearance setting) for the stacked slot. prompt_compact: bool, @@ -170,22 +146,6 @@ impl WelcomeLayout { tip_height + tip_gap + PROMPT_HEIGHT + VERSION_GAP + 1 } - pub(super) fn effective_changelog( - content_height: u16, - fixed_above: u16, - content_slot: u16, - fixed_below: u16, - requested: u16, - ) -> (u16, u16) { - let gap = if requested > 0 { 1u16 } else { 0 }; - let min_without = fixed_above + content_slot + 1 + fixed_below; - if requested > 0 && content_height >= min_without + gap + requested { - (requested, 1) - } else { - (0, 0) - } - } - /// Compute the welcome screen layout, allowing the wide hero-box variant. fn compute(input: WelcomeLayoutInput) -> Self { Self::compute_inner(input, true) @@ -203,17 +163,14 @@ impl WelcomeLayout { /// Compute the welcome screen layout. /// - /// Picks hero vs stacked, then measures the info slot (the changelog) at - /// that layout's slot width before placing rects — width is - /// content-size-only, so it's a clean two-phase computation. `allow_hero_box` - /// gates the wide variant; stacked-only callers pass `false`. + /// Picks hero vs stacked. `allow_hero_box` gates the wide variant; + /// stacked-only callers pass `false`. fn compute_inner(input: WelcomeLayoutInput, allow_hero_box: bool) -> Self { let WelcomeLayoutInput { content_area, error_height, menu_height, tip_height, - changelog_height, compact, prompt_compact, } = input; @@ -224,26 +181,12 @@ impl WelcomeLayout { && content_area.width >= HERO_BOX_MIN_WIDTH && menu_height > 0 && content_area.height - >= hero_box::min_content_height( - error_height, - menu_height, - tip_height, - changelog_height, - ); + >= hero_box::min_content_height(error_height, menu_height, tip_height); if use_hero_box { - return hero_box::compute_hero_box( - content_area, - error_height, - menu_height, - tip_height, - changelog_height, - ); + return hero_box::compute_hero_box(content_area, error_height, menu_height, tip_height); } - // Stacked info slot: the changelog. - let info_height = changelog_height; - // Stacked layout: skip the logo in compact mode (the session picker // needs the space); otherwise pick small/full/none by height. let logo_rows = if compact { @@ -256,19 +199,6 @@ impl WelcomeLayout { let tip_gap = if tip_height > 0 { 1u16 } else { 0 }; let fixed_below = Self::fixed_below(tip_height); let fixed_above = logo_rows + 1 + gap_after_logo + error_height; // +1 for gap after logo - // The stacked info slot below the menu holds the changelog. - let (eff_changelog_height, _) = if !compact { - Self::effective_changelog( - content_area.height, - fixed_above, - menu_height, - fixed_below, - info_height, - ) - } else { - (0, 0) - }; - let eff_changelog_gap = if eff_changelog_height > 0 { 1u16 } else { 0 }; // Compute top_pad using the *default* menu height (4 items = 7 rows) so // the logo position stays constant regardless of picker/focus state. let top_pad = if compact { @@ -278,36 +208,18 @@ impl WelcomeLayout { let remaining = content_area.height.saturating_sub(fixed_above); remaining .saturating_sub(default_menu_height) - .saturating_sub(eff_changelog_gap + eff_changelog_height) .saturating_sub(fixed_below) / 3 }; let logo_gap = 1u16; let flex_gap = 1u16; - let [ - _, - logo, - _, - _, - error, - menu, - _, - changelog, - _, - tip, - _, - prompt, - _, - version, - ] = Layout::vertical([ + let [_, logo, _, _, error, menu, _, tip, _, prompt, _, version] = Layout::vertical([ Constraint::Length(top_pad), Constraint::Length(logo_rows), Constraint::Length(logo_gap), // gap after logo Constraint::Length(gap_after_logo), Constraint::Length(error_height), Constraint::Length(menu_height), - Constraint::Length(eff_changelog_gap), - Constraint::Length(eff_changelog_height), Constraint::Min(flex_gap), Constraint::Length(tip_height), Constraint::Length(tip_gap), @@ -320,7 +232,6 @@ impl WelcomeLayout { logo, error, menu, - changelog, tip, prompt, version, @@ -328,7 +239,6 @@ impl WelcomeLayout { hero_logo: zero, hero_version: zero, hero_subtitle: zero, - hero_info: zero, hero_menu: zero, } } @@ -566,10 +476,6 @@ pub struct WelcomeRenderParams<'a> { /// Live working directory (tracks `Effect::SetWorkingDir`), used to pin /// the current repo's session group to the top of the picker. pub cwd: &'a std::path::Path, - /// Cached changelog bullets for the welcome screen (up to 3). - pub changelog_bullets: &'a [String], - /// Whether full release notes markdown is available (controls the CTA hint). - pub changelog_has_full_notes: bool, } /// Render the welcome screen. @@ -642,8 +548,6 @@ pub fn render_welcome( import_banner_rect: None, auth_url_rect: None, auth_fallback_rect: None, - changelog_action_present: false, - changelog_cta_rect: None, } } AuthState::Authenticating { auth_url, mode, .. } => { @@ -668,8 +572,6 @@ pub fn render_welcome( import_banner_rect: None, auth_url_rect: url_rect, auth_fallback_rect: fallback_rect, - changelog_action_present: false, - changelog_cta_rect: None, } } // Folder-trust question: shown after auth, before any session is @@ -1450,73 +1352,6 @@ fn inset_horizontal(rect: Rect, inset: u16) -> Rect { } } -/// Render the changelog section (header + bullets), centered to the menu width. -/// When `clickable` (full notes exist) the whole block opens the notes on click -/// and brightens while hovered; returns that clickable rect. -#[allow(clippy::too_many_arguments)] -fn render_changelog_section( - area: Rect, - buf: &mut Buffer, - theme: &Theme, - bullets: &[String], - min_width_hint: u16, - content_height: u16, - clickable: bool, - mouse_pos: Option<(u16, u16)>, -) -> Option { - let menu_width = logo::logo_visual_width(content_height) - .max(30) - .max(min_width_hint); - let [_, centered, _] = Layout::horizontal([ - Constraint::Min(0), - Constraint::Length(menu_width), - Constraint::Min(0), - ]) - .flex(Flex::Center) - .areas(area); - - if centered.width < 20 || centered.height == 0 { - return None; - } - - let hovered = - clickable && mouse_pos.is_some_and(|(mx, my)| centered.contains(Position::new(mx, my))); - - let header_style = hover_style( - theme, - hovered, - Style::default() - .fg(theme.gray_bright) - .add_modifier(Modifier::DIM), - ); - let title = "Changelog"; - buf.set_span( - centered.x, - centered.y, - &Span::styled(title, header_style), - centered.width, - ); - - let bullet_style = hover_style(theme, hovered, Style::default().fg(theme.gray_bright)); - let max_text_width = centered.width.saturating_sub(2) as usize; // "• " prefix = 2 cols - for (i, bullet) in bullets.iter().enumerate() { - let row = centered.y + 2 + i as u16; - if row >= centered.y + centered.height { - break; - } - let truncated = crate::render::line_utils::truncate_str(bullet, max_text_width); - let text = format!("\u{2022} {truncated}"); - buf.set_span( - centered.x, - row, - &Span::styled(text, bullet_style), - centered.width, - ); - } - - clickable.then_some(centered) -} - /// Render the normal welcome screen (Done state -- already authenticated). fn render_welcome_done( content_area: Rect, @@ -1535,8 +1370,6 @@ fn render_welcome_done( let in_vscode_family = welcome_in_vscode_family(); - // Heights that don't depend on the menu — computed first so the menu - // builder can probe the layout to decide whether to add a Changelog row. // Startup-warning hint height (multi-line aware). let hint_height = p.startup_warnings.first().map_or(0u16, |w| { let msg_lines = w.message.lines().count() as u16; @@ -1558,14 +1391,6 @@ fn render_welcome_done( } else { 0 }; - let changelog_height = if !show_picker && !p.changelog_bullets.is_empty() { - 2 + p.changelog_bullets.len() as u16 - } else { - 0 - }; - // Changelog is reachable via this menu row (ctrl+l). Show from the first - // frame so the menu doesn't shift while the CDN fetch completes. - let show_changelog_action = !show_picker; let owned_menu; let menu_items: &[(&str, &str)] = { @@ -1577,7 +1402,7 @@ fn render_welcome_done( ); // Insert the import row at the top when there are pending `.claude/` // settings to import — it's the most actionable item right now. - let mut items: Vec<(&str, &str)> = Vec::with_capacity(5); + let mut items: Vec<(&str, &str)> = Vec::with_capacity(4); if p.has_claude_import { // The trailing "[x]" is a clickable dismiss affordance — the // welcome screen mouse handler treats clicks on the rightmost @@ -1588,10 +1413,6 @@ fn render_welcome_done( } items.push((key_w, "New worktree")); items.push((key_s, "Resume session")); - // "Changelog" above Quit; no shortcut — opened by click (row or block). - if show_changelog_action { - items.push(("", "Changelog")); - } items.push((key_q, "Quit")); owned_menu = items; owned_menu.as_slice() @@ -1620,7 +1441,6 @@ fn render_welcome_done( error_height: hint_height, menu_height: content_height, tip_height, - changelog_height, compact: welcome_compact, prompt_compact: p.compact, }); @@ -1628,9 +1448,6 @@ fn render_welcome_done( // Render startup warning in the error area (same slot as auth errors). let import_banner_rect = render_startup_warnings(layout.error, buf, theme, p.startup_warnings); - // Hit-rects, set by whichever layout draws each block. - let mut changelog_cta_rect: Option = None; - let (menu_rects, picker_close_button) = if show_picker { // Use the full area since logo/menu are hidden and shortcuts // are now rendered inside the picker content area. @@ -1663,20 +1480,9 @@ fn render_welcome_done( (vec![], Some(hit_areas)) } else if layout.has_hero_box() { // Wide layout: render bordered hero box with logo left, version + menu right. - let rects = hero_box::render_hero_box( - &layout, - buf, - theme, - menu_items, - p.selected, - p.mouse_pos, - hero_box::ChangelogDisplay { - bullets: p.changelog_bullets, - has_full_notes: p.changelog_has_full_notes, - }, - ); - changelog_cta_rect = rects.changelog_cta_rect; - (rects.menu_rects, None) + let menu_rects = + hero_box::render_hero_box(&layout, buf, theme, menu_items, p.selected, p.mouse_pos); + (menu_rects, None) } else { // Narrow layout: stacked logo above, menu below. Inset the menu the // same as the input bar (`prompt_inset`) so it keeps side spacing @@ -1697,23 +1503,6 @@ fn render_welcome_done( ) }; - // Stacked info slot below the menu (narrow layout): show the changelog, - // mirroring the hero box. Inset to match the input bar so it lines up with - // the menu above. - if layout.changelog.height > 0 { - let info_area = inset_horizontal(layout.changelog, prompt::prompt_inset(p.compact)); - changelog_cta_rect = render_changelog_section( - info_area, - buf, - theme, - p.changelog_bullets, - MENU_MIN_WIDTH, - content_area.height, - p.changelog_has_full_notes, - p.mouse_pos, - ); - } - // Skip the prompt input when picker is visible to save space; // shortcuts are rendered inside the picker content area. let (cursor_pos, post_flush_escapes) = if show_picker { @@ -1840,8 +1629,6 @@ fn render_welcome_done( import_banner_rect, auth_url_rect: None, auth_fallback_rect: None, - changelog_action_present: show_changelog_action, - changelog_cta_rect, } } @@ -2278,8 +2065,6 @@ mod tests { session_picker_source_filter: crate::views::session_picker::SourceFilter::All, chat_mode: false, cwd: std::path::Path::new("/repo"), - changelog_bullets: &[], - changelog_has_full_notes: false, } } @@ -2747,106 +2532,6 @@ mod tests { assert_eq!(state.query, "e"); } - #[test] - fn changelog_hidden_on_short_terminal() { - let area = Rect::new(0, 0, 80, 15); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 0); - } - - #[test] - fn changelog_shown_on_tall_terminal() { - let area = Rect::new(0, 0, 80, 50); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 5); - } - - #[test] - fn changelog_hidden_when_compact() { - let area = Rect::new(0, 0, 80, 60); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - changelog_height: 5, - compact: true, - prompt_compact: true, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 0); - } - - #[test] - fn changelog_hidden_when_zero_requested() { - let area = Rect::new(0, 0, 80, 60); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 0); - } - - #[test] - fn changelog_boundary_exact_fit() { - // No logo at h < 22. fixed_above = 0 + 1 + 0 + 0 = 1. - // fixed_below = 0 (tip) + 0 (tip_gap) + 3 (prompt) + 1 (ver_gap) + 1 (ver) = 5. - // min_without_changelog = 1 + 4 (menu) + 1 (flex) + 5 = 11. - // changelog slot = 1 (gap) + 5 (height) = 6. Threshold = 11 + 6 = 17. - let just_fits = Rect::new(0, 0, 80, 17); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: just_fits, - menu_height: 4, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 5); - - let too_short = Rect::new(0, 0, 80, 16); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: too_short, - menu_height: 4, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 0); - } - - #[test] - fn changelog_hidden_when_tip_steals_space() { - // Use narrow width to avoid hero box path, keeping stacked layout. - // With tip_height=2: fixed_below(2) = 8. min = 1 + 4 + 1 + 8 = 14. - // Threshold = 14 + 6 = 20. At h=19 the tip pushes changelog out. - let with_tip = Rect::new(0, 0, 60, 19); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: with_tip, - menu_height: 4, - tip_height: 2, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 0); - - // Same size without tip: threshold = 17 <= 19, changelog fits. - let without_tip = Rect::new(0, 0, 60, 19); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: without_tip, - menu_height: 4, - changelog_height: 5, - ..Default::default() - }); - assert_eq!(layout.changelog.height, 5); - } - #[test] fn hero_box_active_on_wide_tall_terminal() { // 90 cols, 50 rows: meets the minimum for the hero box. @@ -2866,6 +2551,7 @@ mod tests { assert!(layout.hero_logo.height > 0); assert!(layout.hero_menu.height > 0); assert_eq!(layout.hero_version.height, 1); + assert_eq!(layout.hero_subtitle.height, 1); } #[test] @@ -3056,53 +2742,6 @@ mod tests { assert_eq!(layout.hero_logo.y, layout.hero_box.y + 2); } - #[test] - fn hero_box_with_changelog() { - // The changelog renders inside the box (info slot), not in a - // separate area below it. - let area = Rect::new(0, 0, 100, 50); - let layout = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 3, - changelog_height: 5, - ..Default::default() - }); - assert!(layout.has_hero_box()); - assert_eq!(layout.changelog.height, 0); - assert_eq!(layout.hero_info.height, 5); - // The subtitle is hidden when the info slot is shown. - assert_eq!(layout.hero_subtitle.height, 0); - assert!(layout.hero_info.y > layout.hero_version.y); - } - - #[test] - fn hero_box_keeps_one_bottom_pad_below_actions() { - // With a changelog the subtitle is hidden, but there's still exactly - // one padding row between the actions and the bottom border. - // (menu=4 + info=3 fills the inner, so the menu reaches the pad.) - let area = Rect::new(0, 0, 100, 50); - let no_info = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - ..Default::default() - }); - let with_info = WelcomeLayout::compute(WelcomeLayoutInput { - content_area: area, - menu_height: 4, - changelog_height: 3, - ..Default::default() - }); - assert_eq!(no_info.hero_subtitle.height, 1); - assert_eq!(with_info.hero_subtitle.height, 0); - let menu_bottom = with_info.hero_menu.y + with_info.hero_menu.height; - let border_bottom = with_info.hero_box.y + with_info.hero_box.height - 1; - assert_eq!( - border_bottom - menu_bottom, - 1, - "one pad row below the actions" - ); - } - /// Flatten a rendered buffer into one string for substring assertions. fn buffer_text(buf: &Buffer) -> String { let area = *buf.area(); diff --git a/crates/codegen/kigi-tui/tests/scenarios/release_notes_scroll.yaml b/crates/codegen/kigi-tui/tests/scenarios/release_notes_scroll.yaml deleted file mode 100644 index 1df8f5e..0000000 --- a/crates/codegen/kigi-tui/tests/scenarios/release_notes_scroll.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: release-notes-scroll -description: > - Open /release-notes (DocViewer modal) and verify keyboard + mouse-wheel scrolling - moves the changelog body. Seeds CHANGELOG cache offline via setup is not available - in YAML, so this scenario relies on CDN/cache if present and still asserts the - modal chrome + scroll affordances. Prefer the pty_e2e Rust tests for deterministic - offline seeding; this YAML exercises the scripted ptyctl runner path. -terminal: - rows: 40 - cols: 110 -mock: - response: "SCRIPTED_RELEASE_NOTES_SCROLL unused for this scenario." -steps: - - action: wait_for_text - text: Quit - timeout_ms: 20000 - # Promote welcome → session and open release notes (uses CDN or disk cache). - - action: type_text - text: "/release-notes" - - action: keys - keys: "" - - action: wait_for_text - text: Release Notes - timeout_ms: 20000 - - action: assert_contains - text: scroll - # Keyboard scroll through the modal body. - - action: keys - keys: "" - - action: wait - millis: 200 - - action: keys - keys: "jjjjjjjjjj" - - action: wait - millis: 200 - # Mouse wheel at the center of the modal. - - action: scroll - row: 20 - col: 55 - direction: down - count: 15 - - action: wait - millis: 250 - - action: assert_contains - text: Release Notes - - action: assert_not_contains - text: panicked - - action: keys - keys: "" - - action: screenshot - name: release-notes-after-scroll - note: Release notes modal after keyboard + wheel scroll. diff --git a/crates/codegen/kigi-tui/tests/scripted_scenarios.rs b/crates/codegen/kigi-tui/tests/scripted_scenarios.rs index 1435aa4..fdbf88b 100644 --- a/crates/codegen/kigi-tui/tests/scripted_scenarios.rs +++ b/crates/codegen/kigi-tui/tests/scripted_scenarios.rs @@ -64,12 +64,6 @@ async fn scripted_slash_resize_storm() { run_scenario("slash_resize_storm.yaml").await; } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore] -async fn scripted_release_notes_scroll() { - run_scenario("release_notes_scroll.yaml").await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[ignore] async fn scripted_mock_response() {