* fix: unify startup logo and loading theme behavior Show desktop window immediately with animated splash logo Align splash/logo colors with selected app theme and default themes Keep auth loading state on full-screen logo without size jump Make project SVG icons follow active app theme Use the active theme foreground color for project icons discovered from favicons Apply server-side SVG color overrides for currentColor icons via icon request params Keep non-SVG project icons unchanged while preserving existing fallback behavior * perf: speed up desktop startup and unify loading logo visuals Desktop startup now shows UI sooner while backend boot continues in background Startup host probing uses a faster local path with safer remote fallback retries OpenChamber logo cube highlights now match splash screens consistently * fix: keep macOS traffic-light buttons in the correct position on load Stop native window title updates on macOS during app initialization Prevent title bar relayout that reset custom traffic-light positioning * fix: recover missing providers and agents after fast startup Retries provider/agent loading when connection is up but core config is still empty Prevents cold-start state where models/agents appear only after manual project switch Keeps startup responsive with throttled background recovery in app bootstrap
136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
import React from 'react';
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
import { isDesktopShell, isTauriShell } from '@/lib/desktop';
|
|
import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
|
|
|
|
const APP_TITLE = 'OpenChamber';
|
|
|
|
const formatProjectLabel = (label: string): string => {
|
|
return label.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
};
|
|
|
|
const getProjectNameFromPath = (path: string): string => {
|
|
const normalized = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
const segments = normalized.split('/').filter(Boolean);
|
|
return segments[segments.length - 1] ?? '';
|
|
};
|
|
|
|
const buildWindowTitle = (projectLabel: string | null, instanceLabel: string | null): string => {
|
|
const parts = [projectLabel, instanceLabel, APP_TITLE].filter((part): part is string => typeof part === 'string' && part.trim().length > 0);
|
|
return parts.join(' | ');
|
|
};
|
|
|
|
export const useWindowTitle = () => {
|
|
const activeProject = useProjectsStore((state) => {
|
|
if (!state.activeProjectId) {
|
|
return null;
|
|
}
|
|
return state.projects.find((project) => project.id === state.activeProjectId) ?? null;
|
|
});
|
|
|
|
const projectLabel = React.useMemo(() => {
|
|
if (!activeProject) {
|
|
return null;
|
|
}
|
|
|
|
const label = activeProject.label?.trim();
|
|
if (label) {
|
|
return formatProjectLabel(label);
|
|
}
|
|
|
|
const pathName = getProjectNameFromPath(activeProject.path);
|
|
if (pathName) {
|
|
return formatProjectLabel(pathName);
|
|
}
|
|
|
|
return null;
|
|
}, [activeProject]);
|
|
|
|
const [instanceLabel, setInstanceLabel] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof window === 'undefined' || !isDesktopShell()) {
|
|
setInstanceLabel(null);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
const refreshInstanceLabel = async () => {
|
|
try {
|
|
const currentHref = window.location.href;
|
|
const localOrigin = window.__OPENCHAMBER_LOCAL_ORIGIN__ || window.location.origin;
|
|
|
|
if (locationMatchesHost(currentHref, localOrigin)) {
|
|
if (!cancelled) {
|
|
setInstanceLabel(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const cfg = await desktopHostsGet();
|
|
const match = cfg.hosts.find((host) => locationMatchesHost(currentHref, host.url));
|
|
const nextLabel = match?.label?.trim() ? redactSensitiveUrl(match.label.trim()) : 'Instance';
|
|
if (!cancelled) {
|
|
setInstanceLabel(nextLabel);
|
|
}
|
|
} catch {
|
|
if (!cancelled) {
|
|
setInstanceLabel('Instance');
|
|
}
|
|
}
|
|
};
|
|
|
|
void refreshInstanceLabel();
|
|
|
|
const handleFocus = () => {
|
|
void refreshInstanceLabel();
|
|
};
|
|
|
|
window.addEventListener('focus', handleFocus);
|
|
return () => {
|
|
cancelled = true;
|
|
window.removeEventListener('focus', handleFocus);
|
|
};
|
|
}, []);
|
|
|
|
const title = React.useMemo(() => buildWindowTitle(projectLabel, instanceLabel), [projectLabel, instanceLabel]);
|
|
|
|
React.useEffect(() => {
|
|
if (typeof document !== 'undefined') {
|
|
document.title = title;
|
|
}
|
|
|
|
if (!isTauriShell()) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
const applyTitle = async () => {
|
|
try {
|
|
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
const isMac = typeof navigator !== 'undefined' && /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
|
if (isMac) {
|
|
return;
|
|
}
|
|
|
|
const currentWindow = getCurrentWindow();
|
|
await currentWindow.setTitle(title);
|
|
} catch {
|
|
return;
|
|
}
|
|
};
|
|
|
|
void applyTitle();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [title]);
|
|
};
|