Tidy Tauri code and enable "Open file in Stirling PDF" (#4836)

# Description of Changes
Tidy Tauri code and enable "Open file in Stirling PDF"
This commit is contained in:
James Brunton
2025-11-10 12:15:39 +00:00
committed by GitHub
parent f4543d26cd
commit ebf4bab80b
15 changed files with 241 additions and 1102 deletions

View File

@@ -8,7 +8,7 @@ import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext";
import { HotkeyProvider } from "@app/contexts/HotkeyContext";
import { SidebarProvider } from "@app/contexts/SidebarContext";
import { PreferencesProvider } from "@app/contexts/PreferencesContext";
import { AppConfigProvider } from "@app/contexts/AppConfigContext";
import { AppConfigProvider, AppConfigRetryOptions } from "@app/contexts/AppConfigContext";
import { RightRailProvider } from "@app/contexts/RightRailContext";
import { ViewerProvider } from "@app/contexts/ViewerContext";
import { SignatureProvider } from "@app/contexts/SignatureContext";
@@ -16,6 +16,7 @@ import { OnboardingProvider } from "@app/contexts/OnboardingContext";
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
import { useScarfTracking } from "@app/hooks/useScarfTracking";
import { useAppInitialization } from "@app/hooks/useAppInitialization";
// Component to initialize scarf tracking (must be inside AppConfigProvider)
function ScarfTrackingInitializer() {
@@ -23,19 +24,31 @@ function ScarfTrackingInitializer() {
return null;
}
// Component to run app-level initialization (must be inside AppProviders for context access)
function AppInitializer() {
useAppInitialization();
return null;
}
export interface AppProvidersProps {
children: ReactNode;
appConfigRetryOptions?: AppConfigRetryOptions;
}
/**
* Core application providers
* Contains all providers needed for the core
*/
export function AppProviders({ children }: { children: ReactNode }) {
export function AppProviders({ children, appConfigRetryOptions }: AppProvidersProps) {
return (
<PreferencesProvider>
<RainbowThemeProvider>
<ErrorBoundary>
<OnboardingProvider>
<AppConfigProvider>
<AppConfigProvider retryOptions={appConfigRetryOptions}>
<ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<AppInitializer />
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>

View File

@@ -1,6 +1,18 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import apiClient from '@app/services/apiClient';
/**
* Sleep utility for delays
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export interface AppConfigRetryOptions {
maxRetries?: number;
initialDelay?: number;
}
export interface AppConfig {
baseUrl?: string;
contextPath?: string;
@@ -47,12 +59,18 @@ const AppConfigContext = createContext<AppConfigContextValue | undefined>({
* Provider component that fetches and provides app configuration
* Should be placed at the top level of the app, before any components that need config
*/
export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
export const AppConfigProvider: React.FC<{
children: ReactNode;
retryOptions?: AppConfigRetryOptions;
}> = ({ children, retryOptions }) => {
const [config, setConfig] = useState<AppConfig | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [fetchCount, setFetchCount] = useState(0);
const maxRetries = retryOptions?.maxRetries ?? 0;
const initialDelay = retryOptions?.initialDelay ?? 1000;
const fetchConfig = async (force = false) => {
// Prevent duplicate fetches unless forced
if (!force && fetchCount > 0) {
@@ -60,35 +78,59 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children
return;
}
try {
setLoading(true);
setError(null);
setLoading(true);
setError(null);
// apiClient automatically adds JWT header if available via interceptors
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config');
const data = response.data;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
const delay = initialDelay * Math.pow(2, attempt - 1);
console.log(`[AppConfig] Retry attempt ${attempt}/${maxRetries} after ${delay}ms delay...`);
await sleep(delay);
} else {
console.log('[AppConfig] Fetching app config...');
}
console.debug('[AppConfig] Config fetched successfully:', data);
setConfig(data);
setFetchCount(prev => prev + 1);
} catch (err: any) {
// On 401 (not authenticated), use default config with login enabled
// This allows the app to work even without authentication
if (err.response?.status === 401) {
console.debug('[AppConfig] 401 error - using default config (login enabled)');
setConfig({ enableLogin: true });
// apiClient automatically adds JWT header if available via interceptors
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config');
const data = response.data;
console.debug('[AppConfig] Config fetched successfully:', data);
setConfig(data);
setFetchCount(prev => prev + 1);
setLoading(false);
return;
}
return; // Success - exit function
} catch (err: any) {
const status = err?.response?.status;
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
setError(errorMessage);
console.error('[AppConfig] Failed to fetch app config:', err);
// On error, assume login is enabled (safe default)
setConfig({ enableLogin: true });
} finally {
setLoading(false);
// On 401 (not authenticated), use default config with login enabled
// This allows the app to work even without authentication
if (status === 401) {
console.debug('[AppConfig] 401 error - using default config (login enabled)');
setConfig({ enableLogin: true });
setLoading(false);
return;
}
// Check if we should retry (network errors or 5xx errors)
const shouldRetry = (!status || status >= 500) && attempt < maxRetries;
if (shouldRetry) {
console.warn(`[AppConfig] Attempt ${attempt + 1} failed (status ${status || 'network error'}):`, err.message, '- will retry...');
continue;
}
// Final attempt failed or non-retryable error (4xx)
const errorMessage = err?.response?.data?.message || err?.message || 'Unknown error occurred';
setError(errorMessage);
console.error(`[AppConfig] Failed to fetch app config after ${attempt + 1} attempts:`, err);
// On error, assume login is enabled (safe default)
setConfig({ enableLogin: true });
break;
}
}
setLoading(false);
};
useEffect(() => {

View File

@@ -0,0 +1,10 @@
/**
* App initialization hook
* Core version: no initialization needed
*
* This hook is called once when the app starts to allow different builds
* to perform initialization tasks that require access to contexts like FileContext.
*/
export function useAppInitialization(): void {
// Core version has no initialization
}

View File

@@ -20,21 +20,13 @@ import { useFilesModalContext } from "@app/contexts/FilesModalContext";
import AppConfigModal from "@app/components/shared/AppConfigModal";
import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt";
import AdminAnalyticsChoiceModal from "@app/components/shared/AdminAnalyticsChoiceModal";
import { useHomePageExtensions } from "@app/pages/useHomePageExtensions";
import "@app/pages/HomePage.css";
type MobileView = "tools" | "workbench";
interface HomePageProps {
openedFile?: File | null;
}
export default function HomePage({ openedFile }: HomePageProps = {}) {
export default function HomePage() {
const { t } = useTranslation();
// Extension hook for desktop-specific behavior (e.g., file opening)
useHomePageExtensions(openedFile);
const {
sidebarRefs,
} = useSidebarContext();

View File

@@ -1,10 +0,0 @@
import { useEffect } from 'react';
/**
* Extension point for HomePage behaviour.
* Core version does nothing.
*/
export function useHomePageExtensions(_openedFile?: File | null) {
useEffect(() => {
}, [_openedFile]);
}