Auto-derived project labels were title-cased, turning .ssh into .Ssh and opencode-claude into Opencode Claude. Show the folder name verbatim in the sidebar, window title, settings selector and notification templates, and migrate persisted legacy labels back to the folder name (manual renames are preserved).
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
import React from 'react';
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
|
import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop';
|
|
import { desktopHostsGet, getDesktopHostApiUrl, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
|
|
import { setDesktopWindowTitle } from '@/lib/desktopNative';
|
|
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
|
|
|
const APP_TITLE = 'OpenChamber';
|
|
|
|
const formatProjectLabel = (label: string): string => label.trim();
|
|
|
|
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 {
|
|
if (isDesktopLocalOriginActive()) {
|
|
if (!cancelled) {
|
|
setInstanceLabel(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const localOrigin = window.__OPENCHAMBER_LOCAL_ORIGIN__ || window.location.origin;
|
|
const runtimeApiBaseUrl = getRuntimeApiBaseUrl();
|
|
|
|
if (runtimeApiBaseUrl && locationMatchesHost(runtimeApiBaseUrl, localOrigin)) {
|
|
if (!cancelled) {
|
|
setInstanceLabel(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const cfg = await desktopHostsGet();
|
|
const match = cfg.hosts.find((host) => runtimeApiBaseUrl ? locationMatchesHost(runtimeApiBaseUrl, getDesktopHostApiUrl(host)) : false);
|
|
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 (!isDesktopShell()) {
|
|
return;
|
|
}
|
|
|
|
const applyTitle = async () => {
|
|
try {
|
|
const isMac = typeof navigator !== 'undefined' && /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
|
if (isMac) {
|
|
return;
|
|
}
|
|
|
|
await setDesktopWindowTitle(title);
|
|
} catch {
|
|
return;
|
|
}
|
|
};
|
|
|
|
void applyTitle();
|
|
}, [title]);
|
|
};
|