diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ea1d643..7db17bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 2d3b7feb..a473f974 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -3787,7 +3787,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() diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index d13d5d50..0fb499de 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -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", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 5feb9313..1a75ffc5 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -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 => "作業ディレクトリ", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 1d8b6c5c..027d4710 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -768,6 +768,8 @@ l10n_keys! { PanelMoreChangedFiles, PanelSessionSubtitle, PanelConversationSubtitle, + PanelTurnAltScreenNow, + PanelTurnNoScrollback, PanelProcessesSubtitle, PanelPortsSubtitle, PanelCwd, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index a1f451e0..8d23225d 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -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 => "工作目录", diff --git a/src/ui/right_panel.rs b/src/ui/right_panel.rs index ecfd467a..a22fc6a1 100644 --- a/src/ui/right_panel.rs +++ b/src/ui/right_panel.rs @@ -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::().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, 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!(