diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a0f3a4..5cc7ce34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- When shell integration never engages in a pane, pressing Ctrl+R now explains + why the history menu can't appear (once per pane, dismissed by the next + keystroke) instead of failing silently — naming the wrapper when a + figterm-style PTY shim (`kiro-cli-term`, `figterm`, `qterm`) is intercepting + the shell's OSC 133 reports. The chord still reaches the shell, so its own + reverse-i-search keeps working. (#46) + ## [0.9.0] - 2026-07-10 ### Changed diff --git a/README.md b/README.md index 10430501..09deb20a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) [![Discord](https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/s3dethqz2V) -[**Install**](#-install) · [**Benchmarks**](#-benchmarks) · [**Shortcuts**](#️-shortcuts) · [**Contributing**](#-contributing) +[**Why tty7**](#why-tty7) · [**Install**](#install) · [**Features**](#features) · [**Benchmarks**](#benchmarks) · [**Keybindings**](#keybindings) English · [简体中文](README.zh-CN.md) @@ -25,48 +25,50 @@ -
+## Why tty7 -tty7 is a GPU-rendered terminal. An 11 MB `cat` finishes in **95 ms** — about -2× faster than Alacritty, Ghostty, or Kitty on the same hardware — and the -built-in prompt gives you inline completion, syntax highlighting, and per-flag -hints for common commands. Pure Rust, native on macOS, Windows, and Linux, no -configuration needed. +- **Fast** — about 2× the throughput of Alacritty, Ghostty, or Kitty on the + same hardware ([benchmarks](#benchmarks)) +- **A modern prompt** — completion, syntax highlighting, and history search + built in; no plugins to assemble +- **Sessions that survive** — close the window or quit the app, your shells + keep running; no tmux +- **Zero config** — zsh, bash, fish, and PowerShell work out of the box -- ⚡ **Fast** — an 11 MB `cat` completes in **95 ms**, versus - 179–239 ms for Alacritty/Ghostty/Kitty; DOOM-fire renders at **888 fps** - against their 485–617. Same machine, same grid; the harness is in the repo - ([benchmarks](#-benchmarks)). -- ⌨️ **Prompt with completions** — inline completion, syntax highlighting, - history, and in-terminal search. Type - `git commit --`, `kubectl`, or `npm` and every flag and subcommand shows up - with its description — signatures for ~100 common commands, generated - from Fig's spec corpus. -- 🧠 **Shell-aware, zero config** — new tabs and splits open in the current - working directory, and path completion always follows where you are. zsh, - bash, fish, and PowerShell are wired up automatically. -- 🔌 **Sessions that survive** — shells run in a background daemon, so closing a - window, quitting the app, or swapping in a new build never takes a shell down. - Detach and reattach, no tmux. +Native builds for macOS, Windows, and Linux ship with every release. -Also included: tabs (drag to reorder, inline rename, number keys to switch) and -resizable splits, a command palette, click-to-open links, desktop notifications, -and focus-follows-mouse. Eight built-in themes from light to dark, with the -native window chrome following the one you pick, plus CJK/IME input. +## Install -Native builds for macOS, Windows, and Linux — every release ships all three. +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). -
+## 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://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) [![Discord](https://img.shields.io/badge/Discord-%E5%8A%A0%E5%85%A5%E7%BE%A4%E7%BB%84-5865F2?logo=discord&logoColor=white)](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.