Command Center's Models section and Settings > Model rendered the same model state with identical persistence semantics — both write config and apply to new sessions only (POST /api/model/set). The Command Center UI was strictly better (provider catalog, curated model lists, friendly auxiliary-task labels, Nous-gateway auto-routing on main-provider switch), while Settings > Model was three barebones config fields. Extract that UI into a shared settings/model-settings.tsx (restyled with Settings primitives) and render it at the top of Settings > Model: main model picker via setModelAssignment + the 9 auxiliary task slots with per-task set-to-main / change / reset-all. model_context_length and fallback_providers stay as config fields below it; the raw auxiliary.* keys are dropped from Advanced (now covered by the panel). Strip the Models section from Command Center entirely (section, state, handlers, render, nav, search entry) leaving it focused on Sessions / System / Usage, and move the live store-sync callback (onMainModelChanged) from CommandCenterView to SettingsView. The composer's per-session model picker (the only live hot-swap, via /model) is unchanged.
714 lines
23 KiB
TypeScript
714 lines
23 KiB
TypeScript
import { useStore } from '@nanostores/react'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { lazy, Suspense, useCallback, useEffect, useRef } from 'react'
|
|
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
|
|
import { BootFailureOverlay } from '@/components/boot-failure-overlay'
|
|
import { DesktopInstallOverlay } from '@/components/desktop-install-overlay'
|
|
import { DesktopOnboardingOverlay } from '@/components/desktop-onboarding-overlay'
|
|
import { GatewayConnectingOverlay } from '@/components/gateway-connecting-overlay'
|
|
import { Pane, PaneMain } from '@/components/pane-shell'
|
|
import { useSkinCommand } from '@/themes/use-skin-command'
|
|
|
|
import { formatRefValue } from '../components/assistant-ui/directive-text'
|
|
import { getSessionMessages, listSessions } from '../hermes'
|
|
import { preserveLocalAssistantErrors, toChatMessages } from '../lib/chat-messages'
|
|
import {
|
|
$pinnedSessionIds,
|
|
$sessionsLimit,
|
|
bumpSessionsLimit,
|
|
FILE_BROWSER_DEFAULT_WIDTH,
|
|
FILE_BROWSER_MAX_WIDTH,
|
|
FILE_BROWSER_MIN_WIDTH,
|
|
pinSession,
|
|
SIDEBAR_DEFAULT_WIDTH,
|
|
SIDEBAR_MAX_WIDTH,
|
|
unpinSession
|
|
} from '../store/layout'
|
|
import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview'
|
|
import {
|
|
$activeSessionId,
|
|
$currentCwd,
|
|
$freshDraftReady,
|
|
$gatewayState,
|
|
$selectedStoredSessionId,
|
|
setAwaitingResponse,
|
|
setBusy,
|
|
setCurrentBranch,
|
|
setCurrentCwd,
|
|
setCurrentModel,
|
|
setCurrentProvider,
|
|
setMessages,
|
|
setSessions,
|
|
setSessionsLoading,
|
|
setSessionsTotal
|
|
} from '../store/session'
|
|
import { openUpdatesWindow, startUpdatePoller, stopUpdatePoller } from '../store/updates'
|
|
|
|
import { ChatView } from './chat'
|
|
import { useComposerActions } from './chat/hooks/use-composer-actions'
|
|
import {
|
|
ChatPreviewRail,
|
|
PREVIEW_RAIL_MAX_WIDTH,
|
|
PREVIEW_RAIL_MIN_WIDTH,
|
|
PREVIEW_RAIL_PANE_WIDTH
|
|
} from './chat/right-rail'
|
|
import { ChatSidebar } from './chat/sidebar'
|
|
import { useGatewayBoot } from './gateway/hooks/use-gateway-boot'
|
|
import { useGatewayRequest } from './gateway/hooks/use-gateway-request'
|
|
import { ModelPickerOverlay } from './model-picker-overlay'
|
|
import { RightSidebarPane } from './right-sidebar'
|
|
import { $terminalTakeover } from './right-sidebar/store'
|
|
import { PersistentTerminal, TerminalSlot } from './right-sidebar/terminal/persistent'
|
|
import { NEW_CHAT_ROUTE, routeSessionId, sessionRoute } from './routes'
|
|
import { useContextSuggestions } from './session/hooks/use-context-suggestions'
|
|
import { useCwdActions } from './session/hooks/use-cwd-actions'
|
|
import { useHermesConfig } from './session/hooks/use-hermes-config'
|
|
import { useMessageStream } from './session/hooks/use-message-stream'
|
|
import { useModelControls } from './session/hooks/use-model-controls'
|
|
import { usePreviewRouting } from './session/hooks/use-preview-routing'
|
|
import { usePromptActions } from './session/hooks/use-prompt-actions'
|
|
import { useRouteResume } from './session/hooks/use-route-resume'
|
|
import { useSessionActions } from './session/hooks/use-session-actions'
|
|
import { useSessionStateCache } from './session/hooks/use-session-state-cache'
|
|
import { AppShell } from './shell/app-shell'
|
|
import { useOverlayRouting } from './shell/hooks/use-overlay-routing'
|
|
import { useStatusSnapshot } from './shell/hooks/use-status-snapshot'
|
|
import { useStatusbarItems } from './shell/hooks/use-statusbar-items'
|
|
import type { StatusbarItem } from './shell/statusbar-controls'
|
|
import type { TitlebarTool } from './shell/titlebar-controls'
|
|
import { useGroupRegistry } from './shell/use-group-registry'
|
|
import { UpdatesOverlay } from './updates-overlay'
|
|
|
|
const AgentsView = lazy(async () => ({ default: (await import('./agents')).AgentsView }))
|
|
const ArtifactsView = lazy(async () => ({ default: (await import('./artifacts')).ArtifactsView }))
|
|
const CommandCenterView = lazy(async () => ({ default: (await import('./command-center')).CommandCenterView }))
|
|
const CronView = lazy(async () => ({ default: (await import('./cron')).CronView }))
|
|
const MessagingView = lazy(async () => ({ default: (await import('./messaging')).MessagingView }))
|
|
const ProfilesView = lazy(async () => ({ default: (await import('./profiles')).ProfilesView }))
|
|
const SettingsView = lazy(async () => ({ default: (await import('./settings')).SettingsView }))
|
|
const SkillsView = lazy(async () => ({ default: (await import('./skills')).SkillsView }))
|
|
|
|
export function DesktopController() {
|
|
const queryClient = useQueryClient()
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
|
|
const busyRef = useRef(false)
|
|
const creatingSessionRef = useRef(false)
|
|
const refreshSessionsRequestRef = useRef(0)
|
|
|
|
const gatewayState = useStore($gatewayState)
|
|
const activeSessionId = useStore($activeSessionId)
|
|
const currentCwd = useStore($currentCwd)
|
|
const freshDraftReady = useStore($freshDraftReady)
|
|
const filePreviewTarget = useStore($filePreviewTarget)
|
|
const previewTarget = useStore($previewTarget)
|
|
const selectedStoredSessionId = useStore($selectedStoredSessionId)
|
|
const terminalTakeover = useStore($terminalTakeover)
|
|
|
|
const routedSessionId = routeSessionId(location.pathname)
|
|
const routeToken = `${location.pathname}:${location.search}:${location.hash}`
|
|
const routeTokenRef = useRef(routeToken)
|
|
routeTokenRef.current = routeToken
|
|
const getRouteToken = useCallback(() => routeTokenRef.current, [])
|
|
|
|
const {
|
|
agentsOpen,
|
|
chatOpen,
|
|
closeOverlayToPreviousRoute,
|
|
commandCenterInitialSection,
|
|
commandCenterOpen,
|
|
currentView,
|
|
openAgents,
|
|
openCommandCenterSection,
|
|
settingsOpen,
|
|
toggleCommandCenter
|
|
} = useOverlayRouting()
|
|
|
|
const terminalTakeoverActive = chatOpen && terminalTakeover
|
|
|
|
const titlebarToolGroups = useGroupRegistry<TitlebarTool>()
|
|
const statusbarItemGroups = useGroupRegistry<StatusbarItem>()
|
|
const setTitlebarToolGroup = titlebarToolGroups.set
|
|
const setStatusbarItemGroup = statusbarItemGroups.set
|
|
|
|
const {
|
|
activeSessionIdRef,
|
|
ensureSessionState,
|
|
runtimeIdByStoredSessionIdRef,
|
|
selectedStoredSessionIdRef,
|
|
sessionStateByRuntimeIdRef,
|
|
syncSessionStateToView,
|
|
updateSessionState
|
|
} = useSessionStateCache({
|
|
activeSessionId,
|
|
busyRef,
|
|
selectedStoredSessionId,
|
|
setAwaitingResponse,
|
|
setBusy,
|
|
setMessages
|
|
})
|
|
|
|
const { connectionRef, gatewayRef, requestGateway } = useGatewayRequest()
|
|
|
|
useEffect(() => {
|
|
window.hermesDesktop?.setPreviewShortcutActive?.(Boolean(chatOpen && (filePreviewTarget || previewTarget)))
|
|
}, [chatOpen, filePreviewTarget, previewTarget])
|
|
|
|
useEffect(() => {
|
|
startUpdatePoller()
|
|
const unsubscribe = window.hermesDesktop?.onOpenUpdatesRequested?.(() => openUpdatesWindow())
|
|
|
|
return () => {
|
|
unsubscribe?.()
|
|
stopUpdatePoller()
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (!$filePreviewTarget.get() && !$previewTarget.get()) {
|
|
return
|
|
}
|
|
|
|
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'w') {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
closeActiveRightRailTab()
|
|
}
|
|
}
|
|
|
|
const unsubscribe = window.hermesDesktop?.onClosePreviewRequested?.(closeActiveRightRailTab)
|
|
|
|
window.addEventListener('keydown', onKeyDown, { capture: true })
|
|
|
|
return () => {
|
|
unsubscribe?.()
|
|
window.removeEventListener('keydown', onKeyDown, { capture: true })
|
|
}
|
|
}, [])
|
|
|
|
const refreshSessions = useCallback(async () => {
|
|
const requestId = refreshSessionsRequestRef.current + 1
|
|
refreshSessionsRequestRef.current = requestId
|
|
setSessionsLoading(true)
|
|
|
|
try {
|
|
const limit = $sessionsLimit.get()
|
|
// Require at least one message so abandoned/empty "Untitled" drafts (one
|
|
// was created per TUI/desktop launch before the lazy-create fix) don't
|
|
// clutter the sidebar.
|
|
const result = await listSessions(limit, 1)
|
|
|
|
if (refreshSessionsRequestRef.current === requestId) {
|
|
setSessions(result.sessions)
|
|
setSessionsTotal(typeof result.total === 'number' ? result.total : result.sessions.length)
|
|
}
|
|
} finally {
|
|
if (refreshSessionsRequestRef.current === requestId) {
|
|
setSessionsLoading(false)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const loadMoreSessions = useCallback(() => {
|
|
bumpSessionsLimit()
|
|
void refreshSessions()
|
|
}, [refreshSessions])
|
|
|
|
const toggleSelectedPin = useCallback(() => {
|
|
const sessionId = $selectedStoredSessionId.get()
|
|
|
|
if (!sessionId) {
|
|
return
|
|
}
|
|
|
|
if ($pinnedSessionIds.get().includes(sessionId)) {
|
|
unpinSession(sessionId)
|
|
} else {
|
|
pinSession(sessionId)
|
|
}
|
|
}, [])
|
|
|
|
const { gatewayLogLines, inferenceStatus, statusSnapshot } = useStatusSnapshot(gatewayState, requestGateway)
|
|
|
|
const updateActiveSessionRuntimeInfo = useCallback(
|
|
(info: { branch?: string; cwd?: string }) => {
|
|
const sessionId = activeSessionIdRef.current
|
|
|
|
if (!sessionId) {
|
|
return
|
|
}
|
|
|
|
updateSessionState(sessionId, state => ({
|
|
...state,
|
|
branch: info.branch ?? state.branch,
|
|
cwd: info.cwd ?? state.cwd
|
|
}))
|
|
},
|
|
[activeSessionIdRef, updateSessionState]
|
|
)
|
|
|
|
const { changeSessionCwd, refreshProjectBranch } = useCwdActions({
|
|
activeSessionId,
|
|
activeSessionIdRef,
|
|
onSessionRuntimeInfo: updateActiveSessionRuntimeInfo,
|
|
requestGateway
|
|
})
|
|
|
|
const { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds } = useHermesConfig({
|
|
activeSessionIdRef,
|
|
refreshProjectBranch
|
|
})
|
|
|
|
const { refreshCurrentModel, selectModel, updateModelOptionsCache } = useModelControls({
|
|
activeSessionId,
|
|
queryClient,
|
|
requestGateway
|
|
})
|
|
|
|
useContextSuggestions({
|
|
activeSessionId,
|
|
activeSessionIdRef,
|
|
currentCwd,
|
|
gatewayState,
|
|
requestGateway
|
|
})
|
|
|
|
const hydrateFromStoredSession = useCallback(
|
|
async (
|
|
attempts = 1,
|
|
storedSessionId = selectedStoredSessionIdRef.current,
|
|
runtimeSessionId = activeSessionIdRef.current
|
|
) => {
|
|
if (!storedSessionId || !runtimeSessionId) {
|
|
return
|
|
}
|
|
|
|
for (let index = 0; index < Math.max(1, attempts); index += 1) {
|
|
try {
|
|
const latest = await getSessionMessages(storedSessionId)
|
|
updateSessionState(
|
|
runtimeSessionId,
|
|
state => ({
|
|
...state,
|
|
messages: preserveLocalAssistantErrors(toChatMessages(latest.messages), state.messages)
|
|
}),
|
|
storedSessionId
|
|
)
|
|
|
|
return
|
|
} catch {
|
|
// Best-effort fallback when live stream payloads are empty.
|
|
}
|
|
|
|
if (index < attempts - 1) {
|
|
await new Promise(resolve => window.setTimeout(resolve, 250))
|
|
}
|
|
}
|
|
},
|
|
[activeSessionIdRef, selectedStoredSessionIdRef, updateSessionState]
|
|
)
|
|
|
|
const { handleGatewayEvent } = useMessageStream({
|
|
activeSessionIdRef,
|
|
hydrateFromStoredSession,
|
|
queryClient,
|
|
refreshHermesConfig,
|
|
refreshSessions,
|
|
updateSessionState
|
|
})
|
|
|
|
const { handleDesktopGatewayEvent, restartPreviewServer } = usePreviewRouting({
|
|
activeSessionIdRef,
|
|
baseHandleGatewayEvent: handleGatewayEvent,
|
|
currentCwd,
|
|
currentView,
|
|
requestGateway,
|
|
routedSessionId,
|
|
selectedStoredSessionId
|
|
})
|
|
|
|
const {
|
|
archiveSession,
|
|
branchCurrentSession,
|
|
createBackendSessionForSend,
|
|
openSettings,
|
|
removeSession,
|
|
resumeSession,
|
|
selectSidebarItem,
|
|
startFreshSessionDraft
|
|
} = useSessionActions({
|
|
activeSessionId,
|
|
activeSessionIdRef,
|
|
busyRef,
|
|
creatingSessionRef,
|
|
ensureSessionState,
|
|
getRouteToken,
|
|
navigate,
|
|
requestGateway,
|
|
runtimeIdByStoredSessionIdRef,
|
|
selectedStoredSessionId,
|
|
selectedStoredSessionIdRef,
|
|
sessionStateByRuntimeIdRef,
|
|
syncSessionStateToView,
|
|
updateSessionState
|
|
})
|
|
|
|
useEffect(() => {
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
const target = event.target as HTMLElement | null
|
|
|
|
const editing =
|
|
target?.isContentEditable ||
|
|
target instanceof HTMLInputElement ||
|
|
target instanceof HTMLTextAreaElement ||
|
|
target instanceof HTMLSelectElement
|
|
|
|
if (editing || event.defaultPrevented || event.repeat || event.altKey || event.ctrlKey || event.metaKey) {
|
|
return
|
|
}
|
|
|
|
if (event.shiftKey && event.code === 'KeyN') {
|
|
event.preventDefault()
|
|
startFreshSessionDraft()
|
|
}
|
|
}
|
|
|
|
window.addEventListener('keydown', onKeyDown)
|
|
|
|
return () => window.removeEventListener('keydown', onKeyDown)
|
|
}, [startFreshSessionDraft])
|
|
|
|
const composer = useComposerActions({
|
|
activeSessionId,
|
|
currentCwd,
|
|
requestGateway
|
|
})
|
|
|
|
const branchInNewChat = useCallback(
|
|
async (messageId?: string) => {
|
|
const branched = await branchCurrentSession(messageId)
|
|
|
|
if (branched) {
|
|
await refreshSessions().catch(() => undefined)
|
|
}
|
|
|
|
return branched
|
|
},
|
|
[branchCurrentSession, refreshSessions]
|
|
)
|
|
|
|
const startSessionInWorkspace = useCallback(
|
|
(path: null | string) => {
|
|
startFreshSessionDraft()
|
|
|
|
const target = path?.trim()
|
|
|
|
if (!target) {
|
|
return
|
|
}
|
|
|
|
// The next message creates the backend session in $currentCwd, so seed
|
|
// it (and the branch) from the workspace the user clicked the + on.
|
|
setCurrentCwd(target)
|
|
void requestGateway<{ branch?: string; cwd?: string }>('config.get', { key: 'project', cwd: target })
|
|
.then(info => {
|
|
setCurrentCwd(info.cwd || target)
|
|
setCurrentBranch(info.branch || '')
|
|
})
|
|
.catch(() => undefined)
|
|
},
|
|
[requestGateway, startFreshSessionDraft]
|
|
)
|
|
|
|
const handleSkinCommand = useSkinCommand()
|
|
|
|
const { cancelRun, editMessage, handleThreadMessagesChange, reloadFromMessage, submitText, transcribeVoiceAudio } =
|
|
usePromptActions({
|
|
activeSessionId,
|
|
activeSessionIdRef,
|
|
branchCurrentSession: branchInNewChat,
|
|
busyRef,
|
|
createBackendSessionForSend,
|
|
handleSkinCommand,
|
|
requestGateway,
|
|
selectedStoredSessionIdRef,
|
|
startFreshSessionDraft,
|
|
sttEnabled,
|
|
updateSessionState
|
|
})
|
|
|
|
useGatewayBoot({
|
|
handleGatewayEvent: handleDesktopGatewayEvent,
|
|
onConnectionReady: c => {
|
|
connectionRef.current = c
|
|
},
|
|
onGatewayReady: g => {
|
|
gatewayRef.current = g
|
|
},
|
|
refreshHermesConfig,
|
|
refreshSessions
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (gatewayState === 'open') {
|
|
void refreshCurrentModel()
|
|
void refreshSessions().catch(() => undefined)
|
|
}
|
|
}, [gatewayState, refreshCurrentModel, refreshSessions])
|
|
|
|
useRouteResume({
|
|
activeSessionId,
|
|
activeSessionIdRef,
|
|
creatingSessionRef,
|
|
currentView,
|
|
freshDraftReady,
|
|
gatewayState,
|
|
locationPathname: location.pathname,
|
|
resumeSession,
|
|
routedSessionId,
|
|
runtimeIdByStoredSessionIdRef,
|
|
selectedStoredSessionId,
|
|
selectedStoredSessionIdRef,
|
|
startFreshSessionDraft
|
|
})
|
|
|
|
const { leftStatusbarItems, statusbarItems } = useStatusbarItems({
|
|
agentsOpen,
|
|
commandCenterOpen,
|
|
extraLeftItems: statusbarItemGroups.flat.left,
|
|
extraRightItems: statusbarItemGroups.flat.right,
|
|
gatewayLogLines,
|
|
gatewayState,
|
|
inferenceStatus,
|
|
openAgents,
|
|
openCommandCenterSection,
|
|
statusSnapshot,
|
|
toggleCommandCenter
|
|
})
|
|
|
|
const sidebar = (
|
|
<ChatSidebar
|
|
currentView={currentView}
|
|
onArchiveSession={sessionId => void archiveSession(sessionId)}
|
|
onDeleteSession={sessionId => void removeSession(sessionId)}
|
|
onLoadMoreSessions={loadMoreSessions}
|
|
onNavigate={selectSidebarItem}
|
|
onNewSessionInWorkspace={startSessionInWorkspace}
|
|
onResumeSession={sessionId => navigate(sessionRoute(sessionId))}
|
|
/>
|
|
)
|
|
|
|
const overlays = (
|
|
<>
|
|
<DesktopInstallOverlay />
|
|
{/* One PTY-backed terminal mounted forever; <TerminalSlot /> placeholders
|
|
decide where it shows. Toggling fullscreen never rebuilds the shell. */}
|
|
<PersistentTerminal cwd={currentCwd} onAddSelectionToChat={composer.addTerminalSelectionAttachment} />
|
|
<DesktopOnboardingOverlay
|
|
enabled={gatewayState === 'open'}
|
|
onCompleted={() => {
|
|
void refreshHermesConfig()
|
|
void refreshCurrentModel()
|
|
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
|
}}
|
|
requestGateway={requestGateway}
|
|
/>
|
|
<ModelPickerOverlay gateway={gatewayRef.current || undefined} onSelect={selectModel} />
|
|
<UpdatesOverlay />
|
|
<GatewayConnectingOverlay />
|
|
<BootFailureOverlay />
|
|
|
|
{settingsOpen && (
|
|
<Suspense fallback={null}>
|
|
<SettingsView
|
|
gateway={gatewayRef.current}
|
|
onClose={closeOverlayToPreviousRoute}
|
|
onConfigSaved={() => {
|
|
void refreshHermesConfig()
|
|
void refreshCurrentModel()
|
|
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
|
}}
|
|
onMainModelChanged={(provider, model) => {
|
|
setCurrentProvider(provider)
|
|
setCurrentModel(model)
|
|
updateModelOptionsCache(provider, model, true)
|
|
void refreshCurrentModel()
|
|
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
|
|
}}
|
|
/>
|
|
</Suspense>
|
|
)}
|
|
|
|
{commandCenterOpen && (
|
|
<Suspense fallback={null}>
|
|
<CommandCenterView
|
|
initialSection={commandCenterInitialSection}
|
|
onClose={closeOverlayToPreviousRoute}
|
|
onDeleteSession={removeSession}
|
|
onNavigateRoute={path => navigate(path)}
|
|
onOpenSession={sessionId => navigate(sessionRoute(sessionId))}
|
|
/>
|
|
</Suspense>
|
|
)}
|
|
|
|
{agentsOpen && (
|
|
<Suspense fallback={null}>
|
|
<AgentsView onClose={closeOverlayToPreviousRoute} />
|
|
</Suspense>
|
|
)}
|
|
</>
|
|
)
|
|
|
|
const chatView = (
|
|
<ChatView
|
|
gateway={gatewayRef.current}
|
|
maxVoiceRecordingSeconds={voiceMaxRecordingSeconds}
|
|
onAddContextRef={composer.addContextRefAttachment}
|
|
onAddUrl={url => composer.addContextRefAttachment(`@url:${formatRefValue(url)}`, url)}
|
|
onAttachDroppedItems={composer.attachDroppedItems}
|
|
onAttachImageBlob={composer.attachImageBlob}
|
|
onBranchInNewChat={messageId => void branchInNewChat(messageId)}
|
|
onCancel={cancelRun}
|
|
onDeleteSelectedSession={() => {
|
|
if (selectedStoredSessionId) {
|
|
void removeSession(selectedStoredSessionId)
|
|
}
|
|
}}
|
|
onEdit={editMessage}
|
|
onPasteClipboardImage={() => void composer.pasteClipboardImage()}
|
|
onPickFiles={() => void composer.pickContextPaths('file')}
|
|
onPickFolders={() => void composer.pickContextPaths('folder')}
|
|
onPickImages={() => void composer.pickImages()}
|
|
onReload={reloadFromMessage}
|
|
onRemoveAttachment={id => void composer.removeAttachment(id)}
|
|
onSubmit={submitText}
|
|
onThreadMessagesChange={handleThreadMessagesChange}
|
|
onToggleSelectedPin={toggleSelectedPin}
|
|
onTranscribeAudio={transcribeVoiceAudio}
|
|
/>
|
|
)
|
|
|
|
const takeoverTerminalView = (
|
|
<div className="relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-(--ui-chat-surface-background) pt-(--titlebar-height)">
|
|
<TerminalSlot />
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<AppShell
|
|
commandCenterOpen={commandCenterOpen}
|
|
leftStatusbarItems={leftStatusbarItems}
|
|
leftTitlebarTools={titlebarToolGroups.flat.left}
|
|
onOpenSearch={() => openCommandCenterSection('sessions')}
|
|
onOpenSettings={openSettings}
|
|
overlays={overlays}
|
|
statusbarItems={statusbarItems}
|
|
titlebarTools={titlebarToolGroups.flat.right}
|
|
>
|
|
<Pane
|
|
disabled={terminalTakeoverActive}
|
|
id="chat-sidebar"
|
|
maxWidth={SIDEBAR_MAX_WIDTH}
|
|
minWidth={SIDEBAR_DEFAULT_WIDTH}
|
|
resizable
|
|
side="left"
|
|
width={`${SIDEBAR_DEFAULT_WIDTH}px`}
|
|
>
|
|
{sidebar}
|
|
</Pane>
|
|
<PaneMain>
|
|
<Routes>
|
|
<Route element={terminalTakeoverActive ? takeoverTerminalView : chatView} index />
|
|
<Route element={terminalTakeoverActive ? takeoverTerminalView : chatView} path=":sessionId" />
|
|
<Route
|
|
element={
|
|
<Suspense fallback={null}>
|
|
<SkillsView setStatusbarItemGroup={setStatusbarItemGroup} />
|
|
</Suspense>
|
|
}
|
|
path="skills"
|
|
/>
|
|
<Route
|
|
element={
|
|
<Suspense fallback={null}>
|
|
<MessagingView setStatusbarItemGroup={setStatusbarItemGroup} />
|
|
</Suspense>
|
|
}
|
|
path="messaging"
|
|
/>
|
|
<Route
|
|
element={
|
|
<Suspense fallback={null}>
|
|
<ArtifactsView setStatusbarItemGroup={setStatusbarItemGroup} />
|
|
</Suspense>
|
|
}
|
|
path="artifacts"
|
|
/>
|
|
<Route
|
|
element={
|
|
<Suspense fallback={null}>
|
|
<CronView setStatusbarItemGroup={setStatusbarItemGroup} />
|
|
</Suspense>
|
|
}
|
|
path="cron"
|
|
/>
|
|
<Route
|
|
element={
|
|
<Suspense fallback={null}>
|
|
<ProfilesView
|
|
setStatusbarItemGroup={setStatusbarItemGroup}
|
|
setTitlebarToolGroup={setTitlebarToolGroup}
|
|
/>
|
|
</Suspense>
|
|
}
|
|
path="profiles"
|
|
/>
|
|
<Route element={null} path="settings" />
|
|
<Route element={null} path="command-center" />
|
|
<Route element={null} path="agents" />
|
|
<Route element={<Navigate replace to={NEW_CHAT_ROUTE} />} path="new" />
|
|
<Route element={<LegacySessionRedirect />} path="sessions/:sessionId" />
|
|
<Route element={<Navigate replace to={NEW_CHAT_ROUTE} />} path="*" />
|
|
</Routes>
|
|
</PaneMain>
|
|
<Pane
|
|
disabled={!chatOpen || (!previewTarget && !filePreviewTarget)}
|
|
id="preview"
|
|
maxWidth={PREVIEW_RAIL_MAX_WIDTH}
|
|
minWidth={PREVIEW_RAIL_MIN_WIDTH}
|
|
resizable
|
|
side="right"
|
|
width={PREVIEW_RAIL_PANE_WIDTH}
|
|
>
|
|
{chatOpen ? (
|
|
<ChatPreviewRail onRestartServer={restartPreviewServer} setTitlebarToolGroup={setTitlebarToolGroup} />
|
|
) : null}
|
|
</Pane>
|
|
<Pane
|
|
defaultOpen={false}
|
|
disabled={!chatOpen}
|
|
id="file-browser"
|
|
maxWidth={FILE_BROWSER_MAX_WIDTH}
|
|
minWidth={FILE_BROWSER_MIN_WIDTH}
|
|
resizable
|
|
side="right"
|
|
width={FILE_BROWSER_DEFAULT_WIDTH}
|
|
>
|
|
<RightSidebarPane
|
|
onActivateFile={composer.attachContextFilePath}
|
|
onActivateFolder={composer.attachContextFolderPath}
|
|
onChangeCwd={changeSessionCwd}
|
|
/>
|
|
</Pane>
|
|
</AppShell>
|
|
)
|
|
}
|
|
|
|
function LegacySessionRedirect() {
|
|
const { sessionId } = useParams()
|
|
|
|
return <Navigate replace to={sessionId ? sessionRoute(sessionId) : NEW_CHAT_ROUTE} />
|
|
}
|