src/assistant/AssistantPanel.tsx
import {lazy,Suspense,useEffect,useMemo,useRef,useState,type ReactNode} from 'react';
import {useChat} from '@ai-sdk/react';
import {DefaultChatTransport} from 'ai';
import {AnimatePresence,motion,MotionConfig,useReducedMotion} from 'motion/react';
import {Badge,Button,Heading,IconButton,Select,Text,TextArea,Tooltip,Checkbox,SegmentedControlRadioGroup} from '@whop/react/components';
import {Activity,ArrowUp,ArrowUpRight,ChartNoAxesCombined,Check,CheckCircle2,ChevronDown,ChevronRight,Clock3,Copy,Database,ExternalLink,Info,Maximize2,MessageSquare,Minimize2,Plug,Plus,RefreshCw,Send,Sparkles,Square,X,XCircle} from 'lucide-react';
import {CoordinatorJobs} from '../coordinator/CoordinatorJobs';
import {ConnectionsPanel} from '../connections/ConnectionsPanel';
import {navigateSection} from '../blackrock/navigation';
const ResearchReport=lazy(()=>import('./ResearchReport').then(module=>({default:module.ResearchReport})));
import {friendlyError,hydrateMessages,messageText,pendingTask,updateTasks,type SavedMessage,type WorkspaceMessage} from './workspace-state';
import type {AssistantSource,AssistantTask,AssistantTaskStatus,WorkspaceEvent} from '../../shared/assistant';
import './assistant.css';
import {stopAssistantResponse} from './cancellation';
export type ScreenContext={mode:'blackrock'|'palantir';section:string;label:string;entity?:{kind:string;key:string;name:string}|null};
type Model={id:string;name:string;efforts:string[];defaultEffort:string};
type ConnectionStatus={connected:boolean;auth?:string|null;models:Model[];busy:boolean;orchestrator?:string};
type WorkspaceTab='chat'|'activity'|'reports'|'jobs';
const conversationKey='rs-research-assistant-conversation';
async function request<T=any>(path:string,options:RequestInit={}):Promise<T>{const r=await fetch('/api/assistant/'+path,options);const body=await r.json();if(!r.ok)throw Error(typeof body.error==='string'?body.error:'Assistant unavailable');return body;}
function inline(text:string){return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\(\/api\/[^)\s]+\))/g).map((part,i)=>{const link=part.match(/^\[([^\]]+)\]\((\/api\/[^)\s]+)\)$/);return link?<a key={i} href={link[2]} target="_blank" rel="noreferrer">{link[1]}</a>:part.startsWith('**')?<strong key={i}>{part.slice(2,-2)}</strong>:part.startsWith('`')?<code key={i}>{part.slice(1,-1)}</code>:part;});}
function Answer({text}:{text:string}){return <div className="assistant-answer">{text.split('\n').map((line,i)=>/^#{1,4} /.test(line)?<p className="assistant-answer-title" key={i}>{inline(line.replace(/^#+ /,''))}</p>:<div key={i}>{inline(line)||<br/>}</div>)}</div>}
function CopyMessage({text}:{text:string}){const [copied,setCopied]=useState(false),[error,setError]=useState(false);return <Tooltip content={error?'Copy unavailable':copied?'Copied':'Copy answer'}><IconButton size="1" variant="ghost" color="gray" aria-label={error?'Copy unavailable':'Copy answer'} onClick={()=>void navigator.clipboard.writeText(text).then(()=>{setCopied(true);setError(false);setTimeout(()=>setCopied(false),1500)}).catch(()=>setError(true))}>{copied?<Check size={14}/>:<Copy size={14}/>}</IconButton></Tooltip>}
function stateLabel(status:AssistantTaskStatus){return ({queued:'Queued',running:'Working',completed:'Completed',failed:'Failed',interrupted:'Interrupted'})[status];}
function stateColor(status:AssistantTaskStatus){return status==='completed'?'green':status==='failed'?'red':status==='interrupted'?'amber':status==='running'?'blue':'gray';}
function time(value:number){return new Date(value).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});}
export function AssistantPanel({open,onClose,context,draftRequest}:{open:boolean;onClose:()=>void;context:ScreenContext;draftRequest?:{id:number;prompt:string}|null}){
const [expanded,setExpanded]=useState(false),[draft,setDraft]=useState(''),[status,setStatus]=useState<ConnectionStatus|null>(null),[loading,setLoading]=useState(false),[localError,setLocalError]=useState(''),[model,setModel]=useState('gpt-6.1-sol'),[effort,setEffort]=useState('high'),[includeContext,setIncludeContext]=useState(true),[unread,setUnread]=useState(false),[liveSources,setLiveSources]=useState<AssistantSource[]>([]);
const [tab,setTab]=useState<WorkspaceTab>('chat'),[tasks,setTasks]=useState<AssistantTask[]>([]),[selectedTaskId,setSelectedTaskId]=useState<string|null>(null),[detailsOpen,setDetailsOpen]=useState(false),[prepared,setPrepared]=useState(''),[connectionsOpen,setConnectionsOpen]=useState(false),[handoffTask,setHandoffTask]=useState<string|undefined>();
const conversationId=useRef<string|null>(localStorage.getItem(conversationKey)),feed=useRef<HTMLDivElement>(null),textarea=useRef<HTMLTextAreaElement>(null),nearBottom=useRef(true),wasOpen=useRef(false),lastQuestion=useRef(''),sending=useRef(false),hydrationRevision=useRef(0),activeTaskId=useRef<string|null>(null),refreshAfterFinish=useRef<()=>void>(()=>{});
const reduced=useReducedMotion();
const transport=useMemo(()=>new DefaultChatTransport<WorkspaceMessage>({api:'/api/assistant/chat-sdk',prepareSendMessagesRequest:({body})=>({body:body??{}})}),[]);
const receive=(event:WorkspaceEvent)=>{
if(event.type==='conversation'){conversationId.current=event.conversationId;localStorage.setItem(conversationKey,event.conversationId);activeTaskId.current=event.taskId;setSelectedTaskId(event.taskId);}
if(event.type==='source')setLiveSources(old=>[...old.filter(source=>source.url!==event.source.url),event.source]);
setTasks(old=>updateTasks(old,event));
};
const chat=useChat<WorkspaceMessage>({transport,throttle:50,resume:false,
onData:part=>{if(part.type==='data-workspace')receive(part.data);},
onError:error=>{setLocalError(friendlyError(error));setDraft(old=>old||lastQuestion.current);},
onFinish:({isAbort,isDisconnect,isError})=>{if(isDisconnect)setLocalError('Connection interrupted. Check Activity for the saved result before sending again.');if(isAbort)setLocalError('Response stopped. Coordinated jobs and gameplay missions continue; use their own cancellation controls.');if(!isError&&!isDisconnect&&!isAbort)setLiveSources([]);refreshAfterFinish.current();},
});
const {messages,setMessages}=chat,working=chat.status==='submitted'||chat.status==='streaming';
const models=status?.models??[],chosen=models.find(item=>item.id===model),selectedTask=tasks.find(task=>task.id===selectedTaskId)??tasks[0],activeTask=tasks.find(pendingTask),reports=tasks.flatMap(task=>task.reports.map(report=>({report,task}))),progress=activeTask?.events.at(-1)?.label??(chat.status==='submitted'?'Connecting to your agent…':'Writing answer…');
const canSend=!!status?.connected&&!!chosen?.efforts.includes(effort)&&!working&&!activeTask;
const refreshStatus=async()=>{setLoading(true);try{setStatus(await request<ConnectionStatus>('status'));}catch(error){setStatus(null);setLocalError((error as Error).message)}finally{setLoading(false)}};
const refreshTasks=async(id=conversationId.current)=>{if(!id)return [];const result=await request<{tasks:AssistantTask[]}>('tasks?conversationId='+encodeURIComponent(id));if(id===conversationId.current)setTasks(old=>[...new Map([...old,...result.tasks].map(task=>{const existing=old.find(row=>row.id===task.id);return [task.id,existing&&existing.updatedAt>task.updatedAt?existing:task]})).values()].sort((a,b)=>b.createdAt-a.createdAt).slice(0,50));return result.tasks;};
const loadConversation=async(id=conversationId.current)=>{if(!id)return;const revision=++hydrationRevision.current;const result=await request<{messages:SavedMessage[]}>('conversation?id='+encodeURIComponent(id));if(id===conversationId.current&&revision===hydrationRevision.current&&!sending.current)setMessages(hydrateMessages(result.messages));};
refreshAfterFinish.current=()=>{void refreshTasks().catch(error=>setLocalError(error.message));};
useEffect(()=>{if(!draftRequest)return;if(working||draft.trim())setPrepared(draftRequest.prompt);else setDraft(draftRequest.prompt);setTab('chat');requestAnimationFrame(()=>textarea.current?.focus());},[draftRequest]);
useEffect(()=>{if(open&&!wasOpen.current){void refreshStatus();void refreshTasks().catch(error=>setLocalError(error.message));if(!working&&!messages.length)void loadConversation().catch(error=>setLocalError(error.message));}wasOpen.current=open;},[open]);
useEffect(()=>{if(!open||!activeTask||working)return;let cancelled=false;const id=conversationId.current;const timer=setInterval(()=>{if(document.visibilityState!=='visible')return;void refreshTasks(id).then(next=>{if(!cancelled&&!next.some(pendingTask))void loadConversation(id).catch(error=>setLocalError(error.message));}).catch(error=>{if(!cancelled)setLocalError(error.message)});},2500);return()=>{cancelled=true;clearInterval(timer)}},[open,activeTask?.id,working]);
useEffect(()=>{if(!open||tab!=='chat'||!messages.length&&!working)return;if(nearBottom.current)feed.current?.scrollTo({top:feed.current.scrollHeight,behavior:'instant'});else setUnread(true)},[messages,progress,working,open,tab]);
const newChat=()=>{if(working||activeTask)return;hydrationRevision.current++;conversationId.current=null;localStorage.removeItem(conversationKey);setMessages([]);setTasks([]);setSelectedTaskId(null);setLiveSources([]);setLocalError('');chat.clearError();setDraft('');setPrepared('');setTab('chat');nearBottom.current=true;textarea.current?.focus();};
const stop=async()=>{
const id=conversationId.current;
try{await stopAssistantResponse(()=>{
const stopped=chat.stop();
setMessages(old=>old.map((message,index)=>index===old.length-1&&message.role==='assistant'?{...message,metadata:{...message.metadata,status:'interrupted'}}:message));
return stopped;
},signal=>id?request('cancel',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({conversationId:id}),signal}):Promise.resolve());}
catch(error){setLocalError('Could not confirm server cancellation: '+(error as Error).message)}
finally{void refreshTasks(id).catch(error=>setLocalError(error.message));}
};
const send=async(question=draft)=>{if(!question.trim()||!canSend||sending.current)return;const text=question.trim();sending.current=true;hydrationRevision.current++;lastQuestion.current=text;setDraft('');setTab('chat');setLocalError('');chat.clearError();setLiveSources([]);nearBottom.current=true;
try{await chat.sendMessage({text,metadata:{createdAt:Date.now()}},{body:{conversationId:conversationId.current??undefined,requestId:crypto.randomUUID(),message:text,model,effort,context:includeContext?{...context,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone}:null}});}catch(error){setLocalError(friendlyError(error as Error));setDraft(text)}finally{sending.current=false;void refreshTasks().catch(error=>setLocalError(error.message));}
};
const openConnections=(task?:string)=>{setHandoffTask(task);setConnectionsOpen(true)};
const reviewCommand=()=>{navigateSection('missions');onClose()};
return <MotionConfig reducedMotion="user"><AnimatePresence initial={false}>{open&&<motion.aside key="assistant" className={`research-assistant assistant-view-${tab} ${expanded?'assistant-expanded':''}`} aria-label="Research assistant" initial={{opacity:0,x:reduced?0:24}} animate={{opacity:1,x:0}} exit={{opacity:0,x:reduced?0:24}} transition={{duration:.2,ease:'easeOut'}} onKeyDown={event=>{if(event.key==='Escape'){event.stopPropagation();onClose()}}}>
<div className="assistant-heading"><div><Sparkles size={18} className="assistant-spark"/><div><Heading as="h2" size="3">Assistant</Heading><Text as="div" size="1" color="gray">Research · plan · review</Text></div></div><div className="assistant-heading-actions"><Tooltip content="New conversation"><IconButton aria-label="New conversation" variant="ghost" color="gray" size="1" disabled={working||!!activeTask} onClick={newChat}><Plus size={17}/></IconButton></Tooltip><Tooltip content={expanded?'Compact panel':'Expand panel'}><IconButton aria-label={expanded?'Compact assistant':'Expand assistant'} className="assistant-expand-button" variant="ghost" color="gray" size="1" onClick={()=>setExpanded(!expanded)}>{expanded?<Minimize2 size={16}/>:<Maximize2 size={16}/>}</IconButton></Tooltip><Tooltip content="Close assistant"><IconButton aria-label="Close assistant" variant="ghost" color="gray" size="1" onClick={onClose}><X size={17}/></IconButton></Tooltip></div></div>
<div className="assistant-connection"><span className={`assistant-status-dot ${status?.connected?'connected':''}`}/><Text size="1" color="gray">{loading?'Checking connection…':status?.connected?'Codex connected':'Codex not connected'}</Text><Tooltip content="Runtime and account details"><IconButton size="1" variant="ghost" color="gray" aria-label="Runtime and account details" aria-expanded={detailsOpen} onClick={()=>setDetailsOpen(!detailsOpen)}><Info size={13}/></IconButton></Tooltip><Button size="1" variant="ghost" onClick={()=>openConnections()}><Plug size={13}/>Connections</Button></div>
{detailsOpen&&<div className="assistant-runtime"><dl><div><dt>Orchestration</dt><dd>Mastra · local workflow</dd></div><div><dt>Agent runtime</dt><dd>Codex app-server</dd></div><div><dt>Model usage</dt><dd>Your signed-in Codex account</dd></div><div><dt>Gameplay</dt><dd>Review in Command</dd></div></dl><Button size="1" variant="ghost" disabled={loading||working} onClick={()=>void refreshStatus()}><RefreshCw size={12}/>Check connection</Button></div>}
<div className="assistant-tabs"><SegmentedControlRadioGroup.Root aria-label="Assistant workspace view" value={tab} onValueChange={value=>setTab(value as WorkspaceTab)}><SegmentedControlRadioGroup.Item value="chat"><MessageSquare size={13}/>Chat</SegmentedControlRadioGroup.Item><SegmentedControlRadioGroup.Item value="activity"><Activity size={13}/>Activity{tasks.length>0&&<span className="assistant-tab-count">{tasks.length}</span>}</SegmentedControlRadioGroup.Item><SegmentedControlRadioGroup.Item value="jobs"><Plug size={13}/>Jobs</SegmentedControlRadioGroup.Item><SegmentedControlRadioGroup.Item value="reports"><ChartNoAxesCombined size={13}/>Reports{reports.length>0&&<span className="assistant-tab-count">{reports.length}</span>}</SegmentedControlRadioGroup.Item></SegmentedControlRadioGroup.Root></div>
{includeContext&&tab==='chat'&&<div className="assistant-context"><Database size={14}/><div><Text size="1" weight="medium">{context.mode==='blackrock'?'RS-BLACKROCK':'RS-PALANTIR'}</Text><Text as="div" size="1" color="gray">{context.entity?.name??context.label??context.section}</Text>{context.entity&&context.label&&context.label!==context.entity.name&&<Text as="div" size="1" color="gray">{context.label}</Text>}</div><Badge size="1" color="blue" variant="soft">Current view</Badge></div>}
{(working||activeTask)&&<div className="assistant-running"><span className="assistant-live-pulse"/><Text size="1">{working?progress:'Task is running. Checking saved progress…'}</Text><Button size="1" variant="ghost" onClick={()=>{setSelectedTaskId(activeTask?.id??activeTaskId.current);setTab('activity')}}>View</Button><Tooltip content="Stops this response; jobs and gameplay continue"><IconButton size="1" variant="soft" color="gray" aria-label="Stop response" onClick={()=>void stop()}><Square size={12}/></IconButton></Tooltip></div>}
<div className="assistant-feed" ref={feed} onScroll={()=>{const el=feed.current!;nearBottom.current=el.scrollHeight-el.scrollTop-el.clientHeight<80;if(nearBottom.current)setUnread(false)}} aria-label={tab==='chat'?'Assistant conversation':tab==='activity'?'Assistant task activity':'Assistant reports'}>
{tab==='chat'&&<>
{!messages.length&&!working&&<motion.div className="assistant-welcome" initial={{opacity:0,y:reduced?0:8}} animate={{opacity:1,y:0}}><span className="assistant-welcome-icon"><Sparkles size={25}/></span><Heading size="5" as="h3">Your game, in context.</Heading><Text as="p" size="2" color="gray">Research the economy, inspect your team, or prepare a plan to review in Command.</Text><div className="assistant-starters">{(context.mode==='blackrock'?['Are prices rising over time? Show the evidence.','Compare steel bars and cannonballs.','What supplies does my team need?']:['What are my saved swarms doing, and what is unknown?','Help me preview a Wilderness-approved squad mission.','Explain the limits of observed player gear.']).map(question=><Button key={question} size="2" variant="surface" color="gray" disabled={!canSend} onClick={()=>void send(question)}>{question}<ArrowUpRight size={14}/></Button>)}</div><Text as="p" size="1" color="gray">Uses your Codex allowance. Relevant game data and recent chat are sent when you ask. Conversations and task results are saved locally.</Text></motion.div>}
{messages.map((message,index)=>{const text=messageText(message),metadata=message.metadata,isLive=working&&index===messages.length-1&&message.role==='assistant';return <div key={message.id} className={`assistant-message assistant-${message.role}`}><div className="assistant-message-label"><Text size="1" weight="medium">{message.role==='user'?'You':'Assistant'}</Text>{message.role==='assistant'&&<Text size="0" color="gray">{(metadata?.model??model).replace('gpt-','GPT-')} · {metadata?.effort??effort}</Text>}{metadata?.createdAt&&<time dateTime={new Date(metadata.createdAt).toISOString()}>{time(metadata.createdAt)}</time>}</div>{text?<Answer text={text}/>:isLive?<div className="assistant-thinking" aria-label="Preparing response"><span/><span/><span/></div>:null}{message.role==='assistant'&&<SourceLinks sources={metadata?.sources?.length?metadata.sources:isLive?liveSources:[]}/>}<div className="assistant-message-footer">{metadata?.status&&metadata.status!=='completed'&&<Badge color={metadata.status==='failed'?'red':'amber'} size="1">{metadata.status==='interrupted'?'Interrupted':metadata.status}</Badge>}{message.role==='assistant'&&text&&<CopyMessage text={text}/>}</div></div>})}
{working&&messages.at(-1)?.role!=='assistant'&&<div className="assistant-thinking" aria-label="Preparing response"><span/><span/><span/></div>}
{!working&&tasks[0]&&<div className="assistant-next-actions"><Button size="1" variant="soft" onClick={()=>{setSelectedTaskId(tasks[0].id);setTab('activity')}}><Activity size={13}/>View task activity</Button>{tasks[0].reports.length>0&&<Button size="1" variant="soft" onClick={()=>setTab('reports')}><ChartNoAxesCombined size={13}/>Open reports</Button>}</div>}
</>}
{tab==='activity'&&<><div className="assistant-section-heading"><div><Heading as="h3" size="3">Task activity</Heading><Text as="p" size="1" color="gray">Saved steps and results for this conversation.</Text></div><Tooltip content="Refresh saved tasks"><IconButton aria-label="Refresh saved tasks" size="1" variant="ghost" onClick={()=>void refreshTasks().catch(error=>setLocalError(error.message))}><RefreshCw size={14}/></IconButton></Tooltip></div>{!tasks.length?<EmptyState icon={<Activity size={23}/>} title="Your next question starts a task" text="Research calls, sources and generated reports will appear here. No task runs until you ask."/>:<><div className="assistant-task-list" role="group" aria-label="Saved assistant tasks">{tasks.map(task=><Button key={task.id} size="2" variant={selectedTask?.id===task.id?'soft':'surface'} color={selectedTask?.id===task.id?'blue':'gray'} aria-pressed={selectedTask?.id===task.id} onClick={()=>setSelectedTaskId(task.id)}><span><strong>{task.question}</strong><small>{new Date(task.createdAt).toLocaleString()}</small></span><Badge size="1" color={stateColor(task.status)}>{stateLabel(task.status)}</Badge></Button>)}</div>{selectedTask&&<TaskDetail task={selectedTask} onReports={()=>setTab('reports')} onReview={reviewCommand} onDelegate={()=>openConnections(selectedTask.question)}/>}</>}</>}
{tab==='jobs'&&<CoordinatorJobs conversationId={conversationId.current}/>}
{tab==='reports'&&<><div className="assistant-section-heading"><div><Heading as="h3" size="3">Research reports</Heading><Text as="p" size="1" color="gray">Interactive evidence from your research tasks.</Text></div><Badge color="gray">{reports.length}</Badge></div>{!reports.length?<EmptyState icon={<ChartNoAxesCombined size={23}/>} title="Reports follow the evidence" text="Ask about an item, price history or the economy. Supported research results become charts and tables here."/>:<div className="assistant-report-list">{reports.map(({report,task})=><section className="assistant-report-entry" key={`${task.id}:${report.id}`}><Button size="1" variant="ghost" color="gray" className="assistant-report-task" onClick={()=>{setSelectedTaskId(task.id);setTab('activity')}}><Activity size={12}/><span>{task.question}</span><ChevronRight size={12}/></Button><Suspense fallback={<Text size="1" color="gray">Loading report…</Text>}><ResearchReport report={report} onNavigate={()=>{if(window.matchMedia('(max-width:1050px)').matches)onClose()}}/></Suspense></section>)}</div>}</>}
</div>
{unread&&tab==='chat'&&<Button size="1" variant="soft" className="assistant-jump" onClick={()=>{nearBottom.current=true;setUnread(false);feed.current?.scrollTo({top:feed.current.scrollHeight,behavior:reduced?'instant':'smooth'})}}><ChevronDown size={14}/>Latest response</Button>}
{tab==='chat'?<div className="assistant-composer-zone">{localError&&<div className="assistant-error" role="alert"><Text size="1">{localError}</Text><IconButton size="1" variant="ghost" aria-label="Dismiss assistant error" onClick={()=>{setLocalError('');chat.clearError()}}><X size={12}/></IconButton></div>}{!loading&&!status?.connected&&<Text as="p" size="1" color="gray">Sign in with <code>codex login</code> on this computer, then <Button size="1" variant="ghost" onClick={()=>void refreshStatus()}>reconnect</Button>.</Text>}{status?.connected&&!chosen&&<Text as="p" size="1" color="amber">Your selected model is unavailable. Choose one below.</Text>}
<div className="assistant-model-controls"><Select.Root value={model} onValueChange={value=>{setModel(value);const next=models.find(item=>item.id===value);setEffort(next?.efforts.includes('high')?'high':next?.defaultEffort??'medium')}} disabled={working} size="1"><Select.Trigger aria-label="Assistant model" variant="ghost" placeholder="Choose model"/><Select.Content position="popper">{models.map(item=><Select.Item key={item.id} value={item.id}>{item.name}</Select.Item>)}</Select.Content></Select.Root><Select.Root value={effort} onValueChange={setEffort} disabled={working||!chosen} size="1"><Select.Trigger aria-label="Reasoning effort" variant="ghost"/><Select.Content position="popper">{(chosen?.efforts??['high']).map(value=><Select.Item key={value} value={value}>{value[0].toUpperCase()+value.slice(1)}</Select.Item>)}</Select.Content></Select.Root></div>
{prepared&&<div className="assistant-prepared"><Text size="1">A new research question is ready. Your draft is preserved.</Text><Button size="1" disabled={working} onClick={()=>{setDraft(prepared);setPrepared('');setTab('chat');textarea.current?.focus()}}>Use question</Button><Button size="1" variant="ghost" onClick={()=>setPrepared('')}>Dismiss</Button></div>}<form className="assistant-composer" onSubmit={event=>{event.preventDefault();void send()}}><TextArea ref={textarea} size="2" variant="surface" aria-label="Ask the research assistant" placeholder="Ask about the game or prepare a plan…" value={draft} maxLength={4000} disabled={working} onChange={event=>setDraft(event.target.value)} onKeyDown={event=>{if(event.key==='Enter'&&!event.shiftKey&&!event.nativeEvent.isComposing){event.preventDefault();void send()}}}/><div className="assistant-composer-actions"><Text as="label" size="1" color="gray" className="assistant-context-toggle"><Checkbox size="1" checked={includeContext} onCheckedChange={value=>setIncludeContext(value===true)}/>Include current view</Text>{working||activeTask?<Tooltip content="Stops this response; jobs and gameplay continue"><IconButton type="button" aria-label="Stop response" size="2" color="gray" onClick={()=>void stop()}><Square size={14}/></IconButton></Tooltip>:<Tooltip content="Send question"><IconButton type="submit" aria-label="Send question" size="2" color="blue" disabled={!draft.trim()||!canSend}><ArrowUp size={18}/></IconButton></Tooltip>}</div></form><Text as="p" size="0" color="gray" className="assistant-footnote">Check sources and timestamps. Commands require review.</Text>
</div>:<div className="assistant-followup-zone">
{localError&&<div className="assistant-error" role="alert"><Text size="1">{localError}</Text><IconButton size="1" variant="ghost" aria-label="Dismiss assistant error" onClick={()=>{setLocalError('');chat.clearError()}}><X size={12}/></IconButton></div>}
<Button size="2" variant="soft" onClick={()=>{setTab('chat');requestAnimationFrame(()=>textarea.current?.focus())}}><MessageSquare size={14}/>Ask a follow-up<ArrowUpRight size={14}/></Button>{draft.trim()&&<Text size="1" color="gray">Draft saved</Text>}
</div>}
</motion.aside>}</AnimatePresence><ConnectionsPanel open={connectionsOpen} onClose={()=>setConnectionsOpen(false)} initialTask={handoffTask} conversationId={conversationId.current} initialSection={handoffTask?'jobs':'agents'}/></MotionConfig>
}
function TaskDetail({task,onReports,onReview,onDelegate}:{task:AssistantTask;onReports:()=>void;onReview:()=>void;onDelegate:()=>void}){return <section className="assistant-task-detail" aria-label="Selected task details"><div className="assistant-section-heading"><Heading size="3" as="h4">Execution details</Heading><Badge color={stateColor(task.status)}>{stateLabel(task.status)}</Badge></div><Text as="p" size="1" color="gray">{task.model} · {task.effort} · updated {time(task.updatedAt)}</Text><ol className="assistant-timeline">{task.events.map(event=><li key={event.id} className={`assistant-event-${event.state}`}><span className="assistant-event-icon">{event.state==='completed'?<CheckCircle2 size={15}/>:event.state==='error'?<XCircle size={15}/>:<Clock3 size={15}/>}</span><div><Text size="2">{event.label}</Text><div><time dateTime={new Date(event.at).toISOString()}>{time(event.at)}</time>{event.source?.startsWith('/api/')&&<a href={event.source} target="_blank" rel="noreferrer">Evidence<ExternalLink size={11}/></a>}</div></div></li>)}</ol>{!task.events.length&&<Text as="p" size="1" color="gray">Waiting for the first recorded step.</Text>}{task.error&&<div className="assistant-error" role="alert">{task.error}</div>}{task.status==='interrupted'&&<Text as="p" size="1" color="gray">This response ended before completion. Review its saved evidence before starting another request.</Text>}<div className="assistant-task-actions">{task.reports.length>0&&<Button size="1" variant="soft" onClick={onReports}><ChartNoAxesCombined size={13}/>{task.reports.length} reports</Button>}{task.events.some(event=>event.operation==='mission_preview'&&event.state==='completed')&&<Button size="1" onClick={onReview}>Review in Command<ArrowUpRight size={13}/></Button>}<Button size="1" variant="surface" onClick={onDelegate}><Send size={13}/>Delegate planning</Button></div><Text as="p" size="1" color="gray">Task progress describes the assistant. Gameplay execution and Stop controls are in Command.</Text></section>}
function EmptyState({icon,title,text}:{icon:ReactNode;title:string;text:string}){return <div className="assistant-empty"><span>{icon}</span><Heading as="h4" size="3">{title}</Heading><Text as="p" size="2" color="gray">{text}</Text></div>}
function SourceLinks({sources}:{sources:AssistantSource[]}){return sources.length?<div className="assistant-sources">{[...new Map(sources.filter(source=>source.url.startsWith('/api/')).map(source=>[source.url,source])).values()].map(source=><a key={source.url} href={source.url} target="_blank" rel="noreferrer" title={`Read ${new Date(source.readAt).toLocaleString()}; source observation timestamps are in the response`}><Database size={11}/>{source.label}<ArrowUpRight size={11}/></a>)}</div>:null}