/**
* 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="*" />
)
})