fix: unify company runtime recovery lifecycle
This commit is contained in:
@@ -476,7 +476,6 @@ export default function App() {
|
||||
const [globalCompanyProfile, setGlobalCompanyProfile] = useState<'corporate' | 'custom'>('corporate')
|
||||
const [globalTaskPreferredAgent, setGlobalTaskPreferredAgent] = useState<TaskPreferredAgent>('native')
|
||||
const [orgInfoData, setOrgInfoData] = useState<OrgInfoPayload | null>(null)
|
||||
const [recoveryStatus, setRecoveryStatus] = useState<any>(null)
|
||||
const [commsState, setCommsState] = useState<import('./lib/wsClient').CommsStatePayload | null>(null)
|
||||
const [commsMessage, setCommsMessage] = useState<import('./lib/wsClient').CommsMessagePayload | null>(null)
|
||||
const [talentTemplates, setTalentTemplates] = useState<TalentTemplate[]>([])
|
||||
@@ -1239,7 +1238,6 @@ export default function App() {
|
||||
runtimeControlState: String(payload.runtime_control_state ?? payload.runtimeControlState ?? 'idle') as any,
|
||||
canStop: Boolean(payload.can_stop ?? payload.canStop),
|
||||
canResume: Boolean(payload.can_resume ?? payload.canResume),
|
||||
resumeParentTaskId: String(payload.resume_parent_task_id ?? payload.resumeParentTaskId ?? ''),
|
||||
resumeParentSessionId: String(payload.resume_parent_session_id ?? payload.resumeParentSessionId ?? ''),
|
||||
pendingRuntimeCheckpointId: String(payload.pending_runtime_checkpoint_id ?? payload.pendingRuntimeCheckpointId ?? ''),
|
||||
stopIntentId: String(payload.stop_intent_id ?? payload.stopIntentId ?? ''),
|
||||
@@ -1749,10 +1747,6 @@ export default function App() {
|
||||
clientRef.current?.collabSync(getActiveProjectId(), undefined, projectViewGenerationRef.current)
|
||||
}
|
||||
},
|
||||
onRecoveryStatus: (payload) => {
|
||||
if (!payloadMatchesActiveProject(payload as unknown as Record<string, unknown>, false)) return
|
||||
setRecoveryStatus(payload)
|
||||
},
|
||||
onCommsState: (payload) => {
|
||||
if (!payloadMatchesActiveProject(payload as unknown as Record<string, unknown>, false)) return
|
||||
setCommsState(payload)
|
||||
@@ -1761,13 +1755,6 @@ export default function App() {
|
||||
if (!payloadMatchesActiveProject(payload as unknown as Record<string, unknown>, true)) return
|
||||
setCommsMessage(payload)
|
||||
},
|
||||
onRecoveryResult: (payload) => {
|
||||
if (!payloadMatchesActiveProject(payload as unknown as Record<string, unknown>, false)) return
|
||||
if (payload?.status === 'completed' || payload?.status === 'cancelled') {
|
||||
// Trigger a re-scan
|
||||
clientRef.current?.recoveryAction(getActiveProjectId(), 'scan')
|
||||
}
|
||||
},
|
||||
onTalentList: (payload) => {
|
||||
setTalentTemplates(payload.templates ?? [])
|
||||
if (payload.talent_dir) setDefaultTalentDir(payload.talent_dir)
|
||||
@@ -2242,18 +2229,13 @@ export default function App() {
|
||||
) => {
|
||||
const session = sessionStore.sessions.find(s => s.taskId === taskId)
|
||||
const parentSessionId = session?.resumeParentSessionId ?? session?.parentSessionId ?? session?.sessionId
|
||||
const parentTaskId = session?.resumeParentTaskId
|
||||
?? (parentSessionId ? sessionStore.sessions.find(s => s.sessionId === parentSessionId && !s.parentSessionId)?.taskId : undefined)
|
||||
?? taskId
|
||||
for (const candidate of sessionStore.sessions) {
|
||||
if (
|
||||
candidate.taskId === taskId
|
||||
|| candidate.taskId === parentTaskId
|
||||
|| (!!parentSessionId && (candidate.parentSessionId === parentSessionId || candidate.sessionId === parentSessionId))
|
||||
) {
|
||||
sessionStore.updateSession(candidate.taskId, {
|
||||
...patch,
|
||||
resumeParentTaskId: parentTaskId,
|
||||
resumeParentSessionId: parentSessionId,
|
||||
})
|
||||
}
|
||||
@@ -2278,7 +2260,7 @@ export default function App() {
|
||||
clientRef.current?.sessionStop(getActiveProjectId(), taskId)
|
||||
}, [sessionStore.sessions, markRuntimeControlForTask, getActiveProjectId])
|
||||
|
||||
const handleSessionResume = useCallback((taskId: string) => {
|
||||
const handleSessionResume = useCallback((taskId: string, runtimeSessionId?: string, checkpointId?: string) => {
|
||||
const session = sessionStore.sessions.find(s => s.taskId === taskId)
|
||||
const isCompanyRuntime = session?.execMode === 'company'
|
||||
|| session?.execMode === 'org'
|
||||
@@ -2293,7 +2275,12 @@ export default function App() {
|
||||
canResume: false,
|
||||
})
|
||||
}
|
||||
clientRef.current?.sessionResume(getActiveProjectId(), taskId)
|
||||
clientRef.current?.sessionResume(
|
||||
getActiveProjectId(),
|
||||
taskId,
|
||||
runtimeSessionId ?? session?.resumeParentSessionId ?? session?.parentSessionId ?? session?.sessionId,
|
||||
checkpointId ?? session?.pendingRuntimeCheckpointId,
|
||||
)
|
||||
}, [sessionStore.sessions, markRuntimeControlForTask, getActiveProjectId])
|
||||
|
||||
const handleGlobalModeChange = useCallback((mode: 'task' | 'company' | 'org' | 'custom', profile?: string, orgId?: string) => {
|
||||
@@ -2426,9 +2413,6 @@ export default function App() {
|
||||
activeSavedOrg={activeSavedOrg}
|
||||
onSavedOrgsList={handleSavedOrgsList}
|
||||
onSavedOrgLoad={handleSavedOrgLoad}
|
||||
recoveryStatus={recoveryStatus}
|
||||
onRecoveryResume={(id) => clientRef.current?.recoveryAction(getActiveProjectId(), 'resume', id)}
|
||||
onRecoveryCancel={(id) => clientRef.current?.recoveryAction(getActiveProjectId(), 'cancel', id)}
|
||||
commsState={commsState}
|
||||
commsMessage={commsMessage}
|
||||
onCommsRefresh={(opts) => {
|
||||
|
||||
@@ -676,7 +676,6 @@ export function mapBackendSession(raw: any): Session {
|
||||
runtimeControlState: raw.runtime_control_state ?? raw.runtimeControlState,
|
||||
canStop: raw.can_stop ?? raw.canStop,
|
||||
canResume: raw.can_resume ?? raw.canResume,
|
||||
resumeParentTaskId: raw.resume_parent_task_id ?? raw.resumeParentTaskId,
|
||||
resumeParentSessionId: raw.resume_parent_session_id ?? raw.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: raw.pending_runtime_checkpoint_id ?? raw.pendingRuntimeCheckpointId,
|
||||
stopIntentId: raw.stop_intent_id ?? raw.stopIntentId,
|
||||
|
||||
@@ -387,7 +387,6 @@ export function getConversationSessionView(
|
||||
runtimeControlState: runtimeSource.runtimeControlState ?? normalizedActiveSession.runtimeControlState,
|
||||
canStop: runtimeSource.canStop ?? normalizedActiveSession.canStop,
|
||||
canResume: runtimeSource.canResume ?? normalizedActiveSession.canResume,
|
||||
resumeParentTaskId: runtimeSource.resumeParentTaskId ?? normalizedActiveSession.resumeParentTaskId,
|
||||
resumeParentSessionId: runtimeSource.resumeParentSessionId ?? normalizedActiveSession.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: runtimeSource.pendingRuntimeCheckpointId ?? normalizedActiveSession.pendingRuntimeCheckpointId,
|
||||
stopIntentId: runtimeSource.stopIntentId ?? normalizedActiveSession.stopIntentId,
|
||||
@@ -447,7 +446,6 @@ export function getConversationHeaderSession(
|
||||
runtimeControlState: runtimeSource.runtimeControlState ?? normalizedActiveSession.runtimeControlState,
|
||||
canStop: runtimeSource.canStop ?? normalizedActiveSession.canStop,
|
||||
canResume: runtimeSource.canResume ?? normalizedActiveSession.canResume,
|
||||
resumeParentTaskId: runtimeSource.resumeParentTaskId ?? normalizedActiveSession.resumeParentTaskId,
|
||||
resumeParentSessionId: runtimeSource.resumeParentSessionId ?? normalizedActiveSession.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: runtimeSource.pendingRuntimeCheckpointId ?? normalizedActiveSession.pendingRuntimeCheckpointId,
|
||||
stopIntentId: runtimeSource.stopIntentId ?? normalizedActiveSession.stopIntentId,
|
||||
|
||||
@@ -30,6 +30,17 @@ const flushPromises = async () => {
|
||||
|
||||
const client = new VisualSocketClient('ws://unit.test', {})
|
||||
|
||||
// Company Continue keeps the selected UI channel task separate from the
|
||||
// durable runtime identity used by the checkpoint handoff.
|
||||
client.sessionResume('project-a', 'ui-task', 'runtime-session', 'checkpoint-1')
|
||||
const resumeEnvelope = JSON.parse(
|
||||
(client as unknown as TestSocketClient).pendingQueue.pop() ?? '{}',
|
||||
) as Record<string, unknown>
|
||||
assert.equal(resumeEnvelope.type, 'session_resume')
|
||||
assert.equal(resumeEnvelope.task_id, 'ui-task')
|
||||
assert.equal(resumeEnvelope.runtime_session_id, 'runtime-session')
|
||||
assert.equal(resumeEnvelope.checkpoint_id, 'checkpoint-1')
|
||||
|
||||
// A summary and a full request for the same task are distinct correlations.
|
||||
// Neither Promise may settle merely because the request was queued locally.
|
||||
const summaryPromise = client.sessionDetail('project-a', 'task-1', { detailLevel: 'summary' })
|
||||
|
||||
@@ -41,8 +41,6 @@ interface SocketHandlers {
|
||||
onProjectSwitched?: (payload: { project_id: string; switch_seq?: string }) => void
|
||||
onProjectDeleted?: (payload: { project_id: string }) => void
|
||||
onOrgInfo?: (payload: OrgInfoPayload) => void
|
||||
onRecoveryStatus?: (payload: any) => void
|
||||
onRecoveryResult?: (payload: any) => void
|
||||
onTalentList?: (payload: TalentListPayload) => void
|
||||
onTalentScanLocal?: (payload: { templates: Array<{ template_id: string; name: string; description: string; category: string; domains: string[]; tags: string[] }> }) => void
|
||||
onEmployeeDetail?: (payload: EmployeeDetailPayload) => void
|
||||
@@ -169,7 +167,6 @@ const PROJECT_SCOPED_MESSAGE_TYPES = new Set([
|
||||
'session_update_title',
|
||||
'secretary_send',
|
||||
'project_index',
|
||||
'recovery_action',
|
||||
'comms_state',
|
||||
'comms_read_message',
|
||||
])
|
||||
@@ -420,9 +417,22 @@ export class VisualSocketClient {
|
||||
this.send({ type: 'session_stop', project_id: pid, task_id: taskId })
|
||||
}
|
||||
|
||||
sessionResume(projectId: string, taskId: string, content?: string): void {
|
||||
sessionResume(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
runtimeSessionId?: string,
|
||||
checkpointId?: string,
|
||||
content?: string,
|
||||
): void {
|
||||
const pid = this.requireProjectId(projectId, 'session_resume')
|
||||
this.send({ type: 'session_resume', project_id: pid, task_id: taskId, content })
|
||||
this.send({
|
||||
type: 'session_resume',
|
||||
project_id: pid,
|
||||
task_id: taskId,
|
||||
runtime_session_id: runtimeSessionId,
|
||||
checkpoint_id: checkpointId,
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
sessionComplete(projectId: string, taskId: string): void {
|
||||
@@ -646,11 +656,6 @@ export class VisualSocketClient {
|
||||
this.send({ type: 'org_saved_delete', name })
|
||||
}
|
||||
|
||||
recoveryAction(projectId: string, action: 'resume' | 'cancel' | 'scan', parentTaskId?: string): void {
|
||||
const pid = this.requireProjectId(projectId, 'recovery_action')
|
||||
this.send({ type: 'recovery_action', project_id: pid, action, parent_task_id: parentTaskId })
|
||||
}
|
||||
|
||||
commsState(projectId: string, opts?: { task_id?: string; session_id?: string }): void {
|
||||
const pid = this.requireProjectId(projectId, 'comms_state')
|
||||
this.send({ type: 'comms_state', project_id: pid, ...(opts || {}) })
|
||||
@@ -801,12 +806,6 @@ export class VisualSocketClient {
|
||||
if (projectId) this.commsState(projectId)
|
||||
} catch { /* ignore */ }
|
||||
break
|
||||
case 'recovery_status':
|
||||
this.handlers.onRecoveryStatus?.(parsed.payload)
|
||||
break
|
||||
case 'recovery_result':
|
||||
this.handlers.onRecoveryResult?.(parsed.payload)
|
||||
break
|
||||
case 'talent_list':
|
||||
this.handlers.onTalentList?.(parsed.payload)
|
||||
break
|
||||
|
||||
@@ -244,7 +244,6 @@ function sessionReducer(state: Session[], action: SessionAction): Session[] {
|
||||
runtimeControlState: guardedRuntimeControl.runtimeControlState ?? existing.runtimeControlState,
|
||||
canStop: guardedRuntimeControl.canStop ?? existing.canStop,
|
||||
canResume: guardedRuntimeControl.canResume ?? existing.canResume,
|
||||
resumeParentTaskId: incoming.resumeParentTaskId ?? existing.resumeParentTaskId,
|
||||
resumeParentSessionId: incoming.resumeParentSessionId ?? existing.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: guardedRuntimeControl.pendingRuntimeCheckpointId ?? existing.pendingRuntimeCheckpointId,
|
||||
stopIntentId: guardedRuntimeControl.stopIntentId ?? existing.stopIntentId,
|
||||
@@ -337,7 +336,6 @@ function sessionReducer(state: Session[], action: SessionAction): Session[] {
|
||||
runtimeControlState: guarded.runtimeControlState ?? s.runtimeControlState,
|
||||
canStop: guarded.canStop ?? s.canStop,
|
||||
canResume: guarded.canResume ?? s.canResume,
|
||||
resumeParentTaskId: guarded.resumeParentTaskId ?? s.resumeParentTaskId,
|
||||
resumeParentSessionId: guarded.resumeParentSessionId ?? s.resumeParentSessionId,
|
||||
pendingRuntimeCheckpointId: guarded.pendingRuntimeCheckpointId ?? s.pendingRuntimeCheckpointId,
|
||||
stopIntentId: guarded.stopIntentId ?? s.stopIntentId,
|
||||
|
||||
@@ -316,10 +316,9 @@ export interface Session {
|
||||
originChannel?: string
|
||||
originTaskId?: string
|
||||
runtimeControlState?: 'running' | 'suspending' | 'suspended' | 'resuming' | 'idle'
|
||||
canStop?: boolean
|
||||
canResume?: boolean
|
||||
resumeParentTaskId?: string
|
||||
resumeParentSessionId?: string
|
||||
canStop?: boolean
|
||||
canResume?: boolean
|
||||
resumeParentSessionId?: string
|
||||
pendingRuntimeCheckpointId?: string
|
||||
stopIntentId?: string
|
||||
// Handoff context from upstream work item (Company Mode)
|
||||
|
||||
@@ -100,8 +100,6 @@ export type SocketEnvelope =
|
||||
| { type: 'work_item_batch_updated'; payload: { run_id?: string; work_items: RuntimeWorkItemInfo[]; frontier?: RuntimeFrontierSummary } }
|
||||
| { type: 'project_recovery_updated'; payload: Record<string, unknown> }
|
||||
| { type: 'project_revision_created'; payload: { run_id?: string; revision_links: SessionLinkInfo[] } }
|
||||
| { type: 'recovery_status'; payload: Record<string, unknown> }
|
||||
| { type: 'recovery_result'; payload: Record<string, unknown> }
|
||||
| { type: 'talent_list'; payload: TalentListPayload }
|
||||
| { type: 'talent_scan_local'; payload: { templates: Array<{ template_id: string; name: string; description: string; category: string; domains: string[]; tags: string[] }> } }
|
||||
| { type: 'employee_detail'; payload: EmployeeDetailPayload }
|
||||
|
||||
@@ -77,7 +77,6 @@ interface ContextPanelProps {
|
||||
onCommsRefresh?: () => void
|
||||
onCommsReadMessage?: (path: string) => void
|
||||
orgInfoData?: OrgInfoPayload | null
|
||||
recoveryStatus?: Record<string, unknown> | null
|
||||
canShowTeamTab?: boolean
|
||||
onTeamStopRun?: () => void
|
||||
|
||||
@@ -497,7 +496,6 @@ export function ContextPanel({
|
||||
onCommsRefresh,
|
||||
onCommsReadMessage,
|
||||
orgInfoData,
|
||||
recoveryStatus,
|
||||
canShowTeamTab = false,
|
||||
onTeamStopRun,
|
||||
onTitleChange,
|
||||
@@ -1306,7 +1304,6 @@ export function ContextPanel({
|
||||
<div style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
||||
<ProjectCockpit
|
||||
orgInfoData={orgInfoData ?? null}
|
||||
recoveryStatus={recoveryStatus ?? null}
|
||||
commsState={commsState ?? null}
|
||||
onStopRun={onTeamStopRun}
|
||||
embedded
|
||||
|
||||
@@ -48,7 +48,6 @@ interface TeamCardInfo {
|
||||
|
||||
interface ProjectCockpitProps {
|
||||
orgInfoData?: OrgInfoPayload | null
|
||||
recoveryStatus?: Record<string, unknown> | null
|
||||
commsState?: CommsStatePayload | null
|
||||
onStopRun?: () => void
|
||||
embedded?: boolean
|
||||
@@ -56,7 +55,6 @@ interface ProjectCockpitProps {
|
||||
|
||||
export function ProjectCockpit({
|
||||
orgInfoData,
|
||||
recoveryStatus,
|
||||
commsState,
|
||||
onStopRun,
|
||||
embedded = false,
|
||||
@@ -78,7 +76,6 @@ export function ProjectCockpit({
|
||||
count + asRecordList(asRecord(digest.manager_digest).notification_backlog).length
|
||||
), 0)
|
||||
const unreadCount = actionableCount + protocolCount + notificationCount
|
||||
const interrupted = Array.isArray(recoveryStatus?.interrupted) ? recoveryStatus.interrupted.length : 0
|
||||
|
||||
const communicationItems = [
|
||||
{ label: 'Actionable', value: actionableCount },
|
||||
@@ -198,7 +195,7 @@ export function ProjectCockpit({
|
||||
<span>Seats {runtimeView.runtimeSeats.length}</span>
|
||||
<span>Approvals {pendingDecisionCount}</span>
|
||||
<span>Unread {unreadCount}</span>
|
||||
<span>Recovery {interrupted > 0 ? `${interrupted} interrupted` : summarizeText(asRecord(projectRun?.recovery_pointer).status, 'clean')}</span>
|
||||
<span>Run state {summarizeText(asRecord(projectRun?.recovery_pointer).status, 'clean')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export interface RecoverableWorkItem {
|
||||
work_item_projection_id: string
|
||||
title: string
|
||||
task_id: string
|
||||
status: string
|
||||
interrupted: boolean
|
||||
previous_status: string
|
||||
}
|
||||
|
||||
export interface InterruptedWorkItemRuntime {
|
||||
parent_session_id: string
|
||||
parent_task_id: string
|
||||
project_id: string
|
||||
title: string
|
||||
profile: string
|
||||
interrupted_at: string
|
||||
work_items: RecoverableWorkItem[]
|
||||
}
|
||||
|
||||
export interface RecoveryStatusPayload {
|
||||
interrupted: InterruptedWorkItemRuntime[]
|
||||
active_recoveries: string[]
|
||||
scanned_at: number
|
||||
}
|
||||
|
||||
interface WorkItemRecoveryPanelProps {
|
||||
data: RecoveryStatusPayload
|
||||
onResume: (parentTaskId: string) => void
|
||||
onCancel: (parentTaskId: string) => void
|
||||
}
|
||||
|
||||
const STATUS_ICON: Record<string, string> = {
|
||||
done: '\u2713',
|
||||
failed: '\u2717',
|
||||
pending: '\u25CB',
|
||||
blocked: '\u25A0',
|
||||
cancelled: '\u2014',
|
||||
running: '\u25B6',
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
done: 'var(--green, #27ae60)',
|
||||
failed: 'var(--red, #e74c3c)',
|
||||
pending: 'var(--text-secondary, #888)',
|
||||
blocked: 'var(--yellow, #f39c12)',
|
||||
cancelled: 'var(--text-dim, #555)',
|
||||
running: 'var(--accent, #3498db)',
|
||||
}
|
||||
|
||||
export function WorkItemRecoveryPanel({ data, onResume, onCancel }: WorkItemRecoveryPanelProps) {
|
||||
const [dismissed, setDismissed] = useState<Set<string>>(new Set())
|
||||
|
||||
if (!data.interrupted.length && !data.active_recoveries.length) return null
|
||||
|
||||
const visible = data.interrupted.filter(w => !dismissed.has(w.parent_task_id))
|
||||
if (!visible.length && !data.active_recoveries.length) return null
|
||||
|
||||
return (
|
||||
<div className="wfr-panel">
|
||||
{visible.map(wf => {
|
||||
const isRecovering = data.active_recoveries.includes(wf.parent_task_id)
|
||||
const doneCount = wf.work_items.filter(item => item.status === 'done').length
|
||||
const failedCount = wf.work_items.filter(item => item.interrupted || item.status === 'failed').length
|
||||
|
||||
return (
|
||||
<div key={wf.parent_task_id} className="wfr-card">
|
||||
<div className="wfr-header">
|
||||
<span className="wfr-icon">⚠</span>
|
||||
<div className="wfr-header-text">
|
||||
<span className="wfr-title">Interrupted: {wf.title}</span>
|
||||
<span className="wfr-subtitle">
|
||||
{doneCount}/{wf.work_items.length} work items done, {failedCount} interrupted
|
||||
{wf.profile && <> · {wf.profile}</>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wfr-work-items">
|
||||
{wf.work_items.map(item => (
|
||||
<div key={item.work_item_projection_id} className={`wfr-work-item wfr-work-item--${item.status}`}>
|
||||
<span className="wfr-work-item-icon" style={{ color: STATUS_COLOR[item.status] || STATUS_COLOR.pending }}>
|
||||
{STATUS_ICON[item.status] || STATUS_ICON.pending}
|
||||
</span>
|
||||
<span className="wfr-work-item-title">{item.title}</span>
|
||||
{item.interrupted && <span className="wfr-work-item-badge">interrupted</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="wfr-actions">
|
||||
{isRecovering ? (
|
||||
<span className="wfr-recovering">
|
||||
<span className="spinner-inline" /> Recovering...
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button className="wfr-btn wfr-btn--resume" onClick={() => onResume(wf.parent_task_id)}>
|
||||
Resume
|
||||
</button>
|
||||
<button className="wfr-btn wfr-btn--cancel" onClick={() => onCancel(wf.parent_task_id)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="wfr-btn wfr-btn--dismiss" onClick={() => setDismissed(prev => new Set(prev).add(wf.parent_task_id))}>
|
||||
Dismiss
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { AgentInfo, OrgInfoPayload, SavedOrgSummary } from '../types/visual'
|
||||
import { WorkItemRecoveryPanel } from './WorkItemRecoveryPanel'
|
||||
import type { ChatMessage, CheckpointReplyMetadata, OutgoingAttachmentPayload } from '../types/chat'
|
||||
import type { KanbanTask, Session, TaskPreferredAgent } from '../types/kanban'
|
||||
import type { BoardStoreState } from '../kanban/BoardStore'
|
||||
@@ -253,7 +252,7 @@ interface WorkspacePageProps {
|
||||
*/
|
||||
onContinueInNewChat?: (mode: 'task' | 'company' | 'org' | 'custom', companyProfile?: 'corporate' | 'custom', orgId?: string) => void
|
||||
onSessionStop?: (taskId: string) => void
|
||||
onSessionResume?: (taskId: string) => void
|
||||
onSessionResume?: (taskId: string, runtimeSessionId?: string, checkpointId?: string) => void
|
||||
onSessionComplete?: (taskId: string) => void
|
||||
onLoadSessionDetail?: (
|
||||
taskId: string,
|
||||
@@ -263,9 +262,6 @@ interface WorkspacePageProps {
|
||||
onCollabSync?: () => void
|
||||
orgInfoData?: OrgInfoPayload | null
|
||||
onNavigateToOrg?: () => void
|
||||
recoveryStatus?: any
|
||||
onRecoveryResume?: (parentTaskId: string) => void
|
||||
onRecoveryCancel?: (parentTaskId: string) => void
|
||||
commsState?: import('../lib/wsClient').CommsStatePayload | null
|
||||
commsMessage?: import('../lib/wsClient').CommsMessagePayload | null
|
||||
onCommsRefresh?: (opts?: { task_id?: string; session_id?: string; project_id?: string }) => void
|
||||
@@ -305,9 +301,6 @@ export function WorkspacePage({
|
||||
onCollabSync,
|
||||
orgInfoData,
|
||||
onNavigateToOrg,
|
||||
recoveryStatus,
|
||||
onRecoveryResume,
|
||||
onRecoveryCancel,
|
||||
commsState,
|
||||
commsMessage,
|
||||
onCommsRefresh,
|
||||
@@ -988,13 +981,21 @@ export function WorkspacePage({
|
||||
|
||||
const handleResume = useCallback(() => {
|
||||
const targetSession = activeConversation.runtimeSession ?? activeConversation.displaySession ?? activeSession
|
||||
const targetTaskId = targetSession?.resumeParentTaskId ?? targetSession?.taskId ?? activeSessionId
|
||||
if (targetTaskId) onSessionResume?.(targetTaskId)
|
||||
const uiTaskId = activeSessionId ?? targetSession?.taskId
|
||||
const runtimeSessionId = targetSession?.resumeParentSessionId
|
||||
?? targetSession?.parentSessionId
|
||||
?? targetSession?.sessionId
|
||||
if (uiTaskId) {
|
||||
onSessionResume?.(uiTaskId, runtimeSessionId, targetSession?.pendingRuntimeCheckpointId)
|
||||
}
|
||||
}, [activeConversation.runtimeSession, activeConversation.displaySession, activeSession, activeSessionId, onSessionResume])
|
||||
|
||||
const handleResumeTask = useCallback((taskId: string) => {
|
||||
const session = sessions.find(s => s.taskId === taskId)
|
||||
onSessionResume?.(session?.resumeParentTaskId ?? taskId)
|
||||
const runtimeSessionId = session?.resumeParentSessionId
|
||||
?? session?.parentSessionId
|
||||
?? session?.sessionId
|
||||
onSessionResume?.(taskId, runtimeSessionId, session?.pendingRuntimeCheckpointId)
|
||||
}, [sessions, onSessionResume])
|
||||
|
||||
const handleCompleteTask = useCallback((taskId: string) => {
|
||||
@@ -1034,8 +1035,11 @@ export function WorkspacePage({
|
||||
}
|
||||
const targetTaskId = activeSessionId
|
||||
if (!targetTaskId) return
|
||||
const checkpointReplyId = String(latestPendingCheckpointReply?.response_to_checkpoint_id ?? '').trim()
|
||||
const runtimeSession = activeConversation.runtimeSession ?? activeConversation.displaySession ?? activeSession
|
||||
const runtimeCheckpointId = String(runtimeSession?.pendingRuntimeCheckpointId ?? '').trim()
|
||||
let outgoingMetadata = latestPendingCheckpointReply
|
||||
?? (runtimeCheckpointId ? { response_to_checkpoint_id: runtimeCheckpointId } : undefined)
|
||||
const checkpointReplyId = String(outgoingMetadata?.response_to_checkpoint_id ?? '').trim()
|
||||
if (!checkpointReplyId) {
|
||||
const uiMessageId = makeOptimisticUserMessageId()
|
||||
outgoingMetadata = { ...(latestPendingCheckpointReply ?? {}), ui_message_id: uiMessageId }
|
||||
@@ -1050,7 +1054,7 @@ export function WorkspacePage({
|
||||
}
|
||||
dispatchSessionSend(targetTaskId, content, attachments, outgoingMetadata)
|
||||
},
|
||||
[effectiveView.kind, activeSessionId, activeConversation.displaySession, activeSession, latestPendingCheckpointReply, chatStore, dispatchSessionSend, onSecretarySend],
|
||||
[effectiveView.kind, activeSessionId, activeConversation.runtimeSession, activeConversation.displaySession, activeSession, latestPendingCheckpointReply, chatStore, dispatchSessionSend, onSecretarySend],
|
||||
)
|
||||
|
||||
// ── MessageList send (checkpoint replies) ──
|
||||
@@ -1114,13 +1118,6 @@ export function WorkspacePage({
|
||||
{/* Middle column: Kanban Board (hidden when panel maximized) */}
|
||||
{panelState !== 'maximized' && (
|
||||
<div className="workspace-board">
|
||||
{recoveryStatus && onRecoveryResume && onRecoveryCancel && (
|
||||
<WorkItemRecoveryPanel
|
||||
data={recoveryStatus}
|
||||
onResume={onRecoveryResume}
|
||||
onCancel={onRecoveryCancel}
|
||||
/>
|
||||
)}
|
||||
{agents.length > 0 && <AgentStatusBar agents={agents} tasks={boardStore.tasks} />}
|
||||
{isCompanyMode ? (
|
||||
boardStore.activeBoard && activeSession && (
|
||||
@@ -1206,7 +1203,6 @@ export function WorkspacePage({
|
||||
onCommsRefresh={onCommsRefresh ? () => onCommsRefresh({ session_id: activeSession?.sessionId || undefined, project_id: projectId || undefined }) : undefined}
|
||||
onCommsReadMessage={onCommsReadMessage}
|
||||
orgInfoData={orgInfoData ?? null}
|
||||
recoveryStatus={recoveryStatus ?? null}
|
||||
canShowTeamTab={canShowTeamTab}
|
||||
onTeamStopRun={activeSessionId ? () => onSessionStop?.(activeSessionId) : undefined}
|
||||
onTitleChange={onTitleChange}
|
||||
|
||||
@@ -1605,132 +1605,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Work Item Recovery Panel ────────────────────────────────────────── */
|
||||
|
||||
.wfr-panel {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.wfr-card {
|
||||
background: color-mix(in srgb, var(--yellow, #f39c12) 8%, var(--bg-secondary));
|
||||
border: 1px solid color-mix(in srgb, var(--yellow, #f39c12) 25%, transparent);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wfr-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wfr-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfr-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wfr-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wfr-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.wfr-work-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wfr-work-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.wfr-work-item-icon {
|
||||
font-size: 12px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wfr-work-item-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wfr-work-item-badge {
|
||||
font-size: 9px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: color-mix(in srgb, var(--red, #e74c3c) 15%, transparent);
|
||||
color: var(--red, #e74c3c);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wfr-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wfr-btn {
|
||||
padding: 5px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wfr-btn--resume {
|
||||
background: var(--green, #27ae60);
|
||||
color: #fff;
|
||||
}
|
||||
.wfr-btn--resume:hover { opacity: 0.85; }
|
||||
|
||||
.wfr-btn--cancel {
|
||||
background: var(--red, #e74c3c);
|
||||
color: #fff;
|
||||
}
|
||||
.wfr-btn--cancel:hover { opacity: 0.85; }
|
||||
|
||||
.wfr-btn--dismiss {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.wfr-btn--dismiss:hover { color: var(--text); }
|
||||
|
||||
.wfr-recovering {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════
|
||||
Kanban empty-state
|
||||
═══════════════════════════════════════════════════════ */
|
||||
|
||||
Reference in New Issue
Block a user