|
|
|
@@ -91,7 +91,7 @@ pub enum DraftField {
|
|
|
|
|
/// Disclosure collapsing the optional view and worktree controls below.
|
|
|
|
|
/// Closed by default; Enter / Space toggles.
|
|
|
|
|
Advanced,
|
|
|
|
|
/// ACP-backed transcript instead of a terminal. Claude Code only.
|
|
|
|
|
/// ACP-backed transcript instead of a terminal. Claude Code or Codex.
|
|
|
|
|
StructuredToggle,
|
|
|
|
|
/// Worktree on/off for this session (#65). Enter / Space toggles.
|
|
|
|
|
WorktreeToggle,
|
|
|
|
@@ -256,7 +256,7 @@ pub struct DraftPane {
|
|
|
|
|
/// take the default target.
|
|
|
|
|
pub advanced_collapsed: bool,
|
|
|
|
|
/// ACP-backed structured transcript. Off by default and supported only
|
|
|
|
|
/// when the resolved command is Claude Code.
|
|
|
|
|
/// when the resolved command is Claude Code or Codex.
|
|
|
|
|
pub structured_view: bool,
|
|
|
|
|
/// Enclosing repository of the chosen target — from the project scan for
|
|
|
|
|
/// picked rows, `/api/gitroot` for raw paths. `None` until known.
|
|
|
|
@@ -673,7 +673,7 @@ impl DraftPane {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn supports_structured_view(&self) -> bool {
|
|
|
|
|
command_tool(self.command()) == Some("claude")
|
|
|
|
|
matches!(command_tool(self.command()), Some("claude" | "codex"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn clear_unsupported_structured_view(&mut self) {
|
|
|
|
@@ -1888,11 +1888,28 @@ mod tests {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn structured_view_is_default_off_and_only_visible_for_claude_code() {
|
|
|
|
|
fn structured_view_is_default_off_and_visible_for_supported_agents() {
|
|
|
|
|
let mut draft = DraftPane {
|
|
|
|
|
advanced_collapsed: false,
|
|
|
|
|
..DraftPane::default()
|
|
|
|
|
};
|
|
|
|
|
draft.set_presets(vec![
|
|
|
|
|
SpawnPreset {
|
|
|
|
|
id: "claude".into(),
|
|
|
|
|
label: "Claude".into(),
|
|
|
|
|
command: "claude".into(),
|
|
|
|
|
},
|
|
|
|
|
SpawnPreset {
|
|
|
|
|
id: "codex".into(),
|
|
|
|
|
label: "Codex".into(),
|
|
|
|
|
command: "codex".into(),
|
|
|
|
|
},
|
|
|
|
|
SpawnPreset {
|
|
|
|
|
id: "opencode".into(),
|
|
|
|
|
label: "OpenCode".into(),
|
|
|
|
|
command: "opencode".into(),
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
assert!(!draft.structured_view);
|
|
|
|
|
assert!(draft.field_order().contains(&DraftField::StructuredToggle));
|
|
|
|
|
let rendered = render_to_string(100, 24, |f| draft.render(f, f.area(), true));
|
|
|
|
@@ -1901,6 +1918,12 @@ mod tests {
|
|
|
|
|
draft.toggle_structured_view();
|
|
|
|
|
assert!(draft.structured_view);
|
|
|
|
|
draft.cycle_preset(1);
|
|
|
|
|
assert_eq!(draft.command(), "codex");
|
|
|
|
|
assert!(
|
|
|
|
|
draft.structured_view,
|
|
|
|
|
"Codex keeps the structured selection"
|
|
|
|
|
);
|
|
|
|
|
draft.cycle_preset(1);
|
|
|
|
|
assert_eq!(draft.command(), "opencode");
|
|
|
|
|
assert!(
|
|
|
|
|
!draft.structured_view,
|
|
|
|
@@ -1928,9 +1951,9 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
/// A custom command typed into the free-text field, run through the same
|
|
|
|
|
/// resolver the daemon/web gate uses (`command_tool`, #199): a path, an
|
|
|
|
|
/// env prefix, or a wrapper still resolves to Claude, so the toggle stays
|
|
|
|
|
/// offered and the request carries `Structured`; an unrelated program
|
|
|
|
|
/// does not.
|
|
|
|
|
/// env prefix, or a wrapper still resolves to Claude Code or Codex, so
|
|
|
|
|
/// the toggle stays offered and the request carries `Structured`; an
|
|
|
|
|
/// unrelated program does not.
|
|
|
|
|
#[test]
|
|
|
|
|
fn structured_view_follows_command_resolution_for_custom_commands() {
|
|
|
|
|
let cases: &[(&str, bool)] = &[
|
|
|
|
@@ -1952,6 +1975,16 @@ mod tests {
|
|
|
|
|
("yarn --silent dlx claude-code", true),
|
|
|
|
|
("bun x claude-code", true),
|
|
|
|
|
("\"/opt/my agent/claude\" --resume", true),
|
|
|
|
|
("codex", true),
|
|
|
|
|
("codex --full-auto", true),
|
|
|
|
|
("CODEX=1 codex", true),
|
|
|
|
|
("/usr/local/bin/codex exec", true),
|
|
|
|
|
("npx codex", true),
|
|
|
|
|
("sudo -u root codex", true),
|
|
|
|
|
("env -i codex", true),
|
|
|
|
|
("command codex", true),
|
|
|
|
|
("pnpm --silent dlx codex", true),
|
|
|
|
|
("\"/opt/my agent/codex\" exec", true),
|
|
|
|
|
("bash", false),
|
|
|
|
|
("opencode", false),
|
|
|
|
|
("/usr/local/bin/bash -l", false),
|
|
|
|
@@ -1999,7 +2032,7 @@ mod tests {
|
|
|
|
|
|
|
|
|
|
/// A configured preset (the spawn-form "alias" for a command) is gated
|
|
|
|
|
/// the same way as free text: a preset whose command resolves to Claude
|
|
|
|
|
/// through a wrapper still offers structured view.
|
|
|
|
|
/// Code or Codex through a wrapper still offers structured view.
|
|
|
|
|
#[test]
|
|
|
|
|
fn structured_view_follows_command_resolution_for_presets() {
|
|
|
|
|
let mut draft = DraftPane {
|
|
|
|
@@ -2014,6 +2047,11 @@ mod tests {
|
|
|
|
|
label: "Claude (npx)".into(),
|
|
|
|
|
command: "npx claude-code --resume".into(),
|
|
|
|
|
},
|
|
|
|
|
SpawnPreset {
|
|
|
|
|
id: "codex-wrapped".into(),
|
|
|
|
|
label: "Codex (npx)".into(),
|
|
|
|
|
command: "npx codex exec".into(),
|
|
|
|
|
},
|
|
|
|
|
SpawnPreset {
|
|
|
|
|
id: "aider".into(),
|
|
|
|
|
label: "Aider".into(),
|
|
|
|
@@ -2027,7 +2065,13 @@ mod tests {
|
|
|
|
|
assert!(draft.structured_view);
|
|
|
|
|
assert_eq!(draft.build_request().unwrap().view, SessionView::Structured);
|
|
|
|
|
|
|
|
|
|
// Switching to a preset that does not resolve to Claude clears the
|
|
|
|
|
// Switching between supported presets preserves the selection.
|
|
|
|
|
draft.cycle_preset(1);
|
|
|
|
|
assert_eq!(draft.command(), "npx codex exec");
|
|
|
|
|
assert!(draft.structured_view);
|
|
|
|
|
assert_eq!(draft.build_request().unwrap().view, SessionView::Structured);
|
|
|
|
|
|
|
|
|
|
// Switching to a preset that does not resolve to an ACP agent clears the
|
|
|
|
|
// stale selection instead of letting an invalid request through.
|
|
|
|
|
draft.cycle_preset(1);
|
|
|
|
|
assert_eq!(draft.command(), "aider");
|
|
|
|
@@ -2036,11 +2080,11 @@ mod tests {
|
|
|
|
|
assert_eq!(draft.build_request().unwrap().view, SessionView::Terminal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Editing the custom-command text away from Claude clears an armed
|
|
|
|
|
/// Editing the custom-command text away from a supported agent clears an armed
|
|
|
|
|
/// selection immediately, matching the preset-switch case above — an
|
|
|
|
|
/// invalid `Structured` request can never reach `build_request`.
|
|
|
|
|
#[test]
|
|
|
|
|
fn structured_view_clears_when_custom_command_edited_away_from_claude() {
|
|
|
|
|
fn structured_view_clears_when_custom_command_edited_away_from_supported_agent() {
|
|
|
|
|
let mut draft = DraftPane {
|
|
|
|
|
picked_cwd: Some(PathBuf::from("/srv/work")),
|
|
|
|
|
worktree_enabled: false,
|
|
|
|
@@ -2049,22 +2093,22 @@ mod tests {
|
|
|
|
|
..DraftPane::default()
|
|
|
|
|
};
|
|
|
|
|
draft.selected_preset = draft.presets.len() - 1;
|
|
|
|
|
draft.custom_command = crate::text_input::TextInput::with_value("claude");
|
|
|
|
|
draft.custom_command = crate::text_input::TextInput::with_value("codex");
|
|
|
|
|
draft.toggle_structured_view();
|
|
|
|
|
assert!(draft.structured_view);
|
|
|
|
|
|
|
|
|
|
typed(&mut draft, " -p");
|
|
|
|
|
assert_eq!(draft.command(), "claude -p");
|
|
|
|
|
assert!(draft.structured_view, "still Claude, selection survives");
|
|
|
|
|
assert_eq!(draft.command(), "codex -p");
|
|
|
|
|
assert!(draft.structured_view, "still Codex, selection survives");
|
|
|
|
|
|
|
|
|
|
for _ in 0.."claude -p".chars().count() {
|
|
|
|
|
for _ in 0.."codex -p".chars().count() {
|
|
|
|
|
draft.on_input(&InputKey::Backspace);
|
|
|
|
|
}
|
|
|
|
|
typed(&mut draft, "bash");
|
|
|
|
|
assert_eq!(draft.command(), "bash");
|
|
|
|
|
assert!(
|
|
|
|
|
!draft.structured_view,
|
|
|
|
|
"editing the program away from claude clears the stale selection"
|
|
|
|
|
"editing the program away from Codex clears the stale selection"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(draft.build_request().unwrap().view, SessionView::Terminal);
|
|
|
|
|
}
|
|
|
|
|