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
@@ -1471,10 +1471,6 @@ pub enum Effect {
/// `SwitchModelComplete` so `IncompatibleAgent` can roll back.
prev_model_id: Option<acp::ModelId>,
},
/// 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<acp::ModelId>,
},
/// Changelog fetched from CDN (both formats).
ChangelogFetched {
markdown: Option<String>,
entries: Vec<kigi_shell::util::changelog::ChangelogEntry>,
},
/// Cross-session prompt history loaded from ACP.
PromptHistoryLoaded {
agent_id: AgentId,
+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)
));
}
@@ -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
@@ -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,
@@ -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,
@@ -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,
@@ -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()