Compare commits

..
2 changed files with 63 additions and 17 deletions
+3 -1
View File
@@ -3053,17 +3053,19 @@ mod tests {
let request = |view| CreateSessionRequest {
view,
cwd: std::path::PathBuf::from("/srv/work"),
command: "claude".into(),
command: "codex".into(),
env: Default::default(),
metadata: Default::default(),
worktree: None,
};
let structured_id = Uuid::from_u128(188);
app.state.mode = Mode::Insert;
app.submit_spawn(request(SessionView::Structured)).await;
assert!(app.catalog.snapshot().sessions.is_empty());
assert!(app.structured.contains_key(&structured_id));
assert!(app.emulators.get(structured_id).is_none());
assert_eq!(app.state.mode, Mode::Normal);
let terminal_id = Uuid::from_u128(187);
app.submit_spawn(request(SessionView::Terminal)).await;
+60 -16
View File
@@ -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);
}