Initial commit
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import type { ChatMessageMeta, HumanEscalationOption } from '../types/chat'
|
||||
import { MarkdownBody } from './MarkdownBody'
|
||||
|
||||
interface EscalationPanelProps {
|
||||
meta: ChatMessageMeta
|
||||
onReply: (text: string) => void
|
||||
responded: boolean
|
||||
}
|
||||
|
||||
function firstLine(text: string): string {
|
||||
return text.split('\n').map((line) => line.trim()).find(Boolean) ?? text
|
||||
}
|
||||
|
||||
function checkpointStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'timeout':
|
||||
case 'timed_out':
|
||||
case 'expired':
|
||||
return 'Expired'
|
||||
case 'stale':
|
||||
case 'invalid':
|
||||
return 'Inactive'
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
return 'Cancelled'
|
||||
case 'resolved':
|
||||
return 'Resolved'
|
||||
default:
|
||||
return 'Responded'
|
||||
}
|
||||
}
|
||||
|
||||
export const EscalationPanel = React.memo(function EscalationPanel({
|
||||
meta, onReply, responded,
|
||||
}: EscalationPanelProps) {
|
||||
const isResponded = responded
|
||||
const checkpointStatus = String(meta.checkpoint_status ?? '').trim().toLowerCase()
|
||||
const resolvedLabel = checkpointStatusLabel(checkpointStatus)
|
||||
const prompt = String(meta.prompt ?? meta.summary ?? '')
|
||||
const lines = useMemo(
|
||||
() => prompt.split('\n').map((line) => line.trim()).filter(Boolean),
|
||||
[prompt],
|
||||
)
|
||||
const title = firstLine(prompt).replace(/^\[[^\]]+\]\s*/, '') || 'Action Required'
|
||||
const details = lines.slice(1).join('\n').trim()
|
||||
const summary = String(meta.summary ?? '').trim()
|
||||
const options = (meta.options ?? []).filter((opt): opt is HumanEscalationOption => !!opt?.id)
|
||||
const activeSubagents = useMemo(
|
||||
() => (meta.active_subagents ?? []).filter((item) => !!item && typeof item === 'object'),
|
||||
[meta.active_subagents],
|
||||
)
|
||||
const permissionRequests = useMemo(
|
||||
() => (meta.permission_requests ?? []).filter((item) => !!item && typeof item === 'object'),
|
||||
[meta.permission_requests],
|
||||
)
|
||||
const worktreePath = String(meta.worktree_path ?? '').trim()
|
||||
const hasRuntimeState = activeSubagents.length > 0 || permissionRequests.length > 0 || !!worktreePath
|
||||
|
||||
const handleReply = useCallback((option: HumanEscalationOption) => {
|
||||
if (isResponded) return
|
||||
onReply(option.label || option.id)
|
||||
}, [isResponded, onReply])
|
||||
|
||||
return (
|
||||
<div className="ckpt-panel ckpt-escalation">
|
||||
<div className="ckpt-header">
|
||||
<div className="ckpt-icon ckpt-icon-escalation">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M8 1.5L14.5 13H1.5L8 1.5Z" />
|
||||
<path d="M8 5.5V9" />
|
||||
<circle cx="8" cy="11.5" r="0.75" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ckpt-title">{title}</div>
|
||||
<span className="ckpt-badge ckpt-badge-scope">
|
||||
{String(meta.escalation_type ?? 'decision_needed').replace(/_/g, ' ')}
|
||||
</span>
|
||||
{isResponded && <span className="ckpt-badge ckpt-badge-responded">{resolvedLabel}</span>}
|
||||
</div>
|
||||
|
||||
{summary && summary !== title && (
|
||||
<div className="ckpt-section">
|
||||
<div className="ckpt-section-title">Summary</div>
|
||||
<MarkdownBody content={summary} className="ckpt-markdown" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details && (
|
||||
<div className="ckpt-section">
|
||||
<div className="ckpt-section-title">Request</div>
|
||||
<MarkdownBody content={details} className="ckpt-markdown" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasRuntimeState && (
|
||||
<details className="ckpt-runtime-details">
|
||||
<summary>Runtime State</summary>
|
||||
<div className="ckpt-runtime-body">
|
||||
{worktreePath && <div>Worktree: <code>{worktreePath}</code></div>}
|
||||
{activeSubagents.length > 0 && <div>Active subagents: {activeSubagents.length}</div>}
|
||||
{permissionRequests.length > 0 && <div>Pending permission records: {permissionRequests.length}</div>}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{!isResponded && options.length > 0 && (
|
||||
<div className="ckpt-actions ckpt-escalation-actions">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
className={`ckpt-btn ${option.id.includes('deny') ? 'ckpt-btn-deny' : 'ckpt-btn-approve'}`}
|
||||
onClick={() => handleReply(option)}
|
||||
>
|
||||
{option.label || option.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isResponded && meta.default_action && (
|
||||
<div className="ckpt-escalation-hint">
|
||||
Default on timeout: <code>{meta.default_action}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user