/** * Wiring surfaces — each pane is its own memoized component. Every surface * reads the reactive state it renders from at the leaf (its own atom * subscriptions) and reaches the controller's callbacks through the stable * `actions` bag, so a state change scoped to one surface (or a bare * wiring-controller tick) never re-renders another. This is what keeps the * layout tree's zones independently rendered — the whole point of the shell. */ import { useStore } from '@nanostores/react' import { type ComponentProps, lazy, memo, type ReactNode, Suspense, useMemo } from 'react' import { Navigate, Route, Routes, useParams } from 'react-router' import { ContribBoundary, ContribRender } from '@/contrib/react/boundary' import { useContributions } from '@/contrib/react/use-contributions' import { $activeConnectionId } from '@/store/connections' import { $gateway } from '@/store/gateway' import { $activeGatewayProfile } from '@/store/profile' import { $freshDraftReady, $gatewayState } from '@/store/session' import { ChatView } from '../chat' import { ChatSidebar } from '../chat/sidebar' import { TerminalPaneChrome } from '../right-sidebar/terminal/chrome' import { contributedRoutes, NEW_CHAT_ROUTE, ROUTES_AREA, sessionRoute } from '../routes' import { useStatusSnapshot } from '../shell/hooks/use-status-snapshot' import { useStatusbarItems } from '../shell/hooks/use-statusbar-items' import { ModelMenuPanel } from '../shell/model-menu-panel' import { StatusbarControls } from '../shell/statusbar-controls' import { latestChatActions, latestSidebarActions } from './latest-actions' import { setStatusbarItemGroup, useStatusbarContributions } from './panes' import type { SidebarActions, WiringActions } from './types' // Same lazy-view split as DesktopController — pages load on demand. The // full-page views the workspace route table mounts live here; overlay views // (agents/settings/…) are the controller's and stay in wiring.tsx. const ArtifactsView = lazy(async () => ({ default: (await import('../artifacts')).ArtifactsView })) const MessagingView = lazy(async () => ({ default: (await import('../messaging')).MessagingView })) const SkillsView = lazy(async () => ({ default: (await import('../skills')).SkillsView })) export function LegacySessionRedirect() { const { sessionId } = useParams() return } export const SidebarSurface = memo(function SidebarSurface({ actions, currentView }: { actions: SidebarActions currentView: ComponentProps['currentView'] }) { const latestActions = useMemo(() => latestSidebarActions(actions), [actions]) return }) export const TerminalSurface = memo(function TerminalSurface() { return (
) }) /** Owns the statusbar's own data hooks (status snapshot poll, contributed * items) so its 15s refresh — and any statusbar-only churn — re-renders the * bar alone, never the chat/sidebar/terminal. */ export const StatusbarSurface = memo(function StatusbarSurface({ actions, agentsOpen, chatOpen, commandCenterOpen }: { actions: WiringActions agentsOpen: boolean chatOpen: boolean commandCenterOpen: boolean }) { const activeConnectionId = useStore($activeConnectionId) const activeGatewayProfile = useStore($activeGatewayProfile) const gatewayState = useStore($gatewayState) const freshDraftReady = useStore($freshDraftReady) const gatewayScope = `${activeConnectionId ?? ''}\0${activeGatewayProfile}` const { inferenceStatus, statusSnapshot } = useStatusSnapshot(gatewayState, actions.requestGateway, gatewayScope) const extraLeftItems = useStatusbarContributions('left') const extraRightItems = useStatusbarContributions('right') const { leftStatusbarItems, statusbarItems } = useStatusbarItems({ agentsOpen, chatOpen, commandCenterOpen, extraLeftItems, extraRightItems, freshDraftReady, gatewayState, inferenceStatus, openAgents: actions.openAgents, openCommandCenterSection: actions.openCommandCenterSection, requestGateway: actions.requestGateway, statusSnapshot, toggleCommandCenter: actions.toggleCommandCenter }) return }) /** The workspace pane: the real route table (chat + full-page views + plugin * routes). Subscribes to the gateway instance/state and ROUTES_AREA itself; * the voice cap arrives as a prop. ChatView subscribes to its own session * atoms, so streaming never round-trips through the controller. */ export const ChatRoutesSurface = memo(function ChatRoutesSurface({ actions, maxVoiceRecordingSeconds }: { actions: WiringActions maxVoiceRecordingSeconds?: number }) { const activeConnectionId = useStore($activeConnectionId) const activeGatewayProfile = useStore($activeGatewayProfile) const gateway = useStore($gateway) const gatewayState = useStore($gatewayState) useContributions(ROUTES_AREA) const routeContributions = contributedRoutes() const modelMenuContent = useMemo( () => gatewayState === 'open' ? ( ) : null, [actions, activeConnectionId, activeGatewayProfile, gateway, gatewayState] ) const chatActions = useMemo(() => latestChatActions(actions), [actions]) const chatView = ( ) // FULL-PAGE views (not chat): a page is not a tab-able surface, so the zone's // tab strip stands down while one is showing. That is `paneChrome.headerVeto` // on the contribution, not a DOM marker — the `data-zone-no-header` attribute // that used to ride this wrapper gated a body double-click toggle that no // longer exists, and nothing has read it since. const page = (view: ReactNode) => (
{view}
) return ( )} path="skills" /> )} path="messaging" /> )} path="artifacts" /> {/* Registry-contributed pages (core features + plugins) render in the workspace pane like any built-in view — behind the same blast wall as every other contribution mount. */} {routeContributions.map(route => ( )} key={route.key} path={route.path.slice(1)} /> ))} } path="new" /> } path="sessions/:sessionId" /> } path="*" /> ) })