From e8634afb8294277deca9bb03500c724f6b4fa936 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, 9 Oct 2026 01:26:15 -0400 Subject: [PATCH] Q152: chat replies and goal checkpoints stay out of work lists, load and progress Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/pages/HomePage.tsx | 7 +++++-- src/renderer/src/pages/OfficePage.tsx | 3 ++- src/renderer/src/pages/WorkPage.tsx | 9 ++++++--- src/renderer/src/pages/work/task-helpers.ts | 5 +++++ src/renderer/src/shared/Shell.tsx | 3 ++- src/shared/domain.ts | 2 ++ tests/e2e/ceo-chat.spec.ts | 2 ++ tests/unit/business-tasks.test.ts | 10 ++++++++++ 8 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 tests/unit/business-tasks.test.ts diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx index f77faf34..a99b9692 100644 --- a/src/renderer/src/pages/HomePage.tsx +++ b/src/renderer/src/pages/HomePage.tsx @@ -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) {
- + diff --git a/src/renderer/src/pages/OfficePage.tsx b/src/renderer/src/pages/OfficePage.tsx index 34cb49cc..19efdecc 100644 --- a/src/renderer/src/pages/OfficePage.tsx +++ b/src/renderer/src/pages/OfficePage.tsx @@ -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, diff --git a/src/renderer/src/pages/WorkPage.tsx b/src/renderer/src/pages/WorkPage.tsx index 459eebb9..a696deb7 100644 --- a/src/renderer/src/pages/WorkPage.tsx +++ b/src/renderer/src/pages/WorkPage.tsx @@ -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 && (focus.kind === "goal" || focus.kind === "project") ? focus : null); const [actionError, setActionError] = useState(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 (
diff --git a/src/renderer/src/pages/work/task-helpers.ts b/src/renderer/src/pages/work/task-helpers.ts index 6c2a5ca2..1441d774 100644 --- a/src/renderer/src/pages/work/task-helpers.ts +++ b/src/renderer/src/pages/work/task-helpers.ts @@ -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; diff --git a/src/renderer/src/shared/Shell.tsx b/src/renderer/src/shared/Shell.tsx index 9b373ffa..4a325099 100644 --- a/src/renderer/src/shared/Shell.tsx +++ b/src/renderer/src/shared/Shell.tsx @@ -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 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, }; diff --git a/src/shared/domain.ts b/src/shared/domain.ts index 11bf8210..eb62bc12 100644 --- a/src/shared/domain.ts +++ b/src/shared/domain.ts @@ -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. */ diff --git a/tests/e2e/ceo-chat.spec.ts b/tests/e2e/ceo-chat.spec.ts index ea93cd31..9665c203 100644 --- a/tests/e2e/ceo-chat.spec.ts +++ b/tests/e2e/ceo-chat.spec.ts @@ -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(); diff --git a/tests/unit/business-tasks.test.ts b/tests/unit/business-tasks.test.ts new file mode 100644 index 00000000..a1bda70e --- /dev/null +++ b/tests/unit/business-tasks.test.ts @@ -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]); + }); +});