mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Fit composer footer hints to available terminal space (#60683)
## Why Narrow composer footers can clip hints or crowd out mode indicators and actions. ## What changed Use a shared layout for navigation, actions, mode indicators, context, and warnings. Shorten or drop whole labels in priority order instead of clipping them. Add a second row when input modes and queue or active side-conversation actions fit separately but not together, keeping navigation and modes above actions. ## Testing Add coverage for fullscreen and scrollback layouts, narrow Plan mode hints, active side-conversation actions, oversized goal indicators, and warning hit areas. GitOrigin-RevId: 849a53d45ef1b36b40c52282e329b48ec7d7d12e
This commit is contained in:
+1
-1
@@ -50,7 +50,7 @@ Conversation
|
||||
› draft stays
|
||||
|
||||
GPT-5.6-Sol def…
|
||||
tab to queue mes
|
||||
tab to queue
|
||||
|
||||
80 columns, 18 rows, running=true, queued=true
|
||||
Conversation row 18
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ transcript row 39
|
||||
here
|
||||
|
||||
GPT-5.6-Sol def…
|
||||
tab to queue mes
|
||||
tab to queue
|
||||
|
||||
6 columns, 18 rows
|
||||
row 38
|
||||
@@ -49,8 +49,8 @@ row 39
|
||||
e
|
||||
|
||||
GPT-
|
||||
5.6…
|
||||
tab
|
||||
5.6-
|
||||
Sol…
|
||||
|
||||
80 columns, 6 rows
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
+2
-2
@@ -152,8 +152,8 @@ working=false, response rows=1, 12x12
|
||||
|
||||
› Ask Codex
|
||||
|
||||
gpt-test …
|
||||
? for shor
|
||||
gpt-test
|
||||
default…
|
||||
|
||||
working=false, response rows=1, 80x4
|
||||
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ expression: "render_bottom_popup(&app.chat_widget, 80)"
|
||||
› kept draft
|
||||
|
||||
gpt-test default
|
||||
Plan mode (⇧tab to cycle)
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/app/tests/background_exit_tests.rs
|
||||
assertion_line: 57
|
||||
expression: "render_bottom_popup(&app.chat_widget, 90)"
|
||||
---
|
||||
Quit and stop running sessions?
|
||||
|
||||
@@ -352,7 +352,6 @@ use super::footer::CollaborationModeIndicator;
|
||||
use super::footer::FooterMode;
|
||||
use super::footer::FooterProps;
|
||||
use super::footer::GoalStatusIndicator;
|
||||
use super::footer::SummaryLeft;
|
||||
use super::footer::can_show_left_with_context;
|
||||
use super::footer::context_window_line;
|
||||
use super::footer::esc_hint_mode;
|
||||
@@ -367,7 +366,6 @@ use super::footer::render_footer_hint_items;
|
||||
use super::footer::render_footer_line;
|
||||
use super::footer::reset_mode_after_activity;
|
||||
use super::footer::side_conversation_context_line;
|
||||
use super::footer::single_line_footer_layout;
|
||||
use super::footer::status_line_right_indicator_line;
|
||||
use super::footer::toggle_shortcut_mode;
|
||||
use super::mention_usage::MentionUsage;
|
||||
@@ -410,6 +408,7 @@ mod history_search;
|
||||
mod inline_input;
|
||||
mod mouse;
|
||||
pub(crate) use mouse::ClipboardPasteTarget;
|
||||
mod passive_hints;
|
||||
mod paste_input;
|
||||
mod popup_state;
|
||||
mod reconnect;
|
||||
@@ -4717,22 +4716,30 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
let transcript_hint_area = inset_footer_hint_area(footer_rect);
|
||||
let warning_notice = self
|
||||
.warning_notice_layout(transcript_hint_area, options)
|
||||
.filter(|_| !options.unfocused);
|
||||
let passive = self.passive_hints(options);
|
||||
let warning_notice = if !options.unfocused && passive.is_none() {
|
||||
self.warning_notice_layout(transcript_hint_area, options)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let warning_area = warning_notice.as_ref().map(|(area, _)| *area);
|
||||
match options.footer {
|
||||
let mut painted_warning = warning_area;
|
||||
match (options.footer, passive) {
|
||||
_ if options.unfocused => {}
|
||||
Some(footer) => {
|
||||
(_, Some(hints)) => {
|
||||
let hint_area = passive_hints::hint_area(footer_rect);
|
||||
painted_warning = hints.render(hint_area, buf);
|
||||
}
|
||||
(Some(footer), None) => {
|
||||
self.render_transcript_footer(transcript_hint_area, buf, footer, warning_area);
|
||||
}
|
||||
None if self.shortcuts_above_composer(options) => {
|
||||
(None, None) if self.shortcuts_above_composer(options) => {
|
||||
let props = self.hint_footer_props();
|
||||
super::shortcut_overlay::render(&props, inset_footer_hint_area(popup_rect), buf);
|
||||
super::shortcut_overlay::close_hint(&props, transcript_hint_area.width)
|
||||
.render(transcript_hint_area, buf);
|
||||
}
|
||||
None => {
|
||||
(None, None) => {
|
||||
let hint_rect = warning_notice::before_warning(footer_rect, warning_area);
|
||||
if let Some(input) = self.draft.textarea.vim_query() {
|
||||
input.render(inset_footer_hint_area(hint_rect), buf);
|
||||
@@ -4795,73 +4802,23 @@ impl ChatComposer {
|
||||
can_show_left_with_context(hint_rect, left_width, right_width);
|
||||
let has_override =
|
||||
self.footer.flash_visible() || active_footer_hint_override.is_some();
|
||||
let single_line_layout = if has_override {
|
||||
None
|
||||
} else {
|
||||
match footer_props.mode {
|
||||
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft => {
|
||||
// Both of these modes render the single-line footer style (with
|
||||
// either the shortcuts hint or the optional queue hint). We still
|
||||
// want the single-line collapse rules so the mode label can win over
|
||||
// the context indicator on narrow widths.
|
||||
Some(single_line_footer_layout(
|
||||
hint_rect,
|
||||
right_width,
|
||||
left_mode_indicator,
|
||||
show_cycle_hint,
|
||||
show_shortcuts_hint,
|
||||
show_queue_hint,
|
||||
footer_props.key_hints,
|
||||
))
|
||||
}
|
||||
FooterMode::EscHint
|
||||
| FooterMode::HistorySearch
|
||||
| FooterMode::QuitShortcutReminder
|
||||
| FooterMode::ShortcutOverlay => None,
|
||||
}
|
||||
};
|
||||
let passive_mode = matches!(
|
||||
footer_props.mode,
|
||||
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
|
||||
);
|
||||
let show_right = if has_override && status_line_active {
|
||||
can_show_left_and_context
|
||||
} else if matches!(
|
||||
footer_props.mode,
|
||||
FooterMode::EscHint
|
||||
| FooterMode::HistorySearch
|
||||
| FooterMode::QuitShortcutReminder
|
||||
| FooterMode::ShortcutOverlay
|
||||
) {
|
||||
false
|
||||
} else {
|
||||
single_line_layout
|
||||
.as_ref()
|
||||
.map(|(_, show_context)| *show_context)
|
||||
.unwrap_or(can_show_left_and_context)
|
||||
passive_mode && (!has_override || can_show_left_and_context)
|
||||
};
|
||||
|
||||
if let Some((summary_left, _)) = single_line_layout {
|
||||
match summary_left {
|
||||
SummaryLeft::Default => {
|
||||
render_footer_from_props(
|
||||
hint_rect,
|
||||
buf,
|
||||
&footer_props,
|
||||
left_mode_indicator,
|
||||
show_cycle_hint,
|
||||
show_shortcuts_hint,
|
||||
show_queue_hint,
|
||||
);
|
||||
}
|
||||
SummaryLeft::Custom(line) => {
|
||||
render_footer_line(hint_rect, buf, line);
|
||||
}
|
||||
SummaryLeft::None => {}
|
||||
}
|
||||
} else if self.footer.flash_visible() {
|
||||
if self.footer.flash_visible() {
|
||||
if let Some(flash) = self.footer.flash.as_ref() {
|
||||
Widget::render(&flash.line, inset_footer_hint_area(hint_rect), buf);
|
||||
}
|
||||
} else if let Some(items) = active_footer_hint_override {
|
||||
render_footer_hint_items(hint_rect, buf, items);
|
||||
} else {
|
||||
} else if !passive_mode {
|
||||
render_footer_from_props(
|
||||
hint_rect,
|
||||
buf,
|
||||
@@ -4878,7 +4835,7 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.footer.warning_notice_area.set(warning_area);
|
||||
self.footer.warning_notice_area.set(painted_warning);
|
||||
if let Some((warning_area, line)) = warning_notice {
|
||||
line.render(warning_area, buf);
|
||||
}
|
||||
|
||||
@@ -293,36 +293,9 @@ impl super::ChatComposer {
|
||||
if options.unfocused || self.shortcuts_above_composer(options) {
|
||||
return 1;
|
||||
}
|
||||
if self.footer.status_line_enabled
|
||||
&& options.footer.is_none()
|
||||
&& self.custom_footer_height().is_none()
|
||||
&& matches!(
|
||||
self.footer_mode(),
|
||||
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
|
||||
)
|
||||
{
|
||||
let mode = self.footer_mode();
|
||||
let navigation = mode == FooterMode::ComposerEmpty
|
||||
&& !self.is_in_paste_burst()
|
||||
&& (self.agents_navigation_available()
|
||||
|| self.footer.toggle_shortcuts_key.is_some());
|
||||
let queue = mode == FooterMode::ComposerHasDraft
|
||||
&& self.is_task_running
|
||||
&& self.footer.queue_key.is_some();
|
||||
return u16::from(
|
||||
self.warning_notice_layout(
|
||||
super::inset_footer_hint_area(Rect::new(0, 0, width, 1)),
|
||||
options,
|
||||
)
|
||||
.is_some()
|
||||
|| navigation
|
||||
|| queue
|
||||
|| self
|
||||
.mode_indicator_line(/*show_cycle_hint*/ false)
|
||||
.is_some()
|
||||
|| self.shell_mode_footer_line().is_some()
|
||||
|| self.footer.side_conversation_context_label.is_some(),
|
||||
);
|
||||
if let Some(hints) = self.passive_hints(options) {
|
||||
return hints
|
||||
.desired_rows(super::passive_hints::hint_area(Rect::new(0, 0, width, 1)).width);
|
||||
}
|
||||
options.footer.map_or_else(
|
||||
|| {
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
//! Fit whole composer hints with navigation and actions on the left and indicators on the right.
|
||||
//! Request a second row only when a compact input mode (Shell, Vim, Plan) and a
|
||||
//! current action (queue or active side conversation) fit separately but not together.
|
||||
//! In two rows, navigation and modes stay above actions and the remaining indicators.
|
||||
//! Shorten or drop labels in collapse order. If a row still overflows, refill it
|
||||
//! in interaction priority order, then paint the surviving labels in reading order.
|
||||
|
||||
use super::*;
|
||||
use crate::bottom_pane::footer::goal_status_indicator_line;
|
||||
use crate::bottom_pane::footer::mode_indicator_line;
|
||||
use crate::line_truncation::line_width;
|
||||
use crate::style::secondary_text_style;
|
||||
use ratatui::style::Styled;
|
||||
|
||||
// Declaration order is the reading order within each side.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum Kind {
|
||||
Agents,
|
||||
Shortcuts,
|
||||
Action,
|
||||
Side,
|
||||
SideNav,
|
||||
AgentName,
|
||||
Shell,
|
||||
Vim,
|
||||
Plan,
|
||||
Goal,
|
||||
Ide,
|
||||
Context,
|
||||
Warning,
|
||||
}
|
||||
|
||||
use Kind::*;
|
||||
|
||||
impl Kind {
|
||||
fn is_input_mode(self) -> bool {
|
||||
matches!(self, Shell | Vim | Plan)
|
||||
}
|
||||
|
||||
fn left(self) -> bool {
|
||||
matches!(self, Agents | Shortcuts | Action | Side | SideNav)
|
||||
}
|
||||
|
||||
fn row(self, two_rows: bool) -> usize {
|
||||
usize::from(
|
||||
two_rows && !self.is_input_mode() && !matches!(self, Agents | Shortcuts | SideNav),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn joined_width(first: usize, second: usize) -> usize {
|
||||
first + second + usize::from(first > 0 && second > 0) * 3
|
||||
}
|
||||
|
||||
struct Label {
|
||||
kind: Kind,
|
||||
forms: Vec<(usize, Line<'static>)>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
fn width(&self) -> usize {
|
||||
self.selected
|
||||
.and_then(|i| self.forms.get(i))
|
||||
.map_or(0, |(cells, _)| *cells)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Hints {
|
||||
labels: Vec<Label>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct Row {
|
||||
left: usize,
|
||||
right: usize,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
fn width(self) -> usize {
|
||||
self.left + self.right + usize::from(self.left > 0 && self.right > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hints {
|
||||
fn add(&mut self, kind: Kind, forms: impl IntoIterator<Item = Line<'static>>) {
|
||||
let forms: Vec<_> = forms
|
||||
.into_iter()
|
||||
.map(|line| (line_width(&line), line))
|
||||
.filter(|(cells, _)| *cells > 0)
|
||||
.collect();
|
||||
if !forms.is_empty() {
|
||||
self.labels.push(Label {
|
||||
kind,
|
||||
forms,
|
||||
selected: Some(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn desired_rows(&self, width: u16) -> u16 {
|
||||
let width = usize::from(width);
|
||||
let mut compact = Row::default();
|
||||
let mut any_fits = false;
|
||||
for label in &self.labels {
|
||||
let Some(&(cells, _)) = label.forms.last() else {
|
||||
continue;
|
||||
};
|
||||
any_fits |= cells <= width;
|
||||
let group = if label.kind.is_input_mode() {
|
||||
&mut compact.right
|
||||
} else if matches!(label.kind, Action | Side) {
|
||||
&mut compact.left
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let next = joined_width(*group, cells);
|
||||
if next <= width {
|
||||
*group = next;
|
||||
}
|
||||
}
|
||||
u16::from(any_fits) + u16::from(compact.width() > width)
|
||||
}
|
||||
|
||||
fn placement(&self, two_rows: bool) -> [Row; 2] {
|
||||
let mut rows = [Row::default(); 2];
|
||||
for label in &self.labels {
|
||||
let row = &mut rows[label.kind.row(two_rows)];
|
||||
let group = if label.kind.left() {
|
||||
&mut row.left
|
||||
} else {
|
||||
&mut row.right
|
||||
};
|
||||
*group = joined_width(*group, label.width());
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
fn fit(&mut self, width: usize, two: bool) -> [Row; 2] {
|
||||
let mut p = self.placement(two);
|
||||
for (kind, form) in [
|
||||
(Shortcuts, None),
|
||||
(Plan, Some(1)),
|
||||
(Warning, Some(1)),
|
||||
(Ide, Some(1)),
|
||||
(Action, Some(1)),
|
||||
(Warning, Some(2)),
|
||||
(Agents, None),
|
||||
] {
|
||||
if p[kind.row(two)].width() <= width {
|
||||
continue;
|
||||
}
|
||||
let Some(label) = self.labels.iter_mut().find(|label| label.kind == kind) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(i) = form
|
||||
&& label
|
||||
.forms
|
||||
.get(i)
|
||||
.is_none_or(|(cells, _)| *cells >= label.width())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
label.selected = form;
|
||||
p = self.placement(two);
|
||||
}
|
||||
let overflow = p.map(|row| row.width() > width);
|
||||
if !overflow.contains(&true) {
|
||||
return p;
|
||||
}
|
||||
// Refill only overflowing rows in interaction priority order.
|
||||
for label in &mut self.labels {
|
||||
if overflow[label.kind.row(two)] {
|
||||
label.selected = None;
|
||||
}
|
||||
}
|
||||
for kind in [
|
||||
Action, Side, Shell, Vim, Plan, Goal, Ide, Warning, Context, SideNav, AgentName,
|
||||
Agents, Shortcuts,
|
||||
] {
|
||||
if !overflow[kind.row(two)] {
|
||||
continue;
|
||||
}
|
||||
if let Some(i) = self.labels.iter().position(|label| label.kind == kind) {
|
||||
self.labels[i].selected = Some(self.labels[i].forms.len() - 1);
|
||||
if self.placement(two)[kind.row(two)].width() > width {
|
||||
self.labels[i].selected = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.placement(two)
|
||||
}
|
||||
|
||||
pub(super) fn render(mut self, area: Rect, buf: &mut Buffer) -> Option<Rect> {
|
||||
if area.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let two = area.height > 1;
|
||||
let rows = self.fit(usize::from(area.width), two);
|
||||
self.labels.sort_by_key(|label| label.kind);
|
||||
let mut cursors = rows.map(|row| [area.x, area.right() - row.right as u16]);
|
||||
let mut warning = None;
|
||||
for label in self.labels {
|
||||
let Some((cells, line)) = label.selected.map(|i| &label.forms[i]) else {
|
||||
continue;
|
||||
};
|
||||
let row = label.kind.row(two);
|
||||
let x = &mut cursors[row][usize::from(!label.kind.left())];
|
||||
let y = area.y + row as u16;
|
||||
// Keep the meter's extra right inset when it is alone on its side and space permits.
|
||||
let inset = label.kind == Context
|
||||
&& rows[row].right == *cells
|
||||
&& rows[row].width() < usize::from(area.width);
|
||||
let rect = Rect::new(*x - u16::from(inset), y, *cells as u16, 1);
|
||||
Widget::render(line, rect, buf);
|
||||
if label.kind == Warning {
|
||||
warning = Some(rect);
|
||||
}
|
||||
*x += *cells as u16;
|
||||
let end = if label.kind.left() {
|
||||
area.x + rows[row].left as u16
|
||||
} else {
|
||||
area.right()
|
||||
};
|
||||
if *x < end {
|
||||
buf.set_string(*x, y, " · ", secondary_text_style());
|
||||
*x += 3;
|
||||
}
|
||||
}
|
||||
warning
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcut(key: key_hint::ShortcutHint, action: &'static str) -> Line<'static> {
|
||||
let mut line = Line::from(key.spans());
|
||||
line.push_span(action.set_style(crate::style::secondary_text_style()));
|
||||
line
|
||||
}
|
||||
|
||||
pub(super) fn hint_area(area: Rect) -> Rect {
|
||||
let mut area = inset_footer_hint_area(area);
|
||||
area.width = area.width.saturating_sub(1);
|
||||
area
|
||||
}
|
||||
|
||||
impl ChatComposer {
|
||||
pub(super) fn passive_hints(&self, options: ComposerRenderOptions<'_>) -> Option<Hints> {
|
||||
let mode = self.footer_mode();
|
||||
if options.footer.is_some()
|
||||
|| self.custom_footer_height().is_some()
|
||||
|| !matches!(
|
||||
mode,
|
||||
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut hints = Hints::default();
|
||||
hints.add(Shell, self.shell_mode_footer_line());
|
||||
hints.add(Vim, self.vim_mode_indicator_span().map(Line::from));
|
||||
let can_navigate = self.has_focus && self.draft.input_enabled && !self.popup_active();
|
||||
let can_type = can_navigate && !self.blocks_direct_input;
|
||||
let show_cycle = can_type && !self.is_task_running;
|
||||
if let Some(plan) = self.footer.collaboration_mode_indicator {
|
||||
hints.add(
|
||||
Plan,
|
||||
[
|
||||
mode_indicator_line(Some(plan), show_cycle),
|
||||
mode_indicator_line(Some(plan), /*show_cycle_hint*/ false),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
} else {
|
||||
hints.add(
|
||||
Goal,
|
||||
goal_status_indicator_line(self.footer.goal_status_indicator.as_ref()),
|
||||
);
|
||||
}
|
||||
if self.footer.ide_context_active {
|
||||
hints.add(
|
||||
Ide,
|
||||
[Line::from("IDE context".cyan()), Line::from("IDE".cyan())],
|
||||
);
|
||||
}
|
||||
if mode == FooterMode::ComposerHasDraft
|
||||
&& can_type
|
||||
&& self.is_task_running
|
||||
&& let Some(key) = self.footer.queue_key
|
||||
{
|
||||
hints.add(
|
||||
Action,
|
||||
[
|
||||
shortcut(key, " to queue message"),
|
||||
shortcut(key, " to queue"),
|
||||
],
|
||||
);
|
||||
}
|
||||
if let Some(label) = &self.footer.side_conversation_context_label {
|
||||
let kind = if self.side_conversation_active {
|
||||
Side
|
||||
} else {
|
||||
SideNav
|
||||
};
|
||||
hints.add(kind, [side_conversation_context_line(label)]);
|
||||
}
|
||||
if !self.footer.status_line_enabled
|
||||
&& let Some(agent) = &self.footer.active_agent_label
|
||||
{
|
||||
hints.add(
|
||||
AgentName,
|
||||
[Line::from(agent.clone().set_style(secondary_text_style()))],
|
||||
);
|
||||
}
|
||||
if mode == FooterMode::ComposerEmpty && can_navigate && !self.is_in_paste_burst() {
|
||||
if self.agents_navigation_available() {
|
||||
hints.add(
|
||||
Agents,
|
||||
[shortcut(
|
||||
key_hint::plain(KeyCode::Left).into(),
|
||||
" for agents",
|
||||
)],
|
||||
);
|
||||
}
|
||||
if let Some(key) = self.footer.toggle_shortcuts_key {
|
||||
hints.add(Shortcuts, [shortcut(key, " for shortcuts")]);
|
||||
}
|
||||
}
|
||||
let show_warning = self.show_warning_notice(options);
|
||||
if !self.footer.status_line_enabled
|
||||
&& !self.footer.context_window_pending
|
||||
&& self.footer.side_conversation_context_label.is_none()
|
||||
&& !self.is_bang_shell_command()
|
||||
{
|
||||
if hints.labels.is_empty() && !show_warning {
|
||||
// A lone context meter retains its existing inset and narrow-width clipping.
|
||||
return None;
|
||||
}
|
||||
hints.add(
|
||||
Context,
|
||||
[context_window_line(
|
||||
self.footer.context_window_percent,
|
||||
self.footer.context_window_used_tokens,
|
||||
)],
|
||||
);
|
||||
}
|
||||
if show_warning {
|
||||
hints.add(Warning, self.warning_labels(options.warning_count));
|
||||
}
|
||||
Some(hints)
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
|
||||
expression: "frames.join(\"\\n---\\n\")"
|
||||
---
|
||||
fullscreen 120×2 empty
|
||||
Build ready
|
||||
← for agents · ? for shortcuts Vim: Normal · Plan mode (⇧tab to cycle) · IDE context · ⚠ 2 warnings · f2 to view
|
||||
---
|
||||
fullscreen 60×2 empty
|
||||
Build ready
|
||||
← for agents Vim: Normal · Plan mode · IDE · ⚠ 2 · f2
|
||||
---
|
||||
fullscreen 31×3 empty
|
||||
Build ready
|
||||
Vim: Normal · Plan mode
|
||||
---
|
||||
fullscreen 31×3 draft
|
||||
Build ready
|
||||
Vim: Normal · Plan mode
|
||||
tab to queue message IDE
|
||||
---
|
||||
fullscreen 31×2 draft
|
||||
Build ready
|
||||
tab to queue Vim: Normal
|
||||
---
|
||||
scrollback 31×4 draft
|
||||
Build ready
|
||||
tab to queue Vim: Normal
|
||||
---
|
||||
scrollback 31×1 draft
|
||||
tab to queue Vim: Normal
|
||||
+1
-1
@@ -14,4 +14,4 @@ expression: wide
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
---
|
||||
|
||||
|
||||
@@ -56,4 +56,4 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
› Continue the task
|
||||
|
||||
MODEL · ~/project · Context 20% used
|
||||
tab to queue message Plan mode
|
||||
tab to queue message Plan mode
|
||||
|
||||
+2
-2
@@ -3,6 +3,6 @@ source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
|
||||
expression: "frames.join(\"\\n---\\n\")"
|
||||
---
|
||||
codex-internal feat/statusline Build 1042 128 tests passed Deploy ready
|
||||
? for shortcuts Plan mode (⇧tab to cycle) ⚠ 2 warnings · f2 to view
|
||||
? for shortcuts Plan mode (⇧tab to cycle) · ⚠ 2 warnings · f2 to view
|
||||
---
|
||||
? for shortcuts Plan mode (⇧tab to cycle) ⚠ 2 warnings · f2 to view
|
||||
? for shortcuts Plan mode (⇧tab to cycle) · ⚠ 2 warnings · f2 to view
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ height 8
|
||||
|› [Image #1]draft |
|
||||
| |
|
||||
| one… |
|
||||
| Plan mode (⇧tab to cycle) |
|
||||
| Plan mode (⇧tab to cycle) |
|
||||
|
||||
height 9
|
||||
| |
|
||||
@@ -18,4 +18,4 @@ height 9
|
||||
|› [Image #1]draft |
|
||||
| |
|
||||
| one… |
|
||||
| Plan mode (⇧tab to cycle) |
|
||||
| Plan mode (⇧tab to cycle) |
|
||||
|
||||
@@ -66,11 +66,7 @@ fn extension_status_line_clips_links_to_footer_layout() {
|
||||
.collect();
|
||||
assert_eq!(
|
||||
linked_text,
|
||||
match (width, separate_status_line) {
|
||||
(12, false) => "P",
|
||||
(12, true) => "PR#",
|
||||
_ => "PR #42",
|
||||
},
|
||||
if width == 12 { "PR#" } else { "PR #42" },
|
||||
"{buffer:?}"
|
||||
);
|
||||
assert!(linked.iter().all(|cell| !cell.symbol().contains('…')));
|
||||
@@ -278,13 +274,17 @@ fn sidebar_focus_collapses_extension_status_to_its_dim_line() {
|
||||
let frames = [false, true, false].map(|unfocused| {
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line: true,
|
||||
warning_count: usize::from(unfocused),
|
||||
unfocused,
|
||||
..Default::default()
|
||||
};
|
||||
let height = composer.desired_height_with_options(/*width*/ 40, options);
|
||||
render_status(&composer, Rect::new(0, 0, 40, height), options)
|
||||
.1
|
||||
.join("\n")
|
||||
let (buffer, rows) = render_status(&composer, Rect::new(0, 0, 40, height), options);
|
||||
if unfocused {
|
||||
assert_eq!(composer.footer.warning_notice_area.get(), None);
|
||||
assert!(!buffer.content.iter().any(|cell| cell.symbol() == "⚠"));
|
||||
}
|
||||
rows.join("\n")
|
||||
});
|
||||
insta::assert_snapshot!(frames.join("\n---\n"), @"
|
||||
one
|
||||
@@ -468,7 +468,7 @@ fn extension_status_keeps_its_rows_when_effort_changes() {
|
||||
if separate_status_line {
|
||||
vec!["one", "two", "three"]
|
||||
} else {
|
||||
vec!["one…"]
|
||||
vec!["one", "two…"]
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -713,11 +713,28 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
|
||||
|
||||
// Configured items such as git-branch can have no value outside a repository.
|
||||
composer.set_status_line(/*status_line*/ None);
|
||||
let (empty_narrow, _) = render(&composer, /*width*/ 24, /*footer*/ None);
|
||||
assert!(
|
||||
empty_narrow.lines().last().unwrap().contains("Plan mode"),
|
||||
"{empty_narrow}"
|
||||
);
|
||||
for separate_status_line in [false, true] {
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line,
|
||||
..Default::default()
|
||||
};
|
||||
let width = 20;
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
composer.desired_height_with_options(width, options),
|
||||
);
|
||||
let (buf, _) = render_status(&composer, area, options);
|
||||
let hint: String = (0..width)
|
||||
.map(|x| buf[(x, area.bottom() - 1)].symbol())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
hint.trim(),
|
||||
"Plan mode",
|
||||
"separate_status_line={separate_status_line}"
|
||||
);
|
||||
}
|
||||
let (empty_wide, _) = render(&composer, /*width*/ 100, /*footer*/ None);
|
||||
assert!(
|
||||
empty_wide.contains("Plan mode (⇧tab to cycle)"),
|
||||
@@ -742,21 +759,113 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
|
||||
#[test]
|
||||
fn footer_layout_in_fullscreen_and_scrollback() {
|
||||
let mut composer = composer();
|
||||
composer.set_status_line(Some("Build ready".into()));
|
||||
composer.command_center_enabled = true;
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
composer.set_collaboration_modes_enabled(/*enabled*/ true);
|
||||
composer.set_collaboration_mode_indicator(Some(CollaborationModeIndicator::Plan));
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 31, /*height*/ 4,
|
||||
);
|
||||
for separate_status_line in [false, true] {
|
||||
composer.footer.ide_context_active = true;
|
||||
composer.set_status_line(Some("Build ready".into()));
|
||||
let mut frames = Vec::new();
|
||||
for (fullscreen, width, rows, drafting) in [
|
||||
(true, 120, 2, false),
|
||||
(true, 60, 2, false),
|
||||
(true, 31, 3, false),
|
||||
(true, 31, 3, true),
|
||||
(true, 31, 2, true),
|
||||
(false, 31, 4, true),
|
||||
(false, 31, 1, true),
|
||||
] {
|
||||
if drafting && !composer.is_task_running {
|
||||
composer.insert_str("draft");
|
||||
composer.set_task_running(/*running*/ true);
|
||||
}
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line,
|
||||
separate_status_line: fullscreen,
|
||||
warning_count: 2,
|
||||
..Default::default()
|
||||
};
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, 3 + rows);
|
||||
let layout = composer.layout_with_options(area, options);
|
||||
assert_eq!((layout.status.height, layout.footer.height), (0, 1));
|
||||
let (buf, _) = render_status(&composer, area, options);
|
||||
assert!(frame(&buf).lines().last().unwrap().contains("Plan"));
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render_with_options(area, &mut buf, /*mask_char*/ None, options);
|
||||
let lines = (layout.status.y..area.bottom())
|
||||
.map(|y| {
|
||||
(0..width)
|
||||
.map(|x| buf[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(hit) = composer.footer.warning_notice_area.get() {
|
||||
assert_eq!(buf[(hit.x, hit.y)].symbol(), "⚠");
|
||||
assert_eq!(hit.y, layout.footer.bottom() - 1);
|
||||
}
|
||||
frames.push(format!(
|
||||
"{} {width}×{rows} {}\n{}",
|
||||
if fullscreen {
|
||||
"fullscreen"
|
||||
} else {
|
||||
"scrollback"
|
||||
},
|
||||
if drafting { "draft" } else { "empty" },
|
||||
lines.join("\n")
|
||||
));
|
||||
}
|
||||
insta::assert_snapshot!(frames.join("\n---\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_side_action_keeps_plan_expanded_on_the_other_row() {
|
||||
let mut composer = composer();
|
||||
composer.set_collaboration_mode_indicator(Some(CollaborationModeIndicator::Plan));
|
||||
composer.set_side_conversation_active(/*active*/ true);
|
||||
let label = "Side from main thread · ctrl+/ to switch · ctrl+c to close";
|
||||
composer.set_side_conversation_context_label(Some(label.to_string()));
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line: true,
|
||||
warning_count: 2,
|
||||
..Default::default()
|
||||
};
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 63, /*height*/ 6,
|
||||
);
|
||||
let (buf, _) = render_status(&composer, area, options);
|
||||
let text = frame(&buf);
|
||||
let mut rows = text.lines().rev().map(|row| row.trim_matches('|').trim());
|
||||
assert_eq!(rows.next(), Some(label));
|
||||
assert!(rows.next().unwrap().contains("Plan mode (⇧tab to cycle)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_goal_yields_to_an_eligible_whole_hint_with_status_disabled() {
|
||||
let mut composer = composer();
|
||||
composer.set_status_line_enabled(/*enabled*/ false);
|
||||
composer.set_context_window(Some(100), /*used_tokens*/ None);
|
||||
let (text, _) = render(&composer, /*width*/ 18, /*footer*/ None);
|
||||
assert_eq!(text.lines().last().map(str::trim), Some("? for shortcuts"));
|
||||
let (text, _) = render(&composer, /*width*/ 20, /*footer*/ None);
|
||||
assert_eq!(
|
||||
text.lines().last().map(str::trim),
|
||||
Some("100% context left")
|
||||
);
|
||||
|
||||
composer.set_context_window_pending(/*pending*/ true);
|
||||
composer.set_goal_status_indicator(Some(GoalStatusIndicator::Paused));
|
||||
let (text, _) = render(&composer, /*width*/ 18, /*footer*/ None);
|
||||
assert_eq!(text.lines().last().map(str::trim), Some("? for shortcuts"));
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line: true,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(composer.footer_hint_height(/*width*/ 18, options), 1);
|
||||
composer.set_input_enabled(/*enabled*/ false, /*placeholder*/ None);
|
||||
assert_eq!(composer.footer_hint_height(/*width*/ 18, options), 0);
|
||||
let (text, _) = render(&composer, /*width*/ 80, /*footer*/ None);
|
||||
assert_eq!(
|
||||
text.lines().last().map(str::trim),
|
||||
Some("Goal paused (/goal resume)")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -64,6 +64,17 @@ impl ChatComposer {
|
||||
}
|
||||
|
||||
pub(super) fn warning_notice(&self, count: usize, width: u16) -> Line<'static> {
|
||||
let [full, compact, count] = self.warning_labels(count);
|
||||
if full.width() <= usize::from(width) {
|
||||
return full;
|
||||
}
|
||||
if compact.width() <= usize::from(width) {
|
||||
return compact;
|
||||
}
|
||||
truncate_line_with_ellipsis_if_overflow(count, usize::from(width))
|
||||
}
|
||||
|
||||
pub(super) fn warning_labels(&self, count: usize) -> [Line<'static>; 3] {
|
||||
let plural = if count == 1 { "" } else { "s" };
|
||||
let accent = crate::style::warning_notice_style();
|
||||
let secondary = crate::style::secondary_text_style();
|
||||
@@ -80,9 +91,6 @@ impl ChatComposer {
|
||||
.style(secondary);
|
||||
full.extend(key_hint::key_label_spans(&shortcut));
|
||||
full.push_span(" to view");
|
||||
if full.width() <= usize::from(width) {
|
||||
return full;
|
||||
}
|
||||
let mut compact = Line::from(vec![
|
||||
"⚠ ".into(),
|
||||
Span::styled(count.to_string(), accent),
|
||||
@@ -90,13 +98,9 @@ impl ChatComposer {
|
||||
])
|
||||
.style(secondary);
|
||||
compact.extend(key_hint::key_label_spans(&shortcut));
|
||||
if compact.width() <= usize::from(width) {
|
||||
return compact;
|
||||
}
|
||||
truncate_line_with_ellipsis_if_overflow(
|
||||
Line::from(vec!["⚠ ".into(), Span::styled(count.to_string(), accent)]).style(secondary),
|
||||
usize::from(width),
|
||||
)
|
||||
let count =
|
||||
Line::from(vec!["⚠ ".into(), Span::styled(count.to_string(), accent)]).style(secondary);
|
||||
[full, compact, count]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,10 @@ fn warning_notice_respects_shortcuts_and_interactive_hints() {
|
||||
composer.footer.show_warnings_key = Some(crate::key_hint::plain(KeyCode::F(12)).into());
|
||||
assert!(text(&render(&composer, /*width*/ 64, /*count*/ 2)).contains("f12 to view"));
|
||||
composer.footer.show_warnings_key = None;
|
||||
assert!(text(&render(&composer, /*width*/ 64, /*count*/ 2)).contains("⚠ 2 · /warnings"));
|
||||
assert!(
|
||||
text(&render(&composer, /*width*/ 64, /*count*/ 2))
|
||||
.contains("⚠ 2 warnings · /warnings to view")
|
||||
);
|
||||
let interactive = TranscriptFooter {
|
||||
text: Line::from("Find: needle").into(),
|
||||
cursor_column: Some(6),
|
||||
|
||||
@@ -21,23 +21,7 @@
|
||||
//! instruction. In that state, the footer may render the configured status line, the active
|
||||
//! agent label, side-conversation state, or some combination of those.
|
||||
//!
|
||||
//! Single-line collapse overview:
|
||||
//! 1. The composer decides the current `FooterMode` and hint flags, then calls
|
||||
//! `single_line_footer_layout` for the base single-line modes.
|
||||
//! 2. `single_line_footer_layout` applies the width-based fallback rules:
|
||||
//! (If this description is hard to follow, just try it out by resizing
|
||||
//! your terminal width; these rules were built out of trial and error.)
|
||||
//! - Start with the fullest left-side hint plus the right-side context.
|
||||
//! - When the queue hint is active, prefer keeping that queue hint visible,
|
||||
//! even if it means dropping the right-side context earlier; the queue
|
||||
//! hint may also be shortened before it is removed.
|
||||
//! - When the queue hint is not active but the mode cycle hint is applicable,
|
||||
//! drop "? for shortcuts" before dropping "(shift+tab to cycle)".
|
||||
//! - If "(shift+tab to cycle)" cannot fit, also hide the right-side
|
||||
//! context to avoid too many state transitions in quick succession.
|
||||
//! - Finally, try a mode-only line (with and without context), and fall
|
||||
//! back to no left-side footer if nothing can fit.
|
||||
//! 3. The composer renders the chosen line, the default `FooterProps` mapping, or no left content.
|
||||
//! The composer fits ordinary navigation, modes, actions, and warnings in `chat_composer::passive_hints`.
|
||||
pub(crate) mod footer_content;
|
||||
|
||||
use self::footer_content::FooterContent;
|
||||
@@ -292,6 +276,7 @@ pub(crate) fn render_footer_from_props(
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool {
|
||||
let max_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16);
|
||||
left_width <= max_width
|
||||
@@ -302,6 +287,7 @@ enum SummaryHintKind {
|
||||
None,
|
||||
Shortcuts,
|
||||
QueueMessage,
|
||||
#[cfg(test)]
|
||||
QueueShort,
|
||||
}
|
||||
|
||||
@@ -338,6 +324,7 @@ fn left_side_line(
|
||||
line.push_span(" to queue message".set_style(secondary_text_style()));
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
SummaryHintKind::QueueShort => {
|
||||
if let Some(key) = key_hints.queue {
|
||||
line.extend(key.spans());
|
||||
@@ -360,12 +347,14 @@ fn left_side_line(
|
||||
line
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) enum SummaryLeft {
|
||||
Default,
|
||||
Custom(Line<'static>),
|
||||
None,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Compute the single-line footer layout and whether the right-side context
|
||||
/// indicator can be shown alongside it.
|
||||
pub(crate) fn single_line_footer_layout(
|
||||
@@ -752,10 +741,7 @@ pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(S
|
||||
///
|
||||
/// This is the canonical FooterMode-to-text mapping. It powers transient,
|
||||
/// instructional states (shortcut overlay, Esc hint, quit reminder) and also
|
||||
/// the default rendering for base states when collapse is not applied (or when
|
||||
/// `single_line_footer_layout` returns `SummaryLeft::Default`). Collapse and
|
||||
/// fallback decisions live in `single_line_footer_layout`; this function only
|
||||
/// formats the chosen/default content.
|
||||
/// the default text for base states. The caller owns placement and collapse.
|
||||
fn footer_from_props_lines(
|
||||
props: &FooterProps,
|
||||
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ source: tui/src/bottom_pane/chat_composer_effort_tests.rs
|
||||
expression: "rows.join(\"\\n\")"
|
||||
---
|
||||
gpt-5.4 high · feature-branch
|
||||
? for shortcuts Plan mode
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ? for shortcuts · Plan mode (⇧tab to cycle) 100% context left "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" Plan mode (⇧tab to cycle) 100% context left "
|
||||
" Plan mode (⇧tab to cycle) · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" Plan mode (⇧tab to cycle) "
|
||||
" Plan mode · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" Plan mode "
|
||||
" Plan mode "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue message · Plan mode 98% context left "
|
||||
" tab to queue message Plan mode · 98% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue message · Plan mode "
|
||||
" tab to queue Plan mode "
|
||||
|
||||
+2
-2
@@ -9,5 +9,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" Plan mode "
|
||||
" Plan mode "
|
||||
" tab to queue "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue · Plan mode 98% context left "
|
||||
" tab to queue Plan mode · 98% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue · Plan mode "
|
||||
" tab to queue Plan mode "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue 98% context left "
|
||||
" tab to queue message 98% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" tab to queue message "
|
||||
" tab to queue "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used "
|
||||
" Shell mode "
|
||||
" Shell mode "
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 5289
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
@@ -11,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Normal "
|
||||
" Vim: Normal · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
"› / "
|
||||
" "
|
||||
" 100% context left | Vim: Insert "
|
||||
" Vim: Insert · 100% context left "
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 5360
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
@@ -11,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Normal "
|
||||
" Vim: Normal · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ? for shortcuts 100% context left | Vim: Insert "
|
||||
" ? for shortcuts Vim: Insert · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Insert "
|
||||
" Vim: Insert · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Replace "
|
||||
" Vim: Replace · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Replace "
|
||||
" Vim: Replace · 100% context left "
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 5328
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /diff show git diff (including untracked files) "
|
||||
@@ -11,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Replace "
|
||||
" Vim: Replace · 100% context left "
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 5389
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
@@ -11,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Normal "
|
||||
" Vim: Normal · 100% context left "
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left | Vim: Normal "
|
||||
" Vim: Normal · 100% context left "
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ expression: popup
|
||||
› /daybreak
|
||||
|
||||
profile: work · provider: server-updated
|
||||
Plan mode (⇧tab to cycle)
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ expression: snapshot
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ expression: snapshot
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: snapshot
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 max "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: terminal.backend()
|
||||
"› Ask a follow-up question "
|
||||
" "
|
||||
" GPT-5.6-Sol "
|
||||
" ? for shortcuts Side from main thread · ctrl+/ to switch · ctrl+c to close "
|
||||
" ? for shortcuts · Side from main thread · ctrl+/ to switch · ctrl+c to close "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: terminal.backend()
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol "
|
||||
" ? for shortcuts ctrl+/ for side "
|
||||
" ? for shortcuts · ctrl+/ for side "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: terminal.backend()
|
||||
"› Ask a follow-up question "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" Side from main thread · main needs input · ctrl+/ to switch · ctrl+c to close "
|
||||
" Side from main thread · main needs input · ctrl+/ to switch · ctrl+c to close "
|
||||
|
||||
+1
-1
@@ -9,4 +9,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default "
|
||||
" ? for shortcuts Side starting... "
|
||||
" ? for shortcuts · Side starting... "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 "
|
||||
" ? for shortcuts Pursuing goal (40K / 50K) "
|
||||
" ? for shortcuts Pursuing goal (40K / 50K) "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 "
|
||||
" ? for shortcuts Goal achieved (2d 23h 42m) "
|
||||
" ? for shortcuts Goal achieved (2d 23h 42m) "
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.2 medium "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) "
|
||||
|
||||
-1
@@ -10,4 +10,3 @@ expression: "setup.replace(&chat.config.cwd.display().to_string(), \"[CWD]\")"
|
||||
› Input disabled until setup completes.
|
||||
|
||||
GPT-5.6-Sol default · [CWD]
|
||||
? for shortcuts
|
||||
|
||||
@@ -341,7 +341,8 @@ async fn side_context_label_shows_hidden_side_snapshot() {
|
||||
chat.show_welcome_banner = false;
|
||||
chat.local_settings.tui.status_line = Some(vec!["model-name".to_string()]);
|
||||
chat.refresh_status_line();
|
||||
chat.set_side_conversation_context_label(Some("ctrl+/ for side".to_string()));
|
||||
let label = "ctrl+/ for side";
|
||||
chat.set_side_conversation_context_label(Some(label.to_string()));
|
||||
|
||||
let width = 80;
|
||||
let height = chat.desired_height(width);
|
||||
@@ -349,13 +350,22 @@ async fn side_context_label_shows_hidden_side_snapshot() {
|
||||
terminal
|
||||
.draw(|f| chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw hidden side conversation footer");
|
||||
let hint_start = width - "ctrl+/ for side".len() as u16 - 2;
|
||||
let hint = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content()
|
||||
.windows(label.len())
|
||||
.find(|cells| {
|
||||
cells
|
||||
.iter()
|
||||
.map(ratatui::buffer::Cell::symbol)
|
||||
.collect::<String>()
|
||||
== label
|
||||
})
|
||||
.expect("side shortcut");
|
||||
let key_style = crate::key_hint::ctrl(KeyCode::Char('/')).spans()[0].style;
|
||||
let secondary_style = crate::style::secondary_text_style();
|
||||
let styles = [hint_start, hint_start + 4, hint_start + 5, hint_start + 7].map(|x| {
|
||||
let cell = &terminal.backend().buffer()[(x, height - 1)];
|
||||
(cell.fg, cell.modifier)
|
||||
});
|
||||
let styles = [0, 4, 5, 7].map(|i| (hint[i].fg, hint[i].modifier));
|
||||
assert_eq!(
|
||||
styles,
|
||||
[key_style, key_style, key_style, secondary_style].map(|style| (
|
||||
|
||||
+5
-5
@@ -37,10 +37,10 @@ parent-29 │
|
||||
│child-29
|
||||
│
|
||||
│
|
||||
› parent draft- │› child draft!
|
||||
│
|
||||
GPT-6.1-Sol default │ GPT-6.1-Sol default · Agent
|
||||
│ Side from main thread · ⌃/ to swit
|
||||
› parent draft- │
|
||||
│› child draft!
|
||||
GPT-6.1-Sol default │
|
||||
│ GPT-6.1-Sol default · Agent
|
||||
|
||||
99 columns, reopened
|
||||
Side chat× +
|
||||
@@ -60,4 +60,4 @@ child-29
|
||||
› child draft!
|
||||
|
||||
GPT-6.1-Sol default · Agent
|
||||
Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
|
||||
@@ -20,7 +20,7 @@ Zoomed
|
||||
› xchild draft!
|
||||
|
||||
GPT-6.1-Sol default · Agent
|
||||
tab to queue message Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
tab to queue message · Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
Restored
|
||||
parent-24 │ Side chat× +
|
||||
│• unfinished answer
|
||||
@@ -39,7 +39,7 @@ parent-29
|
||||
› xparent draft-! │› xchild draft!
|
||||
second line │
|
||||
GPT-6.1-Sol default │ GPT-6.1-Sol default · Agent
|
||||
│ tab to queue message Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
│ tab to queue message · Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
Equalized
|
||||
parent-24 │ Side chat× +
|
||||
│• unfinished answer
|
||||
@@ -58,4 +58,4 @@ parent-29
|
||||
› xparent draft-! │› xchild draft!
|
||||
second line │
|
||||
GPT-6.1-Sol default │ GPT-6.1-Sol default · Agent
|
||||
│ tab to queue message Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
│ tab to queue message · Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@ expression: "crate::chatwidget::tests::helpers::render_bottom_popup(&chat, 40)"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.5 default · /tmp/project
|
||||
? for shortcuts Plan mode
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
-4
@@ -11,7 +11,6 @@ before:
|
||||
|
||||
› draft while loading
|
||||
|
||||
|
||||
after:
|
||||
|
||||
>_ OpenAI Codex (v<VERSION>)
|
||||
@@ -20,7 +19,6 @@ after:
|
||||
|
||||
› draft while loading
|
||||
|
||||
|
||||
---
|
||||
fork lookup fallback:
|
||||
before:
|
||||
@@ -31,7 +29,6 @@ before:
|
||||
|
||||
› draft while loading
|
||||
|
||||
|
||||
after:
|
||||
|
||||
>_ OpenAI Codex (v<VERSION>)
|
||||
@@ -40,7 +37,6 @@ after:
|
||||
|
||||
› draft while loading
|
||||
|
||||
|
||||
---
|
||||
resume picker cancellation:
|
||||
before:
|
||||
|
||||
-2
@@ -41,7 +41,6 @@ forking (48 columns):
|
||||
|
||||
› draft while loading
|
||||
|
||||
|
||||
---
|
||||
multiline (48 columns):
|
||||
|
||||
@@ -52,7 +51,6 @@ multiline (48 columns):
|
||||
› first startup line
|
||||
second startup line
|
||||
|
||||
|
||||
---
|
||||
narrow (18 columns):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user