aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 01:55:59 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-05-21 03:10:52 +0300
commit7515695a1996e9b406a3cf1eef43208c6c248929 (patch)
treec0fd480d20b17a0309fad7a1933aa64d043dafc3
parent6172275252f5f724d874163462f5394800f8d55b (diff)
fix: keep cjk ime agent allow-list safe
refs #149
-rw-r--r--docs/next/CHANGELOG.md4
-rw-r--r--docs/next/website/src/content/docs/configuration.mdx2
-rw-r--r--src/app/mod.rs3
-rw-r--r--src/app/state.rs4
-rw-r--r--src/config/model.rs3
-rw-r--r--src/main.rs1
-rw-r--r--src/server/headless.rs29
-rw-r--r--src/server/render_stream.rs4
8 files changed, 44 insertions, 6 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index f504ac25..ae7e16c5 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -2,6 +2,9 @@
## Unreleased
+### Added
+- Added `experimental.reveal_hidden_cursor_for_cjk_ime = false` (opt-in), `experimental.cjk_ime_agents = []` (optional allow-list), and `experimental.cjk_ime_cursor_shape = "steady_block"` to expose the focused pane's cursor anchor to the outer terminal even when the pane requested `?25l`, restoring macOS IME candidate-window tracking for TUIs that paint their own cursor (Claude Code, pi, codex). When `cjk_ime_agents` is non-empty, the reveal applies only to focused panes whose detected agent matches one of the listed names. When the pane reports no cursor position, the anchor falls back to the pane's top-left so a stable IME hint is always available. Trade-off when enabled: an extra hardware cursor may appear in the outer terminal for apps that hide the cursor without painting a replacement. (#149, thanks @ChihGodlee)
+
### Fixed
- `herdr --remote` now offers to restart the remote server after installing or replacing a remote binary, or when the running server version differs, even if the client/server protocol is still compatible.
@@ -14,7 +17,6 @@
- Added update badges on the sidebar menu, settings menu item, and integrations settings tab when installed integrations are outdated.
- Added `terminal.default_shell` to choose the executable used for new interactive panes. When unset, Herdr still falls back to `$SHELL`, then `/bin/sh`. (#196)
- Added native Kiro CLI detection with idle and working state heuristics. (#185)
-- Added `experimental.reveal_hidden_cursor_for_cjk_ime = false` (opt-in), `experimental.cjk_ime_agents = []` (optional allow-list), and `experimental.cjk_ime_cursor_shape = "steady_block"` to expose the focused pane's cursor anchor to the outer terminal even when the pane requested `?25l`, restoring macOS IME candidate-window tracking for TUIs that paint their own cursor (Claude Code, pi, codex). When `cjk_ime_agents` is non-empty, the reveal applies only to focused panes whose detected agent matches one of the listed names. When the pane reports no cursor position, the anchor falls back to the pane's top-left so a stable IME hint is always available. Trade-off when enabled: an extra hardware cursor may appear in the outer terminal for apps that hide the cursor without painting a replacement. (#149)
### Fixed
- Keybinding conflict warnings now stay visible and show one readable yellow row per conflicting binding.
diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx
index a026166a..0ab74fd8 100644
--- a/docs/next/website/src/content/docs/configuration.mdx
+++ b/docs/next/website/src/content/docs/configuration.mdx
@@ -299,7 +299,7 @@ cjk_ime_cursor_shape = "steady_block"
When enabled, the cursor stays visible at the focused pane's reported position. If the pane reports no cursor position, the anchor falls back to the pane's top-left so a stable IME hint is always available.
-`cjk_ime_agents` is an optional allow-list. When empty, the reveal applies to any focused pane. When non-empty, the reveal only applies if the focused pane's detected agent matches one of the listed names — useful to enable the reveal only for AI-agent TUIs that paint their own cursor while leaving plain shells untouched. Accepted names: `pi`, `claude`, `codex`, `gemini`, `cursor`, `cline`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`. Unknown names are ignored.
+`cjk_ime_agents` is an optional allow-list. When empty, the reveal applies to any focused pane. When non-empty, the reveal only applies if the focused pane's detected agent matches one of the listed names — useful to enable the reveal only for AI-agent TUIs that paint their own cursor while leaving plain shells untouched. Accepted names: `pi`, `claude`, `codex`, `gemini`, `cursor`, `cline`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`. Unknown names are ignored; if the list contains no valid names, the reveal does not apply.
`cjk_ime_cursor_shape` controls the DECSCUSR shape rendered for the IME anchor. Accepted values: `block`, `steady_block` (default), `underline`, `steady_underline`, `bar`, `steady_bar`.
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 5cc40ecc..4c4845a1 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -428,6 +428,7 @@ impl App {
prompt_new_tab_name: config.ui.prompt_new_tab_name,
show_agent_labels_on_pane_borders: config.ui.show_agent_labels_on_pane_borders,
reveal_hidden_cursor_for_cjk_ime: config.experimental.reveal_hidden_cursor_for_cjk_ime,
+ cjk_ime_agent_filter_configured: !config.experimental.cjk_ime_agents.is_empty(),
cjk_ime_agents: parse_cjk_ime_agents(&config.experimental.cjk_ime_agents),
cjk_ime_cursor_shape: config.experimental.cjk_ime_cursor_shape.to_decscusr(),
kitty_graphics_enabled: config.experimental.kitty_graphics,
@@ -907,6 +908,8 @@ impl App {
}
self.state.reveal_hidden_cursor_for_cjk_ime =
config.experimental.reveal_hidden_cursor_for_cjk_ime;
+ self.state.cjk_ime_agent_filter_configured =
+ !config.experimental.cjk_ime_agents.is_empty();
self.state.cjk_ime_agents = parse_cjk_ime_agents(&config.experimental.cjk_ime_agents);
self.state.cjk_ime_cursor_shape =
config.experimental.cjk_ime_cursor_shape.to_decscusr();
diff --git a/src/app/state.rs b/src/app/state.rs
index 1566fdeb..6924dc65 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -969,7 +969,8 @@ pub struct AppState {
/// the pane requested `?25l`. See `[experimental] reveal_hidden_cursor_for_cjk_ime`.
pub reveal_hidden_cursor_for_cjk_ime: bool,
/// Restrict cursor reveal to focused panes whose detected agent matches
- /// one of these. Empty means apply to any focused pane.
+ /// one of these. When false, apply to any focused pane.
+ pub cjk_ime_agent_filter_configured: bool,
pub cjk_ime_agents: Vec<crate::detect::Agent>,
/// DECSCUSR shape parameter (1–6) for the IME anchor cursor.
pub cjk_ime_cursor_shape: u8,
@@ -1233,6 +1234,7 @@ impl AppState {
prompt_new_tab_name: true,
show_agent_labels_on_pane_borders: false,
reveal_hidden_cursor_for_cjk_ime: false,
+ cjk_ime_agent_filter_configured: false,
cjk_ime_agents: Vec::new(),
cjk_ime_cursor_shape: 2, // steady_block
kitty_graphics_enabled: false,
diff --git a/src/config/model.rs b/src/config/model.rs
index ba52d579..40d425a3 100644
--- a/src/config/model.rs
+++ b/src/config/model.rs
@@ -265,7 +265,8 @@ pub struct ExperimentalConfig {
pub reveal_hidden_cursor_for_cjk_ime: bool,
/// Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose
/// detected agent matches one of these names (case-insensitive). Empty
- /// list means apply to any focused pane. Unknown agent names are ignored.
+ /// list means apply to any focused pane. Unknown agent names are ignored;
+ /// if the list contains no valid names, the reveal does not apply.
/// Accepted names: pi, claude, codex, gemini, cursor, cline, opencode,
/// copilot, kimi, kiro, droid, amp, grok, hermes. Default: empty.
pub cjk_ime_agents: Vec<String>,
diff --git a/src/main.rs b/src/main.rs
index 440a87a5..17052159 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -209,6 +209,7 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
# reveal_hidden_cursor_for_cjk_ime = false
# Optional allow-list: only reveal for focused panes whose detected agent
# matches one of these names. Empty means apply to any focused pane.
+# If the list contains no valid names, the reveal does not apply.
# Accepted: pi, claude, codex, gemini, cursor, cline, opencode, copilot,
# kimi, kiro, droid, amp, grok, hermes.
# cjk_ime_agents = []
diff --git a/src/server/headless.rs b/src/server/headless.rs
index f56dbdeb..539892e0 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -2804,6 +2804,7 @@ mod tests {
state.reveal_hidden_cursor_for_cjk_ime = true;
// Filter only Claude, but the test pane has no detected agent, so the
// reveal must not apply.
+ state.cjk_ime_agent_filter_configured = true;
state.cjk_ime_agents = vec![crate::detect::Agent::Claude];
let mut ws = crate::workspace::Workspace::test_new("test");
let pane_id = ws.tabs[0].root_pane;
@@ -2828,6 +2829,34 @@ mod tests {
}
#[tokio::test]
+ async fn virtual_render_skips_reveal_when_agent_filter_has_no_valid_entries() {
+ let mut state = AppState::test_new();
+ state.reveal_hidden_cursor_for_cjk_ime = true;
+ state.cjk_ime_agent_filter_configured = true;
+ state.cjk_ime_agents = Vec::new();
+ let mut ws = crate::workspace::Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ ws.insert_test_runtime(
+ pane_id,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(20, 5, b"left\x1b[?25l"),
+ );
+
+ state.workspaces = vec![ws];
+ state.active = Some(0);
+ state.selected = 0;
+ state.mode = crate::app::Mode::Terminal;
+
+ let area = Rect::new(0, 0, 80, 24);
+ let (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+
+ assert!(
+ cursor.as_ref().is_none_or(|cursor| !cursor.visible),
+ "agent filter with no valid entries should suppress reveal; got {cursor:?}",
+ );
+ }
+
+ #[tokio::test]
async fn virtual_render_omits_focused_pane_cursor_while_mobile_switcher_open() {
let mut state = AppState::test_new();
let mut ws = crate::workspace::Workspace::test_new("test");
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index 6dd4ad48..b6e50444 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -327,9 +327,9 @@ fn focused_terminal_cursor(app_state: &AppState) -> Option<CursorState> {
// Determine whether the IME-anchor reveal applies to this focused pane.
// The master switch must be on, and either no agent filter is configured
// (apply to any pane) or the focused pane's detected agent matches the
- // allow-list.
+ // allow-list. A configured list with no valid entries reveals nothing.
let reveal = app_state.reveal_hidden_cursor_for_cjk_ime
- && (app_state.cjk_ime_agents.is_empty() || {
+ && (!app_state.cjk_ime_agent_filter_configured || {
let detected = app_state
.workspaces
.get(ws_idx)