PAP-10430: split Issue-to-Task copy migration (#7651)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The board UI is the operator surface where users create, assign,
monitor, and review work items.
> - The product language is moving toward "tasks" for user-facing work
items while the internal API and database still use "issues".
> - PR #7543 bundled this copy migration with broader
information-architecture work, which made the branch too large for
Greptile review.
> - This pull request peels the Issue-to-Task copy migration into a
smaller, independently reviewable change.
> - The benefit is clearer user-facing terminology, less agent confusion
via the Paperclip skill note, and a smaller PR that Greptile can review.

## Linked Issues or Issue Description

Refs #7645
Refs #7543
Refs PAP-10430

This PR was split out of #7543 so the Issue-to-Task copy migration can
be reviewed separately and the remaining IA PR can fall under Greptile's
file limit.

## What Changed

- Preserves Scott Tong's original `PAP-57` copy-only commit, with author
and co-author credit intact, to rename user-facing "Issues" copy to
"Tasks" across the UI while keeping routes/API/internal symbols as
`issue`.
- Updates onboarding and release-smoke browser selectors from `Create &
Open Issue` to `Create & Open Task`.
- Adds a terminology note to `skills/paperclip/SKILL.md` clarifying that
task and issue refer to the same Paperclip work item.
- Resolves the only cherry-pick conflict by keeping current search
artifacts support and changing visible search copy to "tasks".

## Verification

- `pnpm --filter @paperclipai/ui build` passed.
- `NODE_ENV=test pnpm exec vitest run
ui/src/components/IssuesList.test.tsx ui/src/components/Sidebar.test.tsx
ui/src/components/NewIssueDialog.test.tsx
ui/src/pages/IssueDetail.test.tsx` passed: 4 files, 66 tests.
- `git diff --check origin/master...HEAD` passed.
- Diff is 80 files, below Greptile's 100-file limit.
- Before/after UI copy examples: "Issues" -> "Tasks", "New Issue" ->
"New Task", "Create & Open Issue" -> "Create & Open Task".

## Risks

- Medium copy-risk: this intentionally changes user-facing terminology
broadly while keeping internal issue identifiers and routes unchanged.
- Some docs and APIs still say `issue`; the skill note clarifies this so
agents do not treat task and issue as separate entities.
- Browser-level visual validation is expected from CI because this local
container is missing usable browser dependencies.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

Scott Tong authored the original `PAP-57` copy migration, assisted by
Claude Opus 4.8 and Paperclip agents per the preserved commit metadata.
Codex / GPT-5-class coding agent with shell, GitHub CLI, and repository
access performed the PR split, conflict resolution, skill note, and
verification.

## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [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: scotttong <scott.tong@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-06-06 10:10:01 -05:00
committed by GitHub
parent 7428fb956f
commit 2e74d32871
81 changed files with 356 additions and 327 deletions
+5 -5
View File
@@ -156,8 +156,8 @@ export function ApprovalDetail() {
? {
label:
(linkedIssues?.length ?? 0) > 1
? "Review linked issues"
: "Review linked issue",
? "Review linked tasks"
: "Review linked task",
to: `/issues/${primaryLinkedIssue.identifier ?? primaryLinkedIssue.id}`,
}
: linkedAgentId
@@ -183,7 +183,7 @@ export function ApprovalDetail() {
<div>
<p className="text-sm text-green-800 dark:text-green-100 font-medium">Approval confirmed</p>
<p className="text-xs text-green-700 dark:text-green-200/90">
Requesting agent was notified to review this approval and linked issues.
Requesting agent was notified to review this approval and linked tasks.
</p>
</div>
</div>
@@ -240,7 +240,7 @@ export function ApprovalDetail() {
{error && <p className="text-sm text-destructive">{error}</p>}
{linkedIssues && linkedIssues.length > 0 && (
<div className="pt-2 border-t border-border/60">
<p className="text-xs text-muted-foreground mb-1.5">Linked Issues</p>
<p className="text-xs text-muted-foreground mb-1.5">Linked Tasks</p>
<div className="space-y-1.5">
{linkedIssues.map((issue) => (
<Link
@@ -256,7 +256,7 @@ export function ApprovalDetail() {
))}
</div>
<p className="text-[11px] text-muted-foreground mt-2">
Linked issues remain open until the requesting agent follows up and closes them.
Linked tasks remain open until the requesting agent follows up and closes them.
</p>
</div>
)}
+1 -1
View File
@@ -241,7 +241,7 @@ export function Companies() {
<div className="flex items-center gap-1.5">
<CircleDot className="h-3.5 w-3.5" />
<span>
{issueCount} {issueCount === 1 ? "issue" : "issues"}
{issueCount} {issueCount === 1 ? "task" : "tasks"}
</span>
</div>
<div className="flex items-center gap-1.5 tabular-nums">
+5 -5
View File
@@ -175,7 +175,7 @@ export function CompanyAccess() {
title: "Member removed",
body:
result.reassignedIssueCount > 0
? `${result.reassignedIssueCount} assigned issue${result.reassignedIssueCount === 1 ? "" : "s"} cleaned up.`
? `${result.reassignedIssueCount} assigned task${result.reassignedIssueCount === 1 ? "" : "s"} cleaned up.`
: undefined,
tone: "success",
});
@@ -449,14 +449,14 @@ export function CompanyAccess() {
<div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div>
<div className="mt-2 text-sm text-muted-foreground">
{assignedIssuesQuery.isLoading
? "Checking assigned issues..."
: `${assignedIssues.length} open assigned issue${assignedIssues.length === 1 ? "" : "s"}`}
? "Checking assigned tasks..."
: `${assignedIssues.length} open assigned task${assignedIssues.length === 1 ? "" : "s"}`}
</div>
</div>
{assignedIssues.length > 0 ? (
<div className="space-y-2">
<div className="text-sm font-medium">Issue reassignment</div>
<div className="text-sm font-medium">Task reassignment</div>
<select
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
value={reassignmentTarget}
@@ -491,7 +491,7 @@ export function CompanyAccess() {
))}
{assignedIssues.length > 6 ? (
<div className="px-3 py-2 text-sm text-muted-foreground">
{assignedIssues.length - 6} more issue{assignedIssues.length - 6 === 1 ? "" : "s"}
{assignedIssues.length - 6} more task{assignedIssues.length - 6 === 1 ? "" : "s"}
</div>
) : null}
</div>
+1 -1
View File
@@ -422,7 +422,7 @@ export function CompanyEnvironments() {
<h1 className="text-lg font-semibold">Company Environments</h1>
</div>
<p className="max-w-3xl text-sm text-muted-foreground">
Define reusable execution targets for projects, issue workspaces, and remote-capable adapters.
Define reusable execution targets for projects, task workspaces, and remote-capable adapters.
</p>
</div>
+1 -1
View File
@@ -1005,7 +1005,7 @@ export function CompanyExport() {
onClick={() => setTaskLimit((prev) => prev + TASKS_PAGE_SIZE)}
className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground transition-colors"
>
Show more issues ({visibleTaskChildren} of {totalTaskChildren})
Show more tasks ({visibleTaskChildren} of {totalTaskChildren})
</button>
</div>
)}
+1 -1
View File
@@ -805,7 +805,7 @@ export function Costs() {
<Card>
<CardHeader className="px-5 pt-5 pb-2">
<CardTitle className="text-base">By project</CardTitle>
<CardDescription>Run costs attributed through project-linked issues.</CardDescription>
<CardDescription>Run costs attributed through project-linked tasks.</CardDescription>
</CardHeader>
<CardContent className="space-y-2 px-5 pb-5 pt-2">
{(spendData?.byProject.length ?? 0) === 0 ? (
+2 -2
View File
@@ -295,10 +295,10 @@ export function Dashboard() {
<ChartCard title="Run Activity" subtitle="Last 14 days">
<RunActivityChart activity={data.runActivity} />
</ChartCard>
<ChartCard title="Issues by Priority" subtitle="Last 14 days">
<ChartCard title="Tasks by Priority" subtitle="Last 14 days">
<PriorityChart issues={issues ?? []} />
</ChartCard>
<ChartCard title="Issues by Status" subtitle="Last 14 days">
<ChartCard title="Tasks by Status" subtitle="Last 14 days">
<IssueStatusChart issues={issues ?? []} />
</ChartCard>
<ChartCard title="Success Rate" subtitle="Last 14 days">
+13 -13
View File
@@ -470,7 +470,7 @@ export function DesignGuide() {
<SubSection title="With icons">
<div className="flex items-center gap-2 flex-wrap">
<Button><Plus /> New Issue</Button>
<Button><Plus /> New Task</Button>
<Button variant="outline"><Upload /> Upload</Button>
<Button variant="destructive"><Trash2 /> Delete</Button>
<Button size="sm"><Plus /> Add</Button>
@@ -727,11 +727,11 @@ export function DesignGuide() {
checked={menuChecked}
onCheckedChange={(value) => setMenuChecked(value === true)}
>
Watch issue
Watch task
</DropdownMenuCheckboxItem>
<DropdownMenuItem variant="destructive">
<Trash2 className="h-4 w-4" />
Delete issue
Delete task
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -784,7 +784,7 @@ export function DesignGuide() {
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Issue Properties</SheetTitle>
<SheetTitle>Task Properties</SheetTitle>
<SheetDescription>Edit metadata without leaving the current page.</SheetDescription>
</SheetHeader>
<div className="space-y-4 px-4">
@@ -836,7 +836,7 @@ export function DesignGuide() {
</CommandItem>
<CommandItem>
<CircleDot className="h-4 w-4" />
Issues
Tasks
</CommandItem>
</CommandGroup>
<CommandSeparator />
@@ -847,7 +847,7 @@ export function DesignGuide() {
</CommandItem>
<CommandItem>
<Plus className="h-4 w-4" />
Create new issue
Create new task
</CommandItem>
</CommandGroup>
</CommandList>
@@ -870,7 +870,7 @@ export function DesignGuide() {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Issue List</BreadcrumbPage>
<BreadcrumbPage>Task List</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
@@ -899,7 +899,7 @@ export function DesignGuide() {
<SubSection title="Metric Cards">
<div className="grid md:grid-cols-2 xl:grid-cols-4 gap-4">
<MetricCard icon={Bot} value={12} label="Active Agents" description="+3 this week" />
<MetricCard icon={CircleDot} value={48} label="Open Issues" />
<MetricCard icon={CircleDot} value={48} label="Open Tasks" />
<MetricCard icon={DollarSign} value="$1,234" label="Monthly Cost" description="Under budget" />
<MetricCard icon={Zap} value="99.9%" label="Uptime" />
</div>
@@ -1298,7 +1298,7 @@ export function DesignGuide() {
</div>
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground cursor-pointer">
<CircleDot className="h-4 w-4" />
Issues
Tasks
<span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-1.5 py-0.5">
12
</span>
@@ -1331,7 +1331,7 @@ export function DesignGuide() {
{/* ============================================================ */}
{/* GROUPED LIST (Issues pattern) */}
{/* ============================================================ */}
<Section title="Grouped List (Issues pattern)">
<Section title="Grouped List (Tasks pattern)">
<div>
<div className="flex items-center gap-2 px-4 py-2 bg-muted/50 rounded-t-md">
<StatusIcon status="in_progress" />
@@ -1588,7 +1588,7 @@ export function DesignGuide() {
<div className="border border-border rounded-md divide-y divide-border text-sm">
{[
["Cmd+K / Ctrl+K", "Open Command Palette"],
["C", "New Issue (outside inputs)"],
["C", "New Task (outside inputs)"],
["[", "Toggle Sidebar"],
["]", "Toggle Properties Panel"],
@@ -1604,7 +1604,7 @@ export function DesignGuide() {
</div>
</Section>
<Section title="Issue Output Surface">
<Section title="Task Output Surface">
<SubSection title="Multiple outputs (primary video + 'Also produced')">
<IssueOutputSection workProducts={DESIGN_GUIDE_OUTPUTS} />
</SubSection>
@@ -1613,7 +1613,7 @@ export function DesignGuide() {
</SubSection>
<SubSection title="Empty state">
<p className="text-xs text-muted-foreground">
When an issue has produced no artifact work products, the Output section renders nothing
When a task has produced no artifact work products, the Output section renders nothing
at all (no placeholder card).
</p>
</SubSection>
@@ -293,7 +293,7 @@ describe("ExecutionWorkspaceDetail plugin slots", () => {
await render();
expect(container.textContent).toContain("Workspace plugin tab is not available.");
expect(container.querySelector('a[href="/execution-workspaces/workspace-1/issues"]')?.textContent).toBe("Back to issues");
expect(container.querySelector('a[href="/execution-workspaces/workspace-1/issues"]')?.textContent).toBe("Back to tasks");
expect(container.textContent).not.toContain("Workspace routines");
expect(container.querySelector('[data-testid="plugin-slot-mount"]')).toBeNull();
});
@@ -309,7 +309,7 @@ describe("ExecutionWorkspaceDetail plugin slots", () => {
const tabLabels = Array.from(container.querySelectorAll("[data-tab-value]")).map((tab) => tab.textContent);
expect(tabLabels).toEqual([
"Issues",
"Tasks",
"Services",
"Changes",
"Configuration",
+3 -3
View File
@@ -67,7 +67,7 @@ type OrderedExecutionWorkspaceTabItem = {
const DEFAULT_PLUGIN_DETAIL_TAB_ORDER = 100;
const EXECUTION_WORKSPACE_BASE_TAB_ITEMS: OrderedExecutionWorkspaceTabItem[] = [
{ value: "issues", label: "Issues", order: 10 },
{ value: "issues", label: "Tasks", order: 10 },
{ value: "services", label: "Services", order: 20 },
{ value: "configuration", label: "Configuration", order: 30 },
{ value: "runtime_logs", label: "Runtime logs", order: 40 },
@@ -1077,7 +1077,7 @@ export function ExecutionWorkspaceDetail() {
"None"
)}
</DetailRow>
<DetailRow label="Source issue">
<DetailRow label="Source task">
{sourceIssue ? (
<Link to={issueUrl(sourceIssue)} className="hover:underline">
{sourceIssue.identifier ?? sourceIssue.id} · {sourceIssue.title}
@@ -1218,7 +1218,7 @@ export function ExecutionWorkspaceDetail() {
) : isExecutionWorkspacePluginTab(activeTab) ? (
<MissingPluginTabPlaceholder
defaultTabHref={executionWorkspaceTabPath(workspace.id, "issues")}
defaultTabLabel="Back to issues"
defaultTabLabel="Back to tasks"
/>
) : activeTab === "routines" ? (
<ExecutionWorkspaceRoutinesList
+4 -4
View File
@@ -1495,7 +1495,7 @@ export function Inbox() {
return { previousData };
},
onError: (err, id, context) => {
setActionError(err instanceof Error ? err.message : "Failed to archive issue");
setActionError(err instanceof Error ? err.message : "Failed to archive task");
setArchivingIssueIds((prev) => {
const next = new Set(prev);
next.delete(id);
@@ -2208,7 +2208,7 @@ export function Inbox() {
</SelectTrigger>
<SelectContent>
<SelectItem value="everything">All categories</SelectItem>
<SelectItem value="issues_i_touched">My recent issues</SelectItem>
<SelectItem value="issues_i_touched">My recent tasks</SelectItem>
<SelectItem value="join_requests">Join requests</SelectItem>
<SelectItem value="approvals">Approvals</SelectItem>
<SelectItem value="failed_runs">Failed runs</SelectItem>
@@ -2454,8 +2454,8 @@ export function Inbox() {
variant="ghost"
size="icon-xs"
className="-mr-2 text-muted-foreground"
title={`New issue in ${group.label}`}
aria-label={`New issue in ${group.label}`}
title={`New task in ${group.label}`}
aria-label={`New task in ${group.label}`}
onClick={(event) => {
event.stopPropagation();
openCreateIssueForGroup(group);
@@ -291,7 +291,7 @@ export function InstanceExperimentalSettings() {
<h2 className="text-sm font-semibold">Enable Isolated Workspaces</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Show execution workspace controls in project configuration and allow isolated workspace behavior for new
and existing issue runs.
and existing task runs.
</p>
</div>
<ToggleSwitch
@@ -328,9 +328,9 @@ export function InstanceExperimentalSettings() {
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Issue Plan Decomposition Panel</h2>
<h2 className="text-sm font-semibold">Task Plan Decomposition Panel</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Show accepted-plan decomposition history on issue detail pages. Intended for debugging and validating
Show accepted-plan decomposition history on task detail pages. Intended for debugging and validating
subtask creation behavior while the presentation is still being refined.
</p>
</div>
@@ -342,7 +342,7 @@ export function InstanceExperimentalSettings() {
})
}
disabled={toggleMutation.isPending}
aria-label="Toggle issue plan decomposition panel experimental setting"
aria-label="Toggle task plan decomposition panel experimental setting"
/>
</div>
</section>
@@ -387,9 +387,9 @@ export function InstanceExperimentalSettings() {
<div className="flex flex-col gap-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Auto-Create Issue Recovery Tasks</h2>
<h2 className="text-sm font-semibold">Auto-Create Recovery Tasks</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Let the heartbeat scheduler create recovery issues for issue dependency chains found inside the
Let the heartbeat scheduler create recovery tasks for task dependency chains found inside the
configured lookback window.
</p>
</div>
@@ -403,7 +403,7 @@ export function InstanceExperimentalSettings() {
previewForEnable();
}}
disabled={recoveryActionPending}
aria-label="Toggle issue graph liveness auto-recovery"
aria-label="Toggle task graph liveness auto-recovery"
/>
</div>
+1 -1
View File
@@ -158,7 +158,7 @@ export function InstanceGeneralSettings() {
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Keyboard shortcuts</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating issues or
Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating tasks or
toggling panels. This is off by default.
</p>
</div>
+10 -10
View File
@@ -1028,7 +1028,7 @@ describe("IssueDetail", () => {
expect(container.textContent).toContain("Plan decomposition");
expect(container.textContent).toContain("Plan revision 2");
expect(container.textContent).toContain("2 of 2 child issues created");
expect(container.textContent).toContain("2 of 2 child tasks created");
expect(container.textContent).toContain("First child issue");
expect(mockIssuesApi.listAcceptedPlanDecompositions).toHaveBeenCalledWith("issue-1");
});
@@ -1081,8 +1081,8 @@ describe("IssueDetail", () => {
parentId: "parent-1",
includeBlockedBy: true,
});
expect(container.querySelector('a[aria-label="Previous sub-issue: PAP-1 - Previous sibling"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Next sub-issue: PAP-3 - Next sibling"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Previous sub-task: PAP-1 - Previous sibling"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Next sub-task: PAP-3 - Next sibling"]')).toBeTruthy();
expect(container.textContent).toContain("Previous");
expect(container.textContent).toContain("Previous sibling");
expect(container.textContent).toContain("Next");
@@ -1137,7 +1137,7 @@ describe("IssueDetail", () => {
descendantOf: "issue-parent",
includeBlockedBy: true,
});
expect(container.querySelector('a[aria-label="Next sub-issue: PAP-11 - First child"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Next sub-task: PAP-11 - First child"]')).toBeTruthy();
expect(container.textContent).toContain("Next");
expect(container.textContent).toContain("First child");
expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0].footer).toBeTruthy();
@@ -1314,7 +1314,7 @@ describe("IssueDetail", () => {
await flushReact();
await flushReact();
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
expect(moreButton).toBeTruthy();
await act(async () => {
@@ -1424,7 +1424,7 @@ describe("IssueDetail", () => {
metadata: { source: "issue_active_run_control", runId: "run-active-1" },
});
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
expect(moreButton).toBeTruthy();
await act(async () => {
moreButton!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
@@ -1690,7 +1690,7 @@ describe("IssueDetail", () => {
await flushReact();
await flushReact();
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
expect(moreButton).toBeTruthy();
await act(async () => {
@@ -1712,11 +1712,11 @@ describe("IssueDetail", () => {
mode: "restore",
releasePolicy: { strategy: "manual" },
});
expect(container.textContent).toContain("Restore issues cancelled by this subtree operation so work can resume.");
expect(container.textContent).toContain("Restore tasks cancelled by this subtree operation so work can resume.");
expect(container.textContent).toContain("Cancelled child");
const restoreApplyButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.trim() === "Restore 1 issues");
.find((button) => button.textContent?.trim() === "Restore 1 tasks");
expect(restoreApplyButton).toBeTruthy();
await act(async () => {
@@ -1792,7 +1792,7 @@ describe("IssueDetail", () => {
expect(bodyScrollRegion?.className).toContain("overscroll-contain");
const cancelApplyButton = Array.from(dialogContent!.querySelectorAll("button"))
.find((button) => button.textContent?.trim() === "Cancel 24 issues") as HTMLButtonElement | undefined;
.find((button) => button.textContent?.trim() === "Cancel 24 tasks") as HTMLButtonElement | undefined;
expect(cancelApplyButton).toBeTruthy();
expect(cancelApplyButton!.disabled).toBe(true);
+38 -38
View File
@@ -192,14 +192,14 @@ const LEAF_WORK_CONTROL_MODE_LABEL: Partial<Record<IssueTreeControlMode, string>
resume: "Resume work",
};
const TREE_CONTROL_MODE_HELP_TEXT: Record<IssueTreeControlMode, string> = {
pause: "Pause active execution in this issue subtree until an explicit resume.",
pause: "Pause active execution in this task subtree until an explicit resume.",
resume: "Release the active subtree pause hold so held work can continue.",
cancel: "Cancel non-terminal issues in this subtree and stop queued/running work where possible.",
restore: "Restore issues cancelled by this subtree operation so work can resume.",
cancel: "Cancel non-terminal tasks in this subtree and stop queued/running work where possible.",
restore: "Restore tasks cancelled by this subtree operation so work can resume.",
};
const LEAF_WORK_CONTROL_MODE_HELP_TEXT: Partial<Record<IssueTreeControlMode, string>> = {
pause: "Pause active execution on this issue until an explicit resume.",
resume: "Release the active pause hold so this issue can continue.",
pause: "Pause active execution on this task until an explicit resume.",
resume: "Release the active pause hold so this task can continue.",
};
function issueTreeControlLabel(mode: IssueTreeControlMode, scope: "leaf" | "subtree") {
return scope === "leaf"
@@ -217,7 +217,7 @@ function treeControlPreviewErrorCopy(error: unknown): string {
if (error instanceof ApiError) {
if (error.status === 403) return "Only board users can preview subtree controls.";
if (error.status === 409) return "Preview is stale because subtree hold state changed. Retry to refresh.";
if (error.status === 422) return "This subtree action is currently invalid for the selected issues.";
if (error.status === 422) return "This subtree action is currently invalid for the selected tasks.";
}
return error instanceof Error ? error.message : "Unable to load preview.";
}
@@ -606,7 +606,7 @@ function InboxMobileToolbar({
onClick={() => { onHide(); setMenuOpen(false); }}
>
<EyeOff className="h-3 w-3" />
Hide this issue
Hide this task
</button>
)}
</PopoverContent>
@@ -1122,7 +1122,7 @@ function IssueDetailActivityTab({
) : (
<div className="space-y-1 text-xs text-muted-foreground tabular-nums">
<div className="flex flex-wrap gap-3">
<span className="font-medium text-foreground">This issue</span>
<span className="font-medium text-foreground">This task</span>
{issueCostSummary.hasCost ? (
<span className="font-medium text-foreground">
${issueCostSummary.cost.toFixed(4)}
@@ -1149,7 +1149,7 @@ function IssueDetailActivityTab({
{hasIssueTreeCost && issueTreeCostSummary ? (
<div className="flex flex-wrap gap-3">
<span className="font-medium text-foreground">
Including sub-issues {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
Including sub-tasks {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
style: "currency",
currency: "USD",
minimumFractionDigits: 4,
@@ -1168,7 +1168,7 @@ function IssueDetailActivityTab({
{` (${issueTreeCostSummary.runCount} run${issueTreeCostSummary.runCount === 1 ? "" : "s"})`}
</span>
) : null}
<span>{issueTreeCostSummary.issueCount} issue{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
<span>{issueTreeCostSummary.issueCount} task{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
</div>
) : null}
</div>
@@ -1382,7 +1382,7 @@ export function IssueDetail() {
}
}, [hasLiveRuns, locallyQueuedCommentRunIds.size]);
const sourceBreadcrumb = useMemo(
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Issues", href: "/issues" },
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Tasks", href: "/issues" },
[issueId, location.state, location.search],
);
@@ -1633,7 +1633,7 @@ export function IssueDetail() {
() => mergeIssueComments(comments ?? [], optimisticComments),
[comments, optimisticComments],
);
const breadcrumbTitle = issue?.title ?? issueId ?? "Issue";
const breadcrumbTitle = issue?.title ?? issueId ?? "Task";
const issueCacheRefs = useMemo(() => {
const refs = new Set<string>();
if (issueId) refs.add(issueId);
@@ -1808,8 +1808,8 @@ export function IssueDetail() {
queryClient.setQueryData(queryKeys.issues.list(context.selectedCompanyId), context.previousList);
}
pushToast({
title: "Issue update failed",
body: err instanceof Error ? err.message : "Unable to save issue changes",
title: "Task update failed",
body: err instanceof Error ? err.message : "Unable to save task changes",
tone: "error",
});
},
@@ -1886,7 +1886,7 @@ export function IssueDetail() {
? treeControlScope === "leaf" ? "Work paused" : "Subtree paused"
: `${modeLabel} applied`,
body: result.kind === "release"
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active issue pause released." : "Active subtree pause released."))
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active task pause released." : "Active subtree pause released."))
: result.hold.mode === "pause"
? treeControlScope === "leaf"
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
@@ -1945,7 +1945,7 @@ export function IssueDetail() {
title: "Work paused",
body: cancelCount > 0
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
: "Work paused. This issue is held until resume.",
: "Work paused. This task is held until resume.",
tone: "success",
});
await Promise.all([
@@ -1982,8 +1982,8 @@ export function IssueDetail() {
},
onError: (err) => {
pushToast({
title: "Issue update failed",
body: err instanceof Error ? err.message : "Unable to save sub-issue changes",
title: "Task update failed",
body: err instanceof Error ? err.message : "Unable to save sub-task changes",
tone: "error",
});
},
@@ -2666,12 +2666,12 @@ export function IssueDetail() {
onSuccess: () => {
invalidateIssueCollections();
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
pushToast({ title: "Issue archived from inbox", tone: "success" });
pushToast({ title: "Task archived from inbox", tone: "success" });
},
onError: (err) => {
pushToast({
title: "Archive failed",
body: err instanceof Error ? err.message : "Unable to archive this issue from the inbox",
body: err instanceof Error ? err.message : "Unable to archive this task from the inbox",
tone: "error",
});
},
@@ -3261,9 +3261,9 @@ export function IssueDetail() {
? "Pause work"
: "Pause and stop work"
: treeControlMode === "cancel"
? `Cancel ${previewAffectedIssueCount} issues`
? `Cancel ${previewAffectedIssueCount} tasks`
: treeControlMode === "restore"
? `Restore ${previewAffectedIssueCount} issues`
? `Restore ${previewAffectedIssueCount} tasks`
: treeControlScope === "leaf"
? "Resume work"
: "Resume subtree";
@@ -3362,7 +3362,7 @@ export function IssueDetail() {
{issue.hiddenAt && (
<div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
<EyeOff className="h-4 w-4 shrink-0" />
This issue is hidden
This task is hidden
</div>
)}
{activePauseHold && (
@@ -3375,13 +3375,13 @@ export function IssueDetail() {
</span>
<span className="text-xs text-amber-900/80 dark:text-amber-100/80">
{childIssues.length === 0
? "Issue execution is held until resume. Human comments can still wake the assignee for triage."
? "Task execution is held until resume. Human comments can still wake the assignee for triage."
: "Root and descendant execution is held until resume. Human comments can still wake assignees for triage."}
</span>
</div>
<div className="text-xs text-amber-900/80 dark:text-amber-100/80">
{childIssues.length === 0
? "1 issue held"
? "1 task held"
: `${heldDescendantCount} descendant${heldDescendantCount === 1 ? "" : "s"} held`}
{activeRootPauseHold?.createdAt ? ` · started ${relativeTime(activeRootPauseHold.createdAt)}` : ""}
</div>
@@ -3427,7 +3427,7 @@ export function IssueDetail() {
</div>
) : (
<div className="text-xs">
This issue is paused by ancestor{" "}
This task is paused by ancestor{" "}
{activePauseHoldRoot?.identifier ? (
<Link to={createIssueDetailPath(activePauseHoldRoot.identifier)} className="underline">
{activePauseHoldRoot.identifier}
@@ -3435,7 +3435,7 @@ export function IssueDetail() {
) : (
activePauseHold.rootIssueId.slice(0, 8)
)}
. Resume from the root issue to deliver deferred work.
. Resume from the root task to deliver deferred work.
</div>
)}
</div>
@@ -3491,7 +3491,7 @@ export function IssueDetail() {
{issue.workMode === "planning" ? (
<span
className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-300 shrink-0"
title="This issue is in planning mode."
title="This task is in planning mode."
>
Planning
</span>
@@ -3550,7 +3550,7 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
onClick={copyIssueToClipboard}
title="Copy issue as markdown"
title="Copy task as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
@@ -3584,7 +3584,7 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
onClick={copyIssueToClipboard}
title="Copy issue as markdown"
title="Copy task as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
@@ -3607,8 +3607,8 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
className="shrink-0"
aria-label="More issue actions"
title="More issue actions"
aria-label="More task actions"
title="More task actions"
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
@@ -3716,7 +3716,7 @@ export function IssueDetail() {
}}
>
<EyeOff className="h-3 w-3" />
Hide this Issue
Hide this task
</button>
</PopoverContent>
</Popover>
@@ -3793,7 +3793,7 @@ export function IssueDetail() {
{showRichSubIssuesSection ? (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium text-muted-foreground">Sub-issues</h3>
<h3 className="text-sm font-medium text-muted-foreground">Sub-tasks</h3>
</div>
<IssuesList
issues={childIssues}
@@ -3809,7 +3809,7 @@ export function IssueDetail() {
searchFilters={{ descendantOf: issue.id, includeBlockedBy: true }}
searchWithinLoadedIssues
baseCreateIssueDefaults={buildSubIssueDefaultsForViewer(issue, currentUserId)}
createIssueLabel="Sub-issue"
createIssueLabel="Sub-task"
defaultSortField="workflow"
showProgressSummary
parentIssueIdForCostSummary={issue.id}
@@ -3820,7 +3820,7 @@ export function IssueDetail() {
<div className="flex flex-wrap items-center justify-end gap-2 min-w-0">
<Button variant="outline" size="sm" onClick={openNewSubIssue} className="shrink-0 shadow-none">
<Plus className="mr-1.5 h-3.5 w-3.5" />
New Sub-issue
New Sub-task
</Button>
</div>
)}
@@ -4062,7 +4062,7 @@ export function IssueDetail() {
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-6 py-4">
{treeControlMode === "cancel" ? (
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
Cancelling a subtree is destructive. Non-terminal issues will be marked cancelled, and running or queued work will be interrupted where possible.
Cancelling a subtree is destructive. Non-terminal tasks will be marked cancelled, and running or queued work will be interrupted where possible.
</div>
) : null}
@@ -4120,7 +4120,7 @@ export function IssueDetail() {
checked={treeControlCancelConfirmed}
onChange={(event) => setTreeControlCancelConfirmed(event.target.checked)}
/>
<span>I understand this will cancel {previewAffectedIssueCount} issues.</span>
<span>I understand this will cancel {previewAffectedIssueCount} tasks.</span>
</label>
) : null}
+3 -3
View File
@@ -108,7 +108,7 @@ export function Issues() {
const issueLinkState = useMemo(
() =>
createIssueDetailLocationState(
"Issues",
"Tasks",
`${location.pathname}${location.search}${location.hash}`,
"issues",
),
@@ -116,7 +116,7 @@ export function Issues() {
);
useEffect(() => {
setBreadcrumbs([{ label: "Issues" }]);
setBreadcrumbs([{ label: "Tasks" }]);
}, [setBreadcrumbs]);
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
@@ -175,7 +175,7 @@ export function Issues() {
});
if (!selectedCompanyId) {
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
return <EmptyState icon={CircleDot} message="Select a company to view tasks." />;
}
return (
+3 -3
View File
@@ -17,7 +17,7 @@ export function MyIssues() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "My Issues" }]);
setBreadcrumbs([{ label: "My Tasks" }]);
}, [setBreadcrumbs]);
const { data: issues, isLoading, error } = useQuery({
@@ -27,7 +27,7 @@ export function MyIssues() {
});
if (!selectedCompanyId) {
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
return <EmptyState icon={ListTodo} message="Select a company to view your tasks." />;
}
if (isLoading) {
@@ -44,7 +44,7 @@ export function MyIssues() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{myIssues.length === 0 && (
<EmptyState icon={ListTodo} message="No issues assigned to you." />
<EmptyState icon={ListTodo} message="No tasks assigned to you." />
)}
{myIssues.length > 0 && (
+1 -1
View File
@@ -822,7 +822,7 @@ export function ProjectDetail() {
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
<PageTabBar
items={[
{ value: "list", label: "Issues" },
{ value: "list", label: "Tasks" },
{ value: "overview", label: "Overview" },
...(project.managedByPlugin ? [{ value: "plugin-operations", label: "Plugin operations" }] : []),
...(showWorkspacesTab ? [{ value: "workspaces", label: "Workspaces" }] : []),
+2 -2
View File
@@ -376,9 +376,9 @@ export function ProjectWorkspaceDetail() {
request.action === "run"
? "Workspace job completed."
: request.action === "stop"
? "Workspace service stopped. Issue execution is not paused."
? "Workspace service stopped. Task execution is not paused."
: request.action === "restart"
? "Workspace service restarted. Issue execution is not paused."
? "Workspace service restarted. Task execution is not paused."
: "Workspace service started.",
);
},
+1 -1
View File
@@ -1264,7 +1264,7 @@ export function RoutineDetail() {
<TabsContent value="secrets" className="space-y-3">
<p className="text-xs text-muted-foreground">
Routine secrets apply to every issue this routine creates. They override matching keys in
Routine secrets apply to every task this routine creates. They override matching keys in
project and agent env. <span className="font-mono">PAPERCLIP_*</span> variables are reserved.
</p>
<EnvVarEditor
+1 -1
View File
@@ -498,7 +498,7 @@ export function Routines() {
Routines
</h1>
<p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues.
Recurring work definitions that materialize into auditable execution tasks.
</p>
</div>
<Button onClick={() => setComposerOpen(true)}>
+9 -9
View File
@@ -32,7 +32,7 @@ const IDENTIFIER_PATTERN = /^[A-Z]+-\d+$/;
const SCOPE_LABELS: Record<CompanySearchScope, string> = {
all: "All",
issues: "Issues",
issues: "Tasks",
comments: "Comments",
documents: "Documents",
artifacts: "Artifacts",
@@ -45,7 +45,7 @@ type SubGroupKey = "issues" | "comments" | "documents" | "artifacts" | "agents"
const SUBGROUP_ORDER: SubGroupKey[] = ["issues", "comments", "documents", "artifacts", "agents", "projects"];
const SUBGROUP_LABELS: Record<SubGroupKey, string> = {
issues: "Issues",
issues: "Tasks",
comments: "Comments",
documents: "Documents",
artifacts: "Artifacts",
@@ -346,7 +346,7 @@ export function Search() {
}
}
}}
placeholder="Search issues, comments, documents, agents, projects…"
placeholder="Search tasks, comments, documents, artifacts, agents, projects…"
aria-label="Search query"
className="h-10 pl-9 pr-20 text-sm"
/>
@@ -456,7 +456,7 @@ function SearchTabContent({
<div>
<h2 className="text-lg font-semibold">Type to search company memory.</h2>
<p className="mt-1 text-sm text-muted-foreground">
Issues, comments, plan documents, agents, projects same surface, ranked by relevance.
Tasks, comments, plan documents, artifacts, agents, projects same surface, ranked by relevance.
</p>
</div>
{recentSearches.length > 0 ? (
@@ -483,7 +483,7 @@ function SearchTabContent({
<ul className="space-y-1 text-xs text-muted-foreground">
<li>
<span className="font-medium text-foreground">Identifier lookup:</span> type{" "}
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">PAP-123</code> to jump straight to an issue.
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">PAP-123</code> to jump straight to a task.
</li>
<li>
<span className="font-medium text-foreground">Quoted phrases:</span> wrap a phrase in quotes to match the
@@ -506,14 +506,14 @@ function SearchTabContent({
<div className="text-base font-semibold">Couldnt run that search</div>
<p className="text-sm text-muted-foreground">
{status ? `The server returned ${status}.` : "The request failed."} Your input and filters are still here, so
you can retry or fall back to the Issues filter.
you can retry or fall back to the Tasks filter.
</p>
<div className="flex flex-wrap items-center justify-center gap-2">
<Button onClick={refetch} variant="default" size="sm">
Retry
</Button>
<Button onClick={navigateIssuesFallback} variant="outline" size="sm">
Open Issues filter view
Open Tasks filter view
</Button>
</div>
</div>
@@ -561,10 +561,10 @@ function SearchTabContent({
) : null}
<Button onClick={openNewIssue} size="sm" variant="default">
<Plus className="mr-1.5 h-4 w-4" />
Create issue from this query
Create task from this query
</Button>
<Button onClick={navigateIssuesFallback} size="sm" variant="ghost">
Open Issues filter view
Open Tasks filter view
</Button>
</div>
<ul className="mt-2 space-y-0.5 text-xs text-muted-foreground">
+1 -1
View File
@@ -1645,7 +1645,7 @@ function SecretsHowToUse() {
</p>
<p>
Paperclip resolves the value server-side when the run starts and injects it as that env var. Project env
applies to every issue in the project and overrides agent env on matching keys.
applies to every task in the project and overrides agent env on matching keys.
</p>
</div>
</div>
+1 -1
View File
@@ -219,7 +219,7 @@ export function UserProfile() {
(data?.topAgents ?? []).map((row) => ({
key: row.agentId ?? "unknown",
label: row.agentName ?? (row.agentId ? row.agentId.slice(0, 8) : "unknown"),
sublabel: "Issue-linked usage",
sublabel: "Task-linked usage",
costCents: row.costCents,
inputTokens: row.inputTokens,
cachedInputTokens: row.cachedInputTokens,