Q208: Home shows one ordered list of what needs you and why work stopped, else the next task in line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 11:48:26 -04:00
1 parent ace76dabc7
commit d422b2ce69
6 files changed
+224 -68

No files matched your search

+4 -1
View File
@@ -600,7 +600,10 @@ export const zh: Record<string, string> = {
"Version": "版本",
"Version (semver)": "版本(semver)",
"Visibility": "可见性",
"Waiting on you": "等你处理",
"Needs you": "需要你处理",
"Next in line": "下一项工作",
"Stopped: {reason}": "已停下:{reason}",
"{n} more after these": "之后还有 {n} 项",
"We never embed the key in the build. Credentials encrypt locally via OS-protected storage.": "密钥绝不打包进构建。凭证经操作系统保护的存储在本地加密。",
"Welcome": "欢迎",
"Work": "工作",
+9 -67
View File
@@ -1,5 +1,7 @@
import { runTriggerLabel } from "../i18n/labels";
import { goalProgress, type GoalPace } from "./home/goal-progress";
import { nextUp } from "./home/next-up";
import { NextUpCard } from "./home/NextUpCard";
import { newestRuns } from "./runs/newest-runs";
import { isBusinessTask } from "./work/task-helpers";
import { useMemo, useState, type CSSProperties } from "react";
@@ -7,7 +9,6 @@ import { Avatar, Button, Icon, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import type {
AgentRecord,
ApprovalRecord,
ChatMessageRecord,
EmployeeRecord,
GoalRecord,
@@ -46,7 +47,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
const usageStats = requireUsageStats(snapshot.usageStats, orgId);
const [newTaskOpen, setNewTaskOpen] = useState(false);
const pendingApprovals = approvals.filter((approval) => approval.state === "pending");
const { needsYou, next } = nextUp(approvals, businessTasks);
const totalBudgetCents = agents.reduce((sum, agent) => sum + agent.budgetCents, 0);
const primaryGoal = goals.find((goal) => goal.status === "active") ?? goals[0] ?? null;
const progress = primaryGoal ? goalProgress(primaryGoal, businessTasks, new Date()) : null;
@@ -59,17 +60,19 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> {t("New task")}</Button>
</TopbarActions>
<h1 className="page-h1">{greeting}, {firstName}</h1>
<p className="page-sub">{renderSummary(pendingApprovals.length, agents.length)}</p>
<p className="page-sub">{renderSummary(needsYou.length, agents.length)}</p>
<div style={STRIP}>
<GoalCard goal={primaryGoal} progress={progress} organizationName={organization.name} actions={actions} />
<SpendCard providers={providers} totalBudgetCents={totalBudgetCents} capCents={budgetCapCents} agents={agents} />
<WaitingCard
approvals={pendingApprovals}
<NextUpCard
needsYou={needsYou}
next={next}
agents={agents}
employees={[employee]}
onApprove={(approvalId) => void actions.run(() => window.agentCompany.decideApproval({ orgId, approvalId, state: "approved", decidedByEmployeeId: employee.id }))}
onOpen={() => navigate("approvals")}
onOpenApprovals={() => navigate("approvals")}
onOpenTask={(taskId) => navigate("work", { kind: "task", id: taskId })}
/>
</div>
@@ -172,49 +175,6 @@ function SpendCard({
);
}
function WaitingCard({
approvals,
agents,
employees,
onApprove,
onOpen,
}: {
approvals: ApprovalRecord[];
agents: AgentRecord[];
employees: EmployeeRecord[];
onApprove: (approvalId: string) => void;
onOpen: () => void;
}) {
return (
<section className="card">
<div className="row" style={{ marginBottom: 12 }}>
<span className="card-label">{t("Waiting on you")}</span>
{approvals.length > 0 && <span style={{ marginLeft: "auto" }}><Pill tone="amber">{approvals.length}</Pill></span>}
</div>
{approvals.length === 0 ? (
<div className="row" style={{ gap: 8, color: "var(--text-3)", fontSize: 14 }}>
<Icon name="check" size={16} /><span>{t("You're all caught up")}</span>
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
{approvals.slice(0, 2).map((approval) => (
<div key={approval.id}>
<div style={{ fontSize: 14, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{approval.title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", margin: "2px 0 8px" }}>
{t("{kind} · {risk} risk · from {requester}", { kind: humanizeLabel(approval.kind), risk: humanizeLabel(approval.risk), requester: resolveRequester(approval, employees, agents) })}
</div>
<div className="row" style={{ gap: 8 }}>
<Button tone="primary" size="small" onClick={() => onApprove(approval.id)}>{t("Approve")}</Button>
<Button size="small" onClick={onOpen}>{t("Open")}</Button>
</div>
</div>
))}
</div>
)}
</section>
);
}
function RunActivityCard({
taskRuns,
tasks,
@@ -325,24 +285,6 @@ function renderSummary(pending: number, agents: number): string {
return `${things} · ${onDeck}`;
}
function humanizeLabel(value: string): string {
if (!value) return value;
const spaced = value.replace(/_/g, " ");
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
}
function resolveRequester(approval: ApprovalRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string {
if (approval.requestedByType === "employee" && approval.requestedById) {
const employee = employees.find((item) => item.id === approval.requestedById);
if (employee) return employee.displayName;
}
if (approval.requestedByType === "agent" && approval.requestedById) {
const agent = agents.find((item) => item.id === approval.requestedById);
if (agent) return agent.name;
}
return t("System");
}
function runStateTone(state: string): "red" | "amber" | "violet" | "green" | undefined {
if (state === "failed") return "red";
if (state === "running") return "violet";
@@ -0,0 +1,88 @@
import type { AgentRecord, ApprovalRecord, EmployeeRecord } from "../../../../shared/domain";
import { approvalKindLabel, riskLabel } from "../../i18n/labels";
import { t } from "../../i18n/i18n";
import { Button, Icon, Pill } from "../../shared/ui";
import type { NextUpItem } from "./next-up";
const SHOWN = 3;
const TITLE = { fontSize: 14, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } as const;
const META = { fontSize: 12, color: "var(--text-3)", margin: "2px 0 8px" } as const;
/** Home's one place for what to look at next: decisions and stopped work in priority order, or else the next task in line. */
export function NextUpCard({
needsYou,
next,
agents,
employees,
onApprove,
onOpenApprovals,
onOpenTask,
}: {
needsYou: NextUpItem[];
next: NextUpItem | null;
agents: AgentRecord[];
employees: EmployeeRecord[];
onApprove: (approvalId: string) => void;
onOpenApprovals: () => void;
onOpenTask: (taskId: string) => void;
}) {
const more = needsYou.length - SHOWN;
return (
<section className="card" aria-label={t("Needs you")}>
<div className="row" style={{ marginBottom: 12 }}>
<span className="card-label">{t("Needs you")}</span>
{needsYou.length > 0 && <span style={{ marginLeft: "auto" }}><Pill tone="amber">{needsYou.length}</Pill></span>}
</div>
{needsYou.length === 0 ? (
<>
<div className="row" style={{ gap: 8, color: "var(--text-3)", fontSize: 14, marginBottom: 12 }}>
<Icon name="check" size={16} /><span>{t("You're all caught up")}</span>
</div>
{next?.kind === "next" && (
<div>
<div className="card-label">{t("Next in line")}</div>
<div style={TITLE}>{next.task.title}</div>
<div className="row" style={{ gap: 8, marginTop: 8 }}>
<Button size="small" onClick={() => onOpenTask(next.task.id)}>{t("Open")}</Button>
</div>
</div>
)}
</>
) : (
<ol style={{ display: "flex", flexDirection: "column", gap: 16, listStyle: "none", margin: 0, padding: 0 }}>
{needsYou.slice(0, SHOWN).map((item) => item.kind === "approval" ? (
<li key={item.approval.id}>
<div style={TITLE}>{item.approval.title}</div>
<div style={META}>
{t("{kind} · {risk} risk · from {requester}", { kind: approvalKindLabel(item.approval.kind), risk: riskLabel(item.approval.risk), requester: requesterName(item.approval, employees, agents) })}
</div>
<div className="row" style={{ gap: 8 }}>
<Button tone="primary" size="small" onClick={() => onApprove(item.approval.id)}>{t("Approve")}</Button>
<Button size="small" onClick={onOpenApprovals}>{t("Open")}</Button>
</div>
</li>
) : item.kind === "held" ? (
<li key={item.task.id}>
<div style={TITLE}>{item.task.title}</div>
<div style={{ ...META, color: "var(--amber)" }}>{t("Stopped: {reason}", { reason: item.reason })}</div>
<Button size="small" onClick={() => onOpenTask(item.task.id)}>{t("Open")}</Button>
</li>
) : null)}
{more > 0 && <li style={{ fontSize: 12, color: "var(--text-3)" }}>{t("{n} more after these", { n: more })}</li>}
</ol>
)}
</section>
);
}
function requesterName(approval: ApprovalRecord, employees: EmployeeRecord[], agents: AgentRecord[]): string {
if (approval.requestedByType === "employee" && approval.requestedById) {
const employee = employees.find((item) => item.id === approval.requestedById);
if (employee) return employee.displayName;
}
if (approval.requestedByType === "agent" && approval.requestedById) {
const agent = agents.find((item) => item.id === approval.requestedById);
if (agent) return agent.name;
}
return t("System");
}
+29
View File
@@ -0,0 +1,29 @@
import type { ApprovalRecord, TaskPriority, TaskRecord } from "../../../../shared/domain";
/** One thing to look at next, in the single order Home shows them: decisions waiting on you, then work stopped until you act, then the next task in line. */
export type NextUpItem =
| { kind: "approval"; approval: ApprovalRecord }
| { kind: "held"; task: TaskRecord; reason: string }
| { kind: "next"; task: TaskRecord };
const PRIORITY_RANK: Record<TaskPriority, number> = { high: 0, normal: 1, low: 2 };
const byPriorityThenOldest = (a: TaskRecord, b: TaskRecord) =>
PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority] || a.createdAt.localeCompare(b.createdAt);
export function nextUp(approvals: ApprovalRecord[], tasks: TaskRecord[]): { needsYou: NextUpItem[]; next: NextUpItem | null } {
const waiting: NextUpItem[] = approvals
.filter((approval) => approval.state === "pending")
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
.map((approval) => ({ kind: "approval", approval }));
const open = tasks.filter((task) => task.status !== "done");
const held: NextUpItem[] = open
.filter((task) => task.hold?.waitingOn === "operator")
.sort(byPriorityThenOldest)
.map((task) => ({ kind: "held", task, reason: task.hold!.reason }));
// Work already under way comes before work not yet started.
const inLine = open
.filter((task) => task.hold === null && task.status !== "review")
.sort((a, b) => Number(a.status !== "in_progress") - Number(b.status !== "in_progress") || byPriorityThenOldest(a, b))[0];
return { needsYou: [...waiting, ...held], next: inLine ? { kind: "next", task: inLine } : null };
}