Files
OpenOPC/opc/plugins/office_ui/frontend_src/workspace/ContextPanel.tsx
T
2026-07-14 14:35:43 +08:00

1320 lines
60 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { RoleWorkItemSummary, Session, TaskPreferredAgent } from '../types/kanban'
import type { ChatMessage, CheckpointReplyMetadata, OutgoingAttachmentPayload } from '../types/chat'
import type { AgentInfo, OrgInfoPayload, SavedOrgSummary } from '../types/visual'
import type { CommsStatePayload, CommsMessagePayload } from '../lib/wsClient'
import { CommsPanel } from './CommsPanel'
import { ProjectCockpit } from './ProjectCockpit'
import { PRIORITY_META, type TaskPriority } from '../types/kanban'
import { TaskHeaderBar } from '../chat/TaskHeaderBar'
import { MarkdownBody, MessageList } from '../chat/MessageList'
import { MessageComposer } from '../chat/MessageComposer'
import { analyzeCheckpointMessages, checkpointReplyMetadataForComposer } from '../chat/checkpointUtils'
import { AgentWorkPanel } from '../chat/AgentWorkPanel'
import { WorkItemProgressCard } from '../chat/WorkItemProgressCard'
import { IconTimeline, IconHandoff, IconTool } from '../chat/SvgIcons'
import { isSessionWorking } from '../lib/sessionRuntime'
import { getWorkItemRoleLabel } from '../lib/workItemIdentity'
import { getLinkedRuntimeTaskId } from '../lib/workItemRuntimeIds'
import { TaskDetailView } from './TaskDetailView'
import {
getConversationPeerSessions,
getConversationHeaderSession,
getConversationSessionView,
getConversationMessageCount,
getWorkItemRoleSessions,
mergeConversationProgressLog,
projectSessionConversation,
} from '../lib/workItemSessions'
type ActiveView =
| { kind: 'session'; taskId: string }
| { kind: 'task-detail'; taskId: string }
| { kind: 'activity' }
| { kind: 'secretary' }
| { kind: 'child-detail' }
interface ContextPanelProps {
panelState: 'collapsed' | 'open' | 'maximized'
width: number
onResizeMouseDown: (e: React.MouseEvent) => void
isResizing: boolean
activeView: ActiveView
activeSession: Session | null
activeTask?: import('../types/kanban').KanbanTask | null
linkedTaskSession?: Session | null
linkedTaskSessionMessages?: ChatMessage[]
childDetailSession: Session | null
messages: ChatMessage[]
childDetailMessages: ChatMessage[]
allSessions: Session[]
openSessions: Session[]
openSessionMessages: Record<string, ChatMessage[]>
openSessionChildren: Record<string, Session[]>
agents: AgentInfo[]
childSessions: Session[]
execMode?: string
taskPreferredAgent: TaskPreferredAgent
savedOrgsList?: SavedOrgSummary[] | null
activeSavedOrg?: string | null
onSavedOrgsList?: () => void
onSavedOrgLoad?: (name: string) => void
canShowAgentsTab?: boolean
channelId: string
channelName: string
secretaryChannelId: string
unreadCounts?: Record<string, number>
multiSessionView?: boolean
panelTab: 'chat' | 'agents' | 'info' | 'comms' | 'team'
onPanelTabChange: (tab: 'chat' | 'agents' | 'info' | 'comms' | 'team') => void
commsState?: CommsStatePayload | null
commsMessage?: CommsMessagePayload | null
onCommsRefresh?: () => void
onCommsReadMessage?: (path: string) => void
orgInfoData?: OrgInfoPayload | null
canShowTeamTab?: boolean
onTeamStopRun?: () => void
onTitleChange: (taskId: string, title: string) => void
onSessionConfigChange?: (taskId: string, execMode: string, companyProfile?: string, orgId?: string) => void
onSessionTaskAgentChange?: (taskId: string, preferredAgent: TaskPreferredAgent) => void
/**
* User asked to "continue this conversation in a different mode" from the
* locked-mode chip popover. We expect the host to spin up a fresh chat in
* the requested mode (inside the same project).
*/
onContinueInNewChat?: (mode: 'task' | 'company' | 'org' | 'custom', companyProfile?: 'corporate' | 'custom', orgId?: string) => void
onStop?: () => void
onComplete?: () => void
onResume?: () => void
onResumeTask?: (taskId: string) => void
onStopTask?: (taskId: string) => void
onCompleteTask?: (taskId: string) => void
onLocateOnBoard?: (taskId: string) => void
onBackToParent?: () => void
onCloseTaskDetail?: () => void
onOpenChildDetail?: (taskId: string) => void
onOpenExecutionPanel?: (taskId: string) => void
onSelectSessionTab?: (taskId: string) => void
onCloseSessionTab?: (taskId: string) => void
onToggleMultiSessionView?: () => void
onCollapse: () => void
onExpand: () => void
onMaximize: () => void
onComposerSend: (content: string, attachments?: OutgoingAttachmentPayload[]) => void
onMessageSend: (content: string, taskId?: string, metadata?: CheckpointReplyMetadata) => void
onSessionSend?: (
taskId: string,
content: string,
attachments?: OutgoingAttachmentPayload[],
metadata?: CheckpointReplyMetadata,
) => void
onWorkItemClick: (executionTurnId: string) => void
onWorkItemOpenSession?: (executionTurnId: string) => void
onMarkRead: () => void
onSessionMarkRead?: (taskId: string) => void
onLoadSessionHistory?: (
taskId: string,
oldestMessage?: ChatMessage,
detailLevel?: 'summary' | 'full',
) => Promise<void> | void
isSessionHistoryLoading?: (taskId: string) => boolean
}
function relativeTime(ts: number): string {
const diff = Date.now() - ts
if (diff < 60_000) return 'just now'
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`
return `${Math.floor(diff / 86_400_000)}d`
}
function sessionRuntimeLabel(session: Session, activeChildCount: number): string | null {
if (activeChildCount > 1) return `${activeChildCount} agents working`
const displayTool = session.displayTool || session.currentTool
if (displayTool) return `Running ${displayTool}`
if (session.agentStatus === 'reflecting') return 'Thinking...'
if (session.status === 'running') return 'Running'
return null
}
function activeRoleWorkItemCount(roleWorkItems?: Record<string, RoleWorkItemSummary>): number | undefined {
if (!roleWorkItems || Object.keys(roleWorkItems).length === 0) return undefined
return Object.values(roleWorkItems).filter(summary => summary.aggregatedStatus === 'active').length
}
function distinctWorkingRoleCount(sessions: Session[]): number {
const roles = new Set(
sessions
.filter(isSessionWorking)
.map(s => String(s.workItemRoleId ?? s.assigneeIds[0] ?? s.taskId).trim())
.filter(Boolean),
)
return roles.size
}
function activeAgentCountFor(
roleWorkItems: Record<string, RoleWorkItemSummary> | undefined,
sessions: Session[],
): number | undefined {
const roleCount = activeRoleWorkItemCount(roleWorkItems)
if (roleCount !== undefined) return roleCount || undefined
return distinctWorkingRoleCount(sessions) || undefined
}
function sessionModeLabel(session: Session): string {
const execMode = composerExecModeForSession(session)
if (execMode === 'org' || execMode === 'custom') return `company/${session.orgId ?? 'org'}`
if (execMode === 'company') return `company/${session.companyProfile ?? 'corporate'}`
return execMode
}
function normalizePanelExecMode(value?: string): 'task' | 'company' | 'org' {
const normalized = String(value ?? '').trim().toLowerCase()
if (normalized === 'company') return 'company'
if (normalized === 'org' || normalized === 'custom') return 'org'
return 'task'
}
function hasCompanyRuntimeIdentity(session: Session): boolean {
return !!(
session.isCompanyRuntime
|| session.parentSessionId
|| session.workItemProjectionId
|| session.roleWorkItems
|| session.executorRoleWorkItems
)
}
function isCompanyRuntimeSession(
session: Session | null | undefined,
relatedSessionCount = 0,
): boolean {
if (!session) return false
const mode = String(session.execMode ?? '').trim().toLowerCase()
return relatedSessionCount > 0
|| hasCompanyRuntimeIdentity(session)
|| mode === 'company'
|| mode === 'org'
|| mode === 'custom'
}
export function sessionHasMoreForDetail(
session: Session,
detailLevel: 'summary' | 'full',
): boolean | undefined {
const scoped = detailLevel === 'full' ? session.fullHasMore : session.summaryHasMore
if (scoped !== undefined) return scoped
// Old snapshots only expose the unscoped value. Once either scoped cursor
// has been observed, the generic field may describe the other policy.
if (session.summaryHasMore === undefined && session.fullHasMore === undefined) {
return session.hasMore
}
return undefined
}
export function conversationHasOlderHistory(
sessions: Session[],
displayedMessageCount: number,
detailLevel: 'summary' | 'full',
allowMessageCountFallback = true,
): boolean {
const pagination = sessions.map(session => sessionHasMoreForDetail(session, detailLevel))
if (pagination.some(hasMore => hasMore === true)) return true
if (sessions.length !== 1 || pagination[0] === false) return false
return allowMessageCountFallback && sessions[0].messageCount > displayedMessageCount
}
function hasCustomRuntimeIdentity(session: Session): boolean {
const rawMode = String(session.execMode ?? '').trim().toLowerCase()
const normalizedMode = normalizePanelExecMode(session.execMode)
const profile = String(session.companyProfile ?? '').trim().toLowerCase()
if (normalizedMode === 'company') return false
if (normalizedMode === 'org') return true
if (rawMode) return false
return profile === 'custom' || !!session.orgId
}
function isSessionConfigLocked(session: Session | undefined, visibleMessageCount = 0): boolean {
if (!session) return visibleMessageCount > 0
const messageCount = Math.max(session.messageCount ?? 0, visibleMessageCount)
if (messageCount > 0) return true
const status = String(session.status ?? '').trim().toLowerCase()
if (status && status !== 'pending') return true
if (
session.parentSessionId
|| session.workItemProjectionId
|| session.workItemRoleId
|| session.workItemTurnType
|| session.pendingRuntimeCheckpointId
) {
return true
}
if (session.runtimeControlState && session.runtimeControlState !== 'idle') return true
if ((session.progressLog?.length ?? 0) > 0 || (session.workItemLog?.length ?? 0) > 0) return true
if (session.roleWorkItems && Object.keys(session.roleWorkItems).length > 0) return true
if (session.executorRoleWorkItems && Object.keys(session.executorRoleWorkItems).length > 0) return true
return false
}
export function composerExecModeForSession(session: Session, fallbackExecMode?: string): string {
const rawMode = String(session.execMode ?? '').trim().toLowerCase()
const hasExplicitMode = rawMode.length > 0
const normalized = normalizePanelExecMode(session.execMode)
if (hasCustomRuntimeIdentity(session)) {
return 'org'
}
if (!hasExplicitMode && normalized === 'task' && hasCompanyRuntimeIdentity(session)) {
return 'company'
}
return session.execMode ?? fallbackExecMode ?? 'task'
}
function composerTaskAgentForSession(
session: Session,
locked: boolean,
fallbackAgent: TaskPreferredAgent,
): TaskPreferredAgent {
if (locked) {
if (session.selectedExecutionAgent && session.selectedExecutionAgent !== 'native') {
return session.selectedExecutionAgent
}
return session.preferredAgent ?? session.selectedExecutionAgent ?? fallbackAgent
}
return session.preferredAgent ?? session.selectedExecutionAgent ?? fallbackAgent
}
function sessionDetailLevel(
session: Session | null | undefined,
options?: { childDetail?: boolean },
): 'summary' | 'full' {
const childDetail = !!options?.childDetail
if (!session) return 'summary'
if (childDetail) return 'full'
return session.execMode === 'company' || session.execMode === 'org' || session.execMode === 'custom' ? 'summary' : 'full'
}
const HUMAN_REVIEW_STATUSES = new Set([
'awaiting_human',
'awaiting_manager_review',
'awaiting_review',
'awaiting_peer',
])
function canShowContinue(session: Session): boolean {
const status = String(session.status ?? '').trim()
const fallback = session.runtimeControlState === 'suspended'
|| (!HUMAN_REVIEW_STATUSES.has(status) && status !== 'running' && status !== 'done')
return Boolean(session.canResume ?? fallback)
}
function InfoTabView({
task,
agents,
roleLabel,
}: {
task: Session
agents: AgentInfo[]
roleLabel: string | null
}) {
const [showDev, setShowDev] = useState(false)
const priorityKey = task.priority as TaskPriority | undefined
const priorityMeta = priorityKey && (task.priority as string) in PRIORITY_META
? PRIORITY_META[priorityKey]
: null
const assigneeNames = task.assigneeIds
.map(id => agents.find(a => a.agent_id === id)?.name ?? id)
.filter(Boolean)
const createdAt = new Date(task.createdAt)
const employeeLabel = task.employeeAssignment?.name
? `${task.employeeAssignment.name}${task.employeeAssignment.category ? ` · ${task.employeeAssignment.category}` : ''}`
: null
// Field-level "fact" rendering helper — uniform spacing + treatment.
const Fact = ({ label, value }: { label: string; value: React.ReactNode }) => (
<div className="ctx-info-fact">
<span className="ctx-info-fact-label">{label}</span>
<span className="ctx-info-fact-value">{value}</span>
</div>
)
return (
<div className="ctx-info-v2">
<section className="ctx-info-section">
<h4 className="ctx-info-section-title">Status</h4>
<div className="ctx-info-card">
<div className="ctx-info-status-row">
<span className={`ctx-info-status-pill status-${task.status}`}>
<span className="ctx-info-status-dot" />
{task.status}
</span>
{priorityMeta && (
<span className="ctx-info-priority-pill" style={{ color: priorityMeta.color }}>
<span aria-hidden="true">{priorityMeta.symbol}</span>
{priorityMeta.label}
</span>
)}
</div>
{task.tags && task.tags.length > 0 && (
<div className="ctx-info-chips">
{task.tags.map(tag => (
<span key={tag} className="ctx-info-chip">{tag}</span>
))}
</div>
)}
</div>
</section>
<section className="ctx-info-section">
<h4 className="ctx-info-section-title">People &amp; agent</h4>
<div className="ctx-info-card">
{assigneeNames.length > 0 && (
<Fact
label="Assignees"
value={
<div className="ctx-info-chips">
{assigneeNames.map(name => (
<span key={name} className="ctx-info-chip">{name}</span>
))}
</div>
}
/>
)}
{roleLabel && <Fact label="Role" value={roleLabel} />}
{employeeLabel && <Fact label="Employee" value={employeeLabel} />}
{task.selectedExecutionAgent && (
<Fact label="Execution agent" value={
<span className="ctx-info-mono">{task.selectedExecutionAgent}</span>
} />
)}
</div>
</section>
<section className="ctx-info-section">
<h4 className="ctx-info-section-title">Timing</h4>
<div className="ctx-info-card">
<Fact
label="Created"
value={
<span>
{createdAt.toLocaleString(undefined, {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
})}
</span>
}
/>
</div>
</section>
<section className="ctx-info-section ctx-info-dev-section">
<button
className={`ctx-info-dev-toggle${showDev ? ' expanded' : ''}`}
type="button"
onClick={() => setShowDev(v => !v)}
aria-expanded={showDev}
>
<svg width="11" height="11" viewBox="0 0 16 16" aria-hidden="true">
<path
d={showDev ? 'M3 6l5 5 5-5' : 'M6 3l5 5-5 5'}
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Developer details</span>
</button>
{showDev && (
<div className="ctx-info-card ctx-info-dev-card">
{task.workItemProjectionId && (
<Fact label="Projection" value={
<span className="ctx-info-mono ctx-info-mono-break">{task.workItemProjectionId}</span>
} />
)}
<Fact label="Task ID" value={
<span className="ctx-info-mono ctx-info-mono-break">{task.taskId}</span>
} />
<Fact label="Channel" value={
<span className="ctx-info-mono ctx-info-mono-break">{task.channelId}</span>
} />
{task.sessionId && (
<Fact label="Session" value={
<span className="ctx-info-mono ctx-info-mono-break">{task.sessionId}</span>
} />
)}
</div>
)}
</section>
</div>
)
}
export function ContextPanel({
panelState,
width,
onResizeMouseDown,
isResizing,
activeView,
activeSession,
activeTask,
linkedTaskSession: linkedTaskSessionProp,
linkedTaskSessionMessages,
childDetailSession,
messages,
childDetailMessages,
allSessions,
openSessions,
openSessionMessages,
openSessionChildren,
agents,
childSessions,
execMode,
taskPreferredAgent,
savedOrgsList,
activeSavedOrg,
onSavedOrgsList,
onSavedOrgLoad,
canShowAgentsTab = false,
channelId,
channelName,
secretaryChannelId,
unreadCounts,
multiSessionView = false,
panelTab,
onPanelTabChange,
commsState,
commsMessage,
onCommsRefresh,
onCommsReadMessage,
orgInfoData,
canShowTeamTab = false,
onTeamStopRun,
onTitleChange,
onSessionConfigChange,
onSessionTaskAgentChange,
onContinueInNewChat,
onStop,
onComplete,
onResume,
onResumeTask,
onStopTask,
onCompleteTask,
onLocateOnBoard,
onBackToParent,
onCloseTaskDetail,
onOpenChildDetail,
onOpenExecutionPanel,
onSelectSessionTab,
onCloseSessionTab,
onToggleMultiSessionView,
onCollapse,
onExpand,
onMaximize,
onComposerSend,
onMessageSend,
onSessionSend,
onWorkItemClick,
onWorkItemOpenSession,
onMarkRead,
onSessionMarkRead,
onLoadSessionHistory,
isSessionHistoryLoading,
}: ContextPanelProps) {
const isSecretary = activeView.kind === 'secretary'
const isActivity = activeView.kind === 'activity'
const isChildDetail = activeView.kind === 'child-detail'
const isTaskDetail = activeView.kind === 'task-detail'
const isCompanyRuntime = isCompanyRuntimeSession(activeSession, childSessions.length)
const showTabs = activeView.kind === 'session' && activeSession
const canSend = isSecretary ? true : !!activeSession
const showSessionStrip = !isChildDetail && openSessions.length > 0
const canShowMultiSessionView = openSessions.length > 1
const showMultiSessionGrid = !!(multiSessionView && activeView.kind === 'session' && activeSession && canShowMultiSessionView && panelTab === 'chat')
const childDetailScrollRef = useRef<HTMLDivElement | null>(null)
const activeConversation = useMemo(() => {
const conversationPeers = getConversationPeerSessions(activeSession, allSessions)
return projectSessionConversation(activeSession, [...conversationPeers, ...childSessions])
}, [activeSession, childSessions, allSessions])
const activeDisplaySession = activeConversation.displaySession ?? activeSession
const activeDetailMode = sessionDetailLevel(activeDisplaySession)
const activeConversationSession = useMemo(() => {
return getConversationSessionView(activeSession, activeConversation.runtimeSession, activeConversation.timelineSessions)
}, [activeSession, activeConversation.runtimeSession, activeConversation.timelineSessions])
const activeHeaderSession = useMemo(() => {
return getConversationHeaderSession(activeSession, activeConversation.runtimeSession, activeConversation.timelineSessions)
}, [activeSession, activeConversation.runtimeSession, activeConversation.timelineSessions])
const activeConversationProgress = useMemo(() => {
return mergeConversationProgressLog(activeConversation.timelineSessions)
}, [activeConversation.timelineSessions])
const activeWorkItemLog = useMemo(() => (
activeConversationSession?.workItemLog ?? activeSession?.workItemLog ?? []
), [activeConversationSession?.workItemLog, activeSession?.workItemLog])
const activeWorkItemRoleSessions = useMemo(() => (
getWorkItemRoleSessions(activeConversationSession ?? activeSession, allSessions)
), [activeConversationSession, activeSession, allSessions])
const activeRoleWorkItems = useMemo(() => (
activeConversationSession?.roleWorkItems ?? activeSession?.roleWorkItems
), [activeConversationSession?.roleWorkItems, activeSession?.roleWorkItems])
const activeExecutorRoleWorkItems = useMemo(() => (
activeConversationSession?.executorRoleWorkItems ?? activeSession?.executorRoleWorkItems
), [
activeConversationSession?.executorRoleWorkItems,
activeSession?.executorRoleWorkItems,
])
const hasRoleWorkItems = !!(
(activeRoleWorkItems && Object.keys(activeRoleWorkItems).length > 0)
|| (activeExecutorRoleWorkItems && Object.keys(activeExecutorRoleWorkItems).length > 0)
)
const visibleAgentSessions = activeWorkItemRoleSessions.length > 0
? activeWorkItemRoleSessions
: childSessions
const activeConversationMessageCount = useMemo(() => {
return getConversationMessageCount(activeConversation.timelineSessions)
}, [activeConversation.timelineSessions])
const activeConversationLoading = useMemo(() => (
activeConversation.timelineSessions.some((session) => isSessionHistoryLoading?.(session.taskId) ?? false)
), [activeConversation.timelineSessions, isSessionHistoryLoading])
const resolveConversationHistoryTarget = useCallback((oldestMessage?: ChatMessage) => {
if (oldestMessage) {
const matched = activeConversation.timelineSessions.find(
(session) => session.channelId === oldestMessage.channelId,
)
if (matched && sessionHasMoreForDetail(matched, activeDetailMode) !== false) return matched
}
const knownTarget = activeConversation.timelineSessions.find(
session => sessionHasMoreForDetail(session, activeDetailMode) === true,
)
if (knownTarget) return knownTarget
return activeDisplaySession ?? activeSession
}, [activeConversation.timelineSessions, activeDetailMode, activeDisplaySession, activeSession])
// Child detail: find the agent for this session
const childDetailAgent = useMemo(() => {
if (!childDetailSession) return undefined
const id = childDetailSession.assigneeIds[0]
return id ? agents.find(a => a.agent_id === id) : undefined
}, [childDetailSession, agents])
// Use prop-provided linked session (computed + lazy-loaded by WorkspacePage)
// with a local fallback for backwards compat
const linkedTaskSession = linkedTaskSessionProp ?? (() => {
const linkedRuntimeTaskId = getLinkedRuntimeTaskId(activeTask)
if (!linkedRuntimeTaskId) return null
return allSessions.find(session =>
session.taskId === linkedRuntimeTaskId
|| session.runtimeTaskId === linkedRuntimeTaskId
|| session.executionTurnId === linkedRuntimeTaskId
) ?? null
})()
useEffect(() => {
if (!isChildDetail || !childDetailSession) return
childDetailScrollRef.current?.scrollTo({ top: 0, behavior: 'auto' })
}, [isChildDetail, childDetailSession?.taskId])
// Task for Info tab
const taskForInfo = activeSession
// Collapsed state: render a thin strip
if (panelState === 'collapsed') {
return (
<div
className="ctx-collapse-strip"
onClick={() => onExpand()}
title="Open panel"
>
&#x25C0;
</div>
)
}
return (
<>
<div
className={`ctx-resize-handle${isResizing ? ' active' : ''}`}
onMouseDown={onResizeMouseDown}
/>
<div
className={`ctx-panel${panelState === 'maximized' ? ' maximized' : ''}`}
style={panelState !== 'maximized' ? { width } : undefined}
>
{/* Task detail view */}
{isTaskDetail && activeTask ? (
<TaskDetailView
task={activeTask}
linkedSession={linkedTaskSession}
linkedSessionMessages={linkedTaskSessionMessages}
agents={agents}
onBack={onCloseTaskDetail ?? onCollapse}
onOpenLinkedSession={onSelectSessionTab}
onOpenExecutionPanel={onOpenExecutionPanel}
/>
) : isChildDetail && childDetailSession ? (
/* Child detail view */
<div className="ctx-child-detail" ref={childDetailScrollRef}>
<div className="ctx-child-topbar">
<button className="ctx-back-btn" onClick={onBackToParent}>
&#x2190; Back to parent
</button>
{(childDetailSession.canStop ?? childDetailSession.status === 'running') && childDetailSession.runtimeControlState !== 'suspending' && onStopTask && (
<button
className="ctx-child-stop-btn"
onClick={() => onStopTask(childDetailSession.taskId)}
title="Stop this agent"
>
Stop
</button>
)}
{childDetailSession.runtimeControlState === 'suspending' && (
<button className="ctx-child-stop-btn" disabled title="Stopping this agent">
Stopping...
</button>
)}
{canShowContinue(childDetailSession) && onResumeTask && (
<button
className="ctx-child-resume-btn"
onClick={() => onResumeTask(childDetailSession.taskId)}
title="Continue this agent's runtime"
>
Continue
</button>
)}
</div>
<div className="ctx-child-header">
{childDetailAgent && (
<div className="ctx-child-avatar">
{childDetailAgent.name.charAt(0).toUpperCase()}
</div>
)}
<div className="ctx-child-meta">
<span className="ctx-child-name">
{childDetailAgent?.name ?? childDetailSession.title}
</span>
{childDetailSession.workItemRoleName && (
<span className="ctx-child-work-item">
{childDetailSession.workItemRoleName}
</span>
)}
{!childDetailSession.workItemRoleName && childDetailSession.workItemProjectionId && (
<span className="ctx-child-work-item">
{childDetailSession.workItemProjectionId.replace(/_/g, ' ')}
</span>
)}
</div>
</div>
{childDetailSession.handoffContext && (
<div className="exec-section ctx-child-markdown-section">
<div className="exec-section-header">
<IconHandoff />
<span>Received From</span>
</div>
<div className="msg-content-agent-card ctx-child-markdown-card">
<MarkdownBody content={childDetailSession.handoffContext} />
</div>
</div>
)}
<div className="ctx-child-transcript">
<div className="ctx-child-transcript-header">
<IconTimeline />
<span>Transcript</span>
</div>
<MessageList
key={childDetailSession.channelId}
messages={childDetailMessages}
channelName={childDetailSession.title}
viewKind="session"
detailMode="full"
agentStatus={childDetailSession.agentStatus}
currentTool={childDetailSession.currentTool}
toolElapsedMs={childDetailSession.toolElapsedMs}
lastToolSummary={childDetailSession.lastToolSummary}
progressLog={childDetailSession.progressLog}
draftAssistantText={childDetailSession.draftAssistantText}
draftUpdatedAt={childDetailSession.draftUpdatedAt}
draftIteration={childDetailSession.draftIteration}
draftTurnId={childDetailSession.draftTurnId}
onMarkRead={onMarkRead}
hasOlderHistory={
// A scoped backend cursor remains actionable during live
// work. Only the racy messageCount fallback is suppressed.
conversationHasOlderHistory(
[childDetailSession],
childDetailMessages.length,
'full',
!isSessionWorking(childDetailSession),
)
}
totalMessageCount={childDetailSession.messageCount}
onLoadOlderHistory={(oldestMessage) => onLoadSessionHistory?.(childDetailSession.taskId, oldestMessage, 'full')}
loadingOlderHistory={isSessionHistoryLoading?.(childDetailSession.taskId) ?? false}
scrollPolicy="initial-bottom"
scrollScope={childDetailSession.channelId}
showRuntimeProgress
renderUserMarkdown
/>
</div>
{childDetailSession.artifacts && childDetailSession.artifacts.length > 0 && (
<div className="ctx-child-artifacts">
<div className="ctx-child-section-header">
<IconTool />
<span>Artifacts</span>
</div>
<ul className="ctx-child-artifact-list">
{childDetailSession.artifacts.map((a, i) => (
<li key={i}>{a}</li>
))}
</ul>
</div>
)}
{childDetailSession.handoffTo && (
<div className="ctx-child-markdown-section ctx-child-passed-to">
<div className="ctx-child-section-header">
<IconHandoff />
<span>Passed To</span>
</div>
<div className="msg-content-agent-card ctx-child-markdown-card">
<MarkdownBody content={childDetailSession.handoffTo} />
</div>
</div>
)}
</div>
) : (
<>
{showSessionStrip && (
<div className="ctx-session-strip">
<div className="ctx-session-strip-scroll">
{openSessions.map((session) => {
const unreadCount = unreadCounts?.[session.channelId] ?? 0
const isActiveTab = activeSession?.taskId === session.taskId
return (
<div
key={session.taskId}
className={`ctx-session-chip${isActiveTab ? ' active' : ''}`}
>
<button
className="ctx-session-chip-main"
onClick={() => onSelectSessionTab?.(session.taskId)}
title={session.title}
>
<span className={`ctx-session-chip-status status-${session.status}`} />
<span className="ctx-session-chip-title">{session.title}</span>
{unreadCount > 0 && (
<span className="ctx-session-chip-unread">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>
<button
className="ctx-session-chip-close"
onClick={() => onCloseSessionTab?.(session.taskId)}
title="Close tab"
>
&#x00D7;
</button>
</div>
)
})}
</div>
{canShowMultiSessionView && (
<button
className={`ctx-session-view-btn${multiSessionView ? ' active' : ''}`}
onClick={onToggleMultiSessionView}
>
{multiSessionView ? 'Single View' : 'Multi View'}
</button>
)}
</div>
)}
{/* Header with tabs or title */}
<div className="ctx-header">
{showMultiSessionGrid ? (
<span className="ctx-header-title">{isCompanyRuntime ? 'Open Runtime Sessions' : 'Open Sessions'}</span>
) : showTabs ? (
<div className="ctx-header-tabs" role="tablist" aria-label="Session view">
<button
role="tab"
aria-selected={panelTab === 'chat'}
className={`ctx-tab${panelTab === 'chat' ? ' active' : ''}`}
onClick={() => onPanelTabChange('chat')}
>
Chat
</button>
{canShowAgentsTab && (
<button
role="tab"
aria-selected={panelTab === 'agents'}
className={`ctx-tab${panelTab === 'agents' ? ' active' : ''}`}
onClick={() => onPanelTabChange('agents')}
>
Agents
</button>
)}
<button
role="tab"
aria-selected={panelTab === 'info'}
className={`ctx-tab${panelTab === 'info' ? ' active' : ''}`}
onClick={() => onPanelTabChange('info')}
>
Info
</button>
{onCommsRefresh && (
<button
role="tab"
aria-selected={panelTab === 'comms'}
className={`ctx-tab${panelTab === 'comms' ? ' active' : ''}`}
onClick={() => onPanelTabChange('comms')}
>
Comms
</button>
)}
{canShowTeamTab && (
<button
role="tab"
aria-selected={panelTab === 'team'}
className={`ctx-tab${panelTab === 'team' ? ' active' : ''}`}
onClick={() => onPanelTabChange('team')}
>
Team
</button>
)}
</div>
) : isSecretary ? (
<span style={{ fontSize: 13, fontWeight: 600, padding: '0 4px' }}>Secretary</span>
) : (
<span style={{ fontSize: 13, fontWeight: 600, padding: '0 4px' }}>Activity</span>
)}
<div className="ctx-header-actions">
{panelState === 'open' && (
<button onClick={onMaximize} title="Maximize">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.3" /></svg>
</button>
)}
{panelState === 'maximized' && (
<button onClick={onExpand} title="Restore">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="3" width="8" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.3" /><path d="M5 3V1.5A.5.5 0 015.5 1H12.5a.5.5 0 01.5.5V8.5a.5.5 0 01-.5.5H11" stroke="currentColor" strokeWidth="1.3" /></svg>
</button>
)}
<button onClick={onCollapse} title="Close panel">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /></svg>
</button>
</div>
</div>
{/* Body */}
<div className="ctx-body">
{/* No session selected */}
{!activeSession && !isSecretary && !isActivity && (
<div className="ctx-empty">
<span className="ctx-empty-icon">&#x1F4CB;</span>
<span>{isCompanyRuntime ? 'Select a Work Item to see details' : 'Select a task to see details'}</span>
</div>
)}
{showMultiSessionGrid && (
<div className="ctx-multi-grid">
{openSessions.map((session) => {
const sessionMessages = openSessionMessages[session.taskId] ?? []
const sessionChildren = openSessionChildren[session.taskId] ?? []
const sessionPeers = getConversationPeerSessions(session, allSessions)
const sessionConversation = projectSessionConversation(session, [...sessionPeers, ...sessionChildren])
const sessionConversationSession = getConversationSessionView(
session,
sessionConversation.runtimeSession,
sessionConversation.timelineSessions,
)
const sessionWorkItemRoleSessions = getWorkItemRoleSessions(
sessionConversationSession ?? session,
allSessions,
)
const sessionRoleWorkItems = sessionConversationSession?.roleWorkItems ?? session.roleWorkItems
const activeChildCount = activeAgentCountFor(sessionRoleWorkItems, sessionChildren) ?? 0
const assigneeNames = session.assigneeIds
.map(id => agents.find(agent => agent.agent_id === id)?.name ?? id)
.filter(Boolean)
const runtimeLabel = sessionRuntimeLabel(sessionConversationSession ?? session, activeChildCount)
const sessionIsCompanyRuntime = isCompanyRuntimeSession(session, sessionChildren.length)
const sessionDisplaySession = sessionConversation.displaySession ?? session
const sessionProgressLog = mergeConversationProgressLog(sessionConversation.timelineSessions)
const sessionMessageCount = getConversationMessageCount(sessionConversation.timelineSessions)
const sessionLockedMode = isSessionConfigLocked(
sessionConversationSession ?? sessionDisplaySession,
Math.max(sessionMessageCount, sessionMessages.length),
)
const sessionHistoryLoading = sessionConversation.timelineSessions.some(
(timelineSession) => isSessionHistoryLoading?.(timelineSession.taskId) ?? false,
)
return (
<section
key={session.taskId}
className={`ctx-multi-card${activeSession?.taskId === session.taskId ? ' focused' : ''}`}
>
<div className="ctx-multi-card-header">
<button
className="ctx-multi-card-main"
onClick={() => onSelectSessionTab?.(session.taskId)}
title="Focus session"
>
<span className={`ctx-multi-card-status status-${session.status}`} />
<span className="ctx-multi-card-title">{session.title}</span>
</button>
<div className="ctx-multi-card-actions">
{((sessionConversationSession ?? session).canStop ?? (sessionConversationSession ?? session).status === 'running') && (sessionConversationSession ?? session).runtimeControlState !== 'suspending' && onStopTask && (
<button onClick={() => onStopTask(sessionConversation.runtimeSession?.taskId ?? session.taskId)}>Stop</button>
)}
{(sessionConversationSession ?? session).runtimeControlState === 'suspending' && (
<button disabled>Stopping...</button>
)}
{canShowContinue(sessionConversationSession ?? session) && onResumeTask && (
<button onClick={() => onResumeTask(session.taskId)}>Continue</button>
)}
{(sessionConversationSession ?? session).status !== 'done' && (sessionConversationSession ?? session).status !== 'cancelled' && onCompleteTask && (
<button onClick={() => onCompleteTask(session.taskId)}>Done</button>
)}
<button onClick={() => onSelectSessionTab?.(session.taskId)}>Focus</button>
<button onClick={() => onCloseSessionTab?.(session.taskId)} title="Close tab">
&#x00D7;
</button>
</div>
</div>
<div className="ctx-multi-card-meta">
<span>{sessionModeLabel(session)}</span>
<span>{session.status}</span>
{runtimeLabel && <span>{runtimeLabel}</span>}
{assigneeNames.length > 0 && <span>{assigneeNames.join(', ')}</span>}
<span>{relativeTime(session.updatedAt)}</span>
</div>
<div className="ctx-multi-card-body">
<MessageList
key={sessionDisplaySession?.channelId ?? session.channelId}
messages={sessionMessages}
channelName={sessionDisplaySession?.title ?? session.title}
viewKind="session"
detailMode={sessionDetailLevel(sessionDisplaySession)}
agentStatus={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.agentStatus ?? sessionDisplaySession?.agentStatus)}
currentTool={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.currentTool ?? sessionDisplaySession?.currentTool)}
toolElapsedMs={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.toolElapsedMs ?? sessionDisplaySession?.toolElapsedMs)}
lastToolSummary={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.lastToolSummary ?? sessionDisplaySession?.lastToolSummary)}
progressLog={sessionIsCompanyRuntime ? undefined : sessionProgressLog}
draftAssistantText={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.draftAssistantText ?? sessionDisplaySession?.draftAssistantText)}
draftUpdatedAt={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.draftUpdatedAt ?? sessionDisplaySession?.draftUpdatedAt)}
draftIteration={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.draftIteration ?? sessionDisplaySession?.draftIteration)}
draftTurnId={sessionIsCompanyRuntime ? undefined : (sessionConversationSession?.draftTurnId ?? sessionDisplaySession?.draftTurnId)}
isCompanyRuntime={sessionIsCompanyRuntime}
workItemLog={sessionConversationSession?.workItemLog ?? session.workItemLog}
childSessions={sessionWorkItemRoleSessions}
showWorkItemRuntimeCard={!sessionIsCompanyRuntime}
onSend={(content, _taskId, metadata) => onSessionSend?.(session.taskId, content, undefined, metadata)}
onWorkItemClick={onWorkItemClick}
onWorkItemOpenSession={onWorkItemOpenSession}
onMarkRead={() => onSessionMarkRead?.(session.taskId)}
scrollScope={session.channelId}
hasOlderHistory={
// Keep known cursors available during live work;
// suppress only the count-based fallback.
conversationHasOlderHistory(
sessionConversation.timelineSessions,
sessionMessages.length,
sessionDetailLevel(sessionDisplaySession ?? session),
!sessionConversation.timelineSessions.some(isSessionWorking),
)
}
totalMessageCount={sessionMessageCount}
onLoadOlderHistory={(oldestMessage) => {
const detailLevel = sessionDetailLevel(sessionDisplaySession ?? session)
const matchedSession = sessionConversation.timelineSessions.find(
(timelineSession) => timelineSession.channelId === oldestMessage?.channelId,
)
const targetSession = (
matchedSession
&& sessionHasMoreForDetail(matchedSession, detailLevel) !== false
? matchedSession
: undefined
) ?? sessionConversation.timelineSessions.find(
timelineSession => sessionHasMoreForDetail(timelineSession, detailLevel) === true,
) ?? sessionDisplaySession ?? session
return onLoadSessionHistory?.(
targetSession.taskId,
oldestMessage,
detailLevel,
)
}}
loadingOlderHistory={sessionHistoryLoading}
showRuntimeProgress={sessionDetailLevel(sessionDisplaySession) === 'full'}
/>
</div>
<MessageComposer
disabled={false}
channelId={sessionConversationSession?.channelId ?? session.channelId}
execMode={composerExecModeForSession(session, execMode)}
companyProfile={session.companyProfile}
taskPreferredAgent={composerTaskAgentForSession(session, sessionLockedMode, taskPreferredAgent)}
agentStatus={sessionConversationSession?.agentStatus ?? sessionDisplaySession?.agentStatus}
currentTool={sessionConversationSession?.currentTool ?? sessionDisplaySession?.currentTool}
displayTool={sessionConversationSession?.displayTool ?? sessionDisplaySession?.displayTool}
activeAgentCount={activeChildCount || undefined}
runtimeControlState={(sessionConversationSession ?? sessionDisplaySession)?.runtimeControlState}
canStop={(sessionConversationSession ?? sessionDisplaySession)?.canStop}
savedOrgs={savedOrgsList ?? null}
activeSavedOrg={activeSavedOrg ?? null}
selectedOrgId={session.orgId ?? activeSavedOrg ?? null}
lockedMode={sessionLockedMode}
autoFocus={false}
contextTokens={sessionConversationSession?.contextTokens ?? sessionDisplaySession?.contextTokens}
contextWindow={sessionConversationSession?.contextWindow ?? sessionDisplaySession?.contextWindow}
contextRemainingPct={sessionConversationSession?.contextRemainingPct ?? sessionDisplaySession?.contextRemainingPct ?? 100}
onSend={(content, attachments) => onSessionSend?.(
session.taskId,
content,
attachments,
checkpointReplyMetadataForComposer(
analyzeCheckpointMessages(sessionMessages).latestPendingReplyMetadata,
),
)}
onModeChange={(mode, profile, orgId) => onSessionConfigChange?.(session.taskId, mode, profile, orgId)}
onTaskAgentChange={(preferredAgent) => onSessionTaskAgentChange?.(session.taskId, preferredAgent)}
onContinueInNewChat={onContinueInNewChat}
onSavedOrgsRefresh={onSavedOrgsList}
onSavedOrgLoad={onSavedOrgLoad}
onStop={() => onStopTask?.(sessionConversation.runtimeSession?.taskId ?? session.taskId)}
/>
</section>
)
})}
</div>
)}
{/* Activity view */}
{isActivity && (
<>
<div style={{ padding: '8px 12px', fontSize: 12, color: 'var(--text-secondary)', borderBottom: '1px solid var(--border)' }}>
Recent activity across all sessions
</div>
<MessageList
key={channelId}
messages={messages}
channelName="Activity"
viewKind="activity"
detailMode="summary"
onSend={onMessageSend}
onMarkRead={onMarkRead}
scrollScope={channelId}
/>
</>
)}
{/* Secretary view */}
{isSecretary && (
<>
<MessageList
key={secretaryChannelId}
messages={messages}
channelName="Secretary"
viewKind="secretary"
detailMode="summary"
onSend={onMessageSend}
onMarkRead={onMarkRead}
scrollScope={secretaryChannelId}
/>
<MessageComposer
disabled={false}
channelId={secretaryChannelId}
placeholder="Talk to Secretary about policies, rules, preferences..."
onSend={onComposerSend}
/>
</>
)}
{/* Session view — Chat tab */}
{activeSession && !isSecretary && !isActivity && !showMultiSessionGrid && panelTab === 'chat' && (
<>
<TaskHeaderBar
session={activeHeaderSession ?? activeSession}
agents={agents}
onTitleChange={onTitleChange}
onViewOnBoard={onLocateOnBoard ? () => onLocateOnBoard(activeSession.taskId) : undefined}
onStop={onStop}
onComplete={(activeHeaderSession ?? activeSession).status !== 'done' && (activeHeaderSession ?? activeSession).status !== 'cancelled' ? onComplete : undefined}
onResume={onResume}
/>
{isCompanyRuntime && (
<div className="ctx-work-item-progress">
<WorkItemProgressCard
workItemLog={activeWorkItemLog}
roleWorkItems={activeRoleWorkItems}
executorRoleWorkItems={activeExecutorRoleWorkItems}
childSessions={activeWorkItemRoleSessions}
isCompanyRuntime={isCompanyRuntime}
onWorkItemClick={onWorkItemClick}
/>
</div>
)}
<MessageList
key={channelId}
messages={messages}
channelName={channelName}
viewKind="session"
detailMode={activeDetailMode}
agentStatus={isCompanyRuntime ? undefined : (activeConversationSession?.agentStatus ?? activeDisplaySession?.agentStatus)}
currentTool={isCompanyRuntime ? undefined : (activeConversationSession?.currentTool ?? activeDisplaySession?.currentTool)}
toolElapsedMs={isCompanyRuntime ? undefined : (activeConversationSession?.toolElapsedMs ?? activeDisplaySession?.toolElapsedMs)}
lastToolSummary={isCompanyRuntime ? undefined : (activeConversationSession?.lastToolSummary ?? activeDisplaySession?.lastToolSummary)}
progressLog={isCompanyRuntime ? undefined : activeConversationProgress}
draftAssistantText={isCompanyRuntime ? undefined : (activeConversationSession?.draftAssistantText ?? activeDisplaySession?.draftAssistantText)}
draftUpdatedAt={isCompanyRuntime ? undefined : (activeConversationSession?.draftUpdatedAt ?? activeDisplaySession?.draftUpdatedAt)}
draftIteration={isCompanyRuntime ? undefined : (activeConversationSession?.draftIteration ?? activeDisplaySession?.draftIteration)}
draftTurnId={isCompanyRuntime ? undefined : (activeConversationSession?.draftTurnId ?? activeDisplaySession?.draftTurnId)}
isCompanyRuntime={isCompanyRuntime}
workItemLog={activeWorkItemLog}
roleWorkItems={activeRoleWorkItems}
executorRoleWorkItems={activeExecutorRoleWorkItems}
childSessions={activeWorkItemRoleSessions}
onSend={onMessageSend}
onWorkItemClick={onWorkItemClick}
onWorkItemOpenSession={onWorkItemOpenSession}
onMarkRead={onMarkRead}
scrollScope={channelId}
hasOlderHistory={
// Keep known cursors available during live work;
// suppress only the count-based fallback.
conversationHasOlderHistory(
activeConversation.timelineSessions,
messages.length,
activeDetailMode,
!activeConversation.timelineSessions.some(isSessionWorking),
)
}
totalMessageCount={activeConversationMessageCount}
onLoadOlderHistory={(oldestMessage) => {
const targetSession = resolveConversationHistoryTarget(oldestMessage)
if (!targetSession) return
return onLoadSessionHistory?.(
targetSession.taskId,
oldestMessage,
activeDetailMode,
)
}}
loadingOlderHistory={activeConversationLoading}
showWorkItemRuntimeCard={false}
showRuntimeProgress={activeDetailMode === 'full'}
/>
<MessageComposer
disabled={!canSend}
channelId={activeConversationSession?.channelId ?? channelId}
execMode={composerExecModeForSession(activeSession, execMode)}
companyProfile={activeSession.companyProfile}
taskPreferredAgent={composerTaskAgentForSession(
activeSession,
isSessionConfigLocked(
activeConversationSession ?? activeDisplaySession ?? activeSession,
Math.max(activeConversationMessageCount, messages.length),
),
taskPreferredAgent,
)}
agentStatus={activeConversationSession?.agentStatus ?? activeDisplaySession?.agentStatus}
currentTool={activeConversationSession?.currentTool ?? activeDisplaySession?.currentTool}
displayTool={activeConversationSession?.displayTool ?? activeDisplaySession?.displayTool}
runtimeControlState={(activeConversationSession ?? activeDisplaySession)?.runtimeControlState}
canStop={(activeConversationSession ?? activeDisplaySession)?.canStop}
savedOrgs={savedOrgsList ?? null}
activeSavedOrg={activeSavedOrg ?? null}
selectedOrgId={activeSession.orgId ?? activeSavedOrg ?? null}
lockedMode={isSessionConfigLocked(
activeConversationSession ?? activeDisplaySession ?? activeSession,
Math.max(activeConversationMessageCount, messages.length),
)}
activeAgentCount={activeAgentCountFor(activeRoleWorkItems, visibleAgentSessions)}
contextTokens={activeConversationSession?.contextTokens ?? activeDisplaySession?.contextTokens}
contextWindow={activeConversationSession?.contextWindow ?? activeDisplaySession?.contextWindow}
contextRemainingPct={activeConversationSession?.contextRemainingPct ?? activeDisplaySession?.contextRemainingPct ?? 100}
onSend={onComposerSend}
onModeChange={(mode, profile, orgId) => onSessionConfigChange?.(activeSession.taskId, mode, profile, orgId)}
onTaskAgentChange={(preferredAgent) => onSessionTaskAgentChange?.(activeSession.taskId, preferredAgent)}
onContinueInNewChat={onContinueInNewChat}
onSavedOrgsRefresh={onSavedOrgsList}
onSavedOrgLoad={onSavedOrgLoad}
onStop={onStop}
/>
</>
)}
{/* Session view — Agents tab */}
{activeSession && !isSecretary && !showMultiSessionGrid && panelTab === 'agents' && canShowAgentsTab && (
(visibleAgentSessions.length > 0 || hasRoleWorkItems) ? (
<AgentWorkPanel
sessions={visibleAgentSessions}
roleWorkItems={activeRoleWorkItems}
isCompanyRuntime={isCompanyRuntime}
agents={agents}
onOpenChildDetail={onOpenChildDetail}
onOpenExecutionPanel={onOpenExecutionPanel}
/>
) : (
<div className="ctx-empty">
<span className="ctx-empty-icon">&#x1F9E0;</span>
<span>No child agents have started for this session yet</span>
</div>
)
)}
{/* Session view — Info tab.
Reorganised into semantic cards (Overview / Identity /
Timing) instead of a flat key-value list. Internal-only
debug fields (projection id, channel id) are tucked into
a collapsed "Developer details" section so they don't
dominate the user-facing summary. */}
{activeSession && !isSecretary && !showMultiSessionGrid && panelTab === 'info' && taskForInfo && (
<InfoTabView
task={taskForInfo}
agents={agents}
roleLabel={getWorkItemRoleLabel(taskForInfo)}
/>
)}
{/* Comms tab */}
{panelTab === 'comms' && onCommsRefresh && onCommsReadMessage && (
<div style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
<CommsPanel
state={commsState ?? null}
message={commsMessage ?? null}
onRefresh={onCommsRefresh}
onReadMessage={onCommsReadMessage}
embedded
/>
</div>
)}
{panelTab === 'team' && canShowTeamTab && (
<div style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
<ProjectCockpit
orgInfoData={orgInfoData ?? null}
commsState={commsState ?? null}
onStopRun={onTeamStopRun}
embedded
/>
</div>
)}
</div>
</>
)}
</div>
</>
)
}