mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Unify TUI picker styling and improve compact layouts (#46692)
## What changed - Apply shared picker styling to apps, plugins, settings, skills, hooks, and multi-select views, with full-width selection highlights and compact key hints. - Share row measurement and layout helpers, show scroll indicators, and let optional spacing yield on short screens to keep selected rows visible. - Wrap headers and footers, simplify search fields, and use a compact hooks event table at narrow widths. - Reserve plugin result rows while filtering and cap the plugin name column width. ## Testing Add regression coverage for selected rows in short multi-select views, compact hooks navigation, and checkbox, preview, and overflow rendering. Extend experimental-feature and memory-reset wrapping snapshots to narrower widths and update affected popup snapshots. GitOrigin-RevId: 0eedbbff307c84cc858629e578091bbbd59b2423
This commit is contained in:
@@ -629,7 +629,12 @@ impl AppLinkView {
|
||||
}
|
||||
.dim(),
|
||||
]));
|
||||
let url_line = Line::from(vec![self.url.clone().cyan().underlined()]);
|
||||
let url_line = Line::from(vec![
|
||||
self.url
|
||||
.clone()
|
||||
.fg(crate::style::accent_color())
|
||||
.underlined(),
|
||||
]);
|
||||
lines.extend(adaptive_wrap_lines(
|
||||
vec![url_line],
|
||||
RtOptions::new(usable_width),
|
||||
@@ -649,6 +654,8 @@ impl AppLinkView {
|
||||
' '
|
||||
};
|
||||
GenericDisplayRow {
|
||||
category_tag: None,
|
||||
selection_style: Some(crate::bottom_pane::selection_style()),
|
||||
name: format!("{prefix} {}. {label}", index + 1),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -669,19 +676,25 @@ impl AppLinkView {
|
||||
}
|
||||
|
||||
fn hint_line(&self) -> Line<'static> {
|
||||
let mut spans = vec!["Use ".into(), key_hint::plain(KeyCode::Tab).into()];
|
||||
let mut spans = vec![key_hint::plain(KeyCode::Tab).into()];
|
||||
if let Some(move_up) = self.list_keymap.primary_hint(ListAction::MoveUp) {
|
||||
spans.extend([" / ".into(), move_up.into()]);
|
||||
spans.push(" / ".into());
|
||||
spans.extend(move_up.spans());
|
||||
}
|
||||
if let Some(move_down) = self.list_keymap.primary_hint(ListAction::MoveDown) {
|
||||
spans.extend([" ".into(), move_down.into()]);
|
||||
spans.push(" ".into());
|
||||
spans.extend(move_down.spans());
|
||||
}
|
||||
spans.push(" to move".into());
|
||||
spans.push(" move".into());
|
||||
if let Some(accept) = self.list_keymap.primary_hint(ListAction::Accept) {
|
||||
spans.extend([", ".into(), accept.into(), " to select".into()]);
|
||||
spans.push(" · ".into());
|
||||
spans.extend(accept.spans());
|
||||
spans.push(" select".into());
|
||||
}
|
||||
if let Some(cancel) = self.list_keymap.primary_hint(ListAction::Cancel) {
|
||||
spans.extend([", ".into(), cancel.into(), " to close".into()]);
|
||||
spans.push(" · ".into());
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" close".into());
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
@@ -811,8 +824,11 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(content_width)
|
||||
.max(1) as u16;
|
||||
let action_rows_height = self.action_rows_height(content_width);
|
||||
content_rows + action_rows_height + 3
|
||||
let action_rows_height = self.action_rows_height(width);
|
||||
let hint_rows = Paragraph::new(self.hint_line())
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(width.saturating_sub(/*rhs*/ 2)) as u16;
|
||||
content_rows + action_rows_height + hint_rows + 2
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -824,11 +840,15 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
||||
.style(user_message_style())
|
||||
.render(area, buf);
|
||||
|
||||
let actions_height = self.action_rows_height(area.width.saturating_sub(4));
|
||||
let actions_height = self.action_rows_height(area.width);
|
||||
let hint = Paragraph::new(self.hint_line())
|
||||
.dim()
|
||||
.wrap(Wrap { trim: false });
|
||||
let hint_height = hint.line_count(area.width.saturating_sub(/*rhs*/ 2)) as u16;
|
||||
let [content_area, actions_area, hint_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(actions_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(hint_height),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
@@ -841,12 +861,6 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
||||
crate::terminal_hyperlinks::mark_url_hyperlink(buf, inner, &self.url);
|
||||
|
||||
if actions_area.height > 0 {
|
||||
let actions_area = Rect {
|
||||
x: actions_area.x.saturating_add(2),
|
||||
y: actions_area.y,
|
||||
width: actions_area.width.saturating_sub(2),
|
||||
height: actions_area.height,
|
||||
};
|
||||
let action_rows = self.action_rows();
|
||||
let action_state = self.action_state();
|
||||
render_rows(
|
||||
@@ -866,7 +880,7 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
||||
width: hint_area.width.saturating_sub(2),
|
||||
height: hint_area.height,
|
||||
};
|
||||
self.hint_line().dim().render(hint_area, buf);
|
||||
hint.render(hint_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -30,19 +32,17 @@ use crate::key_hint;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::keymap::ListAction;
|
||||
use crate::keymap::ListKeymap;
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt as _;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::style::user_message_style;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::picker_rows::measure_rows_height;
|
||||
use super::picker_rows::render_rows;
|
||||
use super::popup_consts::MAX_POPUP_ROWS;
|
||||
use super::scroll_state::ScrollState;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::measure_rows_height;
|
||||
use super::selection_popup_common::render_rows;
|
||||
|
||||
pub(crate) struct ExperimentalFeatureItem {
|
||||
pub key: String,
|
||||
@@ -102,13 +102,15 @@ impl ExperimentalFeaturesView {
|
||||
|
||||
fn header(&self, width: u16) -> impl Renderable {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Experimental features".bold()));
|
||||
header.push(
|
||||
Paragraph::new(Line::from("Experimental features".bold())).wrap(Wrap { trim: false }),
|
||||
);
|
||||
for text in [
|
||||
"Checked features are configured on. Some experimental features take effect only in new tasks or after restarting the Codex server.",
|
||||
self.discovery_status.as_str(),
|
||||
].into_iter().filter(|text| !text.is_empty()) {
|
||||
for line in textwrap::wrap(text, usize::from(width.max(1))) {
|
||||
header.push(Line::from(line.into_owned().dim()));
|
||||
header.push(Paragraph::new(Line::from(line.into_owned().dim())).wrap(Wrap { trim: false }));
|
||||
}
|
||||
}
|
||||
header
|
||||
@@ -122,6 +124,16 @@ impl ExperimentalFeaturesView {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_hint(&self) -> Line<'static> {
|
||||
if self.write_rx.is_some() {
|
||||
Line::from("Saving… Closing this popup will not cancel the write.")
|
||||
} else if !self.unconfirmed.is_empty() {
|
||||
Line::from("Selections retained. Save to retry, or cancel to close.")
|
||||
} else {
|
||||
self.footer_hint.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_len(&self) -> usize {
|
||||
self.features.len()
|
||||
}
|
||||
@@ -139,6 +151,9 @@ impl ExperimentalFeaturesView {
|
||||
let read_only = if item.writable { "" } else { " (read-only)" };
|
||||
let name = format!("{prefix} [{marker}] {}{read_only}", item.name);
|
||||
rows.push(GenericDisplayRow {
|
||||
category_tag: None,
|
||||
selection_style: Some(super::picker_style::selection_style()),
|
||||
wrap_indent: Some(6),
|
||||
name,
|
||||
description: Some(item.description.clone()),
|
||||
is_disabled: !item.writable || self.write_rx.is_some(),
|
||||
@@ -256,10 +271,6 @@ impl ExperimentalFeaturesView {
|
||||
self.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn rows_width(total_width: u16) -> u16 {
|
||||
total_width.saturating_sub(2)
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomPaneView for ExperimentalFeaturesView {
|
||||
@@ -410,8 +421,16 @@ impl Renderable for ExperimentalFeaturesView {
|
||||
return;
|
||||
}
|
||||
|
||||
let [content_area, footer_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
let hint = self.current_hint();
|
||||
let hint_lines = super::selection_popup_common::wrap_styled_line(
|
||||
&hint,
|
||||
area.width.saturating_sub(/*rhs*/ 2),
|
||||
);
|
||||
let [content_area, footer_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(hint_lines.len() as u16),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
Block::default()
|
||||
.style(user_message_style())
|
||||
@@ -420,24 +439,14 @@ impl Renderable for ExperimentalFeaturesView {
|
||||
let header = self.header(content_area.width.saturating_sub(4));
|
||||
let header_height = header.desired_height(content_area.width.saturating_sub(4));
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(content_area.width);
|
||||
let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, rows_width);
|
||||
let [header_area, _, list_area] = Layout::vertical([
|
||||
Constraint::Max(header_height),
|
||||
Constraint::Max(1),
|
||||
Constraint::Length(rows_height),
|
||||
])
|
||||
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
|
||||
let rows_height =
|
||||
measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, content_area.width);
|
||||
let [header_area, _, render_area] =
|
||||
super::picker_rows::layout(content_area, header_height, /*search*/ 0, rows_height);
|
||||
|
||||
header.render(header_area, buf);
|
||||
|
||||
if list_area.height > 0 && (!rows.is_empty() || self.catalog_rx.is_none()) {
|
||||
let render_area = Rect {
|
||||
x: list_area.x.saturating_sub(2),
|
||||
y: list_area.y,
|
||||
width: rows_width.max(1),
|
||||
height: list_area.height,
|
||||
};
|
||||
if render_area.height > 0 && (!rows.is_empty() || self.catalog_rx.is_none()) {
|
||||
render_rows(
|
||||
render_area,
|
||||
buf,
|
||||
@@ -454,37 +463,38 @@ impl Renderable for ExperimentalFeaturesView {
|
||||
width: footer_area.width.saturating_sub(2),
|
||||
height: footer_area.height,
|
||||
};
|
||||
let hint = if self.write_rx.is_some() {
|
||||
Line::from("Saving… Closing this popup will not cancel the write.")
|
||||
} else if !self.unconfirmed.is_empty() {
|
||||
Line::from("Selections retained. Save to retry, or cancel to close.")
|
||||
} else {
|
||||
self.footer_hint.clone()
|
||||
};
|
||||
hint.dim().render(hint_area, buf);
|
||||
Paragraph::new(hint_lines).dim().render(hint_area, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(width);
|
||||
let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, rows_width);
|
||||
let rows_height = measure_rows_height(&rows, &self.state, MAX_POPUP_ROWS, width);
|
||||
|
||||
let mut height = self
|
||||
.header(width.saturating_sub(4))
|
||||
.desired_height(width.saturating_sub(4));
|
||||
height = height.saturating_add(rows_height + 3);
|
||||
height.saturating_add(1)
|
||||
height.saturating_add(
|
||||
super::selection_popup_common::wrap_styled_line(
|
||||
&self.current_hint(),
|
||||
width.saturating_sub(/*rhs*/ 2),
|
||||
)
|
||||
.len() as u16,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn experimental_popup_hint_line(keymap: &ListKeymap) -> Line<'static> {
|
||||
let mut spans = vec![
|
||||
"Press ".into(),
|
||||
key_hint::plain(KeyCode::Char(' ')).into(),
|
||||
" to select".into(),
|
||||
];
|
||||
let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".into()];
|
||||
if let Some(accept) = keymap.primary_hint(ListAction::Accept) {
|
||||
spans.extend([" or ".into(), accept.into(), " to save".into()]);
|
||||
spans.push(" · ".into());
|
||||
spans.extend(accept.spans());
|
||||
spans.push(" save".into());
|
||||
}
|
||||
if let Some(cancel) = keymap.primary_hint(ListAction::Cancel) {
|
||||
spans.push(" · ".into());
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" save/close".into());
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ fn experimental_features_keep_following_choice_at_wrap_boundary() {
|
||||
);
|
||||
|
||||
let mut snapshots = Vec::new();
|
||||
for width in [70, 71, 72] {
|
||||
for width in [68, 69, 70, 71, 72] {
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! Responsive hook browsing presentation; trust and toggle policies stay in the view.
|
||||
|
||||
use super::*;
|
||||
use crate::bottom_pane::picker_style;
|
||||
use crate::bottom_pane::selection_popup_common::RenderedRows;
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
impl HooksBrowserView {
|
||||
// Keep the existing yellow warning treatment for hooks awaiting review.
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
fn event_parts(&self, width: u16) -> (Vec<Line<'static>>, Vec<Line<'static>>) {
|
||||
let events = self.event_rows();
|
||||
let mut header = self.event_page_lines();
|
||||
let mut rows = header.split_off(header.len().saturating_sub(events.len()));
|
||||
if width < 72 {
|
||||
header.pop();
|
||||
header.push("Event Active/Installed".dim().into());
|
||||
rows = events
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, row)| {
|
||||
let name = event_label(row.event_name);
|
||||
let review = if row.needs_review > 0 { " !" } else { "" };
|
||||
let line = Line::from(format!(
|
||||
"{name:<18} {}/{}{review}",
|
||||
row.active, row.installed
|
||||
));
|
||||
if self.state.selected_idx == Some(index) {
|
||||
line.style(selection_style())
|
||||
} else if row.needs_review > 0 {
|
||||
line.yellow()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
for (index, line) in rows.iter_mut().enumerate() {
|
||||
let prefix = if self.state.selected_idx == Some(index) {
|
||||
"› "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
line.spans.insert(/*index*/ 0, prefix.into());
|
||||
}
|
||||
(header, rows)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_line_rows(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
rows: Vec<Line<'static>>,
|
||||
mut state: ScrollState,
|
||||
) {
|
||||
let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0));
|
||||
let visible = usize::from(body.height);
|
||||
state.ensure_visible(rows.len(), visible.max(/*other*/ 1));
|
||||
for (offset, line) in rows.iter().skip(state.scroll_top).take(visible).enumerate() {
|
||||
let row_area = Rect::new(
|
||||
body.x,
|
||||
body.y + offset as u16,
|
||||
body.width,
|
||||
/*height*/ 1,
|
||||
);
|
||||
line.clone().render(row_area, buf);
|
||||
if line.style.bg.is_some() {
|
||||
buf.set_style(row_area, line.style);
|
||||
}
|
||||
}
|
||||
picker_style::render_scroll_indicators(
|
||||
area,
|
||||
buf,
|
||||
RenderedRows {
|
||||
has_above: state.scroll_top > 0,
|
||||
has_below: state.scroll_top + visible < rows.len(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
impl Renderable for HooksBrowserView {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let inner_width = width.saturating_sub(/*rhs*/ 4).max(/*other*/ 1);
|
||||
let (header, rows, detail) = match self.page {
|
||||
HooksBrowserPage::Events => {
|
||||
let (header, rows) = self.event_parts(inner_width);
|
||||
(header, rows.len(), 0)
|
||||
}
|
||||
HooksBrowserPage::Handlers(event) => {
|
||||
let rows = self.handlers_for_event(event).count();
|
||||
let detail = if rows == 0 {
|
||||
0
|
||||
} else {
|
||||
self.detail_lines(event, usize::from(inner_width)).len() + 1
|
||||
};
|
||||
(
|
||||
Self::handler_header_lines(event, self.review_needed_count(event)),
|
||||
rows,
|
||||
detail,
|
||||
)
|
||||
}
|
||||
};
|
||||
let header_height = Paragraph::new(header)
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(inner_width);
|
||||
u16::try_from(header_height + rows.clamp(/*min*/ 1, MAX_POPUP_ROWS) + detail + 5)
|
||||
.unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let [panel, footer] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
let content = render_menu_surface(panel, buf);
|
||||
if content.is_empty() {
|
||||
self.render_footer(footer, buf);
|
||||
return;
|
||||
}
|
||||
let (header, rows) = match self.page {
|
||||
HooksBrowserPage::Events => self.event_parts(content.width),
|
||||
HooksBrowserPage::Handlers(event) => (
|
||||
Self::handler_header_lines(event, self.review_needed_count(event)),
|
||||
self.handler_row_lines(event, usize::from(area.width)),
|
||||
),
|
||||
};
|
||||
let header = Paragraph::new(header).wrap(Wrap { trim: false });
|
||||
let header_height =
|
||||
(header.line_count(content.width) as u16).min(content.height.saturating_sub(/*rhs*/ 3));
|
||||
let [header_area, body] =
|
||||
Layout::vertical([Constraint::Length(header_height), Constraint::Fill(1)])
|
||||
.areas(content);
|
||||
header.render(header_area, buf);
|
||||
let body = Rect::new(area.x, body.y, area.width, body.height);
|
||||
match self.page {
|
||||
HooksBrowserPage::Events => render_line_rows(body, buf, rows, self.state),
|
||||
HooksBrowserPage::Handlers(event) => {
|
||||
if rows.is_empty() {
|
||||
Paragraph::new(" No hooks installed for this event.")
|
||||
.dim()
|
||||
.render(body, buf);
|
||||
} else {
|
||||
let desired = rows.len().min(MAX_POPUP_ROWS) as u16 + 2;
|
||||
let list_height = desired
|
||||
.min(body.height.saturating_sub(/*rhs*/ 3).max(/*other*/ 3))
|
||||
.min(body.height);
|
||||
let [list, details] =
|
||||
Layout::vertical([Constraint::Length(list_height), Constraint::Fill(1)])
|
||||
.areas(body);
|
||||
render_line_rows(list, buf, rows, self.state);
|
||||
Paragraph::new(self.detail_lines(event, usize::from(content.width)))
|
||||
.render(details.inset(Insets::vh(/*v*/ 0, /*h*/ 2)), buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.render_footer(footer, buf);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::picker_style::selection_style;
|
||||
use super::popup_consts::MAX_POPUP_ROWS;
|
||||
use super::scroll_state::ScrollState;
|
||||
use super::selection_popup_common::render_menu_surface;
|
||||
@@ -36,7 +37,6 @@ use crate::keymap::ListKeymap;
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::status::format_directory_display;
|
||||
use crate::style::accent_style;
|
||||
|
||||
const EVENT_COLUMN_WIDTH: usize = 22;
|
||||
const COUNT_COLUMN_WIDTH: usize = 12;
|
||||
@@ -371,7 +371,7 @@ impl HooksBrowserView {
|
||||
row_line.push(Span::from(event_description(row.event_name)));
|
||||
|
||||
if selected {
|
||||
let style = accent_style();
|
||||
let style = selection_style();
|
||||
for span in &mut row_line {
|
||||
*span = span.clone().set_style(style);
|
||||
}
|
||||
@@ -384,7 +384,11 @@ impl HooksBrowserView {
|
||||
let description_idx = row_line.len() - 1;
|
||||
row_line[description_idx] = row_line[description_idx].clone().dim();
|
||||
}
|
||||
lines.push(Line::from(row_line));
|
||||
lines.push(if selected {
|
||||
Line::from(row_line).style(selection_style())
|
||||
} else {
|
||||
Line::from(row_line)
|
||||
});
|
||||
}
|
||||
lines
|
||||
}
|
||||
@@ -451,15 +455,16 @@ impl HooksBrowserView {
|
||||
format!("[{marker}] {}", hook_title(idx))
|
||||
}
|
||||
};
|
||||
let mut line = Line::from(row);
|
||||
let prefix = if self.state.selected_idx == Some(idx) {
|
||||
"› "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let mut line = Line::from(format!("{prefix}{row}"));
|
||||
line = truncate_line_with_ellipsis_if_overflow(line, width);
|
||||
let needs_review = hook_needs_review(hook);
|
||||
if self.state.selected_idx == Some(idx) {
|
||||
if needs_review {
|
||||
line = line.yellow().bold();
|
||||
} else {
|
||||
line = line.patch_style(accent_style());
|
||||
}
|
||||
line = line.patch_style(selection_style());
|
||||
} else if needs_review {
|
||||
line = line.yellow();
|
||||
} else if hook.is_managed {
|
||||
@@ -540,56 +545,45 @@ impl HooksBrowserView {
|
||||
.keymap
|
||||
.primary_hint(ListAction::Cancel)
|
||||
.unwrap_or_else(|| key_hint::plain(KeyCode::Esc).into());
|
||||
let footer = match self.page {
|
||||
HooksBrowserPage::Events if self.review_needed_total_count() > 0 => {
|
||||
let mut spans = vec![
|
||||
"Press ".into(),
|
||||
key_hint::plain(KeyCode::Char('t')).into(),
|
||||
" to trust all; ".into(),
|
||||
];
|
||||
if let Some(accept) = accept {
|
||||
spans.extend([accept.into(), " to review hooks; ".into()]);
|
||||
}
|
||||
spans.extend([cancel.into(), " to close".into()]);
|
||||
Line::from(spans)
|
||||
}
|
||||
let mut spans = Vec::new();
|
||||
match self.page {
|
||||
HooksBrowserPage::Events => {
|
||||
let mut spans = vec!["Press ".into()];
|
||||
if let Some(accept) = accept {
|
||||
spans.extend([accept.into(), " to view hooks; ".into()]);
|
||||
if self.review_needed_total_count() > 0 {
|
||||
spans.extend(key_hint::plain(KeyCode::Char('t')).spans());
|
||||
spans.push(" trust all · ".dim());
|
||||
if let Some(accept) = accept {
|
||||
spans.extend(accept.spans());
|
||||
spans.push(" review · ".dim());
|
||||
}
|
||||
} else if let Some(accept) = accept {
|
||||
spans.extend(accept.spans());
|
||||
spans.push(" details · ".dim());
|
||||
}
|
||||
spans.extend([cancel.into(), " to close".into()]);
|
||||
Line::from(spans)
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" close".dim());
|
||||
}
|
||||
HooksBrowserPage::Handlers(event_name) => {
|
||||
let selected_hook = self.selected_hook(event_name);
|
||||
if selected_hook.is_none() {
|
||||
Line::from(vec!["Press ".into(), cancel.into(), " to go back".into()])
|
||||
} else if selected_hook.is_some_and(|hook| hook.is_managed) {
|
||||
Line::from(vec![
|
||||
"Managed hooks are always on; press ".into(),
|
||||
cancel.into(),
|
||||
" to go back".into(),
|
||||
])
|
||||
} else if selected_hook.is_some_and(hook_needs_review) {
|
||||
Line::from(vec![
|
||||
"Press ".into(),
|
||||
key_hint::plain(KeyCode::Char('t')).into(),
|
||||
" to trust; ".into(),
|
||||
cancel.into(),
|
||||
" to go back".into(),
|
||||
])
|
||||
} else {
|
||||
let mut spans =
|
||||
vec!["Press ".into(), key_hint::plain(KeyCode::Char(' ')).into()];
|
||||
if let Some(accept) = accept {
|
||||
spans.extend([" or ".into(), accept.into()]);
|
||||
if let Some(hook) = self.selected_hook(event_name) {
|
||||
if hook.is_managed {
|
||||
spans.push("Managed (always on) · ".dim());
|
||||
} else if hook_needs_review(hook) {
|
||||
spans.extend(key_hint::plain(KeyCode::Char('t')).spans());
|
||||
spans.push(" trust · ".dim());
|
||||
} else {
|
||||
let mut toggle = key_hint::plain(KeyCode::Char(' ')).display_label();
|
||||
if let Some(accept) = accept {
|
||||
toggle.push('/');
|
||||
toggle.push_str(&accept.display_label());
|
||||
}
|
||||
spans.extend(key_hint::key_label_spans(&toggle));
|
||||
spans.push(" toggle · ".dim());
|
||||
}
|
||||
spans.extend([" to toggle; ".into(), cancel.into(), " to go back".into()]);
|
||||
Line::from(spans)
|
||||
}
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" back".dim());
|
||||
}
|
||||
};
|
||||
}
|
||||
let footer = Line::from(spans);
|
||||
footer.dim().render(hint_area, buf);
|
||||
}
|
||||
}
|
||||
@@ -656,83 +650,8 @@ impl BottomPaneView for HooksBrowserView {
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for HooksBrowserView {
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
let content_width = width.saturating_sub(4) as usize;
|
||||
let height = match self.page {
|
||||
HooksBrowserPage::Events => self.event_page_lines().len(),
|
||||
HooksBrowserPage::Handlers(event_name) => {
|
||||
let row_count = self.handler_row_lines(event_name, content_width).len();
|
||||
let header_line_count =
|
||||
Self::handler_header_lines(event_name, self.review_needed_count(event_name))
|
||||
.len();
|
||||
if row_count == 0 {
|
||||
header_line_count + 2
|
||||
} else {
|
||||
let visible_row_count = row_count.min(MAX_POPUP_ROWS);
|
||||
header_line_count
|
||||
+ 1
|
||||
+ visible_row_count
|
||||
+ 1
|
||||
+ self.detail_lines(event_name, content_width).len()
|
||||
}
|
||||
}
|
||||
};
|
||||
(height + 3).try_into().unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let [content_area, footer_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
let content_area = render_menu_surface(content_area, buf);
|
||||
let width = content_area.width as usize;
|
||||
let lines = match self.page {
|
||||
HooksBrowserPage::Events => self.event_page_lines(),
|
||||
HooksBrowserPage::Handlers(event_name) => {
|
||||
let mut lines =
|
||||
Self::handler_header_lines(event_name, self.review_needed_count(event_name));
|
||||
let rows = self.handler_row_lines(event_name, width);
|
||||
if rows.is_empty() {
|
||||
lines.push(Line::default());
|
||||
lines.push(Line::from(
|
||||
"No hooks installed for this event.".dim().italic(),
|
||||
));
|
||||
lines.push(Line::default());
|
||||
Paragraph::new(lines).render(content_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
return;
|
||||
}
|
||||
let list_height = rows.len().clamp(1, MAX_POPUP_ROWS) as u16;
|
||||
lines.push(Line::default());
|
||||
let header_height = lines.len() as u16;
|
||||
let [header_area, list_area, detail_area] = Layout::vertical([
|
||||
Constraint::Length(header_height),
|
||||
Constraint::Length(list_height),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(content_area);
|
||||
Paragraph::new(lines.clone()).render(header_area, buf);
|
||||
let visible_rows = rows
|
||||
.into_iter()
|
||||
.skip(self.state.scroll_top)
|
||||
.take(list_height as usize)
|
||||
.collect::<Vec<_>>();
|
||||
Paragraph::new(visible_rows).render(list_area, buf);
|
||||
let mut detail_lines = vec![Line::default()];
|
||||
detail_lines.extend(self.detail_lines(event_name, width));
|
||||
Paragraph::new(detail_lines).render(detail_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
return;
|
||||
}
|
||||
};
|
||||
Paragraph::new(lines).render(content_area, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
#[path = "hooks_browser_render.rs"]
|
||||
mod render;
|
||||
|
||||
fn hook_is_active(hook: &HookMetadata) -> bool {
|
||||
hook.enabled
|
||||
@@ -1045,14 +964,44 @@ mod tests {
|
||||
let mut view = view();
|
||||
assert_snapshot!("hooks_browser_events", render_lines(&view, /*width*/ 112));
|
||||
view.keymap.accept.clear();
|
||||
assert!(!render_lines(&view, /*width*/ 112).contains("enter to view hooks"));
|
||||
assert!(!render_lines(&view, /*width*/ 112).contains("enter details"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_event_rows_use_the_shared_accent_style() {
|
||||
fn compact_hooks_keep_the_last_event_and_selection_visible() {
|
||||
let mut view = view();
|
||||
view.state.selected_idx = Some(view.event_rows().len() - 1);
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 40, /*height*/ 16,
|
||||
);
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
let text = (0..area.height)
|
||||
.map(|y| {
|
||||
(0..area.width)
|
||||
.map(|x| buf[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!(text);
|
||||
let selected_y = text
|
||||
.lines()
|
||||
.position(|line| line.starts_with("› "))
|
||||
.unwrap() as u16;
|
||||
assert_eq!(buf[(0, selected_y)].bg, selection_style().bg.unwrap());
|
||||
assert_eq!(buf[(39, selected_y)].bg, selection_style().bg.unwrap());
|
||||
assert!(text.contains("Interrupt"));
|
||||
assert!(text.contains("Active/Installed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_event_rows_use_the_shared_selection_style() {
|
||||
let view = view();
|
||||
let buf = render_buffer(&view, /*width*/ 112);
|
||||
let expected = accent_style();
|
||||
let expected = selection_style();
|
||||
|
||||
let selected_cell = buf
|
||||
.content
|
||||
@@ -1093,7 +1042,16 @@ mod tests {
|
||||
"hooks_browser_events_with_review_column",
|
||||
render_lines(&view, /*width*/ 112)
|
||||
);
|
||||
assert_eq!(view.event_table_lines()[1].spans[3].style, accent_style());
|
||||
assert_eq!(
|
||||
view.event_table_lines()[1].spans[3].style.fg,
|
||||
selection_style().fg
|
||||
);
|
||||
assert!(
|
||||
view.event_table_lines()[1].spans[3]
|
||||
.style
|
||||
.add_modifier
|
||||
.contains(ratatui::style::Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,11 +8,13 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap;
|
||||
use crate::bottom_pane::popup_consts::picker_hint_line_for_keymap;
|
||||
use crate::key_hint;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::keymap::ListAction;
|
||||
@@ -25,11 +27,11 @@ use crate::style::user_message_style;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::picker_rows::measure_rows_height;
|
||||
use super::picker_rows::render_rows;
|
||||
use super::popup_consts::MAX_POPUP_ROWS;
|
||||
use super::scroll_state::ScrollState;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::measure_rows_height;
|
||||
use super::selection_popup_common::render_rows;
|
||||
|
||||
const MEMORIES_DOC_URL: &str = "https://developers.openai.com/codex/memories";
|
||||
|
||||
@@ -101,7 +103,9 @@ impl MemoriesSettingsView {
|
||||
app_event_tx,
|
||||
docs_link: Line::from(vec![
|
||||
"Learn more: ".dim(),
|
||||
MEMORIES_DOC_URL.cyan().underlined(),
|
||||
MEMORIES_DOC_URL
|
||||
.fg(crate::style::accent_color())
|
||||
.underlined(),
|
||||
]),
|
||||
keymap,
|
||||
};
|
||||
@@ -115,20 +119,29 @@ impl MemoriesSettingsView {
|
||||
|
||||
fn settings_header(&self) -> ColumnRenderable<'_> {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Memories".bold()));
|
||||
header.push(Line::from(
|
||||
"Choose how Codex uses and creates memories. Changes are saved to config.toml".dim(),
|
||||
));
|
||||
header.push(Paragraph::new(Line::from("Memories".bold())).wrap(Wrap { trim: false }));
|
||||
header.push(
|
||||
Paragraph::new(Line::from(
|
||||
"Choose how Codex uses and creates memories. Changes are saved to config.toml"
|
||||
.dim(),
|
||||
))
|
||||
.wrap(Wrap { trim: false }),
|
||||
);
|
||||
header
|
||||
}
|
||||
|
||||
fn reset_confirmation_header(&self) -> ColumnRenderable<'_> {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Reset all memories?".bold()));
|
||||
header.push(Line::from(
|
||||
"This clears local memory files and rollout summaries for the current Codex home."
|
||||
.dim(),
|
||||
));
|
||||
header.push(
|
||||
Paragraph::new(Line::from("Reset all memories?".bold())).wrap(Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
Paragraph::new(Line::from(
|
||||
"This clears local memory files and rollout summaries for the current Codex home."
|
||||
.dim(),
|
||||
))
|
||||
.wrap(Wrap { trim: false }),
|
||||
);
|
||||
header
|
||||
}
|
||||
|
||||
@@ -154,6 +167,9 @@ impl MemoriesSettingsView {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, name)| GenericDisplayRow {
|
||||
category_tag: None,
|
||||
selection_style: Some(super::picker_style::selection_style()),
|
||||
wrap_indent: Some(2),
|
||||
name: if state.selected_idx == Some(idx) {
|
||||
format!("› {name}")
|
||||
} else {
|
||||
@@ -194,6 +210,13 @@ impl MemoriesSettingsView {
|
||||
} => (format!("{prefix} {name}"), description),
|
||||
};
|
||||
GenericDisplayRow {
|
||||
category_tag: None,
|
||||
selection_style: Some(super::picker_style::selection_style()),
|
||||
wrap_indent: Some(if matches!(item, MemoriesMenuItem::Setting { .. }) {
|
||||
6
|
||||
} else {
|
||||
2
|
||||
}),
|
||||
name,
|
||||
description: Some((*description).to_string()),
|
||||
..Default::default()
|
||||
@@ -260,10 +283,6 @@ impl MemoriesSettingsView {
|
||||
}
|
||||
}
|
||||
|
||||
fn rows_width(total_width: u16) -> u16 {
|
||||
total_width.saturating_sub(2)
|
||||
}
|
||||
|
||||
fn current_setting(&self, setting: MemoriesSetting) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
@@ -291,7 +310,7 @@ impl MemoriesSettingsView {
|
||||
|
||||
fn footer_hint(&self) -> Line<'static> {
|
||||
if self.reset_confirmation.is_some() {
|
||||
standard_popup_hint_line_for_keymap(&self.keymap)
|
||||
picker_hint_line_for_keymap(&self.keymap)
|
||||
} else {
|
||||
memories_settings_hint_line(&self.keymap)
|
||||
}
|
||||
@@ -376,8 +395,16 @@ impl Renderable for MemoriesSettingsView {
|
||||
return;
|
||||
}
|
||||
|
||||
let [content_area, footer_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
let hint = self.footer_hint();
|
||||
let hint_lines = super::selection_popup_common::wrap_styled_line(
|
||||
&hint,
|
||||
area.width.saturating_sub(/*rhs*/ 2),
|
||||
);
|
||||
let [content_area, footer_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(hint_lines.len() as u16),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
Block::default()
|
||||
.style(user_message_style())
|
||||
@@ -390,27 +417,26 @@ impl Renderable for MemoriesSettingsView {
|
||||
};
|
||||
let header_height = header.desired_height(content_area.width.saturating_sub(4));
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(content_area.width);
|
||||
let rows_height =
|
||||
measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, rows_width);
|
||||
let [header_area, _, list_area, _, docs_area] = Layout::vertical([
|
||||
Constraint::Max(header_height),
|
||||
Constraint::Max(1),
|
||||
Constraint::Length(rows_height),
|
||||
Constraint::Max(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
|
||||
let rows_height = measure_rows_height(
|
||||
&rows,
|
||||
self.active_state(),
|
||||
MAX_POPUP_ROWS,
|
||||
content_area.width,
|
||||
);
|
||||
let docs_height = if self.reset_confirmation.is_some() {
|
||||
0
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let [body_area, docs_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(docs_height)])
|
||||
.areas(content_area);
|
||||
let [header_area, _, render_area] =
|
||||
super::picker_rows::layout(body_area, header_height, /*search*/ 0, rows_height);
|
||||
|
||||
header.render(header_area, buf);
|
||||
|
||||
if list_area.height > 0 {
|
||||
let render_area = Rect {
|
||||
x: list_area.x.saturating_sub(2),
|
||||
y: list_area.y,
|
||||
width: rows_width.max(1),
|
||||
height: list_area.height,
|
||||
};
|
||||
if render_area.height > 0 {
|
||||
render_rows(
|
||||
render_area,
|
||||
buf,
|
||||
@@ -421,6 +447,9 @@ impl Renderable for MemoriesSettingsView {
|
||||
);
|
||||
}
|
||||
if self.reset_confirmation.is_none() {
|
||||
let docs_area = docs_area.inset(Insets::tlbr(
|
||||
/*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 2,
|
||||
));
|
||||
self.docs_link.clone().render(docs_area, buf);
|
||||
crate::terminal_hyperlinks::mark_url_hyperlink(buf, docs_area, MEMORIES_DOC_URL);
|
||||
}
|
||||
@@ -431,7 +460,7 @@ impl Renderable for MemoriesSettingsView {
|
||||
width: footer_area.width.saturating_sub(2),
|
||||
height: footer_area.height,
|
||||
};
|
||||
self.footer_hint().render(hint_area, buf);
|
||||
Paragraph::new(hint_lines).dim().render(hint_area, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
@@ -441,9 +470,7 @@ impl Renderable for MemoriesSettingsView {
|
||||
self.settings_header()
|
||||
};
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(width);
|
||||
let rows_height =
|
||||
measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, rows_width);
|
||||
let rows_height = measure_rows_height(&rows, self.active_state(), MAX_POPUP_ROWS, width);
|
||||
|
||||
let docs_height = if self.reset_confirmation.is_some() {
|
||||
0
|
||||
@@ -452,18 +479,27 @@ impl Renderable for MemoriesSettingsView {
|
||||
};
|
||||
let mut height = header.desired_height(width.saturating_sub(4));
|
||||
height = height.saturating_add(rows_height + 4 + docs_height);
|
||||
height.saturating_add(1)
|
||||
height.saturating_add(
|
||||
super::selection_popup_common::wrap_styled_line(
|
||||
&self.footer_hint(),
|
||||
width.saturating_sub(/*rhs*/ 2),
|
||||
)
|
||||
.len() as u16,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn memories_settings_hint_line(keymap: &ListKeymap) -> Line<'static> {
|
||||
let mut spans = vec![
|
||||
"Press ".into(),
|
||||
key_hint::plain(KeyCode::Char(' ')).into(),
|
||||
" to toggle".into(),
|
||||
];
|
||||
let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".into()];
|
||||
if let Some(accept) = keymap.primary_hint(ListAction::Accept) {
|
||||
spans.extend(["; ".into(), accept.into(), " to save or select".into()]);
|
||||
spans.push(" · ".into());
|
||||
spans.extend(accept.spans());
|
||||
spans.push(" save/select".into());
|
||||
}
|
||||
if let Some(cancel) = keymap.primary_hint(ListAction::Cancel) {
|
||||
spans.push(" · ".into());
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" cancel".into());
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ fn reset_confirmation_keeps_both_choices_at_wrap_boundary() {
|
||||
view.open_reset_confirmation();
|
||||
|
||||
let mut snapshots = Vec::new();
|
||||
for width in [70, 71, 72] {
|
||||
for width in [68, 69, 70, 71, 72] {
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
|
||||
@@ -177,7 +177,9 @@ mod paste_burst;
|
||||
mod pending_input_preview;
|
||||
mod pending_thread_approvals;
|
||||
mod picker_presets;
|
||||
mod picker_rows;
|
||||
mod picker_style;
|
||||
pub(crate) use picker_style::selection_style;
|
||||
pub(crate) mod popup_consts;
|
||||
mod scroll_state;
|
||||
mod selection_picker_layout;
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
//! - **Reordering**: Optional left/right arrow support to reorder items
|
||||
//! - **Live preview**: Optional callback to show a preview of current selections
|
||||
//! - **Callbacks**: Hooks for change, confirm, and cancel events
|
||||
//! - **Picker appearance**: Full-width focus with overflow hints in spacer rows;
|
||||
//! checkboxes remain independent of the focused row
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! let picker = MultiSelectPicker::new(
|
||||
//! let picker = MultiSelectPicker::builder(
|
||||
//! "Select Items".to_string(),
|
||||
//! Some("Choose which items to enable".to_string()),
|
||||
//! app_event_tx,
|
||||
@@ -51,15 +53,21 @@ use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
use super::picker_rows;
|
||||
use super::picker_style;
|
||||
use super::selection_popup_common::ColumnWidthConfig;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::wrap_styled_line;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::bottom_pane::CancellationEvent;
|
||||
use crate::bottom_pane::bottom_pane_view::BottomPaneView;
|
||||
use crate::bottom_pane::popup_consts::MAX_POPUP_ROWS;
|
||||
use crate::bottom_pane::scroll_state::ScrollState;
|
||||
use crate::bottom_pane::selection_popup_common::render_rows_single_line;
|
||||
use crate::bottom_pane::selection_popup_common::render_rows_single_line_with_col_width_mode;
|
||||
use crate::key_hint;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::key_hint::is_plain_text_key_event;
|
||||
@@ -80,9 +88,6 @@ const ITEM_NAME_TRUNCATE_LEN: usize = 21;
|
||||
/// Placeholder text shown in the search input when empty.
|
||||
const SEARCH_PLACEHOLDER: &str = "Type to search";
|
||||
|
||||
/// Prefix displayed before the search query (mimics a command prompt).
|
||||
const SEARCH_PROMPT_PREFIX: &str = "> ";
|
||||
|
||||
const SECTION_BREAK_ROW: &str = " ───────────────────────";
|
||||
|
||||
/// Direction for reordering items in the list.
|
||||
@@ -158,7 +163,7 @@ struct BuiltRows {
|
||||
/// - Press Escape to cancel and close
|
||||
/// - Use Left/Right arrows to reorder items (if ordering is enabled)
|
||||
///
|
||||
/// Create instances using the builder pattern via [`MultiSelectPicker::new`].
|
||||
/// Create instances using the builder pattern via [`MultiSelectPicker::builder`].
|
||||
pub(crate) struct MultiSelectPicker {
|
||||
/// All items in the picker (unfiltered).
|
||||
items: Vec<MultiSelectItem>,
|
||||
@@ -281,18 +286,19 @@ impl MultiSelectPicker {
|
||||
MAX_POPUP_ROWS.min(len.max(1))
|
||||
}
|
||||
|
||||
/// Calculates the width available for row content (accounts for borders).
|
||||
fn rows_width(total_width: u16) -> u16 {
|
||||
total_width.saturating_sub(2)
|
||||
fn footer_lines(&self, width: u16) -> Vec<Line<'_>> {
|
||||
wrap_styled_line(&self.footer_hint, width.saturating_sub(/*rhs*/ 2))
|
||||
}
|
||||
|
||||
/// Calculates the height needed for the row list area.
|
||||
fn rows_height(&self, rows: &BuiltRows) -> u16 {
|
||||
rows.rows
|
||||
let height: u16 = rows
|
||||
.rows
|
||||
.len()
|
||||
.clamp(1, MAX_POPUP_ROWS)
|
||||
.clamp(/*min*/ 1, MAX_POPUP_ROWS)
|
||||
.try_into()
|
||||
.unwrap_or(1)
|
||||
.unwrap_or(/*default*/ 1);
|
||||
height.saturating_add(/*rhs*/ 2)
|
||||
}
|
||||
|
||||
/// Builds the display rows for all currently visible (filtered) items.
|
||||
@@ -313,13 +319,16 @@ impl MultiSelectPicker {
|
||||
let item_name = truncate_text(&item.name, ITEM_NAME_TRUNCATE_LEN);
|
||||
let name = format!("{prefix} [{marker}] {item_name}");
|
||||
rows.push(GenericDisplayRow {
|
||||
category_tag: None,
|
||||
name,
|
||||
description: item.description.clone(),
|
||||
selection_style: Some(picker_style::selection_style()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if item.section_break_after && visible_idx + 1 < self.filtered_indices.len() {
|
||||
rows.push(GenericDisplayRow {
|
||||
category_tag: None,
|
||||
name: SECTION_BREAK_ROW.to_string(),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
@@ -601,8 +610,7 @@ impl Renderable for MultiSelectPicker {
|
||||
|
||||
let mut height = self.header.desired_height(width.saturating_sub(4));
|
||||
height = height.saturating_add(rows_height + 3);
|
||||
height = height.saturating_add(2);
|
||||
height.saturating_add(1 + preview_height)
|
||||
height.saturating_add(self.footer_lines(width).len() as u16 + preview_height)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -612,7 +620,9 @@ impl Renderable for MultiSelectPicker {
|
||||
|
||||
// Reserve the footer line for the key-hint row.
|
||||
let preview_height = if self.preview_line.is_some() { 1 } else { 0 };
|
||||
let footer_height = 1 + preview_height;
|
||||
let hint_lines = self.footer_lines(area.width);
|
||||
let hint_height = hint_lines.len() as u16;
|
||||
let footer_height = hint_height + preview_height;
|
||||
let [content_area, footer_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(footer_height)]).areas(area);
|
||||
|
||||
@@ -624,33 +634,12 @@ impl Renderable for MultiSelectPicker {
|
||||
.header
|
||||
.desired_height(content_area.width.saturating_sub(4));
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(content_area.width);
|
||||
let rows_height = self.rows_height(&rows);
|
||||
let [header_area, _, search_area, list_area] = Layout::vertical([
|
||||
Constraint::Max(header_height),
|
||||
Constraint::Max(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(rows_height),
|
||||
])
|
||||
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
|
||||
let [header_area, search_area, list_area] =
|
||||
picker_rows::layout(content_area, header_height, /*search*/ 1, rows_height);
|
||||
|
||||
self.header.render(header_area, buf);
|
||||
|
||||
// Render the search prompt as two lines to mimic the composer.
|
||||
if search_area.height >= 2 {
|
||||
let [placeholder_area, input_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(search_area);
|
||||
Line::from(SEARCH_PLACEHOLDER.dim()).render(placeholder_area, buf);
|
||||
let line = if self.search_query.is_empty() {
|
||||
Line::from(vec![SEARCH_PROMPT_PREFIX.dim()])
|
||||
} else {
|
||||
Line::from(vec![
|
||||
SEARCH_PROMPT_PREFIX.dim(),
|
||||
self.search_query.clone().into(),
|
||||
])
|
||||
};
|
||||
line.render(input_area, buf);
|
||||
} else if search_area.height > 0 {
|
||||
if search_area.height > 0 {
|
||||
let query_span = if self.search_query.is_empty() {
|
||||
SEARCH_PLACEHOLDER.dim()
|
||||
} else {
|
||||
@@ -660,25 +649,23 @@ impl Renderable for MultiSelectPicker {
|
||||
}
|
||||
|
||||
if list_area.height > 0 {
|
||||
let render_area = Rect {
|
||||
x: list_area.x.saturating_sub(2),
|
||||
y: list_area.y,
|
||||
width: rows_width.max(1),
|
||||
height: list_area.height,
|
||||
};
|
||||
render_rows_single_line(
|
||||
render_area,
|
||||
let body = list_area.inset(Insets::vh(u16::from(list_area.height >= 3), /*h*/ 0));
|
||||
let rendered = render_rows_single_line_with_col_width_mode(
|
||||
body,
|
||||
buf,
|
||||
&rows.rows,
|
||||
&rows.state,
|
||||
render_area.height as usize,
|
||||
usize::from(body.height),
|
||||
"no matches",
|
||||
ColumnWidthConfig::default(),
|
||||
);
|
||||
picker_style::render_scroll_indicators(list_area, buf, rendered);
|
||||
}
|
||||
|
||||
let hint_area = if let Some(preview_line) = &self.preview_line {
|
||||
let [preview_area, hint_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(footer_area);
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(hint_height)])
|
||||
.areas(footer_area);
|
||||
let preview_area = Rect {
|
||||
x: preview_area.x + 2,
|
||||
y: preview_area.y,
|
||||
@@ -699,7 +686,7 @@ impl Renderable for MultiSelectPicker {
|
||||
width: hint_area.width.saturating_sub(2),
|
||||
height: hint_area.height,
|
||||
};
|
||||
self.footer_hint.clone().dim().render(hint_area, buf);
|
||||
Paragraph::new(hint_lines).dim().render(hint_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,7 +695,7 @@ impl Renderable for MultiSelectPicker {
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// let picker = MultiSelectPicker::new("Title".into(), None, tx)
|
||||
/// let picker = MultiSelectPicker::builder("Title".into(), /*subtitle*/ None, tx)
|
||||
/// .items(items)
|
||||
/// .enable_ordering()
|
||||
/// .on_preview(|items| Some(Line::from("Preview")))
|
||||
@@ -818,39 +805,33 @@ impl MultiSelectPickerBuilder {
|
||||
/// preview line if a preview callback was set.
|
||||
pub fn build(self) -> MultiSelectPicker {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from(self.title.bold()));
|
||||
|
||||
header.push(Paragraph::new(self.title.bold()).wrap(Wrap { trim: false }));
|
||||
if let Some(subtitle) = self.subtitle {
|
||||
header.push(Line::from(subtitle.dim()));
|
||||
header.push(Paragraph::new(subtitle.dim()).wrap(Wrap { trim: false }));
|
||||
}
|
||||
|
||||
let instructions = if self.instructions.is_empty() {
|
||||
let mut spans = vec![
|
||||
"Press ".into(),
|
||||
key_hint::plain(KeyCode::Char(' ')).into(),
|
||||
" to toggle".into(),
|
||||
];
|
||||
let mut spans = vec![key_hint::plain(KeyCode::Char(' ')).into(), " toggle".dim()];
|
||||
if self.ordering_enabled
|
||||
&& let (Some(move_left), Some(move_right)) = (
|
||||
&& let (Some(left), Some(right)) = (
|
||||
self.keymap.primary_hint(ListAction::MoveLeft),
|
||||
self.keymap.primary_hint(ListAction::MoveRight),
|
||||
)
|
||||
{
|
||||
spans.push("; ".into());
|
||||
spans.push(move_left.into());
|
||||
spans.push("/".into());
|
||||
spans.push(move_right.into());
|
||||
spans.push(" to move".into());
|
||||
spans.push(" · ".dim());
|
||||
spans.extend(key_hint::key_label_spans(&format!(
|
||||
"{}/{}",
|
||||
left.display_label(),
|
||||
right.display_label()
|
||||
)));
|
||||
spans.push(" reorder".dim());
|
||||
}
|
||||
if let Some(accept) = self.keymap.primary_hint(ListAction::Accept) {
|
||||
spans.push("; ".into());
|
||||
spans.push(accept.into());
|
||||
spans.push(" to confirm and close".into());
|
||||
}
|
||||
if let Some(cancel) = self.keymap.primary_hint(ListAction::Cancel) {
|
||||
spans.push("; ".into());
|
||||
spans.push(cancel.into());
|
||||
spans.push(" to close".into());
|
||||
for (action, label) in [(ListAction::Accept, "save"), (ListAction::Cancel, "cancel")] {
|
||||
if let Some(hint) = self.keymap.primary_hint(action) {
|
||||
spans.push(" · ".dim());
|
||||
spans.extend(hint.spans());
|
||||
spans.push(format!(" {label}").dim());
|
||||
}
|
||||
}
|
||||
spans
|
||||
} else {
|
||||
@@ -1096,4 +1077,110 @@ mod tests {
|
||||
picker.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL));
|
||||
assert_eq!(picker.state.selected_idx, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_picker_preserves_selected_row_when_overflow_spacers_yield() {
|
||||
let mut picker = test_picker(
|
||||
(0..12)
|
||||
.map(|index| {
|
||||
item(
|
||||
&format!("field-{index:02}"),
|
||||
/*orderable*/ true,
|
||||
/*section_break_after*/ false,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
picker.header = Box::new(());
|
||||
picker.jump_bottom();
|
||||
let mut snapshots = Vec::new();
|
||||
for height in 1..=7 {
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 80, height);
|
||||
let mut buf = Buffer::empty(area);
|
||||
picker.render(area, &mut buf);
|
||||
let text = buf
|
||||
.content
|
||||
.chunks(usize::from(area.width))
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if height >= 4 {
|
||||
assert!(text.contains("› [ ] field-11"), "{text}");
|
||||
}
|
||||
assert_eq!(picker.state.selected_idx, Some(11));
|
||||
snapshots.push(format!("height={height}\n{text}"));
|
||||
}
|
||||
picker.toggle_selected();
|
||||
assert_eq!(
|
||||
picker
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.map(|item| item.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["field-11"]
|
||||
);
|
||||
insta::assert_snapshot!(snapshots.join("\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_appearance_renders_checkboxes_preview_and_overflow() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let mut picker = MultiSelectPicker::builder(
|
||||
"Status line".to_string(),
|
||||
/*subtitle*/ None,
|
||||
AppEventSender::new(tx),
|
||||
)
|
||||
.items(
|
||||
(0..12)
|
||||
.map(|index| MultiSelectItem {
|
||||
id: format!("field-{index}"),
|
||||
name: format!("Field {index}"),
|
||||
description: Some("Status detail".to_string()),
|
||||
enabled: index == 0,
|
||||
section_break_after: index == 0,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.enable_ordering()
|
||||
.on_preview(|items| {
|
||||
Some(Line::from(
|
||||
items
|
||||
.iter()
|
||||
.filter(|item| item.enabled)
|
||||
.map(|item| item.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · "),
|
||||
))
|
||||
})
|
||||
.build();
|
||||
let render = |picker: &MultiSelectPicker| {
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
/*width*/ 48,
|
||||
picker.desired_height(/*width*/ 48),
|
||||
);
|
||||
let mut buf = Buffer::empty(area);
|
||||
picker.render(area, &mut buf);
|
||||
buf
|
||||
};
|
||||
let first = render(&picker);
|
||||
picker.jump_bottom();
|
||||
picker.toggle_selected();
|
||||
picker.move_selected_item(Direction::Up);
|
||||
let last = render(&picker);
|
||||
picker.handle_key_event(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE));
|
||||
let empty = render(&picker);
|
||||
insta::assert_snapshot!(format!(
|
||||
"First page:\n{first:?}\nLast page:\n{last:?}\nNo matches:\n{empty:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Shared row presentation for dedicated picker workflows.
|
||||
//!
|
||||
//! These adapters keep domain-specific state and save policies with their views.
|
||||
//! Measurement and painting share columns, wrapping, and overflow spacer rows.
|
||||
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
use super::picker_style;
|
||||
use super::scroll_state::ScrollState;
|
||||
use super::selection_picker_layout::PickerLayoutSizes;
|
||||
use super::selection_picker_layout::picker_areas;
|
||||
use super::selection_popup_common as rows;
|
||||
use super::selection_popup_common::ColumnWidthConfig;
|
||||
use super::selection_popup_common::ColumnWidthMode;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_row_layout::SelectionDescriptionLayout;
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt;
|
||||
|
||||
const COLUMNS: ColumnWidthConfig = ColumnWidthConfig {
|
||||
mode: ColumnWidthMode::AutoAllRows,
|
||||
name_column_width: None,
|
||||
description_layout: SelectionDescriptionLayout::HideWhenNarrow {
|
||||
min_description_width: 24,
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) fn measure_rows_height(
|
||||
rows: &[GenericDisplayRow],
|
||||
state: &ScrollState,
|
||||
max_results: usize,
|
||||
width: u16,
|
||||
) -> u16 {
|
||||
rows::measure_rows_height_with_col_width_mode(rows, state, max_results, width, COLUMNS)
|
||||
.saturating_add(/*rhs*/ 2)
|
||||
}
|
||||
|
||||
pub(super) fn render_rows(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
rows: &[GenericDisplayRow],
|
||||
state: &ScrollState,
|
||||
max_results: usize,
|
||||
empty_message: &str,
|
||||
) -> u16 {
|
||||
let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0));
|
||||
let rendered = rows::render_rows_with_col_width_mode(
|
||||
body,
|
||||
buf,
|
||||
rows,
|
||||
state,
|
||||
max_results,
|
||||
empty_message,
|
||||
COLUMNS,
|
||||
);
|
||||
picker_style::render_scroll_indicators(area, buf, rendered);
|
||||
rendered.lines.saturating_add(/*rhs*/ 2).min(area.height)
|
||||
}
|
||||
|
||||
pub(super) fn render_rows_single_line(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
rows: &[GenericDisplayRow],
|
||||
state: &ScrollState,
|
||||
max_results: usize,
|
||||
empty_message: &str,
|
||||
) -> u16 {
|
||||
let body = area.inset(Insets::vh(u16::from(area.height >= 3), /*h*/ 0));
|
||||
let rendered = rows::render_rows_single_line_with_col_width_mode(
|
||||
body,
|
||||
buf,
|
||||
rows,
|
||||
state,
|
||||
max_results,
|
||||
empty_message,
|
||||
COLUMNS,
|
||||
);
|
||||
picker_style::render_scroll_indicators(area, buf, rendered);
|
||||
rendered.lines.saturating_add(/*rhs*/ 2).min(area.height)
|
||||
}
|
||||
|
||||
/// Keep headers and search above a list, yielding optional gaps on short screens.
|
||||
pub(super) fn layout(area: Rect, header: u16, search: u16, rows: u16) -> [Rect; 3] {
|
||||
let [header, _, _, _, search, above, rows, below, _, _] = picker_areas(
|
||||
area.inset(Insets::tlbr(
|
||||
/*top*/ 1, /*left*/ 2, /*bottom*/ 0, /*right*/ 2,
|
||||
)),
|
||||
PickerLayoutSizes {
|
||||
header,
|
||||
header_gap: 1,
|
||||
tabs: 0,
|
||||
search,
|
||||
rows: rows.saturating_sub(/*rhs*/ 2),
|
||||
side: 0,
|
||||
},
|
||||
);
|
||||
[
|
||||
header,
|
||||
search,
|
||||
Rect::new(
|
||||
area.x,
|
||||
above.y,
|
||||
area.width,
|
||||
above.height + rows.height + below.height,
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use ratatui::layout::Rect;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -19,8 +21,6 @@ use crate::key_hint::ShortcutHint;
|
||||
use crate::key_hint::is_plain_text_key_event;
|
||||
use crate::keymap::ListAction;
|
||||
use crate::keymap::ListKeymap;
|
||||
use crate::render::Insets;
|
||||
use crate::render::RectExt as _;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::skills_helpers::match_skill;
|
||||
@@ -28,13 +28,12 @@ use crate::style::user_message_style;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::picker_rows::render_rows_single_line;
|
||||
use super::popup_consts::MAX_POPUP_ROWS;
|
||||
use super::scroll_state::ScrollState;
|
||||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::render_rows_single_line;
|
||||
|
||||
const SEARCH_PLACEHOLDER: &str = "Type to search skills";
|
||||
const SEARCH_PROMPT_PREFIX: &str = "> ";
|
||||
|
||||
pub(crate) struct SkillsToggleItem {
|
||||
pub name: String,
|
||||
@@ -63,10 +62,15 @@ impl SkillsToggleView {
|
||||
keymap: ListKeymap,
|
||||
) -> Self {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Enable/Disable Skills".bold()));
|
||||
header.push(Line::from(
|
||||
"Turn skills on or off. Your changes are saved automatically.".dim(),
|
||||
));
|
||||
header.push(
|
||||
Paragraph::new(Line::from("Enable/Disable Skills".bold())).wrap(Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
Paragraph::new(Line::from(
|
||||
"Turn skills on or off. Your changes are saved automatically.".dim(),
|
||||
))
|
||||
.wrap(Wrap { trim: false }),
|
||||
);
|
||||
|
||||
let mut view = Self {
|
||||
items,
|
||||
@@ -148,6 +152,8 @@ impl SkillsToggleView {
|
||||
let item_name = &item.name;
|
||||
let name = format!("{prefix} [{marker}] {item_name}");
|
||||
GenericDisplayRow {
|
||||
category_tag: None,
|
||||
selection_style: Some(super::picker_style::selection_style()),
|
||||
name,
|
||||
description: Some(item.description.clone()),
|
||||
..Default::default()
|
||||
@@ -223,12 +229,8 @@ impl SkillsToggleView {
|
||||
.list_skills(Vec::new(), /*force_reload*/ true);
|
||||
}
|
||||
|
||||
fn rows_width(total_width: u16) -> u16 {
|
||||
total_width.saturating_sub(2)
|
||||
}
|
||||
|
||||
fn rows_height(&self, rows: &[GenericDisplayRow]) -> u16 {
|
||||
rows.len().clamp(1, MAX_POPUP_ROWS).try_into().unwrap_or(1)
|
||||
(rows.len().clamp(/*min*/ 1, MAX_POPUP_ROWS) as u16).saturating_add(/*rhs*/ 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +311,13 @@ impl Renderable for SkillsToggleView {
|
||||
let mut height = self.header.desired_height(width.saturating_sub(4));
|
||||
height = height.saturating_add(rows_height + 3);
|
||||
height = height.saturating_add(2);
|
||||
height.saturating_add(1)
|
||||
height.saturating_add(
|
||||
super::selection_popup_common::wrap_styled_line(
|
||||
&self.footer_hint,
|
||||
width.saturating_sub(/*rhs*/ 2),
|
||||
)
|
||||
.len() as u16,
|
||||
)
|
||||
}
|
||||
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -317,9 +325,15 @@ impl Renderable for SkillsToggleView {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve the footer line for the key-hint row.
|
||||
let [content_area, footer_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
|
||||
let hint_lines = super::selection_popup_common::wrap_styled_line(
|
||||
&self.footer_hint,
|
||||
area.width.saturating_sub(/*rhs*/ 2),
|
||||
);
|
||||
let [content_area, footer_area] = Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(hint_lines.len() as u16),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
Block::default()
|
||||
.style(user_message_style())
|
||||
@@ -329,33 +343,13 @@ impl Renderable for SkillsToggleView {
|
||||
.header
|
||||
.desired_height(content_area.width.saturating_sub(4));
|
||||
let rows = self.build_rows();
|
||||
let rows_width = Self::rows_width(content_area.width);
|
||||
let rows_height = self.rows_height(&rows);
|
||||
let [header_area, _, search_area, list_area] = Layout::vertical([
|
||||
Constraint::Max(header_height),
|
||||
Constraint::Max(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(rows_height),
|
||||
])
|
||||
.areas(content_area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)));
|
||||
let [header_area, search_area, render_area] =
|
||||
super::picker_rows::layout(content_area, header_height, /*search*/ 1, rows_height);
|
||||
|
||||
self.header.render(header_area, buf);
|
||||
|
||||
// Render the search prompt as two lines to mimic the composer.
|
||||
if search_area.height >= 2 {
|
||||
let [placeholder_area, input_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(search_area);
|
||||
Line::from(SEARCH_PLACEHOLDER.dim()).render(placeholder_area, buf);
|
||||
let line = if self.search_query.is_empty() {
|
||||
Line::from(vec![SEARCH_PROMPT_PREFIX.dim()])
|
||||
} else {
|
||||
Line::from(vec![
|
||||
SEARCH_PROMPT_PREFIX.dim(),
|
||||
self.search_query.clone().into(),
|
||||
])
|
||||
};
|
||||
line.render(input_area, buf);
|
||||
} else if search_area.height > 0 {
|
||||
if search_area.height > 0 {
|
||||
let query_span = if self.search_query.is_empty() {
|
||||
SEARCH_PLACEHOLDER.dim()
|
||||
} else {
|
||||
@@ -364,13 +358,7 @@ impl Renderable for SkillsToggleView {
|
||||
Line::from(query_span).render(search_area, buf);
|
||||
}
|
||||
|
||||
if list_area.height > 0 {
|
||||
let render_area = Rect {
|
||||
x: list_area.x.saturating_sub(2),
|
||||
y: list_area.y,
|
||||
width: rows_width.max(1),
|
||||
height: list_area.height,
|
||||
};
|
||||
if render_area.height > 0 {
|
||||
render_rows_single_line(
|
||||
render_area,
|
||||
buf,
|
||||
@@ -387,7 +375,7 @@ impl Renderable for SkillsToggleView {
|
||||
width: footer_area.width.saturating_sub(2),
|
||||
height: footer_area.height,
|
||||
};
|
||||
self.footer_hint.clone().dim().render(hint_area, buf);
|
||||
Paragraph::new(hint_lines).dim().render(hint_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,32 +386,19 @@ fn skills_toggle_hint_line(keymap: &ListKeymap) -> Line<'static> {
|
||||
.filter(|binding| *binding != ShortcutHint::Single(space));
|
||||
let cancel = keymap.primary_hint(ListAction::Cancel);
|
||||
|
||||
match (accept, cancel) {
|
||||
(Some(accept), Some(cancel)) => Line::from(vec![
|
||||
"Press ".into(),
|
||||
space.into(),
|
||||
" or ".into(),
|
||||
accept.into(),
|
||||
" to toggle; ".into(),
|
||||
cancel.into(),
|
||||
" to close".into(),
|
||||
]),
|
||||
(Some(accept), None) => Line::from(vec![
|
||||
"Press ".into(),
|
||||
space.into(),
|
||||
" or ".into(),
|
||||
accept.into(),
|
||||
" to toggle".into(),
|
||||
]),
|
||||
(None, Some(cancel)) => Line::from(vec![
|
||||
"Press ".into(),
|
||||
space.into(),
|
||||
" to toggle; ".into(),
|
||||
cancel.into(),
|
||||
" to close".into(),
|
||||
]),
|
||||
(None, None) => Line::from(vec!["Press ".into(), space.into(), " to toggle".into()]),
|
||||
let mut toggle = space.display_label();
|
||||
if let Some(accept) = accept {
|
||||
toggle.push('/');
|
||||
toggle.push_str(&accept.display_label());
|
||||
}
|
||||
let mut spans = key_hint::key_label_spans(&toggle);
|
||||
spans.push(" toggle".dim());
|
||||
if let Some(cancel) = cancel {
|
||||
spans.push(" · ".dim());
|
||||
spans.extend(cancel.spans());
|
||||
spans.push(" close".dim());
|
||||
}
|
||||
spans.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+3
-3
@@ -13,6 +13,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72))
|
||||
Sign in to this app in your browser, then return here.
|
||||
|
||||
|
||||
› 1. Open sign-in URL
|
||||
2. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
› 1. Open sign-in URL
|
||||
2. Back
|
||||
tab / ↑ ↓ move · enter select · esc close
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72))
|
||||
Newly installed apps can take a few minutes to appear in /apps.
|
||||
|
||||
|
||||
› 1. Manage on ChatGPT
|
||||
2. Enable app
|
||||
3. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
› 1. Manage on ChatGPT
|
||||
2. Enable app
|
||||
3. Back
|
||||
tab / ↑ ↓ move · enter select · esc close
|
||||
|
||||
+3
-3
@@ -14,6 +14,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72))
|
||||
Complete the requested action in your browser, then return here.
|
||||
|
||||
|
||||
› 1. Open link
|
||||
2. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
› 1. Open link
|
||||
2. Back
|
||||
tab / ↑ ↓ move · enter select · esc close
|
||||
|
||||
+3
-3
@@ -12,6 +12,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72))
|
||||
Link:
|
||||
https://payments.example/checkout/123
|
||||
|
||||
› 1. I finished
|
||||
2. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
› 1. I finished
|
||||
2. Back
|
||||
tab / ↑ ↓ move · enter select · esc close
|
||||
|
||||
+3
-3
@@ -13,6 +13,6 @@ expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72))
|
||||
After installed, use $ to insert this app into the prompt.
|
||||
|
||||
|
||||
› 1. Install on ChatGPT
|
||||
2. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
› 1. Install on ChatGPT
|
||||
2. Back
|
||||
tab / ↑ ↓ move · enter select · esc close
|
||||
|
||||
+6
-3
@@ -2,13 +2,16 @@
|
||||
source: tui/src/bottom_pane/experimental_features_view_tests.rs
|
||||
expression: buffer_text(&buffer)
|
||||
---
|
||||
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
› [ ] Automatically start the background server Use the shared local server
|
||||
for new, resumed, and forked
|
||||
|
||||
› [ ] Automatically start the background server Use the shared local server for
|
||||
new, resumed, and forked
|
||||
sessions. Takes effect next
|
||||
launch.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+5
-3
@@ -7,7 +7,9 @@ expression: buffer_text(&buffer)
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
› [ ] Analytics plan history Preview five-hour and weekly allowance history
|
||||
for consumer accounts in /analytics.
|
||||
|
||||
Press space to select or enter to save
|
||||
› [ ] Analytics plan history Preview five-hour and weekly allowance history for
|
||||
consumer accounts in /analytics.
|
||||
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-1
@@ -7,6 +7,8 @@ expression: buffer_text(&buffer)
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
|
||||
› [x] Server network_proxy Description supplied by the server.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-1
@@ -8,6 +8,8 @@ expression: buffer_text(&buffer)
|
||||
only in new tasks or after restarting the Codex server.
|
||||
No server experiments available.
|
||||
|
||||
|
||||
No experimental features available for now
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+41
-7
@@ -2,6 +2,36 @@
|
||||
source: tui/src/bottom_pane/experimental_features_view_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
width=68
|
||||
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features
|
||||
take effect only in new tasks or after restarting the Codex
|
||||
server.
|
||||
|
||||
|
||||
› [x] First feature A description that fits exactly at this
|
||||
boundary.
|
||||
[ ] Last feature Keep this choice visible.
|
||||
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
width=69
|
||||
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features
|
||||
take effect only in new tasks or after restarting the Codex
|
||||
server.
|
||||
|
||||
|
||||
› [x] First feature A description that fits exactly at this
|
||||
boundary.
|
||||
[ ] Last feature Keep this choice visible.
|
||||
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
width=70
|
||||
|
||||
Experimental features
|
||||
@@ -9,11 +39,12 @@ width=70
|
||||
take effect only in new tasks or after restarting the Codex
|
||||
server.
|
||||
|
||||
› [x] First feature A description that fits exactly at this
|
||||
boundary.
|
||||
|
||||
› [x] First feature A description that fits exactly at this boundary.
|
||||
[ ] Last feature Keep this choice visible.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
width=71
|
||||
|
||||
@@ -21,11 +52,12 @@ width=71
|
||||
Checked features are configured on. Some experimental features take
|
||||
effect only in new tasks or after restarting the Codex server.
|
||||
|
||||
› [x] First feature A description that fits exactly at this
|
||||
boundary.
|
||||
|
||||
› [x] First feature A description that fits exactly at this boundary.
|
||||
[ ] Last feature Keep this choice visible.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
width=72
|
||||
|
||||
@@ -33,7 +65,9 @@ width=72
|
||||
Checked features are configured on. Some experimental features take
|
||||
effect only in new tasks or after restarting the Codex server.
|
||||
|
||||
|
||||
› [x] First feature A description that fits exactly at this boundary.
|
||||
[ ] Last feature Keep this choice visible.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+2
@@ -8,8 +8,10 @@ expression: buffer_text(&buffer)
|
||||
only in new tasks or after restarting the Codex server.
|
||||
Saved but readback failed
|
||||
|
||||
|
||||
› [ ] Server network_proxy Description supplied by the server.
|
||||
[ ] Server future_feature Description supplied by the server.
|
||||
[ ] Server prevent_idle_sleep Description supplied by the server.
|
||||
|
||||
|
||||
Selections retained. Save to retry, or cancel to close.
|
||||
|
||||
+2
@@ -8,8 +8,10 @@ expression: buffer_text(&buffer)
|
||||
only in new tasks or after restarting the Codex server.
|
||||
Saving experimental features…
|
||||
|
||||
|
||||
› [ ] Server network_proxy Description supplied by the server.
|
||||
[ ] Server future_feature Description supplied by the server.
|
||||
[ ] Server prevent_idle_sleep Description supplied by the server.
|
||||
|
||||
|
||||
Saving… Closing this popup will not cancel the write.
|
||||
|
||||
+3
-1
@@ -7,8 +7,10 @@ expression: buffer_text(&buffer)
|
||||
Checked features are configured on. Some experimental features take effect only in new tasks or
|
||||
after restarting the Codex server.
|
||||
|
||||
|
||||
› [x] Server network_proxy Description supplied by the server.
|
||||
[x] Server future_feature Description supplied by the server.
|
||||
[x] Server prevent_idle_sleep Description supplied by the server.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-1
@@ -9,6 +9,8 @@ expression: buffer_text(&buffer)
|
||||
Server experiments unavailable. Reopen /experimental to retry; restart this
|
||||
Codex client if requests remain unanswered.
|
||||
|
||||
|
||||
No experimental features available for now
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/experimental_features_view_tests.rs
|
||||
assertion_line: 208
|
||||
expression: buffer_text(&buffer)
|
||||
---
|
||||
|
||||
@@ -8,6 +7,8 @@ expression: buffer_text(&buffer)
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
|
||||
› [x] Server realtime_conversation Description supplied by the server.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/experimental_features_view_tests.rs
|
||||
assertion_line: 208
|
||||
expression: buffer_text(&buffer)
|
||||
---
|
||||
|
||||
@@ -9,6 +8,8 @@ expression: buffer_text(&buffer)
|
||||
only in new tasks or after restarting the Codex server.
|
||||
No server experiments available.
|
||||
|
||||
|
||||
No experimental features available for now
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/hooks_browser_view.rs
|
||||
expression: text
|
||||
---
|
||||
|
||||
Hooks
|
||||
Lifecycle hooks from config and
|
||||
enabled plugins.
|
||||
|
||||
Event Active/Installed
|
||||
↑
|
||||
SessionEnd 0/0
|
||||
UserPromptSubmit 0/0
|
||||
SubagentStart 0/0
|
||||
SubagentStop 0/0
|
||||
Stop 0/0
|
||||
› Interrupt 0/0
|
||||
|
||||
|
||||
enter details · esc close
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PreToolUse hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
› [x] Hook 1
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)"
|
||||
Context unlimited
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go back
|
||||
|
||||
space/enter toggle · esc back
|
||||
|
||||
+5
-3
@@ -4,9 +4,10 @@ expression: "render_lines(&view, 44)"
|
||||
---
|
||||
|
||||
PreToolUse hooks
|
||||
Turn hooks on or off. Your changes are s
|
||||
Turn hooks on or off. Your changes are
|
||||
saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
› [x] Hook 1
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -18,4 +19,5 @@ expression: "render_lines(&view, 44)"
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go
|
||||
|
||||
space/enter toggle · esc back
|
||||
|
||||
+3
-2
@@ -5,7 +5,8 @@ expression: "render_lines(&view, 112)"
|
||||
|
||||
PermissionRequest hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
No hooks installed for this event.
|
||||
|
||||
Press esc to go back
|
||||
|
||||
|
||||
esc back
|
||||
|
||||
+4
-6
@@ -7,7 +7,8 @@ expression: "render_lines(&view, 112)"
|
||||
Lifecycle hooks from config and enabled plugins.
|
||||
|
||||
Event Installed Active Description
|
||||
PreToolUse 2 1 Before a tool executes
|
||||
|
||||
› PreToolUse 2 1 Before a tool executes
|
||||
PermissionRequest 1 1 When permission is requested
|
||||
PostToolUse 0 0 After a tool executes
|
||||
PreCompact 0 0 Before context compaction
|
||||
@@ -15,9 +16,6 @@ expression: "render_lines(&view, 112)"
|
||||
SessionStart 0 0 When a new session starts
|
||||
SessionEnd 0 0 Right before a session ends
|
||||
UserPromptSubmit 0 0 When the user submits a prompt
|
||||
SubagentStart 0 0 When a subagent is created
|
||||
SubagentStop 0 0 Right before a subagent ends its turn
|
||||
Stop 0 0 Right before Codex ends its turn
|
||||
Interrupt 0 0 Right before an interrupted turn is aborted
|
||||
↓
|
||||
|
||||
Press enter to view hooks; esc to close
|
||||
enter details · esc close
|
||||
|
||||
+4
-6
@@ -11,7 +11,8 @@ expression: "render_lines(&view, 112)"
|
||||
■ /tmp/hooks.json: failed to parse hooks config
|
||||
|
||||
Event Installed Active Description
|
||||
PreToolUse 0 0 Before a tool executes
|
||||
|
||||
› PreToolUse 0 0 Before a tool executes
|
||||
PermissionRequest 0 0 When permission is requested
|
||||
PostToolUse 0 0 After a tool executes
|
||||
PreCompact 0 0 Before context compaction
|
||||
@@ -19,9 +20,6 @@ expression: "render_lines(&view, 112)"
|
||||
SessionStart 0 0 When a new session starts
|
||||
SessionEnd 0 0 Right before a session ends
|
||||
UserPromptSubmit 0 0 When the user submits a prompt
|
||||
SubagentStart 0 0 When a subagent is created
|
||||
SubagentStop 0 0 Right before a subagent ends its turn
|
||||
Stop 0 0 Right before Codex ends its turn
|
||||
Interrupt 0 0 Right before an interrupted turn is aborted
|
||||
↓
|
||||
|
||||
Press enter to view hooks; esc to close
|
||||
enter details · esc close
|
||||
|
||||
+4
-6
@@ -9,7 +9,8 @@ expression: "render_lines(&view, 112)"
|
||||
⚠ 1 hook needs review before it can run.
|
||||
|
||||
Event Installed Active Review Description
|
||||
PreToolUse 1 0 1 Before a tool executes
|
||||
|
||||
› PreToolUse 1 0 1 Before a tool executes
|
||||
PermissionRequest 0 0 0 When permission is requested
|
||||
PostToolUse 0 0 0 After a tool executes
|
||||
PreCompact 0 0 0 Before context compaction
|
||||
@@ -17,9 +18,6 @@ expression: "render_lines(&view, 112)"
|
||||
SessionStart 0 0 0 When a new session starts
|
||||
SessionEnd 0 0 0 Right before a session ends
|
||||
UserPromptSubmit 0 0 0 When the user submits a prompt
|
||||
SubagentStart 0 0 0 When a subagent is created
|
||||
SubagentStop 0 0 0 Right before a subagent ends its turn
|
||||
Stop 0 0 0 Right before Codex ends its turn
|
||||
Interrupt 0 0 0 Right before an interrupted turn is aborted
|
||||
↓
|
||||
|
||||
Press t to trust all; enter to review hooks; esc to close
|
||||
t trust all · enter review · esc close
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PreToolUse hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
› [x] Hook 1
|
||||
[ ] Hook 2
|
||||
|
||||
Event PreToolUse
|
||||
@@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go back
|
||||
|
||||
space/enter toggle · esc back
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PermissionRequest hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
› [x] Hook 1
|
||||
|
||||
Event PermissionRequest
|
||||
Matcher Bash
|
||||
@@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust Managed
|
||||
|
||||
Managed hooks are always on; press esc to go back
|
||||
|
||||
Managed (always on) · esc back
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PreToolUse hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
› [x] Hook 1
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go back
|
||||
|
||||
space/enter toggle · esc back
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PreToolUse hooks
|
||||
1 hook needs review before it can run.
|
||||
|
||||
[!] Hook 1 · new
|
||||
› [!] Hook 1 · new
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust New hook - review required
|
||||
|
||||
Press t to trust; esc to go back
|
||||
|
||||
t trust · esc back
|
||||
|
||||
+4
-3
@@ -5,7 +5,7 @@ expression: "render_lines(&view, 112)"
|
||||
|
||||
PreToolUse hooks
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
↑
|
||||
[x] Hook 2
|
||||
[x] Hook 3
|
||||
[x] Hook 4
|
||||
@@ -13,7 +13,7 @@ expression: "render_lines(&view, 112)"
|
||||
[x] Hook 6
|
||||
[x] Hook 7
|
||||
[x] Hook 8
|
||||
[x] Hook 9
|
||||
› [x] Hook 9
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -23,4 +23,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
Press space or enter to toggle; esc to go back
|
||||
|
||||
space/enter toggle · esc back
|
||||
|
||||
+3
-2
@@ -7,7 +7,7 @@ expression: "render_lines(&view, 112)"
|
||||
Turn hooks on or off. Your changes are saved automatically.
|
||||
|
||||
[x] Hook 1
|
||||
[x] Hook 2
|
||||
› [x] Hook 2
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -17,4 +17,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust Managed
|
||||
|
||||
Managed hooks are always on; press esc to go back
|
||||
|
||||
Managed (always on) · esc back
|
||||
|
||||
+3
-2
@@ -6,7 +6,7 @@ expression: "render_lines(&view, 112)"
|
||||
PreToolUse hooks
|
||||
1 hook needs review before it can run.
|
||||
|
||||
[!] Hook 1 · new
|
||||
› [!] Hook 1 · new
|
||||
|
||||
Event PreToolUse
|
||||
Matcher Bash
|
||||
@@ -16,4 +16,5 @@ expression: "render_lines(&view, 112)"
|
||||
Timeout 30s
|
||||
Trust New hook - review required
|
||||
|
||||
Press t to trust; esc to go back
|
||||
|
||||
t trust · esc back
|
||||
|
||||
+61
-24
@@ -2,37 +2,74 @@
|
||||
source: tui/src/bottom_pane/memories_settings_view_tests.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
width=68
|
||||
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the
|
||||
current Codex home.
|
||||
|
||||
|
||||
› Reset all memories Delete local memory files and rollout
|
||||
summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
enter select · esc back
|
||||
|
||||
width=69
|
||||
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the
|
||||
current Codex home.
|
||||
|
||||
|
||||
› Reset all memories Delete local memory files and rollout
|
||||
summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
enter select · esc back
|
||||
|
||||
width=70
|
||||
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the
|
||||
current Codex home.
|
||||
|
||||
› Reset all memories Delete local memory files and rollout
|
||||
summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
width=71
|
||||
|
||||
Reset all memories?
|
||||
|
||||
› Reset all memories Delete local memory files and rollout
|
||||
summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
width=72
|
||||
|
||||
Reset all memories?
|
||||
|
||||
› Reset all memories Delete local memory files and rollout summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
width=71
|
||||
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the
|
||||
current Codex home.
|
||||
|
||||
|
||||
› Reset all memories Delete local memory files and rollout summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
enter select · esc back
|
||||
|
||||
width=72
|
||||
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the current
|
||||
Codex home.
|
||||
|
||||
|
||||
› Reset all memories Delete local memory files and rollout summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
enter select · esc back
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/multi_select_picker.rs
|
||||
expression: "format!(\"First page:\\n{first:?}\\nLast page:\\n{last:?}\\nNo matches:\\n{empty:?}\")"
|
||||
---
|
||||
First page:
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 17 },
|
||||
content: [
|
||||
" ",
|
||||
" Status line ",
|
||||
" ",
|
||||
" Type to search ",
|
||||
" ",
|
||||
"› [x] Field 0 Status detail ",
|
||||
" ─────────────────────── ",
|
||||
" [ ] Field 1 Status detail ",
|
||||
" [ ] Field 2 Status detail ",
|
||||
" [ ] Field 3 Status detail ",
|
||||
" [ ] Field 4 Status detail ",
|
||||
" [ ] Field 5 Status detail ",
|
||||
" [ ] Field 6 Status detail ",
|
||||
"↓ ",
|
||||
" Field 0 ",
|
||||
" space toggle · ←/→ reorder · enter save · esc ",
|
||||
" cancel ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
|
||||
x: 27, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED,
|
||||
x: 40, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
|
||||
x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 25, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 27, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 40, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 7, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 17, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 20, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 31, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 36, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 44, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 47, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 0, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
]
|
||||
}
|
||||
Last page:
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 17 },
|
||||
content: [
|
||||
" ",
|
||||
" Status line ",
|
||||
" ",
|
||||
" Type to search ",
|
||||
"↑ ",
|
||||
" [ ] Field 4 Status detail ",
|
||||
" [ ] Field 5 Status detail ",
|
||||
" [ ] Field 6 Status detail ",
|
||||
" [ ] Field 7 Status detail ",
|
||||
" [ ] Field 8 Status detail ",
|
||||
" [ ] Field 9 Status detail ",
|
||||
"› [x] Field 11 Status detail ",
|
||||
" [ ] Field 10 Status detail ",
|
||||
" ",
|
||||
" Field 0 · Field 11 ",
|
||||
" space toggle · ←/→ reorder · enter save · esc ",
|
||||
" cancel ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
|
||||
x: 16, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: REVERSED,
|
||||
x: 29, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
|
||||
x: 0, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 16, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 29, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 7, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 17, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 20, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 31, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 36, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 44, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 47, y: 15, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 0, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 16, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
]
|
||||
}
|
||||
No matches:
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 48, height: 10 },
|
||||
content: [
|
||||
" ",
|
||||
" Status line ",
|
||||
" ",
|
||||
" z ",
|
||||
" ",
|
||||
"no matches ",
|
||||
" ",
|
||||
" Field 0 · Field 11 ",
|
||||
" space toggle · ←/→ reorder · enter save · esc ",
|
||||
" cancel ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 10, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 7, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 17, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 20, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 31, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 36, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 44, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 47, y: 8, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 0, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
]
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/multi_select_picker.rs
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
height=1
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=2
|
||||
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=3
|
||||
|
||||
Type to search
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=4
|
||||
|
||||
Type to search
|
||||
› [ ] field-11
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=5
|
||||
|
||||
Type to search
|
||||
[ ] field-10
|
||||
› [ ] field-11
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=6
|
||||
|
||||
Type to search
|
||||
↑
|
||||
› [ ] field-11
|
||||
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
height=7
|
||||
|
||||
Type to search
|
||||
↑
|
||||
[ ] field-10
|
||||
› [ ] field-11
|
||||
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
+4
-2
@@ -7,8 +7,10 @@ expression: "render_lines(&view, 72)"
|
||||
Turn skills on or off. Your changes are saved automatically.
|
||||
|
||||
Type to search skills
|
||||
>
|
||||
|
||||
› [x] Repo Scout Summarize the repo layout
|
||||
[ ] Changelog Writer Draft release notes
|
||||
|
||||
Press space or enter to toggle; esc to close
|
||||
|
||||
|
||||
space/enter toggle · esc close
|
||||
|
||||
+9
-5
@@ -1,14 +1,18 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/skills_toggle_view.rs
|
||||
assertion_line: 569
|
||||
expression: "render_lines(&view, 48)"
|
||||
---
|
||||
|
||||
Enable/Disable Skills
|
||||
Turn skills on or off. Your changes are save
|
||||
Turn skills on or off. Your changes are
|
||||
saved automatically.
|
||||
|
||||
Type to search skills
|
||||
>
|
||||
› [x] superpowers-systematic-d… Find root cau…
|
||||
[ ] superpowers-verification… Verify comple…
|
||||
|
||||
Press space or enter to toggle; esc to close
|
||||
› [x] superpowers-systematic-debugging (polish)
|
||||
[ ] superpowers-verification-before-completio…
|
||||
|
||||
|
||||
|
||||
space/enter toggle · esc close
|
||||
|
||||
+6
-4
@@ -7,8 +7,10 @@ expression: "render_lines(&view, 96)"
|
||||
Turn skills on or off. Your changes are saved automatically.
|
||||
|
||||
Type to search skills
|
||||
>
|
||||
› [x] superpowers-systematic-debugging (polish) Find root causes before fixing bugs
|
||||
[ ] superpowers-verification-before-completion (polish) Verify completion before claiming …
|
||||
|
||||
Press space or enter to toggle; esc to close
|
||||
› [x] superpowers-systematic-debugging (polish) Find root causes before fixing bugs
|
||||
[ ] superpowers-verification-before-completion (polish) Verify completion before claiming su…
|
||||
|
||||
|
||||
|
||||
space/enter toggle · esc close
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect::<Vec<
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] hostname Current machine hostname (omitted when unavailable)
|
||||
@@ -16,6 +16,6 @@ expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect::<Vec<
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
[ ] project-name Project name (omitted when unavailable)
|
||||
|
||||
↓
|
||||
ssh-build-01.example.com · ~/codex-rs
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+5
-5
@@ -7,15 +7,15 @@ expression: "render_lines(&view, 100)"
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitte…
|
||||
[x] estimated-thread-cost Estimated current-thread cost in USD (Enterprise workspaces only; om…
|
||||
[x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitted …
|
||||
[x] estimated-thread-cost Estimated current-thread cost in USD (Enterprise workspaces only; omit…
|
||||
[ ] model Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
|
||||
↓
|
||||
5.2 credits · ~$1.82
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+4
-4
@@ -7,15 +7,15 @@ expression: "render_lines(&view, 72)"
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] model Current model name
|
||||
[x] current-dir Current working directory
|
||||
[x] git-branch Current Git branch (omitted when unavaila…
|
||||
[x] git-branch Current Git branch (omitted when unavailabl…
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
[ ] project-name Project name (omitted when unavailable)
|
||||
|
||||
↓
|
||||
gpt-5-codex · ~/codex-rs · jif/statusline-preview
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ expression: "render_lines(&view, 84)"
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] project-name Project name (falls back to current directory name)
|
||||
[x] activity Spinner while working, action-required message while blocked.
|
||||
[x] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
@@ -16,6 +16,6 @@ expression: "render_lines(&view, 84)"
|
||||
[ ] current-dir Current working directory
|
||||
[ ] thread-name Current thread name (omitted when unnamed)
|
||||
[ ] git-branch Current Git branch (omitted when unavailable)
|
||||
|
||||
↓
|
||||
[ ! ] Action Required | my-project | Working | thread title
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
@@ -305,7 +305,6 @@ use crate::key_hint::KeyBindingListExt;
|
||||
use crate::keymap::ChatKeymap;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crate::render::Insets;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::FlexRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableExt;
|
||||
|
||||
@@ -213,32 +213,27 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn connectors_loading_popup_params(&self) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Apps".bold()));
|
||||
header.push(Line::from("Loading installed and available apps...".dim()));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(CONNECTORS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Apps".to_string()),
|
||||
subtitle: Some("Loading installed and available apps...".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Loading apps...".to_string(),
|
||||
description: Some("This updates when the full list is ready.".to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
fn connectors_error_popup_params(&self) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Apps".bold()));
|
||||
header.push(Line::from("Failed to load apps.".dim()));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(CONNECTORS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(self.bottom_pane.standard_popup_hint_line()),
|
||||
title: Some("Apps".to_string()),
|
||||
subtitle: Some("Failed to load apps.".to_string()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "App directory unavailable".to_string(),
|
||||
@@ -260,7 +255,7 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,14 +269,12 @@ impl ChatWidget {
|
||||
.iter()
|
||||
.filter(|connector| connector.is_accessible)
|
||||
.count();
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Apps".bold()));
|
||||
header.push(Line::from(
|
||||
"Use $ to insert an installed app into your prompt.".dim(),
|
||||
));
|
||||
header.push(Line::from(
|
||||
format!("Installed {installed} of {total} available apps.").dim(),
|
||||
));
|
||||
let header = Paragraph::new(vec![
|
||||
Line::from("Apps".bold()),
|
||||
Line::from("Use $ to insert an installed app into your prompt.".dim()),
|
||||
Line::from(format!("Installed {installed} of {total} available apps.").dim()),
|
||||
])
|
||||
.wrap(Wrap { trim: false });
|
||||
let initial_selected_idx = selected_connector_id.and_then(|selected_connector_id| {
|
||||
connectors
|
||||
.iter()
|
||||
@@ -351,15 +344,15 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(CONNECTORS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(self.bottom_pane.standard_popup_hint_line()),
|
||||
items,
|
||||
is_searchable: true,
|
||||
search_placeholder: Some("Type to search apps".to_string()),
|
||||
col_width_mode: ColumnWidthMode::AutoAllRows,
|
||||
initial_selected_idx,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -340,6 +340,7 @@ impl Renderable for PluginDisclosureLine {
|
||||
impl ChatWidget {
|
||||
pub(super) fn plugins_loading_popup_params(&self) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
@@ -353,12 +354,13 @@ impl ChatWidget {
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn marketplace_add_loading_popup_params(&self) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
@@ -374,7 +376,7 @@ impl ChatWidget {
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,14 +386,12 @@ impl ChatWidget {
|
||||
marketplace_name: String,
|
||||
marketplace_display_name: String,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("Remove {marketplace_display_name} marketplace?").dim(),
|
||||
));
|
||||
header.push(Line::from(
|
||||
"This removes the configured marketplace from Codex.".dim(),
|
||||
));
|
||||
let header = Paragraph::new(vec![
|
||||
Line::from("Plugins".bold()),
|
||||
Line::from(format!("Remove {marketplace_display_name} marketplace?").dim()),
|
||||
Line::from("This removes the configured marketplace from Codex.".dim()),
|
||||
])
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
let cwd_for_remove = self.config.cwd.to_path_buf();
|
||||
let cwd_for_cancel = self.config.cwd.to_path_buf();
|
||||
@@ -400,6 +400,7 @@ impl ChatWidget {
|
||||
let plugins_response_for_on_cancel = plugins_response.clone();
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(Line::from(vec![
|
||||
@@ -449,7 +450,7 @@ impl ChatWidget {
|
||||
response: plugins_response_for_on_cancel.clone(),
|
||||
});
|
||||
})),
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,22 +458,18 @@ impl ChatWidget {
|
||||
&self,
|
||||
marketplace_display_name: &str,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("Removing {marketplace_display_name}...").dim(),
|
||||
));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some(format!("Removing {marketplace_display_name}...")),
|
||||
items: vec![SelectionItem {
|
||||
name: "Removing marketplace...".to_string(),
|
||||
description: Some("This updates when marketplace removal completes.".to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +481,7 @@ impl ChatWidget {
|
||||
.map(|name| format!("Upgrading {name} marketplace..."))
|
||||
.unwrap_or_else(|| "Upgrading marketplaces...".to_string());
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
@@ -497,7 +495,7 @@ impl ChatWidget {
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +504,7 @@ impl ChatWidget {
|
||||
plugin_display_name: &str,
|
||||
) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(DelayedLoadingHeader::new(
|
||||
self.frame_requester.clone(),
|
||||
@@ -519,7 +518,7 @@ impl ChatWidget {
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,22 +526,18 @@ impl ChatWidget {
|
||||
&self,
|
||||
plugin_display_name: &str,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("Installing {plugin_display_name}...").dim(),
|
||||
));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some(format!("Installing {plugin_display_name}...")),
|
||||
items: vec![SelectionItem {
|
||||
name: "Installing plugin...".to_string(),
|
||||
description: Some("This updates when plugin installation completes.".to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,48 +545,38 @@ impl ChatWidget {
|
||||
&self,
|
||||
plugin_display_name: &str,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("Uninstalling {plugin_display_name}...").dim(),
|
||||
));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some(format!("Uninstalling {plugin_display_name}...")),
|
||||
items: vec![SelectionItem {
|
||||
name: "Uninstalling plugin...".to_string(),
|
||||
description: Some("This updates when the plugin removal completes.".to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_error_popup_params(&self, err: &str) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from("Failed to load plugins.".dim()));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some("Failed to load plugins.".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Plugin marketplace unavailable".to_string(),
|
||||
description: Some(err.to_string()),
|
||||
is_disabled: true,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn marketplace_add_error_popup_params(&self) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from("Failed to add marketplace.".dim()));
|
||||
|
||||
let mut items = vec![
|
||||
SelectionItem {
|
||||
name: "Marketplace add failed".to_string(),
|
||||
@@ -629,11 +614,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some("Failed to add marketplace.".to_string()),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,10 +629,6 @@ impl ChatWidget {
|
||||
marketplace_name: &str,
|
||||
marketplace_display_name: &str,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from("Failed to remove marketplace.".dim()));
|
||||
|
||||
let marketplace_name = marketplace_name.to_string();
|
||||
let marketplace_display_name = marketplace_display_name.to_string();
|
||||
let mut items = vec![
|
||||
@@ -686,11 +669,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some("Failed to remove marketplace.".to_string()),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,10 +684,6 @@ impl ChatWidget {
|
||||
err: &str,
|
||||
plugins_response: Option<&PluginListResponse>,
|
||||
) -> SelectionViewParams {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from("Failed to load plugin details.".dim()));
|
||||
|
||||
let mut items = vec![SelectionItem {
|
||||
name: "Plugin detail unavailable".to_string(),
|
||||
description: Some(err.to_string()),
|
||||
@@ -726,11 +707,13 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
title: Some("Plugins".to_string()),
|
||||
subtitle: Some("Failed to load plugin details.".to_string()),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,7 +738,8 @@ impl ChatWidget {
|
||||
PLUGIN_ROW_PREFIX_WIDTH + UnicodeWidthStr::width(display_name.as_str())
|
||||
})
|
||||
.chain([UnicodeWidthStr::width("Add marketplace")])
|
||||
.max();
|
||||
.max()
|
||||
.map(|width| width.min(/*other*/ 36));
|
||||
let installed_entries = all_entries
|
||||
.iter()
|
||||
.filter(|(_, plugin, _)| plugin.installed)
|
||||
@@ -950,6 +934,7 @@ impl ChatWidget {
|
||||
active_tab_id.and_then(|tab_id| plugin_tab_id_matching_saved_id(&tab_id, &tabs));
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(()),
|
||||
footer_hint: Some(plugins_popup_hint_line(
|
||||
@@ -957,6 +942,7 @@ impl ChatWidget {
|
||||
)),
|
||||
tab_footer_hints,
|
||||
tabs,
|
||||
reserve_result_rows: true,
|
||||
initial_tab_id,
|
||||
is_searchable: true,
|
||||
search_placeholder: Some("Type to search plugins".to_string()),
|
||||
@@ -964,7 +950,7 @@ impl ChatWidget {
|
||||
row_display: SelectionRowDisplay::SingleLine,
|
||||
name_column_width,
|
||||
initial_selected_idx,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,10 +993,16 @@ impl ChatWidget {
|
||||
let display_name = plugin_display_name(&plugin.summary);
|
||||
let detail_status_label = plugin_detail_status_label(&plugin.summary);
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(),
|
||||
));
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from("Plugins".bold()))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from(
|
||||
format!("{display_name} · {detail_status_label} · {marketplace_label}").bold(),
|
||||
))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
if !plugin.summary.installed {
|
||||
header.push(PluginDisclosureLine {
|
||||
line: Line::from(vec![
|
||||
@@ -1025,7 +1017,10 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
if let Some(description) = plugin_detail_description(plugin) {
|
||||
header.push(Line::from(description.dim()));
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from(description.dim()))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
}
|
||||
|
||||
let cwd = self.config.cwd.to_path_buf();
|
||||
@@ -1156,12 +1151,13 @@ impl ChatWidget {
|
||||
});
|
||||
|
||||
SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
col_width_mode: ColumnWidthMode::AutoAllRows,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,30 +1298,31 @@ fn plugins_popup_hint_line(
|
||||
) -> Line<'static> {
|
||||
match (can_remove_marketplace, can_upgrade_marketplace) {
|
||||
(true, true) => Line::from(
|
||||
"ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close",
|
||||
"ctrl+u upgrade · ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close",
|
||||
),
|
||||
(true, false) => {
|
||||
Line::from("ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close")
|
||||
Line::from("ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close")
|
||||
}
|
||||
(false, true) => {
|
||||
Line::from("ctrl + u upgrade · space toggle · ←/→ tabs · enter details · esc close")
|
||||
Line::from("ctrl+u upgrade · space toggle · ←/→ tabs · enter details · esc close")
|
||||
}
|
||||
(false, false) => Line::from(
|
||||
"space enable/disable · ←/→ select marketplace · enter view details · esc close",
|
||||
),
|
||||
(false, false) => Line::from("←/→ tabs · enter details · space toggle · esc close"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugin_detail_hint_line() -> Line<'static> {
|
||||
Line::from("Press esc to close.")
|
||||
Line::from("esc close")
|
||||
}
|
||||
|
||||
pub(super) fn plugins_header(subtitle: String, count_line: String) -> Box<dyn Renderable> {
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(subtitle.dim()));
|
||||
header.push(Line::from(count_line.dim()));
|
||||
Box::new(header)
|
||||
Box::new(
|
||||
Paragraph::new(vec![
|
||||
Line::from("Plugins".bold()),
|
||||
Line::from(subtitle.dim()),
|
||||
Line::from(count_line.dim()),
|
||||
])
|
||||
.wrap(Wrap { trim: false }),
|
||||
)
|
||||
}
|
||||
|
||||
fn dedupe_plugin_entries<'a>(
|
||||
|
||||
@@ -865,14 +865,26 @@ impl ChatWidget {
|
||||
"Install the required Apps in ChatGPT to continue:"
|
||||
};
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(Line::from("Plugins".bold()));
|
||||
header.push(Line::from(
|
||||
format!("{} plugin installed.", flow.plugin_display_name).bold(),
|
||||
));
|
||||
header.push(Line::from(
|
||||
format!("App setup {current}/{total}: {}", app.name).dim(),
|
||||
));
|
||||
header.push(Line::from(status_label.dim()));
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from("Plugins".bold()))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from(
|
||||
format!("{} plugin installed.", flow.plugin_display_name).bold(),
|
||||
))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from(
|
||||
format!("App setup {current}/{total}: {}", app.name).dim(),
|
||||
))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
header.push(
|
||||
ratatui::widgets::Paragraph::new(Line::from(status_label.dim()))
|
||||
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
@@ -943,12 +955,13 @@ impl ChatWidget {
|
||||
});
|
||||
|
||||
Some(SelectionViewParams {
|
||||
appearance: crate::bottom_pane::SelectionAppearance::Picker,
|
||||
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
|
||||
header: Box::new(header),
|
||||
footer_hint: Some(plugin_detail_hint_line()),
|
||||
items,
|
||||
col_width_mode: ColumnWidthMode::AutoAllRows,
|
||||
..Default::default()
|
||||
..SelectionViewParams::picker()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 2317
|
||||
expression: snapshot
|
||||
---
|
||||
› $
|
||||
@@ -14,9 +15,10 @@ expression: snapshot
|
||||
App setup 1/1: % Arabica UAE
|
||||
Already installed in this session.
|
||||
|
||||
|
||||
ChatGPT apps link unavailable This app did not provide an install/manage
|
||||
URL.
|
||||
› 1. Continue Advance to the next app.
|
||||
2. Skip remaining app setup Stop this follow-up flow for this plugin.
|
||||
|
||||
Press esc to close.
|
||||
esc close
|
||||
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 2796
|
||||
expression: popup
|
||||
---
|
||||
Apps
|
||||
Failed to load apps.
|
||||
|
||||
|
||||
App directory unavailable The app directory request failed. Retry, or
|
||||
press Esc to continue.
|
||||
› 1. Retry Reload installed and available apps.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ expression: before
|
||||
Apps
|
||||
Loading installed and available apps...
|
||||
|
||||
|
||||
› Loading apps... This updates when the full list is ready.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
+2
-1
@@ -8,9 +8,10 @@ Before account change:
|
||||
Installed 1 of 1 available apps.
|
||||
|
||||
Type to search apps
|
||||
|
||||
› Previous Account App Installed. App link unavailable.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
After account change:
|
||||
› Ask Codex to do anything
|
||||
|
||||
+6
-5
@@ -1,16 +1,17 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 3142
|
||||
assertion_line: 3203
|
||||
expression: popup
|
||||
---
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
› [ ] Network proxy Apply network proxy restrictions to
|
||||
sandboxed sessions that already have
|
||||
network access.
|
||||
|
||||
› [ ] Network proxy Apply network proxy restrictions to sandboxed
|
||||
sessions that already have network access.
|
||||
[ ] Prevent sleep while running Keep your computer awake while Codex is
|
||||
running a thread.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 3167
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Experimental features
|
||||
@@ -9,4 +10,6 @@ expression: "render_bottom_popup(&chat, 80)"
|
||||
|
||||
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-2
@@ -1,16 +1,17 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 3083
|
||||
expression: popup
|
||||
---
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
|
||||
› [ ] JavaScript REPL Enable a persistent Node-backed JavaScript REPL for
|
||||
interactive website debugging and other inline
|
||||
JavaScript execution capabilities.
|
||||
[x] Shell tool Allow the model to run shell commands.
|
||||
[ ] Voice conversations Talk with Codex using /voice.
|
||||
|
||||
Press space to select or enter to save
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
|
||||
+3
-1
@@ -6,6 +6,8 @@ expression: popup
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
|
||||
|
||||
› [x] Shell tool Allow the model to run shell commands.
|
||||
|
||||
Press space to select or ctrl+x enter to save
|
||||
|
||||
space toggle · ctrl+x enter save · esc save/close
|
||||
|
||||
+4
-6
@@ -10,7 +10,8 @@ expression: popup
|
||||
■ /tmp/hooks.json: failed to parse hooks config
|
||||
|
||||
Event Installed Active Description
|
||||
PreToolUse 0 0 Before a tool executes
|
||||
|
||||
› PreToolUse 0 0 Before a tool executes
|
||||
PermissionRequest 0 0 When permission is requested
|
||||
PostToolUse 0 0 After a tool executes
|
||||
PreCompact 0 0 Before context compaction
|
||||
@@ -18,9 +19,6 @@ expression: popup
|
||||
SessionStart 0 0 When a new session starts
|
||||
SessionEnd 0 0 Right before a session ends
|
||||
UserPromptSubmit 0 0 When the user submits a prompt
|
||||
SubagentStart 0 0 When a subagent is created
|
||||
SubagentStop 0 0 Right before a subagent ends its turn
|
||||
Stop 0 0 Right before Codex ends its turn
|
||||
Interrupt 0 0 Right before an interrupted turn is aborted
|
||||
↓
|
||||
|
||||
Press enter to view hooks; esc to close
|
||||
enter details · esc close
|
||||
|
||||
+4
-1
@@ -3,10 +3,13 @@ source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
expression: popup
|
||||
---
|
||||
Reset all memories?
|
||||
This clears local memory files and rollout summaries for the current Codex
|
||||
home.
|
||||
|
||||
|
||||
› Reset all memories Delete local memory files and rollout summaries.
|
||||
Go back Return to memory settings.
|
||||
|
||||
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
+5
-3
@@ -5,13 +5,15 @@ expression: popup
|
||||
Memories
|
||||
Choose how Codex uses and creates memories. Changes are saved to config.toml
|
||||
|
||||
› [x] Use memories Use memories in the following threads. Applied at
|
||||
next thread.
|
||||
|
||||
› [x] Use memories Use memories in the following threads. Applied at next
|
||||
thread.
|
||||
[ ] Generate memories Generate memories from the following threads. Current
|
||||
thread included.
|
||||
Reset all memories Clear local memory files and summaries. Existing
|
||||
threads stay intact.
|
||||
|
||||
|
||||
Learn more: https://developers.openai.com/codex/memories
|
||||
|
||||
Press space to toggle; enter to save or select
|
||||
space toggle · enter save/select · esc cancel
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ expression: popup
|
||||
Plugins
|
||||
Failed to load plugin details.
|
||||
|
||||
|
||||
Plugin detail unavailable Failed to load plugin details.
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
|
||||
Press esc to close.
|
||||
esc close
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
more.
|
||||
Turn Figma files into implementation context.
|
||||
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Install plugin Install this plugin now.
|
||||
Source Local
|
||||
@@ -17,4 +18,4 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Apps Figma, Slack
|
||||
MCP Servers figma-mcp, docs-mcp
|
||||
|
||||
Press esc to close.
|
||||
esc close
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Figma · Installed by admin · ChatGPT Marketplace
|
||||
Turn Figma files into implementation context.
|
||||
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
Installed by admin This plugin is installed by your workspace admin.
|
||||
Source Local
|
||||
@@ -15,4 +16,4 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Apps Figma, Slack
|
||||
MCP Servers figma-mcp, docs-mcp
|
||||
|
||||
Press esc to close.
|
||||
esc close
|
||||
|
||||
+3
-2
@@ -7,6 +7,7 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Data shared with this app is subject to the app's terms of service and privacy policy. Learn more.
|
||||
Workspace docs.
|
||||
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Install plugin Install this plugin now.
|
||||
Source Local
|
||||
@@ -15,5 +16,5 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Sharing Private · creator Test User · https://chatgpt.com/codex/plugins/share/docs
|
||||
Skills No plugin skills.
|
||||
Hooks No plugin hooks.
|
||||
|
||||
Press esc to close.
|
||||
↓
|
||||
esc close
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
more.
|
||||
Turn Figma files into implementation context.
|
||||
|
||||
|
||||
› 1. Back to plugins Return to the plugin list.
|
||||
2. Install plugin Install this plugin now.
|
||||
Source npm · @acme/figma-plugin@^1.2.0
|
||||
@@ -17,4 +18,4 @@ expression: strip_osc8_for_snapshot(&popup)
|
||||
Apps Figma, Slack
|
||||
MCP Servers figma-mcp, docs-mcp
|
||||
|
||||
Press esc to close.
|
||||
esc close
|
||||
|
||||
+10
-2
@@ -6,9 +6,17 @@ expression: popup
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 1 of 1 available plugins.
|
||||
|
||||
[All Plugins] Installed (1) OpenAI Curated Workspace Add Marketplace
|
||||
All Plugins Installed (1) OpenAI Curated Workspace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
|
||||
› [!] Admin Blocked Disabled Press Enter to view plugin details.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
←/→ tabs · enter details · space toggle · esc close
|
||||
|
||||
+7
-2
@@ -6,12 +6,17 @@ expression: popup
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 1 of 4 available plugins.
|
||||
|
||||
[All Plugins] Installed (1) OpenAI Curated Workspace Repo Marketplace Add Marketplace
|
||||
All Plugins Installed (1) OpenAI Curated Workspace Repo Marketplace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
|
||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||
[-] Bravo Search Available · OpenAI Curated · Search docs and tickets.
|
||||
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins.
|
||||
[-] Starter Admin assigned · OpenAI Curated · Included by default.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
|
||||
|
||||
|
||||
←/→ tabs · enter details · space toggle · esc close
|
||||
|
||||
+10
-2
@@ -6,9 +6,17 @@ expression: loaded_popup
|
||||
Workspace.
|
||||
This section loaded successfully.
|
||||
|
||||
All Plugins Installed (0) OpenAI Curated [Workspace] Add Marketplace
|
||||
All Plugins Installed (0) OpenAI Curated Workspace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
|
||||
› No workspace plugins available No workspace directory plugins are available.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
←/→ tabs · enter details · space toggle · esc close
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ expression: popup
|
||||
Loading available plugins...
|
||||
This updates when the marketplace list is ready.
|
||||
|
||||
|
||||
› Loading plugins... This updates when the marketplace list is ready.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
enter select · esc back
|
||||
|
||||
+1
-1
@@ -1,12 +1,12 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 411
|
||||
expression: confirmation
|
||||
---
|
||||
Plugins
|
||||
Remove Repo Marketplace marketplace?
|
||||
This removes the configured marketplace from Codex.
|
||||
|
||||
|
||||
› 1. Remove marketplace Remove this marketplace from the available plugin list.
|
||||
2. Back to plugins Keep this marketplace installed.
|
||||
|
||||
|
||||
+10
-3
@@ -1,15 +1,22 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 473
|
||||
expression: popup
|
||||
---
|
||||
Plugins
|
||||
Debug Marketplace installed successfully.
|
||||
Select the plugins you want to use and press Enter to install or view details.
|
||||
|
||||
All Plugins Installed (0) OpenAI Curated Workspace [Debug Marketplace] Add Marketplace
|
||||
All Plugins Installed (0) OpenAI Curated Workspace Debug Marketplace Add Marketplace
|
||||
|
||||
Type to search plugins
|
||||
|
||||
› [-] Debug Plugin Available Press Enter to install or view plugin details.
|
||||
|
||||
ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ctrl+u upgrade · ctrl+r remove · space toggle · ←/→ tabs · enter details · esc close
|
||||
|
||||
+10
-3
@@ -1,15 +1,22 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||
assertion_line: 1684
|
||||
expression: popup
|
||||
---
|
||||
Plugins
|
||||
Browse plugins from available marketplaces.
|
||||
Installed 0 of 3 available plugins.
|
||||
|
||||
[All Plugins] Installed (0) OpenAI Curated Workspace Add Marketplace
|
||||
All Plugins Installed (0) OpenAI Curated Workspace Add Marketplace
|
||||
|
||||
sla
|
||||
|
||||
› [-] Slack Available Press Enter to install or view plugin details.
|
||||
|
||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
←/→ tabs · enter details · space toggle · esc close
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] project-name Project name (omitted when unavailable)
|
||||
@@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
[ ] model Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
|
||||
↓
|
||||
my-project · feat/awesome-feature · thread title
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] project-name Project name (omitted when unavailable)
|
||||
@@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
[ ] model Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
|
||||
↓
|
||||
preview-live-root · feature/live-preview-branch · Live preview thread
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] project-name Project name (omitted when unavailable)
|
||||
@@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
[ ] model Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
|
||||
↓
|
||||
my-project · feature/mixed-preview · Mixed preview thread
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] monthly-limit Remaining usage on the monthly usage limit (omitted when unavailable)
|
||||
@@ -15,6 +15,6 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
|
||||
↓
|
||||
monthly 65% left · weekly 50% left
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+4
-4
@@ -6,15 +6,15 @@ expression: status_line_popup_snapshot(&mut chat)
|
||||
Select which items to display in the status line.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] Use theme colors Apply colors from the active /theme
|
||||
───────────────────────
|
||||
[x] workspace-headline Workspace notification headline (Enterprise workspaces only; omitted …
|
||||
[x] workspace-headline Workspace notification headline (Enterprise workspaces only; omitted wh…
|
||||
[ ] model Current model name
|
||||
[ ] model-with-reasoning Current model name with reasoning level
|
||||
[ ] reasoning Current reasoning level
|
||||
[ ] current-dir Current working directory
|
||||
[ ] project-name Project name (omitted when unavailable)
|
||||
|
||||
↓
|
||||
Workspace maintenance starts at 5pm
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] thread-title Current thread title, or thread identifier when unnamed
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
[x] task-progress Latest task progress from update_plan (omitted until available)
|
||||
@@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
[ ] current-dir Current working directory
|
||||
[ ] activity Spinner while working, action-required message while blocked.
|
||||
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
|
||||
↓
|
||||
thread title | feat/awesome-feature | Tasks 0/0
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] project-name Project name (falls back to current directory name)
|
||||
[x] thread-title Current thread title, or thread identifier when unnamed
|
||||
[x] git-branch Current Git branch (omitted when unavailable)
|
||||
@@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
[ ] current-dir Current working directory
|
||||
[ ] activity Spinner while working, action-required message while blocked.
|
||||
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
|
||||
↓
|
||||
preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] project-name Project name (falls back to current directory name)
|
||||
[x] thread-title Current thread title, or thread identifier when unnamed
|
||||
[x] task-progress Latest task progress from update_plan (omitted until available)
|
||||
@@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
[ ] activity Spinner while working, action-required message while blocked.
|
||||
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
[ ] thread-name Current thread name (omitted when unnamed)
|
||||
|
||||
↓
|
||||
project | Mixed preview thread | Tasks 0/0
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
|
||||
› [x] monthly-limit Remaining usage on the monthly usage limit (omitted when unavailable)
|
||||
[x] weekly-limit Remaining usage on the weekly usage limit (omitted when unavailable)
|
||||
[ ] app-name Codex app name
|
||||
@@ -15,6 +15,6 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
[ ] activity Spinner while working, action-required message while blocked.
|
||||
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
[ ] thread-name Current thread name (omitted when unnamed)
|
||||
|
||||
↓
|
||||
monthly 65% left | weekly 50% left
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
+5
-5
@@ -6,15 +6,15 @@ expression: terminal_title_popup_snapshot(&mut chat)
|
||||
Select which items to display in the terminal title.
|
||||
|
||||
Type to search
|
||||
>
|
||||
› [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitte…
|
||||
[x] estimated-thread-cost Estimated current-thread cost (Enterprise workspaces only; omitted w…
|
||||
|
||||
› [x] thread-credits Estimated current-thread credits (Enterprise workspaces only; omitted …
|
||||
[x] estimated-thread-cost Estimated current-thread cost (Enterprise workspaces only; omitted whe…
|
||||
[ ] app-name Codex app name
|
||||
[ ] project-name Project name (falls back to current directory name)
|
||||
[ ] current-dir Current working directory
|
||||
[ ] activity Spinner while working, action-required message while blocked.
|
||||
[ ] run-state Compact session run-state text (Ready, Working, Thinking)
|
||||
[ ] thread-name Current thread name (omitted when unnamed)
|
||||
|
||||
↓
|
||||
5.2 credits | ~$1.82
|
||||
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
|
||||
space toggle · ←/→ reorder · enter save · esc cancel
|
||||
|
||||
@@ -84,7 +84,7 @@ async fn plugins_popup_uses_product_labels_for_remote_and_personal_tabs() {
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(selected_tab, product_label, plugin_name, raw_label)| {
|
||||
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 120, selected_tab);
|
||||
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 120, product_label);
|
||||
assert!(
|
||||
popup.contains(product_label)
|
||||
&& popup.contains(plugin_name)
|
||||
|
||||
@@ -277,7 +277,7 @@ async fn plugins_popup_truncates_long_descriptions_in_list_rows() {
|
||||
.expect("expected verbose plugin row in popup");
|
||||
insta::assert_snapshot!(
|
||||
verbose_row,
|
||||
@" [-] Verbose Plugin Available · OpenAI Curated · This description…"
|
||||
@" [-] Verbose Plugin Available · OpenAI Curated · This description k…"
|
||||
);
|
||||
assert!(
|
||||
!popup
|
||||
@@ -379,8 +379,8 @@ async fn plugins_popup_upgrades_user_configured_git_marketplace_from_marketplace
|
||||
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||
assert!(
|
||||
popup.contains("Repo Marketplace.")
|
||||
&& popup.contains("ctrl + u upgrade")
|
||||
&& popup.contains("ctrl + r remove")
|
||||
&& popup.contains("ctrl+u upgrade")
|
||||
&& popup.contains("ctrl+r remove")
|
||||
&& popup.contains("Debug Plugin"),
|
||||
"expected upgradeable user-configured marketplace tab, got:\n{popup}"
|
||||
);
|
||||
@@ -476,8 +476,8 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
assert_chatwidget_snapshot!("plugins_popup_newly_installed_marketplace", popup);
|
||||
assert!(
|
||||
popup.contains("Debug Marketplace installed successfully.")
|
||||
&& popup.contains("ctrl + u upgrade")
|
||||
&& popup.contains("ctrl + r remove")
|
||||
&& popup.contains("ctrl+u upgrade")
|
||||
&& popup.contains("ctrl+r remove")
|
||||
&& popup.contains("Debug Plugin"),
|
||||
"expected marketplace add refresh to switch to the new marketplace tab, got:\n{popup}"
|
||||
);
|
||||
@@ -487,7 +487,7 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
|
||||
let reopened_popup = (0..8)
|
||||
.find_map(|_| {
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
if popup.contains("[Debug Marketplace]") {
|
||||
if popup.contains("Installed 0 of 1 Debug Marketplace plugins.") {
|
||||
Some(popup)
|
||||
} else {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
@@ -542,8 +542,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
||||
select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||
assert!(
|
||||
repo_tab.contains("Repo Marketplace.")
|
||||
&& repo_tab.contains("ctrl + u upgrade")
|
||||
&& repo_tab.contains("ctrl + r remove")
|
||||
&& repo_tab.contains("ctrl+u upgrade")
|
||||
&& repo_tab.contains("ctrl+r remove")
|
||||
&& repo_tab.contains("Debug Plugin"),
|
||||
"expected removable user-configured marketplace tab, got:\n{repo_tab}"
|
||||
);
|
||||
@@ -611,7 +611,7 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
||||
refreshed.contains("Browse plugins from available marketplaces.")
|
||||
&& !refreshed.contains("Repo Marketplace")
|
||||
&& !refreshed.contains("Debug Plugin")
|
||||
&& !refreshed.contains("ctrl + r remove"),
|
||||
&& !refreshed.contains("ctrl+r remove"),
|
||||
"expected refreshed plugin list without removed marketplace, got:\n{refreshed}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Windows sandbox prompts and warning surfaces for `ChatWidget`.
|
||||
|
||||
use super::*;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
|
||||
impl ChatWidget {
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
|
||||
Reference in New Issue
Block a user