import * as React from 'react'
import { invoke as tauriInvoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { AppSidebar, type SidebarView } from '@/components/app-sidebar'
import {
CommandPalette,
useCommandPaletteShortcut,
} from '@/components/command-palette'
import { DraftDialog } from '@/components/draft-dialog'
import { OnboardingWizard } from '@/components/onboarding-wizard'
import { WorkbenchPage } from '@/pages/workbench'
import { SessionsPage } from '@/pages/sessions'
import { IndexNavPage } from '@/pages/index-nav'
import { LintPage } from '@/pages/lint'
import { SettingsPage } from '@/pages/settings'
import { isTauriHost } from '@/lib/api/desktop'
import { useConfig } from '@/state/config'
import { useI18n } from '@/state/i18n'
import { BrainCircuit, Settings } from 'lucide-react'
type Screen = SidebarView | 'settings'
const VIEW_STORAGE_KEY = 'spool.view'
function readStoredView(): SidebarView {
if (typeof window === 'undefined') return 'inbox'
const raw = window.sessionStorage.getItem(VIEW_STORAGE_KEY)
if (raw === 'inbox' || raw === 'sessions' || raw === 'index' || raw === 'lint') {
return raw
}
return 'inbox'
}
export function AppShell() {
const cfg = useConfig()
const { t } = useI18n()
const [view, setView] = React.useState<Screen>(() => readStoredView())
const [navigateToId, setNavigateToId] = React.useState<string | null>(null)
const [paletteOpen, setPaletteOpen] = React.useState(false)
const [draftMode, setDraftMode] = React.useState<'manual' | 'propose' | null>(null)
React.useEffect(() => {
if (typeof window === 'undefined') return
if (view === 'settings') return
window.sessionStorage.setItem(VIEW_STORAGE_KEY, view)
}, [view])
const handleSidebarChange = React.useCallback((next: SidebarView) => {
setView(next)
}, [])
const handleHeaderMouseDown = React.useCallback(
(e: React.MouseEvent<HTMLElement>) => {
// Only drag on left button, not on interactive elements
if (e.button !== 0) return
const target = e.target as HTMLElement
if (
target.closest('button') ||
target.closest('a') ||
target.closest('input') ||
target.closest('[role="button"]')
) return
void getCurrentWindow().startDragging()
},
[],
)
useCommandPaletteShortcut(setPaletteOpen)
const commandCtx = React.useMemo(
() => ({
onCreateManual: () => {
setView(readStoredView())
setDraftMode('manual')
},
onCreateProposal: () => {
setView(readStoredView())
setDraftMode('propose')
},
onImportSession: () => setView('sessions'),
onNavigateInbox: () => setView('inbox'),
onNavigateSessions: () => setView('sessions'),
onNavigateIndex: () => setView('index'),
onNavigateLint: () => setView('lint'),
onNavigateSettings: () => setView('settings'),
onOpenPopover: () => {
void tauriInvoke('show_popover').catch(() => undefined)
},
onOpenMain: () => {
void tauriInvoke('show_main_window').catch(() => undefined)
},
}),
[],
)
if (cfg.ready && !cfg.configPath.trim()) {
return <OnboardingWizard />
}
if (view === 'settings') {
return (
<>
<SettingsPage onBack={() => setView(readStoredView())} />
<CommandPalette
open={paletteOpen}
onOpenChange={setPaletteOpen}
ctx={commandCtx}
/>
</>
)
}
return (
<div className="flex h-screen flex-col bg-bg-deep">
{/* Header */}
<header
onMouseDown={handleHeaderMouseDown}
className="flex h-11 shrink-0 items-center justify-between border-b border-border-subtle px-4 pl-[76px] select-none cursor-default"
>
<div className="flex items-center gap-2.5">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/15">
<BrainCircuit className="h-3.5 w-3.5 text-primary" />
</div>
<span className="text-sm font-semibold tracking-tight text-foreground">spool</span>
{isTauriHost() && (
<div className="flex items-center gap-1 ml-1">
<div className="h-1.5 w-1.5 rounded-full bg-success animate-pulse" />
<span className="text-[10px] text-muted-foreground/60">{t('status.connected')}</span>
</div>
)}
</div>
<button
onClick={() => setView('settings')}
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-all duration-150 hover:bg-bg-elevated hover:text-foreground cursor-pointer"
title="设置"
>
<Settings className="h-3.5 w-3.5" />
</button>
</header>
{/* Body */}
<div className="flex flex-1 overflow-hidden">
<AppSidebar view={view} onChange={handleSidebarChange} />
<main className="flex-1 overflow-auto bg-bg-base p-5">
<div className="animate-fade-in-up">
{view === 'inbox' && <WorkbenchPage navigateToId={navigateToId} />}
{view === 'sessions' && (
<SessionsPage onImported={() => setView('inbox')} />
)}
{view === 'index' && (
<IndexNavPage
onNavigateToRecord={(id) => {
setNavigateToId(id)
setView('inbox')
}}
/>
)}
{view === 'lint' && <LintPage />}
</div>
</main>
</div>
{draftMode && (
<DraftDialog
mode={draftMode}
open
onOpenChange={(open) => !open && setDraftMode(null)}
onCreated={() => setDraftMode(null)}
/>
)}
<CommandPalette
open={paletteOpen}
onOpenChange={setPaletteOpen}
ctx={commandCtx}
/>
</div>
)
}