mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
feat(tui)!: two transcript spacings, with spacious as the default
`comfortable` and `compact` rendered identically in a conversation: the separator matrix gave `Compact | Comfortable` one arm at turn boundaries, and only differed at Activity boundaries (0 vs 1 row). A setting that renders the same as its neighbour is a lie in the settings list, so the ladder is now two real levels. - `compact`: 0 rows at activity seams, 1 at turn seams (unchanged). - `spacious`: 2 at activity seams, 3 at turn seams. Previously 1 and 2, so this is roughly 1.6x the air it had. `spacious` becomes the default. An existing `comfortable` (or `default`/ `normal`) value normalizes to `spacious`, and the schema declares only the two live values so the picker cannot offer a setting that does nothing. The transcript tests are updated from the 3-way matrix to assert both ladders and to prove compact never spends a double blank. `history.rs` and the widget test fixture move from Comfortable to Spacious. NOT VERIFIED: no compile or test run. The workspace build lock was held by another session for this entire slice (their `cargo check`/`cargo test` cycles), and one native build per machine is the rule. Verified by inspection only: the match in `spacer_rows_for_boundary` covers 4 boundaries x 2 spacings exhaustively, and `settings_schema`'s `enum_values_are_unique_and_defaults_are_declared_values` still holds because `spacious` is a declared value.
This commit is contained in:
1 parent
eb8271e20a
commit
39621ffb5f
8 files changed
+66
-85
No files matched your search
@@ -346,6 +346,11 @@ const DENSITY: &[SettingOption] = &[
|
||||
SettingOption::new("spacious", "", ""),
|
||||
];
|
||||
|
||||
const TRANSCRIPT_SPACING: &[SettingOption] = &[
|
||||
SettingOption::new("compact", "", ""),
|
||||
SettingOption::new("spacious", "", ""),
|
||||
];
|
||||
|
||||
const TOOL_COLLAPSE: &[SettingOption] = &[
|
||||
SettingOption::new("compact", "", ""),
|
||||
SettingOption::new("expanded", "", ""),
|
||||
@@ -687,8 +692,8 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
),
|
||||
def(
|
||||
"transcript_spacing",
|
||||
SettingKind::Enum(DENSITY),
|
||||
"comfortable",
|
||||
SettingKind::Enum(TRANSCRIPT_SPACING),
|
||||
"spacious",
|
||||
ui(
|
||||
TAB_APPEARANCE,
|
||||
"display",
|
||||
|
||||
@@ -199,7 +199,6 @@ pub(crate) fn show_single_setting(app: &App, key: &str) -> ConfigReply {
|
||||
fn spacing_display(s: crate::tui::app::TranscriptSpacing) -> &'static str {
|
||||
match s {
|
||||
crate::tui::app::TranscriptSpacing::Compact => "compact",
|
||||
crate::tui::app::TranscriptSpacing::Comfortable => "comfortable",
|
||||
crate::tui::app::TranscriptSpacing::Spacious => "spacious",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,7 +585,7 @@ impl Default for Settings {
|
||||
composer_border: true,
|
||||
composer_multiline_mode: false,
|
||||
composer_vim_mode: "normal".to_string(),
|
||||
transcript_spacing: "comfortable".to_string(),
|
||||
transcript_spacing: "spacious".to_string(),
|
||||
default_mode: "agent".to_string(),
|
||||
sidebar_width_percent: 28,
|
||||
sidebar_focus: "auto".to_string(),
|
||||
@@ -2885,7 +2885,7 @@ fn normalize_composer_density(value: &str) -> &str {
|
||||
fn normalize_transcript_spacing(value: &str) -> &str {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"compact" | "tight" => "compact",
|
||||
"comfortable" | "default" | "normal" => "comfortable",
|
||||
"comfortable" | "default" | "normal" => "spacious",
|
||||
"spacious" | "loose" => "spacious",
|
||||
_ => value,
|
||||
}
|
||||
@@ -3602,7 +3602,7 @@ mod tests {
|
||||
load_error: None,
|
||||
fancy_animations: true,
|
||||
show_tool_details: true,
|
||||
transcript_spacing: "comfortable".to_string(),
|
||||
transcript_spacing: "spacious".to_string(),
|
||||
..Settings::default()
|
||||
}
|
||||
}
|
||||
@@ -3634,13 +3634,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_settings_use_comfortable_transcript_spacing() {
|
||||
fn default_settings_use_spacious_transcript_spacing() {
|
||||
let settings = Settings::default();
|
||||
assert!(settings.calm_mode);
|
||||
assert!(!settings.show_tool_details);
|
||||
assert!(!settings.low_motion);
|
||||
assert!(settings.fancy_animations);
|
||||
assert_eq!(settings.transcript_spacing, "comfortable");
|
||||
assert_eq!(settings.transcript_spacing, "spacious");
|
||||
assert_eq!(settings.tool_collapse_mode, "compact");
|
||||
// Thinking is opt-in so the transcript stays focused on the chat.
|
||||
assert!(!settings.show_thinking);
|
||||
@@ -3859,7 +3859,7 @@ mod tests {
|
||||
"underwater presentation is the default"
|
||||
);
|
||||
assert!(!settings.low_motion);
|
||||
assert_eq!(settings.transcript_spacing, "comfortable");
|
||||
assert_eq!(settings.transcript_spacing, "spacious");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -120,7 +120,6 @@ impl ComposerDensity {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TranscriptSpacing {
|
||||
Compact,
|
||||
Comfortable,
|
||||
Spacious,
|
||||
}
|
||||
|
||||
@@ -129,8 +128,7 @@ impl TranscriptSpacing {
|
||||
pub fn from_setting(value: &str) -> Self {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"compact" | "tight" => Self::Compact,
|
||||
"spacious" | "loose" => Self::Spacious,
|
||||
_ => Self::Comfortable,
|
||||
_ => Self::Spacious,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ impl Default for TranscriptRenderOptions {
|
||||
calm_mode: false,
|
||||
low_motion: false,
|
||||
motion_mode: MotionMode::Full,
|
||||
spacing: TranscriptSpacing::Comfortable,
|
||||
spacing: TranscriptSpacing::Spacious,
|
||||
palette_mode: palette::PaletteMode::detect(),
|
||||
prose_measure: None,
|
||||
}
|
||||
|
||||
@@ -1155,11 +1155,9 @@ const fn spacer_rows_for_boundary(
|
||||
match (boundary, spacing) {
|
||||
(TranscriptBoundary::Joined | TranscriptBoundary::GroupedTool, _) => 0,
|
||||
(TranscriptBoundary::Activity, TranscriptSpacing::Compact) => 0,
|
||||
(TranscriptBoundary::Activity, _) => BLOCK_SEPARATOR_ROWS,
|
||||
(TranscriptBoundary::Turn, TranscriptSpacing::Compact | TranscriptSpacing::Comfortable) => {
|
||||
BLOCK_SEPARATOR_ROWS
|
||||
}
|
||||
(TranscriptBoundary::Turn, TranscriptSpacing::Spacious) => BLOCK_SEPARATOR_ROWS + 1,
|
||||
(TranscriptBoundary::Activity, TranscriptSpacing::Spacious) => BLOCK_SEPARATOR_ROWS + 1,
|
||||
(TranscriptBoundary::Turn, TranscriptSpacing::Compact) => BLOCK_SEPARATOR_ROWS,
|
||||
(TranscriptBoundary::Turn, TranscriptSpacing::Spacious) => BLOCK_SEPARATOR_ROWS + 2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -764,36 +764,23 @@ fn semantic_boundary_matrix_has_four_deliberate_rhythm_levels() {
|
||||
spacer_rows_for_boundary(Turn, TranscriptSpacing::Compact),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(Turn, TranscriptSpacing::Comfortable),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(Turn, TranscriptSpacing::Spacious),
|
||||
2
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(Activity, TranscriptSpacing::Compact),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(Activity, TranscriptSpacing::Comfortable),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(Activity, TranscriptSpacing::Spacious),
|
||||
1
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(GroupedTool, TranscriptSpacing::Compact),
|
||||
0,
|
||||
"compact density buys its density by spending no separator rows"
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(GroupedTool, TranscriptSpacing::Comfortable),
|
||||
0,
|
||||
"the shared rail carries grouping without a row per tool call"
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_for_boundary(GroupedTool, TranscriptSpacing::Spacious),
|
||||
0,
|
||||
@@ -801,29 +788,18 @@ fn semantic_boundary_matrix_has_four_deliberate_rhythm_levels() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Separation is one row or none. Nothing in the matrix may produce a
|
||||
/// double blank, because a scrolling terminal cannot afford it.
|
||||
/// Compact never spends a double blank, because a scrolling terminal cannot
|
||||
/// afford it. Spacious buys its air at turn and activity seams by design.
|
||||
#[test]
|
||||
fn no_boundary_ever_spends_more_than_one_row_below_spacious_turns() {
|
||||
fn compact_never_spends_more_than_one_row_at_any_boundary() {
|
||||
use TranscriptBoundary::{Activity, GroupedTool, Joined, Turn};
|
||||
|
||||
for boundary in [Joined, GroupedTool, Activity, Turn] {
|
||||
for spacing in [
|
||||
TranscriptSpacing::Compact,
|
||||
TranscriptSpacing::Comfortable,
|
||||
TranscriptSpacing::Spacious,
|
||||
] {
|
||||
let rows = spacer_rows_for_boundary(boundary, spacing);
|
||||
let allowed = if boundary == Turn && spacing == TranscriptSpacing::Spacious {
|
||||
2
|
||||
} else {
|
||||
BLOCK_SEPARATOR_ROWS
|
||||
};
|
||||
assert!(
|
||||
rows <= allowed,
|
||||
"{boundary:?} at {spacing:?} spent {rows} rows (max {allowed})"
|
||||
);
|
||||
}
|
||||
let rows = spacer_rows_for_boundary(boundary, TranscriptSpacing::Compact);
|
||||
assert!(
|
||||
rows <= BLOCK_SEPARATOR_ROWS,
|
||||
"{boundary:?} at compact spent {rows} rows"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,26 +884,26 @@ fn durable_work_starts_a_new_activity_rail_without_wasting_compact_rows() {
|
||||
"durable Work should open its own rail: {durable_work:?}"
|
||||
);
|
||||
|
||||
let mut comfortable = TranscriptViewCache::new();
|
||||
comfortable.ensure(
|
||||
let mut spacious = TranscriptViewCache::new();
|
||||
spacious.ensure(
|
||||
&cells,
|
||||
&revisions,
|
||||
80,
|
||||
TranscriptRenderOptions {
|
||||
spacing: TranscriptSpacing::Comfortable,
|
||||
spacing: TranscriptSpacing::Spacious,
|
||||
low_motion: true,
|
||||
..TranscriptRenderOptions::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_after_cell(&comfortable, 0),
|
||||
spacer_rows_after_cell(&spacious, 0),
|
||||
0,
|
||||
"two commands inside one activity rail must remain compact"
|
||||
);
|
||||
assert_eq!(
|
||||
spacer_rows_after_cell(&comfortable, 1),
|
||||
1,
|
||||
"durable Work needs a semantic activity row outside compact density"
|
||||
spacer_rows_after_cell(&spacious, 1),
|
||||
2,
|
||||
"durable Work needs a semantic activity break outside compact density"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1092,30 +1068,38 @@ fn transcript_rhythm_is_width_and_reduced_motion_invariant() {
|
||||
];
|
||||
let revisions = vec![1u64; cells.len()];
|
||||
// user | reasoning | answer | tool | work | answer | user.
|
||||
// Every seam is one row: the reasoning→answer seam (index 1) used to be
|
||||
// the one place the transcript ran two blocks together.
|
||||
let expected = [1, 1, 1, 1, 1, 1, 0];
|
||||
// Turn seams breathe widest; activity seams are the interior of one turn.
|
||||
let expected_compact = [1, 0, 0, 0, 0, 1, 0];
|
||||
let expected_spacious = [3, 2, 2, 2, 2, 3, 0];
|
||||
|
||||
for width in [40, 80, 100, 140] {
|
||||
for low_motion in [false, true] {
|
||||
let options = TranscriptRenderOptions {
|
||||
low_motion,
|
||||
spacing: TranscriptSpacing::Comfortable,
|
||||
..TranscriptRenderOptions::default()
|
||||
};
|
||||
let mut cache = TranscriptViewCache::new();
|
||||
cache.ensure(&cells, &revisions, width, options);
|
||||
for (spacing, expected) in [
|
||||
(TranscriptSpacing::Compact, expected_compact),
|
||||
(TranscriptSpacing::Spacious, expected_spacious),
|
||||
] {
|
||||
let options = TranscriptRenderOptions {
|
||||
low_motion,
|
||||
spacing,
|
||||
..TranscriptRenderOptions::default()
|
||||
};
|
||||
let mut cache = TranscriptViewCache::new();
|
||||
cache.ensure(&cells, &revisions, width, options);
|
||||
|
||||
let actual =
|
||||
std::array::from_fn::<_, 7, _>(|index| spacer_rows_after_cell(&cache, index));
|
||||
assert_eq!(actual, expected, "width={width} low_motion={low_motion}");
|
||||
assert!(
|
||||
cache
|
||||
.lines()
|
||||
.iter()
|
||||
.all(|line| line.width() <= usize::from(width)),
|
||||
"render exceeded width={width} low_motion={low_motion}"
|
||||
);
|
||||
let actual =
|
||||
std::array::from_fn::<_, 7, _>(|index| spacer_rows_after_cell(&cache, index));
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"width={width} low_motion={low_motion} spacing={spacing:?}"
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.lines()
|
||||
.iter()
|
||||
.all(|line| line.width() <= usize::from(width)),
|
||||
"render exceeded width={width} low_motion={low_motion}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2636,11 +2620,8 @@ impl TranscriptModel {
|
||||
"width"
|
||||
}
|
||||
11 => {
|
||||
self.options.spacing = [
|
||||
TranscriptSpacing::Compact,
|
||||
TranscriptSpacing::Comfortable,
|
||||
TranscriptSpacing::Spacious,
|
||||
][rng.below(3)];
|
||||
self.options.spacing =
|
||||
[TranscriptSpacing::Compact, TranscriptSpacing::Spacious][rng.below(2)];
|
||||
self.options.show_thinking = rng.below(4) != 0;
|
||||
"options"
|
||||
}
|
||||
|
||||
@@ -3950,7 +3950,7 @@ mod tests {
|
||||
let mut app = create_test_app();
|
||||
app.low_motion = true;
|
||||
app.fancy_animations = false;
|
||||
app.transcript_spacing = TranscriptSpacing::Comfortable;
|
||||
app.transcript_spacing = TranscriptSpacing::Spacious;
|
||||
|
||||
for turn in 0..4 {
|
||||
app.add_message(HistoryCell::User {
|
||||
|
||||
Reference in new issue
Block a user