fix: unify company runtime recovery lifecycle

This commit is contained in:
LZH-YS1998
2026-07-14 14:35:43 +08:00
parent 5e02364eb4
commit b8202bbe9e
56 changed files with 8753 additions and 3542 deletions
+7 -23
View File
@@ -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">&#x26A0;</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 && <> &middot; {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
═══════════════════════════════════════════════════════ */