Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
edb5ec7
docs: add terminal scrollbar and search design spec
oso95 Mar 18, 2026
b1ad6a5
fix: route hook signals by UUID instead of legacy integer session ID
oso95 Mar 18, 2026
502317f
Add terminal search and fix hook signal UUID routing
oso95 Mar 19, 2026
2ce4030
Fix search, shortcut, and layout toggle UI regressions
oso95 Mar 19, 2026
ef356d4
Remap terminal-safe app shortcuts
oso95 Mar 19, 2026
b8aa79c
Simplify theme dropdown options
oso95 Mar 19, 2026
7f89dcd
Add terminal styling controls to settings
oso95 Mar 19, 2026
abf0cce
Optimize terminal search and normalize Codex hook paths
oso95 Mar 19, 2026
0f6f9ae
Show app version in settings sidebar
oso95 Mar 20, 2026
7f0ffe4
Refine terminal search caret UI
oso95 Mar 20, 2026
8a1d521
Add terminal theme presets to settings
oso95 Mar 20, 2026
f41498e
Promote terminal presets into full themes
oso95 Mar 20, 2026
d586b29
Fix Codex hook task lifecycle mapping
oso95 Mar 20, 2026
7fdb281
Document local macOS DMG build flow
oso95 Mar 20, 2026
4c80f13
Add more built-in light themes
oso95 Mar 20, 2026
30ab14f
Add custom terminal theme editor
oso95 Mar 20, 2026
44a38ec
Restore update toast after restart
oso95 Mar 20, 2026
10e30e7
Remove unused terminal theme preset module
oso95 Mar 20, 2026
59a3dd3
Harden update restore persistence
oso95 Mar 20, 2026
065d98c
Optimize terminal search and address review follow-ups
oso95 Mar 20, 2026
b250561
Debounce theme persistence and gate settings migrations
oso95 Mar 20, 2026
2a948e5
Fix idle Claude Code sessions falsely showing as Working after 10 min
oso95 Mar 21, 2026
a45fabb
Move built-in themes into JSON registry
oso95 Mar 21, 2026
540f5c2
Fix terminal font freeze and decouple font family from themes
oso95 Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 173 additions & 18 deletions crates/codirigent-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,94 @@ impl Default for GeneralSettings {
}
}

/// Internal user-settings migration markers.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct UserSettingsMigrations {
/// Version of the terminal-safe keybinding migration already applied.
#[serde(default)]
pub terminal_safe_keybindings_version: u32,
}

// ============================================================================
// Terminal Settings
// ============================================================================

/// Optional ANSI palette overrides layered on top of the selected theme.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TerminalPaletteOverrides {
/// ANSI black override.
#[serde(default)]
pub black: String,
/// ANSI red override.
#[serde(default)]
pub red: String,
/// ANSI green override.
#[serde(default)]
pub green: String,
/// ANSI yellow override.
#[serde(default)]
pub yellow: String,
/// ANSI blue override.
#[serde(default)]
pub blue: String,
/// ANSI magenta override.
#[serde(default)]
pub magenta: String,
/// ANSI cyan override.
#[serde(default)]
pub cyan: String,
/// ANSI white override.
#[serde(default)]
pub white: String,
/// ANSI bright black override.
#[serde(default)]
pub bright_black: String,
/// ANSI bright red override.
#[serde(default)]
pub bright_red: String,
/// ANSI bright green override.
#[serde(default)]
pub bright_green: String,
/// ANSI bright yellow override.
#[serde(default)]
pub bright_yellow: String,
/// ANSI bright blue override.
#[serde(default)]
pub bright_blue: String,
/// ANSI bright magenta override.
#[serde(default)]
pub bright_magenta: String,
/// ANSI bright cyan override.
#[serde(default)]
pub bright_cyan: String,
/// ANSI bright white override.
#[serde(default)]
pub bright_white: String,
}

/// Per-user terminal theme overrides layered on top of the selected theme.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TerminalThemeOverrides {
/// Terminal background color override.
#[serde(default)]
pub background: String,
/// Terminal foreground color override.
#[serde(default)]
pub foreground: String,
/// Terminal cursor color override.
#[serde(default)]
pub cursor: String,
/// Terminal selection background override.
#[serde(default)]
pub selection_background: String,
/// Terminal selection foreground override.
#[serde(default)]
pub selection_foreground: String,
/// ANSI palette overrides.
#[serde(default)]
pub palette: TerminalPaletteOverrides,
}

/// Terminal rendering preferences.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TerminalSettings {
Expand All @@ -222,15 +306,39 @@ pub struct TerminalSettings {
pub cursor_style: String,
/// Line height multiplier (1.0 = natural font height).
pub line_height: f32,
/// Per-user terminal theme overrides layered on top of the selected theme.
#[serde(default)]
pub theme_overrides: TerminalThemeOverrides,
}

/// Default monospace terminal font family for the current platform.
pub fn default_terminal_font_family() -> &'static str {
#[cfg(target_os = "windows")]
{
"Consolas"
}
#[cfg(target_os = "macos")]
{
"Menlo"
}
#[cfg(all(unix, not(target_os = "macos")))]
{
"DejaVu Sans Mono"
}
#[cfg(not(any(windows, unix)))]
{
"monospace"
}
}

impl Default for TerminalSettings {
fn default() -> Self {
Self {
font_family: "JetBrains Mono".to_string(),
font_family: default_terminal_font_family().to_string(),
font_size: 13.0,
cursor_style: "block".to_string(),
line_height: 1.0,
theme_overrides: TerminalThemeOverrides::default(),
}
}
}
Expand Down Expand Up @@ -286,6 +394,9 @@ pub struct UserSettings {
/// User-saved custom layout profiles.
#[serde(default)]
pub saved_layouts: Vec<SavedLayout>,
/// Internal migration markers for one-time settings rewrites.
#[serde(default)]
pub migrations: UserSettingsMigrations,
}

impl Default for UserSettings {
Expand All @@ -298,6 +409,7 @@ impl Default for UserSettings {
modules: ModuleSettings::default(),
keybindings: Self::default_keybindings(),
saved_layouts: Vec::new(),
migrations: UserSettingsMigrations::default(),
}
}
}
Expand All @@ -323,19 +435,20 @@ impl UserSettings {
bindings.insert("switch_session_7".to_string(), format!("{m}+7"));
bindings.insert("switch_session_8".to_string(), format!("{m}+8"));
bindings.insert("switch_session_9".to_string(), format!("{m}+9"));
bindings.insert("new_session".to_string(), format!("{m}+N"));
bindings.insert("close_session".to_string(), format!("{m}+W"));
bindings.insert("toggle_layout".to_string(), format!("{m}+\\"));
bindings.insert("toggle_sidebar".to_string(), format!("{m}+B"));
bindings.insert("toggle_task_board".to_string(), format!("{m}+T"));
bindings.insert("new_session".to_string(), format!("{m}+Shift+N"));
bindings.insert("close_session".to_string(), format!("{m}+Alt+W"));
bindings.insert("toggle_layout".to_string(), format!("{m}+Shift+L"));
bindings.insert("toggle_sidebar".to_string(), format!("{m}+Shift+E"));
bindings.insert("toggle_task_board".to_string(), format!("{m}+Shift+T"));
bindings.insert("open_settings".to_string(), format!("{m}+,"));
bindings.insert("quit".to_string(), format!("{m}+Q"));
bindings.insert("paste".to_string(), format!("{m}+V"));
bindings.insert("copy".to_string(), format!("{m}+C"));
bindings.insert("split_horizontal".to_string(), format!("{m}+D"));
bindings.insert("split_vertical".to_string(), format!("{m}+Shift+D"));
bindings.insert("close_pane".to_string(), format!("{m}+Shift+W"));
bindings.insert("quick_switch".to_string(), format!("{m}+K"));
bindings.insert("search_terminal".to_string(), format!("{m}+Shift+F"));
bindings.insert("split_horizontal".to_string(), format!("{m}+Alt+Shift+H"));
bindings.insert("split_vertical".to_string(), format!("{m}+Alt+Shift+V"));
bindings.insert("close_pane".to_string(), format!("{m}+Alt+Shift+W"));
bindings.insert("quick_switch".to_string(), format!("{m}+Shift+K"));
bindings
}
}
Expand Down Expand Up @@ -788,6 +901,14 @@ mod tests {
assert!(!settings.keybindings.is_empty());
}

#[test]
fn test_terminal_settings_default_font_family_is_platform_specific() {
let settings = TerminalSettings::default();
assert_eq!(settings.font_family, default_terminal_font_family());
assert!(settings.theme_overrides.background.is_empty());
assert!(settings.theme_overrides.palette.bright_white.is_empty());
}

#[test]
fn test_user_settings_serialization() {
let settings = UserSettings::default();
Expand All @@ -802,22 +923,56 @@ mod tests {
let bindings = UserSettings::default_keybindings();
#[cfg(target_os = "macos")]
{
assert_eq!(bindings.get("new_session"), Some(&"Cmd+N".to_string()));
assert_eq!(bindings.get("close_session"), Some(&"Cmd+W".to_string()));
assert_eq!(bindings.get("toggle_sidebar"), Some(&"Cmd+B".to_string()));
assert_eq!(
bindings.get("new_session"),
Some(&"Cmd+Shift+N".to_string())
);
assert_eq!(
bindings.get("close_session"),
Some(&"Cmd+Alt+W".to_string())
);
assert_eq!(
bindings.get("toggle_sidebar"),
Some(&"Cmd+Shift+E".to_string())
);
assert_eq!(
bindings.get("toggle_task_board"),
Some(&"Cmd+T".to_string())
Some(&"Cmd+Shift+T".to_string())
);
assert_eq!(
bindings.get("toggle_layout"),
Some(&"Cmd+Shift+L".to_string())
);
assert_eq!(
bindings.get("search_terminal"),
Some(&"Cmd+Shift+F".to_string())
);
}
#[cfg(not(target_os = "macos"))]
{
assert_eq!(bindings.get("new_session"), Some(&"Ctrl+N".to_string()));
assert_eq!(bindings.get("close_session"), Some(&"Ctrl+W".to_string()));
assert_eq!(bindings.get("toggle_sidebar"), Some(&"Ctrl+B".to_string()));
assert_eq!(
bindings.get("new_session"),
Some(&"Ctrl+Shift+N".to_string())
);
assert_eq!(
bindings.get("close_session"),
Some(&"Ctrl+Alt+W".to_string())
);
assert_eq!(
bindings.get("toggle_sidebar"),
Some(&"Ctrl+Shift+E".to_string())
);
assert_eq!(
bindings.get("toggle_task_board"),
Some(&"Ctrl+T".to_string())
Some(&"Ctrl+Shift+T".to_string())
);
assert_eq!(
bindings.get("toggle_layout"),
Some(&"Ctrl+Shift+L".to_string())
);
assert_eq!(
bindings.get("search_terminal"),
Some(&"Ctrl+Shift+F".to_string())
);
}
assert!(bindings.contains_key("quick_switch"));
Expand Down
1 change: 1 addition & 0 deletions crates/codirigent-core/src/config_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub struct EffectiveConfig {
///
/// Emitted when a configuration file is modified and reloaded.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ConfigChange {
/// Project configuration was changed.
ProjectConfigChanged(ProjectConfig),
Expand Down
91 changes: 89 additions & 2 deletions crates/codirigent-core/src/hook_installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,49 @@ fn merge_codex_notify(settings: &mut TomlValue, command: &str) -> Result<bool> {

match notify_value {
TomlValue::Array(arr) => {
if arr.iter().any(|value| value.as_str() == Some(command)) {
let mut normalized = Vec::with_capacity(arr.len().max(1));
let mut inserted_codirigent = false;
let mut modified = false;

for value in arr.iter() {
match value.as_str() {
Some(existing) if existing == command => {
if inserted_codirigent {
modified = true;
continue;
}
normalized.push(TomlValue::String(command.to_owned()));
inserted_codirigent = true;
}
Some(existing) if existing.contains(HOOK_MARKER) => {
if !inserted_codirigent {
normalized.push(TomlValue::String(command.to_owned()));
inserted_codirigent = true;
}
modified = true;
}
_ => normalized.push(value.clone()),
}
}

if !inserted_codirigent {
normalized.push(TomlValue::String(command.to_owned()));
modified = true;
}

if !modified && normalized.len() == arr.len() {
return Ok(false);
}
arr.push(TomlValue::String(command.to_owned()));

*arr = normalized;
Ok(true)
}
TomlValue::String(existing) => {
if existing == command {
Ok(false)
} else if existing.contains(HOOK_MARKER) {
*existing = command.to_owned();
Ok(true)
} else {
*notify_value = TomlValue::Array(vec![
TomlValue::String(existing.clone()),
Expand Down Expand Up @@ -654,6 +688,14 @@ mod tests {
.len(),
2
);
assert_eq!(
settings["notify"]
.as_array()
.expect("hook installer test should succeed")[0]
.as_str()
.expect("hook installer test should succeed"),
"/usr/local/bin/codirigent-hook"
);
}

#[test]
Expand Down Expand Up @@ -681,6 +723,51 @@ mod tests {
);
}

#[test]
fn codex_config_replaces_stale_codirigent_paths_in_notify_array() {
let mut settings: TomlValue = toml::from_str(
r#"
notify = [
"/Volumes/Codirigent 3/Codirigent.app/Contents/MacOS/codirigent-hook",
"/Applications/Codirigent.app/Contents/MacOS/codirigent-hook",
"notify-send"
]
"#,
)
.expect("hook installer test should succeed");
let modified =
merge_codex_notify(&mut settings, CMD).expect("hook installer test should succeed");

assert!(modified);
assert_eq!(
settings["notify"]
.as_array()
.expect("hook installer test should succeed"),
&vec![
TomlValue::String(CMD.to_string()),
TomlValue::String("notify-send".to_string())
]
);
}

#[test]
fn codex_config_upgrades_legacy_string_notify_in_place() {
let mut settings: TomlValue = toml::from_str(
r#"notify = "/Volumes/Codirigent 3/Codirigent.app/Contents/MacOS/codirigent-hook""#,
)
.expect("hook installer test should succeed");
let modified =
merge_codex_notify(&mut settings, CMD).expect("hook installer test should succeed");

assert!(modified);
assert_eq!(
settings["notify"]
.as_str()
.expect("hook installer test should succeed"),
CMD
);
}

#[test]
fn fresh_gemini_install_adds_expected_hooks() {
let mut settings = json!({});
Expand Down
Loading
Loading