mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - A task can cause an agent to create more tasks. > - Those tasks can belong to other projects or have another parent. > - The subtask view does not show all work created from the current task. > - This pull request adds a Tasks tab with separate subtask and creation groups. > - Operators can follow created work without changing its parent or project. ## Linked Issues or Issue Description **Problem or motivation** Operators need to see all work that an agent creates while running for a task. Parentage alone does not describe this relationship. Legacy and native runs must follow the same rules. **Proposed solution** Show all subtasks in one section. Separately group tasks created from the source task by their current project, with a No project group when needed. A created subtask appears in both sections. Use saved run context and recorded creation activity to find the source task. **Alternatives considered** Making all created tasks children would change their hierarchy. Removing overlap between sections would hide the creation relationship. This change keeps the two memberships separate. **Roadmap alignment** This extends the existing Activity log & action attribution capability. It does not add a new roadmap area. Related PR: #9727 adds a stored source-task field and inbound attribution UI. This PR adds the outgoing task list using existing run and activity records and does not require that schema change. ## What Changed - Add a company-scoped createdFromIssueId filter to issue lists. - Save the actor run during task creation, including legacy child-helper calls. - Recover historical run attribution from creation activity when the origin run is absent. - Render the production Tasks panel with all subtasks and independently grouped created work. - Keep progress only for subtasks. Add folding, hover fades and project links. - Fetch all result pages and refresh on issue activity. Show load failures with Retry. - Add database, API, UI and pagination tests, design-guide examples and Storybook pages. ## Verification - Before rebase: 158 targeted tests passed. Workspace typecheck, UI/server builds, Storybook build and token gates passed. - After rebase: full workspace typecheck and build passed. The cursor fix passes 26 focused tests and UI/server typechecks. - The full local test command completed its general-server group with 10,563 passing tests and two failures: a missing native-runner fixture and a concurrency-test timeout. Building the fixture and rerunning both affected files passed all 41 tests. The local command stopped before its remaining groups; all corresponding GitHub test shards passed on the submitted head. - GitHub checks on commit3b4bf0bd8: 31 passed, including the aggregate CI gate, all server/workspace/browser test shards, typecheck, build, runner verification, canary dry run, policy and security checks. Two conditional Storybook jobs were skipped by the workflow. - Greptile reviewed commit3b4bf0bd8at 5/5 with no unresolved comments. - Open Storybook at UX Labs / Tasks Created From a Task / Full Task Page. Check that a created subtask appears in both sections. Fold each group and use a project link. Check the No project group and first-task arrival story. ## Risks - Old tasks without a saved origin run or attributed creation activity cannot be linked to a source. The code does not infer a source from a shared creator or a comment. - The new list filter reads run and activity records. Source, run, activity and result stay within the requested company. - Task creation now saves the actor run when no explicit origin run is supplied. Existing explicit origins remain unchanged. No database migration is required. ## Model Used - OpenAI GPT-6 through Codex. The runtime identifies the model family as GPT-6 but does not expose a more specific model ID or context-window size. Used reasoning, repository tools, code execution and browser inspection. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
242 lines
7.6 KiB
TypeScript
242 lines
7.6 KiB
TypeScript
import type { Issue, IssueStatus } from "@paperclipai/shared";
|
|
import { Plus } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { IssueRow } from "@/components/IssueRow";
|
|
import { StatusIcon } from "@/components/StatusIcon";
|
|
import {
|
|
createIssueDetailPath,
|
|
rememberIssueDetailLocationState,
|
|
} from "@/lib/issueDetailBreadcrumb";
|
|
import { Link } from "@/lib/router";
|
|
|
|
export interface TaskDetailRelationItem {
|
|
id: string;
|
|
identifier?: string | null;
|
|
title: string;
|
|
status?: IssueStatus | null;
|
|
}
|
|
|
|
function RelationNavigationList({
|
|
items,
|
|
emptyMessage,
|
|
ariaLabel,
|
|
issueLinkState,
|
|
}: {
|
|
items: TaskDetailRelationItem[];
|
|
emptyMessage: string;
|
|
ariaLabel: string;
|
|
issueLinkState?: unknown;
|
|
}) {
|
|
if (items.length === 0) {
|
|
return <p className="py-6 text-center text-sm text-muted-foreground">{emptyMessage}</p>;
|
|
}
|
|
|
|
return (
|
|
<ul className="flex flex-col gap-0.5" aria-label={ariaLabel}>
|
|
{items.map((item) => {
|
|
const pathId = item.identifier ?? item.id;
|
|
return (
|
|
<li key={item.id}>
|
|
<Link
|
|
to={createIssueDetailPath(pathId)}
|
|
state={issueLinkState}
|
|
onClickCapture={() => rememberIssueDetailLocationState(pathId, issueLinkState)}
|
|
className="flex min-w-0 items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
title={`${item.identifier ? `${item.identifier} — ` : ""}${item.title}`}
|
|
>
|
|
{item.status ? (
|
|
<StatusIcon status={item.status} className="h-3.5 w-3.5 shrink-0" />
|
|
) : null}
|
|
<span className="min-w-0 flex-1 truncate">{item.title}</span>
|
|
{item.identifier ? (
|
|
<span className="shrink-0 font-mono text-xs text-muted-foreground">
|
|
{item.identifier}
|
|
</span>
|
|
) : null}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
const NEXT_SUBTASK_STATUS_ORDER: IssueStatus[] = [
|
|
"in_progress",
|
|
"in_review",
|
|
"todo",
|
|
"backlog",
|
|
"blocked",
|
|
];
|
|
|
|
export function resolveTaskDetailSubtaskState(items: Issue[]) {
|
|
const nextAction = NEXT_SUBTASK_STATUS_ORDER
|
|
.map((status) => items.find((item) => item.status === status))
|
|
.find((item): item is Issue => Boolean(item)) ?? null;
|
|
const rootBlocker = nextAction?.status === "blocked"
|
|
? nextAction.blockerAttention?.terminalBlocker ?? null
|
|
: null;
|
|
|
|
return {
|
|
nextAction,
|
|
rootBlocker,
|
|
remainingItems: nextAction
|
|
? items.filter((item) => item.id !== nextAction.id)
|
|
: items,
|
|
};
|
|
}
|
|
|
|
export function TaskDetailTaskList({
|
|
items,
|
|
ariaLabel,
|
|
issueLinkState,
|
|
}: {
|
|
items: Issue[];
|
|
ariaLabel: string;
|
|
issueLinkState?: unknown;
|
|
}) {
|
|
return (
|
|
<ul className="flex flex-col gap-0.5" aria-label={ariaLabel}>
|
|
{items.map((item) => (
|
|
<li key={item.id}>
|
|
<IssueRow
|
|
issue={item}
|
|
issueLinkState={issueLinkState}
|
|
presentation="task"
|
|
className="rounded-md"
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
export function TaskDetailSubtasksPanel({
|
|
items,
|
|
onAddSubtask,
|
|
issueLinkState,
|
|
}: {
|
|
items: Issue[];
|
|
onAddSubtask?: () => void;
|
|
issueLinkState?: unknown;
|
|
}) {
|
|
const completed = items.filter((item) => item.status === "done").length;
|
|
const progress = items.length > 0 ? Math.round((completed / items.length) * 100) : 0;
|
|
const { nextAction, rootBlocker, remainingItems } = resolveTaskDetailSubtaskState(items);
|
|
const allCompleted = items.length > 0 && completed === items.length;
|
|
|
|
return (
|
|
<section className="flex flex-col gap-4" aria-label="Subtasks">
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
|
<span>Progress</span>
|
|
<span className="font-mono">{completed} of {items.length} complete</span>
|
|
</div>
|
|
<div
|
|
className="h-1.5 overflow-hidden rounded-full bg-muted"
|
|
role="progressbar"
|
|
aria-label="Subtask completion"
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={progress}
|
|
>
|
|
<div
|
|
className="h-full rounded-full bg-(--status-task-icon-done) transition-[width]"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{rootBlocker ? (
|
|
<section className="flex flex-col gap-1.5" aria-labelledby="task-root-blocker-heading">
|
|
<h3 id="task-root-blocker-heading" className="text-xs font-medium text-muted-foreground">
|
|
Root blocker
|
|
</h3>
|
|
<RelationNavigationList
|
|
items={[rootBlocker]}
|
|
emptyMessage=""
|
|
ariaLabel="Root blocker"
|
|
issueLinkState={issueLinkState}
|
|
/>
|
|
</section>
|
|
) : null}
|
|
|
|
{nextAction ? (
|
|
<section className="flex flex-col gap-1.5" aria-labelledby="task-next-action-heading">
|
|
<h3 id="task-next-action-heading" className="text-xs font-medium text-muted-foreground">
|
|
{nextAction.status === "blocked" ? "Blocked subtask" : "Next action"}
|
|
</h3>
|
|
<TaskDetailTaskList
|
|
items={[nextAction]}
|
|
ariaLabel="Next subtask action"
|
|
issueLinkState={issueLinkState}
|
|
/>
|
|
</section>
|
|
) : items.length > 0 ? (
|
|
<p className="text-xs text-muted-foreground">
|
|
{allCompleted ? "All subtasks are complete." : "No remaining subtask actions."}
|
|
</p>
|
|
) : (
|
|
<p className="py-6 text-center text-sm text-muted-foreground">No subtasks yet.</p>
|
|
)}
|
|
|
|
{remainingItems.length > 0 ? (
|
|
<section className="flex flex-col gap-1.5" aria-labelledby="task-other-subtasks-heading">
|
|
<h3 id="task-other-subtasks-heading" className="text-xs font-medium text-muted-foreground">
|
|
{nextAction ? "Other subtasks" : "Subtasks"}
|
|
</h3>
|
|
<TaskDetailTaskList
|
|
items={remainingItems}
|
|
ariaLabel={nextAction ? "Other subtasks" : "Subtasks"}
|
|
issueLinkState={issueLinkState}
|
|
/>
|
|
</section>
|
|
) : null}
|
|
|
|
{onAddSubtask ? (
|
|
<Button type="button" variant="outline" size="sm" className="self-start" onClick={onAddSubtask}>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
Add subtask
|
|
</Button>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function TaskDetailReferencesPanel({
|
|
referenced,
|
|
mentionedIn,
|
|
issueLinkState,
|
|
}: {
|
|
referenced: TaskDetailRelationItem[];
|
|
mentionedIn: TaskDetailRelationItem[];
|
|
issueLinkState?: unknown;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<section className="flex flex-col gap-2" aria-labelledby="task-referenced-heading">
|
|
<h3 id="task-referenced-heading" className="text-xs font-medium text-muted-foreground">
|
|
Referenced
|
|
</h3>
|
|
<RelationNavigationList
|
|
items={referenced}
|
|
emptyMessage="This task does not reference another task."
|
|
ariaLabel="Referenced tasks"
|
|
issueLinkState={issueLinkState}
|
|
/>
|
|
</section>
|
|
<section className="flex flex-col gap-2" aria-labelledby="task-mentioned-in-heading">
|
|
<h3 id="task-mentioned-in-heading" className="text-xs font-medium text-muted-foreground">
|
|
Mentioned in
|
|
</h3>
|
|
<RelationNavigationList
|
|
items={mentionedIn}
|
|
emptyMessage="No other task mentions this task."
|
|
ariaLabel="Tasks that mention this task"
|
|
issueLinkState={issueLinkState}
|
|
/>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|