Repository navigation
Expand file tree
/
Copy pathApp.tsx
More file actions
277 lines (259 loc) · 11.9 KB
/
Copy pathApp.tsx
File metadata and controls
277 lines (259 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
import * as Sentry from '@sentry/react';
import { useEffect, useRef } from 'react';
import { Provider } from 'react-redux';
import {
HashRouter as Router,
useLocation,
useNavigate,
useNavigationType,
} from 'react-router-dom';
import { PersistGate } from 'redux-persist/integration/react';
import AppRoutes from './AppRoutes';
import { AnalyticsPageTracker } from './components/analytics';
import AnnouncementGate from './components/Announcement/AnnouncementGate';
import AppBackground from './components/AppBackground';
import AppUpdatePrompt from './components/AppUpdatePrompt';
import BootCheckGate from './components/BootCheckGate/BootCheckGate';
import CommandProvider from './components/commands/CommandProvider';
import ServiceBlockingGate from './components/daemon/ServiceBlockingGate';
import DictationHotkeyManager from './components/DictationHotkeyManager';
import ErrorFallbackScreen from './components/ErrorFallbackScreen';
import HarnessInitOverlay from './components/InitProgressScreen/HarnessInitOverlay';
import KeyringConsentOverlay from './components/keyring/KeyringConsentOverlay';
import AppSidebar from './components/layout/shell/AppSidebar';
import RootShellLayout from './components/layout/shell/RootShellLayout';
import { SidebarSlotProvider } from './components/layout/shell/SidebarSlot';
import LocalAIDownloadSnackbar from './components/LocalAIDownloadSnackbar';
import SecretPromptDialog from './components/mcp-setup/SecretPromptDialog';
import NoticeCenter from './components/notices/NoticeCenter';
import OpenhumanLinkModal from './components/OpenhumanLinkModal';
import PersistRehydrationScreen from './components/PersistRehydrationScreen';
import PttHotkeyManager from './components/PttHotkeyManager';
import SecurityBanner from './components/SecurityBanner';
import AppWalkthrough from './components/walkthrough/AppWalkthrough';
import { useNotchBootSync } from './hooks/useNotchBootSync';
import { I18nProvider } from './lib/i18n/I18nContext';
import {
startNativeNotificationsService,
stopNativeNotificationsService,
} from './lib/nativeNotifications';
import { getIsMobile } from './lib/platform';
import ChatRuntimeProvider from './providers/ChatRuntimeProvider';
import CoreStateProvider, { useCoreState } from './providers/CoreStateProvider';
import SocketProvider from './providers/SocketProvider';
import ThemeProvider from './providers/ThemeProvider';
import { startCoreHealthMonitor, stopCoreHealthMonitor } from './services/coreHealthMonitor';
import {
startInternetStatusListener,
stopInternetStatusListener,
} from './services/internetStatusListener';
import { persistor, store } from './store';
import { DEV_FORCE_ONBOARDING } from './utils/config';
startNativeNotificationsService();
// Connectivity status (#1527): wire navigator.onLine + start core sidecar
// health poll. Both idempotent via internal `started` guards.
startInternetStatusListener();
startCoreHealthMonitor();
export function stopBootServicesForHmr(): void {
stopNativeNotificationsService();
stopInternetStatusListener();
stopCoreHealthMonitor();
}
if (import.meta.hot) {
import.meta.hot.dispose(stopBootServicesForHmr);
}
function App() {
const >
// On mobile (iOS or Android) the SocketProvider would try to connect to the
// local core HTTP socket, which does not exist on device (the core runs on
// the remote desktop). Gate it out to prevent spurious connection errors —
// chat events arrive through TunnelTransport's socket.io relay instead.
// NOTE: useHumanMascot's subscribeChatEvents() still returns a no-op unsub
// when the socket is absent — mascot state falls back to 'idle'.
const socketWrapped = (children: React.ReactNode) =>
onMobile ? <>{children}</> : <SocketProvider>{children}</SocketProvider>;
/*
* @generated-source:provider-chain
* Authoritative top-level provider / gate nesting for the desktop shell,
* outermost first. Keep this list in sync with the JSX returned below;
* `scripts/generate-architecture-docs.mjs` renders it into
* `gitbooks/developing/architecture/frontend.md` and CI (`pnpm docs:check`)
* fails if the doc drifts. Refresh the doc with `pnpm docs:generate`.
* Format per line: `<order>. <Component> — <role>` (role must not contain " — ").
* 1. Sentry.ErrorBoundary — Crash boundary; renders ErrorFallbackScreen
* 2. Provider — Redux store; enables useAppSelector / dispatch app-wide
* 3. PersistGate — Holds UI until persisted Redux slices rehydrate
* 4. ThemeProvider — Theme tokens and dark-mode handling
* 5. I18nProvider — Localization context consumed via useT
* 6. BootCheckGate — Blocks render until the core boot snapshot resolves
* 7. CoreStateProvider — Core app snapshot: auth, session, onboarding state
* 8. SocketProvider — Core socket.io events; desktop only (mobile uses the TunnelTransport relay)
* 9. ChatRuntimeProvider — Chat runtime events, tool timeline, and approvals
* 10. Router — HashRouter navigation for all routes
* 11. CommandProvider — Command palette context
* 12. ServiceBlockingGate — Blocks the shell until required services are configured
* @end-source:provider-chain
*/
return (
<Sentry.ErrorBoundary
fallback={({ error, componentStack, resetError, eventId }) => (
<ErrorFallbackScreen
error={error}
componentStack={componentStack}
eventId={eventId}
>
/>
)}>
<Provider store={store}>
<PersistGate loading={<PersistRehydrationScreen />} persistor={persistor}>
<ThemeProvider>
<I18nProvider>
<BootCheckGate>
<CoreStateProvider>
{socketWrapped(
<ChatRuntimeProvider>
<Router>
<CommandProvider>
<ServiceBlockingGate>
<AnalyticsPageTracker />
<AppShell />
<SecurityBanner />
{!onMobile && <DictationHotkeyManager />}
{!onMobile && <PttHotkeyManager />}
{!onMobile && <LocalAIDownloadSnackbar />}
{!onMobile && <AppUpdatePrompt />}
<KeyringConsentOverlay />
<HarnessInitOverlay />
<AnnouncementGate />
<SecretPromptDialog />
</ServiceBlockingGate>
</CommandProvider>
</Router>
</ChatRuntimeProvider>
)}
</CoreStateProvider>
</BootCheckGate>
</I18nProvider>
</ThemeProvider>
</PersistGate>
</Provider>
</Sentry.ErrorBoundary>
);
}
/** Minimal mobile shell — renders routes only, no desktop chrome. */
function AppShellMobile() {
return (
<div className="relative h-screen flex flex-col overflow-hidden bg-[#0f1117]">
<AppRoutes />
</div>
);
}
/**
* Top-level shell router — chooses mobile or desktop shell at render time.
* Must NOT call hooks before the branch because each sub-component has its
* own hook calls that obey the rules-of-hooks within their own scope.
*/
function AppShell() {
const >
if (onMobile) {
return <AppShellMobile />;
}
return <AppShellDesktop />;
}
/** Desktop inner shell — lives inside the Router so it can use useLocation. */
export function AppShellDesktop() {
const location = useLocation();
const navigate = useNavigate();
const { snapshot, isBootstrapping } = useCoreState();
const >
const >
!!snapshot.sessionToken && (DEV_FORCE_ONBOARDING || !snapshot.onboardingCompleted);
// Onboarding gate: while `onboarding_completed=false`, force any non-
// onboarding route back to `/onboarding`. Once completed, bounce the
// user off `/onboarding` so they don't get stuck on the stepper.
useEffect(() => {
if (isBootstrapping || !snapshot.sessionToken) return;
if (onboardingPending && !onOnboardingRoute) {
console.debug(
`[onboarding-gate] redirecting ${location.pathname} -> /onboarding (onboarding incomplete)`
);
navigate('/onboarding', { replace: true });
} else if (!onboardingPending && onOnboardingRoute) {
console.debug(
`[onboarding-gate] redirecting ${location.pathname} -> /chat (onboarding complete)`
);
navigate('/chat', { replace: true });
}
}, [
isBootstrapping,
snapshot.sessionToken,
onboardingPending,
onOnboardingRoute,
location.pathname,
navigate,
]);
// Sync the notch indicator to the persisted always-on listening state once
// the core is ready (once per boot). Extracted to a hook so it's testable.
useNotchBootSync(isBootstrapping);
const scrollRef = useRef<HTMLDivElement>(null);
const navType = useNavigationType();
useEffect(() => {
if (navType !== 'POP') {
scrollRef.current?.scrollTo(0, 0);
}
}, [location.pathname, navType]);
// Routes that own the full viewport with no app chrome: the public
// welcome/login screens, the onboarding stepper, and any pre-auth state.
// Everything else renders inside the root two-pane shell (sidebar + main).
const token = snapshot.sessionToken;
const '/login'].some(
path => location.pathname === path || location.pathname.startsWith(`${path}/`)
);
// The workflow graph canvas (`/flows/:id`, `/flows/draft`) owns the full
// viewport for a focused builder — no app sidebar. The `/flows` list (and its
// in-page Runs / Discoveries sub-views on `?view=`) keep their chrome.
const >
const chromeless = !token || onOnboardingRoute || onHiddenChromePath || onWorkflowCanvas;
const content = (
<div ref={scrollRef} className="relative h-full overflow-y-auto">
{/* The plan-usage upsell and the #5324 memory-embedding warning used to
be full-width banners here, pushing every route down. Both are
notices in `NoticeCenter` now — see its docs for why. */}
<AppRoutes />
</div>
);
return (
<SidebarSlotProvider>
<div className="relative h-screen flex flex-col overflow-hidden">
<AppBackground />
<div className="relative z-10 flex-1 min-h-0 flex flex-col overflow-hidden">
{chromeless ? (
content
) : (
// Nothing sets `unframed` today. It existed for live CEF provider
// webviews — WebviewHost handed the Rust side a plain rectangle and
// CEF composited that child view above the whole HTML layer, so a
// rounded card under it showed four square corners punching through
// the radius. That surface was removed upstream along with
// WebviewHost, so no route needs the escape hatch right now; the
// prop stays on the primitive for the next full-bleed surface.
<RootShellLayout sidebar={<AppSidebar />}>{content}</RootShellLayout>
)}
</div>
<OpenhumanLinkModal />
{/* Every notice the app raises, in one bottom-left FAB: classified
runtime errors (#3931), the memory-embedding budget (#5324), plan
usage limits. Mounted outside the routes so entries survive route
changes and background-job completion. */}
<NoticeCenter />
{/* Post-onboarding Joyride walkthrough — mounted here (outside routes) so
it persists across tab navigations. Joyride targets span Home + the
sidebar nav so it must stay mounted while the user moves between routes. */}
{!isBootstrapping && !onOnboardingRoute && (
<AppWalkthrough />
)}
</div>
</SidebarSlotProvider>
);
}
export default App;
You can’t perform that action at this time.