import React from 'react'; import { RiExternalLinkLine, RiGitBranchLine, RiPushpin2Fill, RiPushpin2Line } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { ChatContainer } from '@/components/chat/ChatContainer'; import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { invokeDesktop, isElectronShell } from '@/lib/desktop'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionWorktreeStore } from '@/sync/session-worktree-store'; import { useSessions } from '@/sync/sync-context'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useGitBranchLabel, useGitStore } from '@/stores/useGitStore'; import { resolveSessionDiffStats } from '@/components/session/sidebar/utils'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; type MiniChatMode = 'session' | 'draft'; type MiniChatLayoutProps = { mode: MiniChatMode; autoOpenDraft?: boolean; unavailable?: boolean; }; const compactPath = (value: string | null | undefined): string => { const path = typeof value === 'string' ? value.trim() : ''; if (!path) return ''; const home = typeof window !== 'undefined' ? window.__OPENCHAMBER_HOME__ : ''; if (home && path === home) return '~'; if (home && path.startsWith(`${home}/`)) return `~/${path.slice(home.length + 1)}`; const segments = path.split('/').filter(Boolean); if (segments.length <= 3) return path; return `.../${segments.slice(-3).join('/')}`; }; const normalizePath = (value: string | null | undefined): string => { const raw = typeof value === 'string' ? value.trim() : ''; if (!raw) return ''; const normalized = raw.replace(/\\/g, '/'); return normalized === '/' ? '/' : normalized.replace(/\/+$/, ''); }; const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => { const { t } = useI18n(); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const draftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const draftProjectId = useSessionUIStore((state) => state.newSessionDraft?.selectedProjectId ?? null); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const projects = useProjectsStore((state) => state.projects); const activeProject = useProjectsStore((state) => state.getActiveProject()); const sessions = useSessions(); const runtimeApis = useRuntimeAPIs(); const ensureGitStatus = useGitStore((state) => state.ensureStatus); const worktreePath = useSessionUIStore((state) => currentSessionId ? state.worktreeMetadata.get(currentSessionId)?.path ?? '' : ''); const worktreeMetadataBranch = useSessionUIStore((state) => currentSessionId ? state.worktreeMetadata.get(currentSessionId)?.branch?.trim() ?? null : null); const worktreeAttachment = useSessionWorktreeStore((state) => currentSessionId ? state.getAttachment(currentSessionId) : undefined); const draftDirectory = useSessionUIStore((state) => { if (!state.newSessionDraft?.open) return ''; return normalizePath(state.newSessionDraft.bootstrapPendingDirectory ?? state.newSessionDraft.directoryOverride ?? ''); }); const [pinned, setPinned] = React.useState(false); const macosMajor = typeof window !== 'undefined' ? window.__OPENCHAMBER_MACOS_MAJOR__ ?? 0 : 0; const hasMacTrafficLights = Number.isFinite(macosMajor) && macosMajor > 0; const macosHeaderSizeClass = hasMacTrafficLights ? macosMajor >= 26 ? 'h-12' : macosMajor <= 15 ? 'h-14' : '' : ''; const session = React.useMemo( () => currentSessionId ? sessions.find((entry) => entry.id === currentSessionId) ?? null : null, [currentSessionId, sessions], ); const sessionWorktreeMetadata = (session as { worktreeMetadata?: { path?: string | null; branch?: string | null; projectDirectory?: string | null } } | null)?.worktreeMetadata ?? null; React.useEffect(() => { if (!isElectronShell()) return; void invokeDesktop<{ pinned?: boolean }>('desktop_get_window_pinned').then((result) => { if (typeof result?.pinned === 'boolean') setPinned(result.pinned); }); }, []); const title = session?.title?.trim() || (draftOpen || mode === 'draft' ? t('miniChat.header.newSession') : t('miniChat.header.session')); const sessionDirectory = normalizePath((session as { directory?: string | null } | null)?.directory ?? null); const worktreeDirectory = normalizePath(worktreePath || sessionWorktreeMetadata?.path || worktreeAttachment?.cwd || worktreeAttachment?.worktreeRoot || ''); const currentDirectoryNormalized = normalizePath(currentDirectory); const openDirectory = worktreeDirectory || sessionDirectory || draftDirectory || currentDirectoryNormalized; const directoryLabel = compactPath(openDirectory); const catalogWorktreeBranch = useSessionUIStore((state) => { const candidateDirectory = normalizePath(worktreeDirectory || sessionDirectory || ''); if (!candidateDirectory) return null; for (const worktrees of state.availableWorktreesByProject.values()) { const match = worktrees.find((worktree) => normalizePath(worktree.path) === candidateDirectory); const branch = match?.branch?.trim(); if (branch) return branch; } return null; }); React.useEffect(() => { if (!openDirectory) return; void ensureGitStatus(openDirectory, runtimeApis.git).catch(() => {}); }, [ensureGitStatus, openDirectory, runtimeApis.git]); const pathMatchedProject = React.useMemo(() => { const projectDirectory = normalizePath(sessionWorktreeMetadata?.projectDirectory ?? worktreeAttachment?.worktreeRoot ?? null); const candidateDirectory = projectDirectory || openDirectory; if (!candidateDirectory) return null; return projects .map((entry) => ({ ...entry, normalizedPath: normalizePath(entry.path) })) .filter((entry) => entry.normalizedPath && (entry.normalizedPath === candidateDirectory || candidateDirectory.startsWith(`${entry.normalizedPath}/`))) .sort((left, right) => right.path.length - left.path.length)[0] ?? null; }, [openDirectory, projects, sessionWorktreeMetadata?.projectDirectory, worktreeAttachment?.worktreeRoot]); const projectLabel = React.useMemo(() => { const project = pathMatchedProject ?? activeProject; if (!project) return directoryLabel || 'OpenChamber'; const label = project.label?.trim(); if (label) return label; const segments = project.path.split(/[\\/]/).filter(Boolean); return segments.at(-1) ?? project.path; }, [activeProject, directoryLabel, pathMatchedProject]); const gitBranchForDirectory = useGitBranchLabel(openDirectory || null); const branchLabel = gitBranchForDirectory || worktreeMetadataBranch || sessionWorktreeMetadata?.branch?.trim() || worktreeAttachment?.branch?.trim() || catalogWorktreeBranch; const diffStats = React.useMemo(() => { return resolveSessionDiffStats(session?.summary as Parameters[0]); }, [session?.summary]); const changes = diffStats ?? { additions: 0, deletions: 0 }; const hasChanges = changes.additions > 0 || changes.deletions > 0; const dragRegionStyle = { WebkitAppRegion: 'drag' } as React.CSSProperties; const noDragRegionStyle = { WebkitAppRegion: 'no-drag' } as React.CSSProperties; const handleTogglePinned = React.useCallback(() => { const nextPinned = !pinned; setPinned(nextPinned); void invokeDesktop('desktop_set_window_pinned', { pinned: nextPinned }).catch(() => { setPinned(!nextPinned); }); }, [pinned]); const handleOpenMainApp = React.useCallback(() => { const payload = currentSessionId ? { sessionId: currentSessionId, directory: (session as { directory?: string | null } | null)?.directory ?? currentDirectory ?? '' } : { mode: 'draft', directory: openDirectory || currentDirectory || '', projectId: draftProjectId }; void invokeDesktop<{ focused?: boolean }>('desktop_focus_main_window', payload) .then((result) => { if (result?.focused === true) { return invokeDesktop('desktop_close_current_window'); } return null; }); }, [currentDirectory, currentSessionId, draftProjectId, openDirectory, session]); return (
{title}
{projectLabel} {branchLabel ? ( {branchLabel} ) : null} {hasChanges ? ( +{changes.additions} / -{changes.deletions} ) : null}
); }; export const MiniChatLayout: React.FC = ({ mode, autoOpenDraft = false, unavailable = false }) => { const { t } = useI18n(); return (
{unavailable ? (
{t('miniChat.unavailable.title')}
{t('miniChat.unavailable.description')}
) : ( )}
); };