Merge PR #9: harden company resume, external sessions, and Office UI i18n

Merges xiaodong-l's hardening work: company suspend-checkpoint resume
reconciliation, external session token lifecycle fixes, Windows launch
shims, codex shell_environment_policy forwarding, Office UI i18n
(en/zh-CN), and two org presets. frontend_dist is rebuilt from the
merged sources in this commit so the #11 runtime-status fixes and the
i18n bundle coexist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	opc/plugins/office_ui/frontend_dist/assets/index-BgyI65M_.js
#	opc/plugins/office_ui/frontend_dist/index.html
This commit is contained in:
LZH-YS1998
2026-07-21 15:34:28 +08:00
32 changed files with 2562 additions and 445 deletions
+73 -49
View File
@@ -27,6 +27,7 @@ import { extractSessionRecruitmentByRole, sessionChannelId } from './lib/session
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './lib/turnIdentity'
import { unassignAgent } from './game/map/OfficeStore'
import type { AgentAnimStatus, EmployeeAssignment, KanbanPhase, KanbanTask, RoleAggregatedStatus, RoleWorkItemSummary, Session, TaskPreferredAgent } from './types/kanban'
import { useI18n } from './i18n'
function readOutdoorOverrideUi(): 'auto' | 'day' | 'night' {
try {
@@ -444,6 +445,7 @@ function MaybeExecutionPanel({ taskId, sessions, agents, onClose }: {
}
export default function App() {
const { locale, setLocale, t, translateMaybe } = useI18n()
const bridgeRef = useRef(new GameBridge())
useMemo(() => registerTestRunner(bridgeRef.current), [])
const clientRef = useRef<VisualSocketClient | null>(null)
@@ -2397,26 +2399,47 @@ export default function App() {
<div className="topbar-center">
<div className="page-nav">
<button className={`page-nav-btn${activePage === 'workspace' ? ' active' : ''}`} onClick={() => setActivePage('workspace')}>
Workspace
{t('app.page.workspace')}
{(() => {
const total = chatStore.channels.reduce((sum, ch) => sum + chatStore.getUnreadCount(ch.id), 0)
return total > 0 ? <span className="nav-unread-badge">{total > 99 ? '99+' : total}</span> : null
})()}
</button>
<button className={`page-nav-btn${activePage === 'office' ? ' active' : ''}`} onClick={() => setActivePage('office')}>Office</button>
<button className={`page-nav-btn${activePage === 'org' ? ' active' : ''}`} onClick={() => setActivePage('org')}>Org</button>
<button className={`page-nav-btn${activePage === 'office' ? ' active' : ''}`} onClick={() => setActivePage('office')}>{t('app.page.office')}</button>
<button className={`page-nav-btn${activePage === 'org' ? ' active' : ''}`} onClick={() => setActivePage('org')}>{t('app.page.org')}</button>
</div>
<div className="stat-chips">
<span className="stat-chip"><b>{metrics.totalAgents}</b> agents</span>
<span className="stat-chip"><b>{metrics.totalSkills}</b> skills</span>
<span className="stat-chip"><b>{boardStore.getOpenTaskCount()}</b> tasks</span>
<span className="stat-chip"><b>{metrics.totalAgents}</b> {t('app.metric.agents')}</span>
<span className="stat-chip"><b>{metrics.totalSkills}</b> {t('app.metric.skills')}</span>
<span className="stat-chip"><b>{boardStore.getOpenTaskCount()}</b> {t('app.metric.tasks')}</span>
</div>
</div>
<div className="topbar-right">
<div className="language-toggle" role="group" aria-label={t('language.label')} title={t('language.label')}>
<button
type="button"
data-locale="en"
aria-pressed={locale === 'en'}
className={`language-toggle-btn${locale === 'en' ? ' active' : ''}`}
onClick={() => setLocale('en')}
>
{t('language.english')}
</button>
<button
type="button"
data-locale="zh-CN"
aria-pressed={locale === 'zh-CN'}
className={`language-toggle-btn${locale === 'zh-CN' ? ' active' : ''}`}
onClick={() => setLocale('zh-CN')}
>
{t('language.chinese')}
</button>
</div>
<select
className="theme-select"
value={outdoorOverride}
title="Outdoor lighting override"
title={t('outdoor.title')}
aria-label={t('outdoor.title')}
onChange={(e) => {
const v = e.target.value as 'auto' | 'day' | 'night'
setOutdoorOverride(v)
@@ -2434,20 +2457,20 @@ export default function App() {
bridgeRef.current.syncOutdoorLighting()
}}
>
<option value="auto">Outdoor auto</option>
<option value="day">Outdoor day</option>
<option value="night">Outdoor night</option>
<option value="auto">{t('outdoor.auto')}</option>
<option value="day">{t('outdoor.day')}</option>
<option value="night">{t('outdoor.night')}</option>
</select>
<select className="theme-select" value={theme} onChange={(e) => setTheme(e.target.value as ThemeName)}>
<option value="midnight">Midnight</option>
<option value="neon">Neon</option>
<option value="paper">Paper</option>
<option value="retro">Retro</option>
<option value="terminal">Terminal</option>
<option value="cozy">Cozy</option>
<option value="openopc">OpenOPC</option>
<option value="midnight">{t('theme.midnight')}</option>
<option value="neon">{t('theme.neon')}</option>
<option value="paper">{t('theme.paper')}</option>
<option value="retro">{t('theme.retro')}</option>
<option value="terminal">{t('theme.terminal')}</option>
<option value="cozy">{t('theme.cozy')}</option>
<option value="openopc">{t('theme.openopc')}</option>
</select>
<button className={`icon-btn ${showDevTools ? 'active' : ''}`} onClick={() => setShowDevTools((v) => !v)} title="Developer Tools">
<button className={`icon-btn ${showDevTools ? 'active' : ''}`} onClick={() => setShowDevTools((v) => !v)} title={t('dev.tools')} aria-label={t('dev.tools')}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5.5 2L2 5.5 5.5 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M10.5 7L14 10.5 10.5 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
</div>
@@ -2602,14 +2625,14 @@ export default function App() {
{/* Phaser Game Canvas */}
<section className="canvas-wrap">
<PhaserGame bridge={bridgeRef.current} active={activePage === 'office'} />
<button className="canvas-float-btn" onClick={() => setShowSubagents((v) => !v)} title={showSubagents ? 'Hide sub-agents' : 'Show sub-agents'}>
<button className="canvas-float-btn" onClick={() => setShowSubagents((v) => !v)} title={showSubagents ? t('office.hideSubagents') : t('office.showSubagents')}>
{showSubagents ? '👥' : '👤'}
</button>
<button
className="sidebar-collapse-btn"
onClick={toggleSidebar}
title={sidebarCollapsed ? 'Show side panel' : 'Hide side panel'}
aria-label={sidebarCollapsed ? 'Show side panel' : 'Hide side panel'}
title={sidebarCollapsed ? t('office.showSidePanel') : t('office.hideSidePanel')}
aria-label={sidebarCollapsed ? t('office.showSidePanel') : t('office.hideSidePanel')}
>
<span className="collapse-glyph">{sidebarCollapsed ? '' : ''}</span>
</button>
@@ -2624,13 +2647,13 @@ export default function App() {
<div className="mode-info-bar">
<span className="mode-badge">{globalExecMode === 'company' ? `${globalExecMode}/${globalCompanyProfile}` : globalModeLabel}</span>
{isOrgMode ? (
<span className="mode-hint">Manage your team in the <b>Org</b> tab</span>
<span className="mode-hint">{t('office.modeHint.org')}</span>
) : (
<span className="mode-hint">Switch to <b>Org</b> mode to create or manage agents</span>
<span className="mode-hint">{t('office.modeHint.switch')}</span>
)}
</div>
<div className="section-label">Offices <span className="count-badge">{offices.length}</span></div>
<div className="section-label">{t('office.offices')} <span className="count-badge">{offices.length}</span></div>
<div className="office-cards">
{offices.map((office) => {
const deskCount = getOfficeDeskSeats(office.id).length
@@ -2652,14 +2675,14 @@ export default function App() {
) : (
<>
<span className="office-name">{office.name}</span>
<button className="office-edit-btn" title="Rename" onClick={(e) => { e.stopPropagation(); setEditingOfficeName(office.id); setOfficeNameDraft(office.name) }}></button>
<button className="office-edit-btn" title={t('office.rename')} onClick={(e) => { e.stopPropagation(); setEditingOfficeName(office.id); setOfficeNameDraft(office.name) }}></button>
</>
)}
<span className="office-capacity">{assignedCards.length}/{deskCount}</span>
</div>
<div className="office-agents">
{assignedCards.map(c => (
<span key={c.id} className="office-agent-chip" title={`${c.displayName}${c.seatId ?? 'no seat'}`} onClick={(e) => { e.stopPropagation(); selectAgent(c.id) }}>
<span key={c.id} className="office-agent-chip" title={`${c.displayName}${c.seatId ?? t('office.noSeat')}`} onClick={(e) => { e.stopPropagation(); selectAgent(c.id) }}>
{c.displayName.slice(0, 8)}
</span>
))}
@@ -2670,7 +2693,7 @@ export default function App() {
onClick={e => e.stopPropagation()}
onChange={e => { if (e.target.value) handleAssignAgent(office.id, e.target.value) }}
>
<option value="">+ Move here</option>
<option value="">{t('office.moveHere')}</option>
{otherAgents.map(a => (
<option key={a.id} value={a.id}>{a.displayName} ({offices.find(o => o.id === (cards.find(cc => cc.id === a.id)?.officeId))?.name ?? '?'})</option>
))}
@@ -2682,7 +2705,7 @@ export default function App() {
})}
</div>
<div className="section-label">Active Agents <span className="count-badge">{swarmAgents.length}</span></div>
<div className="section-label">{t('office.activeAgents')} <span className="count-badge">{swarmAgents.length}</span></div>
<div className="agent-list">
{swarmAgents.map((agent) => (
<div key={agent.agent_id} className={`agent-row ${selectedAgentId === agent.agent_id ? 'selected' : ''}`}>
@@ -2690,7 +2713,7 @@ export default function App() {
<span className={`dot ${agent.status}`} />
<div className="agent-info">
<span className="agent-name">{agent.name}</span>
<span className="agent-spec">{agent.specialties.slice(0, 2).join(' · ') || 'general'}</span>
<span className="agent-spec">{agent.specialties.slice(0, 2).join(' · ') || t('common.general')}</span>
</div>
</button>
{isOrgMode && (
@@ -2698,27 +2721,27 @@ export default function App() {
? <span className="agent-del" style={{ pointerEvents: 'none' }}><span className="spinner-inline" /></span>
: confirmDeleteId === agent.agent_id
? <span className="del-confirm">
<span className="del-confirm-label">Delete?</span>
<button className="del-confirm-yes" onClick={() => { setDeletingAgentId(agent.agent_id); setConfirmDeleteId(null); clientRef.current?.deleteAgent(agent.agent_id) }}>Yes</button>
<button className="del-confirm-no" onClick={() => setConfirmDeleteId(null)}>No</button>
<span className="del-confirm-label">{t('office.deleteQuestion')}</span>
<button className="del-confirm-yes" onClick={() => { setDeletingAgentId(agent.agent_id); setConfirmDeleteId(null); clientRef.current?.deleteAgent(agent.agent_id) }}>{t('common.yes')}</button>
<button className="del-confirm-no" onClick={() => setConfirmDeleteId(null)}>{t('common.no')}</button>
</span>
: <button className="agent-del" title={`Remove ${agent.name}`} onClick={() => setConfirmDeleteId(agent.agent_id)}>×</button>
: <button className="agent-del" title={t('office.removeAgent', { name: agent.name })} onClick={() => setConfirmDeleteId(agent.agent_id)}>×</button>
)}
</div>
))}
{swarmAgents.length === 0 && (
<div className="empty-state">No agents yet click a template above to spawn one.</div>
<div className="empty-state">{t('office.emptyAgents')}</div>
)}
</div>
{selectedCard && (
<div className="agent-detail">
<div className="agent-detail-name">{selectedCard.displayName}</div>
<div className="agent-detail-row"><span className="detail-label">State</span><span className="detail-value">{selectedCard.state}</span></div>
<div className="agent-detail-row"><span className="detail-label">Tool</span><span className="detail-value">{selectedCard.currentTool ?? '—'}</span></div>
<div className="agent-detail-row"><span className="detail-label">Task</span><span className="detail-value">{selectedCard.taskSummary ?? '—'}</span></div>
<div className="agent-detail">
<div className="agent-detail-name">{selectedCard.displayName}</div>
<div className="agent-detail-row"><span className="detail-label">{t('common.state')}</span><span className="detail-value">{translateMaybe('agent.status', selectedCard.state) || selectedCard.state}</span></div>
<div className="agent-detail-row"><span className="detail-label">{t('common.tool')}</span><span className="detail-value">{selectedCard.currentTool ?? '—'}</span></div>
<div className="agent-detail-row"><span className="detail-label">{t('common.task')}</span><span className="detail-value">{selectedCard.taskSummary ?? '—'}</span></div>
<div className="agent-detail-row">
<span className="detail-label">Office</span>
<span className="detail-label">{t('app.page.office')}</span>
<select
className="detail-select"
value={selectedCard.officeId}
@@ -2729,7 +2752,7 @@ export default function App() {
</select>
</div>
<div className="agent-detail-row">
<span className="detail-label">Seat</span>
<span className="detail-label">{t('common.seat')}</span>
<select
className="detail-select"
value={selectedCard.seatId ?? ''}
@@ -2754,8 +2777,8 @@ export default function App() {
{cards.length > swarmAgents.length && (
<>
<div className="section-label">
Characters
<button className="inline-btn" onClick={() => setShowSubagents((v) => !v)}>{showSubagents ? 'hide sub' : 'show sub'}</button>
{t('office.characters')}
<button className="inline-btn" onClick={() => setShowSubagents((v) => !v)}>{showSubagents ? t('office.hideSub') : t('office.showSub')}</button>
</div>
<div className="agent-list">
{cards.filter((c) => !swarmAgents.some((a) => a.agent_id === c.id)).map((card) => (
@@ -2777,28 +2800,29 @@ export default function App() {
{showDevTools && (
<div className="dev-overlay">
<div className="dev-header">
<span className="dev-title">Developer Tools</span>
<span className="dev-title">{t('dev.tools')}</span>
<button className="icon-btn" onClick={() => setShowDevTools(false)}></button>
</div>
<div className="dev-group">
<div className="dev-label">Connection</div>
<div className="dev-label">{t('dev.connection')}</div>
<div className="input-row">
<input value={wsUrlInput} onChange={(e) => setWsUrlInput(e.target.value)} placeholder="ws://..." />
<button className="send-btn" onClick={applyWsUrl}></button>
</div>
</div>
<div className="dev-group">
<div className="dev-label">Evolution Pipeline</div>
<div className="dev-label">{t('dev.evolution')}</div>
<div className="evo-pipeline">
{(['Trace', 'Reflect', 'Synthesize', 'Practice', 'Lifecycle'] as const).map((phase, i) => {
const key = phase.toLowerCase() as keyof typeof evolutionPhases
const active = key in evolutionPhases ? evolutionPhases[key as 'trace' | 'reflect' | 'synthesize'] : false
const phaseLabelKey = `dev.phase.${key}` as Parameters<typeof t>[0]
return (
<div key={phase} className="evo-phase-group">
{i > 0 && <div className="evo-connector" />}
<div className={`evo-node ${active ? 'active' : ''}`}>
<div className="evo-dot" />
<span className="evo-label">{phase}</span>
<span className="evo-label">{t(phaseLabelKey)}</span>
</div>
</div>
)
@@ -2815,7 +2839,7 @@ export default function App() {
</div>
<div className="dev-group">
<div className="dev-label">
Events
{t('dev.events')}
<select className="inline-select" value={eventTypeFilter} onChange={(e) => setEventTypeFilter(e.target.value)}>
{eventTypes.map((type) => <option key={type} value={type}>{type}</option>)}
</select>
@@ -2833,7 +2857,7 @@ export default function App() {
</div>
{Object.keys(snapshot?.channels ?? {}).length > 0 && (
<div className="dev-group">
<div className="dev-label">Channels</div>
<div className="dev-label">{t('dev.channels')}</div>
{Object.entries(snapshot?.channels ?? {}).map(([name, info]) => (
<div className="list-row" key={name}>
<span>{name}</span>