mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Keep TUI status content separate from composer hints (#60680)
## Why Composer hints could replace configured status content, and mode indicators and warnings reduced the width available to status extensions. ## What changed - Give enabled status content a dedicated area below the composer in both inline and fullscreen layouts, preserving it during transient hint states. - Keep shortcuts, queue hints, mode indicators, and warnings in the hint row. - Measure extension status content at the full available width, allowing up to four rows while yielding space to the composer when height is limited. - Suppress composer hints behind centered dialogs. ## Testing Update layout assertions and snapshots for status persistence, extension resizing and clipping, warning placement, and reasoning-effort transitions. GitOrigin-RevId: f11c2e4fb53c35b69c2fbdbe80cef31bb28faca4
This commit is contained in:
+2
-1
@@ -7,4 +7,5 @@ terminal title: project
|
||||
footer:
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5.2 default · /tmp/project · ← for agents
|
||||
gpt-5.2 default · /tmp/project
|
||||
← for agents · ? for shortcuts
|
||||
|
||||
+2
-1
@@ -5,4 +5,5 @@ expression: normalize_snapshot_paths(popup)
|
||||
---
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5.2 default · /tmp/project · Keep this title · ← for agents
|
||||
gpt-5.2 default · /tmp/project · Keep this title
|
||||
← for agents · ? for shortcuts
|
||||
|
||||
@@ -662,7 +662,10 @@ async fn reconnect_exhaustion_and_unknown_initial_thread_stay_offline() -> Resul
|
||||
"reconnect_failed",
|
||||
format!(
|
||||
"{error}\n{}",
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 80)
|
||||
super::disconnect::normalize_reconnect_status(&render_bottom_popup(
|
||||
&app.chat_widget,
|
||||
/*width*/ 80
|
||||
))
|
||||
)
|
||||
);
|
||||
tokio::time::resume();
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn agents_navigation_opens_the_command_center_on_every_target() -> Result<
|
||||
let composer = render_bottom_popup(&app.chat_widget, /*width*/ 96);
|
||||
let footer = composer.lines().last().expect("composer footer").trim();
|
||||
insta::allow_duplicates! {
|
||||
insta::assert_snapshot!(normalize_snapshot_paths(footer), @"GPT-5.6-Sol default · /tmp/project · ← for agents");
|
||||
insta::assert_snapshot!(normalize_snapshot_paths(footer), @"← for agents · ? for shortcuts");
|
||||
}
|
||||
app.handle_tui_event(
|
||||
&mut tui,
|
||||
|
||||
@@ -17,10 +17,10 @@ use serde_json::json;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
fn normalize_reconnect_status(popup: &str) -> String {
|
||||
pub(super) fn normalize_reconnect_status(popup: &str) -> String {
|
||||
let status =
|
||||
regex_lite::Regex::new(r"(?m)^[•◦] (Reconnecting to server… )\([\dhms ]+\)").unwrap();
|
||||
status.replace(popup, "• ${1}(<elapsed>)").into_owned()
|
||||
regex_lite::Regex::new(r"(?m)^[•◦] (Reconnecting to server…|Reconnect failed — check the endpoint, then relaunch) \([\dhms ]+\)").unwrap();
|
||||
normalize_snapshot_paths(status.replace(popup, "• ${1} (<elapsed>)").into_owned())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -500,10 +500,11 @@ async fn replacement_uses_server_defaults_and_preserves_explicit_launch_settings
|
||||
if explicit == "saved" {
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 140)
|
||||
.replace(&server_config.cwd.display().to_string(), "<PROJECT>");
|
||||
insta::assert_snapshot!(rendered, @r"
|
||||
insta::assert_snapshot!(rendered, @"
|
||||
› Ask Codex to do anything
|
||||
|
||||
server-model high · <PROJECT> · ← for agents
|
||||
server-model high · <PROJECT>
|
||||
← for agents · ? for shortcuts
|
||||
");
|
||||
}
|
||||
if explicit.starts_with("profile_summary") {
|
||||
|
||||
+2
-1
@@ -3,9 +3,10 @@ source: tui/src/app/tests/active_reconnect_tests.rs
|
||||
expression: "format!(\"{error}\\n{}\", render_bottom_popup(&app.chat_widget, 80))"
|
||||
---
|
||||
Automatic reconnect timed out while connecting to the server.
|
||||
• Reconnect failed — check the endpoint, then relaunch (0s)
|
||||
• Reconnect failed — check the endpoint, then relaunch (<elapsed>)
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
⌃c quit
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ expression: "render_bottom_popup(&app.chat_widget, 80)"
|
||||
---
|
||||
› kept draft
|
||||
|
||||
gpt-test default Plan mode (⇧tab to cycle)
|
||||
gpt-test default
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
+1
@@ -12,3 +12,4 @@ expression: normalize_snapshot_paths(rendered)
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5.6-terra medium · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -5,3 +5,4 @@ expression: normalize_snapshot_paths(rendered)
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5.5 default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
-2
@@ -11,16 +11,16 @@ startup:
|
||||
|
||||
attached:
|
||||
|
||||
|
||||
› draft during startup
|
||||
|
||||
GPT-6.1-Sol default · /tmp/project
|
||||
|
||||
|
||||
owned=true
|
||||
startup:
|
||||
|
||||
› draft during startup
|
||||
|
||||
› draft during startup
|
||||
|
||||
|
||||
attached:
|
||||
|
||||
+1
@@ -16,3 +16,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&app.chat_widget, 80))
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ expression: "format!(\"{}\\n{}\",\nlines_to_single_string(&app.transcript_cells.
|
||||
|
||||
› keep this draft!
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
⌃c quit
|
||||
|
||||
+1
@@ -7,4 +7,5 @@ expression: "reconnect_status.replace(&render_bottom_popup(&app.chat_widget, 80)
|
||||
|
||||
› startup draft
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
⌃c quit
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ Other task:
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
Restored owner:
|
||||
voice ● listening f9 mute /voice stop
|
||||
mic ▁▁▁▁▁▁ codex ▁▁▁▁▁▁
|
||||
@@ -13,6 +14,7 @@ Restored owner:
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
>_ OpenAI Codex (v0.0.0)
|
||||
/tmp/project
|
||||
|
||||
+5
-1
@@ -13,6 +13,7 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
── Steered ──
|
||||
■ Conversation interrupted - use /feedback if something went wrong
|
||||
@@ -31,6 +32,7 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
── Rejected ──
|
||||
■ reply stream closed before submission
|
||||
@@ -39,7 +41,8 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
|
||||
|
||||
› follow up
|
||||
|
||||
tab to queue message 100% context left
|
||||
gpt-test default · /tmp/project
|
||||
tab to queue message
|
||||
|
||||
── Duplicate ──
|
||||
■ Conversation interrupted - use /feedback if something went wrong
|
||||
@@ -50,3 +53,4 @@ expression: "rendered_receipts.join(\"\\n\\n\")"
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
@@ -729,10 +729,11 @@ async fn fresh_startup_reads_destination_and_cleared_model_uses_catalog() -> Res
|
||||
if !remote {
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 80)
|
||||
.replace(&destination.path().display().to_string(), "<PROJECT>");
|
||||
insta::assert_snapshot!(rendered, @r"
|
||||
insta::assert_snapshot!(rendered, @"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-6.1-Sol high · <PROJECT>
|
||||
? for shortcuts
|
||||
");
|
||||
}
|
||||
let expected_cwd = if override_cwd {
|
||||
|
||||
@@ -360,7 +360,6 @@ use super::footer::footer_height;
|
||||
use super::footer::footer_hint_items_width;
|
||||
use super::footer::footer_line_width;
|
||||
use super::footer::inset_footer_hint_area;
|
||||
use super::footer::max_left_width_for_right;
|
||||
use super::footer::passive_footer_status_line;
|
||||
use super::footer::render_context_right;
|
||||
use super::footer::render_footer_from_props;
|
||||
@@ -371,7 +370,6 @@ 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::footer::uses_passive_footer_status_layout;
|
||||
use super::mention_usage::MentionUsage;
|
||||
use super::mentions_v2::MentionV2Popup;
|
||||
use super::mentions_v2::MentionV2Selection;
|
||||
@@ -397,7 +395,6 @@ use crate::render::RectExt;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::style::user_message_style;
|
||||
use crate::ui_consts::FOOTER_INDENT_COLS;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
@@ -911,12 +908,13 @@ impl ChatComposer {
|
||||
let style = IgnitionStyle::random(self.effort_animation_style);
|
||||
self.effort_ignition = Some(EffortIgnition::new(tier, style));
|
||||
self.effort_animation_style = Some(style);
|
||||
if self.footer.status_line_enabled
|
||||
&& let Some(FooterContent::HyperlinkLine(previous)) =
|
||||
passive_footer_status_line(&self.footer_props())
|
||||
if let Some(previous) = self.status_surface_content()
|
||||
&& !matches!(previous.as_ref(), FooterContent::Extension { .. })
|
||||
{
|
||||
self.effort_status_line_transition =
|
||||
Some(EffortStatusLineTransition::new(tier, previous));
|
||||
self.effort_status_line_transition = Some(EffortStatusLineTransition::new(
|
||||
tier,
|
||||
previous.into_owned().into_hyperlink_line(),
|
||||
));
|
||||
}
|
||||
if let Some(frame_requester) = &self.frame_requester {
|
||||
frame_requester.schedule_frame();
|
||||
@@ -4727,7 +4725,7 @@ impl ChatComposer {
|
||||
self.render_transcript_footer(transcript_hint_area, buf, footer, warning_area);
|
||||
}
|
||||
None if self.shortcuts_above_composer(options) => {
|
||||
let props = self.hint_footer_props(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);
|
||||
@@ -4738,24 +4736,23 @@ impl ChatComposer {
|
||||
input.render(inset_footer_hint_area(hint_rect), buf);
|
||||
} else if let Some(line) = self.history_search_footer_line() {
|
||||
render_footer_line(hint_rect, buf, line);
|
||||
} else if self.shows_shared_status(options) {
|
||||
self.render_status_surface(hint_rect, buf, options);
|
||||
} else {
|
||||
let footer_props = self.hint_footer_props(options);
|
||||
let show_cycle_hint = !self.has_separate_status(options)
|
||||
let footer_props = self.hint_footer_props();
|
||||
let show_cycle_hint = !self.popup_active()
|
||||
&& !self.blocks_direct_input
|
||||
&& self.draft.input_enabled
|
||||
&& !footer_props.is_task_running
|
||||
&& self.footer.collaboration_mode_indicator.is_some();
|
||||
let show_shortcuts_hint =
|
||||
footer_props.mode == FooterMode::ComposerEmpty && !self.is_in_paste_burst();
|
||||
let show_queue_hint = footer_props.mode == FooterMode::ComposerHasDraft
|
||||
&& footer_props.is_task_running;
|
||||
let status_line_active = uses_passive_footer_status_layout(&footer_props);
|
||||
let left_mode_indicator =
|
||||
if status_line_active || self.has_separate_status(options) {
|
||||
None
|
||||
} else {
|
||||
self.footer.collaboration_mode_indicator
|
||||
};
|
||||
let status_line_active = self.footer.status_line_enabled;
|
||||
let left_mode_indicator = if status_line_active {
|
||||
None
|
||||
} else {
|
||||
self.footer.collaboration_mode_indicator
|
||||
};
|
||||
let active_footer_hint_override = self.footer.hint_override.as_ref();
|
||||
let left_width = if self.footer.flash_visible() {
|
||||
self.footer
|
||||
@@ -4779,8 +4776,6 @@ impl ChatComposer {
|
||||
Some(side_conversation_context_line(label))
|
||||
} else if let Some(line) = self.shell_mode_footer_line() {
|
||||
Some(line)
|
||||
} else if self.has_separate_status(options) {
|
||||
None
|
||||
} else if status_line_active {
|
||||
let full = self.mode_indicator_line(show_cycle_hint);
|
||||
let compact = self.mode_indicator_line(/*show_cycle_hint*/ false);
|
||||
@@ -4798,7 +4793,7 @@ 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 || status_line_active {
|
||||
let single_line_layout = if has_override {
|
||||
None
|
||||
} else {
|
||||
match footer_props.mode {
|
||||
@@ -4823,7 +4818,9 @@ impl ChatComposer {
|
||||
| FooterMode::ShortcutOverlay => None,
|
||||
}
|
||||
};
|
||||
let show_right = if matches!(
|
||||
let show_right = if has_override && status_line_active {
|
||||
can_show_left_and_context
|
||||
} else if matches!(
|
||||
footer_props.mode,
|
||||
FooterMode::EscHint
|
||||
| FooterMode::HistorySearch
|
||||
@@ -4867,7 +4864,7 @@ impl ChatComposer {
|
||||
hint_rect,
|
||||
buf,
|
||||
&footer_props,
|
||||
self.footer.collaboration_mode_indicator,
|
||||
left_mode_indicator,
|
||||
show_cycle_hint,
|
||||
show_shortcuts_hint,
|
||||
show_queue_hint,
|
||||
|
||||
@@ -41,26 +41,16 @@ impl ChatComposer {
|
||||
area: Rect,
|
||||
options: ComposerRenderOptions<'_>,
|
||||
) -> ComposerLayout {
|
||||
let mut footer_hint_height = self.footer_hint_height(area.width, options);
|
||||
let mut status_height = self.status_surface_height(area.width, options);
|
||||
let shared = footer_hint_height > 1 && self.shows_shared_status(options);
|
||||
if shared || status_height > 1 {
|
||||
// Extra status rows yield to the draft, voice strip and other composer surfaces.
|
||||
let desired =
|
||||
self.height_with_rows(area.width, options, footer_hint_height, status_height);
|
||||
let rows = if shared {
|
||||
&mut footer_hint_height
|
||||
} else {
|
||||
&mut status_height
|
||||
};
|
||||
*rows = rows
|
||||
.saturating_sub(desired.saturating_sub(area.height))
|
||||
.max(1);
|
||||
}
|
||||
let status_height = status_height.min(
|
||||
area.height
|
||||
.saturating_sub(footer_hint_height.saturating_add(/*rhs*/ 3)),
|
||||
);
|
||||
let footer_hint_height = self.footer_hint_height(area.width, options);
|
||||
let wanted = self.status_surface_height(area.width, /*available*/ 4, options);
|
||||
let desired = self.height_with_rows(area.width, options, footer_hint_height, wanted);
|
||||
let status_height = wanted
|
||||
.saturating_sub(desired.saturating_sub(area.height))
|
||||
.max(wanted.min(1))
|
||||
.min(
|
||||
area.height
|
||||
.saturating_sub(footer_hint_height.saturating_add(3)),
|
||||
);
|
||||
let status = Rect {
|
||||
y: area
|
||||
.bottom()
|
||||
@@ -90,7 +80,7 @@ impl ChatComposer {
|
||||
let shortcuts_above = self.shortcuts_above_composer(options);
|
||||
let (composer_rect, popup_rect, mut footer_rect) = if shortcuts_above {
|
||||
let [shortcuts, composer, footer] = Layout::vertical([
|
||||
Constraint::Max(footer_height(&self.hint_footer_props(options), area.width)),
|
||||
Constraint::Max(footer_height(&self.hint_footer_props(), area.width)),
|
||||
Constraint::Min(3 + voice_rows),
|
||||
Constraint::Length(footer_hint_height),
|
||||
])
|
||||
@@ -213,7 +203,7 @@ impl ChatComposer {
|
||||
width,
|
||||
options,
|
||||
self.footer_hint_height(width, options),
|
||||
self.status_surface_height(width, options),
|
||||
self.status_surface_height(width, /*available*/ 4, options),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -241,7 +231,7 @@ impl ChatComposer {
|
||||
remote_images_separator,
|
||||
status_height,
|
||||
if self.shortcuts_above_composer(options) {
|
||||
footer_height(&self.hint_footer_props(options), width)
|
||||
footer_height(&self.hint_footer_props(), width)
|
||||
} else {
|
||||
0
|
||||
},
|
||||
|
||||
@@ -80,8 +80,10 @@ pub(crate) struct ComposerRenderOptions<'a> {
|
||||
pub(crate) working_tip: Option<&'a crate::turn_tip::TurnTip>,
|
||||
pub(crate) warning_count: usize,
|
||||
pub(crate) textarea_right_reserve: u16,
|
||||
/// Keep configured status below the composer while hints occupy the final row.
|
||||
/// The host is using the separate fullscreen transcript layout.
|
||||
pub(crate) separate_status_line: bool,
|
||||
/// A centered dialog owns input while the composer is painted behind it.
|
||||
pub(crate) is_dialog_backdrop: bool,
|
||||
/// Topmost frame row available for suggestions overlaying the transcript.
|
||||
pub(crate) overlay_top: u16,
|
||||
/// The workspace paints the open Presets menu over the completed frame.
|
||||
@@ -284,8 +286,8 @@ impl super::ChatComposer {
|
||||
}
|
||||
|
||||
pub(super) fn footer_hint_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 {
|
||||
if self.shows_shared_status(options) {
|
||||
return self.status_line_height(width, options);
|
||||
if options.is_dialog_backdrop {
|
||||
return 0;
|
||||
}
|
||||
if self.show_warning_notice(options) || self.shortcuts_above_composer(options) {
|
||||
return 1;
|
||||
@@ -295,7 +297,7 @@ impl super::ChatComposer {
|
||||
.map_or_else(
|
||||
|| {
|
||||
self.custom_footer_height()
|
||||
.unwrap_or_else(|| footer_height(&self.hint_footer_props(options), width))
|
||||
.unwrap_or_else(|| footer_height(&self.hint_footer_props(), width))
|
||||
},
|
||||
|footer| footer.text.height().try_into().unwrap_or(u16::MAX),
|
||||
)
|
||||
@@ -310,7 +312,11 @@ impl super::ChatComposer {
|
||||
warning_area: Option<Rect>,
|
||||
) {
|
||||
let hint_area = warning_notice::before_warning(hint_area, warning_area);
|
||||
if hint_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut text = footer.text.clone();
|
||||
text.lines.truncate(usize::from(hint_area.height));
|
||||
if footer.cursor_column.is_none()
|
||||
&& self.footer.flash_visible()
|
||||
&& let Some(flash) = &self.footer.flash
|
||||
@@ -335,6 +341,17 @@ impl super::ChatComposer {
|
||||
line.extend(shortcuts.spans);
|
||||
}
|
||||
}
|
||||
if self.footer.status_line_enabled
|
||||
&& let Some(mode) = self.mode_indicator_line(/*show_cycle_hint*/ false)
|
||||
&& let Some(row) = text
|
||||
.lines
|
||||
.get(usize::from(hint_area.height.saturating_sub(1)))
|
||||
&& crate::line_truncation::line_width(row) + 1 + mode.width()
|
||||
<= usize::from(hint_area.width)
|
||||
&& let Some(bottom_row) = hint_area.rows().next_back()
|
||||
{
|
||||
mode.right_aligned().render(bottom_row, buf);
|
||||
}
|
||||
Paragraph::new(text).render(hint_area, buf);
|
||||
}
|
||||
|
||||
@@ -365,8 +382,8 @@ pub(super) struct FooterState {
|
||||
pub(super) goal_status_indicator: Option<GoalStatusIndicator>,
|
||||
pub(super) ide_context_active: bool,
|
||||
pub(super) status_line_value: Option<FooterContent>,
|
||||
// Available width and occupied height, shared by layout and cursor placement.
|
||||
pub(super) measured_status_line: std::cell::Cell<Option<(u16, u16)>>,
|
||||
// Measurement bounds and occupied height, shared by layout and cursor placement.
|
||||
pub(super) measured_status_line: std::cell::Cell<Option<(ratatui::layout::Size, u16)>>,
|
||||
pub(super) status_line_enabled: bool,
|
||||
pub(super) side_conversation_context_label: Option<String>,
|
||||
pub(super) active_agent_label: Option<String>,
|
||||
|
||||
@@ -176,10 +176,10 @@ fn mention_menu_reserves_height_above_composer_and_owns_cursor_over_interactive_
|
||||
composer: surface,
|
||||
textarea,
|
||||
popup,
|
||||
footer,
|
||||
status,
|
||||
..
|
||||
} = composer.layout_with_options(area, options);
|
||||
assert_eq!((popup.bottom(), surface.bottom()), (surface.y, footer.y));
|
||||
assert_eq!((popup.bottom(), surface.bottom()), (surface.y, status.y));
|
||||
assert_eq!(
|
||||
composer.cursor_pos_with_options(area, options),
|
||||
Some((3, textarea.y))
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer/footer_state_tests.rs
|
||||
assertion_line: 231
|
||||
expression: "frames.join(\"\\n\\n\")"
|
||||
---
|
||||
60x8, cursor Some((10, 6))
|
||||
@@ -9,7 +8,7 @@ expression: "frames.join(\"\\n\\n\")"
|
||||
|
||||
|
||||
|
||||
|
||||
MODEL STATUS
|
||||
Find: needle
|
||||
Enter next · Esc close
|
||||
|
||||
@@ -19,7 +18,7 @@ expression: "frames.join(\"\\n\\n\")"
|
||||
|
||||
|
||||
|
||||
|
||||
MODEL STATUS
|
||||
Find: needle
|
||||
Enter next · Esc close
|
||||
|
||||
|
||||
+1
@@ -5,5 +5,6 @@ expression: text(&buffer)
|
||||
|
||||
› Ask Codex
|
||||
|
||||
MODEL STATUS
|
||||
Find: needle
|
||||
Copied selection to clipboard
|
||||
|
||||
+1
-1
@@ -12,7 +12,6 @@ Transcript row 06: behind the menu
|
||||
Transcript row 07: behind the menu
|
||||
Transcript row 08: behind the menu
|
||||
Transcript row 09: behind the menu
|
||||
Transcript row 10: behind the menu
|
||||
Mentions
|
||||
Files, directories, plugins, skills and tasks
|
||||
|
||||
@@ -33,4 +32,5 @@ Transcript row 10: behind the menu
|
||||
|
||||
› @
|
||||
|
||||
MODEL STATUS
|
||||
TRANSCRIPT STATUS
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
|
||||
expression: shared
|
||||
---
|
||||
16 | Ready PR 12345
|
||||
15 | Ready
|
||||
15 | PR 12345
|
||||
16 | Ready PR 12345
|
||||
25 | Ready
|
||||
25 | PR 12345 Plan mode
|
||||
+2
-2
@@ -13,5 +13,5 @@ expression: wide
|
||||
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used Plan mode (⇧tab to cycle)
|
||||
? for shortcuts
|
||||
MODEL · ~/project · Context 20% used
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
|
||||
+8
-8
@@ -13,8 +13,8 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used Plan mode
|
||||
? for shortcuts
|
||||
MODEL · ~/project · Context 20% used
|
||||
? for shortcuts Plan mode (⇧tab to cycle)
|
||||
---
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used Plan mode
|
||||
Copied selection
|
||||
MODEL · ~/project · Context 20% used
|
||||
Copied selection Plan mode (⇧tab to cycle)
|
||||
---
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
|
||||
› Ask Codex
|
||||
|
||||
MODEL · ~/project · Context 20% used Plan mode
|
||||
Ctrl+X waiting for chord
|
||||
MODEL · ~/project · Context 20% used
|
||||
Ctrl+X waiting for chord Plan mode (⇧tab to cycle)
|
||||
---
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
|
||||
› Continue the task
|
||||
|
||||
MODEL · ~/project · Context 20% used Plan mode
|
||||
tab to queue message
|
||||
MODEL · ~/project · Context 20% used
|
||||
tab to queue message Plan mode
|
||||
|
||||
+2
-2
@@ -1,10 +1,9 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs
|
||||
assertion_line: 344
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 80, height: 8 },
|
||||
area: Rect { x: 0, y: 0, width: 80, height: 9 },
|
||||
content: [
|
||||
" /model choose what model and reasoning effort to use ",
|
||||
"› /memories configure memory use and generation ",
|
||||
@@ -14,6 +13,7 @@ Buffer {
|
||||
"› /m ",
|
||||
" ",
|
||||
" model · high · fast ",
|
||||
" ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
//! Shortcut help grows above the input, preserving status and close rows below it even when clipped.
|
||||
|
||||
use super::*;
|
||||
use ratatui::layout::Size;
|
||||
|
||||
const MAX_FOOTER_ROWS: u16 = 4;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl ChatComposer {
|
||||
pub(crate) fn set_status_line(&mut self, status_line: Option<FooterContent>) -> bool {
|
||||
@@ -36,61 +35,45 @@ impl ChatComposer {
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn has_separate_status(&self, options: ComposerRenderOptions<'_>) -> bool {
|
||||
options.separate_status_line && self.footer.status_line_enabled
|
||||
}
|
||||
|
||||
pub(super) fn shows_shared_status(&self, options: ComposerRenderOptions<'_>) -> bool {
|
||||
!options.separate_status_line
|
||||
&& options.footer.is_none()
|
||||
&& self.custom_footer_height().is_none()
|
||||
&& self.footer.status_line_enabled
|
||||
&& self.footer_mode().shows_passive_line(self.is_task_running)
|
||||
}
|
||||
|
||||
pub(super) fn status_surface_height(
|
||||
&self,
|
||||
width: u16,
|
||||
available: u16,
|
||||
options: ComposerRenderOptions<'_>,
|
||||
) -> u16 {
|
||||
if self.has_separate_status(options) {
|
||||
self.status_line_height(width, options)
|
||||
// The unfocused composer paints only the raw status, as one dim line.
|
||||
let (available, status) = if options.unfocused {
|
||||
let raw = self
|
||||
.status_line_content()
|
||||
.filter(|_| self.footer.status_line_enabled);
|
||||
(available.min(1), raw.map(Cow::Borrowed))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn status_line_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 {
|
||||
// The unfocused composer paints one dim status line.
|
||||
if options.unfocused {
|
||||
return 1;
|
||||
}
|
||||
let Some(FooterContent::Extension { content }) = &self.footer.status_line_value else {
|
||||
return 1;
|
||||
(available, self.status_surface_content())
|
||||
};
|
||||
let mut area = Rect::new(/*x*/ 0, /*y*/ 0, width, /*height*/ 1);
|
||||
if !options.separate_status_line {
|
||||
let warning = self.warning_notice_layout(inset_footer_hint_area(area), options);
|
||||
area = warning_notice::before_warning(area, warning.map(|(rect, _)| rect));
|
||||
let bounds = inset_footer_hint_area(Rect::new(0, 0, width, available)).as_size();
|
||||
if bounds.width == 0 || bounds.height == 0 {
|
||||
return 0;
|
||||
}
|
||||
let (_, width) = self.status_line_layout(area, options, content.measure_natural().width);
|
||||
let cache = &self.footer.measured_status_line;
|
||||
if let Some((previous_width, height)) = cache.get()
|
||||
&& previous_width == width
|
||||
{
|
||||
return height;
|
||||
match status.as_deref() {
|
||||
Some(FooterContent::Extension { content }) => {
|
||||
let cache = &self.footer.measured_status_line;
|
||||
if let Some((previous, height)) = cache.get()
|
||||
&& previous == bounds
|
||||
{
|
||||
return height;
|
||||
}
|
||||
let height = content.measure(bounds).height;
|
||||
cache.set(Some((bounds, height)));
|
||||
height
|
||||
}
|
||||
Some(line) => u16::from(line.width() > 0),
|
||||
None => 0,
|
||||
}
|
||||
let height = content
|
||||
.measure(Size::new(width, MAX_FOOTER_ROWS))
|
||||
.height
|
||||
.max(1);
|
||||
cache.set(Some((width, height)));
|
||||
height
|
||||
}
|
||||
|
||||
pub(super) fn hint_footer_props(&self, options: ComposerRenderOptions<'_>) -> FooterProps {
|
||||
pub(super) fn hint_footer_props(&self) -> FooterProps {
|
||||
let mut props = self.footer_props();
|
||||
if self.has_separate_status(options) {
|
||||
if props.status_line_enabled {
|
||||
props.status_line_enabled = false;
|
||||
props.status_line_value = None;
|
||||
props.active_agent_label = None;
|
||||
@@ -98,64 +81,21 @@ impl ChatComposer {
|
||||
props
|
||||
}
|
||||
|
||||
fn status_line_layout(
|
||||
&self,
|
||||
area: Rect,
|
||||
options: ComposerRenderOptions<'_>,
|
||||
left_width: u16,
|
||||
) -> (Option<Line<'static>>, u16) {
|
||||
let show_cycle_hint = !self.is_task_running
|
||||
&& if options.separate_status_line {
|
||||
!self.blocks_direct_input
|
||||
&& self.draft.input_enabled
|
||||
&& !self.popup_active()
|
||||
&& matches!(
|
||||
self.footer_mode(),
|
||||
FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
|
||||
)
|
||||
&& options.footer.is_none_or(|footer| !footer.is_interactive)
|
||||
} else {
|
||||
self.footer.collaboration_mode_indicator.is_some()
|
||||
};
|
||||
let right = if !options.separate_status_line
|
||||
&& let Some(label) = &self.footer.side_conversation_context_label
|
||||
pub(super) fn status_surface_content(&self) -> Option<Cow<'_, FooterContent>> {
|
||||
if !self.footer.status_line_enabled {
|
||||
return None;
|
||||
}
|
||||
let status = self.footer.status_line_value.as_ref();
|
||||
if matches!(status, Some(FooterContent::Extension { .. }))
|
||||
|| self.footer.active_agent_label.is_none()
|
||||
{
|
||||
Some(side_conversation_context_line(label))
|
||||
} else if !options.separate_status_line
|
||||
&& let Some(shell) = self.shell_mode_footer_line()
|
||||
{
|
||||
Some(shell)
|
||||
} else if !options.separate_status_line
|
||||
&& self
|
||||
.effort_status_line_transition
|
||||
.as_ref()
|
||||
.is_some_and(|transition| !transition.is_finished())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let full = self.mode_indicator_line(show_cycle_hint);
|
||||
let full_fits = full.as_ref().is_none_or(|right| {
|
||||
let right_width = right.width() as u16;
|
||||
(!options.separate_status_line || right_width <= inset_footer_hint_area(area).width)
|
||||
&& can_show_left_with_context(area, left_width, right_width)
|
||||
});
|
||||
if full_fits {
|
||||
full
|
||||
} else {
|
||||
self.mode_indicator_line(/*show_cycle_hint*/ false)
|
||||
}
|
||||
};
|
||||
let right_width = right
|
||||
.as_ref()
|
||||
.map_or(/*default*/ 0, |line| line.width() as u16);
|
||||
let width = max_left_width_for_right(area, right_width).unwrap_or_else(|| {
|
||||
if options.separate_status_line {
|
||||
inset_footer_hint_area(area).width
|
||||
} else {
|
||||
area.width.saturating_sub(FOOTER_INDENT_COLS as u16)
|
||||
}
|
||||
});
|
||||
(right, width)
|
||||
return status.map(Cow::Borrowed);
|
||||
}
|
||||
let mut props = self.footer_props();
|
||||
// The viewed agent belongs to native status even during a transient composer hint.
|
||||
props.mode = FooterMode::ComposerHasDraft;
|
||||
props.is_task_running = false;
|
||||
passive_footer_status_line(&props).map(Cow::Owned)
|
||||
}
|
||||
|
||||
pub(super) fn render_status_surface(
|
||||
@@ -177,42 +117,24 @@ impl ChatComposer {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut props = self.footer_props();
|
||||
if options.separate_status_line {
|
||||
// Persistent context is independent of help, queue, search, and quit modes.
|
||||
props.mode = FooterMode::ComposerHasDraft;
|
||||
props.is_task_running = false;
|
||||
}
|
||||
let line = passive_footer_status_line(&props);
|
||||
let left_width = line.as_ref().map_or(/*default*/ 0, FooterContent::width);
|
||||
let (right, width) = self.status_line_layout(area, options, left_width);
|
||||
let transition = self
|
||||
let area = inset_footer_hint_area(area);
|
||||
let line = self.status_surface_content();
|
||||
if let Some(transition) = self
|
||||
.effort_status_line_transition
|
||||
.as_ref()
|
||||
.filter(|transition| !transition.is_finished());
|
||||
let content = if let Some(transition) = transition {
|
||||
transition
|
||||
.render_line(line.map(FooterContent::into_hyperlink_line), width)
|
||||
.map(FooterContent::HyperlinkLine)
|
||||
} else {
|
||||
line
|
||||
};
|
||||
if let Some(content) = content {
|
||||
content.render(
|
||||
Rect {
|
||||
width,
|
||||
..inset_footer_hint_area(area)
|
||||
},
|
||||
buf,
|
||||
);
|
||||
}
|
||||
if let Some(right) = right {
|
||||
render_context_right(area, buf, &right);
|
||||
}
|
||||
if transition.is_some()
|
||||
&& let Some(frame_requester) = &self.frame_requester
|
||||
.filter(|transition| !transition.is_finished())
|
||||
{
|
||||
frame_requester.schedule_frame_in(EFFORT_STATUS_LINE_FRAME_TICK);
|
||||
if let Some(frame) = transition.render_line(
|
||||
line.map(|line| line.into_owned().into_hyperlink_line()),
|
||||
area.width,
|
||||
) {
|
||||
FooterContent::HyperlinkLine(frame).render(area, buf);
|
||||
}
|
||||
if let Some(requester) = &self.frame_requester {
|
||||
requester.schedule_frame_in(EFFORT_STATUS_LINE_FRAME_TICK);
|
||||
}
|
||||
} else if let Some(line) = line {
|
||||
line.into_owned().render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,22 +78,29 @@ fn extension_status_line_clips_links_to_footer_layout() {
|
||||
let (output, _) = render(&composer, /*width*/ 40, /*footer*/ None);
|
||||
assert!(output.contains("plain"), "{output}");
|
||||
assert!(!output.contains("bot"), "{output}");
|
||||
|
||||
composer.set_status_line(/*status_line*/ None);
|
||||
let agent_rows = [false, true].map(|unfocused| {
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line: true,
|
||||
unfocused,
|
||||
..Default::default()
|
||||
};
|
||||
render_status(&composer, Rect::new(0, 0, 40, 8), options).1
|
||||
});
|
||||
assert_eq!(agent_rows, [vec!["bot".to_string()], vec![]]);
|
||||
}
|
||||
|
||||
fn status_rows(
|
||||
fn render_status(
|
||||
composer: &ChatComposer,
|
||||
area: Rect,
|
||||
options: ComposerRenderOptions<'_>,
|
||||
) -> Vec<String> {
|
||||
) -> (Buffer, Vec<String>) {
|
||||
let layout = composer.layout_with_options(area, options);
|
||||
let status = if options.separate_status_line {
|
||||
layout.status
|
||||
} else {
|
||||
layout.footer
|
||||
};
|
||||
let status = layout.status;
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render_with_options(area, &mut buf, /*mask_char*/ None, options);
|
||||
(status.y..status.bottom())
|
||||
let rows = (status.y..status.bottom())
|
||||
.map(|y| {
|
||||
(status.x..status.right())
|
||||
.map(|x| buf[(x, y)].symbol())
|
||||
@@ -101,11 +108,12 @@ fn status_rows(
|
||||
.trim()
|
||||
.to_owned()
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
(buf, rows)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize() {
|
||||
fn extension_uses_the_full_width_after_resize() {
|
||||
let mut composer = composer();
|
||||
composer.footer.show_warnings_key = None;
|
||||
let tree: crate::ui_tree::UiTree =
|
||||
@@ -120,46 +128,31 @@ fn extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize() {
|
||||
.chars()
|
||||
.map(|digit| osc8_hyperlink("https://example.com/pr/12345", &digit.to_string()))
|
||||
.collect();
|
||||
let [shared, separate] = [false, true].map(|separate_status_line| {
|
||||
let mut frames = Vec::new();
|
||||
for (width, mode, warning_count) in [
|
||||
(16, None, 0),
|
||||
(15, None, 0),
|
||||
(16, None, 0),
|
||||
(25, Some(CollaborationModeIndicator::Plan), 0),
|
||||
(19, None, 1),
|
||||
for fullscreen in [false, true] {
|
||||
for (width, mode, expected_rows) in [
|
||||
(16, None, 1),
|
||||
(15, None, 2),
|
||||
(16, None, 1),
|
||||
(27, Some(CollaborationModeIndicator::Plan), 1),
|
||||
(19, Some(CollaborationModeIndicator::Plan), 1),
|
||||
] {
|
||||
composer.set_collaboration_mode_indicator(mode);
|
||||
let options = ComposerRenderOptions {
|
||||
separate_status_line,
|
||||
warning_count,
|
||||
separate_status_line: fullscreen,
|
||||
warning_count: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let height = composer.desired_height_with_options(width, options);
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height);
|
||||
let rows = status_rows(&composer, area, options);
|
||||
let (buffer, rows) = render_status(&composer, Rect::new(0, 0, width, height), options);
|
||||
assert_eq!(rows.len(), expected_rows);
|
||||
for row in &rows {
|
||||
let text = strip_osc8(row);
|
||||
assert_eq!(*row, text.replace("12345", &linked_number));
|
||||
if warning_count == 0 {
|
||||
frames.push(format!("{width} | {text}"));
|
||||
}
|
||||
}
|
||||
if warning_count > 0 {
|
||||
assert_eq!(
|
||||
composer.footer.warning_notice_area.get().unwrap().bottom(),
|
||||
height
|
||||
);
|
||||
assert_eq!(rows.len(), if separate_status_line { 1 } else { 2 });
|
||||
let last = strip_osc8(rows.last().unwrap());
|
||||
assert!(last.contains("PR 12345"));
|
||||
assert_eq!(last.contains("⚠ 1"), !separate_status_line);
|
||||
assert_eq!(*row, strip_osc8(row).replace("12345", &linked_number));
|
||||
}
|
||||
let warning = composer.footer.warning_notice_area.get().unwrap();
|
||||
assert_eq!(buffer[(warning.x, warning.y)].symbol(), "⚠");
|
||||
assert_eq!((warning.right(), warning.y), (width - 1, height - 1));
|
||||
}
|
||||
frames.join("\n")
|
||||
});
|
||||
assert_eq!(shared, separate);
|
||||
insta::assert_snapshot!(shared);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -173,17 +166,11 @@ fn status_rows_yield_to_the_composer_and_shared_footer_overrides() {
|
||||
separate_status_line,
|
||||
..Default::default()
|
||||
};
|
||||
let short = if separate_status_line {
|
||||
"one…"
|
||||
} else {
|
||||
"one\ntwo…"
|
||||
};
|
||||
for (height, expected) in [(12, "one\ntwo\nthree\nfour…"), (7, short)] {
|
||||
for (height, expected) in [(12, "one\ntwo\nthree\nfour…"), (7, "one…")] {
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 7, height);
|
||||
assert_eq!(status_rows(&composer, area, options).join("\n"), expected);
|
||||
let (buffer, rows) = render_status(&composer, area, options);
|
||||
assert_eq!(rows.join("\n"), expected);
|
||||
let input = composer.layout_with_options(area, options).textarea;
|
||||
let mut buffer = Buffer::empty(area);
|
||||
composer.render_with_options(area, &mut buffer, /*mask_char*/ None, options);
|
||||
assert!(input.height >= 3);
|
||||
let draft = [0, 1, 2].map(|dy| buffer[(input.x, input.y + dy)].symbol());
|
||||
assert_eq!(draft, ["a", "b", "c"]);
|
||||
@@ -205,7 +192,7 @@ fn status_rows_yield_to_the_composer_and_shared_footer_overrides() {
|
||||
composer.set_status_line(Some(FooterContent::from(tree)));
|
||||
assert_eq!(
|
||||
composer.footer_hint_height(/*width*/ 7, ComposerRenderOptions::default()),
|
||||
2
|
||||
1
|
||||
);
|
||||
composer.show_footer_flash("Copied".into(), Duration::from_secs(/*secs*/ 3));
|
||||
assert_eq!(
|
||||
@@ -227,7 +214,9 @@ fn sidebar_focus_collapses_extension_status_to_its_dim_line() {
|
||||
..Default::default()
|
||||
};
|
||||
let height = composer.desired_height_with_options(/*width*/ 40, options);
|
||||
status_rows(&composer, Rect::new(0, 0, 40, height), options).join("\n")
|
||||
render_status(&composer, Rect::new(0, 0, 40, height), options)
|
||||
.1
|
||||
.join("\n")
|
||||
});
|
||||
insta::assert_snapshot!(frames.join("\n---\n"), @"
|
||||
one
|
||||
@@ -307,7 +296,7 @@ fn extension_status_keeps_its_rows_when_effort_changes() {
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 7, /*height*/ 12,
|
||||
);
|
||||
assert_eq!(
|
||||
status_rows(&composer, area, options),
|
||||
render_status(&composer, area, options).1,
|
||||
["one", "two", "three"]
|
||||
);
|
||||
}
|
||||
@@ -401,8 +390,14 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
|
||||
composer.set_status_line(Some(status.into()));
|
||||
composer.set_active_agent_label(Some("reviewer".to_string()));
|
||||
let (narrow, _) = render(&composer, /*width*/ 37, /*footer*/ None);
|
||||
let row = narrow.lines().rev().nth(1).expect("persistent status row");
|
||||
insta::assert_snapshot!(strip_osc8(row).trim(), @"project · PR #42 · rev… Plan mode");
|
||||
assert!(
|
||||
strip_osc8(&narrow).contains("project · PR #42 · reviewer"),
|
||||
"{narrow}"
|
||||
);
|
||||
assert!(
|
||||
narrow.lines().last().unwrap().contains("Plan mode"),
|
||||
"{narrow}"
|
||||
);
|
||||
composer.set_active_agent_label(/*active_agent_label*/ None);
|
||||
|
||||
composer.set_task_running(/*running*/ true);
|
||||
@@ -417,15 +412,16 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
|
||||
is_interactive: true,
|
||||
};
|
||||
let (search, _) = render(&composer, /*width*/ 100, Some(&footer));
|
||||
assert!(search.contains("Search: verify"), "{search}");
|
||||
assert!(search.contains("Plan mode"), "{search}");
|
||||
assert!(!search.contains("⇧tab"), "{search}");
|
||||
|
||||
// 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_eq!(
|
||||
empty_narrow.lines().rev().nth(1).map(str::trim),
|
||||
Some("Plan mode")
|
||||
assert!(
|
||||
empty_narrow.lines().last().unwrap().contains("Plan mode"),
|
||||
"{empty_narrow}"
|
||||
);
|
||||
let (empty_wide, _) = render(&composer, /*width*/ 100, /*footer*/ None);
|
||||
assert!(
|
||||
@@ -435,16 +431,16 @@ fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() {
|
||||
|
||||
composer.set_input_enabled(/*enabled*/ false, /*placeholder*/ None);
|
||||
let (read_only, _) = render(&composer, /*width*/ 100, /*footer*/ None);
|
||||
assert_eq!(
|
||||
read_only.lines().rev().nth(1).map(str::trim),
|
||||
Some("Plan mode")
|
||||
assert!(
|
||||
read_only.lines().last().unwrap().contains("Plan mode"),
|
||||
"{read_only}"
|
||||
);
|
||||
composer.set_input_enabled(/*enabled*/ true, /*placeholder*/ None);
|
||||
composer.set_parent_owned_thread();
|
||||
let (parent_owned, _) = render(&composer, /*width*/ 100, /*footer*/ None);
|
||||
assert_eq!(
|
||||
parent_owned.lines().rev().nth(1).map(str::trim),
|
||||
Some("Plan mode")
|
||||
assert!(
|
||||
parent_owned.lines().last().unwrap().contains("Plan mode"),
|
||||
"{parent_owned}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -687,14 +683,14 @@ fn slash_popup_preserves_footer_and_selection_style() {
|
||||
composer: surface,
|
||||
textarea,
|
||||
popup,
|
||||
footer,
|
||||
status,
|
||||
..
|
||||
} = composer.layout_with_options(area, ComposerRenderOptions::default());
|
||||
assert_eq!(popup.bottom(), surface.y);
|
||||
assert_eq!(surface.bottom(), footer.y);
|
||||
assert_eq!(surface.bottom(), status.y);
|
||||
assert_eq!(composer.cursor_pos(area), Some((4, textarea.y)));
|
||||
let footer_text: String = (0..width).map(|x| buf[(x, height - 1)].symbol()).collect();
|
||||
assert!(footer_text.contains("model · high · fast"));
|
||||
let status_text: String = (0..width).map(|x| buf[(x, status.y)].symbol()).collect();
|
||||
assert!(status_text.contains("model · high · fast"));
|
||||
if !clipped && width == 80 {
|
||||
insta::assert_snapshot!("slash_popup_footer_wide", format!("{buf:?}"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Fit warning counts and the configured warnings shortcut into the existing passive hint row.
|
||||
//! Interactive hints retain priority. Shared status content wraps beside the warning.
|
||||
//! Interactive hints retain priority. Warnings occupy the trailing edge of the hints row.
|
||||
//! Only the count and warning label receive an amber accent; surrounding hints stay muted.
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -67,7 +67,7 @@ fn extension_content_skips_the_effort_transition_in_both_directions() {
|
||||
fn effort_transition_does_not_queue_a_missing_outgoing_status_line() {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.set_status_line_enabled(/*enabled*/ true);
|
||||
composer.set_status_line(Some(Line::from("gpt-5.4 high · main").into()));
|
||||
composer.set_status_line(/*status_line*/ None);
|
||||
composer.set_task_running(/*running*/ true);
|
||||
composer.set_text_content("queued draft".to_string(), Vec::new(), Vec::new());
|
||||
composer.set_active_reasoning_effort(
|
||||
@@ -171,13 +171,17 @@ fn effort_transition_keeps_the_full_footer_row() {
|
||||
);
|
||||
let mut buf = Buffer::empty(area);
|
||||
composer.render(area, &mut buf);
|
||||
let footer = (0..area.width)
|
||||
.map(|column| buf[(column, area.bottom() - 1)].symbol())
|
||||
.collect::<String>();
|
||||
|
||||
assert!(footer.contains("gpt-5.4 high · feature-branch"));
|
||||
assert!(!footer.contains("Plan mode"));
|
||||
insta::assert_snapshot!("effort_transition_keeps_the_full_footer_row", footer);
|
||||
let rows = [area.bottom() - 2, area.bottom() - 1].map(|y| {
|
||||
(0..area.width)
|
||||
.map(|column| buf[(column, y)].symbol())
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
});
|
||||
insta::assert_snapshot!(
|
||||
"effort_transition_keeps_the_full_footer_row",
|
||||
rows.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -877,15 +877,6 @@ impl FooterMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether callers should reserve the dedicated status-line layout for a contextual footer row.
|
||||
///
|
||||
/// The dedicated layout exists for the configurable `/statusline` row. An agent label by itself
|
||||
/// can be rendered by the standard footer flow, so this only becomes `true` when the status line
|
||||
/// feature is enabled and the current mode allows contextual footer content.
|
||||
pub(crate) fn uses_passive_footer_status_layout(props: &FooterProps) -> bool {
|
||||
props.status_line_enabled && props.mode.shows_passive_line(props.is_task_running)
|
||||
}
|
||||
|
||||
pub(crate) fn footer_line_width(
|
||||
props: &FooterProps,
|
||||
collaboration_mode_indicator: Option<CollaborationModeIndicator>,
|
||||
@@ -1070,7 +1061,8 @@ mod tests {
|
||||
| FooterMode::ShortcutOverlay
|
||||
| FooterMode::EscHint => false,
|
||||
};
|
||||
let status_line_active = uses_passive_footer_status_layout(props);
|
||||
let status_line_active = props.status_line_enabled
|
||||
&& props.mode.shows_passive_line(props.is_task_running);
|
||||
let passive_status_line = if status_line_active {
|
||||
passive_footer_status_line(props)
|
||||
.map(|content| content.into_hyperlink_line().line)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//! The composer measures the natural width before allocating the left column, then renders
|
||||
//! the selected content in the allotted rectangle.
|
||||
//! Native and extension status content measured and painted inside the status rectangle.
|
||||
|
||||
use crate::line_truncation::line_width;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
|
||||
@@ -13,7 +12,7 @@ use crate::terminal_hyperlinks::HyperlinkLine;
|
||||
use crate::ui_tree::PreparedUiTree;
|
||||
use crate::ui_tree::UiTree;
|
||||
|
||||
/// Owned content for the footer's negotiated left-hand area.
|
||||
/// Owned status content displayed below the composer.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum FooterContent {
|
||||
Line(Line<'static>),
|
||||
@@ -30,10 +29,10 @@ impl From<UiTree> for FooterContent {
|
||||
}
|
||||
|
||||
impl FooterContent {
|
||||
/// Natural display width, before the footer allocates space for the right indicator.
|
||||
/// Natural display width in terminal cells.
|
||||
pub(crate) fn width(&self) -> u16 {
|
||||
let width = match self {
|
||||
Self::Line(line) => line.width(),
|
||||
Self::Line(line) => line_width(line),
|
||||
Self::HyperlinkLine(line) => line.width(),
|
||||
Self::Extension { content } => return content.measure_natural().width,
|
||||
};
|
||||
@@ -43,11 +42,11 @@ impl FooterContent {
|
||||
/// Paint only inside the final area supplied by the parent.
|
||||
pub(crate) fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match self {
|
||||
Self::Line(line) => Widget::render(line, area, buf),
|
||||
Self::Extension { content } => {
|
||||
with_syntax_theme(|theme| content.render(area, theme, buf));
|
||||
}
|
||||
Self::HyperlinkLine(mut line) => {
|
||||
native => {
|
||||
let mut line = native.into_hyperlink_line();
|
||||
let area = Rect {
|
||||
height: area.height.min(1),
|
||||
..area
|
||||
|
||||
@@ -2445,9 +2445,10 @@ impl BottomPane {
|
||||
|
||||
pub(crate) fn backdrop_with_options<'a>(
|
||||
&'a self,
|
||||
options: ComposerRenderOptions<'a>,
|
||||
mut options: ComposerRenderOptions<'a>,
|
||||
) -> RenderableItem<'a> {
|
||||
let views = if self.centered_dialog().is_some() {
|
||||
options.is_dialog_backdrop = true;
|
||||
&self.view_stack[..self.view_stack.len() - 1]
|
||||
} else {
|
||||
&self.view_stack
|
||||
@@ -2598,6 +2599,7 @@ impl BottomPane {
|
||||
&& options.warning_count == 0
|
||||
&& options.footer.is_none()
|
||||
&& !options.separate_status_line
|
||||
&& !options.is_dialog_backdrop
|
||||
&& options.command_popup_placement == CommandPopupPlacement::AboveComposer
|
||||
{
|
||||
RenderableItem::Borrowed(&self.composer)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer_effort_tests.rs
|
||||
expression: footer
|
||||
expression: "rows.join(\"\\n\")"
|
||||
---
|
||||
gpt-5.4 high · feature-branch
|
||||
? for shortcuts Plan mode
|
||||
|
||||
+2
-2
@@ -9,5 +9,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" model · project · ← for agents "
|
||||
" model · project "
|
||||
" ← for agents · ? for shortcuts "
|
||||
|
||||
+2
-2
@@ -9,5 +9,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used Shell mode "
|
||||
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used "
|
||||
" Shell mode "
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 4485
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
@@ -10,5 +9,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used "
|
||||
" ? for shortcuts "
|
||||
|
||||
@@ -605,7 +605,6 @@ async fn clipped_voice_composer_keeps_the_draft_and_cursor_visible() {
|
||||
|
||||
insta::assert_snapshot!(layouts.join("\n\n"), @"
|
||||
5 rows:
|
||||
voice ● listening f9 mute /voice stop
|
||||
› typed
|
||||
|
||||
6 rows:
|
||||
|
||||
+6
@@ -8,6 +8,7 @@ connecting:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
listening:
|
||||
voice ● listening f9 mute /voice stop
|
||||
@@ -16,6 +17,7 @@ listening:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
speaking:
|
||||
voice ● speaking f9 mute /voice stop
|
||||
@@ -24,6 +26,7 @@ speaking:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
muted:
|
||||
voice ◌ muted f9 unmute /voice stop
|
||||
@@ -32,6 +35,7 @@ muted:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
transcript:
|
||||
voice ● speaking f9 mute /voice stop
|
||||
@@ -40,6 +44,7 @@ transcript:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
interrupted:
|
||||
voice ● heard f9 mute /voice stop
|
||||
@@ -48,3 +53,4 @@ interrupted:
|
||||
› Ask Codex to do anything
|
||||
|
||||
status line stays visible
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
✗ Request denied for codex to run curl -sS -i -X POST --data-binary
|
||||
@core/src/codex.rs https://example.com
|
||||
|
||||
@@ -18,3 +17,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
✗ Review timed out before codex could run curl -sS -i -X POST --data-binary
|
||||
@core/src/codex.rs https://example.com
|
||||
|
||||
@@ -18,3 +17,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -4,7 +4,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
---
|
||||
|
||||
|
||||
|
||||
⚠ MCP client for `alpha` failed to start: handshake failed
|
||||
⚠ MCP startup incomplete (failed: alpha)
|
||||
|
||||
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
-2
@@ -26,7 +26,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
• I’m going to search the repo for where “Change Approved” is rendered to update
|
||||
@@ -41,4 +40,5 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
› Summarize recent commits
|
||||
|
||||
tab to queue message 100% context left
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
tab to queue message
|
||||
|
||||
+1
-1
@@ -21,8 +21,8 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
↳ Hello, world! 12
|
||||
↳ Hello, world! 13
|
||||
↳ Hello, world! 14
|
||||
↳ Hello, world! 15
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -11,7 +11,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Messages to be submitted at end of turn
|
||||
@@ -20,3 +19,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -9,3 +9,4 @@ expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 80))"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -9,3 +9,4 @@ expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 40))"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
<empty>
|
||||
|
||||
@@ -21,6 +22,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
• Blocked by hook
|
||||
└ command blocked by policy
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
• Hook stopped
|
||||
└ continue with more context
|
||||
|
||||
+2
@@ -10,9 +10,11 @@ before completion:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
after completion:
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -17,3 +17,4 @@ After account change:
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6 Luna · GPT-5.6 Luna high "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
-1
@@ -5,4 +5,5 @@ expression: popup
|
||||
---
|
||||
› /daybreak
|
||||
|
||||
profile: work · provider: server-updated Plan mode (⇧tab to cycle)
|
||||
profile: work · provider: server-updated
|
||||
Plan mode (⇧tab to cycle)
|
||||
|
||||
+1
@@ -11,3 +11,4 @@ expression: "format!(\"{}\\n{}\", lines_to_single_string(cells.last().unwrap()),
|
||||
› Viewing sub-agent — direct input is disabled
|
||||
|
||||
GPT-6.1-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -6,3 +6,4 @@ expression: popup
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
-1
@@ -8,4 +8,5 @@ expression: snapshot
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project Goal paused (/goal resume) "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
|
||||
+2
-1
@@ -8,4 +8,5 @@ expression: snapshot
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project Goal paused (/goal resume) "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts Goal paused (/goal resume) "
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -6,10 +6,10 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -8,7 +8,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
⚠ Automatic approval review denied (risk: high): The planned action would
|
||||
transmit the full contents of a workspace source file (`core/src/codex.rs`) to
|
||||
`https://example.com`, which is an external and untrusted endpoint.
|
||||
@@ -22,3 +21,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -9,3 +9,4 @@ expression: normalize_snapshot_paths(rendered)
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -10,3 +10,4 @@ expression: normalize_snapshot_paths(rendered)
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -10,7 +10,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
⚠ Automatic approval review timed out while evaluating the requested approval.
|
||||
|
||||
✗ Review timed out before codex could run curl -sS -i -X POST --data-binary
|
||||
@@ -22,3 +21,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -9,3 +9,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
@@ -11,3 +11,4 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+3
@@ -15,6 +15,7 @@ exec and hook running:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history after exec:
|
||||
• Ran echo done
|
||||
└ done
|
||||
@@ -27,6 +28,7 @@ hook running after exec:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
quiet hook completed:
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) "
|
||||
@@ -35,3 +37,4 @@ quiet hook completed:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -10,4 +10,5 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80)).replace('
|
||||
|
||||
› describe
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
⌃c quit
|
||||
|
||||
+2
@@ -10,9 +10,11 @@ before interrupt:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
after interrupt:
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
120 columns:
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close "
|
||||
@@ -20,3 +21,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/app_server.rs
|
||||
expression: "format!(\"transcript:\\n{transcript}\\nfooter:\\n{footer}\\ntitle: {title}\")"
|
||||
expression: "normalize_snapshot_paths(format!(\"transcript:\\n{transcript}\\nfooter:\\n{footer}\\ntitle: {title}\"))"
|
||||
---
|
||||
transcript:
|
||||
|
||||
@@ -8,4 +8,5 @@ footer:
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project · review-fix
|
||||
? for shortcuts
|
||||
title: review-fix | project
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -5,4 +5,5 @@ expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 80))"
|
||||
---
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
esc esc to edit previous message
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
120 columns:
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close · Running hooks "
|
||||
@@ -19,3 +20,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+4
@@ -11,6 +11,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
<empty>
|
||||
|
||||
@@ -23,6 +24,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
<empty>
|
||||
|
||||
@@ -35,6 +37,7 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
<empty>
|
||||
|
||||
@@ -47,5 +50,6 @@ activity:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
history:
|
||||
<empty>
|
||||
|
||||
+2
@@ -16,3 +16,5 @@ expression: "render_bottom_popup(&chat, 100)"
|
||||
|
||||
› Review the current changes
|
||||
for safety
|
||||
|
||||
GPT-5.5 default · /tmp/project
|
||||
|
||||
+2
@@ -10,3 +10,5 @@ expression: "render_bottom_popup(&chat, 100)"
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.5 default · /tmp/project
|
||||
|
||||
+2
@@ -20,3 +20,5 @@ expression: "render_bottom_popup(&chat, 48)"
|
||||
|
||||
› Review the current changes
|
||||
for safety
|
||||
|
||||
GPT-5.5 default · /tmp/project
|
||||
|
||||
+1
@@ -10,3 +10,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: snapshot
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 max "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@ expression: snapshot
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 max Plan mode (⇧tab to cycle) "
|
||||
" GPT-5.5 max "
|
||||
" ? for shortcuts Plan mode (⇧tab to cycle) "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.2 high · Roadmap cleanup "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -8,3 +8,4 @@ expression: "timer.replace_all(&normalize_snapshot_paths(render_bottom_popup(&ch
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -11,7 +11,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
|
||||
|
||||
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Messages to be submitted at end of turn
|
||||
@@ -20,3 +19,4 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
@@ -11,6 +11,7 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
120 columns:
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view · /stop to close "
|
||||
@@ -20,3 +21,4 @@ expression: "format!(\"80 columns:\\n{}\\n120 columns:\\n{}\", hook_status_frame
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ before reveal:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
running:
|
||||
" "
|
||||
"• Saving session summary "
|
||||
@@ -16,9 +17,11 @@ running:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
completed:
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ before reveal:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
running:
|
||||
" "
|
||||
"• Running hook "
|
||||
@@ -16,9 +17,11 @@ running:
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
completed:
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
"› Ask a follow-up question "
|
||||
" "
|
||||
" GPT-5.6-Sol Side from main thread · ctrl+/ to switch · ctrl+c to close "
|
||||
" GPT-5.6-Sol "
|
||||
" ? for shortcuts Side from main thread · ctrl+/ to switch · ctrl+c to close "
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@ expression: terminal.backend()
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol ctrl+/ for side "
|
||||
" GPT-5.6-Sol "
|
||||
" ? for shortcuts ctrl+/ for side "
|
||||
|
||||
+1
@@ -6,4 +6,5 @@ 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 "
|
||||
|
||||
+1
-1
@@ -2,7 +2,6 @@
|
||||
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||
expression: normalize_snapshot_paths(terminal.backend().vt100().screen().contents())
|
||||
---
|
||||
|
||||
› count to 1
|
||||
|
||||
|
||||
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(terminal.backend().vt100().screen().content
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5 default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+1
-1
@@ -3,7 +3,6 @@ source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||
expression: normalize_snapshot_paths(terminal.backend().vt100().screen().contents())
|
||||
---
|
||||
|
||||
|
||||
• 1
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
@@ -12,3 +11,4 @@ expression: normalize_snapshot_paths(terminal.backend().vt100().screen().content
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-5 default · /tmp/project
|
||||
? for shortcuts
|
||||
|
||||
+2
-1
@@ -8,4 +8,5 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default Side starting... "
|
||||
" GPT-5.6-Sol default "
|
||||
" ? for shortcuts Side starting... "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · 5.2 credits · ~$0.21 "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.2 high · Standard "
|
||||
" ? for shortcuts "
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 Pursuing goal (40K / 50K) "
|
||||
" GPT-5.5 "
|
||||
" ? for shortcuts Pursuing goal (40K / 50K) "
|
||||
|
||||
+2
-1
@@ -6,4 +6,5 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.5 Goal achieved (2d 23h 42m) "
|
||||
" GPT-5.5 "
|
||||
" ? for shortcuts Goal achieved (2d 23h 42m) "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.4 xhigh fast · Context 100% left · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
+1
@@ -7,3 +7,4 @@ expression: normalized_backend_snapshot(terminal.backend())
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.4 xhigh fast · Context 0% used · /tmp/project "
|
||||
" ? for shortcuts "
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user