/** Compact conversation summaries navigate to the native sidebar's detail and chat views. */ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react' import { runsOf, dependencies } from '../graph.ts' import type { TaskHistoryEntry } from '../state.ts' import { ConsultationHost, ConsultationConversation, type ConsultationPanelProps } from './consultation.tsx' import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client' import type { PropsRuntime, PropsRenderFactories } from '@deepseek-ai/dsh-client-ui-slots' import { TaskGraph, GRAPH_CSS } from './task-graph.tsx' import { taskStore, type PanelState } from './task-store.ts' import { milestoneDefinition, type Milestone } from './milestones.ts' import { displayedPlan, executorLabel, nodeLabel, headline, progress, taskStatus, VERDICT } from './presentation.ts' import { ReviewReport } from './review-report.tsx' import { ReviewRequirements } from './review-requirements.tsx' import { currentReviewJob, reviewPresentation } from './review-presentation.ts' import { ReviewProgress } from './review-progress.tsx' import { Button, Menu, Modal, IconEllipsisOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' import { Disclosure } from './disclosure.tsx' import { TaskOverview, type TaskNavigation } from './inline-task.tsx' import { RepairPanel } from './repair-panel.tsx' import { AttemptDetails } from './attempt-details.tsx' import { ReviewInspection } from './review-inspection.tsx' import { CSS } from './styles.ts' import { draftDefinition, type DraftCard } from './draft-events.ts' import { zh, en, type SupervisorTranslate } from './locales.ts' import { reviewDefinition } from './review-events.ts' import { createReviewPortals, ReviewPortalHost, type ReviewPortalProps } from './review-portals.tsx' import { ReviewConversation, ReviewSession, type ReviewNodeProps } from './review-session.tsx' import { createReviewDisclosureState } from './review-presentation.ts' import type { ReviewJob } from '../review-records.ts' import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' import { SessionConnection, connectSupervisorSession } from './session-connection.tsx' const PANEL_ID = 'dsh-task-supervisor/sidebar' declare module '@deepseek-ai/dsh-client-ui-sidebar-right/client' { interface SidebarRightTabParamsMap { 'task-supervisor': TaskNavigation } } interface PanelProps { sessionId: string } interface TailProps extends PanelProps { turn: { data: { get(key: 'task-supervisor-milestones'): readonly Milestone[] | undefined; get(key: 'task-supervisor-drafts'): readonly DraftCard[] | undefined } } } interface ClientContext { locale: { register(namespace: string, dictionaries: { zh: typeof zh; en: typeof en }): () => void; bind(namespace: string): SupervisorTranslate } sidebarRight: { openTab(kind: string, options?: { params: TaskNavigation }): void } sessions: ISessions effect(factory: () => (() => void) | void, label?: string): void uiConversation: { events: { register(definition: typeof milestoneDefinition | typeof draftDefinition | typeof reviewDefinition): () => void } } sidebarRightTabs: { register(definition: { id: string; kind: string; priority: 'extension'; title: () => string; guide: Array<{ id: string; order: number; title: () => string; description: () => string }> }): () => void } slots: { register(definition: { name: 'shell.overlay'; id: string; children: { 'task-supervisor.review-session': { kind: 'single'; scope: 'session' } } }, component: (props: ReviewPortalProps) => ReactNode): () => void register(definition: { name: 'conversation.chat.node'; key: 'task-supervisor-review' }, component: (props: ReviewNodeProps) => ReactNode): () => void register(definition: { name: 'task-supervisor.review-session' }, component: (props: PropsRuntime<'task-supervisor.review-session'> & PropsRenderFactories) => ReactNode): () => void register(definition: { name: 'sidebar.right.pane.tab'; key: string; children: { 'task-supervisor.consultation': { kind: 'single'; scope: 'session' } } }, component: (props: ConsultationPanelProps) => ReactNode): () => void register(definition: { name: 'task-supervisor.consultation' }, component: (props: PropsRuntime<'task-supervisor.consultation'> & PropsRenderFactories) => ReactNode): () => void inject(name: string, factory: () => () => void): () => void register(definition: { name: string; key: string }, component: (props: PanelProps) => ReactNode): () => void register(definition: { name: string; id: string }, component: (props: TailProps) => ReactNode): () => void register(definition: { name: string; id: string }, component: (props: PanelProps) => ReactNode): () => void } } export const inject = ['sessions', 'slots', 'sidebarRightTabs', 'sidebarRight', 'uiSession', 'uiConversation', 'locale'] const ACTION_LABEL: Record = { 'auto-approve-on': '审查通过后自动执行', 'auto-approve-off': '改为手动批准', approve: '批准计划', pause: '暂停', resume: '恢复任务', 'retry-review': '重试审查', 'restart-review': '重新审查', off: '关闭督导', on: '重新启用督导' } const SOURCE_LABEL = { user: '用户要求', project: '项目约束', implementation: '实现选择' } function useTask(sessionId: string) { const store = useMemo(() => taskStore(sessionId), [sessionId]) return { ...useSyncExternalStore(store.subscribe, store.getSnapshot), store } } function Actions({ sessionId }: PanelProps) { const { state, busy, store } = useTask(sessionId) return
{state?.actions.map(action => )}
} function InlineTask({ sessionId, open, t, connect }: PanelProps & { open: (params?: TaskNavigation) => void; t: SupervisorTranslate; connect: () => Promise }): ReactNode { const { state, error } = useTask(sessionId) const task = state?.task const previous = useRef<{ id: string | null; phase: string | null } | null>(null) useEffect(() => { if (!state) return const before = previous.current if (before && task && (before.id !== task.id || (task.phase === 'paused' && before.phase !== 'paused'))) { open({ view: 'details' }) } previous.current = { id: task?.id ?? null, phase: task?.phase ?? null } }, [state, task?.id, task?.phase, open]) if (state?.entryActive) return
任务规划 · 等待输入目标
if (!task || task.phase === 'cleared') return null return } open={open} t={t} /> } function HistoricalDetails({ entry, sessionId, t }: { entry: TaskHistoryEntry; sessionId: string; t: SupervisorTranslate }): ReactNode { const [selected, setSelected] = useState(null) const { state, busy, store } = useTask(sessionId) const task = entry.task const stage = task.stages.find(item => item.id === selected) ?? task.stages[task.stageIndex] ?? task.stages[0] const historicalState: PanelState = { task, live: false, armed: false, reviewing: false, actions: [], reviews: entry.reviews, reworks: entry.reworks } return <>

任务目标

{task.objective}

{task.phase === 'complete' ? '已完成' : '已清除'} · 记录 seq {entry.lastSeq}

{task.stages.length > 0 &&

执行计划 · {task.stages.length} 个节点

nodeLabel(task, id, historicalState)} executor={id => executorLabel(task, sessionId, id)} />
} {stage &&

节点详情 · {nodeLabel(task, stage.id, historicalState)}

{stage.title}

执行者:{executorLabel(task, sessionId, stage.id)}

{stage.description ?? '暂无补充说明。'}

    {task.criteria.filter(item => stage.criterionIds.includes(item.id)) .map(item =>
  • {item.text}
  • )}
} {state && } {entry.reviews.length > 0 &&

审查记录 · {entry.reviews.length} 项

{entry.reviews.slice().reverse().map(review =>

主 Session 截至 seq {review.cutoff} · 审查 Session {review.reviewerSessionId ?? '未记录'}

)}
} } function TaskPanel({ sessionId, navigation, renderConsult, showReview, t, connect }: PanelProps & { navigation: ReturnType['tab']['navigation']; renderConsult: (id: string) => ReactNode; t: SupervisorTranslate showReview: (job: ReviewJob) => Promise connect: () => Promise }): ReactNode { const { state, error, busy, store } = useTask(sessionId) const [tab, setTab] = useState<'details' | 'consultation'>('details') const [showHistory, setShowHistory] = useState(false) const [historyEntries, setHistoryEntries] = useState(null) const [historySelection, setHistorySelection] = useState(null) const [historyBusy, setHistoryBusy] = useState(false) const [historyError, setHistoryError] = useState('') const [menuOpen, setMenuOpen] = useState(false) const [helpOpen, setHelpOpen] = useState(false) const [consultation, setConsultation] = useState(null) const [consultError, setConsultError] = useState('') const [opening, setOpening] = useState(false) const [selected, setSelected] = useState(null) const [reviewId, setReviewId] = useState(null) const [reviewNavigationError, setReviewNavigationError] = useState('') const detailsBody = useRef(null) const historyBody = useRef(null) const task = state?.task const previousTaskId = useRef(null) useEffect(() => { const params = navigation.params as TaskNavigation | undefined setTab(params?.view ?? 'details'); setSelected(params?.nodeId ?? null); setReviewId(params?.reviewerSessionId ?? null) if (detailsBody.current) detailsBody.current.scrollTop = 0 }, [navigation.revision]) // Keep the main-scoped conversation mounted when the current task changes. useEffect(() => { if (previousTaskId.current && previousTaskId.current !== task?.id) { setTab('details'); setShowHistory(false); setSelected(null); setReviewId(null) } previousTaskId.current = task?.id ?? null }, [task?.id]) const plan = state ? displayedPlan(state) : null const graphTask = plan?.task ?? task const index = Math.max(0, graphTask?.stages.findIndex(item => item.id === selected) ?? -1) const stage = graphTask?.stages[selected === null ? graphTask.stageIndex : index] const review = reviewId ? state?.reviews?.find(item => item.reviewerSessionId === reviewId) : task?.lastReview const reviewJob = reviewId ? state?.reviewJobs?.find(item => item.reviewerSessionId === reviewId) : currentReviewJob(state) ?? state?.reviewJobs?.find(item => item.taskId === task?.id && item.id === task?.lastReview?.jobId) const outcomeJob = state?.reviewJobs?.find(item => item.taskId === task?.id && item.reviewerSessionId === review?.reviewerSessionId && item.cutoff === review.cutoff) const historicalTask = historyEntries?.find(entry => entry.task.id === historySelection) useEffect(() => { if (showHistory && historySelection === task?.id && task && !['complete', 'cleared'].includes(task.phase)) { setShowHistory(false); setHistorySelection(null); setTab('details'); setSelected(null) } }, [showHistory, historySelection, task?.id, task?.phase]) const reviewSection = review &&
{state?.reviewing && review.reviewerSessionId !== reviewJob?.reviewerSessionId &&

{t('reviewPreviousDecision')}

}

Supervisor · {review.stageId === 'planning' ? '规划进展审查' : review.stageId === 'plan' ? '计划审查' : review.stageId === 'completion' ? task?.phase === 'complete' && state?.repairs?.some(p => p.taskId === task.id && ['pending', 'confirmed'].includes(p.status)) ? '此前完成审查' : '完成审查' : '节点审查'} · {outcomeJob ? t(reviewPresentation(outcomeJob, state).label) : VERDICT[review.verdict]}

{outcomeJob && }

主 Session 截至 seq {review.cutoff} · 证据 {review.evidenceSeqs?.join(', ')}
审查 Session {review.reviewerSessionId}

function openConsultation() { setTab('consultation') if (consultation || opening || !state?.live) return setOpening(true); setConsultError('') void fetch(`/api/task-supervisor?sessionId=${encodeURIComponent(sessionId)}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'consult' }) }) .then(async response => { const body = await response.json() as { consultationSessionId?: string; consultationMode?: 'discussion' | 'direct'; error?: string } if (!response.ok || !body.consultationSessionId) throw new Error(body.error ?? '无法打开督导对话') setConsultation(body.consultationSessionId) }).catch(error => setConsultError(String(error))).finally(() => setOpening(false)) } function openHistory() { setShowHistory(true); setHistorySelection(null); setHistoryBusy(true); setHistoryError('') if (historyBody.current) historyBody.current.scrollTop = 0 void fetch(`/api/task-supervisor?sessionId=${encodeURIComponent(sessionId)}&view=history`, { cache: 'no-store' }) .then(async response => { const body = await response.json() as { entries?: TaskHistoryEntry[]; error?: string } if (!response.ok || !body.entries) throw new Error(body.error ?? '无法读取历史任务') setHistoryEntries(body.entries) }).catch(error => setHistoryError(String(error))).finally(() => setHistoryBusy(false)) } useEffect(() => { if (state?.live && !task) openConsultation() }, [state?.live, task?.id]) function closeHistory() { setShowHistory(false); setHistorySelection(null) } function selectHistory(id: string | null) { setHistorySelection(id) if (historyBody.current) historyBody.current.scrollTop = 0 } return
{showHistory ? :
} setMenuOpen(false)} anchor={} items={[{ id: 'history', label: t('history') }, { id: 'help', label: t('help') }]} onSelect={id => { setMenuOpen(false); if (id === 'history') openHistory(); else setHelpOpen(true) }} />
{showHistory ? t('ended') : state?.entryActive ? t('waiting') : state ? task ? `${taskStatus(state)}${task.stages.length ? ` · ${progress(task)}` : ''}` : t('idle') : t('reading')} {showHistory && {historyEntries?.length ?? 0}}
setHelpOpen(false)} title={t('help')} closeLabel={t('close')}>

{t('helpText')}

{showHistory &&
{historicalTask ? <> : <>

已结束 · {historyEntries?.length ?? 0} 项

{historyBusy &&

正在读取历史任务…

} {!historyBusy && historyEntries?.length === 0 &&

这里还没有已结束的任务。

} {historyEntries?.map(entry => )}
} {historyError &&

{historyError}

} {error &&

{error}

}
} {error &&

{error}

} {task && !showHistory && tab === 'details' && ((state?.actions.length ?? 0) > 0 || task.phase === 'paused' || task.phase === 'awaiting-approval') &&
{task.phase === 'paused' && 等待手动恢复} {task.phase === 'awaiting-approval' && 也可在主会话输入“批准”}
}
} /** Review findings and applied reworks are notices; native main-Agent answers stay untouched. */ function ReviewNotes({ turn, open }: TailProps & { open: (params?: TaskNavigation) => void }): ReactNode { const milestones = turn.data.get('task-supervisor-milestones') return <>{milestones?.filter(item => item.reviewerSessionId || item.kind === 'rework').map(item =>
)} } function DraftProposal({ draft, sessionId, t }: { draft: DraftCard; sessionId: string; t: SupervisorTranslate }) { const { state, busy, error, store } = useTask(draft.mainSessionId) if (sessionId !== draft.sourceSessionId) return null const current = state?.draft const latest = current?.id === draft.id && current.version === draft.version && current.status === 'draft' const created = current?.id === draft.id && current.status === 'created' const available = latest && state?.live && (!state.task || ['complete', 'cleared'].includes(state.task.phase)) return
{draft.title}

{draft.requirements}

{draft.questions.length > 0 &&
    {draft.questions.map((question, index) =>
  • {question}
  • )}
}
{latest ? <> {(!available || draft.questions.length > 0) && {t(!available ? 'blocked' : 'unresolved')}} : {t(created ? 'created' : 'older')}}
{error &&

{error}

}
} function DraftNotes({ sessionId, turn, t }: TailProps & { t: SupervisorTranslate }) { return <>{turn.data.get('task-supervisor-drafts')?.map(draft => )} } export function apply(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register('task-supervisor', { zh, en })) const t = ctx.locale.bind('task-supervisor') const open = (params: TaskNavigation = {}) => ctx.sidebarRight.openTab('task-supervisor', { params }) const connect = async (sessionId: string) => { await connectSupervisorSession(ctx.sessions, sessionId as SessionId) await taskStore(sessionId).refresh() } const Panel = (props: ConsultationPanelProps) => { const { tab } = props.useTabInfo() return connect(props.sessionId)} showReview={async job => { const binding = ctx.sessions.binding(props.sessionId as SessionId) if (!binding || job.mainSessionId !== props.sessionId) throw new Error(t('reviewUnavailable')) await binding.session.loadThrough(Math.max(0, job.cutoff) as SessionSeq) for (let frame = 0; frame < 3; frame++) await new Promise(resolve => requestAnimationFrame(() => resolve())) const node = document.getElementById(`task-review-${job.id}`) if (!node) throw new Error(t('reviewUnavailable')) node.scrollIntoView({ block: 'start', behavior: 'smooth' }) }} renderConsult={id => } /> } const Inline = (props: PanelProps) => connect(props.sessionId)} /> const Notes = (props: TailProps) => <> const portals = createReviewPortals() const disclosures = createReviewDisclosureState() ctx.effect(() => () => { portals.dispose(); disclosures.clear() }) const Review = (props: ReviewNodeProps) => connect(props.sessionId)} /> const ReviewHost = (props: ReviewPortalProps) => ctx.effect(() => ctx.uiConversation.events.register(reviewDefinition), 'task-supervisor:reviews') ctx.effect(() => ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'task-supervisor/review-portals', children: { 'task-supervisor.review-session': { kind: 'single', scope: 'session' } } }, ReviewHost))) ctx.effect(() => ctx.slots.inject('task-supervisor.review-session', () => ctx.slots.register({ name: 'task-supervisor.review-session' }, ReviewConversation))) ctx.effect(() => ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'task-supervisor-review' }, Review))) ctx.effect(() => ctx.slots.inject('task-supervisor.consultation', () => ctx.slots.register({ name: 'task-supervisor.consultation' }, ConsultationConversation))) ctx.effect(() => ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({ name: 'conversation.input.dock', id: `${PANEL_ID}/current` }, Inline))) ctx.effect(() => ctx.uiConversation.events.register(milestoneDefinition), 'task-supervisor:milestones') ctx.effect(() => ctx.uiConversation.events.register(draftDefinition), 'task-supervisor:drafts') ctx.effect(() => { const style = document.createElement('style'); style.dataset.pluginCss = 'dsh-task-supervisor'; style.textContent = CSS + GRAPH_CSS document.head.append(style); return () => { style.remove() } }) ctx.effect(() => ctx.sidebarRightTabs.register({ id: PANEL_ID, kind: 'task-supervisor', priority: 'extension', title: () => '任务督导', guide: [{ id: 'task-supervisor', order: 5, title: () => '任务督导', description: () => '查看任务详情或与督导对话' }] })) ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: PANEL_ID, children: { 'task-supervisor.consultation': { kind: 'single', scope: 'session' } } }, Panel))) ctx.effect(() => ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({ name: 'conversation.chat.turnTail', id: `${PANEL_ID}/milestones` }, Notes))) }