mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
fix(tui): reflow live table tail after resize
Allow the pending resize reflow to run while an agent or plan stream is active so boxed markdown tables are rerendered at the latest width.
This commit is contained in:
Executable
+256
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/tui-resize-stream-repro.sh [options]
|
||||
|
||||
Start Codex in tmux, send a long table-heavy prompt, resize repeatedly while
|
||||
the response streams, and capture scrollback after each resize.
|
||||
|
||||
Options:
|
||||
--session NAME tmux session name (default: codex-resize-repro-$$)
|
||||
--out DIR output directory (default: /tmp/codex-resize-repro-*)
|
||||
--duration SECONDS resize duration after prompt submission (default: 20)
|
||||
--sleep SECONDS delay between resizes (default: 0.20)
|
||||
--startup SECONDS wait after launching Codex before sending prompt (default: 8)
|
||||
--expect MODE either, artifact, or clean (default: either)
|
||||
--keep-session leave the tmux session running for manual inspection
|
||||
--skip-prebuild do not run cargo build --bin codex before launching tmux
|
||||
--prompt TEXT override the default prompt
|
||||
-h, --help show this help
|
||||
|
||||
Examples:
|
||||
scripts/tui-resize-stream-repro.sh --keep-session
|
||||
scripts/tui-resize-stream-repro.sh --expect clean --duration 30
|
||||
EOF
|
||||
}
|
||||
|
||||
session="codex-resize-repro-$$"
|
||||
out_dir=""
|
||||
duration=20
|
||||
sleep_interval=0.20
|
||||
startup_wait=8
|
||||
expect="either"
|
||||
keep_session=0
|
||||
prebuild=1
|
||||
prompt="Produce 6 long Markdown tables, 25 rows each. Include emojis, bold, italic, strikethrough, inline code, markdown links, code-like values, short cells, wrapped cells, and pipe characters escaped inside cells. Include the token RESIZE_REPRO_SENTINEL in several table cells. Stream the answer directly. Do not use tools."
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--session)
|
||||
session="$2"
|
||||
shift 2
|
||||
;;
|
||||
--out)
|
||||
out_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--duration)
|
||||
duration="$2"
|
||||
shift 2
|
||||
;;
|
||||
--sleep)
|
||||
sleep_interval="$2"
|
||||
shift 2
|
||||
;;
|
||||
--startup)
|
||||
startup_wait="$2"
|
||||
shift 2
|
||||
;;
|
||||
--expect)
|
||||
expect="$2"
|
||||
shift 2
|
||||
;;
|
||||
--keep-session)
|
||||
keep_session=1
|
||||
shift
|
||||
;;
|
||||
--skip-prebuild)
|
||||
prebuild=0
|
||||
shift
|
||||
;;
|
||||
--prompt)
|
||||
prompt="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$expect" in
|
||||
either|artifact|clean) ;;
|
||||
*)
|
||||
echo "--expect must be one of: either, artifact, clean" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "tmux is required for this repro" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$prebuild" -eq 1 ]]; then
|
||||
cargo build --bin codex
|
||||
fi
|
||||
|
||||
if [[ -z "$out_dir" ]]; then
|
||||
out_dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-resize-repro-XXXXXX")"
|
||||
else
|
||||
mkdir -p "$out_dir"
|
||||
fi
|
||||
|
||||
captures_dir="$out_dir/captures"
|
||||
log_dir="$out_dir/logs"
|
||||
mkdir -p "$captures_dir" "$log_dir"
|
||||
|
||||
if tmux has-session -t "$session" 2>/dev/null; then
|
||||
echo "tmux session already exists: $session" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
if [[ "$keep_session" -eq 0 ]] && tmux has-session -t "$session" 2>/dev/null; then
|
||||
tmux kill-session -t "$session"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
capture() {
|
||||
local idx="$1"
|
||||
local width="$2"
|
||||
local height="$3"
|
||||
local stem
|
||||
stem="$(printf '%03d_%sx%s' "$idx" "$width" "$height")"
|
||||
tmux capture-pane -t "$session" -p -S -5000 >"$captures_dir/$stem.txt"
|
||||
tmux capture-pane -t "$session" -p -e -S -5000 >"$captures_dir/$stem.ansi"
|
||||
}
|
||||
|
||||
wait_for_tui_ready() {
|
||||
local deadline=$((SECONDS + startup_wait))
|
||||
while [[ "$SECONDS" -lt "$deadline" ]]; do
|
||||
if tmux capture-pane -t "$session" -p | grep -q 'OpenAI Codex'; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_artifacts() {
|
||||
local report="$out_dir/artifacts.txt"
|
||||
: >"$report"
|
||||
|
||||
for capture in "$captures_dir"/*.txt; do
|
||||
[[ -e "$capture" ]] || continue
|
||||
local raw_count box_count sentinel_count
|
||||
raw_count="$(grep -Ec '^[[:space:]]*\|.*\|' "$capture" || true)"
|
||||
box_count="$(grep -Ec '[┌┬┐└┴┘├┼┤│]' "$capture" || true)"
|
||||
sentinel_count="$(grep -Ec 'RESIZE_REPRO_SENTINEL' "$capture" || true)"
|
||||
|
||||
if [[ "$raw_count" -ge 3 && "$box_count" -ge 3 ]]; then
|
||||
printf '%s mixed_raw_and_boxed_table raw=%s boxed=%s sentinel=%s\n' \
|
||||
"$(basename "$capture")" "$raw_count" "$box_count" "$sentinel_count" >>"$report"
|
||||
fi
|
||||
|
||||
local prompt_count
|
||||
prompt_count="$(grep -Ec '^[[:space:]]*› ' "$capture" || true)"
|
||||
if [[ "$prompt_count" -ge 2 && "$box_count" -ge 3 ]]; then
|
||||
printf '%s duplicated_inline_prompt_with_table prompts=%s boxed=%s sentinel=%s\n' \
|
||||
"$(basename "$capture")" "$prompt_count" "$box_count" "$sentinel_count" >>"$report"
|
||||
fi
|
||||
|
||||
if awk '
|
||||
/^[[:space:]]*\|.*\|[[:space:]]*$/ {
|
||||
pipe_rows++
|
||||
next
|
||||
}
|
||||
pipe_rows >= 3 && /[┌┬┐└┴┘├┼┤│]/ {
|
||||
found = 1
|
||||
}
|
||||
END { exit found ? 0 : 1 }
|
||||
' "$capture"; then
|
||||
printf '%s raw_table_precedes_boxed_table\n' "$(basename "$capture")" >>"$report"
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -s "$report" ]]
|
||||
}
|
||||
|
||||
tmux new-session -d -s "$session" -x 100 -y 34
|
||||
|
||||
launch_cmd="RUST_LOG=trace ./target/debug/codex --no-alt-screen -C '$PWD' -c 'log_dir=$log_dir'"
|
||||
tmux send-keys -t "$session" -l "$launch_cmd"
|
||||
tmux send-keys -t "$session" Enter
|
||||
if ! wait_for_tui_ready; then
|
||||
capture 0 100 34
|
||||
echo "Codex TUI did not become ready within ${startup_wait}s" >&2
|
||||
echo "captures: $captures_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
capture 0 100 34
|
||||
|
||||
tmux send-keys -t "$session" -l "$prompt"
|
||||
sleep 0.2
|
||||
tmux send-keys -t "$session" Enter
|
||||
|
||||
widths=(110 62 132 74 120 56 140)
|
||||
heights=(36 28 38 24 34 22 40)
|
||||
idx=1
|
||||
deadline=$((SECONDS + duration))
|
||||
|
||||
while [[ "$SECONDS" -lt "$deadline" ]]; do
|
||||
for i in "${!widths[@]}"; do
|
||||
width="${widths[$i]}"
|
||||
height="${heights[$i]}"
|
||||
tmux resize-window -t "$session" -x "$width" -y "$height"
|
||||
sleep "$sleep_interval"
|
||||
capture "$idx" "$width" "$height"
|
||||
idx=$((idx + 1))
|
||||
if [[ "$SECONDS" -ge "$deadline" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
sleep 2
|
||||
capture "$idx" "final" "final"
|
||||
|
||||
cat >"$out_dir/metadata.txt" <<EOF
|
||||
session=$session
|
||||
cwd=$PWD
|
||||
log_dir=$log_dir
|
||||
duration=$duration
|
||||
sleep_interval=$sleep_interval
|
||||
startup_wait=$startup_wait
|
||||
expect=$expect
|
||||
keep_session=$keep_session
|
||||
prebuild=$prebuild
|
||||
prompt=$prompt
|
||||
EOF
|
||||
|
||||
if detect_artifacts; then
|
||||
echo "resize-stream repro captured possible artifacts:"
|
||||
cat "$out_dir/artifacts.txt"
|
||||
echo "captures: $captures_dir"
|
||||
if [[ "$expect" == "clean" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "resize-stream repro found no text artifacts"
|
||||
echo "captures: $captures_dir"
|
||||
if [[ "$expect" == "artifact" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -3423,11 +3423,7 @@ impl App {
|
||||
let Some(deadline) = self.resize_reflow_pending_until else {
|
||||
return Ok(());
|
||||
};
|
||||
if Instant::now() < deadline
|
||||
|| self.overlay.is_some()
|
||||
|| self.chat_widget.has_active_agent_stream()
|
||||
|| self.chat_widget.has_active_plan_stream()
|
||||
{
|
||||
if Instant::now() < deadline || self.overlay.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -1235,6 +1235,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_live_tail_reflow_rerenders_table_tail_after_resize() {
|
||||
let mut ctrl = stream_controller_with_live_tail_reflow(Some(96));
|
||||
ctrl.push("| # | Feature | Details | Link |\n");
|
||||
ctrl.push("| --- | --- | --- | --- |\n");
|
||||
ctrl.push(
|
||||
"| 1 | RESIZE_REPRO_SENTINEL | long wrapped content that should be reflowed | https://example.com/resize |\n",
|
||||
);
|
||||
|
||||
for width in [48, 104, 56] {
|
||||
ctrl.set_width(Some(width));
|
||||
let tail = lines_to_plain_strings(&ctrl.current_tail_lines());
|
||||
|
||||
let mut expected = Vec::new();
|
||||
crate::markdown::append_markdown_agent(
|
||||
&ctrl.core.raw_source,
|
||||
Some(width),
|
||||
&mut expected,
|
||||
);
|
||||
let expected = lines_to_plain_strings(&expected);
|
||||
|
||||
assert_eq!(
|
||||
tail, expected,
|
||||
"expected live table tail to be rerendered at width {width}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_set_width_partial_drain_no_lost_lines() {
|
||||
let mut ctrl = stream_controller(Some(40));
|
||||
|
||||
Reference in New Issue
Block a user