Files
heicode-win/cc-haha/desktop/src/components/layout/AppShell.tsx
T
chenchenandClaude Opus 4.7 ac1aa87312 release: 0.1.10 — balance widget actually shows + avatar ring
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>
2026-05-14 16:44:13 +08:00

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>
)
}