From d2ed17a81b685245aef63f3ca734c8bdb65b9f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 22 Mar 2026 02:56:42 -0400 Subject: [PATCH] refactor(01-06): decompose inbox.tsx into focused sub-components - inbox.tsx (489 lines) -> 4 files: Inbox, StandupView, PerformanceView, inbox-helpers - All sub-component files under 300 lines - Backward-compatible re-export barrel preserves all 3 named exports (Inbox, StandupView, PerformanceView) - App.tsx imports unchanged and resolving correctly --- src/renderer/components/inbox.tsx | 493 +----------------- src/renderer/components/inbox/Inbox.tsx | 160 ++++++ .../components/inbox/PerformanceView.tsx | 126 +++++ src/renderer/components/inbox/StandupView.tsx | 144 +++++ .../components/inbox/inbox-helpers.tsx | 72 +++ 5 files changed, 506 insertions(+), 489 deletions(-) create mode 100644 src/renderer/components/inbox/Inbox.tsx create mode 100644 src/renderer/components/inbox/PerformanceView.tsx create mode 100644 src/renderer/components/inbox/StandupView.tsx create mode 100644 src/renderer/components/inbox/inbox-helpers.tsx diff --git a/src/renderer/components/inbox.tsx b/src/renderer/components/inbox.tsx index f5cc223a..82e3aaf1 100644 --- a/src/renderer/components/inbox.tsx +++ b/src/renderer/components/inbox.tsx @@ -1,489 +1,4 @@ -import { useMemo, useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { - Bell, - ShieldCheck, - Warning, - XCircle, - CurrencyCircleDollar, - ArrowRight, - CheckCircle, - Clock, - Eye, - Robot, - Heartbeat, - Target, - ChatCircle, - Tray, -} from "@phosphor-icons/react"; -import type { InboxItem, InboxItemType, SectionId, StandupReport, CompanyMetrics } from "@shared/types"; -import { formatMoney, timeAgo, getAgentInitials, formatDuration } from "../lib/formatters"; -import { useT } from "../i18n"; - -const typeConfig: Record = { - pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" }, - failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" }, - budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" }, - blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" }, - review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" }, - agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" }, -}; - -const severityColors: Record = { - critical: "border-l-[color:var(--danger)]", - high: "border-l-[color:var(--warn)]", - medium: "border-l-[color:var(--accent)]", - low: "border-l-[color:var(--line)]", -}; - -interface InboxProps { - items: InboxItem[]; - onNavigate: (section: SectionId, entityId?: string) => void; -} - -export function Inbox({ items, onNavigate }: InboxProps) { - const t = useT(); - const [filter, setFilter] = useState("all"); - - const filteredItems = useMemo(() => { - if (filter === "all") return items; - return items.filter(i => i.type === filter); - }, [items, filter]); - - const counts = useMemo(() => ({ - all: items.length, - pending_approval: items.filter(i => i.type === "pending_approval").length, - failed_run: items.filter(i => i.type === "failed_run").length, - budget_warning: items.filter(i => i.type === "budget_warning").length, - blocked_task: items.filter(i => i.type === "blocked_task").length, - review_needed: items.filter(i => i.type === "review_needed").length, - agent_message: items.filter(i => i.type === "agent_message").length, - }), [items]); - - const handleItemClick = (item: InboxItem) => { - const sectionMap: Record = { - approval: "approvals", - run: "runs", - agent: "agents", - task: "tasks", - message: "communication", - }; - const section = sectionMap[item.entityType] ?? "overview"; - onNavigate(section, item.entityId); - }; - - const filterButtons: Array<{ key: InboxItemType | "all"; label: string; count: number }> = [ - { key: "all", label: "All", count: counts.all }, - { key: "pending_approval", label: "Approvals", count: counts.pending_approval }, - { key: "review_needed", label: "Reviews", count: counts.review_needed }, - { key: "failed_run", label: "Failed", count: counts.failed_run }, - { key: "blocked_task", label: "Blocked", count: counts.blocked_task }, - { key: "budget_warning", label: "Budget", count: counts.budget_warning }, - { key: "agent_message", label: "Messages", count: counts.agent_message }, - ]; - - return ( -
-
-
-
- -
-
-

{t("inbox.title")}

-

- {counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention -

-
-
-
- -
- {filterButtons.map(btn => ( - - ))} -
- - {filteredItems.length === 0 ? ( -
- -
{t("inbox.allClear")}
-
{t("inbox.noItemsNow")}
-
- ) : ( -
- - {filteredItems.map((item) => { - const config = typeConfig[item.type]; - const Icon = config.icon; - return ( - -
- -
- - -
- ); - })} -
-
- )} -
- ); -} - - -interface StandupProps { - report: StandupReport | null; - onNavigate: (section: SectionId, entityId?: string) => void; -} - -export function StandupView({ report, onNavigate }: StandupProps) { - const t = useT(); - if (!report) { - return ( -
- -
{t("standup.noData")}
-
{t("standup.noDataDesc")}
-
- ); - } - - return ( -
-
-
-
- -
-
-

{t("standup.dailyStandup")}

-

- Generated {new Date(report.generatedAt).toLocaleString()} -

-
-
-
{report.summary}
-
-
-
{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}
-
{t("standup.productive")}
-
-
-
-
{report.blockers.length}
-
{t("standup.blockers")}
-
-
-
-
{report.pendingApprovals}
-
{t("standup.pendingApprovals")}
-
-
-
-
80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}> - {report.budgetUtilization}% -
-
{t("standup.budgetUsed")}
-
-
-
- - {report.companyHighlights.length > 0 ? ( -
-
{t("standup.highlights")}
-
- {report.companyHighlights.map((h, i) => ( -
- - {h} -
- ))} -
-
- ) : null} - - {report.blockers.length > 0 ? ( -
-
Blockers
-
- {report.blockers.map((b, i) => ( -
- - {b} -
- ))} -
-
- ) : null} - -
-
{t("standup.agentStatus")}
-
- {report.agentReports.map((ar) => ( - - ))} -
-
-
- ); -} - - -interface PerformanceProps { - metrics: CompanyMetrics | null; - onNavigate: (section: SectionId, entityId?: string) => void; -} - -export function PerformanceView({ metrics, onNavigate }: PerformanceProps) { - const t = useT(); - if (!metrics) { - return ( -
- -
{t("perf.noMetrics")}
-
{t("perf.noMetricsDesc")}
-
- ); - } - - return ( -
-
- = 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"} - /> - - - -
- -
-
Run Activity (30d)
-
-
- -
-
- - - {metrics.successfulRuns} succeeded - - - - {metrics.failedRuns} failed - -
-
-
- -
-
Agent Performance
-
- - - - - - - - - - - - - {metrics.agentMetrics - .sort((a, b) => b.tasksCompleted - a.tasksCompleted) - .map((am) => ( - onNavigate("agents", am.agentId)} - > - - - - - - - - ))} - -
AgentTasks DoneSuccess %RunsAvg DurationCost
-
-
- {getAgentInitials(am.agentName)} -
- {am.agentName} -
-
{am.tasksCompleted} - = 80 ? "text-[color:var(--success)]" - : am.successRate >= 50 ? "text-[color:var(--warn)]" - : "text-[color:var(--danger)]" - }`}> - {am.successRate}% - - {am.runsTotal}{formatDuration(am.avgRunDurationSec)}{formatMoney(am.totalCostUsd)}
-
-
-
- ); -} - - -function CompactMetric({ label, value, tone = "default", subtitle }: { - label: string; - value: string | number; - tone?: "default" | "success" | "warn" | "danger" | "accent"; - subtitle?: string; -}) { - const toneMap = { - default: "text-[color:var(--text)]", - success: "text-[color:var(--success)]", - warn: "text-[color:var(--warn)]", - danger: "text-[color:var(--danger)]", - accent: "text-[color:var(--accent)]", - }; - - return ( -
-
{label}
-
{value}
- {subtitle ?
{subtitle}
: null} -
- ); -} - -function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) { - if (total === 0) return
; - const successPct = (succeeded / total) * 100; - const failPct = (failed / total) * 100; - - return ( -
- - -
- ); -} +// Backward-compatible re-exports from decomposed inbox components +export { Inbox } from "./inbox/Inbox"; +export { StandupView } from "./inbox/StandupView"; +export { PerformanceView } from "./inbox/PerformanceView"; diff --git a/src/renderer/components/inbox/Inbox.tsx b/src/renderer/components/inbox/Inbox.tsx new file mode 100644 index 00000000..b77a28da --- /dev/null +++ b/src/renderer/components/inbox/Inbox.tsx @@ -0,0 +1,160 @@ +import { useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + Bell, + ArrowRight, + Robot, + Tray, +} from "@phosphor-icons/react"; +import type { InboxItem, InboxItemType, SectionId } from "@shared/types"; +import { timeAgo } from "../../lib/formatters"; +import { useT } from "../../i18n"; +import { typeConfig, severityColors } from "./inbox-helpers"; + +interface InboxProps { + items: InboxItem[]; + onNavigate: (section: SectionId, entityId?: string) => void; +} + +export function Inbox({ items, onNavigate }: InboxProps) { + const t = useT(); + const [filter, setFilter] = useState("all"); + + const filteredItems = useMemo(() => { + if (filter === "all") return items; + return items.filter(i => i.type === filter); + }, [items, filter]); + + const counts = useMemo(() => ({ + all: items.length, + pending_approval: items.filter(i => i.type === "pending_approval").length, + failed_run: items.filter(i => i.type === "failed_run").length, + budget_warning: items.filter(i => i.type === "budget_warning").length, + blocked_task: items.filter(i => i.type === "blocked_task").length, + review_needed: items.filter(i => i.type === "review_needed").length, + agent_message: items.filter(i => i.type === "agent_message").length, + }), [items]); + + const handleItemClick = (item: InboxItem) => { + const sectionMap: Record = { + approval: "approvals", + run: "runs", + agent: "agents", + task: "tasks", + message: "communication", + }; + const section = sectionMap[item.entityType] ?? "overview"; + onNavigate(section, item.entityId); + }; + + const filterButtons: Array<{ key: InboxItemType | "all"; label: string; count: number }> = [ + { key: "all", label: "All", count: counts.all }, + { key: "pending_approval", label: "Approvals", count: counts.pending_approval }, + { key: "review_needed", label: "Reviews", count: counts.review_needed }, + { key: "failed_run", label: "Failed", count: counts.failed_run }, + { key: "blocked_task", label: "Blocked", count: counts.blocked_task }, + { key: "budget_warning", label: "Budget", count: counts.budget_warning }, + { key: "agent_message", label: "Messages", count: counts.agent_message }, + ]; + + return ( +
+
+
+
+ +
+
+

{t("inbox.title")}

+

+ {counts.all} item{counts.all !== 1 ? "s" : ""} need{counts.all === 1 ? "s" : ""} your attention +

+
+
+
+ +
+ {filterButtons.map(btn => ( + + ))} +
+ + {filteredItems.length === 0 ? ( +
+ +
{t("inbox.allClear")}
+
{t("inbox.noItemsNow")}
+
+ ) : ( +
+ + {filteredItems.map((item) => { + const config = typeConfig[item.type]; + const Icon = config.icon; + return ( + +
+ +
+ + +
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/src/renderer/components/inbox/PerformanceView.tsx b/src/renderer/components/inbox/PerformanceView.tsx new file mode 100644 index 00000000..d53c71ab --- /dev/null +++ b/src/renderer/components/inbox/PerformanceView.tsx @@ -0,0 +1,126 @@ +import { Target } from "@phosphor-icons/react"; +import type { CompanyMetrics, SectionId } from "@shared/types"; +import { formatDuration, formatMoney, getAgentInitials } from "../../lib/formatters"; +import { useT } from "../../i18n"; +import { CompactMetric, RunProgressBar } from "./inbox-helpers"; + +interface PerformanceProps { + metrics: CompanyMetrics | null; + onNavigate: (section: SectionId, entityId?: string) => void; +} + +export function PerformanceView({ metrics, onNavigate }: PerformanceProps) { + const t = useT(); + if (!metrics) { + return ( +
+ +
{t("perf.noMetrics")}
+
{t("perf.noMetricsDesc")}
+
+ ); + } + + return ( +
+
+ = 80 ? "success" : metrics.overallSuccessRate >= 50 ? "warn" : "danger"} + /> + + + +
+ +
+
Run Activity (30d)
+
+
+ +
+
+ + + {metrics.successfulRuns} succeeded + + + + {metrics.failedRuns} failed + +
+
+
+ +
+
Agent Performance
+
+ + + + + + + + + + + + + {metrics.agentMetrics + .sort((a, b) => b.tasksCompleted - a.tasksCompleted) + .map((am) => ( + onNavigate("agents", am.agentId)} + > + + + + + + + + ))} + +
AgentTasks DoneSuccess %RunsAvg DurationCost
+
+
+ {getAgentInitials(am.agentName)} +
+ {am.agentName} +
+
{am.tasksCompleted} + = 80 ? "text-[color:var(--success)]" + : am.successRate >= 50 ? "text-[color:var(--warn)]" + : "text-[color:var(--danger)]" + }`}> + {am.successRate}% + + {am.runsTotal}{formatDuration(am.avgRunDurationSec)}{formatMoney(am.totalCostUsd)}
+
+
+
+ ); +} diff --git a/src/renderer/components/inbox/StandupView.tsx b/src/renderer/components/inbox/StandupView.tsx new file mode 100644 index 00000000..86b440f2 --- /dev/null +++ b/src/renderer/components/inbox/StandupView.tsx @@ -0,0 +1,144 @@ +import { + ArrowRight, + CheckCircle, + Clock, + Heartbeat, + Warning, +} from "@phosphor-icons/react"; +import type { SectionId, StandupReport } from "@shared/types"; +import { getAgentInitials } from "../../lib/formatters"; +import { useT } from "../../i18n"; + +interface StandupProps { + report: StandupReport | null; + onNavigate: (section: SectionId, entityId?: string) => void; +} + +export function StandupView({ report, onNavigate }: StandupProps) { + const t = useT(); + if (!report) { + return ( +
+ +
{t("standup.noData")}
+
{t("standup.noDataDesc")}
+
+ ); + } + + return ( +
+
+
+
+ +
+
+

{t("standup.dailyStandup")}

+

+ Generated {new Date(report.generatedAt).toLocaleString()} +

+
+
+
{report.summary}
+
+
+
{report.agentReports.filter(r => r.tasksCompleted24h > 0).length}
+
{t("standup.productive")}
+
+
+
+
{report.blockers.length}
+
{t("standup.blockers")}
+
+
+
+
{report.pendingApprovals}
+
{t("standup.pendingApprovals")}
+
+
+
+
80 ? "text-[color:var(--danger)]" : "text-[color:var(--text)]"}`}> + {report.budgetUtilization}% +
+
{t("standup.budgetUsed")}
+
+
+
+ + {report.companyHighlights.length > 0 ? ( +
+
{t("standup.highlights")}
+
+ {report.companyHighlights.map((h, i) => ( +
+ + {h} +
+ ))} +
+
+ ) : null} + + {report.blockers.length > 0 ? ( +
+
Blockers
+
+ {report.blockers.map((b, i) => ( +
+ + {b} +
+ ))} +
+
+ ) : null} + +
+
{t("standup.agentStatus")}
+
+ {report.agentReports.map((ar) => ( + + ))} +
+
+
+ ); +} diff --git a/src/renderer/components/inbox/inbox-helpers.tsx b/src/renderer/components/inbox/inbox-helpers.tsx new file mode 100644 index 00000000..d588012d --- /dev/null +++ b/src/renderer/components/inbox/inbox-helpers.tsx @@ -0,0 +1,72 @@ +import { motion } from "framer-motion"; +import { + ShieldCheck, + XCircle, + CurrencyCircleDollar, + Warning, + Eye, + ChatCircle, +} from "@phosphor-icons/react"; +import type { InboxItemType } from "@shared/types"; + +export const typeConfig: Record = { + pending_approval: { icon: ShieldCheck, color: "var(--warn)", bg: "var(--warn-soft)", label: "Approval" }, + failed_run: { icon: XCircle, color: "var(--danger)", bg: "var(--danger-soft)", label: "Failed Run" }, + budget_warning: { icon: CurrencyCircleDollar, color: "var(--warn)", bg: "var(--warn-soft)", label: "Budget" }, + blocked_task: { icon: Warning, color: "var(--danger)", bg: "var(--danger-soft)", label: "Blocked" }, + review_needed: { icon: Eye, color: "var(--accent)", bg: "var(--accent-soft)", label: "Review" }, + agent_message: { icon: ChatCircle, color: "var(--accent)", bg: "var(--accent-soft)", label: "Message" }, +}; + +export const severityColors: Record = { + critical: "border-l-[color:var(--danger)]", + high: "border-l-[color:var(--warn)]", + medium: "border-l-[color:var(--accent)]", + low: "border-l-[color:var(--line)]", +}; + +export function CompactMetric({ label, value, tone = "default", subtitle }: { + label: string; + value: string | number; + tone?: "default" | "success" | "warn" | "danger" | "accent"; + subtitle?: string; +}) { + const toneMap = { + default: "text-[color:var(--text)]", + success: "text-[color:var(--success)]", + warn: "text-[color:var(--warn)]", + danger: "text-[color:var(--danger)]", + accent: "text-[color:var(--accent)]", + }; + + return ( +
+
{label}
+
{value}
+ {subtitle ?
{subtitle}
: null} +
+ ); +} + +export function RunProgressBar({ succeeded, failed, total }: { succeeded: number; failed: number; total: number }) { + if (total === 0) return
; + const successPct = (succeeded / total) * 100; + const failPct = (failed / total) * 100; + + return ( +
+ + +
+ ); +}