Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 37 additions & 59 deletions rust/limux-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,82 +1016,60 @@ async fn run_agent_hook(
// Build a human-friendly title + body depending on event + agent.
let agent_label = agent.label();
persist_agent_hook_session(agent, args, &payload, &event)?;
let (title, body) = match event.as_str() {
"Notification" => (
let notification_title_body = match event.as_str() {
"Notification" | "notification" => Some((
format!("{agent_label} needs you"),
hook_str(&payload, &["message", "notification"])
.unwrap_or("waiting for input")
.to_owned(),
),
"Stop" | "SubagentStop" => (
)),
"Stop" | "stop" | "finished" => Some((
format!("{agent_label} finished"),
hook_str(&payload, &["message", "reason"])
.unwrap_or("task complete")
.to_owned(),
),
"SessionStart" => (
format!("{agent_label} session started"),
hook_str(&payload, &["cwd", "source"])
.unwrap_or("")
.to_owned(),
),
"SessionEnd" => (
format!("{agent_label} session ended"),
hook_str(&payload, &["reason"]).unwrap_or("").to_owned(),
),
"PreToolUse" | "PostToolUse" => (
format!(
"{agent_label}: {}",
hook_str(&payload, &["tool_name"]).unwrap_or("tool")
),
hook_str(&payload, &["tool_input", "summary"])
.unwrap_or("")
.to_owned(),
),
"UserPromptSubmit" => (
format!("{agent_label}: new prompt"),
hook_str(&payload, &["prompt"])
.unwrap_or("")
.chars()
.take(120)
.collect(),
),
other => (
)),
"SessionStart" | "session-start" | "SessionEnd" | "session-end" | "UserPromptSubmit"
| "prompt-submit" | "PreToolUse" | "PostToolUse" | "SubagentStop" | "subagent-stop"
| "Cleanup" | "cleanup" | "restore-exit" => None,
other => Some((
format!("{agent_label}: {other}"),
hook_str(&payload, &["message", "summary"])
.unwrap_or("")
.to_owned(),
),
)),
};

let subtitle = hook_str(&payload, &["session_id"])
.map(|s| {
// Show only a short prefix of the session id to keep sidebar tidy.
s.chars().take(8).collect::<String>()
})
.unwrap_or_default();
if let Some((title, body)) = notification_title_body {
let subtitle = hook_str(&payload, &["session_id"])
.map(|s| {
// Show only a short prefix of the session id to keep sidebar tidy.
s.chars().take(8).collect::<String>()
})
.unwrap_or_default();

let (workspace, surface) = notification_scope(args, env_opt);
let (workspace, surface) = notification_scope(args, env_opt);

let mut params = Map::new();
params.insert("title".to_string(), Value::String(title));
if !subtitle.is_empty() {
params.insert("subtitle".to_string(), Value::String(subtitle));
}
if !body.is_empty() {
params.insert("body".to_string(), Value::String(body));
}
if let Some(surface) = surface {
params.insert("surface_id".to_string(), Value::String(surface));
}
let mut params = Map::new();
params.insert("title".to_string(), Value::String(title));
if !subtitle.is_empty() {
params.insert("subtitle".to_string(), Value::String(subtitle));
}
if !body.is_empty() {
params.insert("body".to_string(), Value::String(body));
}
if let Some(surface) = surface {
params.insert("surface_id".to_string(), Value::String(surface));
}

let _ = call_in_workspace_scope(
client,
workspace,
"notification.create",
Value::Object(params),
)
.await;
let _ = call_in_workspace_scope(
client,
workspace,
"notification.create",
Value::Object(params),
)
.await;
}

Ok(agent_hook_output(&event, &payload))
}
Expand Down
104 changes: 104 additions & 0 deletions rust/limux-host-linux/src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,80 @@ pub struct WorkspaceConfig {
pub keep_open_after_last_terminal_closes: bool,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum UnreadColor {
#[default]
Amber,
Emerald,
Rose,
Cyan,
AccentBlue,
}

impl UnreadColor {
pub fn as_str(self) -> &'static str {
match self {
Self::Amber => "amber",
Self::Emerald => "emerald",
Self::Rose => "rose",
Self::Cyan => "cyan",
Self::AccentBlue => "accent_blue",
}
}

pub fn from_str(s: &str) -> Option<Self> {
match s {
"amber" => Some(Self::Amber),
"emerald" => Some(Self::Emerald),
"rose" => Some(Self::Rose),
"cyan" => Some(Self::Cyan),
"accent_blue" => Some(Self::AccentBlue),
_ => None,
}
}

pub fn css_color(self) -> &'static str {
match self {
Self::Amber => "#f59e0b",
Self::Emerald => "#10b981",
Self::Rose => "#f43f5e",
Self::Cyan => "#06b6d4",
Self::AccentBlue => "@accent_bg_color",
}
}

pub fn labels() -> &'static [&'static str] {
&["Amber (Default)", "Emerald", "Rose", "Cyan", "Accent Blue"]
}

pub fn dropdown_index(self) -> u32 {
match self {
Self::Amber => 0,
Self::Emerald => 1,
Self::Rose => 2,
Self::Cyan => 3,
Self::AccentBlue => 4,
}
}

pub fn from_dropdown_index(index: u32) -> Self {
match index {
1 => Self::Emerald,
2 => Self::Rose,
3 => Self::Cyan,
4 => Self::AccentBlue,
_ => Self::Amber,
}
}
}

#[derive(Clone, Debug, PartialEq)]
pub struct AppearanceConfig {
pub color_scheme: ColorScheme,
pub ghostty_color_scheme: ColorScheme,
pub ui_scale: UiScale,
pub show_workspace_path: bool,
pub unread_color: UnreadColor,
}

impl Default for AppearanceConfig {
Expand All @@ -103,6 +171,7 @@ impl Default for AppearanceConfig {
ghostty_color_scheme: ColorScheme::default(),
ui_scale: UiScale::default(),
show_workspace_path: true,
unread_color: UnreadColor::default(),
}
}
}
Expand Down Expand Up @@ -409,6 +478,12 @@ fn parse_app_config_value(root: &Value) -> AppConfig {
.and_then(Value::as_bool)
.unwrap_or(true);

let unread_color = appearance
.and_then(|appearance| appearance.get("unread_color"))
.and_then(Value::as_str)
.and_then(UnreadColor::from_str)
.unwrap_or_default();

let workspace = root.get("workspace").and_then(Value::as_object);
let keep_open_after_last_terminal_closes = workspace
.and_then(|workspace| workspace.get("keep_open_after_last_terminal_closes"))
Expand Down Expand Up @@ -475,6 +550,7 @@ fn parse_app_config_value(root: &Value) -> AppConfig {
ghostty_color_scheme,
ui_scale,
show_workspace_path,
unread_color,
},
workspace: WorkspaceConfig {
keep_open_after_last_terminal_closes,
Expand Down Expand Up @@ -527,6 +603,12 @@ fn save_to_path(path: &Path, config: &AppConfig) -> Result<(), String> {
json!(config.appearance.ui_scale.get()),
);
}
if config.appearance.unread_color != UnreadColor::default() {
appearance.insert(
"unread_color".to_string(),
json!(config.appearance.unread_color.as_str()),
);
}
root.insert("appearance".to_string(), Value::Object(appearance));
root.insert(
"focus".to_string(),
Expand Down Expand Up @@ -900,6 +982,28 @@ mod tests {
assert_eq!(loaded.config.appearance.ui_scale.get(), MAX_UI_SCALE);
}

#[test]
fn save_and_load_round_trips_unread_color() {
let dir = TempDir::new().expect("temp dir");
let path = settings_path_in(dir.path());
let mut config = AppConfig::default();
assert_eq!(config.appearance.unread_color, UnreadColor::Amber);

for color in [
UnreadColor::Amber,
UnreadColor::Emerald,
UnreadColor::Rose,
UnreadColor::Cyan,
UnreadColor::AccentBlue,
] {
config.appearance.unread_color = color;
save_to_path(&path, &config).expect("save config");
let loaded = load_from_path(&path);
assert!(loaded.warnings.is_empty());
assert_eq!(loaded.config.appearance.unread_color, color);
}
}

#[test]
fn load_from_path_reads_font_size_when_valid() {
let dir = TempDir::new().expect("temp dir");
Expand Down
Loading
Loading