Excise the Changelog feature

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.
This commit is contained in:
2026-07-18 12:31:52 -04:00
parent 6e26d15428
commit 10d0c0816f
19 changed files with 66 additions and 1206 deletions
+16 -133
View File
@@ -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<String>,
/// Cached changelog bullets (for welcome screen). Populated by
/// `FetchChangelog` at startup; empty until the fetch completes.
pub changelog_bullets: Vec<String>,
/// Resolved tip list from config layers.
pub tips: Vec<String>,
/// Selected tip for the current launch/session.
@@ -709,10 +703,6 @@ pub struct AppView {
pub welcome_menu_index: Option<usize>,
/// Hit-test rects for welcome menu items (populated during render).
pub welcome_menu_rects: Vec<ratatui::layout::Rect>,
/// 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<ratatui::layout::Rect>,
/// Last known mouse position (column, row), updated on every Mouse event.
@@ -734,14 +724,8 @@ pub struct AppView {
pub welcome_auth_url_rect: Option<ratatui::layout::Rect>,
/// 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<ratatui::layout::Rect>,
/// 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<ratatui::layout::Rect>,
/// 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<Vec<SessionPickerEntry>>,
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<crate::views::import_claude_modal::ImportClaudeModalState>,
welcome_doc_viewer: &'a mut Option<crate::views::modal::ActiveModal>,
changelog_markdown: &'a Option<String>,
/// 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)
));
}