|
| 1 | +# Next Step: Restore "Plus" Edits |
| 2 | + |
| 3 | +This branch now carries only the exec cell refactor. Reapply the functionality changes documented below on the follow-up branch. |
| 4 | + |
| 5 | +## Code patches to restore |
| 6 | + |
| 7 | +### core/src/client.rs |
| 8 | +```diff |
| 9 | +diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs |
| 10 | +index 1d1082c2..aedea488 100644 |
| 11 | +--- a/codex-rs/core/src/client.rs |
| 12 | ++++ b/codex-rs/core/src/client.rs |
| 13 | +@@ -572,12 +572,21 @@ fn parse_rate_limit_window( |
| 14 | + window_minutes_header: &str, |
| 15 | + resets_header: &str, |
| 16 | + ) -> Option<RateLimitWindow> { |
| 17 | +- let used_percent = parse_header_f64(headers, used_percent_header)?; |
| 18 | ++ let used_percent: Option<f64> = parse_header_f64(headers, used_percent_header); |
| 19 | + |
| 20 | +- Some(RateLimitWindow { |
| 21 | +- used_percent, |
| 22 | +- window_minutes: parse_header_u64(headers, window_minutes_header), |
| 23 | +- resets_in_seconds: parse_header_u64(headers, resets_header), |
| 24 | ++ used_percent.and_then(|used_percent| { |
| 25 | ++ let window_minutes = parse_header_u64(headers, window_minutes_header); |
| 26 | ++ let resets_in_seconds = parse_header_u64(headers, resets_header); |
| 27 | ++ |
| 28 | ++ let has_data = used_percent != 0.0 |
| 29 | ++ || window_minutes.is_some_and(|minutes| minutes != 0) |
| 30 | ++ || resets_in_seconds.is_some_and(|seconds| seconds != 0); |
| 31 | ++ |
| 32 | ++ has_data.then_some(RateLimitWindow { |
| 33 | ++ used_percent, |
| 34 | ++ window_minutes, |
| 35 | ++ resets_in_seconds, |
| 36 | ++ }) |
| 37 | + }) |
| 38 | + } |
| 39 | +``` |
| 40 | + |
| 41 | +### tui/src/bottom_pane/chat_composer.rs |
| 42 | +```diff |
| 43 | +diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs |
| 44 | +index eb654815..7530794c 100644 |
| 45 | +--- a/codex-rs/tui/src/bottom_pane/chat_composer.rs |
| 46 | ++++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs |
| 47 | +@@ -1330,9 +1330,11 @@ impl WidgetRef for ChatComposer { |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | +- Line::from(hint) |
| 52 | +- .style(Style::default().dim()) |
| 53 | +- .render_ref(hint_rect, buf); |
| 54 | ++ let hint = hint |
| 55 | ++ .into_iter() |
| 56 | ++ .map(|span| span.patch_style(Style::default().dim())) |
| 57 | ++ .collect::<Vec<_>>(); |
| 58 | ++ Line::from(hint).render_ref(hint_rect, buf); |
| 59 | + } |
| 60 | + } |
| 61 | + let border_style = if self.has_focus { |
| 62 | +@@ -1356,9 +1358,8 @@ impl WidgetRef for ChatComposer { |
| 63 | + let mut state = self.textarea_state.borrow_mut(); |
| 64 | + StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); |
| 65 | + if self.textarea.text().is_empty() { |
| 66 | +- Line::from(self.placeholder_text.as_str()) |
| 67 | +- .style(Style::default().dim()) |
| 68 | +- .render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); |
| 69 | ++ let placeholder = Span::from(self.placeholder_text.as_str()).dim(); |
| 70 | ++ Line::from(vec![placeholder]).render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | +``` |
| 75 | + |
| 76 | +### tui/src/chatwidget.rs (rate-limit label helper) |
| 77 | +```diff |
| 78 | +diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs |
| 79 | +index 8bd15b53..66e54cf6 100644 |
| 80 | +--- a/codex-rs/tui/src/chatwidget.rs |
| 81 | ++++ b/codex-rs/tui/src/chatwidget.rs |
| 82 | +@@ |
| 83 | +-fn get_limits_duration(windows_minutes: u64) -> String { |
| 84 | ++pub(crate) fn get_limits_duration(windows_minutes: u64) -> String { |
| 85 | +``` |
| 86 | + |
| 87 | +### tui/src/status/card.rs |
| 88 | +```diff |
| 89 | +diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs |
| 90 | +index a977240b..8f7188d5 100644 |
| 91 | +--- a/codex-rs/tui/src/status/card.rs |
| 92 | ++++ b/codex-rs/tui/src/status/card.rs |
| 93 | +@@ -145,7 +145,7 @@ impl StatusHistoryCell { |
| 94 | +- let base_spans = formatter.full_spans(row.label, value_spans); |
| 95 | ++ let base_spans = formatter.full_spans(row.label.as_str(), value_spans); |
| 96 | +@@ |
| 97 | +- fn collect_rate_limit_labels( |
| 98 | +- &self, |
| 99 | +- seen: &mut BTreeSet<&'static str>, |
| 100 | +- labels: &mut Vec<&'static str>, |
| 101 | +- ) { |
| 102 | ++ fn collect_rate_limit_labels(&self, seen: &mut BTreeSet<String>, labels: &mut Vec<String>) { |
| 103 | +@@ |
| 104 | +- push_label(labels, seen, row.label); |
| 105 | ++ push_label(labels, seen, row.label.as_str()); |
| 106 | +@@ |
| 107 | +- let mut labels: Vec<&'static str> = |
| 108 | +- vec!["Model", "Directory", "Approval", "Sandbox", "Agents.md"]; |
| 109 | +- let mut seen: BTreeSet<&'static str> = labels.iter().copied().collect(); |
| 110 | ++ let mut labels: Vec<String> = |
| 111 | ++ vec!["Model", "Directory", "Approval", "Sandbox", "Agents.md"] |
| 112 | ++ .into_iter() |
| 113 | ++ .map(str::to_string) |
| 114 | ++ .collect(); |
| 115 | ++ let mut seen: BTreeSet<String> = labels.iter().cloned().collect(); |
| 116 | +@@ |
| 117 | +- let formatter = FieldFormatter::from_labels(labels.iter().copied()); |
| 118 | ++ let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str)); |
| 119 | +``` |
| 120 | + |
| 121 | +### tui/src/status/format.rs |
| 122 | +```diff |
| 123 | +diff --git a/codex-rs/tui/src/status/format.rs b/codex-rs/tui/src/status/format.rs |
| 124 | +index 556e5bd1..fda72904 100644 |
| 125 | +--- a/codex-rs/tui/src/status/format.rs |
| 126 | ++++ b/codex-rs/tui/src/status/format.rs |
| 127 | +@@ -15,10 +15,13 @@ pub(crate) struct FieldFormatter { |
| 128 | +-pub(crate) fn from_labels(labels: impl IntoIterator<Item = &'static str>) -> Self { |
| 129 | ++pub(crate) fn from_labels<S>(labels: impl IntoIterator<Item = S>) -> Self |
| 130 | ++where |
| 131 | ++ S: AsRef<str>, |
| 132 | ++{ |
| 133 | +@@ |
| 134 | +- .map(UnicodeWidthStr::width) |
| 135 | ++ .map(|label| UnicodeWidthStr::width(label.as_ref())) |
| 136 | +@@ |
| 137 | +- label: &'static str, |
| 138 | ++ label: &str, |
| 139 | +@@ |
| 140 | +-pub(crate) fn push_label( |
| 141 | +- labels: &mut Vec<&'static str>, |
| 142 | +- seen: &mut BTreeSet<&'static str>, |
| 143 | +- label: &'static str, |
| 144 | +-) { |
| 145 | +- if seen.insert(label) { |
| 146 | +- labels.push(label); |
| 147 | ++pub(crate) fn push_label(labels: &mut Vec<String>, seen: &mut BTreeSet<String>, label: &str) { |
| 148 | ++ if seen.contains(label) { |
| 149 | ++ return; |
| 150 | ++ } |
| 151 | ++ |
| 152 | ++ let owned = label.to_string(); |
| 153 | ++ seen.insert(owned.clone()); |
| 154 | ++ labels.push(owned); |
| 155 | + } |
| 156 | +``` |
| 157 | + |
| 158 | +### tui/src/status/rate_limits.rs |
| 159 | +```diff |
| 160 | +diff --git a/codex-rs/tui/src/status/rate_limits.rs b/codex-rs/tui/src/status/rate_limits.rs |
| 161 | +index 17d2f170..8da3a1c6 100644 |
| 162 | +--- a/codex-rs/tui/src/status/rate_limits.rs |
| 163 | ++++ b/codex-rs/tui/src/status/rate_limits.rs |
| 164 | +@@ -1,3 +1,5 @@ |
| 165 | ++use crate::chatwidget::get_limits_duration; |
| 166 | ++ |
| 167 | +@@ |
| 168 | +- pub label: &'static str, |
| 169 | ++ pub label: String, |
| 170 | +@@ |
| 171 | +- pub resets_at: Option<String>, |
| 172 | ++ pub resets_at: Option<String>, |
| 173 | ++ pub window_minutes: Option<u64>, |
| 174 | +@@ |
| 175 | +- let mut rows = Vec::with_capacity(2); |
| 176 | +- |
| 177 | +- if let Some(primary) = snapshot.primary.as_ref() { |
| 178 | +- rows.push(StatusRateLimitRow { |
| 179 | +- label: "5h limit", |
| 180 | +- percent_used: primary.used_percent, |
| 181 | +- resets_at: primary.resets_at.clone(), |
| 182 | +- }); |
| 183 | +- } |
| 184 | +- |
| 185 | +- if let Some(secondary) = snapshot.secondary.as_ref() { |
| 186 | +- rows.push(StatusRateLimitRow { |
| 187 | +- label: "Weekly limit", |
| 188 | +- percent_used: secondary.used_percent, |
| 189 | +- resets_at: secondary.resets_at.clone(), |
| 190 | +- }); |
| 191 | +- } |
| 192 | ++ let mut rows = Vec::with_capacity(2); |
| 193 | ++ |
| 194 | ++ if let Some(primary) = snapshot.primary.as_ref() { |
| 195 | ++ let label: String = primary |
| 196 | ++ .window_minutes |
| 197 | ++ .map(get_limits_duration) |
| 198 | ++ .unwrap_or_else(|| "5h".to_string()); |
| 199 | ++ let label = capitalize_first(&label); |
| 200 | ++ rows.push(StatusRateLimitRow { |
| 201 | ++ label: format!("{label} limit"), |
| 202 | ++ percent_used: primary.used_percent, |
| 203 | ++ resets_at: primary.resets_at.clone(), |
| 204 | ++ }); |
| 205 | ++ } |
| 206 | ++ |
| 207 | ++ if let Some(secondary) = snapshot.secondary.as_ref() { |
| 208 | ++ let label: String = secondary |
| 209 | ++ .window_minutes |
| 210 | ++ .map(get_limits_duration) |
| 211 | ++ .unwrap_or_else(|| "weekly".to_string()); |
| 212 | ++ let label = capitalize_first(&label); |
| 213 | ++ rows.push(StatusRateLimitRow { |
| 214 | ++ label: format!("{label} limit"), |
| 215 | ++ percent_used: secondary.used_percent, |
| 216 | ++ resets_at: secondary.resets_at.clone(), |
| 217 | ++ }); |
| 218 | ++ } |
| 219 | +@@ |
| 220 | ++fn capitalize_first(label: &str) -> String { |
| 221 | ++ let mut chars = label.chars(); |
| 222 | ++ match chars.next() { |
| 223 | ++ Some(first) => { |
| 224 | ++ let mut capitalized = first.to_uppercase().collect::<String>(); |
| 225 | ++ capitalized.push_str(chars.as_str()); |
| 226 | ++ capitalized |
| 227 | ++ } |
| 228 | ++ None => String::new(), |
| 229 | ++ } |
| 230 | ++} |
| 231 | +``` |
| 232 | + |
| 233 | +### tui/src/status/tests.rs |
| 234 | +```diff |
| 235 | +diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs |
| 236 | +index 467b9dd7..d583a5e5 100644 |
| 237 | +--- a/codex-rs/tui/src/status/tests.rs |
| 238 | ++++ b/codex-rs/tui/src/status/tests.rs |
| 239 | +@@ -93,7 +93,7 @@ fn status_snapshot_includes_reasoning_details() { |
| 240 | +- secondary: Some(RateLimitWindow { |
| 241 | +- used_percent: 45.0, |
| 242 | +- window_minutes: Some(1_440), |
| 243 | +- resets_in_seconds: Some(1_200), |
| 244 | +- }), |
| 245 | ++ secondary: Some(RateLimitWindow { |
| 246 | ++ used_percent: 45.0, |
| 247 | ++ window_minutes: Some(10_080), |
| 248 | ++ resets_in_seconds: Some(1_200), |
| 249 | ++ }), |
| 250 | +@@ |
| 251 | ++#[test] |
| 252 | ++fn status_snapshot_includes_monthly_limit() { /* … */ } |
| 253 | ++ |
| 254 | +@@ |
| 255 | ++#[test] |
| 256 | ++fn status_snapshot_shows_missing_limits_message() { /* … */ } |
| 257 | +``` |
| 258 | + |
| 259 | +## Snapshot updates to recreate |
| 260 | + |
| 261 | +Regenerate the following snapshots after restoring the code above: |
| 262 | + |
| 263 | +- `tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap` |
| 264 | +- `tui/src/snapshots/codex_tui__history_cell__tests__multiline_command_both_lines_wrap_with_correct_prefixes.snap` |
| 265 | +- `tui/src/snapshots/codex_tui__history_cell__tests__multiline_command_without_wrap_uses_branch_then_eight_spaces.snap` |
| 266 | +- `tui/src/snapshots/codex_tui__history_cell__tests__multiline_command_wraps_with_extra_indent_on_subsequent_lines.snap` |
| 267 | +- `tui/src/snapshots/codex_tui__history_cell__tests__ran_cell_multiline_with_stderr_snapshot.snap` |
| 268 | +- `tui/src/snapshots/codex_tui__history_cell__tests__single_line_command_wraps_with_four_space_continuation.snap` |
| 269 | +- `tui/src/snapshots/codex_tui__history_cell__tests__stderr_tail_more_than_five_lines_snapshot.snap` |
| 270 | +- `tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap` |
| 271 | +- `tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap` |
| 272 | + |
| 273 | +Re-run `cargo test -p codex-tui` and accept the new snapshots (`cargo insta accept -p codex-tui`) when reintroducing the "Plus" feature work. |
0 commit comments