mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Merge 4565f6dbb6 into sapling-pr-archive-rhan-oai
This commit is contained in:
@@ -49,6 +49,7 @@ mod mcp_server_elicitation;
|
||||
mod multi_select_picker;
|
||||
mod request_user_input;
|
||||
mod status_line_setup;
|
||||
mod status_surface_preview;
|
||||
mod title_setup;
|
||||
pub(crate) use app_link_view::AppLinkElicitationTarget;
|
||||
pub(crate) use app_link_view::AppLinkSuggestionType;
|
||||
@@ -107,10 +108,13 @@ pub(crate) use feedback_view::feedback_upload_consent_params;
|
||||
pub(crate) use skills_toggle_view::SkillsToggleItem;
|
||||
pub(crate) use skills_toggle_view::SkillsToggleView;
|
||||
pub(crate) use status_line_setup::StatusLineItem;
|
||||
pub(crate) use status_line_setup::StatusLinePreviewData;
|
||||
pub(crate) use status_line_setup::StatusLineSetupView;
|
||||
pub(crate) use status_surface_preview::StatusSurfacePreviewData;
|
||||
pub(crate) use status_surface_preview::StatusSurfacePreviewItem;
|
||||
pub(crate) use title_setup::TerminalTitleItem;
|
||||
pub(crate) use title_setup::TerminalTitleSetupView;
|
||||
#[cfg(test)]
|
||||
pub(crate) use title_setup::preview_line_for_title_items;
|
||||
mod paste_burst;
|
||||
mod pending_input_preview;
|
||||
mod pending_thread_approvals;
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/status_line_setup.rs
|
||||
expression: "render_lines(&view, 72)"
|
||||
---
|
||||
|
||||
Configure Status Line
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] model-name Current model name
|
||||
[x] current-dir Current working directory
|
||||
[x] git-branch Current Git branch (omitted when unavaila…
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] project-root Project root directory (omitted when unav…
|
||||
[ ] context-remaining Percentage of context window remaining (o…
|
||||
[ ] context-used Percentage of context window used (omitte…
|
||||
[ ] five-hour-limit Remaining usage on 5-hour usage limit (om…
|
||||
|
||||
gpt-5-codex · ~/codex-rs · jif/statusline-preview
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/title_setup.rs
|
||||
expression: "render_lines(&view, 84)"
|
||||
---
|
||||
|
||||
Configure Terminal Title
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project Project name (falls back to current directory name)
|
||||
[x] spinner Animated task spinner (omitted while idle or when animations…
|
||||
[x] status Compact session status text (Ready, Working, Thinking)
|
||||
[x] thread Current thread title (omitted until available)
|
||||
[ ] app-name Codex app name
|
||||
[ ] git-branch Current Git branch (omitted when unavailable)
|
||||
[ ] model Current model name
|
||||
[ ] task-progress Latest task progress from update_plan (omitted until availab…
|
||||
|
||||
my-project ⠋ Working | Investigate flaky test
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::Display;
|
||||
@@ -33,6 +31,8 @@ use crate::bottom_pane::CancellationEvent;
|
||||
use crate::bottom_pane::bottom_pane_view::BottomPaneView;
|
||||
use crate::bottom_pane::multi_select_picker::MultiSelectItem;
|
||||
use crate::bottom_pane::multi_select_picker::MultiSelectPicker;
|
||||
use crate::bottom_pane::status_surface_preview::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::status_surface_preview::StatusSurfacePreviewItem;
|
||||
use crate::render::renderable::Renderable;
|
||||
|
||||
/// Available items that can be displayed in the status line.
|
||||
@@ -138,36 +138,26 @@ impl StatusLineItem {
|
||||
StatusLineItem::ThreadTitle => "Current thread title (omitted unless changed by user)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime values used to preview the current status-line selection.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct StatusLinePreviewData {
|
||||
values: BTreeMap<StatusLineItem, String>,
|
||||
}
|
||||
|
||||
impl StatusLinePreviewData {
|
||||
pub(crate) fn from_iter<I>(values: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (StatusLineItem, String)>,
|
||||
{
|
||||
Self {
|
||||
values: values.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn line_for_items(&self, items: &[MultiSelectItem]) -> Option<Line<'static>> {
|
||||
let preview = items
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
|
||||
.filter_map(|item| self.values.get(&item).cloned())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · ");
|
||||
if preview.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Line::from(preview))
|
||||
pub(crate) fn preview_item(self) -> StatusSurfacePreviewItem {
|
||||
match self {
|
||||
StatusLineItem::ModelName => StatusSurfacePreviewItem::Model,
|
||||
StatusLineItem::ModelWithReasoning => StatusSurfacePreviewItem::ModelWithReasoning,
|
||||
StatusLineItem::CurrentDir => StatusSurfacePreviewItem::CurrentDir,
|
||||
StatusLineItem::ProjectRoot => StatusSurfacePreviewItem::ProjectRoot,
|
||||
StatusLineItem::GitBranch => StatusSurfacePreviewItem::GitBranch,
|
||||
StatusLineItem::ContextRemaining => StatusSurfacePreviewItem::ContextRemaining,
|
||||
StatusLineItem::ContextUsed => StatusSurfacePreviewItem::ContextUsed,
|
||||
StatusLineItem::FiveHourLimit => StatusSurfacePreviewItem::FiveHourLimit,
|
||||
StatusLineItem::WeeklyLimit => StatusSurfacePreviewItem::WeeklyLimit,
|
||||
StatusLineItem::CodexVersion => StatusSurfacePreviewItem::CodexVersion,
|
||||
StatusLineItem::ContextWindowSize => StatusSurfacePreviewItem::ContextWindowSize,
|
||||
StatusLineItem::UsedTokens => StatusSurfacePreviewItem::UsedTokens,
|
||||
StatusLineItem::TotalInputTokens => StatusSurfacePreviewItem::TotalInputTokens,
|
||||
StatusLineItem::TotalOutputTokens => StatusSurfacePreviewItem::TotalOutputTokens,
|
||||
StatusLineItem::SessionId => StatusSurfacePreviewItem::SessionId,
|
||||
StatusLineItem::FastMode => StatusSurfacePreviewItem::FastMode,
|
||||
StatusLineItem::ThreadTitle => StatusSurfacePreviewItem::ThreadTitle,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,7 +187,7 @@ impl StatusLineSetupView {
|
||||
/// enabled. Remaining items are appended and marked as disabled.
|
||||
pub(crate) fn new(
|
||||
status_line_items: Option<&[String]>,
|
||||
preview_data: StatusLinePreviewData,
|
||||
preview_data: StatusSurfacePreviewData,
|
||||
app_event_tx: AppEventSender,
|
||||
) -> Self {
|
||||
let mut used_ids = HashSet::new();
|
||||
@@ -236,7 +226,15 @@ impl StatusLineSetupView {
|
||||
])
|
||||
.items(items)
|
||||
.enable_ordering()
|
||||
.on_preview(move |items| preview_data.line_for_items(items))
|
||||
.on_preview(move |items| {
|
||||
preview_data.line_for_items(
|
||||
items
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
|
||||
.map(StatusLineItem::preview_item),
|
||||
)
|
||||
})
|
||||
.on_confirm(|ids, app_event| {
|
||||
let items = ids
|
||||
.iter()
|
||||
@@ -291,14 +289,7 @@ impl Renderable for StatusLineSetupView {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
|
||||
#[test]
|
||||
fn context_used_accepts_context_usage_legacy_id() {
|
||||
@@ -324,130 +315,4 @@ mod tests {
|
||||
"context-remaining"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_uses_runtime_values() {
|
||||
let preview_data = StatusLinePreviewData::from_iter([
|
||||
(StatusLineItem::ModelName, "gpt-5".to_string()),
|
||||
(StatusLineItem::CurrentDir, "/repo".to_string()),
|
||||
]);
|
||||
let items = vec![
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::ModelName.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::CurrentDir.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
preview_data.line_for_items(&items),
|
||||
Some(Line::from("gpt-5 · /repo"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_omits_items_without_runtime_values() {
|
||||
let preview_data =
|
||||
StatusLinePreviewData::from_iter([(StatusLineItem::ModelName, "gpt-5".to_string())]);
|
||||
let items = vec![
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::ModelName.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::GitBranch.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
preview_data.line_for_items(&items),
|
||||
Some(Line::from("gpt-5"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_includes_thread_title() {
|
||||
let preview_data = StatusLinePreviewData::from_iter([
|
||||
(StatusLineItem::ModelName, "gpt-5".to_string()),
|
||||
(StatusLineItem::ThreadTitle, "Roadmap cleanup".to_string()),
|
||||
]);
|
||||
let items = vec![
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::ModelName.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
MultiSelectItem {
|
||||
id: StatusLineItem::ThreadTitle.to_string(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
preview_data.line_for_items(&items),
|
||||
Some(Line::from("gpt-5 · Roadmap cleanup"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_view_snapshot_uses_runtime_preview_values() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let view = StatusLineSetupView::new(
|
||||
Some(&[
|
||||
StatusLineItem::ModelName.to_string(),
|
||||
StatusLineItem::CurrentDir.to_string(),
|
||||
StatusLineItem::GitBranch.to_string(),
|
||||
]),
|
||||
StatusLinePreviewData::from_iter([
|
||||
(StatusLineItem::ModelName, "gpt-5-codex".to_string()),
|
||||
(StatusLineItem::CurrentDir, "~/codex-rs".to_string()),
|
||||
(
|
||||
StatusLineItem::GitBranch,
|
||||
"jif/statusline-preview".to_string(),
|
||||
),
|
||||
(StatusLineItem::WeeklyLimit, "weekly 82%".to_string()),
|
||||
]),
|
||||
AppEventSender::new(tx_raw),
|
||||
);
|
||||
|
||||
assert_snapshot!(render_lines(&view, /*width*/ 72));
|
||||
}
|
||||
|
||||
fn render_lines(view: &StatusLineSetupView, width: u16) -> String {
|
||||
let height = view.desired_height(width);
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
|
||||
(0..area.height)
|
||||
.map(|row| {
|
||||
let mut line = String::new();
|
||||
for col in 0..area.width {
|
||||
let symbol = buf[(area.x + col, area.y + row)].symbol();
|
||||
if symbol.is_empty() {
|
||||
line.push(' ');
|
||||
} else {
|
||||
line.push_str(symbol);
|
||||
}
|
||||
}
|
||||
line
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ratatui::text::Line;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(crate) enum StatusSurfacePreviewItem {
|
||||
AppName,
|
||||
ProjectName,
|
||||
ProjectRoot,
|
||||
CurrentDir,
|
||||
Status,
|
||||
ThreadTitle,
|
||||
GitBranch,
|
||||
ContextRemaining,
|
||||
ContextUsed,
|
||||
FiveHourLimit,
|
||||
WeeklyLimit,
|
||||
CodexVersion,
|
||||
ContextWindowSize,
|
||||
UsedTokens,
|
||||
TotalInputTokens,
|
||||
TotalOutputTokens,
|
||||
SessionId,
|
||||
FastMode,
|
||||
Model,
|
||||
ModelWithReasoning,
|
||||
TaskProgress,
|
||||
}
|
||||
|
||||
impl StatusSurfacePreviewItem {
|
||||
fn placeholder(self) -> &'static str {
|
||||
match self {
|
||||
StatusSurfacePreviewItem::AppName => "codex",
|
||||
StatusSurfacePreviewItem::ProjectName => "my-project",
|
||||
StatusSurfacePreviewItem::ProjectRoot => "my-project",
|
||||
StatusSurfacePreviewItem::CurrentDir => "~/my-project/subdir",
|
||||
StatusSurfacePreviewItem::Status => "Working",
|
||||
StatusSurfacePreviewItem::ThreadTitle => "thread title",
|
||||
StatusSurfacePreviewItem::GitBranch => "feat/awesome-feature",
|
||||
StatusSurfacePreviewItem::ContextRemaining => "Context 0% left",
|
||||
StatusSurfacePreviewItem::ContextUsed => "Context 0% used",
|
||||
StatusSurfacePreviewItem::FiveHourLimit => "5h 0%",
|
||||
StatusSurfacePreviewItem::WeeklyLimit => "weekly 0%",
|
||||
StatusSurfacePreviewItem::CodexVersion => "0.0.0",
|
||||
StatusSurfacePreviewItem::ContextWindowSize => "0 window",
|
||||
StatusSurfacePreviewItem::UsedTokens => "0 used",
|
||||
StatusSurfacePreviewItem::TotalInputTokens => "0 in",
|
||||
StatusSurfacePreviewItem::TotalOutputTokens => "0 out",
|
||||
StatusSurfacePreviewItem::SessionId => "550e8400-e29b-41d4",
|
||||
StatusSurfacePreviewItem::FastMode => "Fast on",
|
||||
StatusSurfacePreviewItem::Model => "gpt-5.2-codex",
|
||||
StatusSurfacePreviewItem::ModelWithReasoning => "gpt-5.2-codex medium",
|
||||
StatusSurfacePreviewItem::TaskProgress => "Tasks 0/0",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter() -> impl Iterator<Item = Self> {
|
||||
[
|
||||
Self::AppName,
|
||||
Self::ProjectName,
|
||||
Self::ProjectRoot,
|
||||
Self::CurrentDir,
|
||||
Self::Status,
|
||||
Self::ThreadTitle,
|
||||
Self::GitBranch,
|
||||
Self::ContextRemaining,
|
||||
Self::ContextUsed,
|
||||
Self::FiveHourLimit,
|
||||
Self::WeeklyLimit,
|
||||
Self::CodexVersion,
|
||||
Self::ContextWindowSize,
|
||||
Self::UsedTokens,
|
||||
Self::TotalInputTokens,
|
||||
Self::TotalOutputTokens,
|
||||
Self::SessionId,
|
||||
Self::FastMode,
|
||||
Self::Model,
|
||||
Self::ModelWithReasoning,
|
||||
Self::TaskProgress,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PreviewValue {
|
||||
text: String,
|
||||
is_placeholder: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct StatusSurfacePreviewData {
|
||||
values: BTreeMap<StatusSurfacePreviewItem, PreviewValue>,
|
||||
}
|
||||
|
||||
impl Default for StatusSurfacePreviewData {
|
||||
fn default() -> Self {
|
||||
let mut data = Self {
|
||||
values: BTreeMap::new(),
|
||||
};
|
||||
for item in StatusSurfacePreviewItem::iter() {
|
||||
data.set_placeholder(item, item.placeholder());
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusSurfacePreviewData {
|
||||
pub(crate) fn from_iter<I, V>(values: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (StatusSurfacePreviewItem, V)>,
|
||||
V: Into<String>,
|
||||
{
|
||||
let mut data = Self::default();
|
||||
for (item, value) in values {
|
||||
data.set_live(item, value);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
pub(crate) fn set_live<V>(&mut self, item: StatusSurfacePreviewItem, value: V)
|
||||
where
|
||||
V: Into<String>,
|
||||
{
|
||||
self.values.insert(
|
||||
item,
|
||||
PreviewValue {
|
||||
text: value.into(),
|
||||
is_placeholder: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn set_placeholder<V>(&mut self, item: StatusSurfacePreviewItem, value: V)
|
||||
where
|
||||
V: Into<String>,
|
||||
{
|
||||
if self
|
||||
.values
|
||||
.get(&item)
|
||||
.is_some_and(|value| !value.is_placeholder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.values.insert(
|
||||
item,
|
||||
PreviewValue {
|
||||
text: value.into(),
|
||||
is_placeholder: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str> {
|
||||
self.values.get(&item).map(|value| value.text.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn line_for_items<I>(&self, items: I) -> Option<Line<'static>>
|
||||
where
|
||||
I: IntoIterator<Item = StatusSurfacePreviewItem>,
|
||||
{
|
||||
let preview = items
|
||||
.into_iter()
|
||||
.filter_map(|item| self.value_for(item))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · ");
|
||||
if preview.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Line::from(preview))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ use crate::bottom_pane::CancellationEvent;
|
||||
use crate::bottom_pane::bottom_pane_view::BottomPaneView;
|
||||
use crate::bottom_pane::multi_select_picker::MultiSelectItem;
|
||||
use crate::bottom_pane::multi_select_picker::MultiSelectPicker;
|
||||
use crate::bottom_pane::status_surface_preview::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::status_surface_preview::StatusSurfacePreviewItem;
|
||||
use crate::render::renderable::Renderable;
|
||||
|
||||
/// Available items that can be displayed in the terminal title.
|
||||
@@ -68,20 +70,16 @@ impl TerminalTitleItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Example text used when previewing the title picker.
|
||||
///
|
||||
/// These are illustrative sample values, not live data from the current
|
||||
/// session.
|
||||
pub(crate) fn preview_example(self) -> &'static str {
|
||||
pub(crate) fn preview_item(self) -> Option<StatusSurfacePreviewItem> {
|
||||
match self {
|
||||
TerminalTitleItem::AppName => "codex",
|
||||
TerminalTitleItem::Project => "my-project",
|
||||
TerminalTitleItem::Spinner => "⠋",
|
||||
TerminalTitleItem::Status => "Working",
|
||||
TerminalTitleItem::Thread => "Investigate flaky test",
|
||||
TerminalTitleItem::GitBranch => "feat/awesome-feature",
|
||||
TerminalTitleItem::Model => "gpt-5.2-codex",
|
||||
TerminalTitleItem::TaskProgress => "Tasks 2/5",
|
||||
TerminalTitleItem::AppName => Some(StatusSurfacePreviewItem::AppName),
|
||||
TerminalTitleItem::Project => Some(StatusSurfacePreviewItem::ProjectName),
|
||||
TerminalTitleItem::Spinner => None,
|
||||
TerminalTitleItem::Status => Some(StatusSurfacePreviewItem::Status),
|
||||
TerminalTitleItem::Thread => Some(StatusSurfacePreviewItem::ThreadTitle),
|
||||
TerminalTitleItem::GitBranch => Some(StatusSurfacePreviewItem::GitBranch),
|
||||
TerminalTitleItem::Model => Some(StatusSurfacePreviewItem::Model),
|
||||
TerminalTitleItem::TaskProgress => Some(StatusSurfacePreviewItem::TaskProgress),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +101,39 @@ impl TerminalTitleItem {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn preview_line_for_title_items(
|
||||
items: &[TerminalTitleItem],
|
||||
preview_data: &StatusSurfacePreviewData,
|
||||
) -> Option<Line<'static>> {
|
||||
let mut previous = None;
|
||||
let preview = items
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(String::new(), |mut preview, item| {
|
||||
if item == TerminalTitleItem::Spinner {
|
||||
preview.push_str(item.separator_from_previous(previous));
|
||||
preview.push('⠋');
|
||||
previous = Some(item);
|
||||
return preview;
|
||||
}
|
||||
let Some(value) = item
|
||||
.preview_item()
|
||||
.and_then(|preview_item| preview_data.value_for(preview_item))
|
||||
else {
|
||||
return preview;
|
||||
};
|
||||
preview.push_str(item.separator_from_previous(previous));
|
||||
preview.push_str(value);
|
||||
previous = Some(item);
|
||||
preview
|
||||
});
|
||||
if preview.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Line::from(preview))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_terminal_title_items<T>(ids: impl Iterator<Item = T>) -> Option<Vec<TerminalTitleItem>>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
@@ -128,7 +159,11 @@ impl TerminalTitleSetupView {
|
||||
/// main TUI still warns about them when rendering the actual title, but the
|
||||
/// picker itself only exposes the selectable items it can meaningfully
|
||||
/// preview and persist.
|
||||
pub(crate) fn new(title_items: Option<&[String]>, app_event_tx: AppEventSender) -> Self {
|
||||
pub(crate) fn new(
|
||||
title_items: Option<&[String]>,
|
||||
preview_data: StatusSurfacePreviewData,
|
||||
app_event_tx: AppEventSender,
|
||||
) -> Self {
|
||||
let selected_items = title_items
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -161,25 +196,14 @@ impl TerminalTitleSetupView {
|
||||
])
|
||||
.items(items)
|
||||
.enable_ordering()
|
||||
.on_preview(|items| {
|
||||
.on_preview(move |items| {
|
||||
let items = parse_terminal_title_items(
|
||||
items
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.map(|item| item.id.as_str()),
|
||||
)?;
|
||||
let mut preview = String::new();
|
||||
let mut previous = None;
|
||||
for item in items.iter().copied() {
|
||||
preview.push_str(item.separator_from_previous(previous));
|
||||
preview.push_str(item.preview_example());
|
||||
previous = Some(item);
|
||||
}
|
||||
if preview.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Line::from(preview))
|
||||
}
|
||||
preview_line_for_title_items(&items, &preview_data)
|
||||
})
|
||||
.on_change(|items, app_event| {
|
||||
let Some(items) = parse_terminal_title_items(
|
||||
@@ -243,49 +267,7 @@ impl Renderable for TerminalTitleSetupView {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
fn render_lines(view: &TerminalTitleSetupView, width: u16) -> String {
|
||||
let height = view.desired_height(width);
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
|
||||
let lines: Vec<String> = (0..area.height)
|
||||
.map(|row| {
|
||||
let mut line = String::new();
|
||||
for col in 0..area.width {
|
||||
let symbol = buf[(area.x + col, area.y + row)].symbol();
|
||||
if symbol.is_empty() {
|
||||
line.push(' ');
|
||||
} else {
|
||||
line.push_str(symbol);
|
||||
}
|
||||
}
|
||||
line
|
||||
})
|
||||
.collect();
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_title_setup_popup() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let selected = [
|
||||
"project".to_string(),
|
||||
"spinner".to_string(),
|
||||
"status".to_string(),
|
||||
"thread".to_string(),
|
||||
];
|
||||
let view = TerminalTitleSetupView::new(Some(&selected), tx);
|
||||
assert_snapshot!(
|
||||
"terminal_title_setup_basic",
|
||||
render_lines(&view, /*width*/ 84)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_terminal_title_items_preserves_order() {
|
||||
|
||||
@@ -52,8 +52,9 @@ use crate::app_server_session::ThreadSessionState;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use crate::audio_device::list_realtime_audio_device_names;
|
||||
use crate::bottom_pane::StatusLineItem;
|
||||
use crate::bottom_pane::StatusLinePreviewData;
|
||||
use crate::bottom_pane::StatusLineSetupView;
|
||||
use crate::bottom_pane::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::StatusSurfacePreviewItem;
|
||||
use crate::bottom_pane::TerminalTitleItem;
|
||||
use crate::bottom_pane::TerminalTitleSetupView;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
@@ -7501,10 +7502,7 @@ impl ChatWidget {
|
||||
let configured_status_line_items = self.configured_status_line_items();
|
||||
let view = StatusLineSetupView::new(
|
||||
Some(configured_status_line_items.as_slice()),
|
||||
StatusLinePreviewData::from_iter(StatusLineItem::iter().filter_map(|item| {
|
||||
self.status_line_value_for_item(&item)
|
||||
.map(|value| (item, value))
|
||||
})),
|
||||
self.status_surface_preview_data(),
|
||||
self.app_event_tx.clone(),
|
||||
);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
@@ -7515,11 +7513,40 @@ impl ChatWidget {
|
||||
self.terminal_title_setup_original_items = Some(self.config.tui_terminal_title.clone());
|
||||
let view = TerminalTitleSetupView::new(
|
||||
Some(configured_terminal_title_items.as_slice()),
|
||||
self.terminal_title_preview_data(),
|
||||
self.app_event_tx.clone(),
|
||||
);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
}
|
||||
|
||||
fn status_surface_preview_data(&mut self) -> StatusSurfacePreviewData {
|
||||
StatusSurfacePreviewData::from_iter(StatusSurfacePreviewItem::iter().filter_map(|item| {
|
||||
self.status_surface_preview_value_for_item(item)
|
||||
.map(|value| (item, value))
|
||||
}))
|
||||
}
|
||||
|
||||
fn terminal_title_preview_data(&mut self) -> StatusSurfacePreviewData {
|
||||
let mut preview_data = self.status_surface_preview_data();
|
||||
let now = Instant::now();
|
||||
for item in [
|
||||
TerminalTitleItem::Project,
|
||||
TerminalTitleItem::Thread,
|
||||
TerminalTitleItem::GitBranch,
|
||||
TerminalTitleItem::Model,
|
||||
TerminalTitleItem::TaskProgress,
|
||||
] {
|
||||
let Some(preview_item) = item.preview_item() else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = self.terminal_title_value_for_item(item, now) else {
|
||||
continue;
|
||||
};
|
||||
preview_data.set_live(preview_item, value);
|
||||
}
|
||||
preview_data
|
||||
}
|
||||
|
||||
fn open_theme_picker(&mut self) {
|
||||
let codex_home = crate::legacy_core::config::find_codex_home().ok();
|
||||
let terminal_width = self
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: status_line_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Status Line
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project-root Project root directory (omitted when unavailable)
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] thread-title Current thread title (omitted unless changed by user)
|
||||
[ ] model-name Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
[ ] context-remaining Percentage of context window remaining (omitted when unknown)
|
||||
[ ] context-used Percentage of context window used (omitted when unknown)
|
||||
|
||||
my-project · feat/awesome-feature · thread title
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: status_line_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Status Line
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project-root Project root directory (omitted when unavailable)
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] thread-title Current thread title (omitted unless changed by user)
|
||||
[ ] model-name Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
[ ] context-remaining Percentage of context window remaining (omitted when unknown)
|
||||
[ ] context-used Percentage of context window used (omitted when unknown)
|
||||
|
||||
preview-live-root · feature/live-preview-branch · Live preview thread
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: status_line_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Status Line
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project-root Project root directory (omitted when unavailable)
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] thread-title Current thread title (omitted unless changed by user)
|
||||
[ ] model-name Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
[ ] context-remaining Percentage of context window remaining (omitted when unknown)
|
||||
[ ] context-used Percentage of context window used (omitted when unknown)
|
||||
|
||||
my-project · feature/mixed-preview · Mixed preview thread
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: snapshot
|
||||
---
|
||||
status line: my-project · feat/awesome-feature · thread title
|
||||
terminal title: thread title | feat/awesome-feature | Tasks 0/0
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: snapshot
|
||||
---
|
||||
status line: preview-live-root · feature/live-preview-branch · Live preview thread
|
||||
terminal title: preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: snapshot
|
||||
---
|
||||
status line: my-project · feature/mixed-preview · Mixed preview thread
|
||||
terminal title: project | Mixed preview thread | Tasks 0/0
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: terminal_title_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Terminal Title
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] thread Current thread title (omitted until available)
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] task-progress Latest task progress from update_plan (omitted until available)
|
||||
[ ] app-name Codex app name
|
||||
[ ] project Project name (falls back to current directory name)
|
||||
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
|
||||
[ ] status Compact session status text (Ready, Working, Thinking)
|
||||
[ ] model Current model name
|
||||
|
||||
thread title | feat/awesome-feature | Tasks 0/0
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: terminal_title_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Terminal Title
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project Project name (falls back to current directory name)
|
||||
[x] thread Current thread title (omitted until available)
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] task-progress Latest task progress from update_plan (omitted until available)
|
||||
[ ] app-name Codex app name
|
||||
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
|
||||
[ ] status Compact session status text (Ready, Working, Thinking)
|
||||
[ ] model Current model name
|
||||
|
||||
preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_surface_previews.rs
|
||||
expression: terminal_title_popup_snapshot(&mut chat)
|
||||
---
|
||||
Configure Terminal Title
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] project Project name (falls back to current directory name)
|
||||
[x] thread Current thread title (omitted until available)
|
||||
[x] task-progress Latest task progress from update_plan (omitted until available)
|
||||
[ ] app-name Codex app name
|
||||
[ ] spinner Animated task spinner (omitted while idle or when animations are off)
|
||||
[ ] status Compact session status text (Ready, Working, Thinking)
|
||||
[ ] git-branch Current Git branch (omitted when unavailable)
|
||||
[ ] model Current model name
|
||||
|
||||
project | Mixed preview thread | Tasks 0/0
|
||||
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel.
|
||||
@@ -505,11 +505,41 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn status_surface_preview_value_for_item(
|
||||
&mut self,
|
||||
item: StatusSurfacePreviewItem,
|
||||
) -> Option<String> {
|
||||
let status_line_item = match item {
|
||||
StatusSurfacePreviewItem::AppName => return Some("codex".to_string()),
|
||||
StatusSurfacePreviewItem::ProjectName => return self.terminal_title_project_name(),
|
||||
StatusSurfacePreviewItem::ProjectRoot => StatusLineItem::ProjectRoot,
|
||||
StatusSurfacePreviewItem::Status => return Some(self.terminal_title_status_text()),
|
||||
StatusSurfacePreviewItem::TaskProgress => return self.terminal_title_task_progress(),
|
||||
StatusSurfacePreviewItem::CurrentDir => StatusLineItem::CurrentDir,
|
||||
StatusSurfacePreviewItem::ThreadTitle => StatusLineItem::ThreadTitle,
|
||||
StatusSurfacePreviewItem::GitBranch => StatusLineItem::GitBranch,
|
||||
StatusSurfacePreviewItem::ContextRemaining => StatusLineItem::ContextRemaining,
|
||||
StatusSurfacePreviewItem::ContextUsed => StatusLineItem::ContextUsed,
|
||||
StatusSurfacePreviewItem::FiveHourLimit => StatusLineItem::FiveHourLimit,
|
||||
StatusSurfacePreviewItem::WeeklyLimit => StatusLineItem::WeeklyLimit,
|
||||
StatusSurfacePreviewItem::CodexVersion => StatusLineItem::CodexVersion,
|
||||
StatusSurfacePreviewItem::ContextWindowSize => StatusLineItem::ContextWindowSize,
|
||||
StatusSurfacePreviewItem::UsedTokens => StatusLineItem::UsedTokens,
|
||||
StatusSurfacePreviewItem::TotalInputTokens => StatusLineItem::TotalInputTokens,
|
||||
StatusSurfacePreviewItem::TotalOutputTokens => StatusLineItem::TotalOutputTokens,
|
||||
StatusSurfacePreviewItem::SessionId => StatusLineItem::SessionId,
|
||||
StatusSurfacePreviewItem::FastMode => StatusLineItem::FastMode,
|
||||
StatusSurfacePreviewItem::Model => StatusLineItem::ModelName,
|
||||
StatusSurfacePreviewItem::ModelWithReasoning => StatusLineItem::ModelWithReasoning,
|
||||
};
|
||||
self.status_line_value_for_item(&status_line_item)
|
||||
}
|
||||
|
||||
/// Resolves one configured terminal-title item into a displayable segment.
|
||||
///
|
||||
/// Returning `None` means "omit this segment for now" so callers can keep
|
||||
/// the configured order while hiding values that are not yet available.
|
||||
fn terminal_title_value_for_item(
|
||||
pub(super) fn terminal_title_value_for_item(
|
||||
&mut self,
|
||||
item: TerminalTitleItem,
|
||||
now: Instant,
|
||||
|
||||
@@ -276,6 +276,7 @@ mod side;
|
||||
mod slash_commands;
|
||||
mod status_and_layout;
|
||||
mod status_command_tests;
|
||||
mod status_surface_previews;
|
||||
|
||||
pub(crate) use helpers::make_chatwidget_manual_with_sender;
|
||||
pub(crate) use helpers::set_chatgpt_auth;
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
use super::*;
|
||||
use crate::bottom_pane::preview_line_for_title_items;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::text::Line;
|
||||
|
||||
fn line_text(line: Line<'static>) -> String {
|
||||
line.spans
|
||||
.into_iter()
|
||||
.map(|span| span.content.into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn status_preview_line(chat: &mut ChatWidget, items: &[StatusLineItem]) -> String {
|
||||
let preview_data = chat.status_surface_preview_data();
|
||||
let preview = preview_data
|
||||
.line_for_items(items.iter().cloned().map(StatusLineItem::preview_item))
|
||||
.expect("status preview line");
|
||||
line_text(preview)
|
||||
}
|
||||
|
||||
fn title_preview_line(chat: &mut ChatWidget, items: &[TerminalTitleItem]) -> String {
|
||||
let preview_data = chat.terminal_title_preview_data();
|
||||
let preview =
|
||||
preview_line_for_title_items(items, &preview_data).expect("terminal title preview line");
|
||||
line_text(preview)
|
||||
}
|
||||
|
||||
fn combined_preview_snapshot(
|
||||
chat: &mut ChatWidget,
|
||||
status_items: &[StatusLineItem],
|
||||
title_items: &[TerminalTitleItem],
|
||||
) -> String {
|
||||
normalize_snapshot_paths(format!(
|
||||
"status line: {}\nterminal title: {}",
|
||||
status_preview_line(chat, status_items),
|
||||
title_preview_line(chat, title_items),
|
||||
))
|
||||
}
|
||||
|
||||
fn status_line_popup_snapshot(chat: &mut ChatWidget) -> String {
|
||||
chat.open_status_line_setup();
|
||||
normalize_snapshot_paths(strip_osc8_for_snapshot(&render_bottom_popup(
|
||||
chat, /*width*/ 100,
|
||||
)))
|
||||
}
|
||||
|
||||
fn terminal_title_popup_snapshot(chat: &mut ChatWidget) -> String {
|
||||
chat.open_terminal_title_setup();
|
||||
normalize_snapshot_paths(strip_osc8_for_snapshot(&render_bottom_popup(
|
||||
chat, /*width*/ 100,
|
||||
)))
|
||||
}
|
||||
|
||||
fn cache_project_root(chat: &mut ChatWidget, root_name: &str) {
|
||||
chat.status_line_project_root_name_cache = Some(CachedProjectRootName {
|
||||
cwd: chat.config.cwd.to_path_buf(),
|
||||
root_name: Some(root_name.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_surface_preview_lines_live_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
cache_project_root(&mut chat, "preview-live-root");
|
||||
chat.status_line_branch = Some("feature/live-preview-branch".to_string());
|
||||
chat.thread_name = Some("Live preview thread".to_string());
|
||||
chat.last_plan_progress = Some((2, 5));
|
||||
|
||||
let snapshot = combined_preview_snapshot(
|
||||
&mut chat,
|
||||
&[
|
||||
StatusLineItem::ProjectRoot,
|
||||
StatusLineItem::GitBranch,
|
||||
StatusLineItem::ThreadTitle,
|
||||
],
|
||||
&[
|
||||
TerminalTitleItem::Project,
|
||||
TerminalTitleItem::Thread,
|
||||
TerminalTitleItem::GitBranch,
|
||||
TerminalTitleItem::TaskProgress,
|
||||
],
|
||||
);
|
||||
|
||||
assert_chatwidget_snapshot!("status_surface_previews_live_only", snapshot);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_setup_popup_live_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
cache_project_root(&mut chat, "preview-live-root");
|
||||
chat.status_line_branch = Some("feature/live-preview-branch".to_string());
|
||||
chat.thread_name = Some("Live preview thread".to_string());
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
"project-root".to_string(),
|
||||
"git-branch".to_string(),
|
||||
"thread-title".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"status_line_setup_popup_live_only",
|
||||
status_line_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_surface_preview_lines_hardcoded_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let snapshot = combined_preview_snapshot(
|
||||
&mut chat,
|
||||
&[
|
||||
StatusLineItem::ProjectRoot,
|
||||
StatusLineItem::GitBranch,
|
||||
StatusLineItem::ThreadTitle,
|
||||
],
|
||||
&[
|
||||
TerminalTitleItem::Thread,
|
||||
TerminalTitleItem::GitBranch,
|
||||
TerminalTitleItem::TaskProgress,
|
||||
],
|
||||
);
|
||||
|
||||
assert_chatwidget_snapshot!("status_surface_previews_hardcoded_only", snapshot);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_setup_popup_hardcoded_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
"project-root".to_string(),
|
||||
"git-branch".to_string(),
|
||||
"thread-title".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"status_line_setup_popup_hardcoded_only",
|
||||
status_line_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_surface_preview_lines_mixed_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.status_line_branch = Some("feature/mixed-preview".to_string());
|
||||
chat.thread_name = Some("Mixed preview thread".to_string());
|
||||
|
||||
let snapshot = combined_preview_snapshot(
|
||||
&mut chat,
|
||||
&[
|
||||
StatusLineItem::ProjectRoot,
|
||||
StatusLineItem::GitBranch,
|
||||
StatusLineItem::ThreadTitle,
|
||||
],
|
||||
&[
|
||||
TerminalTitleItem::Project,
|
||||
TerminalTitleItem::Thread,
|
||||
TerminalTitleItem::TaskProgress,
|
||||
],
|
||||
);
|
||||
|
||||
assert_chatwidget_snapshot!("status_surface_previews_mixed", snapshot);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_setup_popup_mixed_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.status_line_branch = Some("feature/mixed-preview".to_string());
|
||||
chat.thread_name = Some("Mixed preview thread".to_string());
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
"project-root".to_string(),
|
||||
"git-branch".to_string(),
|
||||
"thread-title".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"status_line_setup_popup_mixed",
|
||||
status_line_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_title_setup_popup_live_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
cache_project_root(&mut chat, "preview-live-root");
|
||||
chat.status_line_branch = Some("feature/live-preview-branch".to_string());
|
||||
chat.thread_name = Some("Live preview thread".to_string());
|
||||
chat.last_plan_progress = Some((2, 5));
|
||||
chat.config.tui_terminal_title = Some(vec![
|
||||
"project".to_string(),
|
||||
"thread".to_string(),
|
||||
"git-branch".to_string(),
|
||||
"task-progress".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"terminal_title_setup_popup_live_only",
|
||||
terminal_title_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_title_setup_popup_hardcoded_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.tui_terminal_title = Some(vec![
|
||||
"thread".to_string(),
|
||||
"git-branch".to_string(),
|
||||
"task-progress".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"terminal_title_setup_popup_hardcoded_only",
|
||||
terminal_title_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_title_setup_popup_mixed_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_name = Some("Mixed preview thread".to_string());
|
||||
chat.config.tui_terminal_title = Some(vec![
|
||||
"project".to_string(),
|
||||
"thread".to_string(),
|
||||
"task-progress".to_string(),
|
||||
]);
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"terminal_title_setup_popup_mixed",
|
||||
terminal_title_popup_snapshot(&mut chat)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_project_root_uses_different_status_and_title_preview_sources() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let status_preview = status_preview_line(&mut chat, &[StatusLineItem::ProjectRoot]);
|
||||
let title_preview = title_preview_line(&mut chat, &[TerminalTitleItem::Project]);
|
||||
|
||||
assert_eq!(status_preview, "my-project");
|
||||
assert_eq!(title_preview, "project");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_title_preview_uses_title_truncation_for_live_values() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let long_thread = "This thread title is intentionally much longer than forty-eight characters";
|
||||
let long_branch = "feature/this-branch-name-is-intentionally-longer-than-thirty-two";
|
||||
chat.thread_name = Some(long_thread.to_string());
|
||||
chat.status_line_branch = Some(long_branch.to_string());
|
||||
|
||||
let preview = title_preview_line(
|
||||
&mut chat,
|
||||
&[TerminalTitleItem::Thread, TerminalTitleItem::GitBranch],
|
||||
);
|
||||
let truncated_thread =
|
||||
ChatWidget::truncate_terminal_title_part(long_thread.to_string(), /*max_chars*/ 48);
|
||||
let truncated_branch =
|
||||
ChatWidget::truncate_terminal_title_part(long_branch.to_string(), /*max_chars*/ 32);
|
||||
|
||||
assert_eq!(preview, format!("{truncated_thread} | {truncated_branch}"));
|
||||
}
|
||||
Reference in New Issue
Block a user