From 766d4ab8cd1ca49f09f9a2626390d4b32d52b499 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 00:58:34 -0400 Subject: [PATCH] Q148: a task's goal, project, workspace, runs, approvals and subtasks open with one click, and lead back Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/app/App.tsx | 16 ++++-- src/renderer/src/i18n/zh.ts | 10 ++++ src/renderer/src/pages/ApprovalsPage.tsx | 44 ++++++++++++--- src/renderer/src/pages/RunsPage.tsx | 13 +++-- src/renderer/src/pages/SettingsPage.tsx | 4 +- src/renderer/src/pages/WorkPage.tsx | 27 ++++++++-- .../src/pages/approvals/decided-window.ts | 9 ++++ .../src/pages/settings/WorkspacePanel.tsx | 5 +- src/renderer/src/pages/types.ts | 10 +++- .../src/pages/work/TaskDetailDialog.tsx | 53 ++++++++++++++----- src/renderer/src/styles/components.css | 4 ++ tests/e2e/ceo-chat.spec.ts | 14 +++++ tests/unit/decided-window.test.ts | 15 ++++++ 13 files changed, 188 insertions(+), 36 deletions(-) create mode 100644 src/renderer/src/pages/approvals/decided-window.ts create mode 100644 tests/unit/decided-window.test.ts diff --git a/src/renderer/src/app/App.tsx b/src/renderer/src/app/App.tsx index a8af9592..8c875012 100644 --- a/src/renderer/src/app/App.tsx +++ b/src/renderer/src/app/App.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; +import type { Focus } from "../pages/types"; +import { Fragment, useEffect, useMemo, useState } from "react"; import { Shell, type RouteId } from "../shared/Shell"; import { AvatarDirectoryContext, type AvatarLook } from "../shared/ui"; import { Onboarding } from "./Onboarding"; @@ -18,6 +19,12 @@ import { OfficePage } from "../pages/OfficePage"; export function App() { const { snapshot, actions } = useSnapshot(); const [route, setRoute] = useState("home"); + const [focus, setFocus] = useState(null); + // Opening a page for an object remembers it; plain navigation forgets it. + const navigate = (next: RouteId, nextFocus?: Focus) => { + setRoute(next); + setFocus(nextFocus ?? null); + }; const avatarDirectory = useMemo(() => { // Scope the directory to the active organization: names are only unique // within one org, and a same-named principal in another org must not @@ -75,7 +82,7 @@ export function App() { ); } - const pageProps = { snapshot, actions, navigate: setRoute }; + const pageProps = { snapshot, actions, navigate, focus }; const page = { home: , team: , @@ -89,18 +96,19 @@ export function App() { office: , settings: , }[route]; + const pageKey = focus ? `${route}:${focus.kind}:${focus.id}` : route; return ( navigate(next)} onSwitchOrg={(orgId) => void actions.run(() => window.agentCompany.setActiveOrganization({ orgId }))} onDeleteOrg={(orgId) => void actions.run(() => window.agentCompany.deleteOrganization({ orgId }))} onNewOrg={() => setCreatingOrg(true)} > - {page} + {page} ); diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index f885aa64..a6cc3fe4 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -39,6 +39,16 @@ export const zh: Record = { "Not sent: {reason}": "未发送:{reason}", "Not saved: {reason}": "未保存:{reason}", "Members ({count})": "成员({count})", + "Open task {title}": "打开任务 {title}", + "Open task": "打开任务", + "Open project {name}": "打开项目 {name}", + "Open goal {title}": "打开目标 {title}", + "Open workspace {name}": "打开工作区 {name}", + "Open run {id}": "打开运行 {id}", + "Open approval {title}": "打开审批 {title}", + "Goal: {title}": "目标:{title}", + "Project: {name}": "项目:{name}", + "Show all work": "显示全部工作", "Reads and writes": "可读可写", "Replies to every message": "回复每条消息", "Replies when @mentioned, in threads it joined, and to messages that name no one": "被 @ 时、在参与过的话题中、以及消息未点名任何人时回复", diff --git a/src/renderer/src/pages/ApprovalsPage.tsx b/src/renderer/src/pages/ApprovalsPage.tsx index cf0e1670..c0fd38b2 100644 --- a/src/renderer/src/pages/ApprovalsPage.tsx +++ b/src/renderer/src/pages/ApprovalsPage.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; +import { decidedWindow } from "./approvals/decided-window"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Avatar, Button, Icon, IconButton, Pill } from "../shared/ui"; import { TopbarActions } from "../shared/TopbarActions"; import type { @@ -28,11 +29,15 @@ const RISK_TONES: Record = { [RISK_LEVEL.LOW]: { label: t("Low risk"), color: "var(--text-3)" }, }; -export function ApprovalsPage({ snapshot, actions }: PageProps) { +export function ApprovalsPage({ snapshot, actions, navigate, focus }: PageProps) { const orgId = activeOrgId(snapshot); const employee = requireActiveEmployee(snapshot); const [filter, setFilter] = useState("all"); - const [editingId, setEditingId] = useState(null); + const focusedApproval = focus?.kind === "approval" ? focus.id : null; + const [editingId, setEditingId] = useState( + focusedApproval && snapshot.approvals.some((item) => item.id === focusedApproval && item.state === "pending") ? focusedApproval : null, + ); + const openTask = (taskId: string) => navigate("work", { kind: "task", id: taskId }); const approvals = useMemo(() => snapshot.approvals.filter((item) => item.orgId === orgId), [snapshot.approvals, orgId]); const employees = snapshot.employees.filter((item) => item.orgId === orgId); @@ -40,6 +45,7 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) { const pending = approvals.filter((approval) => approval.state === APPROVAL_STATE.PENDING); const decided = approvals.filter((approval) => approval.state !== APPROVAL_STATE.PENDING); + const shownDecided = decidedWindow(decided, focusedApproval); const filteredPending = filter === "all" ? pending : pending.filter((approval) => approval.risk === filter); const weeklyDecided = decided.filter((approval) => withinDays(approval.decidedAt, 7)); const approved7d = weeklyDecided.filter((approval) => approval.state === APPROVAL_STATE.APPROVED).length; @@ -119,14 +125,16 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) {
{t("Recently decided")}
{driftError &&
{t("Could not check approved content: {message}", { message: driftError })}
}
- {decided.slice(0, 24).map((approval, index) => ( + {shownDecided.map((approval, index) => ( ))}
@@ -138,6 +146,7 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) { requester={editing ? resolveRequester(editing, employees, agents) : ""} onClose={() => setEditingId(null)} onDecide={(state, notes) => editing && decide(editing, state, notes)} + onOpenTask={openTask} /> ); @@ -148,11 +157,13 @@ function ApprovalDetailDialog({ requester, onClose, onDecide, + onOpenTask, }: { approval: ApprovalRecord | null; requester: string; onClose: () => void; onDecide: (state: "approved" | "declined", notes?: string) => void; + onOpenTask: (taskId: string) => void; }) { const [notes, setNotes] = useState(""); if (!approval) return null; @@ -176,7 +187,14 @@ function ApprovalDetailDialog({
{t("Requested by")} {requester} - {approval.sourceEntityType && (<>{t("Source")}{humanizeKind(approval.sourceEntityType)})} + {approval.sourceEntityType && ( + <> + {t("Source")} + {approval.sourceEntityType === "task" && approval.sourceEntityId + ? + : {humanizeKind(approval.sourceEntityType)}} + + )} {t("Requested")} {formatRelative(approval.createdAt)}
@@ -292,28 +310,42 @@ function DecidedRow({ requester, decider, last, + focused, + onOpenTask, }: { approval: ApprovalRecord; drift: ApprovalDrift | null; requester: string; decider: string; last: boolean; + focused: boolean; + onOpenTask: (taskId: string) => void; }) { + const rowRef = useRef(null); + useEffect(() => { + if (focused) rowRef.current?.scrollIntoView({ block: "center" }); + }, [focused]); const approved = approval.state === APPROVAL_STATE.APPROVED; const superseded = approval.state === APPROVAL_STATE.SUPERSEDED; const decidedLabel = approved ? t("Approved") : superseded ? t("Replaced by an edit") : t("Declined"); return (
{approval.title}
+ {approval.sourceEntityType === "task" && approval.sourceEntityId && ( + + )} {drift && {drift === "changed" ? t("Changed since approval") : t("Removed since approval")}}
diff --git a/src/renderer/src/pages/RunsPage.tsx b/src/renderer/src/pages/RunsPage.tsx index 975f354f..2d924975 100644 --- a/src/renderer/src/pages/RunsPage.tsx +++ b/src/renderer/src/pages/RunsPage.tsx @@ -15,7 +15,7 @@ const FILTERS: Array<{ id: StatusFilter; label: string }> = [ { id: "failed", label: t("Failed") }, ]; -export function RunsPage({ snapshot, actions }: PageProps) { +export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) { const orgId = activeOrgId(snapshot); const taskRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]); const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]); @@ -23,7 +23,7 @@ export function RunsPage({ snapshot, actions }: PageProps) { const providers = useMemo(() => snapshot.providers.filter((p) => p.orgId === orgId), [snapshot.providers, orgId]); const providerRequests = useMemo(() => snapshot.providerRequests.filter((r) => r.orgId === orgId), [snapshot.providerRequests, orgId]); const [filter, setFilter] = useState("all"); - const [selectedId, setSelectedId] = useState(null); + const [selectedId, setSelectedId] = useState(focus?.kind === "run" ? focus.id : null); const [search, setSearch] = useState(""); const filtered = useMemo(() => { @@ -90,7 +90,7 @@ export function RunsPage({ snapshot, actions }: PageProps) {
{selected ? ( - + navigate("work", { kind: "task", id: taskId })} /> ) : (
{t("Select a run")} @@ -101,8 +101,9 @@ export function RunsPage({ snapshot, actions }: PageProps) { ); } -function RunDetail({ run, tasks, agents, providers, providerRequests, actions }: { +function RunDetail({ run, tasks, agents, providers, providerRequests, actions, onOpenTask }: { actions: PageProps["actions"]; + onOpenTask: (taskId: string) => void; run: TaskRunRecord; tasks: TaskRecord[]; agents: AgentRecord[]; @@ -129,7 +130,9 @@ function RunDetail({ run, tasks, agents, providers, providerRequests, actions }:
{agent && }
-
{task?.title ?? run.taskId}
+
{run.id}
{runLabel(run.state)} diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index df420934..0089d7df 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -21,7 +21,7 @@ const TABS: NavSpec[] = [ { id: "advanced", label: t("Advanced") }, ]; -export function SettingsPage({ snapshot, actions }: PageProps) { +export function SettingsPage({ snapshot, actions, focus }: PageProps) { const orgId = activeOrgId(snapshot); const organization = requireActiveOrganization(snapshot); const employee = requireActiveEmployee(snapshot); @@ -46,7 +46,7 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
- {tab === "general" && p.orgId === orgId)} orgId={orgId} uiPreferences={snapshot.uiPreferences} actions={actions} />} + {tab === "general" && p.orgId === orgId)} orgId={orgId} uiPreferences={snapshot.uiPreferences} actions={actions} />} {tab === "providers" && ( = [ { id: "timeline", label: t("Timeline") }, ]; -export function WorkPage({ snapshot, actions }: PageProps) { +export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) { const orgId = activeOrgId(snapshot); const [view, setView] = useState("board"); const [newTaskOpen, setNewTaskOpen] = useState(false); @@ -48,7 +48,8 @@ export function WorkPage({ snapshot, actions }: PageProps) { const [newProjectOpen, setNewProjectOpen] = useState(false); const [priorityFilter, setPriorityFilter] = useState("all"); const [assigneeFilter, setAssigneeFilter] = useState("all"); - const [detailTaskId, setDetailTaskId] = useState(null); + const [detailTaskId, setDetailTaskId] = useState(focus?.kind === "task" ? focus.id : null); + 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]); @@ -57,9 +58,11 @@ export function WorkPage({ snapshot, actions }: PageProps) { allTasks.filter((task) => { if (priorityFilter !== "all" && task.priority !== priorityFilter) return false; if (assigneeFilter !== "all" && task.assigneeAgentId !== assigneeFilter) return false; + if (scope?.kind === "goal" && task.goalId !== scope.id) return false; + if (scope?.kind === "project" && task.projectId !== scope.id) return false; return true; }), - [allTasks, priorityFilter, assigneeFilter], + [allTasks, priorityFilter, assigneeFilter, scope], ); const assigneeCount = useMemo( () => new Set(tasks.map((task) => task.assigneeAgentId).filter((id): id is string => id !== null)).size, @@ -103,6 +106,16 @@ export function WorkPage({ snapshot, actions }: PageProps) {

{t("Work")}

+ {scope && ( +
+ + {scope.kind === "goal" + ? t("Goal: {title}", { title: goals.find((goal) => goal.id === scope.id)?.title ?? scope.id }) + : t("Project: {name}", { name: projects.find((project) => project.id === scope.id)?.name ?? scope.id })} + + +
+ )}

{tasks.length === 1 ? t("{n} task", { n: tasks.length }) : t("{n} tasks", { n: tasks.length })} · {assigneeCount === 1 ? t("{n} assignee", { n: assigneeCount }) : t("{n} assignees", { n: assigneeCount })}

@@ -163,6 +176,12 @@ export function WorkPage({ snapshot, actions }: PageProps) { onClose={() => setDetailTaskId(null)} onAdvance={advance} onRun={runTask} + onOpenTask={setDetailTaskId} + onScope={(next) => { + setScope(next); + setDetailTaskId(null); + }} + navigate={navigate} />
); diff --git a/src/renderer/src/pages/approvals/decided-window.ts b/src/renderer/src/pages/approvals/decided-window.ts new file mode 100644 index 00000000..02ef7fad --- /dev/null +++ b/src/renderer/src/pages/approvals/decided-window.ts @@ -0,0 +1,9 @@ +/** How many decided approvals the page lists. */ +export const DECIDED_SHOWN = 24; + +/** The newest decided approvals, plus the focused one when it is older, so a link to it always lands on it. */ +export function decidedWindow(decided: T[], focusedId: string | null): T[] { + const shown = decided.slice(0, DECIDED_SHOWN); + const focused = focusedId === null ? undefined : decided.find((item) => item.id === focusedId); + return focused && !shown.includes(focused) ? [...shown, focused] : shown; +} diff --git a/src/renderer/src/pages/settings/WorkspacePanel.tsx b/src/renderer/src/pages/settings/WorkspacePanel.tsx index b229c70c..ac927dc8 100644 --- a/src/renderer/src/pages/settings/WorkspacePanel.tsx +++ b/src/renderer/src/pages/settings/WorkspacePanel.tsx @@ -21,6 +21,7 @@ function optional(form: FormData, key: string): string | null { } export function WorkspacePanel({ + focusedWorkspaceId, organization, workspaces, projects, @@ -28,6 +29,8 @@ export function WorkspacePanel({ uiPreferences, actions, }: { + /** A workspace the reader was sent here to see, marked in the list. */ + focusedWorkspaceId: string | null; organization: OrganizationRecord; workspaces: WorkspaceRecord[]; projects: ProjectRecord[]; @@ -111,7 +114,7 @@ export function WorkspacePanel({
{workspaces.map((workspace) => ( -
+
{workspace.name}
{workspace.localPath}
diff --git a/src/renderer/src/pages/types.ts b/src/renderer/src/pages/types.ts index 717a5c19..4fbb11a6 100644 --- a/src/renderer/src/pages/types.ts +++ b/src/renderer/src/pages/types.ts @@ -7,10 +7,18 @@ import type { SnapshotActions } from "../app/useSnapshot"; import type { RouteId } from "../shared/Shell"; import { t } from "../i18n/i18n"; +/** One object a page should open when navigated to: a task, run, approval, workspace, or the goal or project to scope work by. */ +export interface Focus { + kind: "task" | "run" | "approval" | "goal" | "project" | "workspace"; + id: string; +} + export interface PageProps { snapshot: AppSnapshot; actions: SnapshotActions; - navigate: (route: RouteId) => void; + navigate: (route: RouteId, focus?: Focus) => void; + /** What the page was opened for; pages remount per navigation, so they read it once. */ + focus: Focus | null; } export function activeOrgId(snapshot: AppSnapshot): string { diff --git a/src/renderer/src/pages/work/TaskDetailDialog.tsx b/src/renderer/src/pages/work/TaskDetailDialog.tsx index 2dd482df..72891e28 100644 --- a/src/renderer/src/pages/work/TaskDetailDialog.tsx +++ b/src/renderer/src/pages/work/TaskDetailDialog.tsx @@ -16,6 +16,7 @@ import { TASK_STATUS } from "../../../../shared/domain"; import { auditEventLabel, formatRelative } from "../types"; import { descendantIds, nextStatus, prettyRunState, prettyStatus, providerFor, runBlocker, runsForTask, runTone } from "./task-helpers"; import type { SnapshotActions } from "../../app/useSnapshot"; +import type { Focus, PageProps } from "../types"; import { t } from "../../i18n/i18n"; type Tab = "overview" | "subtasks" | "activity"; @@ -41,6 +42,9 @@ export function TaskDetailDialog({ onClose, onAdvance, onRun, + onOpenTask, + onScope, + navigate, }: { task: TaskRecord | null; agents: AgentRecord[]; @@ -57,6 +61,9 @@ export function TaskDetailDialog({ onClose: () => void; onAdvance: (task: TaskRecord) => void; onRun: (task: TaskRecord) => void; + onOpenTask: (taskId: string) => void; + onScope: (scope: Focus) => void; + navigate: PageProps["navigate"]; }) { const [tab, setTab] = useState("overview"); const [editing, setEditing] = useState(false); @@ -115,13 +122,25 @@ export function TaskDetailDialog({
{tab === "overview" && ( - + item.id === task.parentTaskId) ?? null} + onOpenTask={onOpenTask} + onScope={onScope} + navigate={navigate} + /> )} {tab === "subtasks" && ( - + )} {tab === "activity" && ( - + navigate("approvals", { kind: "approval", id: approvalId })} /> )}
@@ -136,10 +155,11 @@ export function TaskDetailDialog({ ); } -function OverviewTab({ task, assignee, project, goal, workspace, provider, runs }: { +function OverviewTab({ task, assignee, project, goal, workspace, provider, runs, parent, onOpenTask, onScope, navigate }: { task: TaskRecord; assignee: AgentRecord | null; project: ProjectRecord | null; goal: GoalRecord | null; workspace: WorkspaceRecord | null; provider: ProviderRecord | null; - runs: TaskRunRecord[]; + runs: TaskRunRecord[]; parent: TaskRecord | null; + onOpenTask: (taskId: string) => void; onScope: (scope: Focus) => void; navigate: PageProps["navigate"]; }) { return ( <> @@ -152,9 +172,10 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs
{assignee ? (<>{assignee.name}{provider && ({provider.label})}) : {t("Unassigned")}}
- {project && (<>{t("Project")}
{project.name}
)} - {goal && (<>{t("Goal")}
{goal.title}
)} - {workspace && (<>{t("Workspace")}
{workspace.name}
)} + {parent && (<>{t("Parent task")} onOpenTask(parent.id)}>{parent.title})} + {project && (<>{t("Project")} onScope({ kind: "project", id: project.id })}>{project.name})} + {goal && (<>{t("Goal")} onScope({ kind: "goal", id: goal.id })}>{goal.title})} + {workspace && (<>{t("Workspace")} navigate("settings", { kind: "workspace", id: workspace.id })}>{workspace.name})}
{runs.length > 0 && (
@@ -163,7 +184,9 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs {runs.map((run) => (
- {prettyRunState(run.state)} + {run.trigger} {formatRelative(run.createdAt)}
@@ -182,7 +205,7 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs ); } -function SubtasksTab({ subtasks, agents }: { subtasks: TaskRecord[]; agents: AgentRecord[] }) { +function SubtasksTab({ subtasks, agents, onOpenTask }: { subtasks: TaskRecord[]; agents: AgentRecord[]; onOpenTask: (taskId: string) => void }) { return (
{subtasks.length === 0 &&
{t("No subtasks")}
} @@ -191,7 +214,7 @@ function SubtasksTab({ subtasks, agents }: { subtasks: TaskRecord[]; agents: Age return (
- {st.title} + {prettyStatus(st.status)} {owner && }
@@ -201,7 +224,7 @@ function SubtasksTab({ subtasks, agents }: { subtasks: TaskRecord[]; agents: Age ); } -function ActivityTab({ audit, approvals }: { audit: AuditEventRecord[]; approvals: ApprovalRecord[] }) { +function ActivityTab({ audit, approvals, onOpenApproval }: { audit: AuditEventRecord[]; approvals: ApprovalRecord[]; onOpenApproval: (approvalId: string) => void }) { return (
{approvals.length > 0 && ( @@ -210,7 +233,7 @@ function ActivityTab({ audit, approvals }: { audit: AuditEventRecord[]; approval {approvals.map((a) => (
{a.state} - {a.title} + {formatRelative(a.createdAt)}
))} @@ -277,3 +300,7 @@ function EditOverlay({ task, agents, allTasks, error, onSubmit, onCancel }: { function MetaLabel({ children }: { children: string }) { return
{children}
; } + +function LinkButton({ label, onClick, children }: { label: string; onClick: () => void; children: string }) { + return ; +} diff --git a/src/renderer/src/styles/components.css b/src/renderer/src/styles/components.css index 12fa7173..234ca132 100644 --- a/src/renderer/src/styles/components.css +++ b/src/renderer/src/styles/components.css @@ -343,3 +343,7 @@ textarea.input { min-height: 84px; resize: vertical; line-height: 1.5; } .md-body table { border-collapse: collapse; margin: 0 0 6px; display: block; overflow-x: auto; max-width: 100%; } .md-body th, .md-body td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; } .md-body th { background: var(--panel-2); font-weight: 600; } + +/* A link to another object, styled as text the reader can follow. */ +.link-button { border: 0; background: transparent; padding: 0; color: var(--accent); cursor: pointer; font: inherit; text-align: inherit; } +.link-button:hover { text-decoration: underline; } diff --git a/tests/e2e/ceo-chat.spec.ts b/tests/e2e/ceo-chat.spec.ts index 27892ff5..86b11d41 100644 --- a/tests/e2e/ceo-chat.spec.ts +++ b/tests/e2e/ceo-chat.spec.ts @@ -72,6 +72,20 @@ test("the pinned CEO conversation is the Messages front door", async () => { // The message itself says where the reply stands. await expect(chatLog.locator(".chat-message", { hasText: "How are we tracking against the goal?" }).first()).toContainText("CEO will reply; queued."); + // From a task, its goal, run and back are one click each. + await page.getByRole("button", { name: "Work" }).click(); + 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(); + await expect(page.getByText("Goal: Ship the FlowPilot MVP.", { exact: true })).toBeVisible(); + await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click(); + await taskDialog.getByRole("button", { name: /^Open run/ }).first().click(); + await expect(page.getByRole("button", { name: "Open task Operationalize goal: Ship the FlowPilot MVP." })).toBeVisible(); + await page.getByRole("button", { name: "Open task Operationalize goal: Ship the FlowPilot MVP." }).click(); + await expect(taskDialog).toBeVisible(); + await taskDialog.getByRole("button", { name: "Close" }).click(); + await page.getByRole("button", { name: "Messages" }).click(); + // Recency pressure: newer traffic in a channel must not displace the pin. await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { diff --git a/tests/unit/decided-window.test.ts b/tests/unit/decided-window.test.ts new file mode 100644 index 00000000..302d364b --- /dev/null +++ b/tests/unit/decided-window.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import { decidedWindow } from "../../src/renderer/src/pages/approvals/decided-window"; + +describe("the decided approvals list", () => { + const decided = Array.from({ length: 30 }, (_, index) => ({ id: `apr_${index}` })); + + test("shows the newest 24", () => { + expect(decidedWindow(decided, null).map((item) => item.id)).toEqual(decided.slice(0, 24).map((item) => item.id)); + }); + + test("adds an older focused approval so a link to it lands on it", () => { + expect(decidedWindow(decided, "apr_27").map((item) => item.id)).toEqual([...decided.slice(0, 24).map((item) => item.id), "apr_27"]); + expect(decidedWindow(decided, "apr_3")).toHaveLength(24); + }); +});