mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(panel): stop a conversation row offering a jump the pane cannot make (#759)
A turn's row is a link back into the scrollback, and `scroll_to_agent_turn` refuses two cases: a turn with no anchor, and a pane sitting on the alternate screen with no scrollback behind it. The panel only checked the first. So a conversation recorded under the classic renderer kept its anchors, the user switched the agent into a full-screen renderer — Claude Code's `/tui fullscreen` — and every row went on drawing a pointer and a hover fill while swallowing the click in silence. Both conditions now live in one predicate the panel and the view agree on, and a row that goes nowhere says why on hover: grey text reads as "less important" long before it reads as "not a link". Claude-Session: https://claude.ai/code/session_01A8Hiu4o14SkF5bpoPiV7Ko
This commit is contained in:
@@ -153,6 +153,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A conversation row no longer offers a jump the pane cannot make.** Clicking
|
||||
one under a full-screen agent did nothing at all: no scroll, no message,
|
||||
nothing. The row had an anchor, so the panel drew it as a link — pointer,
|
||||
hover fill and all — while the view refused the jump, because a pane on the
|
||||
alternate screen has no scrollback to land in. Switching Claude Code into
|
||||
`/tui fullscreen` mid-session is enough to produce it: the turns recorded
|
||||
under the classic renderer keep their anchors, and every one of them goes
|
||||
quiet at once. The two conditions now live in one predicate both sides read,
|
||||
and a row that goes nowhere says why on hover rather than leaving grey text
|
||||
to carry a meaning grey does not have. Jumping is still off while an agent
|
||||
renders full-screen — there is genuinely nothing behind it — but the panel no
|
||||
longer pretends otherwise.
|
||||
|
||||
- **In-app updates work again on macOS** (#708). Every "Update and Relaunch"
|
||||
failed with `codesign did not report a designated requirement`, on every
|
||||
build and both channels, with nothing a user could do but download the app by
|
||||
|
||||
@@ -3794,7 +3794,7 @@ impl TerminalView {
|
||||
!self.prompt_editor || self.shell_vi_prompt() || self.handoff_active()
|
||||
}
|
||||
|
||||
fn on_alt_screen(&self) -> bool {
|
||||
pub(crate) fn on_alt_screen(&self) -> bool {
|
||||
self.terminal
|
||||
.term
|
||||
.lock()
|
||||
|
||||
@@ -1033,6 +1033,12 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::PanelNoChangesHint => "The working tree is clean.",
|
||||
L10nKey::PanelSessionSubtitle => "Session",
|
||||
L10nKey::PanelConversationSubtitle => "Conversation",
|
||||
L10nKey::PanelTurnAltScreenNow => {
|
||||
"Nowhere to jump while a full-screen program owns this pane."
|
||||
}
|
||||
L10nKey::PanelTurnNoScrollback => {
|
||||
"This turn was drawn on the alternate screen, so the scrollback never kept it."
|
||||
}
|
||||
L10nKey::PanelProcessesSubtitle => "Processes",
|
||||
L10nKey::PanelPortsSubtitle => "Ports",
|
||||
L10nKey::PanelCwd => "cwd",
|
||||
|
||||
@@ -1097,6 +1097,12 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::PanelNoChangesHint => "ワーキングツリーはクリーンです",
|
||||
L10nKey::PanelSessionSubtitle => "セッション",
|
||||
L10nKey::PanelConversationSubtitle => "会話",
|
||||
L10nKey::PanelTurnAltScreenNow => {
|
||||
"全画面プログラムがこのペインを占有している間は、戻る先がありません"
|
||||
}
|
||||
L10nKey::PanelTurnNoScrollback => {
|
||||
"このターンは代替画面に描かれたため、スクロールバックに残っていません"
|
||||
}
|
||||
L10nKey::PanelProcessesSubtitle => "プロセス",
|
||||
L10nKey::PanelPortsSubtitle => "ポート",
|
||||
L10nKey::PanelCwd => "作業ディレクトリ",
|
||||
|
||||
@@ -768,6 +768,8 @@ l10n_keys! {
|
||||
PanelMoreChangedFiles,
|
||||
PanelSessionSubtitle,
|
||||
PanelConversationSubtitle,
|
||||
PanelTurnAltScreenNow,
|
||||
PanelTurnNoScrollback,
|
||||
PanelProcessesSubtitle,
|
||||
PanelPortsSubtitle,
|
||||
PanelCwd,
|
||||
|
||||
@@ -990,6 +990,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::PanelNoChangesHint => "worktree 是干净的。",
|
||||
L10nKey::PanelSessionSubtitle => "会话",
|
||||
L10nKey::PanelConversationSubtitle => "对话",
|
||||
L10nKey::PanelTurnAltScreenNow => "全屏程序占着此窗格,没有 scrollback 可跳回。",
|
||||
L10nKey::PanelTurnNoScrollback => "这一轮画在 alt screen 上,scrollback 里没有留下它。",
|
||||
L10nKey::PanelProcessesSubtitle => "进程",
|
||||
L10nKey::PanelPortsSubtitle => "端口",
|
||||
L10nKey::PanelCwd => "工作目录",
|
||||
|
||||
+50
-6
@@ -1021,7 +1021,7 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
/// The agent's conversation, one row per turn, each a way back to where
|
||||
/// that turn started in the scrollback.
|
||||
/// that turn started in the scrollback — when there is one to go back to.
|
||||
///
|
||||
/// It sits under the session facts rather than in a tab of its own: this is
|
||||
/// something *this pane* is, like its shell and its cwd, and the tab strip
|
||||
@@ -1042,15 +1042,18 @@ impl Tty7App {
|
||||
if turns.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// A full-screen program owns the whole drawing surface, so there is no
|
||||
// scrollback under it to land in — while one is up every jump is a
|
||||
// no-op, whatever anchor the turn is carrying. An agent that renders
|
||||
// that way (Claude Code's `/tui fullscreen`, Codex) puts every row in
|
||||
// this section here.
|
||||
let alt_now = leaf.read(cx).on_alt_screen();
|
||||
let sf = cx.global::<crate::ui::presets::Surfaces>().sidebar;
|
||||
let count = turns.len().to_string();
|
||||
let mut list = v_flex().px(px(CONTENT_INSET - 4.)).py(px(1.)).gap(px(1.));
|
||||
for turn in turns {
|
||||
let id = turn.id;
|
||||
// Only a turn that was drawn into the scrollback has somewhere to
|
||||
// go: one that began on the alt screen is history the pane never
|
||||
// kept, so its row reads as a label and not as a link.
|
||||
let jumpable = turn.row.is_some();
|
||||
let jumpable = turn_is_jumpable(turn.row, alt_now);
|
||||
let dot = {
|
||||
let d = div().flex_none().size(px(7.)).rounded_full();
|
||||
if turn.done {
|
||||
@@ -1079,6 +1082,18 @@ impl Tty7App {
|
||||
});
|
||||
}))
|
||||
})
|
||||
// A row that goes nowhere says why on hover. Muted text is
|
||||
// the whole of what it says otherwise, and grey reads as
|
||||
// "less important" long before it reads as "not a link".
|
||||
.when(!jumpable, |this| {
|
||||
let tip = t(match alt_now {
|
||||
true => L10nKey::PanelTurnAltScreenNow,
|
||||
false => L10nKey::PanelTurnNoScrollback,
|
||||
});
|
||||
this.tooltip(move |window, cx| {
|
||||
gpui_component::tooltip::Tooltip::new(tip).build(window, cx)
|
||||
})
|
||||
})
|
||||
.child(dot)
|
||||
.child(
|
||||
div()
|
||||
@@ -1488,9 +1503,19 @@ fn compact_path(path: &std::path::Path, home: Option<&std::path::Path>) -> Strin
|
||||
crate::ui::path_display::abbreviate_home(&path.to_string_lossy(), home).into_owned()
|
||||
}
|
||||
|
||||
/// Whether a turn's row is a link back into the scrollback, or only a label.
|
||||
///
|
||||
/// Both halves have to hold, and they are the same two conditions
|
||||
/// [`TerminalView::scroll_to_agent_turn`](crate::terminal::view::TerminalView)
|
||||
/// refuses on — deliberately, because a row that draws as a link and then does
|
||||
/// nothing is worse than one that never offered. Keep the two in step.
|
||||
fn turn_is_jumpable(row: Option<i64>, alt_now: bool) -> bool {
|
||||
row.is_some() && !alt_now
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{InfoRow, InfoValue, split_path_leaf};
|
||||
use super::{InfoRow, InfoValue, split_path_leaf, turn_is_jumpable};
|
||||
|
||||
fn diff(added: u32, removed: u32, open: bool) -> InfoRow {
|
||||
InfoRow {
|
||||
@@ -1534,6 +1559,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_turn_offers_the_jump_only_where_the_jump_would_land() {
|
||||
assert!(
|
||||
turn_is_jumpable(Some(42), false),
|
||||
"a turn anchored in the scrollback of a pane on the normal screen"
|
||||
);
|
||||
assert!(
|
||||
!turn_is_jumpable(None, false),
|
||||
"a turn that began on the alt screen was never written down"
|
||||
);
|
||||
// The one this pair exists for: the anchor survives the switch into a
|
||||
// full-screen renderer, and the row it points at does not. Before, the
|
||||
// row kept its pointer and its hover fill and swallowed every click.
|
||||
assert!(
|
||||
!turn_is_jumpable(Some(42), true),
|
||||
"and an anchor is no use while a full-screen program owns the pane"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_are_a_button_only_when_there_is_a_diff_to_open() {
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user