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 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 00:58:34 -04:00
1 parent 983d37937d
commit 766d4ab8cd
13 files changed
+188 -36

No files matched your search

+12 -4
View File
@@ -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<RouteId>("home");
const [focus, setFocus] = useState<Focus | null>(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: <HomePage {...pageProps} />,
team: <TeamPage {...pageProps} />,
@@ -89,18 +96,19 @@ export function App() {
office: <OfficePage {...pageProps} />,
settings: <SettingsPage {...pageProps} />,
}[route];
const pageKey = focus ? `${route}:${focus.kind}:${focus.id}` : route;
return (
<AvatarDirectoryContext.Provider value={avatarDirectory}>
<Shell
snapshot={snapshot}
route={route}
setRoute={setRoute}
setRoute={(next) => navigate(next)}
onSwitchOrg={(orgId) => void actions.run(() => window.agentCompany.setActiveOrganization({ orgId }))}
onDeleteOrg={(orgId) => void actions.run(() => window.agentCompany.deleteOrganization({ orgId }))}
onNewOrg={() => setCreatingOrg(true)}
>
{page}
<Fragment key={pageKey}>{page}</Fragment>
</Shell>
</AvatarDirectoryContext.Provider>
);
+10
View File
@@ -39,6 +39,16 @@ export const zh: Record<string, string> = {
"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": "被 @ 时、在参与过的话题中、以及消息未点名任何人时回复",
+38 -6
View File
@@ -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<RiskLevel, { label: string; color: string }> = {
[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<RiskFilter>("all");
const [editingId, setEditingId] = useState<string | null>(null);
const focusedApproval = focus?.kind === "approval" ? focus.id : null;
const [editingId, setEditingId] = useState<string | null>(
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) {
<div className="section-label" style={{ margin: "32px 0 10px" }}>{t("Recently decided")}</div>
{driftError && <div style={{ color: "var(--red)", fontSize: 13, marginBottom: 8 }}>{t("Could not check approved content: {message}", { message: driftError })}</div>}
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{decided.slice(0, 24).map((approval, index) => (
{shownDecided.map((approval, index) => (
<DecidedRow
key={approval.id}
approval={approval}
drift={drifts[approval.id] ?? null}
requester={resolveRequester(approval, employees, agents)}
decider={resolveDecider(approval, employees)}
last={index === Math.min(decided.length, 24) - 1}
last={index === shownDecided.length - 1}
focused={approval.id === focusedApproval}
onOpenTask={openTask}
/>
))}
</div>
@@ -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}
/>
</div>
);
@@ -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({
<div style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: "10px 14px", fontSize: 12 }}>
<span style={{ color: "var(--text-3)" }}>{t("Requested by")}</span>
<span>{requester}</span>
{approval.sourceEntityType && (<><span style={{ color: "var(--text-3)" }}>{t("Source")}</span><span>{humanizeKind(approval.sourceEntityType)}</span></>)}
{approval.sourceEntityType && (
<>
<span style={{ color: "var(--text-3)" }}>{t("Source")}</span>
{approval.sourceEntityType === "task" && approval.sourceEntityId
? <button type="button" className="link-button" onClick={() => onOpenTask(approval.sourceEntityId!)}>{t("Open task")}</button>
: <span>{humanizeKind(approval.sourceEntityType)}</span>}
</>
)}
<span style={{ color: "var(--text-3)" }}>{t("Requested")}</span>
<span>{formatRelative(approval.createdAt)}</span>
</div>
@@ -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<HTMLDivElement | null>(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 (
<div
ref={rowRef}
className="row"
aria-current={focused ? "true" : undefined}
style={{
gap: 12,
padding: "12px 18px",
fontSize: 14,
borderBottom: last ? "none" : "1px solid var(--border-soft)",
background: focused ? "var(--accent-soft)" : undefined,
}}
>
<Icon name={approved ? "check" : "x"} size={14} color={approved ? "var(--green)" : superseded ? "var(--text-3)" : "var(--red)"} />
<div style={{ flex: 1, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{approval.title}</div>
{approval.sourceEntityType === "task" && approval.sourceEntityId && (
<button type="button" className="link-button" style={{ fontSize: 12 }} onClick={() => onOpenTask(approval.sourceEntityId!)}>{t("Open task")}</button>
)}
{drift && <Pill tone="amber">{drift === "changed" ? t("Changed since approval") : t("Removed since approval")}</Pill>}
<Avatar name={requester} size="sm" bot />
<div style={{ fontSize: 12, color: "var(--text-3)", width: 180, textAlign: "right", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
+8 -5
View File
@@ -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<StatusFilter>("all");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(focus?.kind === "run" ? focus.id : null);
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
@@ -90,7 +90,7 @@ export function RunsPage({ snapshot, actions }: PageProps) {
</aside>
<div style={{ flex: 1, minWidth: 0, overflow: "auto", padding: 24 }}>
{selected ? (
<RunDetail run={selected} tasks={tasks} agents={agents} providers={providers} providerRequests={providerRequests} actions={actions} />
<RunDetail run={selected} tasks={tasks} agents={agents} providers={providers} providerRequests={providerRequests} actions={actions} onOpenTask={(taskId) => navigate("work", { kind: "task", id: taskId })} />
) : (
<div style={{ display: "grid", placeItems: "center", height: "100%", color: "var(--text-3)", fontSize: 14 }}>
{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 }:
<div className="row" style={{ gap: 10, marginBottom: 16 }}>
{agent && <Avatar name={agent.name} size="sm" bot />}
<div>
<div style={{ fontSize: 16, fontWeight: 600 }}>{task?.title ?? run.taskId}</div>
<button type="button" className="link-button" style={{ fontSize: 16, fontWeight: 600 }} aria-label={t("Open task {title}", { title: task?.title ?? run.taskId })} onClick={() => onOpenTask(run.taskId)}>
{task?.title ?? run.taskId}
</button>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{run.id}</div>
</div>
<Pill tone={runTone(run.state)}>{runLabel(run.state)}</Pill>
+2 -2
View File
@@ -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) {
</aside>
<div className="main" style={{ flex: 1, minWidth: 0 }}>
<div className="content">
{tab === "general" && <WorkspacePanel organization={organization} workspaces={workspaces} projects={snapshot.projects.filter((p) => p.orgId === orgId)} orgId={orgId} uiPreferences={snapshot.uiPreferences} actions={actions} />}
{tab === "general" && <WorkspacePanel focusedWorkspaceId={focus?.kind === "workspace" ? focus.id : null} organization={organization} workspaces={workspaces} projects={snapshot.projects.filter((p) => p.orgId === orgId)} orgId={orgId} uiPreferences={snapshot.uiPreferences} actions={actions} />}
{tab === "providers" && (
<ProvidersPanel
orgId={orgId}
+23 -4
View File
@@ -22,7 +22,7 @@ import { ListView } from "./work/ListView";
import { TimelineView } from "./work/TimelineView";
import { latestTaskRun, nextStatus, prettyRunState, providerFor, runBlocker, runTone } from "./work/task-helpers";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
import { activeOrgId, type Focus } from "./types";
import { t } from "../i18n/i18n";
type View = "board" | "list" | "timeline";
@@ -40,7 +40,7 @@ const VIEW_OPTIONS: ReadonlyArray<{ id: View; label: string }> = [
{ 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<View>("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<TaskPriority | "all">("all");
const [assigneeFilter, setAssigneeFilter] = useState<string>("all");
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
const [detailTaskId, setDetailTaskId] = useState<string | null>(focus?.kind === "task" ? focus.id : null);
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]);
@@ -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) {
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> {t("New task")}</Button>
</TopbarActions>
<h1 className="page-h1">{t("Work")}</h1>
{scope && (
<div className="row" style={{ gap: 8, marginBottom: 8 }}>
<Pill>
{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 })}
</Pill>
<button type="button" className="link-button" style={{ fontSize: 12 }} onClick={() => setScope(null)}>{t("Show all work")}</button>
</div>
)}
<p className="page-sub">
{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 })}
</p>
@@ -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}
/>
</div>
);
@@ -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<T extends { id: string }>(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;
}
@@ -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({
</span>
</div>
{workspaces.map((workspace) => (
<div key={workspace.id} className="row" style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div key={workspace.id} className="row" aria-current={workspace.id === focusedWorkspaceId ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: workspace.id === focusedWorkspaceId ? "var(--accent-soft)" : undefined }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
+9 -1
View File
@@ -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 {
@@ -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<Tab>("overview");
const [editing, setEditing] = useState(false);
@@ -115,13 +122,25 @@ export function TaskDetailDialog({
<div style={{ marginBottom: 12 }}><Segmented value={tab} onChange={setTab} options={TABS} /></div>
{tab === "overview" && (
<OverviewTab task={task} assignee={assignee} project={project} goal={goal} workspace={workspace} provider={provider} runs={runs} />
<OverviewTab
task={task}
assignee={assignee}
project={project}
goal={goal}
workspace={workspace}
provider={provider}
runs={runs}
parent={allTasks.find((item) => item.id === task.parentTaskId) ?? null}
onOpenTask={onOpenTask}
onScope={onScope}
navigate={navigate}
/>
)}
{tab === "subtasks" && (
<SubtasksTab subtasks={subtasks} agents={agents} />
<SubtasksTab subtasks={subtasks} agents={agents} onOpenTask={onOpenTask} />
)}
{tab === "activity" && (
<ActivityTab audit={taskAudit} approvals={taskApprovals} />
<ActivityTab audit={taskAudit} approvals={taskApprovals} onOpenApproval={(approvalId) => navigate("approvals", { kind: "approval", id: approvalId })} />
)}
<div className="modal-actions">
@@ -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
<div className="row" style={{ gap: 8 }}>
{assignee ? (<><Avatar name={assignee.name} size="sm" bot /><span>{assignee.name}</span>{provider && <span style={{ color: "var(--text-3)" }}>({provider.label})</span>}</>) : <span style={{ color: "var(--text-4)" }}>{t("Unassigned")}</span>}
</div>
{project && (<><MetaLabel>{t("Project")}</MetaLabel><div>{project.name}</div></>)}
{goal && (<><MetaLabel>{t("Goal")}</MetaLabel><div>{goal.title}</div></>)}
{workspace && (<><MetaLabel>{t("Workspace")}</MetaLabel><div>{workspace.name}</div></>)}
{parent && (<><MetaLabel>{t("Parent task")}</MetaLabel><LinkButton label={t("Open task {title}", { title: parent.title })} onClick={() => onOpenTask(parent.id)}>{parent.title}</LinkButton></>)}
{project && (<><MetaLabel>{t("Project")}</MetaLabel><LinkButton label={t("Open project {name}", { name: project.name })} onClick={() => onScope({ kind: "project", id: project.id })}>{project.name}</LinkButton></>)}
{goal && (<><MetaLabel>{t("Goal")}</MetaLabel><LinkButton label={t("Open goal {title}", { title: goal.title })} onClick={() => onScope({ kind: "goal", id: goal.id })}>{goal.title}</LinkButton></>)}
{workspace && (<><MetaLabel>{t("Workspace")}</MetaLabel><LinkButton label={t("Open workspace {name}", { name: workspace.name })} onClick={() => navigate("settings", { kind: "workspace", id: workspace.id })}>{workspace.name}</LinkButton></>)}
</div>
{runs.length > 0 && (
<div style={{ marginTop: 16 }}>
@@ -163,7 +184,9 @@ function OverviewTab({ task, assignee, project, goal, workspace, provider, runs
{runs.map((run) => (
<div key={run.id} style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<div className="row" style={{ gap: 8 }}>
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
<button type="button" className="link-button" aria-label={t("Open run {id}", { id: run.id })} onClick={() => navigate("runs", { kind: "run", id: run.id })}>
<Pill tone={runTone(run.state)}>{prettyRunState(run.state)}</Pill>
</button>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{run.trigger}</span>
<span className="num" style={{ marginLeft: "auto", color: "var(--text-3)", fontSize: 12 }}>{formatRelative(run.createdAt)}</span>
</div>
@@ -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 (
<div style={{ minHeight: 60 }}>
{subtasks.length === 0 && <div style={{ fontSize: 14, color: "var(--text-3)" }}>{t("No subtasks")}</div>}
@@ -191,7 +214,7 @@ function SubtasksTab({ subtasks, agents }: { subtasks: TaskRecord[]; agents: Age
return (
<div key={st.id} className="row" style={{ gap: 10, padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name={st.status === TASK_STATUS.DONE ? "check" : "work"} size={16} color={st.status === TASK_STATUS.DONE ? "var(--green)" : "var(--text-3)"} />
<span style={{ flex: 1, fontSize: 14, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{st.title}</span>
<button type="button" className="link-button" aria-label={t("Open task {title}", { title: st.title })} onClick={() => onOpenTask(st.id)} style={{ flex: 1, fontSize: 14, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}>{st.title}</button>
<Pill>{prettyStatus(st.status)}</Pill>
{owner && <Avatar name={owner.name} size="sm" bot />}
</div>
@@ -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 (
<div style={{ minHeight: 60 }}>
{approvals.length > 0 && (
@@ -210,7 +233,7 @@ function ActivityTab({ audit, approvals }: { audit: AuditEventRecord[]; approval
{approvals.map((a) => (
<div key={a.id} className="row" style={{ gap: 8, padding: "6px 0" }}>
<Pill tone={a.state === "approved" ? "green" : a.state === "pending" ? "amber" : "red"}>{a.state}</Pill>
<span style={{ fontSize: 12 }}>{a.title}</span>
<button type="button" className="link-button" style={{ fontSize: 12 }} aria-label={t("Open approval {title}", { title: a.title })} onClick={() => onOpenApproval(a.id)}>{a.title}</button>
<span className="mono" style={{ marginLeft: "auto", fontSize: 12, color: "var(--text-3)" }}>{formatRelative(a.createdAt)}</span>
</div>
))}
@@ -277,3 +300,7 @@ function EditOverlay({ task, agents, allTasks, error, onSubmit, onCancel }: {
function MetaLabel({ children }: { children: string }) {
return <div style={{ color: "var(--text-3)" }}>{children}</div>;
}
function LinkButton({ label, onClick, children }: { label: string; onClick: () => void; children: string }) {
return <button type="button" className="link-button" aria-label={label} onClick={onClick} style={{ textAlign: "left" }}>{children}</button>;
}
+4
View File
@@ -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; }
+14
View File
@@ -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: {
+15
View File
@@ -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);
});
});