parley-cli 0.2.0

Terminal-first review tool for AI-generated code changes
Documentation
use super::*;

impl TuiApp {
    pub(super) fn open_command_palette(&mut self) {
        self.dismiss_ai_progress_popup();
        self.command_palette = Some(CommandPaletteState {
            query: String::new(),
            cursor_col: 0,
            selected_index: 0,
            scroll: 0,
        });
        self.status_line = "command palette opened".into();
    }

    pub(in crate::tui::app) fn command_palette_items() -> Vec<CommandPaletteItem> {
        vec![
            CommandPaletteItem {
                action: CommandPaletteAction::RefreshReviewAndDiff,
                label: "Refresh Review + Diff",
                keywords: "reload sync",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleFullscreen,
                label: "Toggle Content Fullscreen",
                keywords: "layout zoom",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleSplitDiff,
                label: "Toggle Split View",
                keywords: "layout pane",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleSideBySideDiff,
                label: "Toggle Side-by-Side Diff",
                keywords: "layout unified",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleThreadNavigator,
                label: "Toggle Thread Navigator",
                keywords: "thread sidebar",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::JumpNextThread,
                label: "Jump to Next Thread",
                keywords: "thread next",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::JumpPrevThread,
                label: "Jump to Previous Thread",
                keywords: "thread prev previous",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CycleThreadDensityMode,
                label: "Cycle Thread Density Mode",
                keywords: "thread compact expanded",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleSelectedThreadExpansion,
                label: "Toggle Selected Thread Expanded",
                keywords: "thread expand collapse selected",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::SetReviewOpen,
                label: "Set Review State: Open",
                keywords: "state workflow",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::SetReviewUnderReview,
                label: "Set Review State: Under Review",
                keywords: "state workflow",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CycleFileFilter,
                label: "Cycle File Filter",
                keywords: "files open pending",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CycleFileSort,
                label: "Cycle File Sort",
                keywords: "files order ranking",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleActiveFileGroup,
                label: "Toggle Active File Group",
                keywords: "files group collapse expand",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CollapseAllFileGroups,
                label: "Collapse All File Groups",
                keywords: "files group collapse all",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ShowFileHeatmap,
                label: "Show Git File Heatmap",
                keywords: "git history hotspots churn touched files heatmap",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleRootDocumentRendering,
                label: "Toggle Root JSON/Markdown Rendering",
                keywords: "root json markdown pretty prettify render rendering view raw source",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenUserNameEditor,
                label: "Edit User Name",
                keywords: "settings profile",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenThemePicker,
                label: "Open Theme Picker",
                keywords: "theme colors",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenCommitPicker,
                label: "Open Commit Picker",
                keywords: "git commit sha revision ref",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenReviewPicker,
                label: "Open Review Picker",
                keywords: "review context session comments threads",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenThreadSelector,
                label: "Open Thread Selector",
                keywords: "thread comments jump selector navigate",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenCodeSearch,
                label: "Search Codebase",
                keywords: "search code rg grep find files text",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CreateReview,
                label: "Create Review",
                keywords: "review new create context session",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleLightDarkTheme,
                label: "Toggle Theme Light/Dark Variant",
                keywords: "theme light dark",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CycleAiProvider,
                label: "Cycle AI Provider",
                keywords: "ai model provider",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ToggleAiTransport,
                label: "Toggle AI Transport",
                keywords: "ai transport acp cli provider",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::RunAiReviewRefactor,
                label: "Run AI Refactor on Review",
                keywords: "ai run review refactor",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::RunAiThreadRefactor,
                label: "Run AI Refactor on Selected Thread",
                keywords: "ai run thread refactor",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::RunAiThreadReply,
                label: "Run AI Reply on Selected Thread",
                keywords: "ai run thread reply",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::CancelAiRun,
                label: "Cancel AI Run",
                keywords: "ai stop cancel",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::ShowAiActivity,
                label: "Show AI Activity",
                keywords: "ai logs activity sessions providers",
            },
            CommandPaletteItem {
                action: CommandPaletteAction::OpenShortcuts,
                label: "Open Help Docs",
                keywords: "help docs keys shortcuts keybindings",
            },
        ]
    }

    pub(in crate::tui::app) fn command_palette_filtered_items(
        query: &str,
        items: &[CommandPaletteItem],
    ) -> Vec<CommandPaletteItem> {
        let trimmed = query.trim();
        if trimmed.is_empty() {
            return items.to_vec();
        }
        let needle = trimmed.to_lowercase();
        items
            .iter()
            .filter(|item| {
                item.label.to_lowercase().contains(&needle)
                    || item.keywords.to_lowercase().contains(&needle)
            })
            .cloned()
            .collect()
    }

    pub(super) async fn handle_command_palette_key(
        &mut self,
        key: KeyEvent,
        service: &ReviewService,
    ) -> Result<()> {
        if matches!(key.code, KeyCode::Esc) {
            self.command_palette = None;
            self.status_line = "command palette closed".into();
            return Ok(());
        }

        let all_items = Self::command_palette_items();
        let filtered_items = if let Some(palette) = self.command_palette.as_ref() {
            Self::command_palette_filtered_items(&palette.query, &all_items)
        } else {
            Vec::new()
        };

        if matches!(key.code, KeyCode::Enter) {
            let selected_action = self
                .command_palette
                .as_ref()
                .and_then(|palette| filtered_items.get(palette.selected_index))
                .map(|item| item.action);
            if let Some(action) = selected_action {
                self.command_palette = None;
                if let Err(error) = self.apply_command_palette_action(action, service).await {
                    self.status_line = format!("command failed: {error}");
                }
            }
            return Ok(());
        }

        let Some(palette) = self.command_palette.as_mut() else {
            return Ok(());
        };
        match key.code {
            KeyCode::Up => {
                palette.selected_index = palette.selected_index.saturating_sub(1);
            }
            KeyCode::Down => {
                let max_index = filtered_items.len().saturating_sub(1);
                palette.selected_index = (palette.selected_index + 1).min(max_index);
            }
            KeyCode::Home => {
                palette.selected_index = 0;
            }
            KeyCode::End => {
                palette.selected_index = filtered_items.len().saturating_sub(1);
            }
            KeyCode::PageUp => {
                palette.selected_index = palette.selected_index.saturating_sub(8);
            }
            KeyCode::PageDown => {
                let max_index = filtered_items.len().saturating_sub(1);
                palette.selected_index = (palette.selected_index + 8).min(max_index);
            }
            KeyCode::Left => {
                palette.cursor_col = palette.cursor_col.saturating_sub(1);
            }
            KeyCode::Right => {
                palette.cursor_col = (palette.cursor_col + 1).min(palette.query.chars().count());
            }
            KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                palette.cursor_col = 0;
            }
            KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                palette.cursor_col = palette.query.chars().count();
            }
            KeyCode::Backspace if palette.cursor_col > 0 => {
                remove_char_at(&mut palette.query, palette.cursor_col - 1);
                palette.cursor_col -= 1;
                palette.selected_index = 0;
                palette.scroll = 0;
            }
            KeyCode::Delete if palette.cursor_col < palette.query.chars().count() => {
                remove_char_at(&mut palette.query, palette.cursor_col);
                palette.selected_index = 0;
                palette.scroll = 0;
            }
            KeyCode::Char(ch)
                if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
            {
                insert_char_at(&mut palette.query, palette.cursor_col, ch);
                palette.cursor_col += 1;
                palette.selected_index = 0;
                palette.scroll = 0;
            }
            _ => {}
        }

        let refreshed_items = Self::command_palette_filtered_items(&palette.query, &all_items);
        if refreshed_items.is_empty() {
            palette.selected_index = 0;
            palette.scroll = 0;
        } else {
            palette.selected_index = palette
                .selected_index
                .min(refreshed_items.len().saturating_sub(1));
            if palette.selected_index < palette.scroll {
                palette.scroll = palette.selected_index;
            }
            let lower_bound = palette.scroll.saturating_add(8);
            if palette.selected_index > lower_bound {
                palette.scroll = palette.selected_index.saturating_sub(8);
            }
        }
        Ok(())
    }
}