mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(terminal): find Gemini's input in the shaded block it draws now
Gemini CLI no longer frames its input in a rounded box: it shades the prompt line between a row of lower half blocks and a row of upper half blocks, and with shading off it draws a rule over the prompt instead. The composer looked only for the old frame, so over a current Gemini it stayed stepped aside for good and never covered the input. A message already sent is shaded the same way in the transcript, so the block counts only with nothing but the footer under it; a permission prompt or a list in the input's place leaves the last message above more than that, and the box steps aside for it. The old frame is still found for older versions, and the shell, YOLO and history-search marks count as a prompt as well as `>`.
This commit is contained in:
+146
-12
@@ -223,7 +223,8 @@ fn is_rule(row: &str, width: usize) -> bool {
|
||||
/// - **Claude Code** draws its input between two full-width rules with `❯`
|
||||
/// on the first line, and its status rows under the lower rule.
|
||||
/// - **Codex** starts its input line with `›`, on a padded block.
|
||||
/// - **Gemini** frames it in a rounded box whose first line is `> `.
|
||||
/// - **Gemini** shades it as a block whose first line is `> ` — see
|
||||
/// [`gemini_input_top`] for its other drawings.
|
||||
///
|
||||
/// `None` is the agent showing something else there — which is exactly when
|
||||
/// the box must get out of the way.
|
||||
@@ -265,22 +266,76 @@ pub(super) fn input_area(agent: CLIAgent, rows: &[String], width: usize) -> Opti
|
||||
Some(InputArea { top, mode: None })
|
||||
}
|
||||
CLIAgent::Gemini => {
|
||||
let bottom = (floor..rows.len()).rev().find(|&i| starts(i, '╰'))?;
|
||||
let top = (floor..bottom).rev().find(|&i| starts(i, '╭'))?;
|
||||
(top + 1..bottom)
|
||||
.any(|i| {
|
||||
rows[i]
|
||||
.trim_start()
|
||||
.trim_start_matches('│')
|
||||
.trim_start()
|
||||
.starts_with('>')
|
||||
})
|
||||
.then_some(InputArea { top, mode: None })
|
||||
gemini_input_top(rows, floor, width).map(|top| InputArea { top, mode: None })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// How many rows Gemini's footer — the workspace, sandbox and model, under
|
||||
/// its labels — takes below the input at most.
|
||||
const GEMINI_FOOTER_MAX_ROWS: usize = 4;
|
||||
|
||||
/// Where Gemini's input starts, in whichever of its drawings is on screen.
|
||||
///
|
||||
/// - **A shaded block**, the usual one: a row of `▄` and a row of `▀`, as wide
|
||||
/// as the screen, round the prompt line. A message already sent is drawn the
|
||||
/// same way in the transcript, so the block counts only with nothing under
|
||||
/// it but the footer — a permission prompt or a list in the input's place
|
||||
/// puts itself below the last message instead.
|
||||
/// - **A rule over the prompt line**, where the block has no colour to shade
|
||||
/// with (`NO_COLOR`, or its background colour switched off).
|
||||
/// - **A rounded frame**, `╭ … ╰` with `> ` inside, in versions before the
|
||||
/// block.
|
||||
///
|
||||
/// The prompt line opens with `>`, or with `!` in shell mode, `*` in YOLO
|
||||
/// mode and `(r:)` while searching the history.
|
||||
fn gemini_input_top(rows: &[String], floor: usize, width: usize) -> Option<usize> {
|
||||
let prompt = |row: &str| {
|
||||
let t = row.trim_start();
|
||||
['>', '!', '*'].iter().any(|p| t.starts_with(*p)) || t.starts_with("(r:)")
|
||||
};
|
||||
let band = |row: &str, c: char| {
|
||||
let t = row.trim();
|
||||
t.chars().count() >= width * 3 / 5 && t.chars().all(|x| x == c)
|
||||
};
|
||||
|
||||
// Shading is on for the whole screen or off for all of it, so a band
|
||||
// anywhere says which drawing to look for.
|
||||
if let Some(lower) = (floor..rows.len()).rev().find(|&i| band(&rows[i], '▀')) {
|
||||
let footer = rows[lower + 1..]
|
||||
.iter()
|
||||
.filter(|r| !r.trim().is_empty())
|
||||
.count();
|
||||
let upper = (floor..lower).rev().find(|&i| band(&rows[i], '▄'))?;
|
||||
return (footer <= GEMINI_FOOTER_MAX_ROWS && upper + 1 < lower && prompt(&rows[upper + 1]))
|
||||
.then_some(upper);
|
||||
}
|
||||
|
||||
if let Some(line) = (floor + 1..rows.len())
|
||||
.rev()
|
||||
.find(|&i| prompt(&rows[i]) && is_rule(&rows[i - 1], width))
|
||||
{
|
||||
return Some(line - 1);
|
||||
}
|
||||
|
||||
let bottom = (floor..rows.len())
|
||||
.rev()
|
||||
.find(|&i| rows[i].trim_start().starts_with('╰'))?;
|
||||
let top = (floor..bottom)
|
||||
.rev()
|
||||
.find(|&i| rows[i].trim_start().starts_with('╭'))?;
|
||||
(top + 1..bottom)
|
||||
.any(|i| {
|
||||
rows[i]
|
||||
.trim_start()
|
||||
.trim_start_matches('│')
|
||||
.trim_start()
|
||||
.starts_with('>')
|
||||
})
|
||||
.then_some(top)
|
||||
}
|
||||
|
||||
/// How far above Codex's selected row the rest of its list can start: the
|
||||
/// options before it, each with a description that may wrap.
|
||||
const CODEX_LIST_REACH: usize = 16;
|
||||
@@ -2234,6 +2289,85 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
const LOWER_HALVES: &str = "▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄";
|
||||
const UPPER_HALVES: &str = "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀";
|
||||
|
||||
#[test]
|
||||
fn geminis_input_is_its_shaded_block_over_the_footer() {
|
||||
let rows = screen(&[
|
||||
LOWER_HALVES,
|
||||
" > say pong",
|
||||
UPPER_HALVES,
|
||||
"",
|
||||
"✦ pong",
|
||||
"",
|
||||
"? for shortcuts",
|
||||
RULE,
|
||||
" Shift+Tab to accept edits",
|
||||
LOWER_HALVES,
|
||||
" > Type your message or @path/to/file",
|
||||
UPPER_HALVES,
|
||||
" workspace (/directory) sandbox /model",
|
||||
" ~/proj no sandbox gpt-5",
|
||||
]);
|
||||
assert_eq!(
|
||||
input_area(CLIAgent::Gemini, &rows, 40),
|
||||
Some(InputArea { top: 9, mode: None })
|
||||
);
|
||||
// Shell mode and YOLO mode change only the prompt's mark.
|
||||
for mark in [" ! ls", " * fix it"] {
|
||||
let mut rows = rows.clone();
|
||||
rows[10] = mark.to_string();
|
||||
assert_eq!(
|
||||
input_area(CLIAgent::Gemini, &rows, 40).map(|a| a.top),
|
||||
Some(9)
|
||||
);
|
||||
}
|
||||
// Without a footer, too.
|
||||
assert_eq!(
|
||||
input_area(CLIAgent::Gemini, &rows[..12], 40).map(|a| a.top),
|
||||
Some(9)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sent_gemini_message_is_not_its_input() {
|
||||
// The input gives its place to a permission prompt; the last message
|
||||
// sent, shaded like the input, is still on screen above it.
|
||||
let rows = screen(&[
|
||||
LOWER_HALVES,
|
||||
" > make hello.txt",
|
||||
UPPER_HALVES,
|
||||
"",
|
||||
"╭──────────────────────────────────────╮",
|
||||
"│ ? WriteFile Writing to hello.txt │",
|
||||
"│ Allow this change? │",
|
||||
"│ ● 1. Allow once │",
|
||||
"│ 2. Allow for this session │",
|
||||
"│ 3. No, suggest changes (esc) │",
|
||||
"╰──────────────────────────────────────╯",
|
||||
]);
|
||||
assert_eq!(input_area(CLIAgent::Gemini, &rows, 40), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geminis_unshaded_input_is_the_prompt_under_its_rule() {
|
||||
let rows = screen(&[
|
||||
"✦ pong",
|
||||
"",
|
||||
"? for shortcuts",
|
||||
RULE,
|
||||
" Shift+Tab to accept edits",
|
||||
RULE,
|
||||
" > Type your message or @path/to/file",
|
||||
" workspace (/directory) sandbox /model",
|
||||
]);
|
||||
assert_eq!(
|
||||
input_area(CLIAgent::Gemini, &rows, 40),
|
||||
Some(InputArea { top: 5, mode: None })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_ids_read_the_way_people_say_them() {
|
||||
assert_eq!(model_label("claude-opus-5-5"), "Opus 5.5");
|
||||
|
||||
Reference in New Issue
Block a user