use askama::Template;
use std::path::Path;
pub struct AgentTabParams<'a> {
pub tab_name: &'a str,
pub pane_name: &'a str,
pub command: &'a str,
pub cwd: &'a Path,
pub shell: &'a str,
pub focus: bool,
pub close_on_exit: bool,
}
#[derive(Template)]
#[template(path = "agent_tab.kdl.j2", escape = "none")]
struct AgentTab {
tab_name: String,
pane_name: String,
command: String,
cwd: String,
shell: String,
focus: bool,
close_on_exit: bool,
}
#[derive(Template)]
#[template(path = "subagent.kdl.j2", escape = "none")]
struct SubagentLayout {
agent_tab: String,
}
#[derive(Template)]
#[template(path = "main.kdl.j2", escape = "none")]
struct MainLayout {
agent_tabs: Vec<String>,
}
pub fn generate_agent_layout(params: &AgentTabParams) -> Result<String, askama::Error> {
let tab = AgentTab {
tab_name: params.tab_name.to_string(),
pane_name: params.pane_name.to_string(),
command: params.command.to_string(),
cwd: params.cwd.display().to_string(),
shell: params.shell.to_string(),
focus: params.focus,
close_on_exit: params.close_on_exit,
}
.render()?;
SubagentLayout { agent_tab: tab }.render()
}
pub fn generate_main_layout(tabs: Vec<AgentTabParams>) -> Result<String, askama::Error> {
let rendered_tabs: Result<Vec<_>, _> = tabs
.iter()
.map(|params| {
AgentTab {
tab_name: params.tab_name.to_string(),
pane_name: params.pane_name.to_string(),
command: params.command.to_string(),
cwd: params.cwd.display().to_string(),
shell: params.shell.to_string(),
focus: params.focus,
close_on_exit: params.close_on_exit,
}
.render()
})
.collect();
MainLayout {
agent_tabs: rendered_tabs?,
}
.render()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_agent_layout() {
let params = AgentTabParams {
tab_name: "🤖 473-test",
pane_name: "Agent",
command: "claude --prompt 'test'",
cwd: Path::new("/tmp/test"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("tab name=\"🤖 473-test\""));
assert!(layout.contains("focus=true"));
assert!(layout.contains("zjstatus.wasm"));
assert!(layout.contains("exomonad-plugin.wasm"));
assert!(layout.contains("/bin/zsh"));
assert!(layout.contains("claude --prompt 'test'"));
assert!(layout.contains("/tmp/test"));
}
#[test]
fn test_generate_main_layout() {
let tabs = vec![
AgentTabParams {
tab_name: "Tab1",
pane_name: "P1",
command: "cmd1",
cwd: Path::new("/tmp/1"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
},
AgentTabParams {
tab_name: "Tab2",
pane_name: "P2",
command: "cmd2",
cwd: Path::new("/tmp/2"),
shell: "/bin/zsh",
focus: false,
close_on_exit: true,
},
];
let layout = generate_main_layout(tabs).unwrap();
assert!(layout.contains("tab name=\"Tab1\""));
assert!(layout.contains("tab name=\"Tab2\""));
assert!(layout.contains("zjstatus.wasm"));
}
#[test]
fn test_tab_name_with_emoji() {
let params = AgentTabParams {
tab_name: "🤖 473-fix",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("tab name=\"🤖 473-fix\""));
}
#[test]
fn test_tab_name_with_special_chars() {
let params = AgentTabParams {
tab_name: "Tab-123_test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("tab name=\"Tab-123_test\""));
}
#[test]
fn test_command_with_single_quotes() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "claude --prompt 'hello world'",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("claude --prompt 'hello world'"));
}
#[test]
fn test_command_with_double_quotes() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: r#"echo "hello world""#,
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("echo"));
assert!(layout.contains("hello world"));
}
#[test]
fn test_cwd_with_spaces() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/path/with spaces/project"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("/path/with spaces/project"));
}
#[test]
fn test_focus_false() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: false,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("tab name=\"Test\" {"));
assert!(layout.contains("pane name=\"Agent\" focus=true"));
}
#[test]
fn test_empty_pane_name() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("pane name=\"\""));
}
#[test]
fn test_layout_has_zjstatus() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(
layout.contains("zjstatus.wasm"),
"Layout should include zjstatus plugin"
);
}
#[test]
fn test_layout_has_exomonad_plugin() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(
layout.contains("exomonad-plugin.wasm"),
"Layout should include exomonad plugin"
);
}
#[test]
fn test_layout_close_on_exit() {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(
layout.contains("close_on_exit true"),
"Layout should include close_on_exit true"
);
}
#[test]
fn test_main_layout_multiple_tabs() {
let tabs = vec![
AgentTabParams {
tab_name: "Tab1",
pane_name: "P1",
command: "cmd1",
cwd: Path::new("/tmp/1"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
},
AgentTabParams {
tab_name: "Tab2",
pane_name: "P2",
command: "cmd2",
cwd: Path::new("/tmp/2"),
shell: "/bin/zsh",
focus: false,
close_on_exit: true,
},
AgentTabParams {
tab_name: "Tab3",
pane_name: "P3",
command: "cmd3",
cwd: Path::new("/tmp/3"),
shell: "/bin/zsh",
focus: false,
close_on_exit: true,
},
];
let layout = generate_main_layout(tabs).unwrap();
assert!(layout.contains("tab name=\"Tab1\""));
assert!(layout.contains("tab name=\"Tab2\""));
assert!(layout.contains("tab name=\"Tab3\""));
assert!(layout.contains("zjstatus.wasm"));
}
#[test]
fn test_different_shells() {
for shell in ["/bin/bash", "/bin/zsh", "/usr/bin/fish"] {
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: "echo test",
cwd: Path::new("/tmp"),
shell,
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(
layout.contains(shell),
"Layout should contain shell: {}",
shell
);
}
}
#[test]
fn test_long_command() {
let long_command = "claude --prompt 'This is a very long prompt that contains multiple sentences and should still be handled correctly by the layout generator without any truncation or issues'";
let params = AgentTabParams {
tab_name: "Test",
pane_name: "Agent",
command: long_command,
cwd: Path::new("/tmp"),
shell: "/bin/zsh",
focus: true,
close_on_exit: true,
};
let layout = generate_agent_layout(¶ms).unwrap();
assert!(layout.contains("very long prompt"));
}
#[test]
fn test_empty_tabs_list() {
let tabs: Vec<AgentTabParams> = vec![];
let layout = generate_main_layout(tabs).unwrap();
assert!(layout.contains("zjstatus.wasm"));
}
}