Q152: chat replies and goal checkpoints stay out of work lists, load and progress
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18f1e1fcc3
commit
e8634afb82
8 files changed
+34
-7
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { isBusinessTask } from "./work/task-helpers";
|
||||
import { useMemo, useState, type CSSProperties } from "react";
|
||||
import { Avatar, Button, Icon, Pill } from "../shared/ui";
|
||||
import { TopbarActions } from "../shared/TopbarActions";
|
||||
@@ -26,6 +27,8 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
const organization = requireActiveOrganization(snapshot);
|
||||
const employee = requireActiveEmployee(snapshot);
|
||||
const tasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
|
||||
// Progress and workload count real work only; run titles still resolve against every task.
|
||||
const businessTasks = useMemo(() => tasks.filter(isBusinessTask), [tasks]);
|
||||
const agents = useMemo(() => snapshot.agents.filter((agent) => agent.orgId === orgId), [snapshot.agents, orgId]);
|
||||
const approvals = useMemo(() => snapshot.approvals.filter((item) => item.orgId === orgId), [snapshot.approvals, orgId]);
|
||||
const goals = useMemo(() => snapshot.goals.filter((item) => item.orgId === orgId), [snapshot.goals, orgId]);
|
||||
@@ -43,7 +46,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
const pendingApprovals = approvals.filter((approval) => approval.state === "pending");
|
||||
const totalBudgetCents = agents.reduce((sum, agent) => sum + agent.budgetCents, 0);
|
||||
const primaryGoal = goals.find((goal) => goal.status === "active") ?? goals[0] ?? null;
|
||||
const goalProgress = computeGoalProgress(primaryGoal, tasks);
|
||||
const goalProgress = computeGoalProgress(primaryGoal, businessTasks);
|
||||
const firstName = employee.displayName.split(/\s+/)[0] ?? employee.displayName;
|
||||
const greeting = greetingFor(new Date().getHours());
|
||||
|
||||
@@ -68,7 +71,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
|
||||
</div>
|
||||
|
||||
<div style={METRICS}>
|
||||
<AgentWorkloadChart agents={agents} tasks={tasks} />
|
||||
<AgentWorkloadChart agents={agents} tasks={businessTasks} />
|
||||
<RunActivityChart days={usageStats.days} />
|
||||
<SuccessRateChart days={usageStats.days} />
|
||||
<CostTrendChart days={usageStats.days} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isBusinessTask } from "./work/task-helpers";
|
||||
import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, requireActiveEmployee } from "./types";
|
||||
@@ -39,7 +40,7 @@ export function OfficePage({ snapshot }: PageProps) {
|
||||
const orgAgents = snapshot.agents
|
||||
.filter((agent) => agent.orgId === orgId && agent.lifecycle === "active")
|
||||
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
||||
const orgTasks = snapshot.tasks.filter((task) => task.orgId === orgId);
|
||||
const orgTasks = snapshot.tasks.filter((task) => task.orgId === orgId && isBusinessTask(task));
|
||||
return orgAgents.map((agent) => ({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TaskDetailDialog } from "./work/TaskDetailDialog";
|
||||
import { ProjectOverview } from "./work/ProjectOverview";
|
||||
import { ListView } from "./work/ListView";
|
||||
import { TimelineView } from "./work/TimelineView";
|
||||
import { latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
|
||||
import { isBusinessTask, latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, type Focus } from "./types";
|
||||
import { t } from "../i18n/i18n";
|
||||
@@ -52,7 +52,9 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
const [scope, setScope] = useState<Focus | null>(focus && (focus.kind === "goal" || focus.kind === "project") ? focus : null);
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
|
||||
const allTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
|
||||
// The board, its counts and its filters show real work; chat replies and goal checkpoints show on Messages, Home and Runs.
|
||||
const orgTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]);
|
||||
const allTasks = useMemo(() => orgTasks.filter(isBusinessTask), [orgTasks]);
|
||||
const tasks = useMemo(
|
||||
() =>
|
||||
allTasks.filter((task) => {
|
||||
@@ -88,7 +90,8 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
setActionError(null);
|
||||
operation().catch((error: unknown) => setActionError(error instanceof Error ? error.message : String(error)));
|
||||
};
|
||||
const detailTask = detailTaskId ? allTasks.find((task) => task.id === detailTaskId) ?? null : null;
|
||||
// A link (from a run, say) can open an internal task too, though the board does not list it.
|
||||
const detailTask = detailTaskId ? orgTasks.find((task) => task.id === detailTaskId) ?? null : null;
|
||||
|
||||
return (
|
||||
<div className="page-wide work-page">
|
||||
|
||||
@@ -18,6 +18,11 @@ export const WRITING_OPTIONS: ReadonlyArray<{ value: WritingMode; label: string
|
||||
];
|
||||
|
||||
|
||||
/** Real work: not the vehicle of a chat reply or a goal checkpoint, which the system creates for itself. */
|
||||
export function isBusinessTask(task: TaskRecord): boolean {
|
||||
return task.origin !== "chat_wake" && task.origin !== "goal_review";
|
||||
}
|
||||
|
||||
export function nextStatus(status: TaskStatus): TaskStatus | null {
|
||||
if (status === TASK_STATUS.BACKLOG) return TASK_STATUS.IN_PROGRESS;
|
||||
if (status === TASK_STATUS.IN_PROGRESS) return TASK_STATUS.REVIEW;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isBusinessTask } from "../pages/work/task-helpers";
|
||||
import { unreadBySpace } from "../pages/chat/chat-helpers";
|
||||
import { useEffect, useState, type FormEvent, type ReactNode } from "react";
|
||||
import type { AppSnapshot, EmployeeRecord } from "../../../shared/domain";
|
||||
@@ -219,7 +220,7 @@ function computeBadges(snapshot: AppSnapshot, orgId: string): Partial<Record<Rou
|
||||
const tasks = snapshot.tasks.filter((item) => item.orgId === orgId);
|
||||
const approvals = snapshot.approvals.filter((item) => item.orgId === orgId);
|
||||
return {
|
||||
work: tasks.filter((task) => task.status !== "done").length,
|
||||
work: tasks.filter((task) => task.status !== "done" && isBusinessTask(task)).length,
|
||||
chat: employee ? countUnreadMessages(snapshot, orgId, employee.id) : 0,
|
||||
approvals: approvals.filter((approval) => approval.state === "pending").length,
|
||||
};
|
||||
|
||||
@@ -126,6 +126,8 @@ export interface TaskRecord {
|
||||
enqueueError: string | null;
|
||||
/** Why the operator blocked the task; no session starts while it is set. */
|
||||
blockedReason: string | null;
|
||||
/** Provenance: a chat reply vehicle, a goal checkpoint, a manager's review of work, or null for plain work. */
|
||||
origin: "chat_wake" | "goal_review" | "task_review" | null;
|
||||
/** Why the open task is not moving now, or null when nothing holds it. */
|
||||
hold: TaskHold | null;
|
||||
/** Runs at or below this task_runs rowid no longer count toward the run budget. */
|
||||
|
||||
@@ -74,6 +74,8 @@ test("the pinned CEO conversation is the Messages front door", async () => {
|
||||
|
||||
// From a task, its goal, run and back are one click each.
|
||||
await page.getByRole("button", { name: "Work" }).click();
|
||||
// The CEO's reply to the founder is a chat vehicle, not work: it stays off the board.
|
||||
await expect(page.getByRole("button", { name: /Reply to Yang/ })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click();
|
||||
const taskDialog = page.getByRole("dialog", { name: "Operationalize goal: Ship the FlowPilot MVP." });
|
||||
await taskDialog.getByRole("button", { name: "Open goal Ship the FlowPilot MVP." }).click();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isBusinessTask } from "../../src/renderer/src/pages/work/task-helpers";
|
||||
import type { TaskRecord } from "../../src/shared/domain";
|
||||
|
||||
describe("what counts as work", () => {
|
||||
test("chat reply vehicles and goal checkpoints are not work; reviews of work and plain tasks are", () => {
|
||||
const task = (origin: TaskRecord["origin"]) => ({ origin }) as TaskRecord;
|
||||
expect([null, "task_review", "chat_wake", "goal_review"].map((origin) => isBusinessTask(task(origin as TaskRecord["origin"])))).toEqual([true, true, false, false]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user