From 36bed1b0f193fd049687ce769a4988ab24c55d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 17 Apr 2026 19:57:58 -0400 Subject: [PATCH] feat(team): add TeamHeaderSummary strip + delete unreachable orgchart section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two S-level audit items landed together because they are the same "first-second orientation" concern on the team page: S1 — Empty OrgChart was the first thing the board saw; the canvas carried no summary data. Now a compact summary strip sits above the chart showing Members / Running / Attention / Pending hires / This-month budget (spent/cap/%). All five numbers computed from the existing snapshot — no extra IPC. S9 — Activity and budget context previously required visiting three separate pages. The summary strip collapses the minimum-useful subset into a single glance; the full drill-down paths (Work, Approvals, Costs) remain one click away. S10 — The `orgchart` SectionId was a dead rendering branch: - Not in the sidebar (useUIController sidebarGroups) - Not in the command palette (paletteOnlyNavDefs) - Not reachable from any navigateTo() call in renderer code The only callers were an automation post-hire notification and a unit test — both pointed at "orgchart" as a navigation target. Routed both to "agents" instead (which renders the OrgChart at the top anyway, plus the new summary strip and the roster), so clicking the "Agent Hired" notification now lands on the team page with the new hire highlighted. Removed: - `orgchart` from SectionId enum (src/shared/types/enums.ts) - `section === "orgchart"` render branch (OrganizationSections.tsx) - `nav.orgchart` i18n key (zh + en) Added: - src/renderer/components/organization/TeamHeaderSummary.tsx - team.summary.* i18n keys (zh + en) Covers S1 / S9 / S10. --- .../automation/automation-approval-handler.ts | 2 +- .../organization/TeamHeaderSummary.tsx | 81 +++++++++++++++++++ src/renderer/i18n/en.ts | 7 +- src/renderer/i18n/zh.ts | 7 +- .../views/sections/OrganizationSections.tsx | 7 +- src/shared/types/enums.ts | 1 - tests/unit/notification-service.test.ts | 4 +- 7 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 src/renderer/components/organization/TeamHeaderSummary.tsx diff --git a/src/main/automation/automation-approval-handler.ts b/src/main/automation/automation-approval-handler.ts index 23e7a70d..2533501a 100644 --- a/src/main/automation/automation-approval-handler.ts +++ b/src/main/automation/automation-approval-handler.ts @@ -123,7 +123,7 @@ export function handleApprovalResolved( title: localizeNotification(hireLocale, "Agent Hired", "新员工入职"), body: localizeNotification(hireLocale, `${agent.name} joined as ${agent.role}`, `${agent.name} 已加入,职位:${agent.role}`), urgency: "informational", - navigation: { section: "orgchart", entityId: agent.id }, + navigation: { section: "agents", entityId: agent.id }, batchKey: "agent_hired", }); diff --git a/src/renderer/components/organization/TeamHeaderSummary.tsx b/src/renderer/components/organization/TeamHeaderSummary.tsx new file mode 100644 index 00000000..248403c6 --- /dev/null +++ b/src/renderer/components/organization/TeamHeaderSummary.tsx @@ -0,0 +1,81 @@ +import type { AgentRecord, ApprovalRecord } from "@shared/types"; +import { formatMoney } from "../../lib/formatters"; +import { useT } from "../../i18n"; + +interface TeamHeaderSummaryProps { + agents: AgentRecord[]; + pendingApprovals: ApprovalRecord[]; +} + +/** + * Compact summary strip above the org chart. Answers the "what's going on + * with the team right now?" question without requiring the user to scan + * the chart + roster + approvals pages separately. + * + * Shows: total members, how many are actively running, how many need + * attention (paused/error/pending_approval), pending hire approvals, + * and aggregate monthly budget burn. Computed from existing snapshot + * data — no extra IPC round-trip. + */ +export function TeamHeaderSummary({ agents, pendingApprovals }: TeamHeaderSummaryProps) { + const t = useT(); + + const active = agents.filter((a) => a.status !== "terminated"); + const running = active.filter((a) => a.status === "running").length; + const attention = active.filter( + (a) => a.status === "paused" || a.status === "error" || a.status === "pending_approval", + ).length; + const pendingHires = pendingApprovals.filter((a) => a.type === "hire_agent" && a.state === "pending").length; + + const totalBudget = active.reduce((sum, a) => sum + (a.budgetMonthlyUsd ?? 0), 0); + const totalSpent = active.reduce((sum, a) => sum + (a.spentMonthlyUsd ?? 0), 0); + const budgetPct = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0; + + const items: Array<{ label: string; value: string; emphasis?: "warn" | "muted" }> = [ + { label: t("team.summary.members"), value: String(active.length) }, + { label: t("team.summary.running"), value: String(running) }, + { + label: t("team.summary.attention"), + value: String(attention), + emphasis: attention > 0 ? "warn" : "muted", + }, + { + label: t("team.summary.pendingHires"), + value: String(pendingHires), + emphasis: pendingHires > 0 ? "warn" : "muted", + }, + { + label: t("team.summary.budget"), + value: + totalBudget > 0 + ? t("team.summary.budgetFormat") + .replace("{spent}", formatMoney(totalSpent)) + .replace("{budget}", formatMoney(totalBudget)) + .replace("{pct}", String(budgetPct)) + : formatMoney(totalSpent), + }, + ]; + + return ( +
+ {items.map((item) => ( +
+ + {item.label} + + + {item.value} + +
+ ))} +
+ ); +} diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index 0c8ba279..d2d40cf0 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -6,7 +6,6 @@ export const en: Record = { "nav.performance": "Performance", "nav.runs": "Runs", "nav.agents": "Team", - "nav.orgchart": "Org Chart", "nav.hiring": "Hiring", "nav.communication": "Messages", "nav.goals": "Goals", @@ -340,6 +339,12 @@ export const en: Record = { // ── Organization page ── "org.agentRoster": "Team members", + "team.summary.members": "Members", + "team.summary.running": "Running", + "team.summary.attention": "Attention", + "team.summary.pendingHires": "Pending hires", + "team.summary.budget": "This month", + "team.summary.budgetFormat": "{spent} / {budget} · {pct}%", "org.newHireRequest": "New hire request", "org.editAgent": "Edit", "org.editAgentTitle": "Edit {name} ({role})", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index 14e02d70..9c13e15c 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -6,7 +6,6 @@ export const zh: Record = { "nav.performance": "绩效", "nav.runs": "运行", "nav.agents": "团队", - "nav.orgchart": "组织架构", "nav.hiring": "招聘", "nav.communication": "消息", "nav.goals": "目标", @@ -340,6 +339,12 @@ export const zh: Record = { // ── 组织页面 ── "org.agentRoster": "团队成员", + "team.summary.members": "成员", + "team.summary.running": "运行中", + "team.summary.attention": "需关注", + "team.summary.pendingHires": "待审招聘", + "team.summary.budget": "本月预算", + "team.summary.budgetFormat": "{spent} / {budget} · {pct}%", "org.newHireRequest": "新招聘申请", "org.editAgent": "编辑", "org.editAgentTitle": "编辑 {name}({role})", diff --git a/src/renderer/views/sections/OrganizationSections.tsx b/src/renderer/views/sections/OrganizationSections.tsx index dd6cf631..6f136747 100644 --- a/src/renderer/views/sections/OrganizationSections.tsx +++ b/src/renderer/views/sections/OrganizationSections.tsx @@ -12,6 +12,7 @@ import { GoalCreator } from "../../components/organization/GoalCreator"; import { GoalEditor } from "../../components/organization/GoalEditor"; import { GoalList } from "../../components/organization/GoalList"; import { OrgChart } from "../../components/organization/OrgChart"; +import { TeamHeaderSummary } from "../../components/organization/TeamHeaderSummary"; import { ProjectCreator } from "../../components/organization/ProjectCreator"; import { ProjectEditor } from "../../components/organization/ProjectEditor"; import { ProjectList } from "../../components/organization/ProjectList"; @@ -35,6 +36,7 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) { companyGoals, companyProjects, companyWorkspaces, + pendingApprovals, editingAgent, selectedGoal, selectedProject, @@ -65,6 +67,7 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) { <> {section === "agents" ? ( <> + setEditingAgentId(NEW_ENTITY_ID)} />}> ) : null} - {section === "orgchart" ? ( - { setSelectedAgentId(id); setSection("agents"); }} /> - ) : null} - {section === "hiring" && currentCompany ? ( { title: "Agent Hired", body: "Alice joined", urgency: "informational", - navigation: { section: "orgchart", entityId: "agent-1" }, + navigation: { section: "agents", entityId: "agent-1" }, batchKey: "agent_hired", }); @@ -192,7 +192,7 @@ describe("notification-service", () => { title: "Agent Hired", body: "Bob joined", urgency: "informational", - navigation: { section: "orgchart", entityId: "agent-2" }, + navigation: { section: "agents", entityId: "agent-2" }, batchKey: "agent_hired", });