Initial commit

This commit is contained in:
LZH-YS1998
2026-07-01 17:56:31 +08:00
commit d78931979d
731 changed files with 311088 additions and 0 deletions
@@ -0,0 +1,297 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type {
ProgressEntry,
RoleAggregatedStatus,
RoleWorkItemActivitySection,
RoleWorkItemRow,
RoleWorkItemSummary,
} from '../types/kanban'
import type { AgentInfo } from '../types/visual'
import { AgentProgressBlock } from '../chat/AgentProgressBlock'
import { IconClose, IconTimeline, IconWorkItem } from '../chat/SvgIcons'
interface ExecutionPanelProps {
role: RoleWorkItemSummary
focusedWorkItemId?: string
focusedExecutionTurnId?: string
agents: AgentInfo[]
onClose: () => void
}
const ROLE_STATUS_BADGE: Record<RoleAggregatedStatus, { label: string; cls: string }> = {
active: { label: 'Working', cls: 'exec-badge-running' },
waiting: { label: 'Waiting', cls: 'exec-badge-idle' },
pending: { label: 'Pending', cls: 'exec-badge-pending' },
done: { label: 'Done', cls: 'exec-badge-done' },
failed: { label: 'Failed', cls: 'exec-badge-failed' },
}
const ROW_STATUS_BADGE: Record<string, { label: string; cls: string }> = {
todo: { label: 'To do', cls: 'exec-badge-pending' },
'in-progress': { label: 'In progress', cls: 'exec-badge-running' },
'in-review': { label: 'In review', cls: 'exec-badge-idle' },
done: { label: 'Done', cls: 'exec-badge-done' },
failed: { label: 'Failed', cls: 'exec-badge-failed' },
cancelled: { label: 'Cancelled', cls: 'exec-badge-cancelled' },
}
function formatRelativeTime(ts: number): string {
const sec = Math.floor((Date.now() - ts) / 1000)
if (sec < 5) return 'now'
if (sec < 60) return `${sec}s ago`
const min = Math.floor(sec / 60)
if (min < 60) return `${min}m ago`
const hr = Math.floor(min / 60)
if (hr < 24) return `${hr}h ago`
return `${Math.floor(hr / 24)}d ago`
}
function humanize(value?: string): string {
const text = String(value ?? '').trim()
if (!text) return ''
return text.replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}
function rowBadge(row: RoleWorkItemRow): { label: string; cls: string } {
if (row.phase === 'failed') return ROW_STATUS_BADGE.failed
if (row.phase === 'cancelled') return ROW_STATUS_BADGE.cancelled
return ROW_STATUS_BADGE[row.kanbanColumn] ?? ROW_STATUS_BADGE.todo
}
function rowSessionStatus(row: RoleWorkItemRow): string | undefined {
if (row.phase === 'failed') return 'failed'
if (row.phase === 'cancelled') return 'cancelled'
if (row.kanbanColumn === 'done') return 'done'
return undefined
}
function countActivityEntries(row: RoleWorkItemRow): number {
const sections = row.activitySections ?? []
if (sections.length > 0) {
return sections.reduce((count, section) => count + (section.entries?.length ?? 0), 0)
}
return row.progressLog.length
}
function ActivitySections({
sections,
fallbackEntries,
sessionStatus,
}: {
sections?: RoleWorkItemActivitySection[]
fallbackEntries?: ProgressEntry[]
sessionStatus?: string
}) {
const visibleSections = (sections ?? []).filter(section => (
(section.entries?.length ?? 0) > 0 || !!section.runtimeTaskId
))
if (visibleSections.length > 0) {
return (
<div className="exec-activity-sections">
{visibleSections.map((section, index) => {
const entries = section.entries ?? []
const key = `${section.runtimeTaskId || section.kind}:${index}`
return (
<section key={key} className="exec-activity-section">
<div className="exec-activity-section-head">
<span className="exec-activity-section-title">{section.title}</span>
{section.roleName && (
<span className="exec-activity-section-role">{section.roleName}</span>
)}
{entries.length > 0 && (
<span className="exec-section-count">{entries.length}</span>
)}
</div>
{entries.length > 0 ? (
<AgentProgressBlock
entries={entries}
sessionStatus={sessionStatus}
expandedByDefault
/>
) : (
<div className="exec-section-empty">No runtime activity yet</div>
)}
</section>
)
})}
</div>
)
}
if (!fallbackEntries || fallbackEntries.length === 0) {
return <div className="exec-section-empty">No activity recorded yet</div>
}
return (
<AgentProgressBlock
entries={fallbackEntries}
sessionStatus={sessionStatus}
expandedByDefault
/>
)
}
export function ExecutionPanel({
role,
focusedWorkItemId,
focusedExecutionTurnId,
agents,
onClose,
}: ExecutionPanelProps) {
const rows = useMemo(() => (
role.workItems
.slice()
.sort((a, b) => a.createdAt - b.createdAt)
), [role.workItems])
const focused = useMemo(() => (
rows.find(row => (
(!!focusedWorkItemId && row.workItemId === focusedWorkItemId)
|| (!!focusedExecutionTurnId && row.executionTurnId === focusedExecutionTurnId)
)) ?? rows[rows.length - 1] ?? null
), [focusedExecutionTurnId, focusedWorkItemId, rows])
const focusedRowKey = focused?.workItemId
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => {
const init = new Set<string>()
if (focusedRowKey) init.add(focusedRowKey)
return init
})
const autoExpandedRef = useRef<Set<string>>(new Set())
useEffect(() => {
if (!focusedRowKey || autoExpandedRef.current.has(focusedRowKey)) return
autoExpandedRef.current.add(focusedRowKey)
setExpandedIds(prev => {
if (prev.has(focusedRowKey)) return prev
const next = new Set(prev)
next.add(focusedRowKey)
return next
})
}, [focusedRowKey])
const toggleExpanded = useCallback((workItemId: string) => {
setExpandedIds(prev => {
const next = new Set(prev)
if (next.has(workItemId)) next.delete(workItemId)
else next.add(workItemId)
return next
})
}, [])
const focusedCardRef = useRef<HTMLDivElement | null>(null)
const lastScrolledIdRef = useRef<string | null>(null)
useEffect(() => {
if (!focusedRowKey || lastScrolledIdRef.current === focusedRowKey) return
lastScrolledIdRef.current = focusedRowKey
focusedCardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}, [focusedRowKey])
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}, [onClose])
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
const roleAgent = agents.find(agent => agent.agent_id === role.roleId)
const roleName = role.roleName || roleAgent?.name || humanize(role.roleId) || 'Role'
const headerBadge = ROLE_STATUS_BADGE[role.aggregatedStatus] ?? ROLE_STATUS_BADGE.pending
return (
<>
<div className="exec-panel-backdrop" onClick={onClose} />
<div className="exec-panel">
<div className="exec-panel-header">
<div className="exec-panel-title-row">
<IconTimeline />
<h3 className="exec-panel-title">{roleName}</h3>
<span className={`exec-badge ${headerBadge.cls}`}>{headerBadge.label}</span>
<span className="exec-panel-task-count">
{rows.length} Work Item{rows.length === 1 ? '' : 's'}
</span>
</div>
<button className="exec-panel-close" onClick={onClose} title="Close (Esc)">
<IconClose />
</button>
</div>
<div className="exec-panel-identity">
<div className="exec-panel-avatar">
{roleName.charAt(0).toUpperCase()}
</div>
<div className="exec-panel-agent-info">
<span className="exec-panel-agent-name">{roleName}</span>
<span className="exec-panel-agent-role">{humanize(role.roleId)}</span>
{role.roleSessionId && (
<span className="exec-panel-employee">{role.roleSessionId}</span>
)}
</div>
</div>
<div className="exec-panel-body">
{rows.length === 0 && (
<div className="exec-section-empty">No work items yet</div>
)}
{rows.map((row, index) => {
const isFocused = row.workItemId === focusedRowKey
const isExpanded = expandedIds.has(row.workItemId)
const badge = rowBadge(row)
const activityCount = countActivityEntries(row)
return (
<div
key={row.workItemId}
ref={isFocused ? focusedCardRef : null}
className={`exec-task-card${isFocused ? ' exec-task-card-focused' : ''}`}
>
<button
type="button"
className="exec-task-card-header"
onClick={() => toggleExpanded(row.workItemId)}
title={isExpanded ? 'Collapse' : 'Expand'}
>
<span className="exec-task-card-index">Work item #{index + 1}</span>
<span className="exec-task-card-title">{row.title || row.workItemId}</span>
<span className={`exec-badge ${badge.cls}`}>{badge.label}</span>
<span className="exec-task-card-time">{formatRelativeTime(row.updatedAt)}</span>
<span className={`exec-task-card-chevron${isExpanded ? ' open' : ''}`} aria-hidden="true">
</span>
</button>
{isExpanded && (
<div className="exec-task-card-body">
<div className="exec-task-card-projection">
<IconWorkItem />
<span>{humanize(row.kind) || 'Work item'}</span>
{row.workItemProjectionId && <code>{row.workItemProjectionId}</code>}
{row.executionTurnId && <span className="exec-inline-tag">Execution Turn</span>}
{row.isReviewTarget && <span className="exec-inline-tag">Review target</span>}
{row.executorRoleName && <span>{row.executorRoleName}</span>}
</div>
<div className="exec-section">
<div className="exec-section-header">
<IconTimeline />
<span>Activity</span>
<span className="exec-section-count">{activityCount}</span>
</div>
<div className="exec-section-content exec-activity-scroll">
<ActivitySections
sections={row.activitySections}
fallbackEntries={row.progressLog}
sessionStatus={rowSessionStatus(row)}
/>
</div>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
</>
)
}