Files
codex-office/src/components/SidePanel.tsx
T
JianMiauandClaude Fable 5 7d9d477166 建立 Codex Office 虛擬辦公室(模擬 + 實況雙模式)
摘要:
以 claude-office 為基底改造的 Codex CLI 版虛擬辦公室:終端機綠 + 冷灰主題、
>_ 游標標誌,實況模式改為監看 ~/.codex/sessions/ 的 rollout transcripts,
依 cwd 將 session 分組成專案顯示。

根本原因:
既有 claude-office 只能監看 Claude Code 活動,Codex CLI 的 session 格式
(rollout JSONL:session_meta / turn_context / event_msg / response_item)
與存放結構(YYYY/MM/DD 日期目錄)完全不同,需要獨立的轉譯器。

影響:
新專案,與 claude-office 並存:web 5182(0.0.0.0)、watch 5181(僅 127.0.0.1),
與 5179/5180 不衝突,可同時常駐 PM2。

修法:
- server/watch.mjs 重寫:遞迴掃描日期目錄、tail 新增位元組、
  由 session_meta/turn_context 的 cwd 分組專案,
  custom_tool_call/function_call/MCP → 工具事件,agent_message/task_complete → 回覆
- 主題改造:constants 配色(石墨綠)、Sora/Manrope/JetBrains Mono 字型、
  >_ 閃爍游標標誌、agents 改為 Codex/Sol/Nova/Mini、工具改為 exec/apply_patch 等
- live.ts 新增 thinking 事件(對應 reasoning),port 5181

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:02:32 +08:00

122 lines
4.2 KiB
TypeScript

import { STATUS_COLORS, STATUS_LABELS } from "@/lib/constants";
import { generateAppearance } from "@/lib/appearance";
import { useOfficeStore } from "@/store/office-store";
import { getDirector } from "@/sim/runtime";
import { Pawn } from "./Pawn";
const ROLE_LABELS = { lead: "Lead Agent", agent: "Agent", subagent: "Subagent" } as const;
export function SidePanel() {
const selectedAgentId = useOfficeStore((s) => s.selectedAgentId);
const agent = useOfficeStore((s) => (s.selectedAgentId ? s.agents.get(s.selectedAgentId) : undefined));
const agents = useOfficeStore((s) => s.agents);
const events = useOfficeStore((s) => s.events);
const selectAgent = useOfficeStore((s) => s.selectAgent);
const mode = useOfficeStore((s) => s.mode);
return (
<aside className="side-panel">
{selectedAgentId && agent ? (
<div className="agent-card">
<div className="agent-card-head">
<svg viewBox="-30 -42 60 68" width="64" height="72">
<Pawn appearance={generateAppearance(agent.id)} pose="stand" motion="none" />
</svg>
<div>
<h2>
{agent.role === "lead" && <span className="mark"> </span>}
{agent.name}
</h2>
<span className="role-badge">{ROLE_LABELS[agent.role]}</span>
<code className="model-tag">{agent.model}</code>
</div>
<button className="close-btn" onClick={() => selectAgent(null)}>
</button>
</div>
<div className="agent-stat-row">
<span className="status-chip" style={{ backgroundColor: `${STATUS_COLORS[agent.status]}22`, color: STATUS_COLORS[agent.status] }}>
{STATUS_LABELS[agent.status]}
</span>
{agent.currentTool && <code className="tool-now">{agent.currentTool.name}()</code>}
</div>
<dl className="agent-facts">
<div>
<dt>工具調用</dt>
<dd>{agent.toolCallCount} </dd>
</div>
<div>
<dt>所在區域</dt>
<dd>{agent.zone}</dd>
</div>
{agent.parentId && (
<div>
<dt>上級</dt>
<dd>
<button className="link-btn" onClick={() => selectAgent(agent.parentId)}>
{agents.get(agent.parentId)?.name ?? agent.parentId}
</button>
</dd>
</div>
)}
{agent.childIds.length > 0 && (
<div>
<dt>Subagents</dt>
<dd>
{agent.childIds.map((cid) => (
<button key={cid} className="link-btn" onClick={() => selectAgent(cid)}>
{agents.get(cid)?.name ?? cid}
</button>
))}
</dd>
</div>
)}
</dl>
{agent.toolHistory.length > 0 && (
<div className="tool-history">
<h3>最近工具</h3>
<div className="tool-chips">
{agent.toolHistory.map((t, i) => (
<code key={i}>{t}</code>
))}
</div>
</div>
)}
{agent.role !== "subagent" && mode === "sim" && (
<button
className="btn assign-btn"
onClick={() => {
if (!getDirector().assignTask(agent.id)) {
useOfficeStore.getState().addEvent("💬", `${agent.name} 正忙,稍後再指派`);
}
}}
>
📋 指派任務
</button>
)}
</div>
) : (
<div className="panel-hint">
<p>點擊辦公室裡的 agent 查看詳情</p>
</div>
)}
<div className="event-feed">
<h3>事件紀錄</h3>
<ul>
{events.map((e) => (
<li key={e.at}>
<span className="event-icon">{e.icon}</span>
{e.text}
</li>
))}
</ul>
</div>
</aside>
);
}