From 4d76605b4e25d5c534e3058a47ab0c910675ec23 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Fri, 3 Jul 2026 17:44:08 +0300 Subject: [PATCH 1/2] feat(tui): add changelog promptbar browser --- codex-rs/tui/src/app/event_dispatch.rs | 6 + codex-rs/tui/src/app_event.rs | 8 + codex-rs/tui/src/chatwidget/changelog.rs | 329 +++++++++++++++++- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 2 +- ...ts__changelog_browser_release_bullets.snap | 27 ++ ...og__tests__changelog_browser_versions.snap | 17 + ...slash_changelog_release_notes_preview.snap | 15 - .../src/chatwidget/tests/slash_commands.rs | 68 ++-- 8 files changed, 418 insertions(+), 54 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_release_bullets.snap create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_versions.snap delete mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_changelog_release_notes_preview.snap diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index e65fa7427a..e7572722bf 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -853,6 +853,12 @@ impl App { AppEvent::ReloadMcpServers => { self.reload_mcp_servers(app_server); } + AppEvent::OpenChangelogBrowser => { + self.chat_widget.open_changelog_browser(); + } + AppEvent::OpenChangelogRelease { version } => { + self.chat_widget.open_changelog_release(version); + } AppEvent::OpenBackgroundTerminalManager => { self.chat_widget.open_background_terminal_manager(); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 277775630d..87182260fe 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -376,6 +376,14 @@ pub(crate) enum AppEvent { origin: MiniMaxUsageRefreshOrigin, }, + /// Open the top-level changelog browser. + OpenChangelogBrowser, + + /// Open one release in the changelog browser. + OpenChangelogRelease { + version: String, + }, + /// Open the current thread goal summary/action menu. OpenThreadGoalMenu { thread_id: ThreadId, diff --git a/codex-rs/tui/src/chatwidget/changelog.rs b/codex-rs/tui/src/chatwidget/changelog.rs index 9418c7267d..3cb7734843 100644 --- a/codex-rs/tui/src/chatwidget/changelog.rs +++ b/codex-rs/tui/src/chatwidget/changelog.rs @@ -1,39 +1,338 @@ -//! `/changelog` transcript output. +//! `/changelog` promptbar browser. use super::*; +use crate::bottom_pane::SelectionRowDisplay; const CHANGELOG_SOURCE: &str = include_str!("../../../../CHANGELOG.md"); const RELEASE_SECTION_START: &str = "## [Unreleased]"; +pub(crate) const CHANGELOG_BROWSER_VIEW_ID: &str = "changelog-browser"; impl ChatWidget { - pub(crate) fn add_changelog_output(&mut self) { - self.add_to_history(history_cell::AgentMarkdownCell::new( - changelog_markdown(), - &self.config.cwd, - )); + pub(crate) fn open_changelog_browser(&mut self) { + self.replace_or_show_changelog_view(changelog_browser_params()); + } + + pub(crate) fn open_changelog_release(&mut self, version: String) { + self.replace_or_show_changelog_view(changelog_release_params(&version)); + } + + fn replace_or_show_changelog_view(&mut self, params: SelectionViewParams) { + if self.bottom_pane.active_view_id() == Some(CHANGELOG_BROWSER_VIEW_ID) { + let _ = self + .bottom_pane + .replace_selection_view_if_active(CHANGELOG_BROWSER_VIEW_ID, params); + } else { + self.bottom_pane.show_selection_view(params); + } + self.request_redraw(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ChangelogRelease { + version: String, + heading: String, + date: Option, + bullets: Vec, +} + +fn changelog_browser_params() -> SelectionViewParams { + let releases = changelog_releases(); + let mut header = ColumnRenderable::new(); + header.push(Line::from("Codewith Changelog".bold())); + header.push(Line::from( + "Select a release to view its changelog bullets.".dim(), + )); + + let items = if releases.is_empty() { + vec![SelectionItem { + name: "No releases found".to_string(), + description: Some("CHANGELOG.md does not contain release headings.".to_string()), + is_disabled: true, + ..Default::default() + }] + } else { + releases + .into_iter() + .map(|release| { + let version = release.version; + let search_value = Some(format!( + "{} {}", + version, + release.date.clone().unwrap_or_default() + )); + SelectionItem { + name: version.clone(), + description: release.date, + selected_description: Some(format!("Open {version} release notes.")), + search_value, + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::OpenChangelogRelease { + version: version.clone(), + }); + })], + ..Default::default() + } + }) + .collect() + }; + + SelectionViewParams { + view_id: Some(CHANGELOG_BROWSER_VIEW_ID), + header: Box::new(header), + items, + is_searchable: true, + search_placeholder: Some("Search versions".to_string()), + col_width_mode: ColumnWidthMode::Fixed, + row_display: SelectionRowDisplay::SingleLine, + ..Default::default() + } +} + +fn changelog_release_params(version: &str) -> SelectionViewParams { + let release = changelog_releases() + .into_iter() + .find(|release| release.version == version); + let mut header = ColumnRenderable::new(); + let title = release + .as_ref() + .map(|release| release.heading.as_str()) + .unwrap_or(version); + header.push(Line::from(format!("Codewith {title}").bold())); + header.push(Line::from("Bullets copied from CHANGELOG.md.".dim())); + + let mut items = vec![ + SelectionItem { + name: "Back".to_string(), + description: Some("Return to all changelog versions.".to_string()), + actions: vec![Box::new(|tx| { + tx.send(AppEvent::OpenChangelogBrowser); + })], + ..Default::default() + }, + SelectionItem { + name: "Close".to_string(), + description: Some("Close the changelog browser.".to_string()), + dismiss_on_select: true, + ..Default::default() + }, + ]; + + match release { + Some(release) if release.bullets.is_empty() => { + items.push(SelectionItem { + name: "No bullet entries recorded".to_string(), + description: Some( + "This release section has no bullet items in CHANGELOG.md.".to_string(), + ), + is_disabled: true, + ..Default::default() + }); + } + Some(release) => { + items.extend(release.bullets.into_iter().map(|bullet| SelectionItem { + name: format!("- {bullet}"), + search_value: Some(bullet), + is_disabled: true, + ..Default::default() + })); + } + None => { + items.push(SelectionItem { + name: "Release not found".to_string(), + description: Some( + "The selected version is not present in CHANGELOG.md.".to_string(), + ), + is_disabled: true, + ..Default::default() + }); + } + } + + SelectionViewParams { + view_id: Some(CHANGELOG_BROWSER_VIEW_ID), + header: Box::new(header), + items, + is_searchable: false, + col_width_mode: ColumnWidthMode::Fixed, + row_display: SelectionRowDisplay::Wrapped, + ..Default::default() } } -fn changelog_markdown() -> String { - let release_notes = CHANGELOG_SOURCE +fn changelog_releases() -> Vec { + let mut releases = Vec::::new(); + for line in release_notes_source().split_inclusive('\n') { + if let Some((version, date)) = parse_release_heading(line) { + releases.push(ChangelogRelease { + version, + heading: line.trim_start_matches("## ").trim().to_string(), + date, + bullets: Vec::new(), + }); + continue; + } + + if line.trim().starts_with("## ") && !releases.is_empty() { + break; + } + + if let Some(release) = releases.last_mut() { + push_bullet_line(&mut release.bullets, line); + } + } + releases +} + +fn release_notes_source() -> &'static str { + CHANGELOG_SOURCE .find(RELEASE_SECTION_START) .map(|idx| &CHANGELOG_SOURCE[idx..]) .unwrap_or(CHANGELOG_SOURCE) - .trim(); + .trim() +} - format!("# Codewith Changelog\n\n{release_notes}") +fn parse_release_heading(line: &str) -> Option<(String, Option)> { + let heading = line.trim().strip_prefix("## ")?; + let version_end = heading.find(']').filter(|_| heading.starts_with('['))?; + let version = heading.get(1..version_end)?.to_string(); + let date = heading + .get(version_end + 1..) + .and_then(|rest| rest.trim().strip_prefix("- ")) + .map(str::trim) + .filter(|date| !date.is_empty()) + .map(str::to_string); + Some((version, date)) +} + +fn push_bullet_line(bullets: &mut Vec, line: &str) { + let trimmed = line.trim(); + if trimmed.starts_with("- ") { + bullets.push(trimmed.trim_start_matches("- ").trim().to_string()); + } else if let Some(last) = bullets.last_mut() + && !trimmed.is_empty() + && !trimmed.starts_with('#') + && !trimmed.starts_with("Tag:") + && !trimmed.starts_with("npm:") + && !trimmed.starts_with("Compare:") + { + last.push(' '); + last.push_str(trimmed); + } } #[cfg(test)] mod tests { use super::*; + use crate::app_event_sender::AppEventSender; + use crate::bottom_pane::ListSelectionView; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + use tokio::sync::mpsc::unbounded_channel; + + #[test] + fn changelog_releases_use_release_sections_without_repository_notes() { + let releases = changelog_releases(); + + assert_eq!( + releases.first().map(|release| release.version.as_str()), + Some("Unreleased") + ); + assert!( + releases + .iter() + .any(|release| release.version.starts_with("0.1.")) + ); + assert!( + releases + .iter() + .flat_map(|release| release.bullets.iter()) + .all(|bullet| !bullet.contains("Known evidence gaps")) + ); + } + + #[test] + fn changelog_release_parser_collects_real_bullets() { + let releases = changelog_releases(); + let latest_release = releases + .iter() + .find(|release| release.version.starts_with("0.1.")) + .expect("versioned release"); + + assert!(!latest_release.bullets.is_empty()); + assert!( + latest_release + .bullets + .iter() + .any(|bullet| bullet.contains("Release pipeline")), + "expected parsed real release bullet, got {latest_release:#?}" + ); + } + + #[test] + fn changelog_release_parser_stops_before_maintenance_process() { + let releases = changelog_releases(); + let initial_release = releases + .iter() + .find(|release| release.version == "0.1.0") + .expect("initial release"); + + assert!( + initial_release.bullets.iter().all(|bullet| !bullet + .contains("Determine published npm versions") + && !bullet.contains("Maintenance Process")), + "expected only release bullets, got {initial_release:#?}" + ); + } + + #[test] + fn changelog_browser_versions_snapshot() { + let view = selection_view(changelog_browser_params()); + + insta::assert_snapshot!( + "changelog_browser_versions", + render_lines(&view, /*width*/ 72) + ); + } #[test] - fn changelog_output_uses_release_sections_without_repository_notes() { - let markdown = changelog_markdown(); + fn changelog_browser_release_snapshot() { + let latest_version = changelog_releases() + .into_iter() + .find(|release| release.version.starts_with("0.1.")) + .expect("versioned release") + .version; + let view = selection_view(changelog_release_params(&latest_version)); + + insta::assert_snapshot!( + "changelog_browser_release_bullets", + render_lines(&view, /*width*/ 88) + ); + } + + fn selection_view(params: SelectionViewParams) -> ListSelectionView { + let (tx_raw, _rx) = unbounded_channel::(); + ListSelectionView::new( + params, + AppEventSender::new(tx_raw), + crate::keymap::RuntimeKeymap::defaults().list, + ) + } + + fn render_lines(view: &ListSelectionView, width: u16) -> String { + let area = Rect::new(0, 0, width, view.desired_height(width)); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); - assert!(markdown.starts_with("# Codewith Changelog\n\n## [Unreleased]")); - assert!(markdown.contains("## [0.1.")); - assert!(!markdown.contains("Known evidence gaps")); + (0..area.height) + .map(|y| { + let mut line = String::new(); + for x in 0..area.width { + line.push_str(buf[(x, y)].symbol()); + } + line.trim_end().to_string() + }) + .collect::>() + .join("\n") } } diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 3a84cc7a50..b1ebed4cae 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -1405,7 +1405,7 @@ impl ChatWidget { self.open_usage_panel(); } SlashCommand::Changelog => { - self.add_changelog_output(); + self.open_changelog_browser(); } SlashCommand::Ide => { self.handle_ide_command(); diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_release_bullets.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_release_bullets.snap new file mode 100644 index 0000000000..77901d662d --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_release_bullets.snap @@ -0,0 +1,27 @@ +--- +source: tui/src/chatwidget/changelog.rs +assertion_line: 289 +expression: "render_lines(&view, 88)" +--- + + Codewith [0.1.55] - 2026-07-02 + Bullets copied from CHANGELOG.md. + +› 1. Back Return to all changelog versions. + 2. Close Close the changelog browser. + - Release pipeline: libcap-2.75 musl download now falls back across verified + mirrors (kernel.org -> OSUOSL -> Debian) with mandatory checksum; macOS builds + use CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 and the aarch64 primary runs on macos- + 15-xlarge, ending the 3-hour timeouts that killed 0.1.52-0.1.54. + - Goals state: databases stamped by published 0.1.48 are repaired before + migration, preventing a permanent VersionMismatch(5) that disabled the sqlite + state layer (threads, schedules, mailbox, goals) after upgrade. + - OpenRouter GLM-5.2 metadata restored (parallel tool calls, High/XHigh reasoning + presets, gpt-oss-120b as default fallback). + - Usage-profile broker: scheduled dispatches now defer while all sibling profiles + are cooling down instead of feeding the failure circuit breaker. + - Remote and sandboxed filesystems support symlink-safe reads, so project + instructions (CODEWITH.md/AGENTS.md) load in remote sessions. + - Mailbox: queued-input API restored; delivery defer rolls back on enqueue + rejection; delivery-policy parsing matches the SQL claim filter; queued context + delivery is bounded. diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_versions.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_versions.snap new file mode 100644 index 0000000000..320be3c725 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__changelog__tests__changelog_browser_versions.snap @@ -0,0 +1,17 @@ +--- +source: tui/src/chatwidget/changelog.rs +expression: "render_lines(&view, 72)" +--- + + Codewith Changelog + Select a release to view its changelog bullets. + + Search versions +› Unreleased Open Unreleased release notes. + 0.1.55 2026-07-02 + 0.1.54 2026-07-02 + 0.1.53 2026-07-01 + 0.1.52 2026-07-01 + 0.1.51 2026-06-26 + 0.1.50 2026-06-26 + 0.1.49 2026-06-26 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_changelog_release_notes_preview.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_changelog_release_notes_preview.snap deleted file mode 100644 index 47e8498295..0000000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_changelog_release_notes_preview.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: tui/src/chatwidget/tests/slash_commands.rs -expression: preview ---- -• # Codewith Changelog - - ## [Unreleased] - - ### Fixed - - - Recovered loop-driven and long-running turns from context-window overflow - errors by compacting mid-turn and retrying once before surfacing the - failure. - - ## [0.1.54] - 2026-07-02 diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 6e61b1ae44..c7aa672aa7 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -3468,33 +3468,55 @@ async fn slash_exit_requests_exit() { } #[tokio::test] -async fn slash_changelog_prints_release_notes() { +async fn slash_changelog_opens_browser_without_submitting_prompt() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.bottom_pane.set_task_running(/*running*/ true); + + chat.bottom_pane + .set_composer_text("/changelog".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert_eq!(chat.bottom_pane.active_view_id(), Some("changelog-browser")); + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + assert!(chat.bottom_pane.is_task_running()); + + chat.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert_eq!(chat.bottom_pane.active_view_id(), None); + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + assert!(chat.bottom_pane.is_task_running()); +} +#[tokio::test] +async fn slash_changelog_drills_back_and_closes_without_core_ops() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.bottom_pane.set_task_running(/*running*/ true); chat.dispatch_command(SlashCommand::Changelog); - let cells = drain_insert_history(&mut rx); - assert_eq!(cells.len(), 1, "expected one changelog history cell"); - let rendered = lines_to_single_string(&cells[0]); - let preview = lines_to_single_string(&cells[0][..cells[0].len().min(12)]) - .lines() - .map(str::trim_end) - .collect::>() - .join("\n"); - assert_chatwidget_snapshot!("slash_changelog_release_notes_preview", preview); - assert!( - rendered.contains("Codewith Changelog"), - "expected changelog title, got {rendered:?}" - ); - assert!( - rendered.contains("Unreleased"), - "expected unreleased section, got {rendered:?}" - ); - assert!( - !rendered.contains("Known evidence gaps"), - "expected repository notes to be omitted, got {rendered:?}" - ); - assert!(op_rx.try_recv().is_err(), "expected no core op to be sent"); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + let version = match rx.try_recv() { + Ok(AppEvent::OpenChangelogRelease { version }) => version, + other => panic!("expected changelog release event, got {other:?}"), + }; + chat.open_changelog_release(version); + assert_eq!(chat.bottom_pane.active_view_id(), Some("changelog-browser")); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_matches!(rx.try_recv(), Ok(AppEvent::OpenChangelogBrowser)); + chat.open_changelog_browser(); + assert_eq!(chat.bottom_pane.active_view_id(), Some("changelog-browser")); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + + chat.open_changelog_release("0.1.55".to_string()); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(chat.bottom_pane.active_view_id(), None); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + assert!(chat.bottom_pane.is_task_running()); } #[tokio::test] From 695322c259a035046b85ca1f2d1d49848a9061bf Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Tue, 7 Jul 2026 12:25:37 +0300 Subject: [PATCH 2/2] test(tui): stabilize changelog browser snapshots --- codex-rs/tui/src/chatwidget/changelog.rs | 110 +++++++++++++++++++---- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/changelog.rs b/codex-rs/tui/src/chatwidget/changelog.rs index 3cb7734843..bbe0fd6139 100644 --- a/codex-rs/tui/src/chatwidget/changelog.rs +++ b/codex-rs/tui/src/chatwidget/changelog.rs @@ -37,7 +37,10 @@ struct ChangelogRelease { } fn changelog_browser_params() -> SelectionViewParams { - let releases = changelog_releases(); + changelog_browser_params_for(changelog_releases()) +} + +fn changelog_browser_params_for(releases: Vec) -> SelectionViewParams { let mut header = ColumnRenderable::new(); header.push(Line::from("Codewith Changelog".bold())); header.push(Line::from( @@ -90,7 +93,14 @@ fn changelog_browser_params() -> SelectionViewParams { } fn changelog_release_params(version: &str) -> SelectionViewParams { - let release = changelog_releases() + changelog_release_params_for(version, changelog_releases()) +} + +fn changelog_release_params_for( + version: &str, + releases: Vec, +) -> SelectionViewParams { + let release = releases .into_iter() .find(|release| release.version == version); let mut header = ColumnRenderable::new(); @@ -161,8 +171,12 @@ fn changelog_release_params(version: &str) -> SelectionViewParams { } fn changelog_releases() -> Vec { + changelog_releases_from_source(CHANGELOG_SOURCE) +} + +fn changelog_releases_from_source(source: &str) -> Vec { let mut releases = Vec::::new(); - for line in release_notes_source().split_inclusive('\n') { + for line in release_notes_source(source).split_inclusive('\n') { if let Some((version, date)) = parse_release_heading(line) { releases.push(ChangelogRelease { version, @@ -184,11 +198,11 @@ fn changelog_releases() -> Vec { releases } -fn release_notes_source() -> &'static str { - CHANGELOG_SOURCE +fn release_notes_source(source: &str) -> &str { + source .find(RELEASE_SECTION_START) - .map(|idx| &CHANGELOG_SOURCE[idx..]) - .unwrap_or(CHANGELOG_SOURCE) + .map(|idx| &source[idx..]) + .unwrap_or(source) .trim() } @@ -230,6 +244,73 @@ mod tests { use ratatui::layout::Rect; use tokio::sync::mpsc::unbounded_channel; + const TEST_CHANGELOG_SOURCE: &str = r#" +# Changelog + +Known evidence gaps: + +- Known evidence gaps should not be listed as release bullets. + +## [Unreleased] + +## [0.1.55] - 2026-07-02 + +### Fixed + +- Release pipeline: libcap-2.75 musl download now falls back across verified + mirrors (kernel.org -> OSUOSL -> Debian) with mandatory checksum; macOS builds + use CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 and the aarch64 primary runs on + macos-15-xlarge, ending the 3-hour timeouts that killed 0.1.52-0.1.54. +- Goals state: databases stamped by published 0.1.48 are repaired before + migration, preventing a permanent VersionMismatch(5) that disabled the sqlite + state layer (threads, schedules, mailbox, goals) after upgrade. +- OpenRouter GLM-5.2 metadata restored (parallel tool calls, High/XHigh + reasoning presets, gpt-oss-120b as default fallback). +- Usage-profile broker: scheduled dispatches now defer while all sibling + profiles are cooling down instead of feeding the failure circuit breaker. +- Remote and sandboxed filesystems support symlink-safe reads, so project + instructions (CODEWITH.md/AGENTS.md) load in remote sessions. +- Mailbox: queued-input API restored; delivery defer rolls back on enqueue + rejection; delivery-policy parsing matches the SQL claim filter; queued + context delivery is bounded. + +## [0.1.54] - 2026-07-02 + +- Placeholder 0.1.54 release note. + +## [0.1.53] - 2026-07-01 + +- Placeholder 0.1.53 release note. + +## [0.1.52] - 2026-07-01 + +- Placeholder 0.1.52 release note. + +## [0.1.51] - 2026-06-26 + +- Placeholder 0.1.51 release note. + +## [0.1.50] - 2026-06-26 + +- Placeholder 0.1.50 release note. + +## [0.1.49] - 2026-06-26 + +- Placeholder 0.1.49 release note. + +## [0.1.0] - 2026-05-21 + +- Initial fork release. + +## Maintenance Process + +- Determine published npm versions. +"#; + + fn fixture_releases() -> Vec { + changelog_releases_from_source(TEST_CHANGELOG_SOURCE) + } + #[test] fn changelog_releases_use_release_sections_without_repository_notes() { let releases = changelog_releases(); @@ -253,10 +334,10 @@ mod tests { #[test] fn changelog_release_parser_collects_real_bullets() { - let releases = changelog_releases(); + let releases = fixture_releases(); let latest_release = releases .iter() - .find(|release| release.version.starts_with("0.1.")) + .find(|release| release.version == "0.1.55") .expect("versioned release"); assert!(!latest_release.bullets.is_empty()); @@ -271,7 +352,7 @@ mod tests { #[test] fn changelog_release_parser_stops_before_maintenance_process() { - let releases = changelog_releases(); + let releases = fixture_releases(); let initial_release = releases .iter() .find(|release| release.version == "0.1.0") @@ -287,7 +368,7 @@ mod tests { #[test] fn changelog_browser_versions_snapshot() { - let view = selection_view(changelog_browser_params()); + let view = selection_view(changelog_browser_params_for(fixture_releases())); insta::assert_snapshot!( "changelog_browser_versions", @@ -297,12 +378,7 @@ mod tests { #[test] fn changelog_browser_release_snapshot() { - let latest_version = changelog_releases() - .into_iter() - .find(|release| release.version.starts_with("0.1.")) - .expect("versioned release") - .version; - let view = selection_view(changelog_release_params(&latest_version)); + let view = selection_view(changelog_release_params_for("0.1.55", fixture_releases())); insta::assert_snapshot!( "changelog_browser_release_bullets",