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:
CodeWhale Bot committed 2026-10-22 05:16:20 -07:00
1 parent eb8271e20a
commit 39621ffb5f
8 files changed
+66 -85

No files matched your search

+7 -2
View File
@@ -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",
}
}
+6 -6
View File
@@ -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]
+1 -3
View File
@@ -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,
}
}
}
+1 -1
View File
@@ -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,
}
+3 -5
View File
@@ -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,
}
}
+47 -66
View File
@@ -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"
}
+1 -1
View File
@@ -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 {