diff --git a/src/chat/actions.rs b/src/chat/actions.rs index c9a25f5..4b3d5b5 100644 --- a/src/chat/actions.rs +++ b/src/chat/actions.rs @@ -308,7 +308,7 @@ impl RenderOnce for MessageEditor { #[derive(IntoElement)] pub struct BranchNavigator { id: ElementId, - current: usize, + current: Option, total: usize, on_change: Pick, } @@ -320,10 +320,12 @@ impl BranchNavigator { total: usize, on_change: impl Fn(usize, &mut Window, &mut App) + 'static, ) -> Self { - assert!(current < total, "version {current} of {total}"); + if current >= total { + log::error!("branch navigator: version {current} of {total}; none shown"); + } Self { id: id.into(), - current, + current: (current < total).then_some(current), total, on_change: Rc::new(on_change), } @@ -355,7 +357,7 @@ impl RenderOnce for BranchNavigator { "back", IconName::ChevronLeft, "Previous version", - current.checked_sub(1), + current.and_then(|current| current.checked_sub(1)), )) .child( tabular( @@ -363,13 +365,30 @@ impl RenderOnce for BranchNavigator { .text_size(theme.text_size(TextSize::Xs)) .text_color(theme.colors.fg_muted), ) - .child(format!("{} / {total}", current + 1)), + .child(match current { + Some(current) => format!("{} / {total}", current + 1), + None => format!("– / {total}"), + }), ) .child(step( "forward", IconName::ChevronRight, "Next version", - (current + 1 < total).then_some(current + 1), + current.and_then(|current| (current + 1 < total).then_some(current + 1)), )) } } + +#[cfg(test)] +mod tests { + use super::BranchNavigator; + + #[test] + fn a_version_past_the_branches_shows_none() { + let navigator = + |current, total| BranchNavigator::new("branch", current, total, |_, _, _| {}); + assert_eq!(navigator(1, 3).current, Some(1)); + assert_eq!(navigator(3, 3).current, None); + assert_eq!(navigator(0, 0).current, None); + } +} diff --git a/src/chat/params.rs b/src/chat/params.rs index ebb1df0..08cd01a 100644 --- a/src/chat/params.rs +++ b/src/chat/params.rs @@ -58,15 +58,25 @@ impl ParameterPanel { parameters: impl IntoIterator, on_change: impl Fn(&SharedString, f64, &mut Window, &mut App) + 'static, ) -> Self { - let parameters: Vec = parameters.into_iter().collect(); - for parameter in ¶meters { + let mut parameters: Vec = parameters.into_iter().collect(); + for parameter in &mut parameters { assert!( parameter.min < parameter.max && parameter.step > 0.0 - && (parameter.min..=parameter.max).contains(¶meter.value), + && parameter.value.is_finite(), "parameter {} has a bad range", parameter.key ); + if !(parameter.min..=parameter.max).contains(¶meter.value) { + log::error!( + "parameters: {} at {} lies outside {}..={}; pegged", + parameter.key, + parameter.value, + parameter.min, + parameter.max + ); + parameter.value = parameter.value.clamp(parameter.min, parameter.max); + } } Self { id: id.into(), @@ -309,7 +319,7 @@ impl RenderOnce for SystemPromptEditor { #[cfg(test)] mod tests { - use super::{Parameter, cost, places}; + use super::{Parameter, ParameterPanel, cost, places}; #[test] fn cost_counts_tokens_at_their_prices() { @@ -351,4 +361,21 @@ mod tests { "float noise adds no places" ); } + + #[test] + fn a_value_outside_its_range_pegs() { + let knob = |value| Parameter { + key: "temperature".into(), + label: "Temperature".into(), + value, + min: 0.0, + max: 2.0, + step: 0.1, + hint: None, + }; + let panel = + ParameterPanel::new("knobs", [knob(3.5), knob(-1.0), knob(0.7)], |_, _, _, _| {}); + let values: Vec = panel.parameters.iter().map(|knob| knob.value).collect(); + assert_eq!(values, [2.0, 0.0, 0.7]); + } } diff --git a/src/chat/search.rs b/src/chat/search.rs index a5818dd..aab9ff1 100644 --- a/src/chat/search.rs +++ b/src/chat/search.rs @@ -232,13 +232,19 @@ impl DocumentChunkPreview { text: impl Into, matched: Vec>, ) -> Self { - let text = text.into(); - assert!( - matched.iter().all(|range| range.end <= text.len() - && text.is_char_boundary(range.start) - && text.is_char_boundary(range.end)), - "a match lies outside the passage" - ); + let text: SharedString = text.into(); + let matched: Vec> = matched + .into_iter() + .filter(|range| { + let inside = text.get(range.clone()).is_some(); + if !inside { + log::error!( + "document chunk: match {range:?} lies outside the passage; left out" + ); + } + inside + }) + .collect(); assert!( matched.windows(2).all(|pair| pair[0].end <= pair[1].start), "matches come in order without overlapping" @@ -315,3 +321,16 @@ impl RenderOnce for DocumentChunkPreview { ) } } + +#[cfg(test)] +mod tests { + use super::DocumentChunkPreview; + + #[test] + fn a_match_outside_the_passage_is_left_out() { + let ranges = vec![1..2, 2..3, 3..9]; + let preview = DocumentChunkPreview::new("guide.md", "p. 2", "éabc", ranges); + let kept = 2..3; + assert_eq!(preview.matched, [kept]); + } +} diff --git a/tasks/panics.md b/tasks/panics.md index ef6011e..e71502e 100644 --- a/tasks/panics.md +++ b/tasks/panics.md @@ -88,3 +88,4 @@ One line per item: what changed, what stayed and why, review rounds. - P20: a hex selection off the bytes and a stack frame past the stack select none; a flame focus keeps what the profile holds; the timeline's wheel reports only finite, positive ranges from finite factors; a dump's last byte must have an address; flame paints log. Kept: disassembly patterns, `zoomed`'s contract. Review: 3 rounds. - P21: block editor listeners painted for a block, field, cell or kind since gone log and change nothing, a dropped picture drag leaves undo alone; a visual block press or edit over text the owner replaced is dropped; nested images parse; a version or find hit past its list selects none. Review: 4 rounds. - P22: annotations and suggestions outside their text log and stay out, picks and decisions keeping the owner's index; `accept` returns `Option`; a gone pick, active thread or followed peer logs; yours at no count comes off; a masked field clamps a stale offset, so remote cursors and selections draw. Kept: a thread's comments, presence max, one list's order, empty marks, constant role and mode tables. Review: 3 rounds. +- P23: a branch version past its total logs and shows a dash, its arrows off; a parameter outside its range logs and pegs; a passage's matches outside it log and stay out. Kept: one-value contracts (progress, scores, shapes, peaks, levels, limits, prices, capacity, format, hour), the composer's trigger rows and stream reveal by construction; `places` skips zero, so `decimals` meets no zero step. Review: 2 rounds.