import { useEffect, useMemo, useRef, useState } from 'react'
import {
SCHEDULED_TAB_ID,
SETTINGS_TAB_ID,
TERMINAL_TAB_PREFIX,
useTabStore,
type TabType,
} from '../stores/tabStore'
import { useSessionStore } from '../stores/sessionStore'
import { useChatStore } from '../stores/chatStore'
import { useCLITaskStore } from '../stores/cliTaskStore'
import { useTeamStore } from '../stores/teamStore'
import { useWorkspacePanelStore } from '../stores/workspacePanelStore'
import {
TERMINAL_PANEL_DEFAULT_HEIGHT,
TERMINAL_PANEL_MAX_HEIGHT,
TERMINAL_PANEL_MIN_HEIGHT,
useTerminalPanelStore,
} from '../stores/terminalPanelStore'
import { useTranslation } from '../i18n'
import { MessageList } from '../components/chat/MessageList'
import { ChatInput } from '../components/chat/ChatInput'
import { ComputerUsePermissionModal } from '../components/chat/ComputerUsePermissionModal'
import { SessionTaskBar } from '../components/chat/SessionTaskBar'
import { WorkspacePanel } from '../components/workspace/WorkspacePanel'
import { TeamStatusBar } from '../components/teams/TeamStatusBar'
import { TerminalSettings } from './TerminalSettings'
import type { SessionListItem } from '../types/session'
import { useMobileViewport } from '../hooks/useMobileViewport'
import { isTauriRuntime } from '../lib/desktopRuntime'
import type { ActiveGoalState } from '../types/chat'
import { Target } from 'lucide-react'
const TASK_POLL_INTERVAL_MS = 1000
const WORKSPACE_RESIZE_STEP = 32
const TERMINAL_RESIZE_STEP = 24
const CHAT_COLUMN_WITH_WORKSPACE_CLASS =
'min-w-[320px] flex-1 border-r border-[var(--color-border)] bg-[var(--color-surface)]'
function isSessionTabState(activeTabId: string | null, activeTabType: TabType | null | undefined) {
if (!activeTabId) return false
if (activeTabType === 'session') return true
if (activeTabType) return false
return activeTabId !== SETTINGS_TAB_ID &&
activeTabId !== SCHEDULED_TAB_ID &&
!activeTabId.startsWith(TERMINAL_TAB_PREFIX)
}
function getSessionTerminalCwd(session: SessionListItem | undefined) {
if (!session) return undefined
if (session.workDir && session.workDirExists !== false) return session.workDir
return session.projectPath || undefined
}
function WorkspaceResizeHandle() {
const t = useTranslation()
const width = useWorkspacePanelStore((state) => state.width)
const setWidth = useWorkspacePanelStore((state) => state.setWidth)
const [dragState, setDragState] = useState<{ startX: number; startWidth: number } | null>(null)
const dragStateRef = useRef(dragState)
useEffect(() => {
dragStateRef.current = dragState
}, [dragState])
useEffect(() => {
if (!dragState) return
const handlePointerMove = (event: PointerEvent) => {
const current = dragStateRef.current
if (!current) return
setWidth(current.startWidth + current.startX - event.clientX)
}
const handlePointerUp = () => {
setDragState(null)
}
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
return () => {
document.body.style.cursor = ''
document.body.style.userSelect = ''
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}
}, [dragState, setWidth])
return (
{
if (event.button !== 0) return
event.preventDefault()
setDragState({ startX: event.clientX, startWidth: width })
}}
onKeyDown={(event) => {
if (event.key === 'ArrowLeft') {
event.preventDefault()
setWidth(width + WORKSPACE_RESIZE_STEP)
}
if (event.key === 'ArrowRight') {
event.preventDefault()
setWidth(width - WORKSPACE_RESIZE_STEP)
}
}}
className="group relative z-10 flex w-2 shrink-0 cursor-col-resize items-stretch justify-center bg-[var(--color-surface)] outline-none focus-visible:bg-[var(--color-surface-container)]"
>
)
}
function TerminalResizeHandle() {
const t = useTranslation()
const height = useTerminalPanelStore((state) => state.height)
const setHeight = useTerminalPanelStore((state) => state.setHeight)
const [dragState, setDragState] = useState<{ startY: number; startHeight: number } | null>(null)
const dragStateRef = useRef(dragState)
useEffect(() => {
dragStateRef.current = dragState
}, [dragState])
useEffect(() => {
if (!dragState) return
const handlePointerMove = (event: PointerEvent) => {
const current = dragStateRef.current
if (!current) return
setHeight(current.startHeight + current.startY - event.clientY)
}
const handlePointerUp = () => {
setDragState(null)
}
document.body.style.cursor = 'row-resize'
document.body.style.userSelect = 'none'
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
window.addEventListener('pointercancel', handlePointerUp)
return () => {
document.body.style.cursor = ''
document.body.style.userSelect = ''
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
window.removeEventListener('pointercancel', handlePointerUp)
}
}, [dragState, setHeight])
return (
{
if (event.button !== 0) return
event.preventDefault()
setDragState({ startY: event.clientY, startHeight: height })
}}
onKeyDown={(event) => {
if (event.key === 'ArrowUp') {
event.preventDefault()
setHeight(height + TERMINAL_RESIZE_STEP)
}
if (event.key === 'ArrowDown') {
event.preventDefault()
setHeight(height - TERMINAL_RESIZE_STEP)
}
if (event.key === 'Home') {
event.preventDefault()
setHeight(TERMINAL_PANEL_MIN_HEIGHT)
}
if (event.key === 'End') {
event.preventDefault()
setHeight(TERMINAL_PANEL_MAX_HEIGHT)
}
}}
onDoubleClick={() => setHeight(TERMINAL_PANEL_DEFAULT_HEIGHT)}
className="group flex h-2.5 shrink-0 cursor-row-resize items-center bg-[var(--color-surface)] outline-none focus-visible:bg-[var(--color-surface-container)]"
>
)
}
function GoalStatusPanel({
goal,
isRunning,
compact,
}: {
goal: ActiveGoalState
isRunning: boolean
compact: boolean
}) {
const t = useTranslation()
const stateLabel = goal.action === 'completed'
? t('chat.activeGoal.completed')
: goal.action === 'paused' || goal.status === 'paused'
? t('chat.activeGoal.paused')
: isRunning && goal.status !== 'complete'
? t('chat.activeGoal.running')
: t('chat.activeGoal.active')
return (
{t('chat.activeGoal.title')}
{stateLabel}
{goal.objective && (
{goal.objective}
)}
{(goal.budget || goal.continuations || goal.elapsed) && (
{goal.budget && (
{t('chat.activeGoal.budget', { value: goal.budget })}
)}
{goal.continuations && (
{t('chat.activeGoal.continuations', { value: goal.continuations })}
)}
{goal.elapsed && (
{t('chat.activeGoal.elapsed', { value: goal.elapsed })}
)}
)}
)
}
export function ActiveSession() {
const isMobileLayout = useMobileViewport() && !isTauriRuntime()
const activeTabId = useTabStore((s) => s.activeTabId)
const activeTabType = useTabStore((s) => s.tabs.find((tab) => tab.sessionId === s.activeTabId)?.type ?? null)
const sessions = useSessionStore((s) => s.sessions)
const connectToSession = useChatStore((s) => s.connectToSession)
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
const pendingComputerUsePermission = sessionState?.pendingComputerUsePermission ?? null
const fetchSessionTasks = useCLITaskStore((s) => s.fetchSessionTasks)
const trackedTaskSessionId = useCLITaskStore((s) => s.sessionId)
const hasIncompleteTasks = useCLITaskStore((s) => s.tasks.some((task) => task.status !== 'completed'))
const chatState = sessionState?.chatState ?? 'idle'
const activeGoal = sessionState?.activeGoal ?? null
const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 }
const session = sessions.find((s) => s.id === activeTabId)
const memberInfo = useTeamStore((s) => activeTabId ? s.getMemberBySessionId(activeTabId) : null)
const activeTeam = useTeamStore((s) => s.activeTeam)
const isMemberSession = !!memberInfo
const showWorkspacePanel = useWorkspacePanelStore((state) =>
activeTabId && isSessionTabState(activeTabId, activeTabType) && !isMemberSession && !isMobileLayout
? state.isPanelOpen(activeTabId)
: false,
)
const showTerminalPanel = useTerminalPanelStore((state) =>
activeTabId && isSessionTabState(activeTabId, activeTabType) && !isMemberSession && !isMobileLayout
? state.isPanelOpen(activeTabId)
: false,
)
const terminalPanelHeight = useTerminalPanelStore((state) => state.height)
useEffect(() => {
if (activeTabId && !isMemberSession) {
connectToSession(activeTabId)
}
}, [activeTabId, isMemberSession, connectToSession])
useEffect(() => {
if (!activeTabId || isMemberSession) return
const shouldPollTasks =
chatState !== 'idle' ||
(trackedTaskSessionId === activeTabId && hasIncompleteTasks)
if (!shouldPollTasks) return
void fetchSessionTasks(activeTabId)
const timer = setInterval(() => {
void fetchSessionTasks(activeTabId)
}, TASK_POLL_INTERVAL_MS)
return () => clearInterval(timer)
}, [
activeTabId,
isMemberSession,
chatState,
trackedTaskSessionId,
hasIncompleteTasks,
fetchSessionTasks,
])
const t = useTranslation()
const messages = sessionState?.messages ?? []
const streamingText = sessionState?.streamingText ?? ''
const isEmpty = messages.length === 0 && !streamingText && (session?.messageCount ?? 0) === 0
const isActive = chatState !== 'idle'
const totalTokens = tokenUsage.input_tokens + tokenUsage.output_tokens
const lastUpdated = useMemo(() => {
if (!session?.modifiedAt) return ''
const diff = Date.now() - new Date(session.modifiedAt).getTime()
if (diff < 60000) return t('session.timeJustNow')
if (diff < 3600000) return t('session.timeMinutes', { n: Math.floor(diff / 60000) })
if (diff < 86400000) return t('session.timeHours', { n: Math.floor(diff / 3600000) })
return t('session.timeDays', { n: Math.floor(diff / 86400000) })
}, [session?.modifiedAt, t])
if (!activeTabId) return null
return (
{isMemberSession && (
{memberInfo?.status === 'running' && (
)}
{memberInfo?.status === 'completed' && (
check_circle
)}
smart_toy
{memberInfo?.role}
{activeTeam && (
@ {activeTeam.name}
)}
{t('teams.memberSessionHint')}
)}
{isEmpty ? (
{isMemberSession ? (
<>
smart_toy
{memberInfo?.status === 'running'
? `${memberInfo.role} ${t('teams.working')}`
: t('teams.noMessages')}
>
) : (
<>
{t('empty.title')}
{t('empty.subtitle')}
>
)}
) : (
<>
{!isMemberSession && !isMobileLayout && (
{session?.title || t('session.untitled')}
{isActive && (
{t('session.active')}
)}
{totalTokens > 0 && (
<>
·
{totalTokens.toLocaleString()} t
>
)}
{lastUpdated && (
<>
·
{t('session.lastUpdated', { time: lastUpdated })}
>
)}
{!showWorkspacePanel && session?.messageCount !== undefined && session.messageCount > 0 && (
<>
·
{t('session.messages', { count: session.messageCount })}
>
)}
{session?.workDirExists === false && (
warning
{t('session.workspaceUnavailable', { dir: session.workDir || 'directory no longer exists' })}
)}
)}
{activeGoal && (
)}
>
)}
{!isMemberSession &&
}
{showTerminalPanel && activeTabId ? (
{
useTerminalPanelStore.getState().closePanel(activeTabId)
useTabStore.getState().openTerminalTab(getSessionTerminalCwd(session))
}}
onClose={() => useTerminalPanelStore.getState().closePanel(activeTabId)}
/>
) : null}
{showWorkspacePanel ? (
<>
>
) : null}
{!isMemberSession && activeTabId ? (
) : null}
)
}