diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4db4156..6878d78 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,6 +22,7 @@ "Bash(npx c8 report:*)", "Bash(rg:*)", "Bash(npx tsc:*)", + "Bash(git add:*)" "mcp__playwright__browser_take_screenshot" ] }, diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index f5c5be2..41cba42 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -1,8 +1,63 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use tauri::{Manager, State}; +use std::sync::Mutex; +use std::path::PathBuf; + +// State to hold the current project path +struct ProjectState { + path: Mutex>, +} + +// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +#[tauri::command] +fn get_commands_path(state: State) -> Result { + let project_path = state.path.lock().unwrap(); + + match &*project_path { + Some(path) => { + // Return the commands.yaml path within the project directory + let commands_path = PathBuf::from(path).join("commands.yaml"); + Ok(commands_path.to_string_lossy().to_string()) + }, + None => { + // No project path set, return error + Err("No project path set".to_string()) + } + } +} + +#[tauri::command] +fn set_project_path(state: State, path: String) -> Result<(), String> { + let mut project_path = state.path.lock().unwrap(); + *project_path = Some(path); + Ok(()) +} + +#[tauri::command] +fn get_project_path(state: State) -> Result, String> { + let project_path = state.path.lock().unwrap(); + Ok(project_path.clone()) +} + fn main() { tauri::Builder::default() + .setup(|app| { + #[cfg(debug_assertions)] // only in development + { + app.get_window("main").unwrap().open_devtools(); + } + Ok(()) + }) + .manage(ProjectState { + path: Mutex::new(None), + }) + .invoke_handler(tauri::generate_handler![ + get_commands_path, + set_project_path, + get_project_path + ]) // Register all commands .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 846138a..c443c55 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -26,12 +26,17 @@ import { I18nextProvider } from 'react-i18next'; import i18n from './i18n/config'; function App() { - const { loadCommandsYaml, loadSkits, projectPath } = useSkitStore(); + const { loadCommandsYaml, loadSkits, projectPath, setProjectPath } = useSkitStore(); useKeyboardShortcuts(); useEffect(() => { const loadInitialData = async () => { try { + // Sync project path with Tauri on startup if it exists + if (projectPath && window.__TAURI__) { + await setProjectPath(projectPath); + } + const commandsYamlContent = await loadCommandsYamlFile(projectPath); loadCommandsYaml(commandsYamlContent); @@ -60,7 +65,7 @@ function App() { }; loadInitialData(); - }, [loadCommandsYaml, loadSkits, projectPath]); + }, [loadCommandsYaml, loadSkits, projectPath, setProjectPath]); return ( diff --git a/frontend/src/i18n/translationLoader.test.ts b/frontend/src/i18n/translationLoader.test.ts index 46f0ca9..0381cc4 100644 --- a/frontend/src/i18n/translationLoader.test.ts +++ b/frontend/src/i18n/translationLoader.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { loadTranslations, generateCommandTranslationKeys, getAvailableLanguages, getTranslationWithFallback } from './translationLoader'; import i18n from 'i18next'; -import * as tauriApi from '@tauri-apps/api'; import * as tauriFs from '@tauri-apps/api/fs'; import * as tauriPath from '@tauri-apps/api/path'; @@ -36,6 +35,15 @@ vi.mock('@tauri-apps/api/path', () => ({ dirname: vi.fn(), })); +// Mock the store +vi.mock('../store/skitStore', () => ({ + useSkitStore: { + getState: vi.fn(() => ({ + projectPath: null, + })), + }, +})); + // Mock fetch for development mode global.fetch = vi.fn(); @@ -97,8 +105,12 @@ describe('translationLoader', () => { (window as any).__TAURI__ = true; (import.meta as any).env = { MODE: 'production' }; - vi.mocked(tauriApi.invoke).mockResolvedValue('/path/to/commands.yaml'); - vi.mocked(tauriPath.dirname).mockResolvedValue('/path/to'); + // Mock store to return a project path + const { useSkitStore } = await import('../store/skitStore'); + vi.mocked(useSkitStore.getState).mockReturnValue({ + projectPath: '/path/to/project', + } as any); + vi.mocked(tauriPath.join).mockImplementation(async (...args) => args.join('/')); vi.mocked(tauriFs.exists).mockResolvedValue(true); vi.mocked(tauriFs.readTextFile).mockResolvedValue(JSON.stringify({ @@ -109,15 +121,19 @@ describe('translationLoader', () => { await loadTranslations(); - expect(tauriApi.invoke).toHaveBeenCalledWith('get_commands_path'); - expect(tauriFs.exists).toHaveBeenCalledWith('/path/to/i18n'); + expect(tauriFs.exists).toHaveBeenCalledWith('/path/to/project/i18n'); expect(tauriFs.readTextFile).toHaveBeenCalled(); expect(i18n.addResourceBundle).toHaveBeenCalled(); }); - it('should fallback to development mode when commands path not found', async () => { + it('should fallback to development mode when project path not found', async () => { (window as any).__TAURI__ = true; - vi.mocked(tauriApi.invoke).mockRejectedValue(new Error('Not found')); + + // Mock store to return no project path + const { useSkitStore } = await import('../store/skitStore'); + vi.mocked(useSkitStore.getState).mockReturnValue({ + projectPath: null, + } as any); const mockFetch = vi.mocked(global.fetch); mockFetch.mockResolvedValue({ @@ -128,7 +144,7 @@ describe('translationLoader', () => { await loadTranslations(); - expect(consoleWarnSpy).toHaveBeenCalledWith('Commands path not found, using development translations'); + expect(consoleWarnSpy).toHaveBeenCalledWith('No project path set, using development translations'); consoleWarnSpy.mockRestore(); }); @@ -137,8 +153,12 @@ describe('translationLoader', () => { (window as any).__TAURI__ = true; (import.meta as any).env = { MODE: 'production' }; - vi.mocked(tauriApi.invoke).mockResolvedValue('/path/to/commands.yaml'); - vi.mocked(tauriPath.dirname).mockResolvedValue('/path/to'); + // Mock store to return a project path + const { useSkitStore } = await import('../store/skitStore'); + vi.mocked(useSkitStore.getState).mockReturnValue({ + projectPath: '/path/to/project', + } as any); + vi.mocked(tauriPath.join).mockImplementation(async (...args) => args.join('/')); vi.mocked(tauriFs.exists).mockResolvedValue(false); @@ -155,8 +175,12 @@ describe('translationLoader', () => { (window as any).__TAURI__ = true; (import.meta as any).env = { MODE: 'production' }; - vi.mocked(tauriApi.invoke).mockResolvedValue('/path/to/commands.yaml'); - vi.mocked(tauriPath.dirname).mockResolvedValue('/path/to'); + // Mock store to return a project path + const { useSkitStore } = await import('../store/skitStore'); + vi.mocked(useSkitStore.getState).mockReturnValue({ + projectPath: '/path/to/project', + } as any); + vi.mocked(tauriPath.join).mockImplementation(async (...args) => args.join('/')); vi.mocked(tauriFs.exists).mockResolvedValue(true); vi.mocked(tauriFs.readTextFile).mockResolvedValue('invalid json'); @@ -174,8 +198,12 @@ describe('translationLoader', () => { (window as any).__TAURI__ = true; (import.meta as any).env = { MODE: 'production' }; - vi.mocked(tauriApi.invoke).mockResolvedValue('/path/to/commands.yaml'); - vi.mocked(tauriPath.dirname).mockResolvedValue('/path/to'); + // Mock store to return a project path + const { useSkitStore } = await import('../store/skitStore'); + vi.mocked(useSkitStore.getState).mockReturnValue({ + projectPath: '/path/to/project', + } as any); + vi.mocked(tauriPath.join).mockImplementation(async (...args) => args.join('/')); // Only i18n directory exists, but no language files @@ -384,9 +412,9 @@ describe('translationLoader', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - // Make invoke throw an error - vi.mocked(tauriApi.invoke).mockImplementation(() => { - throw new Error('Tauri error'); + // Make the store import throw an error + vi.doMock('../store/skitStore', () => { + throw new Error('Store import error'); }); const mockFetch = vi.mocked(global.fetch); @@ -397,7 +425,7 @@ describe('translationLoader', () => { await loadTranslations(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to load translations:', expect.any(Error)); + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to load translations in Tauri mode:', expect.any(Error)); // Should fallback to development translations expect(mockFetch).toHaveBeenCalled(); diff --git a/frontend/src/i18n/translationLoader.ts b/frontend/src/i18n/translationLoader.ts index 399f5e4..bc3ada5 100644 --- a/frontend/src/i18n/translationLoader.ts +++ b/frontend/src/i18n/translationLoader.ts @@ -1,7 +1,6 @@ import i18n from 'i18next'; -import { invoke } from '@tauri-apps/api'; import { exists, readTextFile } from '@tauri-apps/api/fs'; -import { join, dirname } from '@tauri-apps/api/path'; +import { join } from '@tauri-apps/api/path'; interface TranslationFile { locale: string; @@ -46,24 +45,28 @@ const loadDevelopmentTranslations = async (): Promise => { // Load translations from i18n folder export async function loadTranslations(): Promise { // Check if we're in Tauri environment - if (!window.__TAURI__ || import.meta.env.MODE === 'development') { + if (!window.__TAURI__) { await loadDevelopmentTranslations(); return; } try { - // Get the commands.yaml path from store or use default - const commandsPath = await invoke('get_commands_path').catch(() => null); - if (!commandsPath) { - console.warn('Commands path not found, using development translations'); + // Get the project path from the store + const { useSkitStore } = await import('../store/skitStore'); + const projectPath = useSkitStore.getState().projectPath; + + if (!projectPath) { + console.warn('No project path set, using development translations'); await loadDevelopmentTranslations(); return; } - const i18nPath = await join(await dirname(commandsPath), 'i18n'); + const i18nPath = await join(projectPath, 'i18n'); // Check if i18n directory exists - if (!(await exists(i18nPath))) { + const i18nDirExists = await exists(i18nPath); + + if (!i18nDirExists) { console.warn('i18n directory not found, using development translations'); await loadDevelopmentTranslations(); return; @@ -94,7 +97,7 @@ export async function loadTranslations(): Promise { } } } catch (error) { - console.error('Failed to load translations:', error); + console.error('Failed to load translations in Tauri mode:', error); await loadDevelopmentTranslations(); } } diff --git a/frontend/src/store/skitStore.ts b/frontend/src/store/skitStore.ts index a82b78a..2af38ec 100644 --- a/frontend/src/store/skitStore.ts +++ b/frontend/src/store/skitStore.ts @@ -39,7 +39,7 @@ interface SkitState { redo: () => void; setValidationErrors: (errors: string[]) => void; loadCommandsYaml: (yaml: string) => void; - setProjectPath: (path: string | null) => void; // Add project path setter + setProjectPath: (path: string | null) => Promise; // Add project path setter createGroup: () => void; ungroupCommands: (groupStartId: number) => void; toggleGroupCollapse: (groupStartId: number) => void; @@ -594,10 +594,20 @@ export const useSkitStore = create()( }); }, - setProjectPath: (path) => { + setProjectPath: async (path) => { set((state) => { state.projectPath = path; }); + + // Sync with Tauri backend if available + if (window.__TAURI__ && path) { + try { + const { invoke } = await import('@tauri-apps/api'); + await invoke('set_project_path', { path }); + } catch (error) { + console.error('Failed to sync project path with Tauri:', error); + } + } }, createGroup: () => { @@ -829,10 +839,8 @@ export const useSkitStore = create()( const selectedPath = await selectProjectFolder(); if (selectedPath) { - // Update project path - set((state) => { - state.projectPath = selectedPath; - }); + // Update project path and sync with Tauri + await get().setProjectPath(selectedPath); // Load commands.yaml from the new path try { diff --git a/frontend/src/types/window.d.ts b/frontend/src/types/window.d.ts new file mode 100644 index 0000000..8208a0b --- /dev/null +++ b/frontend/src/types/window.d.ts @@ -0,0 +1,5 @@ +interface Window { + __TAURI__?: { + [key: string]: unknown; + }; +} \ No newline at end of file diff --git a/frontend/src/utils/commandFormatting.ts b/frontend/src/utils/commandFormatting.ts index 83a45e4..044f52d 100644 --- a/frontend/src/utils/commandFormatting.ts +++ b/frontend/src/utils/commandFormatting.ts @@ -22,7 +22,7 @@ export function formatCommandPreview(command: SkitCommand, commandsMap: Map