Balance pill was rendering nothing for admin/root accounts because mcp-server's §4 returns HEICODE_USER_NOT_FOUND for users that never came through from-agnet onboarding. Fall back to Heicode NewAPI's own /api/user/self when that happens; reshape into the same envelope so the UI is path-agnostic. New balanceStore (zustand) — single polling loop, BalanceBar + avatar ring share it. AppShell starts it once auth bootstraps. BalanceBar now shows a "loading…" placeholder on first fetch so the widget is visible from frame one. SidebarUserCard avatar wears an SVG ring whose arc length tracks remaining/(remaining+used) and color hits the same green→amber→red thresholds as the bar. Manager: /desktop-client drops the manifest-notes wall of text, the old HEICODE_DESKTOP_FILE_* subtitle goes away, and a Mac fallback entry is always spliced in when the live manifest is Win-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Sidebar } from './Sidebar'
|
|
import { ContentRouter } from './ContentRouter'
|
|
import { ToastContainer } from '../shared/Toast'
|
|
import { ApprovalDialog } from '../approval/ApprovalDialog'
|
|
import { UpdateChecker } from '../shared/UpdateChecker'
|
|
import { HeicodeLoginPage } from '../login/HeicodeLoginPage'
|
|
import { useSettingsStore } from '../../stores/settingsStore'
|
|
import { useUIStore, type SettingsTab } from '../../stores/uiStore'
|
|
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
|
|
import { useBalanceStore } from '../../stores/balanceStore'
|
|
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
import { initializeDesktopServerUrl } from '../../lib/desktopRuntime'
|
|
import { TabBar } from './TabBar'
|
|
import { StartupErrorView } from './StartupErrorView'
|
|
import { useTabStore, SETTINGS_TAB_ID } from '../../stores/tabStore'
|
|
import { useChatStore } from '../../stores/chatStore'
|
|
import { useTranslation } from '../../i18n'
|
|
|
|
export function AppShell() {
|
|
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
|
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
|
|
const fetchAuth = useHeicodeAuthStore((s) => s.fetch)
|
|
const authStatus = useHeicodeAuthStore((s) => s.status)
|
|
const authHasFetched = useHeicodeAuthStore((s) => s.hasFetched)
|
|
const [ready, setReady] = useState(false)
|
|
const [startupError, setStartupError] = useState<string | null>(null)
|
|
const t = useTranslation()
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const bootstrap = async () => {
|
|
try {
|
|
await initializeDesktopServerUrl()
|
|
await fetchSettings()
|
|
// Pull HeiCode login state up-front so the shell can decide whether
|
|
// to render the login page or the workspace.
|
|
await fetchAuth()
|
|
|
|
// Restore tabs from localStorage
|
|
await useTabStore.getState().restoreTabs()
|
|
const { activeTabId: activeId, tabs } = useTabStore.getState()
|
|
const activeTab = tabs.find((tab) => tab.sessionId === activeId)
|
|
if (activeId && activeTab?.type === 'session') {
|
|
useChatStore.getState().connectToSession(activeId)
|
|
}
|
|
if (!cancelled) {
|
|
setReady(true)
|
|
}
|
|
} catch (error) {
|
|
if (!cancelled) {
|
|
setStartupError(error instanceof Error ? error.message : String(error))
|
|
setReady(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
void bootstrap()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [fetchSettings, fetchAuth])
|
|
|
|
// Start the shared balance poller once the auth bootstrap is done so
|
|
// BalanceBar + the avatar ring read from the same in-memory source.
|
|
// Idempotent on the store side; stops when AppShell unmounts (rare).
|
|
useEffect(() => {
|
|
if (!authHasFetched) return
|
|
if (!authStatus?.loggedIn) return
|
|
const store = useBalanceStore.getState()
|
|
store.start()
|
|
return () => store.stop()
|
|
}, [authHasFetched, authStatus?.loggedIn])
|
|
|
|
// Listen for macOS native menu navigation events (About / Settings)
|
|
useEffect(() => {
|
|
let unlisten: (() => void) | undefined
|
|
import(/* @vite-ignore */ '@tauri-apps/api/event')
|
|
.then(({ listen }) =>
|
|
listen<string>('native-menu-navigate', (event) => {
|
|
const target = event.payload as SettingsTab | 'settings'
|
|
if (target === 'about') {
|
|
useUIStore.getState().setPendingSettingsTab('about')
|
|
}
|
|
useTabStore.getState().openTab(SETTINGS_TAB_ID, 'Settings', 'settings')
|
|
}),
|
|
)
|
|
.then((fn) => { unlisten = fn })
|
|
.catch(() => {})
|
|
return () => { unlisten?.() }
|
|
}, [])
|
|
|
|
useKeyboardShortcuts()
|
|
|
|
if (startupError) {
|
|
return <StartupErrorView error={startupError} />
|
|
}
|
|
|
|
if (!ready) {
|
|
return (
|
|
<div className="h-screen flex items-center justify-center bg-[var(--color-surface)] text-[var(--color-text-secondary)]">
|
|
{t('app.launching')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Gate the workspace behind a successful HeiCode login. We treat any of the
|
|
// existing auth sources (heicode-auth provider, original ~/.claude/settings.json,
|
|
// or process env) as logged-in to avoid forcing existing users to re-login.
|
|
if (authHasFetched && !authStatus?.loggedIn) {
|
|
return (
|
|
<>
|
|
<HeicodeLoginPage />
|
|
<ToastContainer />
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen flex overflow-hidden bg-[var(--color-surface)]">
|
|
<div
|
|
data-testid="sidebar-shell"
|
|
data-state={sidebarOpen ? 'open' : 'closed'}
|
|
className="sidebar-shell"
|
|
>
|
|
<Sidebar />
|
|
</div>
|
|
<main
|
|
id="content-area"
|
|
data-sidebar-state={sidebarOpen ? 'open' : 'closed'}
|
|
className="min-w-0 flex-1 flex flex-col overflow-hidden"
|
|
>
|
|
<TabBar />
|
|
<ContentRouter />
|
|
</main>
|
|
<ToastContainer />
|
|
<UpdateChecker />
|
|
<ApprovalDialog />
|
|
</div>
|
|
)
|
|
}
|