feat: harden company resume, external sessions, and Office UI i18n

Improve suspend/resume identity continuity and external broker session handling, add Office UI internationalization with rebuilt assets, and expand company org configs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
小东
2026-07-16 09:20:40 +08:00
parent eb934cf2bd
commit cb6b8ed6e1
32 changed files with 2562 additions and 446 deletions
@@ -1,7 +1,8 @@
import { useMemo } from 'react'
import type { AgentInfo } from '../types/visual'
import type { AgentAnimStatus, KanbanTask } from '../types/kanban'
import { AGENT_STATUS_LABEL } from '../types/kanban'
import type { AgentInfo } from '../types/visual'
import type { AgentAnimStatus, KanbanTask } from '../types/kanban'
import { AGENT_STATUS_LABEL } from '../types/kanban'
import { useI18n } from '../i18n'
interface AgentStatusBarProps {
agents: AgentInfo[]
@@ -15,8 +16,9 @@ interface AgentState {
taskDisplayId?: string
}
export function AgentStatusBar({ agents, tasks }: AgentStatusBarProps) {
const agentStates = useMemo<AgentState[]>(() => {
export function AgentStatusBar({ agents, tasks }: AgentStatusBarProps) {
const { t, translateMaybe } = useI18n()
const agentStates = useMemo<AgentState[]>(() => {
const tasksById = new Map(tasks.map((task) => [task.id, task]))
return agents.map(agent => {
// Find the first active (non-idle) task for this agent
@@ -38,36 +40,41 @@ export function AgentStatusBar({ agents, tasks }: AgentStatusBarProps) {
const activeCount = agentStates.filter(s => s.status !== 'idle').length
return (
<div className="agent-status-bar">
<span className="agent-status-summary">
{activeCount > 0
? `${activeCount}/${agents.length} active`
: `${agents.length} agent${agents.length !== 1 ? 's' : ''}`}
</span>
<div className="agent-status-chips">
{agentStates.map(({ agent, status, currentTool, taskDisplayId }) => (
<div
key={agent.agent_id}
className={`agent-status-chip status-${status}`}
title={`${agent.name}: ${status === 'tool_active' && currentTool ? currentTool : AGENT_STATUS_LABEL[status]}${taskDisplayId ? ` (${taskDisplayId})` : ''}`}
>
<span className="agent-status-avatar">{agent.name.charAt(0).toUpperCase()}</span>
<span className="agent-status-name">{agent.name}</span>
{status !== 'idle' && (
<>
<span className="kanban-runtime-dot" />
<span className="agent-status-detail">
{status === 'tool_active' && currentTool ? currentTool : AGENT_STATUS_LABEL[status]}
</span>
</>
)}
{taskDisplayId && (
<span className="agent-status-task">{taskDisplayId}</span>
)}
</div>
))}
</div>
</div>
)
}
return (
<div className="agent-status-bar">
<span className="agent-status-summary">
{activeCount > 0
? t('agent.summary.active', { active: activeCount, total: agents.length })
: t(agents.length !== 1 ? 'agent.summary.countPlural' : 'agent.summary.count', { count: agents.length })}
</span>
<div className="agent-status-chips">
{agentStates.map(({ agent, status, currentTool, taskDisplayId }) => {
const statusLabel = status === 'tool_active' && currentTool
? currentTool
: translateMaybe('agent.status', status) || AGENT_STATUS_LABEL[status]
return (
<div
key={agent.agent_id}
className={`agent-status-chip status-${status}`}
title={`${agent.name}: ${statusLabel}${taskDisplayId ? ` (${taskDisplayId})` : ''}`}
>
<span className="agent-status-avatar">{agent.name.charAt(0).toUpperCase()}</span>
<span className="agent-status-name">{agent.name}</span>
{status !== 'idle' && (
<>
<span className="kanban-runtime-dot" />
<span className="agent-status-detail">
{statusLabel}
</span>
</>
)}
{taskDisplayId && (
<span className="agent-status-task">{taskDisplayId}</span>
)}
</div>
)
})}
</div>
</div>
)
}
@@ -1,4 +1,5 @@
import type { KanbanBoard } from '../types/kanban'
import { useI18n } from '../i18n'
interface BoardSelectorProps {
boards: KanbanBoard[]
@@ -7,6 +8,7 @@ interface BoardSelectorProps {
}
export function BoardSelector({ boards, activeBoardId, onSelect }: BoardSelectorProps) {
const { translateMaybe } = useI18n()
return (
<div className="board-selector">
<div className="board-tabs">
@@ -18,7 +20,7 @@ export function BoardSelector({ boards, activeBoardId, onSelect }: BoardSelector
onClick={() => onSelect(b.id)}
>
<span className="board-tab-dot" style={{ background: b.color }} />
{b.name}
{translateMaybe('kanban.column', b.name) || b.name}
</button>
))}
</div>
@@ -3,6 +3,7 @@ import type { AgentInfo } from '../types/visual'
import { PRIORITY_META, AGENT_STATUS_LABEL, type KanbanTask } from '../types/kanban'
import { getWorkItemRoleLabel, humanizeWorkItemRoleId } from '../lib/workItemIdentity'
import { getLinkedRuntimeTaskId } from '../lib/workItemRuntimeIds'
import { useI18n } from '../i18n'
const STATUS_BADGE: Record<string, { label: string; color: string }> = {
todo: { label: 'To do', color: '#9ca3af' },
@@ -32,7 +33,8 @@ interface KanbanCardProps {
}
export function KanbanCard({ task, index, agents, officeMap, companyMode, isSelected, onClick, onStart }: KanbanCardProps) {
const assignees = task.assigneeIds
const { t, translateMaybe } = useI18n()
const assignees = task.assigneeIds
.map(id => agents.find(a => a.agent_id === id))
.filter(Boolean) as AgentInfo[]
const priority = task.priority ? PRIORITY_META[task.priority] : null
@@ -54,6 +56,9 @@ export function KanbanCard({ task, index, agents, officeMap, companyMode, isSele
const blockerLabel = (task.blockedReason ?? '').trim()
const reworkLabel = (task.reworkFeedback ?? '').trim()
const linkedRuntimeTaskId = getLinkedRuntimeTaskId(task)
const runtimeStatusLabel = task.agentStatus
? (translateMaybe('agent.status', task.agentStatus) || AGENT_STATUS_LABEL[task.agentStatus] || task.agentStatus)
: ''
return (
<Draggable draggableId={task.id} index={index} isDragDisabled={!!companyMode}>
@@ -66,22 +71,22 @@ export function KanbanCard({ task, index, agents, officeMap, companyMode, isSele
data-task-id={task.id}
onMouseUp={e => { if (e.button === 0 && !snapshot.isDragging) onClick(task) }}
>
<div className="kanban-card-top">
<span className="kanban-card-id">{task.displayId}</span>
{statusBadge && (
<span className="kanban-status-badge" style={{ color: statusBadge.color }}>
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: statusBadge.color, marginRight: 3 }} />
{statusBadge.label}
</span>
)}
{depCount > 0 && (
<span className="kanban-dep-badge" title={`${depCount} upstream dep(s)`}>{depCount} dep</span>
)}
{crossOffice && <span className="kanban-cross-badge" title="Cross-office">&#x21C4;</span>}
<div className="kanban-card-top">
<span className="kanban-card-id">{task.displayId}</span>
{statusBadge && (
<span className="kanban-status-badge" style={{ color: statusBadge.color }}>
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: statusBadge.color, marginRight: 3 }} />
{translateMaybe('kanban.status', phaseBadge) || statusBadge.label}
</span>
)}
{depCount > 0 && (
<span className="kanban-dep-badge" title={t('kanban.upstreamDeps', { count: depCount })}>{t('kanban.dep', { count: depCount })}</span>
)}
{crossOffice && <span className="kanban-cross-badge" title={t('kanban.crossOffice')}>&#x21C4;</span>}
{onStart && (
<button
className="kanban-start-btn"
title={companyMode ? 'Start Work Item' : 'Start task'}
title={companyMode ? t('kanban.startWorkItem') : t('kanban.startTask')}
onClick={e => { e.stopPropagation(); onStart(task.id) }}
>
&#x25B6;
@@ -94,34 +99,34 @@ export function KanbanCard({ task, index, agents, officeMap, companyMode, isSele
{(roleLabel || employee?.name || gate?.type || task.originChannel || task.workItemProjectionId) && (
<div className="kanban-card-meta-row">
{roleLabel && (
<span className="kanban-meta-badge kanban-role-badge" title={`Role: ${roleLabel}`}>
<span className="kanban-meta-badge kanban-role-badge" title={`${t('common.role')}: ${roleLabel}`}>
{roleLabel}
</span>
)}
{employee?.name && (
<span className="kanban-meta-badge kanban-employee-badge" title={`Employee: ${employee.name}${employee.category ? ` (${employee.category})` : ''}`}>
<span className="kanban-meta-badge kanban-employee-badge" title={`${t('common.employee')}: ${employee.name}${employee.category ? ` (${employee.category})` : ''}`}>
<span className="kanban-meta-icon">&#x1F464;</span>
{employee.name}
</span>
)}
{employee.name}
</span>
)}
{task.workItemProjectionId && (
<span className="kanban-meta-badge kanban-projection-badge" title={`Projection: ${task.workItemProjectionId}`}>
<span className="kanban-meta-badge kanban-projection-badge" title={`${t('common.projection')}: ${task.workItemProjectionId}`}>
{task.workItemProjectionId}
</span>
)}
{gate?.type && (
<span className={`kanban-meta-badge kanban-gate-badge kanban-gate-${gate.type}`} title={`Gate: ${gate.type}${gate.reviewerRole ? ` by ${gate.reviewerRole}` : ''}`}>
{gate.type === 'review' ? '\u2709' : gate.type === 'approval' ? '\u2713' : '\u270B'}
{gate.type}
</span>
)}
{gate?.type && (
<span className={`kanban-meta-badge kanban-gate-badge kanban-gate-${gate.type}`} title={`${t('common.gate')}: ${gate.type}${gate.reviewerRole ? ` by ${gate.reviewerRole}` : ''}`}>
{gate.type === 'review' ? '\u2709' : gate.type === 'approval' ? '\u2713' : '\u270B'}
{gate.type}
</span>
)}
{task.originChannel && (
<span className="kanban-meta-badge kanban-origin-badge" title={`Origin: ${task.originChannel}`}>
<span className="kanban-meta-badge kanban-origin-badge" title={`${t('common.origin')}: ${task.originChannel}`}>
#{task.originChannel}
</span>
)}
{managerLabel && (
<span className="kanban-meta-badge" title={`Manager: ${managerLabel}`}>
<span className="kanban-meta-badge" title={`${t('common.manager')}: ${managerLabel}`}>
{managerLabel}
</span>
)}
@@ -133,8 +138,8 @@ export function KanbanCard({ task, index, agents, officeMap, companyMode, isSele
{blockerLabel && <span className="kanban-tag">{blockerLabel}</span>}
{reworkLabel && <span className="kanban-tag">{reworkLabel}</span>}
{linkedRuntimeTaskId && (
<span className="kanban-tag" title={`Execution Turn: ${linkedRuntimeTaskId}`}>
Runtime
<span className="kanban-tag" title={t('kanban.executionTurn', { id: linkedRuntimeTaskId })}>
{t('common.runtime')}
</span>
)}
</div>
@@ -144,10 +149,10 @@ export function KanbanCard({ task, index, agents, officeMap, companyMode, isSele
<div className={`kanban-card-runtime status-${task.agentStatus}`}>
<span className="kanban-runtime-dot" />
<span className="kanban-runtime-label">
{task.agentStatus === 'tool_active' && task.currentTool
? task.currentTool
: AGENT_STATUS_LABEL[task.agentStatus!] ?? task.agentStatus}
</span>
{task.agentStatus === 'tool_active' && task.currentTool
? task.currentTool
: runtimeStatusLabel}
</span>
</div>
)}
@@ -1,8 +1,9 @@
import { useCallback, useRef, useState } from 'react'
import { Droppable } from '@hello-pangea/dnd'
import type { AgentInfo } from '../types/visual'
import type { KanbanColumn as KanbanColumnType, KanbanTask } from '../types/kanban'
import { KanbanCard } from './KanbanCard'
import type { AgentInfo } from '../types/visual'
import type { KanbanColumn as KanbanColumnType, KanbanTask } from '../types/kanban'
import { KanbanCard } from './KanbanCard'
import { useI18n } from '../i18n'
interface KanbanColumnProps {
column: KanbanColumnType
@@ -17,7 +18,8 @@ interface KanbanColumnProps {
}
export function KanbanColumn({ column, tasks, agents, officeMap, companyMode, selectedTaskId, onCardClick, onStartTask, onQuickCreate }: KanbanColumnProps) {
const [adding, setAdding] = useState(false)
const { t, translateMaybe } = useI18n()
const [adding, setAdding] = useState(false)
const [draft, setDraft] = useState('')
const committedRef = useRef(false)
@@ -35,12 +37,12 @@ export function KanbanColumn({ column, tasks, agents, officeMap, companyMode, se
return (
<div className="kanban-column">
<div className="kanban-column-header">
<span className="kanban-col-dot" style={{ background: column.color }} />
<span className="kanban-col-label">{column.name}</span>
<span className="kanban-col-count">{tasks.length}</span>
{onQuickCreate && (
<button className="kanban-col-add" title="Add task" onClick={() => { committedRef.current = false; setAdding(true) }}>+</button>
)}
<span className="kanban-col-dot" style={{ background: column.color }} />
<span className="kanban-col-label">{translateMaybe('kanban.column', column.name) || column.name}</span>
<span className="kanban-col-count">{tasks.length}</span>
{onQuickCreate && (
<button className="kanban-col-add" title={t('kanban.addTask')} onClick={() => { committedRef.current = false; setAdding(true) }}>+</button>
)}
</div>
{adding && (
@@ -54,7 +56,7 @@ export function KanbanColumn({ column, tasks, agents, officeMap, companyMode, se
if (e.key === 'Escape') { committedRef.current = true; setDraft(''); setAdding(false) }
}}
onBlur={commitAdd}
placeholder="Task title..."
placeholder={t('kanban.taskTitle')}
autoFocus
/>
</div>