diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx index e348cf2..d004a01 100644 --- a/src/components/command-palette/CommandPalette.tsx +++ b/src/components/command-palette/CommandPalette.tsx @@ -13,6 +13,7 @@ import { useNotes } from "../../context/NotesContext"; import { useTheme } from "../../context/ThemeContext"; import { useGit } from "../../context/GitContext"; import * as notesService from "../../services/notes"; +import * as aiService from "../../services/ai"; import { downloadPdf, downloadMarkdown } from "../../services/pdf"; import type { Settings } from "../../types/note"; import type { Editor } from "@tiptap/react"; @@ -88,7 +89,7 @@ export function CommandPalette({ unpinNote, notesFolder, } = useNotes(); - const { theme, setTheme } = useTheme(); + const { setTheme } = useTheme(); const { status, gitAvailable, commit, sync, isSyncing } = useGit(); const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); @@ -98,6 +99,9 @@ export function CommandPalette({ { id: string; title: string; preview: string; modified: number }[] >([]); const [settings, setSettings] = useState(null); + const [availableAiProviders, setAvailableAiProviders] = useState< + AiProvider[] + >([]); const inputRef = useRef(null); const listRef = useRef(null); @@ -108,6 +112,32 @@ export function CommandPalette({ } }, [open, currentNote?.id]); + useEffect(() => { + if (!open || !currentNote) { + setAvailableAiProviders([]); + return; + } + + let active = true; + aiService + .getAvailableAiProviders() + .then((providers) => { + if (active) { + setAvailableAiProviders(providers); + } + }) + .catch((error) => { + if (active) { + console.error("Failed to discover AI providers:", error); + setAvailableAiProviders([]); + } + }); + + return () => { + active = false; + }; + }, [open, currentNote?.id]); + // Memoize commands array const commands = useMemo(() => { const baseCommands: Command[] = [ @@ -127,6 +157,39 @@ export function CommandPalette({ if (currentNote) { const isPinned = settings?.pinnedNoteIds?.includes(currentNote.id) || false; + const aiCommands: Command[] = onOpenAiModal + ? availableAiProviders.map((provider) => { + const action = () => { + onOpenAiModal(provider); + onClose(); + }; + + if (provider === "codex") { + return { + id: "ai-edit-codex", + label: "Edit with OpenAI Codex", + icon: , + action, + }; + } + + if (provider === "ollama") { + return { + id: "ai-edit-ollama", + label: "Edit with Ollama", + icon: , + action, + }; + } + + return { + id: "ai-edit-claude", + label: "Edit with Claude Code", + icon: , + action, + }; + }) + : []; baseCommands.push( { @@ -147,33 +210,7 @@ export function CommandPalette({ } }, }, - { - id: "ai-edit", - label: "Edit with Claude Code", - icon: , - action: () => { - onOpenAiModal?.("claude"); - onClose(); - }, - }, - { - id: "ai-edit-codex", - label: "Edit with OpenAI Codex", - icon: , - action: () => { - onOpenAiModal?.("codex"); - onClose(); - }, - }, - { - id: "ai-edit-ollama", - label: "Edit with Ollama", - icon: , - action: () => { - onOpenAiModal?.("ollama"); - onClose(); - }, - }, + ...aiCommands, { id: "duplicate-note", label: "Duplicate Current Note", @@ -436,8 +473,8 @@ export function CommandPalette({ onClose, onOpenSettings, onOpenAiModal, + availableAiProviders, setTheme, - theme, gitAvailable, status, commit, diff --git a/src/services/ai.ts b/src/services/ai.ts index d2d878e..f597026 100644 --- a/src/services/ai.ts +++ b/src/services/ai.ts @@ -1,6 +1,11 @@ import { invoke } from "@tauri-apps/api/core"; export type AiProvider = "claude" | "codex" | "ollama"; +export const AI_PROVIDER_ORDER: ReadonlyArray = [ + "claude", + "codex", + "ollama", +]; export interface AiExecutionResult { success: boolean; @@ -34,6 +39,27 @@ export async function checkOllamaCli(): Promise { return invoke("ai_check_ollama_cli"); } +const providerCheckers: Record Promise> = { + claude: checkClaudeCli, + codex: checkCodexCli, + ollama: checkOllamaCli, +}; + +export async function getAvailableAiProviders(): Promise { + const checks = await Promise.all( + AI_PROVIDER_ORDER.map(async (provider) => { + try { + const installed = await providerCheckers[provider](); + return installed ? provider : null; + } catch { + return null; + } + }), + ); + + return checks.filter((provider): provider is AiProvider => provider !== null); +} + export async function executeOllamaEdit( filePath: string, prompt: string,