Keep the claude pane prompt-only: compact height + auto transcript wipe

The pane was showing Claude Code's own transcript (tool calls, diffs) —
redundant with the feed above. Two measures:

- Default pane height is now compact (12 rows): room for the input box
  and status, (almost) none for transcript. Interactive grow to 75% is
  unchanged.
- When a turn of the embedded session finishes streaming (Tap drop),
  schedule a ctrl-l into the PTY 400ms later: Claude Code clears the
  screen but keeps the conversation, leaving just the prompt UI.

Residual: while a response is streaming, a few transcript rows can
still scroll through the compact pane until the turn-end wipe fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonas H
2026-06-11 08:52:30 +02:00
parent a770052673
commit b05706a51e
3 changed files with 51 additions and 6 deletions

View File

@@ -27,6 +27,10 @@ pub struct App {
/// (AskUserQuestion/ExitPlanMode seen in the stream, answer not yet
/// echoed back) — the UI gives the pane more rows while set.
pub embed_grow: bool,
/// When set, the UI should send ctrl-l to the embedded claude at this
/// instant (a beat after a turn ends, once Claude Code has printed its
/// final transcript lines) so the pane stays prompt-only.
pub embed_clear_at: Option<Instant>,
}
/// Client-side tools that render a large interactive UI in Claude Code.
@@ -47,6 +51,7 @@ impl App {
show_sessions: true,
embed_session: None,
embed_grow: false,
embed_clear_at: None,
}
}
}
@@ -338,6 +343,11 @@ impl Drop for Tap {
if let Some(i) = self.cur.take() {
s.entries[i].done = true;
}
// A turn of the embedded session just finished streaming: schedule a
// transcript wipe shortly after Claude Code prints its final lines.
if a.embed_session.as_deref() == Some(a.sessions[self.sidx].key.as_str()) {
a.embed_clear_at = Some(Instant::now() + std::time::Duration::from_millis(400));
}
}
}

View File

@@ -137,6 +137,18 @@ impl EmbeddedTerm {
});
}
/// Ask Claude Code to wipe its transcript from the visible screen
/// (ctrl-l clears the terminal but keeps the conversation; the live
/// prompt UI redraws immediately). Used to keep the pane prompt-only —
/// the feed above already shows everything the transcript would.
pub fn clear_screen(&self) {
let _ = self
.term
.lock()
.unwrap()
.key_down(KeyCode::Char('l'), KeyModifiers::CTRL);
}
/// Forward a key press to the child. Returns false for keys we don't map.
pub fn key(&self, k: ratatui::crossterm::event::KeyEvent) -> bool {
use ratatui::crossterm::event::KeyCode as CK;

View File

@@ -83,6 +83,24 @@ fn event_loop(
) -> anyhow::Result<()> {
loop {
terminal.draw(|f| draw(f, &app, eui))?;
// Scheduled transcript wipe (set by the tap when an embedded-session
// turn finishes): keeps the pane prompt-only.
let clear_due = {
let mut a = app.lock().unwrap();
if a.embed_clear_at.is_some_and(|t| std::time::Instant::now() >= t) {
a.embed_clear_at = None;
true
} else {
false
}
};
if clear_due {
if let Some(et) = &eui.term {
if !et.exited() {
et.clear_screen();
}
}
}
if !event::poll(Duration::from_millis(33))? {
continue;
}
@@ -193,15 +211,20 @@ fn event_loop(
fn draw(f: &mut Frame, app: &SharedApp, eui: &mut EmbedUi) {
let mut a = app.lock().unwrap();
// Embedded pane height: nothing when hidden; ~35% of the screen normally,
// 75% while the tap says an interactive prompt is on screen — Claude
// Code's question UI clips its option list to the rows it gets, so the
// prompt wants as much height as we can spare.
// Embedded pane height: nothing when hidden. The pane is meant to be
// prompt-only (the feed above shows the context), so the default is just
// tall enough for Claude Code's input box + status rows; while the tap
// says an interactive prompt is on screen it grows to 75% so the option
// list isn't clipped.
const EMBED_COMPACT: u16 = 12; // 10 inner rows + borders
let show_embed = eui.visible && eui.term.is_some();
let embed_h = if show_embed {
let total = f.area().height;
let pct = if a.embed_grow { 75 } else { 35 };
((total as u32 * pct / 100) as u16).clamp(10, total.saturating_sub(6))
if a.embed_grow {
((total as u32 * 75 / 100) as u16).clamp(10, total.saturating_sub(6))
} else {
EMBED_COMPACT.min(total.saturating_sub(6))
}
} else {
0
};