Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 66 additions & 29 deletions src/components/command-palette/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -98,6 +99,9 @@ export function CommandPalette({
{ id: string; title: string; preview: string; modified: number }[]
>([]);
const [settings, setSettings] = useState<Settings | null>(null);
const [availableAiProviders, setAvailableAiProviders] = useState<
AiProvider[]
>([]);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);

Expand All @@ -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<Command[]>(() => {
const baseCommands: Command[] = [
Expand All @@ -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: <CodexIcon className="w-4.5 h-4.5 fill-text-muted" />,
action,
};
}

if (provider === "ollama") {
return {
id: "ai-edit-ollama",
label: "Edit with Ollama",
icon: <OllamaIcon className="w-4.5 h-4.5 fill-text-muted" />,
action,
};
}

return {
id: "ai-edit-claude",
label: "Edit with Claude Code",
icon: <ClaudeIcon className="w-4.5 h-4.5 fill-text-muted" />,
action,
};
})
: [];

baseCommands.push(
{
Expand All @@ -147,33 +210,7 @@ export function CommandPalette({
}
},
},
{
id: "ai-edit",
label: "Edit with Claude Code",
icon: <ClaudeIcon className="w-4.5 h-4.5 fill-text-muted" />,
action: () => {
onOpenAiModal?.("claude");
onClose();
},
},
{
id: "ai-edit-codex",
label: "Edit with OpenAI Codex",
icon: <CodexIcon className="w-4.5 h-4.5 fill-text-muted" />,
action: () => {
onOpenAiModal?.("codex");
onClose();
},
},
{
id: "ai-edit-ollama",
label: "Edit with Ollama",
icon: <OllamaIcon className="w-4.5 h-4.5 fill-text-muted" />,
action: () => {
onOpenAiModal?.("ollama");
onClose();
},
},
...aiCommands,
{
id: "duplicate-note",
label: "Duplicate Current Note",
Expand Down Expand Up @@ -436,8 +473,8 @@ export function CommandPalette({
onClose,
onOpenSettings,
onOpenAiModal,
availableAiProviders,
setTheme,
theme,
gitAvailable,
status,
commit,
Expand Down
26 changes: 26 additions & 0 deletions src/services/ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { invoke } from "@tauri-apps/api/core";

export type AiProvider = "claude" | "codex" | "ollama";
export const AI_PROVIDER_ORDER: ReadonlyArray<AiProvider> = [
"claude",
"codex",
"ollama",
];

export interface AiExecutionResult {
success: boolean;
Expand Down Expand Up @@ -34,6 +39,27 @@ export async function checkOllamaCli(): Promise<boolean> {
return invoke("ai_check_ollama_cli");
}

const providerCheckers: Record<AiProvider, () => Promise<boolean>> = {
claude: checkClaudeCli,
codex: checkCodexCli,
ollama: checkOllamaCli,
};

export async function getAvailableAiProviders(): Promise<AiProvider[]> {
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,
Expand Down