setMobileOpen(false)} />
- )
- }
-
-
- {isAuthenticated && (
- <>
-
Navigation
- {mobileNav.map(({ path, label, icon: Icon }) => {
- const active = pathname === path || (path !== '/' && path !== '/blog' && pathname.startsWith(path))
- return (
-
setMobileOpen(false)}
- >
-
- {label}
- {active &&
}
-
- )
- })}
-
- >
- )}
-
More
- {isAuthenticated && (
-
setMobileOpen(false)}
- >
-
- Settings
- {pathname.startsWith('/settings') &&
}
-
- )}
-
setMobileOpen(false)}
- >
-
- Blog
- {pathname.startsWith('/blog') &&
}
-
-
setMobileOpen(false)}
- >
-
- Q&A
- {pathname.startsWith('/qna') &&
}
-
-
setMobileOpen(false)}>
-
- Docs
-
- {isAuthenticated && terminalInstalled && (
-
{
- if (terminalVisible && !terminalCollapsed) {
- setTerminalCollapsed(true)
- setTerminalVisible(false)
- } else {
- setTerminalVisible(true)
- setTerminalCollapsed(false)
- }
- setMobileOpen(false)
- }}
- >
-
- Terminal
-
- )}
-
-
setStatusModalOpen(true)}>
-
- System status
-
-
-
-
- >
- )
-}
diff --git a/webapp/src/components/layout/ShellLayout.tsx.bak.pre-branch-compare b/webapp/src/components/layout/ShellLayout.tsx.bak.pre-branch-compare
deleted file mode 100644
index 53932465..00000000
--- a/webapp/src/components/layout/ShellLayout.tsx.bak.pre-branch-compare
+++ /dev/null
@@ -1,388 +0,0 @@
-import { useEffect, useRef, useCallback, useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
-import { Outlet, useLocation } from 'react-router-dom'
-import { GlobalContextPanel } from './SessionsPanel'
-import { ChatWorkspace } from './ChatWorkspace'
-import { RightPanel } from './RightPanel'
-import { useProMode } from '@/hooks/useProMode'
-import { useViewMode } from '@/context/ViewModeContext'
-import { useShell } from '@/context/ShellContext'
-import { AssistantDashboard } from '@/pages/AssistantDashboard'
-import { LayoutDashboard } from 'lucide-react'
-import { approvalsApi, chatSessionsApi, tasksApi } from '@/api/client'
-
-// ── Mobile detection hook ─────────────────────────────────────────────────────
-function useIsMobile() {
- const [mobile, setMobile] = useState(() => window.innerWidth <= 640)
- const [tablet, setTablet] = useState(() => window.innerWidth > 640 && window.innerWidth <= 768)
- useEffect(() => {
- const mq = window.matchMedia('(max-width: 40rem)')
- const mqTablet = window.matchMedia('(min-width: 40.0625rem) and (max-width: 48rem)')
- const onMQ = (e: MediaQueryListEvent) => setMobile(e.matches)
- const onMQTablet = (e: MediaQueryListEvent) => setTablet(e.matches)
- mq.addEventListener('change', onMQ)
- mqTablet.addEventListener('change', onMQTablet)
- return () => { mq.removeEventListener('change', onMQ); mqTablet.removeEventListener('change', onMQTablet) }
- }, [])
- return { isMobile: mobile, isTablet: tablet }
-}
-
-type MobilePane = 'global' | 'chat' | 'inspector'
-
-// Routes always rendered full-page (no 3-pane, no mode guard)
-const FULLPAGE_ROUTES = [
- '/settings', '/profile', '/docs', '/blog', '/blogs', '/qna',
-]
-
-// Routes shown in 3-pane center when pro mode is on
-const PRO_PAGE_ROUTES = [
- '/control', '/apps', '/surfaces', '/agents', '/tasks', '/plugins', '/policies', '/security',
- '/security-center', '/logs', '/integrations', '/models', '/context-bank',
- '/marketplace', '/approvals', '/knowledge', '/workflows',
- '/build', '/analytics', '/admin', '/quota', '/memory',
-]
-
-function isFullPageRoute(pathname: string): boolean {
- return FULLPAGE_ROUTES.some(r => pathname === r || pathname.startsWith(r + '/'))
-}
-
-function isProPageRoute(pathname: string): boolean {
- return PRO_PAGE_ROUTES.some(r => pathname === r || pathname.startsWith(r + '/'))
-}
-
-// ── Storage keys ──────────────────────────────────────────────────────────────
-const LEFT_KEY = 'nebula_shell_left_ratio'
-const RIGHT_KEY = 'nebula_shell_right_ratio'
-
-// ── Defaults and constraints ──────────────────────────────────────────────────
-const DEFAULT_LEFT = 0.20 // 20% sessions panel
-const DEFAULT_RIGHT = 0.27 // 27% right panel
-const MIN_LEFT = 0.12
-const MAX_LEFT = 0.35
-const MIN_RIGHT = 0.18
-const MAX_RIGHT = 0.45
-
-function clamp(v: number, lo: number, hi: number) {
- return Math.max(lo, Math.min(hi, v))
-}
-
-function readRatio(key: string, fallback: number): number {
- try {
- const v = localStorage.getItem(key)
- if (v === null) return fallback
- const n = parseFloat(v)
- return isNaN(n) ? fallback : n
- } catch {
- return fallback
- }
-}
-
-// ── Resize handle ─────────────────────────────────────────────────────────────
-
-interface HandleProps {
- onMouseDown: (e: React.MouseEvent) => void
-}
-
-function ResizeHandle({ onMouseDown }: HandleProps) {
- return (
-
(e.currentTarget as HTMLElement).style.background = 'var(--nbl-cyan)'}
- onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'var(--nbl-border)'}
- />
- )
-}
-
-// ── Shell layout ──────────────────────────────────────────────────────────────
-
-export function ShellLayout() {
- const { pathname } = useLocation()
- const { proMode } = useProMode()
- const { isAssist } = useViewMode()
- const { chatExpanded, setChatExpanded, selectedItem, focusedChatMessage, activeSessionId, allChatMessages } = useShell()
- const { isMobile, isTablet } = useIsMobile()
- const [mobilePane, setMobilePane] = useState
('chat')
-
- // Full-page routes (settings/profile/blog/docs): render as Outlet without 3-pane shell
- const fullPage = isFullPageRoute(pathname)
- // Pro-mode page routes: render Outlet in center panel when pro mode is on
- const showPageInCenter = proMode && isProPageRoute(pathname)
- // Assistant mode: show dashboard on home route unless chat is expanded
- const showDashboard = isAssist && !chatExpanded && pathname === '/chat'
-
- const [leftRatio, setLeftRatio] = useState(() => clamp(readRatio(LEFT_KEY, DEFAULT_LEFT), MIN_LEFT, MAX_LEFT))
- const [rightRatio, setRightRatio] = useState(() => clamp(readRatio(RIGHT_KEY, DEFAULT_RIGHT), MIN_RIGHT, MAX_RIGHT))
- const touchStartX = useRef(0)
- const containerRef = useRef(null)
- const draggingRef = useRef<'left' | 'right' | null>(null)
- const rafRef = useRef(0)
-
- // Persist to localStorage on change
- useEffect(() => { localStorage.setItem(LEFT_KEY, String(leftRatio)) }, [leftRatio])
- useEffect(() => { localStorage.setItem(RIGHT_KEY, String(rightRatio)) }, [rightRatio])
-
- const startDrag = useCallback((which: 'left' | 'right') => (e: React.MouseEvent) => {
- e.preventDefault()
- draggingRef.current = which
- document.body.style.cursor = 'col-resize'
- document.body.style.userSelect = 'none'
-
- const onMove = (ev: MouseEvent) => {
- if (!draggingRef.current || !containerRef.current) return
- cancelAnimationFrame(rafRef.current)
- rafRef.current = requestAnimationFrame(() => {
- const rect = containerRef.current!.getBoundingClientRect()
- const frac = (ev.clientX - rect.left) / rect.width
-
- if (draggingRef.current === 'left') {
- setLeftRatio(clamp(frac, MIN_LEFT, MAX_LEFT))
- } else {
- // right handle: frac is distance from left edge, so right ratio = 1 - frac
- const rr = clamp(1 - frac, MIN_RIGHT, MAX_RIGHT)
- setRightRatio(rr)
- }
- })
- }
-
- const onUp = () => {
- draggingRef.current = null
- document.body.style.cursor = ''
- document.body.style.userSelect = ''
- document.removeEventListener('mousemove', onMove)
- document.removeEventListener('mouseup', onUp)
- }
-
- document.addEventListener('mousemove', onMove)
- document.addEventListener('mouseup', onUp)
- }, [])
-
- // Full-page routes bypass 3-pane shell entirely
- if (fullPage) {
- return (
-
-
-
- )
- }
-
- // Left panel only makes sense on the chat route — sessions are chat-specific
- const isChatRoute = pathname === '/chat'
-
- const { data: sessionDetail } = useQuery({
- queryKey: ['chat-session-detail', activeSessionId],
- queryFn: () => chatSessionsApi.get(activeSessionId!),
- enabled: isChatRoute && !!activeSessionId,
- staleTime: 15_000,
- refetchInterval: 20_000,
- })
-
- const sessionTaskIds = Array.from(new Set((sessionDetail?.entities ?? [])
- .filter(item => item.entity_type === 'task')
- .map(item => item.entity_id)))
-
- const sessionApprovalIds = Array.from(new Set(allChatMessages
- .map(msg => msg.approvalId)
- .filter((id): id is string => !!id)))
-
- const { data: sessionTasks = [] } = useQuery({
- queryKey: ['chat-session-tasks', activeSessionId, sessionTaskIds],
- queryFn: () => Promise.all(sessionTaskIds.map(id => tasksApi.get(id))),
- enabled: isChatRoute && !!activeSessionId && sessionTaskIds.length > 0,
- staleTime: 5_000,
- refetchInterval: 8_000,
- })
-
- const { data: sessionApprovals = [] } = useQuery({
- queryKey: ['chat-session-approvals', activeSessionId, sessionApprovalIds],
- queryFn: () => Promise.all(sessionApprovalIds.map(id => approvalsApi.get(id))),
- enabled: isChatRoute && !!activeSessionId && sessionApprovalIds.length > 0,
- staleTime: 5_000,
- refetchInterval: 10_000,
- })
-
- const runtimeCount = (sessionApprovals.filter(approval => approval.status === 'pending').length)
- + (allChatMessages.filter(message => message.role === 'agent').length)
- + (sessionTasks.filter(task => task.status === 'running').length)
- + ([...sessionTasks].filter(task => task.status !== 'running').slice(0, 8).length)
- + ((sessionDetail?.entities ?? []).length)
-
- // ── Mobile: swipe-based single-pane with top-corner indicators ────────────
- if (isMobile && isChatRoute) {
- const PANES: MobilePane[] = ['global', 'chat', 'inspector']
- const paneIndex = PANES.indexOf(mobilePane)
-
- const centerContent = showDashboard ? (
-
- ) : showPageInCenter ? (
-
- ) : (
-
- )
-
- // Touch swipe handler
- const handleTouchStart = (e: React.TouchEvent) => {
- touchStartX.current = e.touches[0].clientX
- }
- const handleTouchEnd = (e: React.TouchEvent) => {
- const dx = e.changedTouches[0].clientX - touchStartX.current
- if (Math.abs(dx) < 50) return
- if (dx < 0 && paneIndex < PANES.length - 1) setMobilePane(PANES[paneIndex + 1])
- if (dx > 0 && paneIndex > 0) setMobilePane(PANES[paneIndex - 1])
- }
-
- return (
-
- {/* ── Active pane ──────────────────────────────────────────────── */}
-
- {mobilePane === 'global' && }
- {mobilePane === 'chat' && centerContent}
- {mobilePane === 'inspector' && }
-
-
- {/* ── Left edge strip: tap or swipe right to go to previous pane ── */}
- {paneIndex > 0 && (
-
setMobilePane(PANES[paneIndex - 1])}
- aria-label="Previous pane"
- />
- )}
-
- {/* ── Right edge strip: tap or swipe left to go to next pane ──── */}
- {paneIndex < PANES.length - 1 && (
- setMobilePane(PANES[paneIndex + 1])}
- aria-label="Next pane"
- />
- )}
-
- )
- }
-
- // ── Tablet: hide left panel, center + right only ────────────────────────
- const hideLeft = isTablet || !isChatRoute
- const inspectorOpen = isChatRoute && (!!selectedItem || !!focusedChatMessage)
- const rightPanelOpen = isChatRoute && (inspectorOpen || runtimeCount > 0)
-
- const leftPct = `${(leftRatio * 100).toFixed(2)}%`
- const rightPct = rightPanelOpen
- ? isTablet ? '40%' : `${(rightRatio * 100).toFixed(2)}%`
- : '0%'
- const centerPct = rightPanelOpen
- ? hideLeft
- ? isTablet ? '60%' : `${((1 - rightRatio) * 100).toFixed(2)}%`
- : `${((1 - leftRatio - rightRatio) * 100).toFixed(2)}%`
- : hideLeft
- ? '100%'
- : `${((1 - leftRatio) * 100).toFixed(2)}%`
-
- return (
-
- {/* ── Left: Global Context (chat route only, desktop) ──────────── */}
- {isChatRoute && !isTablet && (
- <>
-
-
-
-
- >
- )}
-
- {/* ── Center: Dashboard / Chat / Page ─────────────────────────── */}
-
- {showDashboard ? (
-
- ) : showPageInCenter ? (
-
-
-
- ) : (
- <>
- {isAssist && chatExpanded && pathname === '/chat' && (
-
- setChatExpanded(false)}
- style={{
- display: 'flex', alignItems: 'center', gap: 5,
- padding: '3px 8px', borderRadius: 6,
- border: '1px solid var(--nbl-border)',
- background: 'transparent',
- color: 'var(--nbl-text-ghost)',
- fontSize: 11, cursor: 'pointer',
- transition: 'all 0.12s',
- }}
- onMouseEnter={e => (e.currentTarget.style.color = 'var(--nbl-text-primary)')}
- onMouseLeave={e => (e.currentTarget.style.color = 'var(--nbl-text-ghost)')}
- >
-
- Dashboard
-
-
- )}
-
- >
- )}
-
-
- {/* ── Right: Chat-scoped runtime / inspector ────────────────────── */}
- {rightPanelOpen && !isTablet &&
}
- {rightPanelOpen && (
-
-
-
- )}
-
- )
-}
diff --git a/webapp/src/pages/apps/AppsPage.tsx.bak.pre-branch-compare b/webapp/src/pages/apps/AppsPage.tsx.bak.pre-branch-compare
deleted file mode 100644
index b4925e9f..00000000
--- a/webapp/src/pages/apps/AppsPage.tsx.bak.pre-branch-compare
+++ /dev/null
@@ -1,289 +0,0 @@
-import { useMemo, useState, type ElementType } from 'react'
-import { Link } from 'react-router-dom'
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import {
- Bot,
- GitBranch,
- LayoutGrid,
- ListTodo,
- Package,
- Plug,
- Shield,
- BookOpen,
- ChevronRight,
- CheckCircle2,
- Trash2,
- Loader2,
-} from 'lucide-react'
-import { PageShell } from '@/components/nebula-ux'
-import { agentsApi, corporaApi, installablesApi, integrationsApi, pluginsApi, policiesApi, workflowsApi, type ApiInstallable } from '@/api/client'
-
-const INSTALLABLE_ICONS: Record = {
- bot: Bot,
- 'git-branch': GitBranch,
- 'list-todo': ListTodo,
- package: Package,
- plug: Plug,
- shield: Shield,
- 'shield-check': Shield,
- 'book-open': BookOpen,
- book: BookOpen,
- sparkles: LayoutGrid,
- 'layout-grid': LayoutGrid,
-}
-
-type InstalledApp = {
- id: string
- name: string
- route: string
- icon: ElementType
- accent: string
- description: string
- countLabel: string
- countValue?: number
- installable?: ApiInstallable
-}
-
-export function AppsPage() {
- const qc = useQueryClient()
- const { data: agents } = useQuery({
- queryKey: ['apps-agents'],
- queryFn: () => agentsApi.list({ page_size: 50 }),
- })
- const { data: workflows } = useQuery({
- queryKey: ['apps-workflows'],
- queryFn: () => workflowsApi.list({ page_size: 50 }),
- })
- const { data: plugins } = useQuery({
- queryKey: ['apps-plugins'],
- queryFn: () => pluginsApi.list({ page_size: 100 }),
- })
- const { data: policies } = useQuery({
- queryKey: ['apps-policies'],
- queryFn: () => policiesApi.list({ page_size: 50 }),
- })
- const { data: knowledge } = useQuery({
- queryKey: ['apps-knowledge'],
- queryFn: () => corporaApi.list({ page_size: 50 }),
- })
- const { data: integrations } = useQuery({
- queryKey: ['apps-integrations'],
- queryFn: () => integrationsApi.instances(),
- })
- const { data: installables = [] } = useQuery({
- queryKey: ['installables'],
- queryFn: () => installablesApi.list(),
- })
- const [busyInstallable, setBusyInstallable] = useState(null)
-
- const installableMut = useMutation({
- mutationFn: (item: ApiInstallable) => item.installed ? installablesApi.uninstall(item.slug) : installablesApi.install(item.slug),
- onMutate: (item) => setBusyInstallable(item.slug),
- onSuccess: () => qc.invalidateQueries({ queryKey: ['installables'] }),
- onSettled: () => setBusyInstallable(null),
- })
-
- const enabledPlugins = (plugins?.items ?? []).filter(plugin => plugin.enabled)
- const installableMap = new Map(installables.map(item => [item.slug, item]))
-
- const installedApps = useMemo(() => {
- const coreApps: InstalledApp[] = [
- {
- id: 'agents',
- name: 'Agents',
- route: '/agents',
- icon: Bot,
- accent: 'var(--nbl-cyan)',
- description: 'Create, inspect, and operate the agent workforce behind your runtime.',
- countLabel: 'Installed',
- countValue: agents?.total ?? agents?.items?.length ?? 0,
- installable: installableMap.get('agents'),
- },
- {
- id: 'tasks',
- name: 'Tasks',
- route: '/tasks',
- icon: ListTodo,
- accent: 'var(--nbl-green)',
- description: 'Track live executions, retries, failures, and final outcomes.',
- countLabel: 'Installed',
- installable: installableMap.get('tasks'),
- },
- {
- id: 'workflows',
- name: 'Workflows',
- route: '/workflows',
- icon: GitBranch,
- accent: 'var(--nbl-amber)',
- description: 'Design and run repeatable automations with observable execution paths.',
- countLabel: 'Installed',
- countValue: workflows?.total ?? workflows?.items?.length ?? 0,
- installable: installableMap.get('workflows'),
- },
- ].filter(app => app.installable?.installed)
-
- if (installableMap.get('plugins')?.installed) {
- coreApps.push({
- id: 'plugins',
- name: 'Plugins',
- route: '/plugins',
- icon: Package,
- accent: 'var(--nbl-purple)',
- description: 'Installed tools and extensions that expand what your runtime can do.',
- countLabel: 'Enabled',
- countValue: enabledPlugins.length,
- installable: installableMap.get('plugins'),
- })
- }
-
- if (installableMap.get('policies')?.installed) {
- coreApps.push({
- id: 'policies',
- name: 'Policies',
- route: '/policies',
- icon: Shield,
- accent: 'var(--nbl-red)',
- description: 'Guardrails, approvals, and enforcement logic active in your system.',
- countLabel: 'Active',
- countValue: policies?.items?.length ?? 0,
- installable: installableMap.get('policies'),
- })
- }
-
- if (installableMap.get('knowledge')?.installed) {
- coreApps.push({
- id: 'knowledge',
- name: 'Knowledge',
- route: '/knowledge',
- icon: BookOpen,
- accent: 'var(--nbl-green)',
- description: 'Corpora and sources currently indexed for retrieval and memory augmentation.',
- countLabel: 'Corpora',
- countValue: knowledge?.corpora?.length ?? 0,
- installable: installableMap.get('knowledge'),
- })
- }
-
- if (installableMap.get('integrations')?.installed) {
- coreApps.push({
- id: 'integrations',
- name: 'Integrations',
- route: '/integrations',
- icon: Plug,
- accent: 'var(--nbl-cyan)',
- description: 'Connected services and external systems available to your runtime.',
- countLabel: 'Connected',
- countValue: integrations?.length ?? 0,
- installable: installableMap.get('integrations'),
- })
- }
-
- const knownIds = new Set(coreApps.map(app => app.id))
- const extraApps = installables
- .filter(item => item.installed && !!item.route)
- .filter(item => !['constellation', 'terms', 'terminal'].includes(item.slug))
- .filter(item => !knownIds.has(item.slug))
- .map(item => ({
- id: item.slug,
- name: item.name,
- route: item.route!,
- icon: INSTALLABLE_ICONS[item.icon ?? 'layout-grid'] ?? LayoutGrid,
- accent: 'var(--nbl-nebula)',
- description: item.description,
- countLabel: item.kind === 'workspace' ? 'Installed' : item.kind === 'runtime' ? 'Enabled' : 'Available',
- installable: item,
- }))
-
- return [...coreApps, ...extraApps]
- }, [agents?.items?.length, agents?.total, enabledPlugins.length, installableMap, installables, integrations?.length, knowledge?.corpora?.length, policies?.items?.length, workflows?.items?.length, workflows?.total])
-
- return (
-
-
-
-
-
-
Apps
-
-
Open installed NebulaOS apps and workspace surfaces from one place. Chat remains native and is always available from the dedicated chat button.
-
-
- Open Marketplace
-
-
-
-
- {installedApps.length === 0 && (
-
-
-
-
-
No installed apps yet
-
- Install agents, tasks, workflows, or other workspace apps from the marketplace and they will appear here automatically. Chat remains native and is always available from the header.
-
-
-
- Open Marketplace
-
-
-
-
- )}
-
-
- {installedApps.map((app, index) => {
- const Icon = app.icon
- const installable = app.installable
- const removable = !!app.installable && !app.installable.default_installed
- const busy = busyInstallable === installable?.slug
- return (
-
-
-
-
-
-
-
- {app.countValue != null ? `${app.countLabel} · ${app.countValue}` : app.countLabel}
-
-
-
-
{app.name}
-
{app.description}
-
-
-
Open
-
- {removable && installable && (
- {
- e.preventDefault()
- e.stopPropagation()
- installableMut.mutate(installable)
- }}
- disabled={busy}
- className="ne-AppsPage__remove-btn"
- title="Remove installed app"
- >
- {busy ? : }
- Remove
-
- )}
-
-
-
-
- )
- })}
-
-
- )
-}
diff --git a/webapp/src/pages/apps/index.ts.bak.pre-branch-compare b/webapp/src/pages/apps/index.ts.bak.pre-branch-compare
deleted file mode 100644
index f54a53be..00000000
--- a/webapp/src/pages/apps/index.ts.bak.pre-branch-compare
+++ /dev/null
@@ -1 +0,0 @@
-export { AppsPage } from './AppsPage'