fix(tui): clarify sidebar state and settings wiring
This commit is contained in:
@@ -11,7 +11,9 @@ use crate::llm_client::LlmClient;
|
||||
use crate::localization::resolve_locale;
|
||||
use crate::models::{ContentBlock, Message, MessageRequest, MessageResponse, SystemPrompt};
|
||||
use crate::settings::Settings;
|
||||
use crate::tui::app::{App, AppAction, AppMode, OnboardingState, ReasoningEffort, SidebarFocus};
|
||||
use crate::tui::app::{
|
||||
App, AppAction, AppMode, OnboardingState, ReasoningEffort, SidebarFocus, VimMode,
|
||||
};
|
||||
use crate::tui::approval::ApprovalMode;
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -132,21 +134,73 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
|
||||
Some(if app.auto_compact { "true" } else { "false" }.to_string())
|
||||
}
|
||||
"calm_mode" | "calm" => Some(if app.calm_mode { "true" } else { "false" }.to_string()),
|
||||
"low_motion" | "motion" => Some(if app.low_motion { "true" } else { "false" }.to_string()),
|
||||
"fancy_animations" | "fancy" | "animations" => Some(
|
||||
if app.fancy_animations {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"bracketed_paste" | "paste" => Some(
|
||||
if app.use_bracketed_paste {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"paste_burst_detection" | "paste_burst" => Some(
|
||||
if app.use_paste_burst_detection {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"show_thinking" | "thinking" => {
|
||||
Some(if app.show_thinking { "true" } else { "false" }.to_string())
|
||||
}
|
||||
"show_tool_details" | "tool_details" => Some(
|
||||
if app.show_tool_details {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"mode" | "default_mode" => Some(app.mode.as_setting().to_string()),
|
||||
"max_history" | "history" => Some(app.max_input_history.to_string()),
|
||||
"sidebar_width" | "sidebar" => Some(app.sidebar_width_percent.to_string()),
|
||||
"sidebar_focus" | "focus" => Some(app.sidebar_focus.as_setting().to_string()),
|
||||
"context_panel" | "context" | "session_panel" => {
|
||||
Some(if app.context_panel { "true" } else { "false" }.to_string())
|
||||
}
|
||||
"composer_density" | "composer" => Some(density_display(app.composer_density).to_string()),
|
||||
"composer_border" | "border" => {
|
||||
Some(if app.composer_border { "true" } else { "false" }.to_string())
|
||||
}
|
||||
"composer_vim_mode" | "vim_mode" | "vim" => Some(
|
||||
if app.composer.vim_enabled {
|
||||
"vim"
|
||||
} else {
|
||||
"normal"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"transcript_spacing" | "spacing" => {
|
||||
Some(spacing_display(app.transcript_spacing).to_string())
|
||||
}
|
||||
"status_indicator" | "indicator" => Some(app.status_indicator.clone()),
|
||||
"synchronized_output" | "sync_output" | "sync" => Some(
|
||||
if app.synchronized_output_enabled {
|
||||
"on"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"cost_currency" | "currency" => Some(
|
||||
match app.cost_currency {
|
||||
crate::pricing::CostCurrency::Usd => "usd",
|
||||
@@ -154,6 +208,14 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
"default_model" => Settings::load().ok().map(|settings| {
|
||||
settings
|
||||
.default_model
|
||||
.unwrap_or_else(|| "(default)".to_string())
|
||||
}),
|
||||
"prefer_external_pdftotext" | "external_pdftotext" | "pdftotext" => Settings::load()
|
||||
.ok()
|
||||
.map(|settings| settings.prefer_external_pdftotext.to_string()),
|
||||
_ => {
|
||||
let known = Settings::available_settings()
|
||||
.iter()
|
||||
@@ -406,10 +468,22 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
app.low_motion = settings.low_motion;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"fancy_animations" | "fancy" | "animations" => {
|
||||
app.fancy_animations = settings.fancy_animations;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"bracketed_paste" | "paste" => {
|
||||
app.use_bracketed_paste = settings.bracketed_paste;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"status_indicator" | "indicator" => {
|
||||
app.status_indicator = settings.status_indicator.clone();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"synchronized_output" | "sync_output" | "sync" => {
|
||||
app.synchronized_output_enabled = settings.synchronized_output_enabled();
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"show_thinking" | "thinking" => {
|
||||
app.show_thinking = settings.show_thinking;
|
||||
app.mark_history_updated();
|
||||
@@ -446,6 +520,16 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
app.composer_border = settings.composer_border;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"composer_vim_mode" | "vim_mode" | "vim" => {
|
||||
app.composer.vim_enabled = settings.composer_vim_mode == "vim";
|
||||
app.composer.vim_mode = if app.composer.vim_enabled {
|
||||
VimMode::Normal
|
||||
} else {
|
||||
VimMode::Insert
|
||||
};
|
||||
app.composer.vim_pending_d = false;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
"paste_burst_detection" | "paste_burst" => {
|
||||
app.use_paste_burst_detection = settings.paste_burst_detection;
|
||||
if !app.use_paste_burst_detection {
|
||||
@@ -486,6 +570,10 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
"sidebar_focus" | "focus" => {
|
||||
app.set_sidebar_focus(SidebarFocus::from_setting(&settings.sidebar_focus));
|
||||
}
|
||||
"context_panel" | "context" | "session_panel" => {
|
||||
app.context_panel = settings.context_panel;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -493,10 +581,12 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
"default_mode" | "mode" => settings.default_mode.clone(),
|
||||
"cost_currency" | "currency" => settings.cost_currency.clone(),
|
||||
"theme" | "ui_theme" => settings.theme.clone(),
|
||||
"synchronized_output" | "sync_output" | "sync" => settings.synchronized_output.clone(),
|
||||
"background_color" | "background" | "bg" => settings
|
||||
.background_color
|
||||
.clone()
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
"composer_vim_mode" | "vim_mode" | "vim" => settings.composer_vim_mode.clone(),
|
||||
_ => value.to_string(),
|
||||
};
|
||||
|
||||
|
||||
+143
-1
@@ -58,22 +58,28 @@ pub struct SettingsSection {
|
||||
pub show_thinking: bool,
|
||||
pub show_tool_details: bool,
|
||||
pub locale: UiLocale,
|
||||
pub theme: UiThemeValue,
|
||||
#[schemars(
|
||||
title = "Background color",
|
||||
description = "Main TUI background color as #RRGGBB"
|
||||
)]
|
||||
pub background_color: Option<String>,
|
||||
pub bracketed_paste: bool,
|
||||
pub composer_density: ComposerDensityValue,
|
||||
pub composer_border: bool,
|
||||
pub composer_vim_mode: ComposerVimModeValue,
|
||||
pub transcript_spacing: TranscriptSpacingValue,
|
||||
pub status_indicator: StatusIndicatorValue,
|
||||
pub synchronized_output: SynchronizedOutputValue,
|
||||
pub default_mode: DefaultModeValue,
|
||||
#[schemars(range(min = 10, max = 50))]
|
||||
pub sidebar_width: u16,
|
||||
pub sidebar_focus: SidebarFocusValue,
|
||||
pub context_panel: bool,
|
||||
#[schemars(range(min = 0))]
|
||||
pub max_history: usize,
|
||||
pub cost_currency: CostCurrencyValue,
|
||||
pub prefer_external_pdftotext: bool,
|
||||
pub default_model: Option<String>,
|
||||
}
|
||||
|
||||
@@ -162,6 +168,22 @@ pub enum UiLocale {
|
||||
#[serde(rename = "pt-BR")]
|
||||
#[schemars(rename = "pt-BR")]
|
||||
PtBr,
|
||||
#[serde(rename = "es-419")]
|
||||
#[schemars(rename = "es-419")]
|
||||
Es419,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum UiThemeValue {
|
||||
System,
|
||||
Dark,
|
||||
Light,
|
||||
Grayscale,
|
||||
CatppuccinMocha,
|
||||
TokyoNight,
|
||||
Dracula,
|
||||
GruvboxDark,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -172,6 +194,13 @@ pub enum ComposerDensityValue {
|
||||
Spacious,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ComposerVimModeValue {
|
||||
Normal,
|
||||
Vim,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TranscriptSpacingValue {
|
||||
@@ -224,6 +253,14 @@ pub enum StatusIndicatorValue {
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SynchronizedOutputValue {
|
||||
Auto,
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StatusItemValue {
|
||||
@@ -281,16 +318,22 @@ pub fn build_document(app: &App, config: &Config) -> Result<ConfigUiDocument> {
|
||||
show_thinking: settings.show_thinking,
|
||||
show_tool_details: settings.show_tool_details,
|
||||
locale: UiLocale::from_setting(&settings.locale)?,
|
||||
theme: UiThemeValue::from_setting(&settings.theme)?,
|
||||
background_color: settings.background_color.clone(),
|
||||
bracketed_paste: settings.bracketed_paste,
|
||||
composer_density: settings.composer_density.as_str().into(),
|
||||
composer_border: settings.composer_border,
|
||||
composer_vim_mode: settings.composer_vim_mode.as_str().into(),
|
||||
transcript_spacing: settings.transcript_spacing.as_str().into(),
|
||||
status_indicator: settings.status_indicator.as_str().into(),
|
||||
synchronized_output: settings.synchronized_output.as_str().into(),
|
||||
default_mode: settings.default_mode.as_str().into(),
|
||||
sidebar_width: settings.sidebar_width_percent,
|
||||
sidebar_focus: settings.sidebar_focus.as_str().into(),
|
||||
context_panel: settings.context_panel,
|
||||
max_history: settings.max_input_history,
|
||||
cost_currency: CostCurrencyValue::from_setting(&settings.cost_currency)?,
|
||||
prefer_external_pdftotext: settings.prefer_external_pdftotext,
|
||||
default_model,
|
||||
},
|
||||
config: ConfigSection {
|
||||
@@ -439,6 +482,7 @@ pub fn apply_document(
|
||||
bool_str(doc.settings.show_tool_details),
|
||||
),
|
||||
("locale", doc.settings.locale.as_setting()),
|
||||
("theme", doc.settings.theme.as_setting()),
|
||||
(
|
||||
"background_color",
|
||||
doc.settings
|
||||
@@ -446,11 +490,16 @@ pub fn apply_document(
|
||||
.as_deref()
|
||||
.unwrap_or("default"),
|
||||
),
|
||||
("bracketed_paste", bool_str(doc.settings.bracketed_paste)),
|
||||
(
|
||||
"composer_density",
|
||||
doc.settings.composer_density.as_setting(),
|
||||
),
|
||||
("composer_border", bool_str(doc.settings.composer_border)),
|
||||
(
|
||||
"composer_vim_mode",
|
||||
doc.settings.composer_vim_mode.as_setting(),
|
||||
),
|
||||
(
|
||||
"transcript_spacing",
|
||||
doc.settings.transcript_spacing.as_setting(),
|
||||
@@ -459,11 +508,20 @@ pub fn apply_document(
|
||||
"status_indicator",
|
||||
doc.settings.status_indicator.as_setting(),
|
||||
),
|
||||
(
|
||||
"synchronized_output",
|
||||
doc.settings.synchronized_output.as_setting(),
|
||||
),
|
||||
("default_mode", doc.settings.default_mode.as_setting()),
|
||||
("sidebar_width", &doc.settings.sidebar_width.to_string()),
|
||||
("sidebar_focus", doc.settings.sidebar_focus.as_setting()),
|
||||
("context_panel", bool_str(doc.settings.context_panel)),
|
||||
("max_history", &doc.settings.max_history.to_string()),
|
||||
("cost_currency", doc.settings.cost_currency.as_setting()),
|
||||
(
|
||||
"prefer_external_pdftotext",
|
||||
bool_str(doc.settings.prefer_external_pdftotext),
|
||||
),
|
||||
("mcp_config_path", doc.config.mcp_config_path.as_str()),
|
||||
] {
|
||||
let result = commands::set_config_value(app, key, value, persist);
|
||||
@@ -654,6 +712,7 @@ impl UiLocale {
|
||||
Self::Ja => "ja",
|
||||
Self::ZhHans => "zh-Hans",
|
||||
Self::PtBr => "pt-BR",
|
||||
Self::Es419 => "es-419",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,12 +723,43 @@ impl UiLocale {
|
||||
Some("ja") => Ok(Self::Ja),
|
||||
Some("zh-Hans") => Ok(Self::ZhHans),
|
||||
Some("pt-BR") => Ok(Self::PtBr),
|
||||
Some("es-419") => Ok(Self::Es419),
|
||||
Some(other) => bail!("unsupported locale '{other}'"),
|
||||
None => bail!("invalid locale '{value}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiThemeValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::System => "system",
|
||||
Self::Dark => "dark",
|
||||
Self::Light => "light",
|
||||
Self::Grayscale => "grayscale",
|
||||
Self::CatppuccinMocha => "catppuccin-mocha",
|
||||
Self::TokyoNight => "tokyo-night",
|
||||
Self::Dracula => "dracula",
|
||||
Self::GruvboxDark => "gruvbox-dark",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_setting(value: &str) -> Result<Self> {
|
||||
match crate::palette::normalize_theme_name(value) {
|
||||
Some("system") => Ok(Self::System),
|
||||
Some("dark") => Ok(Self::Dark),
|
||||
Some("light") => Ok(Self::Light),
|
||||
Some("grayscale") => Ok(Self::Grayscale),
|
||||
Some("catppuccin-mocha") => Ok(Self::CatppuccinMocha),
|
||||
Some("tokyo-night") => Ok(Self::TokyoNight),
|
||||
Some("dracula") => Ok(Self::Dracula),
|
||||
Some("gruvbox-dark") => Ok(Self::GruvboxDark),
|
||||
Some(other) => bail!("unsupported theme '{other}'"),
|
||||
None => bail!("invalid theme '{value}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComposerDensityValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
@@ -680,6 +770,24 @@ impl ComposerDensityValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl ComposerVimModeValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => "normal",
|
||||
Self::Vim => "vim",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ComposerVimModeValue {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"vim" => Self::Vim,
|
||||
_ => Self::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TranscriptSpacingValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
@@ -820,6 +928,26 @@ impl StatusIndicatorValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl SynchronizedOutputValue {
|
||||
fn as_setting(self) -> &'static str {
|
||||
match self {
|
||||
Self::Auto => "auto",
|
||||
Self::On => "on",
|
||||
Self::Off => "off",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SynchronizedOutputValue {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"on" | "true" | "yes" | "1" | "enabled" => Self::On,
|
||||
"off" | "false" | "no" | "0" | "disabled" => Self::Off,
|
||||
_ => Self::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for StatusIndicatorValue {
|
||||
fn from(value: &str) -> Self {
|
||||
// Permissive aliases mirror `Settings::normalize_status_indicator`,
|
||||
@@ -1037,7 +1165,21 @@ background_color = "#1A1B26"
|
||||
let locale = &schema["$defs"]["UiLocale"]["enum"];
|
||||
assert_eq!(
|
||||
locale,
|
||||
&serde_json::json!(["auto", "en", "ja", "zh-Hans", "pt-BR"])
|
||||
&serde_json::json!(["auto", "en", "ja", "zh-Hans", "pt-BR", "es-419"])
|
||||
);
|
||||
let theme = &schema["$defs"]["UiThemeValue"]["enum"];
|
||||
assert_eq!(
|
||||
theme,
|
||||
&serde_json::json!([
|
||||
"system",
|
||||
"dark",
|
||||
"light",
|
||||
"grayscale",
|
||||
"catppuccin-mocha",
|
||||
"tokyo-night",
|
||||
"dracula",
|
||||
"gruvbox-dark"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -593,6 +593,9 @@ impl Settings {
|
||||
};
|
||||
self.sidebar_focus = normalized.to_string();
|
||||
}
|
||||
"context_panel" | "context" | "session_panel" => {
|
||||
self.context_panel = parse_bool(value)?;
|
||||
}
|
||||
"cost_currency" | "currency" => {
|
||||
let Some(currency) = crate::pricing::CostCurrency::from_setting(value) else {
|
||||
anyhow::bail!(
|
||||
@@ -681,6 +684,7 @@ impl Settings {
|
||||
self.sidebar_width_percent
|
||||
));
|
||||
lines.push(format!(" sidebar_focus: {}", self.sidebar_focus));
|
||||
lines.push(format!(" context_panel: {}", self.context_panel));
|
||||
lines.push(format!(" cost_currency: {}", self.cost_currency));
|
||||
lines.push(format!(" max_history: {}", self.max_input_history));
|
||||
lines.push(format!(
|
||||
@@ -743,6 +747,7 @@ impl Settings {
|
||||
"composer_border",
|
||||
"Show a border around the composer input area: on/off",
|
||||
),
|
||||
("composer_vim_mode", "Composer editing mode: normal, vim"),
|
||||
(
|
||||
"transcript_spacing",
|
||||
"Transcript spacing: compact, comfortable, spacious",
|
||||
@@ -765,6 +770,10 @@ impl Settings {
|
||||
"sidebar_focus",
|
||||
"Sidebar focus: auto, work, tasks, agents, context",
|
||||
),
|
||||
(
|
||||
"context_panel",
|
||||
"Show the session context sidebar panel: on/off",
|
||||
),
|
||||
("cost_currency", "Cost display currency: usd, cny"),
|
||||
("max_history", "Max input history entries"),
|
||||
(
|
||||
@@ -1089,6 +1098,22 @@ mod tests {
|
||||
assert!(err.to_string().contains("invalid sidebar focus"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_panel_is_configurable() {
|
||||
let mut settings = Settings::default();
|
||||
assert!(!settings.context_panel);
|
||||
|
||||
settings
|
||||
.set("context_panel", "on")
|
||||
.expect("enable context panel");
|
||||
assert!(settings.context_panel);
|
||||
|
||||
settings
|
||||
.set("session_panel", "off")
|
||||
.expect("disable context panel via alias");
|
||||
assert!(!settings.context_panel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_localizes_header_and_config_file_label() {
|
||||
let settings = Settings::default();
|
||||
|
||||
+135
-15
@@ -23,7 +23,7 @@ use crate::tools::subagent::SubAgentStatus;
|
||||
use crate::tools::todo::TodoStatus;
|
||||
|
||||
use super::app::{App, SidebarFocus, TaskPanelEntry};
|
||||
use super::history::{HistoryCell, ToolCell, ToolStatus, summarize_tool_output};
|
||||
use super::history::{GenericToolCell, HistoryCell, ToolCell, ToolStatus, summarize_tool_output};
|
||||
use super::subagent_routing::active_fanout_counts;
|
||||
use super::ui::truncate_line_to_width;
|
||||
|
||||
@@ -389,11 +389,21 @@ fn push_work_checklist_lines(
|
||||
]));
|
||||
|
||||
let reserve_for_strategy = if summary.has_strategy() { 2 } else { 0 };
|
||||
let max_items = max_rows
|
||||
let available_item_rows = max_rows
|
||||
.saturating_sub(lines.len())
|
||||
.saturating_sub(reserve_for_strategy)
|
||||
.min(summary.checklist_items.len());
|
||||
for item in summary.checklist_items.iter().take(max_items) {
|
||||
let max_items =
|
||||
if summary.checklist_items.len() > available_item_rows && available_item_rows > 1 {
|
||||
available_item_rows - 1
|
||||
} else {
|
||||
available_item_rows
|
||||
};
|
||||
let start = checklist_window_start(&summary.checklist_items, max_items);
|
||||
let end = start
|
||||
.saturating_add(max_items)
|
||||
.min(summary.checklist_items.len());
|
||||
for item in summary.checklist_items[start..end].iter() {
|
||||
let (prefix, color) = match item.status {
|
||||
TodoStatus::Pending => ("[ ]", palette::TEXT_MUTED),
|
||||
TodoStatus::InProgress => ("[~]", palette::STATUS_WARNING),
|
||||
@@ -406,15 +416,37 @@ fn push_work_checklist_lines(
|
||||
)));
|
||||
}
|
||||
|
||||
let remaining = summary.checklist_items.len().saturating_sub(max_items);
|
||||
let earlier = start;
|
||||
let later = summary.checklist_items.len().saturating_sub(end);
|
||||
let remaining = earlier.saturating_add(later);
|
||||
if remaining > 0 && lines.len() < max_rows {
|
||||
let label = match (earlier, later) {
|
||||
(0, later) => format!("+{later} more checklist items"),
|
||||
(earlier, 0) => format!("+{earlier} earlier checklist items"),
|
||||
(earlier, later) => format!("+{earlier} earlier, +{later} later"),
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("+{remaining} more checklist items"),
|
||||
label,
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn checklist_window_start(items: &[SidebarWorkChecklistItem], max_items: usize) -> usize {
|
||||
if max_items >= items.len() {
|
||||
return 0;
|
||||
}
|
||||
let Some(active_idx) = items
|
||||
.iter()
|
||||
.position(|item| item.status == TodoStatus::InProgress)
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
active_idx
|
||||
.saturating_sub(max_items / 2)
|
||||
.min(items.len().saturating_sub(max_items))
|
||||
}
|
||||
|
||||
fn push_work_strategy_lines(
|
||||
summary: &SidebarWorkSummary,
|
||||
content_width: usize,
|
||||
@@ -891,22 +923,45 @@ fn sidebar_tool_row_from_cell(cell: &HistoryCell) -> Option<SidebarToolRow> {
|
||||
duration_ms: None,
|
||||
}),
|
||||
ToolCell::Generic(generic) => Some(SidebarToolRow {
|
||||
name: generic.name.clone(),
|
||||
name: friendly_generic_tool_name(&generic.name).to_string(),
|
||||
status: generic.status,
|
||||
summary: compact_join([
|
||||
generic.input_summary.clone().unwrap_or_default(),
|
||||
generic.output_summary.clone().unwrap_or_default(),
|
||||
generic
|
||||
.output
|
||||
.as_deref()
|
||||
.map(summarize_tool_output)
|
||||
.unwrap_or_default(),
|
||||
]),
|
||||
summary: generic_tool_sidebar_summary(generic),
|
||||
duration_ms: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn friendly_generic_tool_name(name: &str) -> &str {
|
||||
match name {
|
||||
"task_shell_start" => "start shell job",
|
||||
"task_shell_wait" => "wait shell job",
|
||||
"task_shell_write" => "write shell job",
|
||||
_ => name,
|
||||
}
|
||||
}
|
||||
|
||||
fn generic_tool_sidebar_summary(generic: &GenericToolCell) -> String {
|
||||
match generic.name.as_str() {
|
||||
"task_shell_start" => compact_join([
|
||||
generic.input_summary.clone().unwrap_or_default(),
|
||||
"background shell job".to_string(),
|
||||
]),
|
||||
"task_shell_wait" => compact_join([
|
||||
generic.input_summary.clone().unwrap_or_default(),
|
||||
generic.output_summary.clone().unwrap_or_default(),
|
||||
]),
|
||||
_ => compact_join([
|
||||
generic.input_summary.clone().unwrap_or_default(),
|
||||
generic.output_summary.clone().unwrap_or_default(),
|
||||
generic
|
||||
.output
|
||||
.as_deref()
|
||||
.map(summarize_tool_output)
|
||||
.unwrap_or_default(),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
fn background_task_rows(app: &App, active_rows: &[SidebarToolRow]) -> Vec<TaskPanelEntry> {
|
||||
let mut rows: Vec<TaskPanelEntry> = app
|
||||
.task_panel
|
||||
@@ -1708,6 +1763,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_panel_keeps_active_checklist_item_visible_when_truncated() {
|
||||
let summary = SidebarWorkSummary {
|
||||
checklist_completion_pct: 38,
|
||||
checklist_items: (1..=8)
|
||||
.map(|id| SidebarWorkChecklistItem {
|
||||
id,
|
||||
content: format!("Release task {id}"),
|
||||
status: if id <= 3 {
|
||||
TodoStatus::Completed
|
||||
} else if id == 5 {
|
||||
TodoStatus::InProgress
|
||||
} else {
|
||||
TodoStatus::Pending
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
..SidebarWorkSummary::default()
|
||||
};
|
||||
|
||||
let text = lines_to_text(&work_panel_lines(&summary, 80, 6, PaletteMode::Dark));
|
||||
|
||||
assert!(
|
||||
text.iter()
|
||||
.any(|line| line.contains("[~] #5 Release task 5")),
|
||||
"active checklist item should stay visible in a short Work panel: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
text.iter().any(|line| line.contains("earlier"))
|
||||
|| text.iter().any(|line| line.contains("later")),
|
||||
"truncation should explain omitted checklist rows: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_panel_includes_strategy_only_when_plan_state_is_non_empty() {
|
||||
let empty_text = lines_to_text(&work_panel_lines(
|
||||
@@ -2034,6 +2123,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_panel_uses_plain_names_for_shell_background_helpers() {
|
||||
let mut app = create_test_app();
|
||||
let mut active = ActiveCell::new();
|
||||
active.push_tool(
|
||||
"shell-wait",
|
||||
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
|
||||
name: "task_shell_wait".to_string(),
|
||||
status: ToolStatus::Running,
|
||||
input_summary: Some("task_id: shell_33a08c3c".to_string()),
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
|
||||
let text = lines_to_text(&task_panel_lines(&app, 80, 6));
|
||||
|
||||
assert!(
|
||||
text.iter().any(|line| line.contains("[~] wait shell job")),
|
||||
"shell helper should render as a user-facing activity: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
!text.iter().any(|line| line.contains("task_shell_wait")),
|
||||
"internal helper name should not leak into sidebar: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_empty_state_says_no_agents() {
|
||||
let summary = SidebarSubagentSummary::default();
|
||||
|
||||
@@ -648,6 +648,13 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "fancy_animations".to_string(),
|
||||
value: settings.fancy_animations.to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "show_thinking".to_string(),
|
||||
@@ -662,6 +669,27 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "status_indicator".to_string(),
|
||||
value: settings.status_indicator.clone(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "synchronized_output".to_string(),
|
||||
value: settings.synchronized_output.clone(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "cost_currency".to_string(),
|
||||
value: settings.cost_currency.clone(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Display,
|
||||
key: "transcript_spacing".to_string(),
|
||||
@@ -683,6 +711,20 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Composer,
|
||||
key: "composer_vim_mode".to_string(),
|
||||
value: settings.composer_vim_mode.clone(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Composer,
|
||||
key: "bracketed_paste".to_string(),
|
||||
value: settings.bracketed_paste.to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Composer,
|
||||
key: "paste_burst_detection".to_string(),
|
||||
@@ -704,6 +746,13 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Sidebar,
|
||||
key: "context_panel".to_string(),
|
||||
value: settings.context_panel.to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::History,
|
||||
key: "auto_compact".to_string(),
|
||||
@@ -718,6 +767,13 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "prefer_external_pdftotext".to_string(),
|
||||
value: settings.prefer_external_pdftotext.to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "mcp_config_path".to_string(),
|
||||
@@ -1904,6 +1960,7 @@ mod tests {
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::localization::Locale;
|
||||
use crate::settings::Settings;
|
||||
use crate::tools::subagent::{
|
||||
SubAgentAssignment, SubAgentResult, SubAgentStatus, SubAgentType,
|
||||
};
|
||||
@@ -2079,12 +2136,32 @@ mod tests {
|
||||
assert!(keys.contains(&"theme"));
|
||||
assert!(keys.contains(&"locale"));
|
||||
assert!(keys.contains(&"background_color"));
|
||||
assert!(keys.contains(&"fancy_animations"));
|
||||
assert!(keys.contains(&"status_indicator"));
|
||||
assert!(keys.contains(&"synchronized_output"));
|
||||
assert!(keys.contains(&"auto_compact"));
|
||||
assert!(keys.contains(&"composer_border"));
|
||||
assert!(keys.contains(&"composer_vim_mode"));
|
||||
assert!(keys.contains(&"bracketed_paste"));
|
||||
assert!(keys.contains(&"context_panel"));
|
||||
assert!(keys.contains(&"cost_currency"));
|
||||
assert!(keys.contains(&"prefer_external_pdftotext"));
|
||||
assert!(keys.contains(&"mcp_config_path"));
|
||||
assert!(view.rows.iter().all(|row| row.editable));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_view_exposes_all_available_saved_settings() {
|
||||
let app = create_test_app();
|
||||
let view = ConfigView::new_for_app(&app);
|
||||
let keys: std::collections::HashSet<&str> =
|
||||
view.rows.iter().map(|row| row.key.as_str()).collect();
|
||||
|
||||
for (key, _) in Settings::available_settings() {
|
||||
assert!(keys.contains(key), "missing native config row for {key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_view_filter_matches_group_and_rows() {
|
||||
let app = create_test_app();
|
||||
@@ -2096,7 +2173,7 @@ mod tests {
|
||||
assert_eq!(visible_section_labels(&view), vec!["Sidebar"]);
|
||||
assert_eq!(
|
||||
visible_row_keys(&view),
|
||||
vec!["sidebar_width", "sidebar_focus"]
|
||||
vec!["sidebar_width", "sidebar_focus", "context_panel"]
|
||||
);
|
||||
assert_eq!(view.rows[view.selected].key, "sidebar_width");
|
||||
}
|
||||
@@ -2190,7 +2267,7 @@ mod tests {
|
||||
let app = create_test_app();
|
||||
let mut view = ConfigView::new_for_app(&app);
|
||||
|
||||
type_filter(&mut view, "mcp");
|
||||
type_filter(&mut view, "mcp_config");
|
||||
assert_eq!(visible_row_keys(&view), vec!["mcp_config_path"]);
|
||||
|
||||
let start = view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
Reference in New Issue
Block a user