feat(ui): add configurable scrollback lines and new‑message alert badge
Introduce `ui.scrollback_lines` (default 2000) to cap the number of chat lines kept in memory, with `0` disabling trimming. Implement automatic trimming of older lines, maintain a scroll offset, and show a “↓ New messages (press G)” badge when new messages arrive off‑screen. Update core UI settings, TUI rendering, chat app state, migrations, documentation, and changelog to reflect the new feature.
This commit is contained in:
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Footer status line includes provider connectivity/credential summaries (e.g., cloud auth failures, missing API keys).
|
- Footer status line includes provider connectivity/credential summaries (e.g., cloud auth failures, missing API keys).
|
||||||
- Secure credential vault integration for Ollama Cloud API keys when `privacy.encrypt_local_data = true`.
|
- Secure credential vault integration for Ollama Cloud API keys when `privacy.encrypt_local_data = true`.
|
||||||
- Input panel respects a new `ui.input_max_rows` setting so long prompts expand predictably before scrolling kicks in.
|
- Input panel respects a new `ui.input_max_rows` setting so long prompts expand predictably before scrolling kicks in.
|
||||||
|
- Chat history honors `ui.scrollback_lines`, trimming older rows to keep the TUI responsive and surfacing a "↓ New messages" badge whenever updates land off-screen.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The main `README.md` has been updated to be more concise and link to the new documentation.
|
- The main `README.md` has been updated to be more concise and link to the new documentation.
|
||||||
|
|||||||
@@ -708,6 +708,8 @@ pub struct UiSettings {
|
|||||||
pub show_onboarding: bool,
|
pub show_onboarding: bool,
|
||||||
#[serde(default = "UiSettings::default_input_max_rows")]
|
#[serde(default = "UiSettings::default_input_max_rows")]
|
||||||
pub input_max_rows: u16,
|
pub input_max_rows: u16,
|
||||||
|
#[serde(default = "UiSettings::default_scrollback_lines")]
|
||||||
|
pub scrollback_lines: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiSettings {
|
impl UiSettings {
|
||||||
@@ -738,6 +740,10 @@ impl UiSettings {
|
|||||||
const fn default_input_max_rows() -> u16 {
|
const fn default_input_max_rows() -> u16 {
|
||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn default_scrollback_lines() -> usize {
|
||||||
|
2000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiSettings {
|
impl Default for UiSettings {
|
||||||
@@ -750,6 +756,7 @@ impl Default for UiSettings {
|
|||||||
wrap_column: Self::default_wrap_column(),
|
wrap_column: Self::default_wrap_column(),
|
||||||
show_onboarding: Self::default_show_onboarding(),
|
show_onboarding: Self::default_show_onboarding(),
|
||||||
input_max_rows: Self::default_input_max_rows(),
|
input_max_rows: Self::default_input_max_rows(),
|
||||||
|
scrollback_lines: Self::default_scrollback_lines(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ pub struct ChatApp {
|
|||||||
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
||||||
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
||||||
chat_cursor: (usize, usize), // Cursor position in Chat panel (row, col)
|
chat_cursor: (usize, usize), // Cursor position in Chat panel (row, col)
|
||||||
|
chat_line_offset: usize, // Number of leading lines trimmed for scrollback
|
||||||
thinking_cursor: (usize, usize), // Cursor position in Thinking panel (row, col)
|
thinking_cursor: (usize, usize), // Cursor position in Thinking panel (row, col)
|
||||||
code_view_path: Option<String>, // Active code view file path
|
code_view_path: Option<String>, // Active code view file path
|
||||||
code_view_lines: Vec<String>, // Cached lines for code view rendering
|
code_view_lines: Vec<String>, // Cached lines for code view rendering
|
||||||
@@ -194,6 +195,8 @@ pub struct ChatApp {
|
|||||||
agent_running: bool,
|
agent_running: bool,
|
||||||
/// Operating mode (Chat or Code)
|
/// Operating mode (Chat or Code)
|
||||||
operating_mode: owlen_core::mode::Mode,
|
operating_mode: owlen_core::mode::Mode,
|
||||||
|
/// Flag indicating new messages arrived while scrolled away from tail
|
||||||
|
new_message_alert: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -266,6 +269,7 @@ impl ChatApp {
|
|||||||
visual_end: None,
|
visual_end: None,
|
||||||
focused_panel: FocusedPanel::Input,
|
focused_panel: FocusedPanel::Input,
|
||||||
chat_cursor: (0, 0),
|
chat_cursor: (0, 0),
|
||||||
|
chat_line_offset: 0,
|
||||||
thinking_cursor: (0, 0),
|
thinking_cursor: (0, 0),
|
||||||
code_view_path: None,
|
code_view_path: None,
|
||||||
code_view_lines: Vec::new(),
|
code_view_lines: Vec::new(),
|
||||||
@@ -287,6 +291,7 @@ impl ChatApp {
|
|||||||
agent_mode: false,
|
agent_mode: false,
|
||||||
agent_running: false,
|
agent_running: false,
|
||||||
operating_mode: owlen_core::mode::Mode::default(),
|
operating_mode: owlen_core::mode::Mode::default(),
|
||||||
|
new_message_alert: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if show_onboarding {
|
if show_onboarding {
|
||||||
@@ -684,6 +689,80 @@ impl ChatApp {
|
|||||||
config.ui.input_max_rows.max(1)
|
config.ui.input_max_rows.max(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scrollback_limit(&self) -> usize {
|
||||||
|
let limit = {
|
||||||
|
let config = self.controller.config();
|
||||||
|
config.ui.scrollback_lines
|
||||||
|
};
|
||||||
|
if limit == 0 { usize::MAX } else { limit }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_new_message_alert(&self) -> bool {
|
||||||
|
self.new_message_alert
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_new_message_alert(&mut self) {
|
||||||
|
self.new_message_alert = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_new_activity(&mut self) {
|
||||||
|
if !self.auto_scroll.stick_to_bottom {
|
||||||
|
self.new_message_alert = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_new_message_alert_after_scroll(&mut self) {
|
||||||
|
if self.auto_scroll.stick_to_bottom {
|
||||||
|
self.clear_new_message_alert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_chat_scrollback_trim(&mut self, removed: usize, remaining: usize) {
|
||||||
|
if removed == 0 {
|
||||||
|
self.chat_line_offset = 0;
|
||||||
|
self.chat_cursor.0 = self.chat_cursor.0.min(remaining.saturating_sub(1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chat_line_offset = removed;
|
||||||
|
self.auto_scroll.scroll = self.auto_scroll.scroll.saturating_sub(removed);
|
||||||
|
self.auto_scroll.content_len = remaining;
|
||||||
|
|
||||||
|
if let Some((row, _)) = &mut self.visual_start {
|
||||||
|
if *row < removed {
|
||||||
|
self.visual_start = None;
|
||||||
|
} else {
|
||||||
|
*row -= removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((row, _)) = &mut self.visual_end {
|
||||||
|
if *row < removed {
|
||||||
|
self.visual_end = None;
|
||||||
|
} else {
|
||||||
|
*row -= removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.chat_cursor.0 = self.chat_cursor.0.saturating_sub(removed);
|
||||||
|
if remaining == 0 {
|
||||||
|
self.chat_cursor = (0, 0);
|
||||||
|
} else if self.chat_cursor.0 >= remaining {
|
||||||
|
self.chat_cursor.0 = remaining - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_scroll = remaining.saturating_sub(self.viewport_height);
|
||||||
|
if self.auto_scroll.scroll > max_scroll {
|
||||||
|
self.auto_scroll.scroll = max_scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.auto_scroll.stick_to_bottom {
|
||||||
|
self.auto_scroll.on_viewport(self.viewport_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_theme(&mut self, theme: Theme) {
|
pub fn set_theme(&mut self, theme: Theme) {
|
||||||
self.theme = theme;
|
self.theme = theme;
|
||||||
}
|
}
|
||||||
@@ -1321,8 +1400,7 @@ impl ChatApp {
|
|||||||
FocusedPanel::Code => {}
|
FocusedPanel::Code => {}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
(KeyCode::Char('$'), KeyModifiers::NONE)
|
(KeyCode::Char('$'), KeyModifiers::NONE) => match self.focused_panel {
|
||||||
| (KeyCode::End, KeyModifiers::NONE) => match self.focused_panel {
|
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
if let Some(line) = self.get_line_at_row(self.chat_cursor.0) {
|
||||||
self.chat_cursor.1 = line.chars().count();
|
self.chat_cursor.1 = line.chars().count();
|
||||||
@@ -1337,6 +1415,22 @@ impl ChatApp {
|
|||||||
FocusedPanel::Code => {}
|
FocusedPanel::Code => {}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
(KeyCode::End, KeyModifiers::NONE) => match self.focused_panel {
|
||||||
|
FocusedPanel::Chat => {
|
||||||
|
self.jump_to_bottom();
|
||||||
|
}
|
||||||
|
FocusedPanel::Thinking => {
|
||||||
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
|
self.thinking_scroll.jump_to_bottom(viewport_height);
|
||||||
|
}
|
||||||
|
FocusedPanel::Code => {
|
||||||
|
if self.code_view_path.is_some() {
|
||||||
|
let viewport = self.code_view_viewport_height.max(1);
|
||||||
|
self.code_view_scroll.jump_to_bottom(viewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FocusedPanel::Input => {}
|
||||||
|
},
|
||||||
// Half-page scrolling
|
// Half-page scrolling
|
||||||
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
|
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
|
||||||
self.scroll_half_page_down();
|
self.scroll_half_page_down();
|
||||||
@@ -1752,6 +1846,9 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
"c" | "clear" => {
|
"c" | "clear" => {
|
||||||
self.controller.clear();
|
self.controller.clear();
|
||||||
|
self.chat_line_offset = 0;
|
||||||
|
self.auto_scroll = AutoScroll::default();
|
||||||
|
self.clear_new_message_alert();
|
||||||
self.status = "Conversation cleared".to_string();
|
self.status = "Conversation cleared".to_string();
|
||||||
}
|
}
|
||||||
"w" | "write" | "save" => {
|
"w" | "write" | "save" => {
|
||||||
@@ -2603,6 +2700,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
// Ensure we have a valid viewport height
|
// Ensure we have a valid viewport height
|
||||||
@@ -2626,6 +2724,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.scroll_half_page_down(self.viewport_height);
|
self.auto_scroll.scroll_half_page_down(self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
let viewport_height = self.thinking_viewport_height.max(1);
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
@@ -2646,6 +2745,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.scroll_half_page_up(self.viewport_height);
|
self.auto_scroll.scroll_half_page_up(self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
let viewport_height = self.thinking_viewport_height.max(1);
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
@@ -2666,6 +2766,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.scroll_full_page_down(self.viewport_height);
|
self.auto_scroll.scroll_full_page_down(self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
let viewport_height = self.thinking_viewport_height.max(1);
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
@@ -2686,6 +2787,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.scroll_full_page_up(self.viewport_height);
|
self.auto_scroll.scroll_full_page_up(self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
let viewport_height = self.thinking_viewport_height.max(1);
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
@@ -2706,6 +2808,7 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.jump_to_top();
|
self.auto_scroll.jump_to_top();
|
||||||
|
self.chat_cursor = (0, 0);
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
self.thinking_scroll.jump_to_top();
|
self.thinking_scroll.jump_to_top();
|
||||||
@@ -2724,6 +2827,18 @@ impl ChatApp {
|
|||||||
match self.focused_panel {
|
match self.focused_panel {
|
||||||
FocusedPanel::Chat => {
|
FocusedPanel::Chat => {
|
||||||
self.auto_scroll.jump_to_bottom(self.viewport_height);
|
self.auto_scroll.jump_to_bottom(self.viewport_height);
|
||||||
|
self.update_new_message_alert_after_scroll();
|
||||||
|
let rendered = self.get_rendered_lines();
|
||||||
|
if rendered.is_empty() {
|
||||||
|
self.chat_cursor = (0, 0);
|
||||||
|
} else {
|
||||||
|
let last_index = rendered.len().saturating_sub(1);
|
||||||
|
let last_col = rendered
|
||||||
|
.last()
|
||||||
|
.map(|line| line.chars().count())
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.chat_cursor = (last_index, last_col);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
FocusedPanel::Thinking => {
|
FocusedPanel::Thinking => {
|
||||||
let viewport_height = self.thinking_viewport_height.max(1);
|
let viewport_height = self.thinking_viewport_height.max(1);
|
||||||
@@ -2749,6 +2864,7 @@ impl ChatApp {
|
|||||||
|
|
||||||
// Update thinking content in real-time during streaming
|
// Update thinking content in real-time during streaming
|
||||||
self.update_thinking_from_last_message();
|
self.update_thinking_from_last_message();
|
||||||
|
self.notify_new_activity();
|
||||||
|
|
||||||
// Auto-scroll will handle this in the render loop
|
// Auto-scroll will handle this in the render loop
|
||||||
if response.is_final {
|
if response.is_final {
|
||||||
@@ -2815,6 +2931,7 @@ impl ChatApp {
|
|||||||
self.controller
|
self.controller
|
||||||
.conversation_mut()
|
.conversation_mut()
|
||||||
.push_assistant_message(answer);
|
.push_assistant_message(answer);
|
||||||
|
self.notify_new_activity();
|
||||||
self.agent_running = false;
|
self.agent_running = false;
|
||||||
self.agent_mode = false;
|
self.agent_mode = false;
|
||||||
self.agent_actions = None;
|
self.agent_actions = None;
|
||||||
@@ -3889,6 +4006,11 @@ impl ChatApp {
|
|||||||
lines.push(format!("🤖 Assistant: {}", self.get_loading_indicator()));
|
lines.push(format!("🤖 Assistant: {}", self.get_loading_indicator()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.chat_line_offset > 0 {
|
||||||
|
let skip = self.chat_line_offset.min(lines.len());
|
||||||
|
lines = lines.into_iter().skip(skip).collect();
|
||||||
|
}
|
||||||
|
|
||||||
if lines.is_empty() {
|
if lines.is_empty() {
|
||||||
lines.push("No messages yet. Press 'i' to start typing.".to_string());
|
lines.push("No messages yet. Press 'i' to start typing.".to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -826,6 +826,15 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
lines.push(Line::from("No messages yet. Press 'i' to start typing."));
|
lines.push(Line::from("No messages yet. Press 'i' to start typing."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scrollback_limit = app.scrollback_limit();
|
||||||
|
if scrollback_limit != usize::MAX && lines.len() > scrollback_limit {
|
||||||
|
let removed = lines.len() - scrollback_limit;
|
||||||
|
lines = lines.into_iter().skip(removed).collect();
|
||||||
|
app.apply_chat_scrollback_trim(removed, lines.len());
|
||||||
|
} else {
|
||||||
|
app.apply_chat_scrollback_trim(0, lines.len());
|
||||||
|
}
|
||||||
|
|
||||||
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
// Apply visual selection highlighting if in visual mode and Chat panel is focused
|
||||||
if matches!(app.mode(), InputMode::Visual)
|
if matches!(app.mode(), InputMode::Visual)
|
||||||
&& matches!(app.focused_panel(), FocusedPanel::Chat)
|
&& matches!(app.focused_panel(), FocusedPanel::Chat)
|
||||||
@@ -860,6 +869,31 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
|
|
||||||
frame.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
|
|
||||||
|
if app.has_new_message_alert() {
|
||||||
|
let badge_text = "↓ New messages (press G)";
|
||||||
|
let text_width = badge_text.chars().count() as u16;
|
||||||
|
let badge_width = text_width.saturating_add(2);
|
||||||
|
if area.width > badge_width + 1 && area.height > 2 {
|
||||||
|
let badge_x = area.x + area.width.saturating_sub(badge_width + 1);
|
||||||
|
let badge_y = area.y + 1;
|
||||||
|
let badge_area = Rect::new(badge_x, badge_y, badge_width, 1);
|
||||||
|
frame.render_widget(Clear, badge_area);
|
||||||
|
let badge_line = Line::from(Span::styled(
|
||||||
|
format!(" {badge_text} "),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.background)
|
||||||
|
.bg(theme.info)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
));
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(badge_line)
|
||||||
|
.style(Style::default().bg(theme.info).fg(theme.background))
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
badge_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render cursor if Chat panel is focused and in Normal mode
|
// Render cursor if Chat panel is focused and in Normal mode
|
||||||
if matches!(app.focused_panel(), FocusedPanel::Chat) && matches!(app.mode(), InputMode::Normal)
|
if matches!(app.focused_panel(), FocusedPanel::Chat) && matches!(app.mode(), InputMode::Normal)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ These settings customize the look and feel of the terminal interface.
|
|||||||
- `input_max_rows` (integer, default: `5`)
|
- `input_max_rows` (integer, default: `5`)
|
||||||
The maximum number of rows the input panel will expand to before it starts scrolling internally. Increase this value if you prefer to see more of long prompts while editing.
|
The maximum number of rows the input panel will expand to before it starts scrolling internally. Increase this value if you prefer to see more of long prompts while editing.
|
||||||
|
|
||||||
|
- `scrollback_lines` (integer, default: `2000`)
|
||||||
|
The maximum number of rendered lines the chat view keeps in memory. Set to `0` to disable trimming entirely if you prefer unlimited history.
|
||||||
|
|
||||||
## Storage Settings (`[storage]`)
|
## Storage Settings (`[storage]`)
|
||||||
|
|
||||||
These settings control how conversations are saved and loaded.
|
These settings control how conversations are saved and loaded.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Owlen is still in alpha, so configuration and storage formats may change between
|
|||||||
|
|
||||||
### Schema 1.2.0 (November 2025)
|
### Schema 1.2.0 (November 2025)
|
||||||
|
|
||||||
`config.toml` now records `schema_version = "1.2.0"` and introduces the optional `ui.input_max_rows` setting. The new key defaults to `5`, so no manual edits are required unless you prefer a taller input panel. Existing files are updated automatically on load/save.
|
`config.toml` now records `schema_version = "1.2.0"` and introduces the optional `ui.input_max_rows` and `ui.scrollback_lines` settings. The new keys default to `5` and `2000` respectively, so no manual edits are required unless you want a taller input panel or a different scrollback cap. Existing files are updated automatically on load/save.
|
||||||
|
|
||||||
### Schema 1.1.0 (October 2025)
|
### Schema 1.1.0 (October 2025)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user