-macos-arm64.dmg` (Apple Silicon) or `…-x86_64.dmg`
+ (Intel); open it and drag `tty7.app` into Applications.
+- **Windows** — `…-windows-x86_64-setup.exe` (installer: Start Menu shortcut +
+ uninstall entry), or `…-windows-x86_64.zip` (portable: unzip and run
+ `tty7.exe`).
+- **Linux** — `…-linux-x86_64.tar.gz`; extract and run `./tty7` (needs the usual
+ x11/wayland runtime libraries).
-
+## Features
-**[Download the latest release ▶](https://github.com/l0ng-ai/tty7/releases/latest)**
+### At the prompt
-
+- **Ghost suggestions** — your history completes the whole line as you type; → to accept
+- **Tab completion that explains** — every flag and subcommand with its description, for ~100 common commands
+- **Syntax highlighting** — as you type, nothing to install
+- **Fuzzy history search** — ⌃ R shows what you ran, where, and whether it failed
+- **History from day one** — your existing shell history just works, and carries across sessions
+- **Real line editing** — selection, word motion, undo
-
+### In the window
-## 📊 Benchmarks
+- **Tabs & splits** — always open in the current directory
+- **Command palette** ⌘ P · scrollback search ⌘ F
+- **⌘-click links** · desktop notifications
+- **Eight themes** · CJK / IME input
+
+## Benchmarks
All four terminals measured back-to-back on the same machine, same day, same
155×40 grid — Apple M1 Pro, macOS 26.3.1, five-run averages (2026-07-04):
@@ -79,28 +81,17 @@ All four terminals measured back-to-back on the same machine, same day, same
¹ GUI 105 MB + the persistent daemon 11 MB.
-tty7 reads the PTY at device speed and parses it in large batches off the render
-path, and the hot paths are lock-free — so a big `cat` never waits on drawing.
-(That's also what the background daemon buys you: it can run up to 16 MiB ahead
-of the window before backpressure applies.)
+Where the speed comes from:
+
+- The PTY is read at device speed and parsed in large batches, off the render path
+- Hot paths are lock-free — a big `cat` never waits on drawing
+- The daemon buffers up to 16 MiB ahead of the window before backpressure applies
Methodology (how each terminal is driven, grid fairness, known pitfalls) and
one-command reproduction live in [`scripts/bench/`](scripts/bench/README.md) —
run it yourself.
-## 🚀 Install
-
-Download the build for your platform from [**Releases**](https://github.com/l0ng-ai/tty7/releases):
-
-- **macOS** — `tty7--macos-arm64.dmg` (Apple Silicon) or `…-x86_64.dmg`
- (Intel); open it and drag `tty7.app` into Applications.
-- **Windows** — `…-windows-x86_64-setup.exe` (installer: Start Menu shortcut +
- uninstall entry), or `…-windows-x86_64.zip` (portable: unzip and run
- `tty7.exe`).
-- **Linux** — `…-linux-x86_64.tar.gz`; extract and run `./tty7` (needs the usual
- x11/wayland runtime libraries).
-
-## ⌨️ Shortcuts
+## Keybindings
Keys are shown in macOS notation — on Windows and Linux, read ⌘ as
Ctrl. Open Settings with ⌘ , to browse or remap them all.
@@ -120,29 +111,12 @@ The essentials:
The full list — and any overrides — lives in **Settings → Keybindings**.
-## 💭 Built with & inspired by
-
-- [gpui](https://github.com/zed-industries/zed) — Zed's GPU-accelerated UI framework
-- [`alacritty_terminal`](https://github.com/zed-industries/alacritty) (Zed's fork) — VT emulator, grid, and PTY
-- [gpui-component](https://github.com/longbridge/gpui-component) — UI widgets, via a [pinned fork](https://github.com/l0ng-ai/gpui-component/tree/tty7)
-- [tmux](https://github.com/tmux/tmux) — the inspiration for the persistent-daemon design
-
-## 🤝 Contributing
-
-Bug reports and PRs are welcome. For questions and ideas, join the
-[Discord](https://discord.gg/s3dethqz2V). Notable changes land in the
-[CHANGELOG](CHANGELOG.md).
-
-## 📝 License
-
-[Apache License 2.0](LICENSE) · © 2026 l0ng-ai
-
-
+---
+
-
-
-tty7 — a GPU-rendered terminal in pure Rust.
+Built on [gpui](https://github.com/zed-industries/zed) and [`alacritty_terminal`](https://github.com/zed-industries/alacritty) · [Apache-2.0](LICENSE) · [Discord](https://discord.gg/s3dethqz2V) · [Changelog](CHANGELOG.md)
+
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 07989581..6daaef31 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -15,7 +15,7 @@
[](LICENSE)
[](https://discord.gg/s3dethqz2V)
-[**安装**](#-安装) · [**基准测试**](#-基准测试) · [**快捷键**](#️-快捷键) · [**参与贡献**](#-参与贡献)
+[**为什么是 tty7**](#为什么是-tty7) · [**安装**](#安装) · [**功能**](#功能) · [**基准测试**](#基准测试) · [**快捷键**](#快捷键)
[English](README.md) · 简体中文
@@ -25,41 +25,46 @@
-
+## 为什么是 tty7
-tty7 是一款 GPU 渲染的终端。11 MB 的 `cat` 用时 **95 ms**,大约是
-Alacritty、Ghostty、Kitty 的两倍速度(同一台机器);内置提示符提供内联补全、
-语法高亮,以及常用命令的逐 flag 说明。纯 Rust 编写,macOS、Windows、Linux
-均为原生构建,无需额外配置。
+- **快** —— 同一台机器上,吞吐约为 Alacritty、Ghostty、Kitty 的
+ 2 倍([基准测试](#基准测试))
+- **现代的提示符** —— 补全、语法高亮、历史搜索全部内置,不用东拼西凑插件
+- **会话不死** —— 关窗口、退应用,shell 照样运行;无需 tmux
+- **零配置** —— zsh、bash、fish、PowerShell 开箱即用
-- ⚡ **快** —— 11 MB 的 `cat` 花 **95 ms**,Alacritty/Ghostty/Kitty
- 要 179–239 ms;DOOM-fire 跑到 **888 fps**,它们在 485–617 之间。同一台机器、
- 同一网格测出来的,脚本就在仓库里(见[基准测试](#-基准测试))。
-- ⌨️ **带补全的提示符** —— 内联补全、语法高亮、历史记录、终端内搜索。输入 `git commit --`、`kubectl` 或 `npm`,每个 flag 和子命令
- 都会带着说明一并列出 —— 覆盖约 100 个常用命令,数据取自 Fig 的语料。
-- 🧠 **懂 shell,零配置** —— 新标签页和分屏都开在当前目录,路径补全也始终跟随
- 你所在的位置。zsh、bash、fish、PowerShell 均自动接入。
-- 🔌 **会话不中断** —— shell 运行在后台守护进程中,关窗口、退应用、乃至换上新
- 版程序,都不会中断任何一个 shell。随时断开,随时接回,无需 tmux。
+macOS、Windows、Linux 三平台原生构建,每个 release 一起打出。
-其他功能:标签页(拖拽重排、双击重命名、数字键切换)、可拖动分隔线
-调节比例的分屏、命令面板、点击打开链接、桌面通知、焦点随鼠标移动。内置 8 套
-主题(由浅及深),系统标题栏的明暗跟随所选主题;CJK 与输入法组合输入也一并
-支持。
+## 安装
-macOS、Windows、Linux 三个平台都有原生构建,每个 release 一起打出。
+到 [**Releases**](https://github.com/l0ng-ai/tty7/releases) 下载对应平台的构建:
-
+- **macOS** —— `tty7--macos-arm64.dmg`(Apple Silicon)或 `…-x86_64.dmg`
+ (Intel);打开后把 `tty7.app` 拖进「应用程序」即可。
+- **Windows** —— `…-windows-x86_64-setup.exe`(安装包:带开始菜单快捷方式和
+ 卸载入口),或 `…-windows-x86_64.zip`(便携版:解压后运行 `tty7.exe`)。
+- **Linux** —— `…-linux-x86_64.tar.gz`;解压后运行 `./tty7`(需要常见的
+ x11/wayland 运行时库)。
-
+## 功能
-**[下载最新版本 ▶](https://github.com/l0ng-ai/tty7/releases/latest)**
+### 提示符
-
+- **影子建议** —— 边打字边用你的历史补全整条命令,→ 接受
+- **会解释的 Tab 补全** —— 每个 flag、每个子命令都带说明,覆盖约 100 个常用命令
+- **语法高亮** —— 边打边亮,什么都不用装
+- **模糊历史搜索** —— ⌃ R 看到每条命令在哪跑的、什么时候、有没有失败
+- **历史开箱即用** —— 你已有的 shell 历史直接生效,并跨会话延续
+- **真正的行编辑** —— 选区、词级移动、撤销
-
+### 窗口
-## 📊 基准测试
+- **标签页与分屏** —— 永远开在当前目录
+- **命令面板** ⌘ P · 回滚搜索 ⌘ F
+- **⌘ 点击打开链接** · 桌面通知
+- **8 套主题** · CJK / 输入法输入
+
+## 基准测试
四款终端在同一台机器上依次测完,网格统一为 155×40 —— Apple M1 Pro,
macOS 26.3.1,取五次运行的平均值(2026-07-04):
@@ -72,25 +77,16 @@ macOS 26.3.1,取五次运行的平均值(2026-07-04):
¹ GUI 105 MB + 常驻守护进程 11 MB。
-tty7 以设备速度读取 PTY,并在渲染路径之外成批解析输出,热路径全程无锁 ——
-再大的 `cat` 也不会阻塞在渲染上。(后台守护进程亦服务于此:触发背压前,它最多
-可领先窗口 16 MiB。)
+速度从哪来:
+
+- 以设备速度读取 PTY,在渲染路径之外成批解析
+- 热路径全程无锁 —— 再大的 `cat` 也不会阻塞在渲染上
+- 触发背压前,守护进程最多可领先窗口缓冲 16 MiB
测试方法(每款终端怎么驱动、网格是否公平、有哪些坑)连同一键复现脚本,都放在
[`scripts/bench/`](scripts/bench/README.md),欢迎自己跑一遍。
-## 🚀 安装
-
-到 [**Releases**](https://github.com/l0ng-ai/tty7/releases) 下载对应平台的构建:
-
-- **macOS** —— `tty7--macos-arm64.dmg`(Apple Silicon)或 `…-x86_64.dmg`
- (Intel);打开后把 `tty7.app` 拖进「应用程序」即可。
-- **Windows** —— `…-windows-x86_64-setup.exe`(安装包:带开始菜单快捷方式和
- 卸载入口),或 `…-windows-x86_64.zip`(便携版:解压后运行 `tty7.exe`)。
-- **Linux** —— `…-linux-x86_64.tar.gz`;解压后运行 `./tty7`(需要常见的
- x11/wayland 运行时库)。
-
-## ⌨️ 快捷键
+## 快捷键
下表按 macOS 记法书写 —— 在 Windows 和 Linux 上,把 ⌘ 读作
Ctrl。按 ⌘ , 打开设置,可查看或重新映射全部键位。最常用的几个:
@@ -104,34 +100,17 @@ tty7 以设备速度读取 PTY,并在渲染路径之外成批解析输出,
| ⌘ K | 清屏并清空回滚缓冲区 |
| ⌘ P | 命令面板 |
| ⌘ F | 搜索回滚缓冲区 |
-| ⌃ R | 反向搜索 shell 历史 |
+| ⌃ R | 模糊搜索 shell 历史 |
| ⌘ + · ⌘ − · ⌘ 0 | 字号增大 · 减小 · 重置 |
完整列表(以及你改过的自定义键位)在 **Settings → Keybindings**。
-## 💭 站在这些之上
-
-- [gpui](https://github.com/zed-industries/zed) —— Zed 的 GPU 加速 UI 框架
-- [`alacritty_terminal`](https://github.com/zed-industries/alacritty)(Zed 的 fork)—— VT 模拟器、网格与 PTY
-- [gpui-component](https://github.com/longbridge/gpui-component) —— UI 组件,经由一个[固定版本的 fork](https://github.com/l0ng-ai/gpui-component/tree/tty7)
-- [tmux](https://github.com/tmux/tmux) —— 常驻守护进程设计的灵感来源
-
-## 🤝 参与贡献
-
-欢迎提 bug 和 PR。有问题、有想法,欢迎来
-[Discord](https://discord.gg/s3dethqz2V) 交流。重要改动都记在
-[CHANGELOG](CHANGELOG.md)。
-
-## 📝 许可证
-
-[Apache License 2.0](LICENSE) · © 2026 l0ng-ai
-
-
+---
+
-
-
-tty7 —— 纯 Rust 编写的 GPU 渲染终端。
+基于 [gpui](https://github.com/zed-industries/zed) 与 [`alacritty_terminal`](https://github.com/zed-industries/alacritty) 构建 · [Apache-2.0](LICENSE) · [Discord](https://discord.gg/s3dethqz2V) · [更新日志](CHANGELOG.md)
+
diff --git a/src/terminal/view.rs b/src/terminal/view.rs
index e9324969..98840a5f 100644
--- a/src/terminal/view.rs
+++ b/src/terminal/view.rs
@@ -224,6 +224,19 @@ pub struct TerminalView {
/// selection into the line, Cmd+Enter runs it outright, and Escape/Ctrl+G
/// cancels.
reverse_search: Option,
+ /// One-shot "shell integration didn't engage" notice (#46). Set when Ctrl+R
+ /// is pressed in a pane whose shell never reported OSC 133 — the history
+ /// menu the user is reaching for can't appear, and without this the feature
+ /// just looks broken. A figterm-style PTY shim (kiro-cli-term, qterm) that
+ /// swallowed the reports is the usual culprit, so the message names the
+ /// wrapper when the daemon's foreground query recognizes one. Cleared on
+ /// the next keystroke, after a timeout, or if integration engages late.
+ integration_notice: Option,
+ /// Latch so the notice shows at most once per pane — a diagnostic, not a nag.
+ integration_notice_shown: bool,
+ /// When this view was created. Ctrl+R inside the startup grace window stays
+ /// silent: slow rc files mean integration legitimately hasn't reported yet.
+ created_at: std::time::Instant,
/// True while a left-drag that began on the command-editor line is in progress,
/// so mouse-move extends the editor selection rather than the terminal's.
editor_selecting: bool,
@@ -300,6 +313,43 @@ const LONG_COMMAND: std::time::Duration = std::time::Duration::from_secs(10);
/// instant once the window lapses.
const HOLD_WINDOW: std::time::Duration = std::time::Duration::from_millis(150);
+/// How long after pane creation Ctrl+R stays silent about missing shell
+/// integration: slow rc files can take several seconds to reach the first
+/// prompt report, and calling integration broken while the shell is still
+/// starting up would be a false alarm.
+const INTEGRATION_GRACE: std::time::Duration = std::time::Duration::from_secs(8);
+
+/// How long the integration notice stays up when no keystroke dismisses it.
+const INTEGRATION_NOTICE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
+
+/// Fig-descended PTY shims known to exec over the shell we spawned and re-host
+/// it on a nested PTY without forwarding OSC 133 — which starves shell
+/// integration and silently kills the whole command-editor overlay (#46).
+/// Matched against the foreground process name the daemon reports; `contains`
+/// because the shim may present as e.g. `zsh (kiro-cli-term)`.
+fn known_pty_shim(fg: &str) -> Option<&'static str> {
+ ["kiro-cli-term", "figterm", "qterm", "cwterm"]
+ .into_iter()
+ .find(|shim| fg.contains(shim))
+}
+
+/// The integration-notice text. Naming the shim matters: "install shell
+/// integration" advice would mislead — the hooks *are* installed, something
+/// between the shell and tty7 is eating their reports.
+fn integration_notice_message(wrapper: Option<&str>) -> String {
+ match wrapper {
+ Some(w) => format!(
+ "tty7 shell integration is blocked in this pane — \u{201c}{w}\u{201d} is intercepting \
+ shell reports, so inline completion and the Ctrl+R menu are unavailable. \
+ The shell's own history search still works."
+ ),
+ None => "tty7 shell integration hasn't engaged in this pane, so inline completion and \
+ the Ctrl+R menu are unavailable. A PTY wrapper (figterm-style) or an \
+ unsupported shell setup can cause this."
+ .to_string(),
+ }
+}
+
/// Post a desktop notification that a command finished. Best-effort and
/// non-blocking: routed through [`super::remote::notify_desktop`] (the single
/// `notify-rust` entry point shared with the escape-sequence path), so there's no
@@ -703,6 +753,9 @@ impl TerminalView {
pending_history: None,
completion: None,
reverse_search: None,
+ integration_notice: None,
+ integration_notice_shown: false,
+ created_at: std::time::Instant::now(),
editor_selecting: false,
editor_select_gesture: false,
hovered_link: None,
@@ -839,6 +892,12 @@ impl TerminalView {
if self.terminal.exited {
return;
}
+ // Any keystroke dismisses a visible integration notice — it has been
+ // read. The Ctrl+R that raises it runs later in this same dispatch, so
+ // the raising chord never clears its own notice.
+ if self.integration_notice.take().is_some() {
+ cx.notify();
+ }
// macOS Option-key policy (see `input::reshape_option_keystroke`):
// reshape the chord once, up front, so every consumer below — the ⌘
// dispatcher, the prompt editor, the raw PTY encoder — sees the same
@@ -931,6 +990,16 @@ impl TerminalView {
return;
}
+ // Ctrl+R reaching this raw path means the tty7 history menu the user is
+ // probably reaching for cannot appear here. When that's because shell
+ // integration never engaged — not because a foreground command owns the
+ // PTY — say so once instead of failing silently (#46). The chord still
+ // goes to the PTY below, so the shell's own reverse-i-search keeps
+ // working as the fallback.
+ if m.control && !m.platform && !m.alt && ks.key == "r" {
+ self.note_integration_gap(cx);
+ }
+
let kitty = self.kitty_flags();
if let Some(bytes) = super::input::keystroke_to_bytes(ks, kitty) {
let plain = !m.control && !m.alt && !m.platform;
@@ -1816,6 +1885,15 @@ impl TerminalView {
self.rerank_history(Some(&cwd));
}
+ // Shell integration engaging late (a slow rc file finally reported)
+ // makes a visible integration notice wrong — retract it. The
+ // once-per-pane latch stays set: the overlay works now, there is
+ // nothing left to explain.
+ if self.integration_notice.is_some() && self.terminal.shell_active() {
+ self.integration_notice = None;
+ cx.notify();
+ }
+
// Redraw when the prompt/running state flips, so the line editor shows or
// hides promptly even when the shell produced no output to trigger a
// repaint (e.g. a command that prints nothing). Without this the editor's
@@ -2240,6 +2318,63 @@ impl TerminalView {
.cloned()
}
+ /// Raise the one-shot integration notice if this Ctrl+R fell through to the
+ /// raw PTY path because shell integration never engaged (#46). Silent when
+ /// the raw path is expected instead: integration did engage and a foreground
+ /// command merely owns the PTY, a full-screen TUI owns the pane, or the
+ /// shell is still inside its startup grace window (slow rc files haven't
+ /// reached the first prompt report yet).
+ ///
+ /// Shows a generic message immediately, then refines it off-thread: the
+ /// daemon's foreground query sees the process actually holding the PTY, and
+ /// when that is a known shim (it exec'd over the shell we spawned), naming
+ /// it turns "the feature looks broken" into "here is the culprit".
+ fn note_integration_gap(&mut self, cx: &mut Context) {
+ if self.integration_notice_shown
+ || self.terminal.shell_active()
+ || self.on_alt_screen()
+ || self.created_at.elapsed() < INTEGRATION_GRACE
+ {
+ return;
+ }
+ self.integration_notice_shown = true;
+ self.integration_notice = Some(integration_notice_message(None));
+ cx.notify();
+
+ let pane_id = self.pane_id;
+ cx.spawn(async move |this, cx| {
+ // Best-effort: no daemon / unknown pane / unreadable process just
+ // leaves the generic message standing.
+ let fg = cx
+ .background_executor()
+ .spawn(async move {
+ RemoteTerminal::list_panes()
+ .into_iter()
+ .find(|p| p.pane_id == pane_id)
+ .map(|p| p.title)
+ })
+ .await;
+ if let Some(shim) = fg.as_deref().and_then(known_pty_shim) {
+ let _ = this.update(cx, |view, cx| {
+ if view.integration_notice.is_some() {
+ view.integration_notice = Some(integration_notice_message(Some(shim)));
+ cx.notify();
+ }
+ });
+ }
+ // Reading time is over either way; a keystroke usually beat us here.
+ cx.background_executor()
+ .timer(INTEGRATION_NOTICE_TIMEOUT)
+ .await;
+ let _ = this.update(cx, |view, cx| {
+ if view.integration_notice.take().is_some() {
+ cx.notify();
+ }
+ });
+ })
+ .detach();
+ }
+
/// Begin a Ctrl+R history search (no-op if one is already active). Opens
/// with the empty query's frecency listing, so the menu is browsable
/// before a single key is typed.
@@ -3347,6 +3482,34 @@ impl TerminalView {
)
}
+ /// The one-shot "shell integration didn't engage" notice (#46): a single
+ /// floating line, bottom-right so it reads as a status aside rather than
+ /// part of the prompt. Rendered whenever set — unlike the editor overlays
+ /// it exists precisely because `input_active()` is false.
+ fn render_integration_notice(
+ &self,
+ cx: &mut Context,
+ ) -> Option> {
+ let text = self.integration_notice.clone()?;
+ let theme = cx.theme();
+ Some(
+ div()
+ .absolute()
+ .bottom(px(8.))
+ .right(px(16.))
+ .max_w(px(560.))
+ .px_3()
+ .py_1()
+ .bg(theme.popover)
+ .border_1()
+ .border_color(theme.border)
+ .rounded(px(6.))
+ .text_size(px(12.))
+ .text_color(theme.muted_foreground)
+ .child(text),
+ )
+ }
+
/// Map a highlighter token kind to a theme color.
fn kind_color(&self, kind: TokenKind, cx: &App) -> gpui::Hsla {
let theme = cx.theme();
@@ -3413,6 +3576,9 @@ impl Render for TerminalView {
.input_active()
.then(|| self.render_reverse_search_menu(cx))
.flatten();
+ // Not gated on `input_active()`: the notice explains why the editor
+ // overlays are absent, so it renders exactly when they can't.
+ let integration_notice = self.render_integration_notice(cx);
// Captured for the right-click menu: the focus handle routes dispatched
// actions to this terminal (and lets tab/split ones bubble to the root),
@@ -3494,6 +3660,7 @@ impl Render for TerminalView {
.children(input_bar)
.children(completion_menu)
.children(reverse_search_menu)
+ .children(integration_notice)
// Right-click context menu (gpui-component PopupMenu).
.context_menu(move |menu, _window, _cx| {
// Small size = tighter 20px rows; the default 26px felt too airy.
@@ -4606,6 +4773,91 @@ mod gpui_tests {
gpui::Keystroke::parse(spec).expect("valid keystroke spec")
}
+ /// The notice names only known fig-style shims — an ordinary foreground
+ /// command (`ssh`) must not be blamed for intercepting anything, and the
+ /// generic message must not claim interception it can't prove.
+ #[test]
+ fn shim_detection_names_known_wrappers_only() {
+ assert_eq!(known_pty_shim("zsh (kiro-cli-term)"), Some("kiro-cli-term"));
+ assert_eq!(known_pty_shim("figterm"), Some("figterm"));
+ assert_eq!(known_pty_shim("qterm"), Some("qterm"));
+ assert_eq!(known_pty_shim("ssh"), None);
+ assert_eq!(known_pty_shim("wezterm"), None);
+ assert_eq!(known_pty_shim(""), None);
+ assert!(integration_notice_message(Some("kiro-cli-term")).contains("kiro-cli-term"));
+ assert!(!integration_notice_message(None).contains("intercepting"));
+ }
+
+ /// The Ctrl+R integration notice (#46), through the real key dispatcher:
+ /// silent inside the startup grace window, raised once integration has had
+ /// time to engage and never did, dismissed by the next keystroke, and
+ /// one-shot per pane. The chord itself still reaches the PTY throughout
+ /// (the shell's own reverse-i-search is the fallback).
+ #[gpui::test]
+ fn ctrl_r_without_integration_raises_the_notice_once(cx: &mut TestAppContext) {
+ // `note_integration_gap` queries the daemon for the pane's foreground
+ // process; pin the config dir to a scratch so the control connection
+ // fails cleanly instead of reaching a real user daemon.
+ let dir = std::env::temp_dir().join(format!("tty7-noticetest-{}", std::process::id()));
+ std::fs::create_dir_all(&dir).ok();
+ crate::core::config::set_config_dir(dir);
+
+ let (window, _daemon) = harness(cx);
+ window
+ .update(cx, |view, window, cx| {
+ let ctrl_r = KeyDownEvent {
+ keystroke: key("ctrl-r"),
+ is_held: false,
+ prefer_character_input: false,
+ };
+ // Fresh pane: the shell may legitimately not have reported yet.
+ view.on_key_down(&ctrl_r, window, cx);
+ assert!(
+ view.integration_notice.is_none(),
+ "the grace window stays silent"
+ );
+
+ // Past the grace window with no OSC 133 ever seen → notice.
+ view.created_at = std::time::Instant::now() - INTEGRATION_GRACE * 2;
+ view.on_key_down(&ctrl_r, window, cx);
+ assert!(
+ view.integration_notice.is_some(),
+ "Ctrl+R raises the notice"
+ );
+ cx.notify();
+ })
+ .unwrap();
+
+ // Let the notified frame actually draw — a panic in the notice layout
+ // fails the test here.
+ cx.run_until_parked();
+ window
+ .update(cx, |view, window, cx| {
+ assert!(
+ view.integration_notice.is_some(),
+ "the notice survives a real render pass"
+ );
+
+ // The next keystroke dismisses it; the latch keeps it one-shot.
+ let ctrl_r = KeyDownEvent {
+ keystroke: key("ctrl-r"),
+ is_held: false,
+ prefer_character_input: false,
+ };
+ view.on_key_down(&ctrl_r, window, cx);
+ assert!(
+ view.integration_notice.is_none(),
+ "a keystroke dismisses the notice"
+ );
+ view.on_key_down(&ctrl_r, window, cx);
+ assert!(
+ view.integration_notice.is_none(),
+ "the notice is one-shot per pane"
+ );
+ })
+ .unwrap();
+ }
+
/// The Ctrl+R flow end-to-end at the editor dispatcher: Ctrl+R opens the
/// search, typed text (the IME/commit path) edits the query with fuzzy
/// matching, Enter loads the selection into the editor without running it.