import React, { useEffect, useState } from 'react' import logoIcon from '../../design/assets/logo-icon.png' import type { IssueChange } from '@commitea/core' import type { PublicConfig } from '../../global.js' import type { IssueRef } from '../../data/fixtures.js' import { ConnectScreen } from '../screens/connect-screen.js' import { backlogCalibration, capacityWorkers, forecastBacklog, issuesToBoardColumns, milestoneView, runwayView, scheduleFocus, } from '../../lib/backlog.js' import { useBacklog } from '../../lib/use-backlog.js' import { useCapacity } from '../../lib/use-capacity.js' import { PrimitivesGallery } from '../gallery.js' import { BoardScreen } from '../screens/board-screen.js' import { CalibrationScreen } from '../screens/calibration-screen.js' import { CaptureScreen } from '../screens/capture-screen.js' import { DirectivesScreen } from '../screens/directives-screen.js' import { FocusScreen } from '../screens/focus-screen.js' import { InboxScreen } from '../screens/inbox-screen.js' import { IssueScreen } from '../screens/issue-screen.js' import { MilestoneScreen } from '../screens/milestone-screen.js' import { OnboardingScreen } from '../screens/onboarding-screen.js' import { RunwayScreen } from '../screens/runway-screen.js' import { SettingsScreen } from '../screens/settings-screen.js' import { StandupScreen } from '../screens/standup-screen.js' import { Icon, Switch } from '../ui/index.js' import { ChatPanel } from './chat-panel.js' import { PlaceholderScreen } from './placeholder-screen.js' import { OfflineBanner, StatesScreen } from './states.js' type View = | 'standup' | 'focus' | 'inbox' | 'capture' | 'board' | 'runway' | 'directives' | 'settings' | 'states' | 'firstrun' | 'primitives' | 'issue' | 'calibration' | 'milestone' interface NavEntry { id: View label: string icon: string count?: number | null } // which phase builds each not-yet-real view (shown on its placeholder) const PHASE: Partial> = { standup: 'P3-3', focus: 'P3-3', inbox: 'P3-6', capture: 'P3-7', board: 'P3-4', runway: 'P3-5', directives: 'P3-8', settings: 'P3-8', firstrun: 'P3-8', issue: 'P3-6', calibration: 'P3-5', milestone: 'P3-5', } const TITLE: Partial> = { standup: 'Standup', focus: 'Morning service', inbox: 'Inbox', capture: 'Capture', board: 'The pot', runway: 'Runway', directives: 'Directives', settings: 'Settings', firstrun: 'First run', issue: 'Issue', calibration: 'Calibration', milestone: 'Milestone', } const INBOX_UNREAD = 3 export function AppShell() { const [view, setView] = useState('focus') const [prevView, setPrevView] = useState('focus') const [dark, setDark] = useState(false) const [offline, setOffline] = useState(false) const [issue, setIssue] = useState(null) const [readIds, setReadIds] = useState([]) const [milestoneId, setMilestoneId] = useState(null) const [gate, setGate] = useState<'checking' | 'connect' | 'ready'>('checking') const [pubConfig, setPubConfig] = useState(null) const [backlog, refetchBacklog] = useBacklog() const capacityMembers = useCapacity() const workers = capacityWorkers(capacityMembers) const boardColumns = backlog.status === 'ready' ? issuesToBoardColumns(backlog.issues, backlog.timelines) : undefined const focus = backlog.status === 'ready' ? scheduleFocus(backlog.issues, backlog.deps, backlog.timelines) : undefined const calibration = backlog.status === 'ready' ? backlogCalibration(backlog.issues, backlog.timelines) : undefined const forecast = backlog.status === 'ready' ? (forecastBacklog(backlog.issues, backlog.deps, new Date(), calibration?.model, workers) ?? undefined) : undefined const runwayMilestones = backlog.status === 'ready' ? runwayView(backlog.issues, backlog.milestones, backlog.deps, new Date(), workers) : undefined const milestone = backlog.status === 'ready' && milestoneId != null ? (milestoneView( milestoneId, backlog.issues, backlog.milestones, backlog.deps, backlog.timelines, calibration?.model, new Date(), workers, ) ?? undefined) : undefined useEffect(() => { document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light') }, [dark]) // Gate: e2e/demo → the shell (fixtures); configured → the shell (real data); // otherwise → the connect screen (each teammate brings their own token). useEffect(() => { window.commitea.gitea .status() .then((s) => setGate(s.demo || s.configured ? 'ready' : 'connect')) .catch(() => setGate('connect')) window.commitea.config.get().then(setPubConfig).catch(() => {}) }, []) const openIssue = (ref: IssueRef) => { if (view !== 'issue') setPrevView(view) setIssue(ref) setView('issue') } // The write path: apply through the bridge, reflect the new labels on the open // issue immediately, and re-reconcile so the board + forecast catch up. const applyChange = async (change: IssueChange) => { const res = await window.commitea.gitea.applyChange(change) if (res.ok) { setIssue((cur) => (cur && cur.id === res.issue.number ? { ...cur, labels: res.issue.labels } : cur)) refetchBacklog() } return res } const NAV: NavEntry[] = [ { id: 'standup', label: 'Standup', icon: 'sun' }, { id: 'focus', label: 'Morning service', icon: 'coffee' }, { id: 'inbox', label: 'Inbox', icon: 'bell', count: INBOX_UNREAD || null }, { id: 'capture', label: 'Capture', icon: 'plus' }, { id: 'board', label: 'The pot', icon: 'square-kanban' }, { id: 'runway', label: 'Runway', icon: 'chart-line' }, { id: 'directives', label: 'Directives', icon: 'flag' }, ] const NavItem = ({ item }: { item: NavEntry }) => { const active = view === item.id || (view === 'issue' && prevView === item.id) || ((view === 'calibration' || view === 'milestone') && item.id === 'runway') return ( ) } const renderScreen = () => { switch (view) { case 'focus': return case 'standup': return setView('focus')} onOpenIssue={openIssue} /> case 'board': return ( ) case 'runway': return ( setView('calibration')} onOpenMilestone={(id) => { setMilestoneId(id ?? null) setView('milestone') }} calibration={calibration ? { n: calibration.model.n, coldStart: calibration.model.coldStart } : undefined} milestones={runwayMilestones} capacity={capacityMembers} /> ) case 'calibration': return setView('runway')} data={calibration?.data} /> case 'milestone': return setView('runway')} onOpenIssue={openIssue} data={milestone} /> case 'inbox': return ( setView('directives')} readIds={readIds} setReadIds={setReadIds} /> ) case 'capture': return setView('focus')} /> case 'directives': return case 'settings': return ( setGate('connect')} onDisconnect={() => { void window.commitea.config.clear().then(() => { setPubConfig(null) setGate('connect') }) }} /> ) case 'issue': return issue ? ( setView(prevView)} onOpenIssue={openIssue} canWrite={backlog.status === 'ready'} onApplyChange={applyChange} /> ) : null case 'states': return setView('capture')} /> case 'primitives': return default: return } } // Connection gate (real onboarding) comes before everything else. if (gate === 'checking') { return
} if (gate === 'connect') { return ( { window.commitea.config.get().then(setPubConfig).catch(() => {}) setGate('ready') refetchBacklog() }} /> ) } // First run is full-window — no rail, no chat panel if (view === 'firstrun') { return setView(dest)} /> } return (
{/* left rail */} {/* main */}
{offline ? : null} {renderScreen()}
setView('directives')} offline={offline} onApplyChange={applyChange} />
) }