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
@@ -73,6 +73,31 @@ if (!embeddedPostgresSupport.supported) {
);
}
function errorHasPostgresCode(error: unknown, code: string): boolean {
let current: unknown = error;
for (let depth = 0; depth < 4; depth += 1) {
if (!current || typeof current !== "object") return false;
const record = current as { code?: unknown; cause?: unknown };
if (record.code === code) return true;
current = record.cause;
}
return false;
}
async function truncateCompaniesWithDeadlockRetry(db: ReturnType<typeof createDb>) {
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
await db.execute(sql.raw(`TRUNCATE TABLE "companies" CASCADE`));
return;
} catch (error) {
if (!errorHasPostgresCode(error, "40P01") || attempt === 4) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
}
}
}
describeEmbeddedPostgres("active-run output watchdog", () => {
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let db: ReturnType<typeof createDb>;
@@ -91,7 +116,7 @@ describeEmbeddedPostgres("active-run output watchdog", () => {
if (activeRuns.length === 0) break;
await new Promise((resolve) => setTimeout(resolve, 25));
}
await db.execute(sql.raw(`TRUNCATE TABLE "companies" CASCADE`));
await truncateCompaniesWithDeadlockRetry(db);
});
afterAll(async () => {
+4
View File
@@ -13,6 +13,10 @@ description: >
You run in **heartbeats** — short execution windows triggered by Paperclip. Each heartbeat, you wake up, check your work, do something useful, and exit. You do not run continuously.
## Terminology
In Paperclip, **task** and **issue** refer to the same work item. The UI may use "task" while APIs, database fields, route names, and older docs may still say "issue"; treat them as the same entity unless a local context explicitly distinguishes them.
## Authentication
Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL.
+1 -1
View File
@@ -109,7 +109,7 @@ test.describe("Onboarding wizard", () => {
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
await page.getByRole("button", { name: "Create & Open Issue" }).click();
await page.getByRole("button", { name: "Create & Open Task" }).click();
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
@@ -61,7 +61,7 @@ test("captures planning mode UI for desktop and mobile", async ({ page }) => {
await page.getByRole("button", { name: "Next" }).click();
await expect(page.locator("h3", { hasText: "Ready to launch" })).toBeVisible({ timeout: 30_000 });
await page.getByRole("button", { name: "Create & Open Issue" }).click();
await page.getByRole("button", { name: "Create & Open Task" }).click();
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
const openedIssueUrl = page.url();
@@ -69,7 +69,7 @@ test.describe("Docker authenticated onboarding smoke", () => {
await expect(page.getByText(AGENT_NAME)).toBeVisible();
await expect(page.getByText(TASK_TITLE)).toBeVisible();
await page.getByRole("button", { name: "Create & Open Issue" }).click();
await page.getByRole("button", { name: "Create & Open Task" }).click();
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
const baseUrl = new URL(page.url()).origin;
+1 -1
View File
@@ -27,7 +27,7 @@ function RunCardRecoveryChip({ action }: { action: IssueRecoveryAction }) {
data-recovery-state={state}
role="status"
aria-label={tone.label}
title={`${tone.label} — open the source issue to act.`}
title={`${tone.label} — open the source task to act.`}
className={cn(
"inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium",
tone.className,
+2 -2
View File
@@ -144,7 +144,7 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
const maxValue = Math.max(...Array.from(grouped.values()).map(v => Object.values(v).reduce((a, b) => a + b, 0)), 1);
const hasData = Array.from(grouped.values()).some(v => Object.values(v).reduce((a, b) => a + b, 0) > 0);
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
if (!hasData) return <p className="text-xs text-muted-foreground">No tasks</p>;
return (
<div>
@@ -211,7 +211,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
const maxValue = Math.max(...Array.from(grouped.values()).map(v => Object.values(v).reduce((a, b) => a + b, 0)), 1);
const hasData = allStatuses.size > 0;
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
if (!hasData) return <p className="text-xs text-muted-foreground">No tasks</p>;
return (
<div>
+1 -1
View File
@@ -794,7 +794,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
<div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
<Field
label="Default environment"
hint="Agent-level default execution target. Project and issue settings can still override this."
hint="Agent-level default execution target. Project and task settings can still override this."
>
<select
className={inputClass}
+1 -1
View File
@@ -178,7 +178,7 @@ export function BlockedInboxView({
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">No work is stopped.</p>
<p className="text-xs text-muted-foreground">
Issues that need a decision, recovery, or external action will appear here.
Tasks that need a decision, recovery, or external action will appear here.
</p>
</div>
</div>
+5 -5
View File
@@ -121,7 +121,7 @@ export function CommandPalette() {
if (v && isMobile) setSidebarOpen(false);
}}>
<CommandInput
placeholder="Search issues, agents, projects..."
placeholder="Search tasks, agents, projects..."
value={query}
onValueChange={setQuery}
onKeyDown={(event) => {
@@ -135,7 +135,7 @@ export function CommandPalette() {
<CommandEmpty>
{showSearchAll ? (
<span>
No quick issue matches. Press{" "}
No quick task matches. Press{" "}
<kbd className="rounded border border-border bg-muted px-1 py-0.5 text-[10px]"></kbd>{" "}
to <span className="font-medium">search all</span> or keep typing to refine.
</span>
@@ -174,7 +174,7 @@ export function CommandPalette() {
}}
>
<SquarePen className="mr-2 h-4 w-4" />
Create new issue
Create new task
<span className="ml-auto text-xs text-muted-foreground">C</span>
</CommandItem>
<CommandItem
@@ -205,7 +205,7 @@ export function CommandPalette() {
</CommandItem>
<CommandItem onSelect={() => go("/issues")}>
<CircleDot className="mr-2 h-4 w-4" />
Issues
Tasks
</CommandItem>
<CommandItem onSelect={() => go("/projects")}>
<Hexagon className="mr-2 h-4 w-4" />
@@ -232,7 +232,7 @@ export function CommandPalette() {
{visibleIssues.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Issues">
<CommandGroup heading="Tasks">
{visibleIssues.slice(0, 10).map((issue) => (
<CommandItem
key={issue.id}
@@ -93,7 +93,7 @@ export function ExecutionWorkspaceCloseDialog({
<DialogTitle>{actionLabel}</DialogTitle>
<DialogDescription className="break-words">
Archive <span className="font-medium text-foreground">{workspaceName}</span> and clean up any owned workspace
artifacts. Paperclip keeps the workspace record and issue history, but removes it from active workspace views.
artifacts. Paperclip keeps the workspace record and task history, but removes it from active workspace views.
</DialogDescription>
</DialogHeader>
@@ -129,7 +129,7 @@ export function ExecutionWorkspaceCloseDialog({
{blockingIssues.length > 0 ? (
<section className="space-y-2">
<h3 className="text-sm font-medium">Blocking issues</h3>
<h3 className="text-sm font-medium">Blocking tasks</h3>
<div className="space-y-2">
{blockingIssues.map((issue) => (
<div key={issue.id} className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm">
@@ -209,7 +209,7 @@ export function ExecutionWorkspaceCloseDialog({
{otherLinkedIssues.length > 0 ? (
<section className="space-y-2">
<h3 className="text-sm font-medium">Other linked issues</h3>
<h3 className="text-sm font-medium">Other linked tasks</h3>
<div className="space-y-2">
{otherLinkedIssues.map((issue) => (
<div key={issue.id} className="rounded-xl border border-border bg-background px-4 py-3 text-sm">
@@ -136,11 +136,11 @@ describe("IssueBlockedNotice", () => {
/>,
);
expect(node.textContent).toContain("This issue still needs a next step.");
expect(node.textContent).toContain("This task still needs a next step.");
expect(node.textContent).toContain("Corrective wake queued for CodexCoder");
expect(node.textContent).toContain("Detected progress: Updated the plan");
expect(node.textContent).not.toContain("Retry now");
expect(node.textContent).not.toContain("Work on this issue is blocked until");
expect(node.textContent).not.toContain("Work on this task is blocked until");
expect(node.querySelector('[data-successful-run-handoff="required"]')).not.toBeNull();
});
+8 -8
View File
@@ -33,7 +33,7 @@ function BlockerRecoveryIndicator({ action }: { action: IssueRecoveryAction }) {
data-recovery-kind={action.kind}
role="status"
aria-label={label}
title={`${label} — open the source issue to act.`}
title={`${label} — open the source task to act.`}
className={`inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium ${tone.className}`}
>
<Icon className="h-2.5 w-2.5" aria-hidden />
@@ -133,7 +133,7 @@ export function IssueBlockedNotice({
? { issueId, scheduledRetry }
: null;
const blockerLabel = blockers.length === 1 ? "the linked issue" : "the linked issues";
const blockerLabel = blockers.length === 1 ? "the linked task" : "the linked tasks";
const terminalBlockers = blockers
.flatMap((blocker) => blocker.terminalBlockers ?? [])
.filter((blocker, index, all) => all.findIndex((candidate) => candidate.id === blocker.id) === index);
@@ -208,9 +208,9 @@ export function IssueBlockedNotice({
<div className="min-w-0 space-y-1.5">
{showSuccessfulRunHandoff ? (
<>
<p className="font-medium leading-5">This issue still needs a next step.</p>
<p className="font-medium leading-5">This task still needs a next step.</p>
<p className="leading-5">
A run finished successfully, but this issue is still open in{" "}
A run finished successfully, but this task is still open in{" "}
<code className="rounded bg-amber-100 px-1 py-0.5 text-[12px] dark:bg-amber-400/15">
in_progress
</code>{" "}
@@ -261,10 +261,10 @@ export function IssueBlockedNotice({
{blockers.length > 0
? isStalled
? stalledLeafBlockers.length > 1
? <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers.</>
: <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker.</>
: <>Work on this issue is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage.</>
: <>Work on this issue is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
? <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers.</>
: <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker.</>
: <>Work on this task is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage.</>
: <>Work on this task is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
</p>
{blockers.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
+4 -4
View File
@@ -1506,7 +1506,7 @@ describe("IssueChatThread", () => {
);
});
expect(container.textContent).toContain("Work on this issue is blocked by the linked issue");
expect(container.textContent).toContain("Work on this task is blocked by the linked task");
expect(container.textContent).toContain("Comments still wake the assignee for questions or triage");
expect(container.textContent).toContain("PAP-1723");
expect(container.textContent).toContain("QA the install flow");
@@ -2204,7 +2204,7 @@ describe("IssueChatThread", () => {
expect(container.querySelector('[data-testid="issue-chat-composer-drop-overlay"]')).not.toBeNull();
expect(container.textContent).toContain("Drop to upload");
expect(container.textContent).toContain("Images insert into the reply");
expect(container.textContent).toContain("Other files are added to this issue");
expect(container.textContent).toContain("Other files are added to this task");
expect(composer?.className).toContain("border-primary/45");
act(() => {
@@ -2260,7 +2260,7 @@ describe("IssueChatThread", () => {
const attachmentList = container.querySelector('[data-testid="issue-chat-composer-attachments"]');
expect(attachmentList).not.toBeNull();
expect(container.textContent).toContain("report.pdf");
expect(container.textContent).toContain("Attached to issue");
expect(container.textContent).toContain("Attached to task");
await act(async () => {
root.unmount();
@@ -2360,7 +2360,7 @@ describe("IssueChatThread", () => {
expect(attachmentList).not.toBeNull();
expect(attachmentList?.className).toContain("mb-3");
expect(container.textContent).toContain("report.pdf");
expect(container.textContent).toContain("Attached to issue");
expect(container.textContent).toContain("Attached to task");
await act(async () => {
root.unmount();
+7 -7
View File
@@ -516,7 +516,7 @@ function IssueChatFallbackThread({
<div className="space-y-1">
<p className="font-medium">Chat renderer hit an internal state error.</p>
<p className="text-xs opacity-80">
Showing a safe fallback transcript instead of crashing the issues page.
Showing a safe fallback transcript instead of crashing the tasks page.
</p>
</div>
</div>
@@ -3549,7 +3549,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">Drop to upload</div>
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
Images insert into the reply. Other files are added to this issue.
Images insert into the reply. Other files are added to this task.
</div>
</div>
</div>
@@ -3584,12 +3584,12 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
const sizeLabel = formatAttachmentSize(attachment.size);
const statusLabel =
attachment.status === "uploading"
? "Uploading to issue"
? "Uploading to task"
: attachment.status === "error"
? attachment.error ?? "Upload failed"
: attachment.inline
? "Inserted inline"
: "Attached to issue";
: "Attached to task";
return (
<div
key={attachment.id}
@@ -4330,7 +4330,7 @@ export function IssueChatThread({
const resolvedEmptyMessage = emptyMessage
?? (variant === "embedded"
? "No run output yet."
: "This issue conversation is empty. Start with a message below.");
: "This task conversation is empty. Start with a message below.");
const previousErrorBoundaryMessagesRef = useRef<readonly ThreadMessage[] | null>(null);
const errorBoundaryResetVersionRef = useRef(0);
if (previousErrorBoundaryMessagesRef.current !== messages) {
@@ -4420,10 +4420,10 @@ export function IssueChatThread({
{legacyRecoverySourceIssue ? (
<SystemNotice
tone="info"
label="Legacy recovery issue"
label="Legacy recovery task"
body={
<span>
Legacy recovery issue. Newer recovery actions live on the source issue
Legacy recovery task. Newer recovery actions live on the source task
{legacyRecoverySourceIssue.identifier ? (
<>
{" — "}
+7 -7
View File
@@ -28,19 +28,19 @@ const issueColumnLabels: Record<InboxIssueColumn, string> = {
assignee: "Assignee",
project: "Project",
workspace: "Workspace",
parent: "Parent issue",
parent: "Parent task",
labels: "Tags",
updated: "Last updated",
};
const issueColumnDescriptions: Record<InboxIssueColumn, string> = {
status: "Issue state chip on the left edge.",
status: "Task state chip on the left edge.",
id: "Ticket identifier like PAP-1009.",
assignee: "Assigned agent or board user.",
project: "Linked project pill with its color.",
workspace: "Execution or project workspace used for the issue.",
parent: "Parent issue identifier and title.",
labels: "Issue labels and tags.",
workspace: "Execution or project workspace used for the task.",
parent: "Parent task identifier and title.",
labels: "Task labels and tags.",
updated: "Latest visible activity time.",
};
@@ -94,7 +94,7 @@ export function IssueColumnPicker({
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">
<div className="space-y-1">
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
Desktop issue rows
Desktop task rows
</div>
<div className="text-sm font-medium text-foreground">
{title}
@@ -369,7 +369,7 @@ export function InboxIssueTrailingColumns({
{parentIdentifier ? (
<span className="font-mono">{parentIdentifier}</span>
) : (
<span className="italic">Sub-issue</span>
<span className="italic">Sub-task</span>
)}
</span>
);
+1 -1
View File
@@ -182,7 +182,7 @@ export const IssueLinkQuicklook = React.forwardRef<
<div className="h-4 w-full rounded bg-accent/40" />
<div className="h-4 w-3/4 rounded bg-accent/30" />
{!isLoading ? (
<p className="text-xs text-muted-foreground">Unable to load issue preview.</p>
<p className="text-xs text-muted-foreground">Unable to load task preview.</p>
) : null}
</div>
)}
@@ -95,7 +95,7 @@ export function IssuePlanDecompositionsSection({
<span className="text-xs text-muted-foreground/70">·</span>
<span className="inline-flex items-center gap-1 text-xs text-foreground">
<GitBranch className="h-3 w-3 text-muted-foreground" />
{created} of {requested} child {requested === 1 ? "issue" : "issues"} created
{created} of {requested} child {requested === 1 ? "task" : "tasks"} created
</span>
{record.status === "completed" && requested > 0 ? (
<span
+7 -7
View File
@@ -395,11 +395,11 @@ describe("IssueProperties", () => {
});
await flush();
expect(container.textContent).toContain("Sub-issues");
expect(container.textContent).toContain("Add sub-issue");
expect(container.textContent).toContain("Sub-tasks");
expect(container.textContent).toContain("Add sub-task");
const addButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Add sub-issue"));
.find((button) => button.textContent?.includes("Add sub-task"));
expect(addButton).not.toBeUndefined();
await act(async () => {
@@ -467,7 +467,7 @@ describe("IssueProperties", () => {
expect(blockerLink?.textContent).toContain("PAP-2");
expect(blockerLink?.closest("button")).toBeNull();
expect(container.textContent).toContain("Add blocker");
expect(container.querySelector('input[placeholder="Search issues..."]')).toBeNull();
expect(container.querySelector('input[placeholder="Search tasks..."]')).toBeNull();
const addButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Add blocker"));
@@ -478,7 +478,7 @@ describe("IssueProperties", () => {
});
await flush();
expect(container.querySelector('input[placeholder="Search issues..."]')).not.toBeNull();
expect(container.querySelector('input[placeholder="Search tasks..."]')).not.toBeNull();
const candidateButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("PAP-3 New blocker"));
@@ -519,7 +519,7 @@ describe("IssueProperties", () => {
});
await flush();
const searchInput = container.querySelector('input[aria-label="Search issues to add as blockers"]') as HTMLInputElement | null;
const searchInput = container.querySelector('input[aria-label="Search tasks to add as blockers"]') as HTMLInputElement | null;
expect(searchInput).not.toBeNull();
await act(async () => {
@@ -586,7 +586,7 @@ describe("IssueProperties", () => {
});
await flush();
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this issue.");
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this task.");
const confirmButton = Array.from(document.body.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Remove blocker"));
expect(confirmButton).not.toBeUndefined();
+11 -11
View File
@@ -268,7 +268,7 @@ function RemovableIssueReferencePill({
"inline-flex items-center gap-1 rounded-full border border-border py-0.5 pl-1 pr-2 text-xs",
)}
title={issue.title}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
aria-label={`Task ${issueLabel}: ${issue.title}`}
>
<button
type="button"
@@ -283,7 +283,7 @@ function RemovableIssueReferencePill({
<Link
to={`/issues/${issueLabel}`}
className="inline-flex min-w-0 items-center gap-1 no-underline hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
aria-label={`Issue ${issueLabel}: ${issue.title}`}
aria-label={`Task ${issueLabel}: ${issue.title}`}
>
{content}
</Link>
@@ -296,7 +296,7 @@ function RemovableIssueReferencePill({
<DialogHeader>
<DialogTitle>Remove blocker?</DialogTitle>
<DialogDescription>
Remove {confirmLabel} as a blocker for this issue.
Remove {confirmLabel} as a blocker for this task.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@@ -766,7 +766,7 @@ export function IssueProperties({
<div className="w-full space-y-2 p-2">
<p className="text-xs text-muted-foreground">
{assignee
? "This assignee's adapter does not expose editable issue overrides."
? "This assignee's adapter does not expose editable task overrides."
: "Select a compatible agent assignee to edit these overrides."}
</p>
<button
@@ -1621,7 +1621,7 @@ export function IssueProperties({
<>
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder="Search issues..."
placeholder="Search tasks..."
value={parentSearch}
onChange={(e) => setParentSearch(e.target.value)}
autoFocus={!inline}
@@ -1693,11 +1693,11 @@ export function IssueProperties({
<>
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder="Search issues..."
placeholder="Search tasks..."
value={blockedBySearch}
onChange={(e) => setBlockedBySearch(e.target.value)}
autoFocus={!inline}
aria-label="Search issues to add as blockers"
aria-label="Search tasks to add as blockers"
/>
<div className="max-h-48 overflow-y-auto overscroll-contain">
<button
@@ -1734,9 +1734,9 @@ export function IssueProperties({
);
})}
{blockerOptionsLoading ? (
<div className="px-2 py-2 text-xs text-muted-foreground">Searching issues...</div>
<div className="px-2 py-2 text-xs text-muted-foreground">Searching tasks...</div>
) : blockerOptions.length === 0 ? (
<div className="px-2 py-2 text-xs text-muted-foreground">No matching issues.</div>
<div className="px-2 py-2 text-xs text-muted-foreground">No matching tasks.</div>
) : null}
</div>
</>
@@ -1913,7 +1913,7 @@ export function IssueProperties({
) : null}
</PropertyRow>
<PropertyRow label="Sub-issues">
<PropertyRow label="Sub-tasks">
<div className="flex flex-wrap items-center gap-1.5">
{childIssues.length > 0
? childIssues.map((child) => (
@@ -1927,7 +1927,7 @@ export function IssueProperties({
onClick={onAddSubIssue}
>
<Plus className="h-3 w-3" />
Add sub-issue
Add sub-task
</button>
) : null}
</div>
@@ -146,7 +146,7 @@ describe("IssueRecoveryActionCard", () => {
expect(node.textContent).toContain("RECOVERY NEEDED");
expect(node.textContent).toContain("Missing Disposition");
expect(node.textContent).not.toContain("missing_disposition");
expect(node.textContent).toContain("This issue's run finished, but no next step was chosen.");
expect(node.textContent).toContain("This task's run finished, but no next step was chosen.");
expect(node.textContent).toContain("ClaudeCoder");
expect(node.textContent).toContain("CodexCoder");
expect(node.textContent).toContain("Choose and record a valid issue disposition.");
@@ -190,7 +190,7 @@ describe("IssueRecoveryActionCard", () => {
expect(node.textContent).toContain("Workspace Validation");
expect(node.textContent).not.toContain("workspace_validation\n");
expect(node.textContent).toContain(
"Paperclip stopped this run because the issue's git workspace could not be validated.",
"Paperclip stopped this run because the task's git workspace could not be validated.",
);
expect(node.textContent).toContain("Repair the source issue workspace link");
expect(node.textContent).toContain("Manual repair required");
@@ -212,7 +212,7 @@ describe("IssueRecoveryActionCard", () => {
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
expect(document.body.textContent).toContain("Try again");
expect(document.body.textContent).toContain("Mark issue done");
expect(document.body.textContent).toContain("Mark task done");
expect(document.body.textContent).not.toContain("Mark blocked");
expect(document.body.textContent).not.toContain("Delegate follow-up issue");
click([...document.body.querySelectorAll("button")].find((button) => button.textContent?.includes("Try again")) ?? null);
@@ -227,7 +227,7 @@ describe("IssueRecoveryActionCard", () => {
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
expect(document.body.textContent).toContain("Try again");
expect(document.body.textContent).toContain("Mark issue done");
expect(document.body.textContent).toContain("Mark task done");
expect(document.body.textContent).toContain("Send for review");
expect(document.body.textContent).toContain("False positive, done");
expect(document.body.textContent).toContain("False positive, review");
@@ -45,22 +45,22 @@ export interface IssueRecoveryActionCardProps {
const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
missing_disposition: "Missing Disposition",
stranded_assigned_issue: "Stranded Issue",
stranded_assigned_issue: "Stranded Task",
workspace_validation: "Workspace Validation",
active_run_watchdog: "Active Watchdog",
issue_graph_liveness: "Graph Liveness",
};
const KIND_HEADLINE: Record<IssueRecoveryActionKind, string> = {
missing_disposition: "This issue's run finished, but no next step was chosen.",
missing_disposition: "This task's run finished, but no next step was chosen.",
stranded_assigned_issue:
"Paperclip retried this issue's last run and it still has no live execution path.",
"Paperclip retried this task's last run and it still has no live execution path.",
workspace_validation:
"Paperclip stopped this run because the issue's git workspace could not be validated.",
"Paperclip stopped this run because the task's git workspace could not be validated.",
active_run_watchdog:
"The active run has been silent. Recovery is observing without interrupting it.",
issue_graph_liveness:
"Paperclip detected this issue lost a live action path. A recovery owner needs to act.",
"Paperclip detected this task lost a live action path. A recovery owner needs to act.",
};
const STATE_TONE: Record<RecoveryCardCardState, {
@@ -300,11 +300,11 @@ const RESOLVE_OPTIONS: Array<{
{
outcome: "todo",
label: "Try again",
description: "Dismiss recovery and return the source issue to todo.",
description: "Dismiss recovery and return the source task to todo.",
},
{
outcome: "done",
label: "Mark issue done",
label: "Mark task done",
description: "Restore by recording the requested work as complete.",
},
{
@@ -315,14 +315,14 @@ const RESOLVE_OPTIONS: Array<{
{
outcome: "false_positive_done",
label: "False positive, done",
description: "Dismiss recovery and mark the source issue complete.",
description: "Dismiss recovery and mark the source task complete.",
destructive: true,
boardOnly: true,
},
{
outcome: "false_positive_in_review",
label: "False positive, review",
description: "Dismiss recovery and send the source issue for review.",
description: "Dismiss recovery and send the source task for review.",
destructive: true,
boardOnly: true,
},
+2 -2
View File
@@ -37,7 +37,7 @@ export function IssueReferencePill({
data-mention-kind="issue"
className={classNames}
title={issue.title}
aria-label={`Issue: ${issue.title}`}
aria-label={`Task: ${issue.title}`}
>
{content}
</span>
@@ -50,7 +50,7 @@ export function IssueReferencePill({
data-mention-kind="issue"
className={classNames}
title={issue.title}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
aria-label={`Task ${issueLabel}: ${issue.title}`}
>
{content}
</Link>
@@ -55,8 +55,8 @@ describe("IssueRelatedWorkPanel", () => {
expect(html).toContain("Referenced by");
expect(html).toContain("PAP-22");
expect(html).toContain("PAP-33");
expect(html).toContain('aria-label="Issue PAP-22: Downstream task"');
expect(html).toContain('aria-label="Issue PAP-33: Upstream task"');
expect(html).toContain('aria-label="Task PAP-22: Downstream task"');
expect(html).toContain('aria-label="Task PAP-33: Upstream task"');
expect(html).toContain("plan");
expect(html).toContain("comment");
});
+4 -4
View File
@@ -95,15 +95,15 @@ export function IssueRelatedWorkPanel({
<div className="space-y-3">
<Section
title="References"
description="Other tasks this issue currently points at in its title, description, comments, or documents."
description="Other tasks this task currently points at in its title, description, comments, or documents."
items={outbound}
emptyLabel="This issue does not reference any other tasks yet."
emptyLabel="This task does not reference any other tasks yet."
/>
<Section
title="Referenced by"
description="Other tasks that currently point at this issue."
description="Other tasks that currently point at this task."
items={inbound}
emptyLabel="No other tasks reference this issue yet."
emptyLabel="No other tasks reference this task yet."
/>
</div>
);
+1 -1
View File
@@ -250,7 +250,7 @@ function renderRecoveryChip(action: IssueRecoveryAction, selected: boolean): Rea
tone.className,
selected ? "!border-muted-foreground !text-muted-foreground" : null,
)}
title={`${label} — open the source issue to act.`}
title={`${label} — open the source task to act.`}
>
<Icon className="h-2.5 w-2.5" aria-hidden />
{label}
+5 -5
View File
@@ -75,7 +75,7 @@ const LIVENESS_COPY: Record<RunLivenessState, LivenessCopy> = {
completed: {
label: "Completed",
tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
description: "Issue reached a terminal state.",
description: "Task reached a terminal state.",
},
advanced: {
label: "Advanced",
@@ -95,7 +95,7 @@ const LIVENESS_COPY: Record<RunLivenessState, LivenessCopy> = {
blocked: {
label: "Blocked",
tone: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300",
description: "Run or issue declared a blocker.",
description: "Run or task declared a blocker.",
},
failed: {
label: "Failed",
@@ -535,7 +535,7 @@ export function IssueRunLedgerContent({
}, [activityEvents, canRenderActivityEvents, ledgerRuns]);
return (
<section className="space-y-3" aria-label="Issue run ledger">
<section className="space-y-3" aria-label="Task run ledger">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<h3 className="text-sm font-medium text-muted-foreground">Run ledger</h3>
@@ -678,8 +678,8 @@ export function IssueRunLedgerContent({
{feedItems.length === 0 ? (
<div className="rounded-md border border-dashed border-border px-3 py-3 text-sm text-muted-foreground">
{renderActivityEvent
? "Runs and activity will appear here once this issue has history."
: "Historical runs without liveness metadata will appear here once linked to this issue."}
? "Runs and activity will appear here once this task has history."
: "Historical runs without liveness metadata will appear here once linked to this task."}
</div>
) : (
<div className="space-y-1.5">
@@ -84,7 +84,7 @@ describe("IssueSiblingNavigation", () => {
);
const nav = node.querySelector("nav");
expect(nav?.getAttribute("aria-label")).toBe("Sub-issue navigation");
expect(nav?.getAttribute("aria-label")).toBe("Sub-task navigation");
expect(nav?.className).toContain("sm:grid-cols-2");
expect(nav?.className).not.toContain("border-t");
@@ -93,13 +93,13 @@ describe("IssueSiblingNavigation", () => {
expect(links[0].textContent).toContain("Previous");
expect(links[0].textContent).toContain("PAP-1");
expect(links[0].textContent).toContain("Previous sibling title");
expect(links[0].getAttribute("aria-label")).toBe("Previous sub-issue: PAP-1 - Previous sibling title");
expect(links[0].getAttribute("aria-label")).toBe("Previous sub-task: PAP-1 - Previous sibling title");
expect(links[0].getAttribute("data-quicklook-align")).toBe("start");
expect(links[1].textContent).toContain("Next");
expect(links[1].textContent).toContain("PAP-3");
expect(links[1].textContent).toContain("Next sibling title");
expect(links[1].getAttribute("aria-label")).toBe("Next sub-issue: PAP-3 - Next sibling title");
expect(links[1].getAttribute("aria-label")).toBe("Next sub-task: PAP-3 - Next sibling title");
expect(links[1].getAttribute("data-quicklook-align")).toBe("end");
expect(links[1].className).toContain("sm:text-right");
+2 -2
View File
@@ -16,7 +16,7 @@ export function IssueSiblingNavigation({ navigation, linkState }: IssueSiblingNa
return (
<nav
aria-label="Sub-issue navigation"
aria-label="Sub-task navigation"
className="mt-4 flex flex-col gap-3 sm:mt-6 sm:grid sm:grid-cols-2"
>
{navigation.previous ? (
@@ -47,7 +47,7 @@ function SiblingLink({
}) {
const issuePathId = issue.identifier ?? issue.id;
const label = direction === "previous" ? "Previous" : "Next";
const ariaDirection = direction === "previous" ? "Previous sub-issue" : "Next sub-issue";
const ariaDirection = direction === "previous" ? "Previous sub-task" : "Next sub-task";
const identifier = issue.identifier ?? issue.id.slice(0, 8);
const Icon = direction === "previous" ? ChevronLeft : ChevronRight;
@@ -467,7 +467,7 @@ function SuggestTasksCard({
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{totalTasks === 1 ? "1 draft issue" : `${totalTasks} draft issues`}</span>
<span>{totalTasks === 1 ? "1 draft task" : `${totalTasks} draft tasks`}</span>
{interaction.payload.defaultParentId ? (
<TaskField label="Default parent" value={interaction.payload.defaultParentId} tone="subtle" />
) : null}
@@ -497,8 +497,8 @@ function SuggestTasksCard({
</div>
<p className="mt-1 leading-6">
{skippedCount > 0
? `Created ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"} and skipped ${skippedCount} during review.`
: `Created all ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"}.`}
? `Created ${createdCount} draft ${createdCount === 1 ? "task" : "tasks"} and skipped ${skippedCount} during review.`
: `Created all ${createdCount} draft ${createdCount === 1 ? "task" : "tasks"}.`}
</p>
</div>
) : null}
@@ -523,8 +523,8 @@ function SuggestTasksCard({
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>
{selectedCount === totalTasks
? `All ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} selected`
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} selected`}
? `All ${totalTasks} draft ${totalTasks === 1 ? "task" : "tasks"} selected`
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "task" : "tasks"} selected`}
</span>
{selectedCount < totalTasks ? (
<span>
+3 -3
View File
@@ -438,10 +438,10 @@ export function IssueWorkspaceCard({
{!workspace && (
<div className="text-muted-foreground">
{currentSelection === "isolated_workspace"
? "A fresh isolated workspace will be created when this issue runs."
? "A fresh isolated workspace will be created when this task runs."
: currentSelection === "reuse_existing"
? "This issue will reuse an existing workspace when it runs."
: "This issue will use the project default workspace configuration when it runs."}
? "This task will reuse an existing workspace when it runs."
: "This task will use the project default workspace configuration when it runs."}
</div>
)}
{currentSelection === "reuse_existing" && selectedReusableExecutionWorkspace && (
+10 -10
View File
@@ -452,12 +452,12 @@ describe("IssuesList", () => {
);
await waitForAssertion(() => {
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue in Feature Branch"]');
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New task in Feature Branch"]');
expect(button).not.toBeNull();
});
await act(async () => {
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue in Feature Branch"]');
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New task in Feature Branch"]');
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();
});
@@ -884,7 +884,7 @@ describe("IssuesList", () => {
container,
);
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
expect(valueSetter).toBeTypeOf("function");
@@ -1157,7 +1157,7 @@ describe("IssuesList", () => {
);
await waitForAssertion(() => {
expect(container.textContent).toContain("Some board columns are showing up to 200 issues. Refine filters or search to reveal the rest.");
expect(container.textContent).toContain("Some board columns are showing up to 200 tasks. Refine filters or search to reveal the rest.");
});
act(() => {
@@ -1187,7 +1187,7 @@ describe("IssuesList", () => {
await waitForAssertion(() => {
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(100);
expect(container.textContent).toContain("Rendering 100 of 220 issues");
expect(container.textContent).toContain("Rendering 100 of 220 tasks");
});
act(() => {
@@ -1226,7 +1226,7 @@ describe("IssuesList", () => {
await waitForAssertion(() => {
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(250);
expect(container.textContent).toContain("Rendering 250 of 420 issues");
expect(container.textContent).toContain("Rendering 250 of 420 tasks");
});
act(() => {
@@ -1669,13 +1669,13 @@ describe("IssuesList", () => {
);
await waitForAssertion(() => {
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
input?.focus();
expect(document.activeElement).toBe(input);
});
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement;
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
act(() => {
input.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter",
@@ -1705,13 +1705,13 @@ describe("IssuesList", () => {
);
await waitForAssertion(() => {
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
input?.focus();
expect(document.activeElement).toBe(input);
});
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement;
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
act(() => {
input.dispatchEvent(new KeyboardEvent("keydown", {
key: "Escape",
+19 -19
View File
@@ -470,9 +470,9 @@ function IssueSearchInput({
e.currentTarget.blur();
}
}}
placeholder="Search issues..."
placeholder="Search tasks..."
className="pl-7 text-xs sm:text-sm"
aria-label="Search issues"
aria-label="Search tasks"
data-page-search-target="true"
/>
</div>
@@ -531,7 +531,7 @@ function SubIssueProgressSummaryStrip({
className="text-muted-foreground tabular-nums"
title={`${costSummary.runCount.toLocaleString()} run${
costSummary.runCount === 1 ? "" : "s"
} across ${costSummary.issueCount} sub-issue${
} across ${costSummary.issueCount} sub-task${
costSummary.issueCount === 1 ? "" : "s"
}`}
>
@@ -545,7 +545,7 @@ function SubIssueProgressSummaryStrip({
</div>
<div
role="progressbar"
aria-label="Sub-issues completion progress"
aria-label="Sub-tasks completion progress"
aria-valuemin={0}
aria-valuenow={summary.doneCount}
aria-valuemax={summary.totalCount}
@@ -582,11 +582,11 @@ function SubIssueProgressSummaryStrip({
</Link>
</>
) : summary.totalCount === 0 ? (
<div className="text-sm font-medium text-foreground">No active sub-issues</div>
<div className="text-sm font-medium text-foreground">No active sub-tasks</div>
) : summary.doneCount === summary.totalCount ? (
<div className="text-sm font-medium text-foreground">All sub-issues done</div>
<div className="text-sm font-medium text-foreground">All sub-tasks done</div>
) : (
<div className="text-sm font-medium text-foreground">No actionable sub-issues</div>
<div className="text-sm font-medium text-foreground">No actionable sub-tasks</div>
)}
</div>
</div>
@@ -1289,8 +1289,8 @@ export function IssuesList({
viewState.groupBy,
]);
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Issue";
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Issue";
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Task";
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Task";
const openCreateIssueDialog = useCallback((group?: { key: string; items: Issue[] }) => {
openNewIssue(newIssueDefaults(group));
}, [newIssueDefaults, openNewIssue]);
@@ -1461,7 +1461,7 @@ export function IssuesList({
visibleColumnSet={visibleIssueColumnSet}
onToggleColumn={toggleIssueColumn}
onResetColumns={() => setIssueColumns(DEFAULT_INBOX_ISSUE_COLUMNS)}
title="Choose which issue columns stay visible"
title="Choose which task columns stay visible"
iconOnly
/>
@@ -1539,7 +1539,7 @@ export function IssuesList({
["assignee", "Assignee"],
["project", "Project"],
["workspace", "Workspace"],
["parent", "Parent Issue"],
["parent", "Parent Task"],
["none", "None"],
] as const).map(([value, label]) => (
<button
@@ -1569,13 +1569,13 @@ export function IssuesList({
)}
{boardColumnLimitReached && (
<p className="text-xs text-muted-foreground">
Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} issues. Refine filters or search to reveal the rest.
Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} tasks. Refine filters or search to reveal the rest.
</p>
)}
{!isLoading && filtered.length === 0 && viewState.viewMode === "list" && (
<EmptyState
icon={CircleDot}
message="No issues match the current filters or search."
message="No tasks match the current filters or search."
action={createActionLabel}
onAction={() => openCreateIssueDialog()}
/>
@@ -1625,8 +1625,8 @@ export function IssuesList({
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={() => openCreateIssueDialog(group)}
>
<Plus className="h-3 w-3" />
@@ -1771,7 +1771,7 @@ export function IssuesList({
<span
className="ml-1.5 inline-flex items-center gap-1 rounded-full border border-amber-400/45 bg-amber-50/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-300/35 dark:bg-amber-400/10 dark:text-amber-300"
aria-label="Needs next step"
title="This issue needs a next step"
title="This task needs a next step"
>
<CircleDot className="h-3 w-3" />
Needs next step
@@ -1959,10 +1959,10 @@ export function IssuesList({
<div className="py-2" data-testid="issues-load-more-sentinel">
<p className="text-xs text-muted-foreground">
{isLoadingMoreIssues
? "Loading more issues..."
? "Loading more tasks..."
: remainingIssueRowCount > 0
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} issues`
: "Scroll to load more issues"}
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} tasks`
: "Scroll to load more tasks"}
</p>
</div>
)}
+1 -1
View File
@@ -236,7 +236,7 @@ function KanbanCard({
{isSuccessfulRunHandoffRequired(issue) ? (
<span
className="inline-flex items-center gap-1 rounded-full border border-amber-400/45 bg-amber-50/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-300/35 dark:bg-amber-400/10 dark:text-amber-300"
title="This issue needs a next step"
title="This task needs a next step"
aria-label="Needs next step"
>
<AlertTriangle className="h-3 w-3" />
@@ -28,7 +28,7 @@ const sections: ShortcutSection[] = [
],
},
{
title: "Issue detail",
title: "Task detail",
shortcuts: [
{ keys: ["y"], label: "Quick-archive back to inbox" },
{ keys: ["g", "i"], label: "Go to inbox" },
@@ -39,7 +39,7 @@ const sections: ShortcutSection[] = [
title: "Global",
shortcuts: [
{ keys: ["/"], label: "Search current page or quick search" },
{ keys: ["c"], label: "New issue" },
{ keys: ["c"], label: "New task" },
{ keys: ["["], label: "Toggle sidebar" },
{ keys: ["]"], label: "Toggle panel" },
{ keys: ["?"], label: "Show keyboard shortcuts" },
+1 -1
View File
@@ -95,7 +95,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
Live Runs
</div>
<div className="mt-1 text-xs text-muted-foreground">
Uses the shared chat-style run surface from issue activity.
Uses the shared chat-style run surface from task activity.
</div>
</div>
+1 -1
View File
@@ -43,7 +43,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
const items = useMemo<MobileNavItem[]>(
() => [
{ type: "link", to: "/dashboard", label: "Home", icon: House },
{ type: "link", to: "/issues", label: "Issues", icon: CircleDot },
{ type: "link", to: "/issues", label: "Tasks", icon: CircleDot },
{ type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() },
{ type: "link", to: "/agents/all", label: "Agents", icon: Users },
{
+21 -21
View File
@@ -352,11 +352,11 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
expect(container.textContent).toContain("New sub-issue");
expect(container.textContent).toContain("Sub-issue of");
expect(container.textContent).toContain("New sub-task");
expect(container.textContent).toContain("Sub-task of");
expect(container.textContent).toContain("PAP-1");
expect(container.textContent).toContain("Parent issue");
expect(container.textContent).toContain("Create Sub-Issue");
expect(container.textContent).toContain("Create Sub-Task");
act(() => root.unmount());
@@ -364,9 +364,9 @@ describe("NewIssueDialog", () => {
const rerendered = renderDialog(container);
await flush();
expect(container.textContent).toContain("New issue");
expect(container.textContent).toContain("Create Issue");
expect(container.textContent).not.toContain("Sub-issue of");
expect(container.textContent).toContain("New task");
expect(container.textContent).toContain("Create Task");
expect(container.textContent).not.toContain("Sub-task of");
act(() => rerendered.root.unmount());
});
@@ -421,7 +421,7 @@ describe("NewIssueDialog", () => {
expect(mockExecutionWorkspacesApi.list).not.toHaveBeenCalled();
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Sub-Issue"));
.find((button) => button.textContent?.includes("Create Sub-Task"));
expect(submitButton).not.toBeUndefined();
await waitForAssertion(() => {
expect(submitButton?.hasAttribute("disabled")).toBe(false);
@@ -460,7 +460,7 @@ describe("NewIssueDialog", () => {
expect(planningButton?.className).toContain("bg-accent");
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Issue"));
.find((button) => button.textContent?.includes("Create Task"));
expect(submitButton).not.toBeUndefined();
await vi.waitFor(() => {
expect(submitButton?.hasAttribute("disabled")).toBe(false);
@@ -531,14 +531,14 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
expect(container.textContent).toContain("New issue");
expect(container.textContent).not.toContain("New sub-issue");
expect(container.textContent).toContain("New task");
expect(container.textContent).not.toContain("New sub-task");
await waitForAssertion(() => {
expect(container.textContent).toContain("Reusing PAP-100");
});
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Issue"));
.find((button) => button.textContent?.includes("Create Task"));
expect(submitButton).not.toBeUndefined();
await act(async () => {
@@ -578,7 +578,7 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
expect(titleInput).not.toBeNull();
expect(descriptionInput).not.toBeNull();
@@ -601,7 +601,7 @@ describe("NewIssueDialog", () => {
await flush();
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Issue"));
.find((button) => button.textContent?.includes("Create Task"));
expect(submitButton).not.toBeUndefined();
await vi.waitFor(() => {
expect(submitButton?.hasAttribute("disabled")).toBe(false);
@@ -635,7 +635,7 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
expect(titleInput).not.toBeNull();
expect(descriptionInput).not.toBeNull();
@@ -644,7 +644,7 @@ describe("NewIssueDialog", () => {
await typeTextareaValue(descriptionInput!, description);
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Issue"));
.find((button) => button.textContent?.includes("Create Task"));
expect(submitButton).not.toBeUndefined();
await vi.waitFor(() => {
expect(submitButton?.hasAttribute("disabled")).toBe(false);
@@ -671,7 +671,7 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
expect(titleInput).not.toBeNull();
await typeTextareaValue(titleInput!, "Plan this first");
@@ -683,7 +683,7 @@ describe("NewIssueDialog", () => {
await flush();
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Issue"));
.find((button) => button.textContent?.includes("Create Task"));
expect(submitButton).not.toBeUndefined();
await vi.waitFor(() => {
expect(submitButton?.hasAttribute("disabled")).toBe(false);
@@ -720,7 +720,7 @@ describe("NewIssueDialog", () => {
await flush();
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Sub-Issue"));
.find((button) => button.textContent?.includes("Create Sub-Task"));
expect(submitButton).not.toBeUndefined();
await act(async () => {
@@ -754,7 +754,7 @@ describe("NewIssueDialog", () => {
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-top)");
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-bottom)");
const titleInput = container.querySelector('textarea[placeholder="Issue title"]');
const titleInput = container.querySelector('textarea[placeholder="Task title"]');
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]');
const bodyScrollRegion = Array.from(container.querySelectorAll("div")).find((element) =>
typeof element.className === "string" && element.className.includes("overscroll-contain"),
@@ -862,7 +862,7 @@ describe("NewIssueDialog", () => {
await flush();
await flush();
expect(container.textContent).not.toContain("will no longer use the parent issue workspace");
expect(container.textContent).not.toContain("will no longer use the parent task workspace");
const selects = Array.from(container.querySelectorAll("select"));
const modeSelect = selects[0] as HTMLSelectElement | undefined;
@@ -874,7 +874,7 @@ describe("NewIssueDialog", () => {
});
await flush();
expect(container.textContent).toContain("will no longer use the parent issue workspace");
expect(container.textContent).toContain("will no longer use the parent task workspace");
expect(container.textContent).toContain("Parent workspace");
act(() => root.unmount());
+8 -8
View File
@@ -316,7 +316,7 @@ const IssueTitleTextarea = memo(function IssueTitleTextarea({
return (
<textarea
className="w-full text-lg font-semibold bg-transparent outline-none resize-none overflow-hidden placeholder:text-muted-foreground/50"
placeholder="Issue title"
placeholder="Task title"
rows={1}
value={draftValue}
onChange={(e) => {
@@ -1145,7 +1145,7 @@ export function NewIssueDialog() {
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
const canDiscardDraft = hasDraft || hasSavedDraft;
const createIssueErrorMessage =
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again.";
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create task. Try again.";
const stagedDocuments = stagedFiles.filter((file) => file.kind === "document");
const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment");
@@ -1294,7 +1294,7 @@ export function NewIssueDialog() {
</PopoverContent>
</Popover>
<span className="text-muted-foreground/60">&rsaquo;</span>
<span>{isSubIssueMode ? "New sub-issue" : "New issue"}</span>
<span>{isSubIssueMode ? "New sub-task" : "New task"}</span>
</div>
<div className="flex items-center gap-1">
<Button
@@ -1579,7 +1579,7 @@ export function NewIssueDialog() {
<div className="max-w-full rounded-md border border-border bg-muted/30 px-2.5 py-1.5 text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<ListTree className="h-3.5 w-3.5 shrink-0" />
<span className="shrink-0">Sub-issue of</span>
<span className="shrink-0">Sub-task of</span>
<span className="font-medium text-foreground">{parentIssueLabel}</span>
</div>
{newIssueDefaults.parentTitle ? (
@@ -1596,7 +1596,7 @@ export function NewIssueDialog() {
<div className="space-y-1.5">
<div className="text-xs font-medium">Execution workspace</div>
<div className="text-[11px] text-muted-foreground">
Control whether this issue runs in the shared workspace, a new isolated workspace, or an existing one.
Control whether this task runs in the shared workspace, a new isolated workspace, or an existing one.
</div>
<select
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
@@ -1635,7 +1635,7 @@ export function NewIssueDialog() {
)}
{showParentWorkspaceWarning ? (
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/30 dark:text-amber-100">
Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
Warning: this sub-task will no longer use the parent task workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
</div>
) : null}
</div>
@@ -1694,7 +1694,7 @@ export function NewIssueDialog() {
<p className="text-[11px] text-muted-foreground">Runs on the agent's primary model.</p>
)}
{assigneeModelLane === "custom" && (
<p className="text-[11px] text-muted-foreground">Override the model and effort for this issue only.</p>
<p className="text-[11px] text-muted-foreground">Override the model and effort for this task only.</p>
)}
</div>
{assigneeModelLane === "custom" && (
@@ -2079,7 +2079,7 @@ export function NewIssueDialog() {
>
<span className="inline-flex items-center justify-center gap-1.5">
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Issue" : "Create Issue"}</span>
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Task" : "Create Task"}</span>
</span>
</Button>
</div>
+2 -2
View File
@@ -1134,7 +1134,7 @@ export function OnboardingWizard() {
<h3 className="font-medium">Ready to launch</h3>
<p className="text-xs text-muted-foreground">
Everything is set up. Launching now will create the
starter task, wake the agent, and open the issue.
starter task, wake the agent, and open the task.
</p>
</div>
</div>
@@ -1249,7 +1249,7 @@ export function OnboardingWizard() {
) : (
<ArrowRight className="h-3.5 w-3.5 mr-1" />
)}
{loading ? "Creating..." : "Create & Open Issue"}
{loading ? "Creating..." : "Create & Open Task"}
</Button>
)}
</div>
+6 -6
View File
@@ -632,7 +632,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
onChange={(env) => commitField("env", { env: env ?? null })}
/>
<p className="text-[11px] text-muted-foreground">
Applied to all runs for issues in this project. Project values override agent env on key conflicts.
Applied to all runs for tasks in this project. Project values override agent env on key conflicts.
</p>
</div>
</PropertyRow>
@@ -925,7 +925,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
</button>
</TooltipTrigger>
<TooltipContent side="top">
Project-owned defaults for isolated issue checkouts and execution workspace behavior.
Project-owned defaults for isolated task checkouts and execution workspace behavior.
</TooltipContent>
</Tooltip>
</div>
@@ -933,11 +933,11 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<div className="flex items-center gap-2 text-sm font-medium">
<span>Enable isolated issue checkouts</span>
<span>Enable isolated task checkouts</span>
<SaveIndicator state={fieldState("execution_workspace_enabled")} />
</div>
<div className="text-xs text-muted-foreground">
Let issues choose between the project's primary checkout and an isolated execution workspace.
Let tasks choose between the project's primary checkout and an isolated execution workspace.
</div>
</div>
{onUpdate || onFieldUpdate ? (
@@ -961,11 +961,11 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<div className="flex items-center gap-2 text-sm">
<span>New issues default to isolated checkout</span>
<span>New tasks default to isolated checkout</span>
<SaveIndicator state={fieldState("execution_workspace_default_mode")} />
</div>
<div className="text-[11px] text-muted-foreground">
If disabled, new issues stay on the project's primary checkout unless someone opts in.
If disabled, new tasks stay on the project's primary checkout unless someone opts in.
</div>
</div>
<ToggleSwitch
@@ -124,7 +124,7 @@ describe("ProjectWorkspaceSummaryCard", () => {
expect(container.textContent).toContain("Branch");
expect(container.textContent).toContain("Path");
expect(container.textContent).toContain("Service");
expect(container.textContent).toContain("Linked issues");
expect(container.textContent).toContain("Linked tasks");
expect(container.textContent).toContain("Start services");
expect(container.textContent).toContain("Close workspace");
expect(container.textContent).toContain("+1 more");
@@ -227,7 +227,7 @@ export function ProjectWorkspaceSummaryCard({
{summary.issues.length > 0 ? (
<div className="space-y-2">
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
Linked issues
Linked tasks
</div>
<div className="flex flex-wrap gap-2">
{visibleIssues.map((issue) => (
+11 -11
View File
@@ -159,7 +159,7 @@ describe("Sidebar", () => {
expect(workSection?.textContent).toContain("Plugin launcher outlet");
const workSectionContainer = workSection?.parentElement?.parentElement;
expect(workSectionContainer?.textContent).toContain("Work");
expect(workSectionContainer?.textContent).toContain("Issues");
expect(workSectionContainer?.textContent).toContain("Tasks");
expect(workSectionContainer?.textContent).toContain("Goals");
flushSync(() => {
@@ -167,19 +167,19 @@ describe("Sidebar", () => {
});
});
it("streamlined (flag ON): keeps Issue wording, top-level Projects link, no per-project collapsible", async () => {
it("streamlined (flag ON): keeps Task wording, top-level Projects link, no per-project collapsible", async () => {
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
enableIsolatedWorkspaces: false,
enableStreamlinedLeftNavigation: true,
});
const root = await renderSidebar();
expect(container.textContent).toContain("New Issue");
expect(container.textContent).not.toContain("New Task");
expect(container.textContent).toContain("New Task");
expect(container.textContent).not.toContain("New Issue");
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
expect(navLabels).toContain("Issues");
expect(navLabels).not.toContain("Tasks");
expect(navLabels).toContain("Tasks");
expect(navLabels).not.toContain("Issues");
const projectsLink = [...container.querySelectorAll("nav a")].find((a) => a.textContent?.trim() === "Projects");
expect(projectsLink?.getAttribute("href")).toBe("/projects");
@@ -194,19 +194,19 @@ describe("Sidebar", () => {
});
});
it("classic (flag OFF): New Issue button, Issues label, per-project collapsible, no top-level Projects link", async () => {
it("classic (flag OFF): New Task button, Tasks label, per-project collapsible, no top-level Projects link", async () => {
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
enableIsolatedWorkspaces: false,
enableStreamlinedLeftNavigation: false,
});
const root = await renderSidebar();
expect(container.textContent).toContain("New Issue");
expect(container.textContent).not.toContain("New Task");
expect(container.textContent).toContain("New Task");
expect(container.textContent).not.toContain("New Issue");
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
expect(navLabels).toContain("Issues");
expect(navLabels).not.toContain("Tasks");
expect(navLabels).toContain("Tasks");
expect(navLabels).not.toContain("Issues");
// No top-level Projects nav link in classic mode (D5 option A).
expect(navLabels).not.toContain("Projects");
+3 -3
View File
@@ -80,14 +80,14 @@ export function Sidebar() {
<nav className="flex-1 min-h-0 overflow-y-auto scrollbar-auto-hide flex flex-col gap-4 pointer-coarse:gap-3 px-3 py-2">
<div className="flex flex-col gap-0.5">
{/* New Issue button aligned with nav items */}
{/* New Task button aligned with nav items */}
<button
onClick={() => openNewIssue()}
data-slot="icon-button"
className="flex items-center gap-2.5 px-3 py-2 pointer-coarse:py-1.5 text-[13px] font-medium text-foreground/80 hover:bg-accent/50 hover:text-foreground transition-colors"
>
<SquarePen className="h-4 w-4 shrink-0" />
<span className="truncate">New Issue</span>
<span className="truncate">New Task</span>
</button>
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} />
<SidebarNavItem
@@ -101,7 +101,7 @@ export function Sidebar() {
</div>
<SidebarSection label="Work">
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
<SidebarNavItem to="/issues" label="Tasks" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
<SidebarNavItem to="/artifacts" label="Artifacts" icon={Package} />
@@ -107,7 +107,7 @@ export function SourceResolvedFoldCallout({
"[&>*]:border-emerald-300/40 dark:[&>*]:border-emerald-500/20",
)}
>
<MetaRow label="Source issue">
<MetaRow label="Source task">
<span className="inline-flex flex-wrap items-center gap-1.5">
<Link
to={issueLink(fold.sourceIssueId, fold.sourceIssueIdentifier)}
@@ -160,7 +160,7 @@ export function SourceResolvedFoldCallout({
</span>
</MetaRow>
{fold.evaluationIssueId ? (
<MetaRow label="Evaluation issue">
<MetaRow label="Evaluation task">
<Link
to={issueLink(fold.evaluationIssueId, fold.evaluationIssueIdentifier)}
className="rounded-sm font-medium underline-offset-2 hover:underline"
+2 -2
View File
@@ -23,8 +23,8 @@ describe("StatusIcon", () => {
);
expect(html).toContain('data-blocker-attention-state="covered"');
expect(html).toContain('aria-label="Blocked · waiting on active sub-issue PAP-2"');
expect(html).toContain('title="Blocked · waiting on active sub-issue PAP-2"');
expect(html).toContain('aria-label="Blocked · waiting on active sub-task PAP-2"');
expect(html).toContain('title="Blocked · waiting on active sub-task PAP-2"');
expect(html).toContain("border-cyan-600");
expect(html).not.toContain("border-red-600");
expect(html).not.toContain("border-dashed");
+3 -3
View File
@@ -25,10 +25,10 @@ function blockedAttentionLabel(blockerAttention: IssueBlockerAttention | null |
if (blockerAttention.reason === "active_child") {
const count = blockerAttention.coveredBlockerCount;
if (count === 1 && blockerAttention.sampleBlockerIdentifier) {
return `Blocked · waiting on active sub-issue ${blockerAttention.sampleBlockerIdentifier}`;
return `Blocked · waiting on active sub-task ${blockerAttention.sampleBlockerIdentifier}`;
}
if (count === 1) return "Blocked · waiting on 1 active sub-issue";
return `Blocked · waiting on ${count} active sub-issues`;
if (count === 1) return "Blocked · waiting on 1 active sub-task";
return `Blocked · waiting on ${count} active sub-tasks`;
}
if (blockerAttention.reason === "active_dependency") {
@@ -27,7 +27,7 @@ export function IssueOutputSection({ workProducts, resolveCreatorName }: IssueOu
const creatorFor = (item: IssueOutputItem) => resolveCreatorName?.(item) ?? null;
return (
<section className="space-y-3" aria-label="Issue outputs">
<section className="space-y-3" aria-label="Task outputs">
<div className="flex items-center gap-2">
<Play className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
<h3 className="text-sm font-medium text-muted-foreground">Output</h3>
+1 -1
View File
@@ -156,7 +156,7 @@ function resolveIssueToastContext(
readString(details?.identifier) ??
readString(details?.issueIdentifier) ??
cachedIssue?.identifier ??
`Issue ${shortId(issueId)}`;
`Task ${shortId(issueId)}`;
const title =
readString(details?.title) ??
readString(details?.issueTitle) ??
+2 -2
View File
@@ -181,7 +181,7 @@ function formatIssueReferenceLabel(reference: ActivityIssueReference): string {
if (reference.identifier) return reference.identifier;
if (reference.title) return reference.title;
if (reference.id) return reference.id.slice(0, 8);
return "issue";
return "task";
}
function formatChangedEntityLabel(
@@ -276,7 +276,7 @@ function formatIssueUpdatedAction(details: ActivityDetails, options: ActivityFor
}
if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) {
const assigneeName = formatAssigneeName(details, options);
parts.push(assigneeName ? `assigned the issue to ${assigneeName}` : "unassigned the issue");
parts.push(assigneeName ? `assigned the task to ${assigneeName}` : "unassigned the task");
}
if (details.title !== undefined) parts.push("updated the title");
if (details.description !== undefined) parts.push("updated the description");
+1 -1
View File
@@ -1274,7 +1274,7 @@ describe("inbox helpers", () => {
expect(groupInboxWorkItems(items, "none")).toEqual([{ key: "__all", label: null, items }]);
expect(groupInboxWorkItems(items, "type")).toEqual([
{ key: "issue", label: "Issues", items: [items[1], items[2]] },
{ key: "issue", label: "Tasks", items: [items[1], items[2]] },
{ key: "approval", label: "Approvals", items: [items[0]] },
{ key: "failed_run", label: "Failed runs", items: [items[3]] },
{ key: "join_request", label: "Join requests", items: [items[4]] },
+1 -1
View File
@@ -821,7 +821,7 @@ const inboxWorkItemKindOrder: InboxWorkItem["kind"][] = [
];
const inboxWorkItemKindLabels: Record<InboxWorkItem["kind"], string> = {
issue: "Issues",
issue: "Tasks",
approval: "Approvals",
failed_run: "Failed runs",
join_request: "Join requests",
+2 -2
View File
@@ -125,13 +125,13 @@ function inferIssueDetailSource(
if (isIssueDetailSource(state?.issueDetailSource)) return state.issueDetailSource;
if (!breadcrumb) return null;
if (breadcrumb.label === "Inbox" || breadcrumb.href.includes("/inbox")) return "inbox";
if (breadcrumb.label === "Issues" || breadcrumb.href.includes("/issues")) return "issues";
if (breadcrumb.label === "Tasks" || breadcrumb.href.includes("/issues")) return "issues";
return null;
}
function breadcrumbForSource(source: IssueDetailSource): IssueDetailBreadcrumb {
if (source === "inbox") return { label: "Inbox", href: "/inbox" };
return { label: "Issues", href: "/issues" };
return { label: "Tasks", href: "/issues" };
}
export function createIssueDetailLocationState(
+1 -1
View File
@@ -20,7 +20,7 @@ export type RunRetryStateSummary = {
const RETRY_REASON_LABELS: Record<string, string> = {
transient_failure: "Transient failure",
missing_issue_comment: "Missing issue comment",
missing_issue_comment: "Missing task comment",
process_lost: "Process lost",
assignment_recovery: "Assignment recovery",
issue_continuation_needed: "Continuation needed",
+2 -2
View File
@@ -37,11 +37,11 @@ function mapMetadataRow(
case "issue_link": {
const identifier = row.identifier ?? null;
if (!identifier) {
return { kind: "text", label: metadataRowText(row, "Issue"), value: row.title ?? "unknown" };
return { kind: "text", label: metadataRowText(row, "Task"), value: row.title ?? "unknown" };
}
return {
kind: "issue",
label: metadataRowText(row, "Issue"),
label: metadataRowText(row, "Task"),
identifier,
href: `/issues/${identifier}`,
title: row.title ?? undefined,
+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,
+1 -1
View File
@@ -303,7 +303,7 @@ function PluginSdkIssuesList({
});
if (!companyId) {
return createElement("div", { className: "text-sm text-muted-foreground" }, "Select a company to view issues.");
return createElement("div", { className: "text-sm text-muted-foreground" }, "Select a company to view tasks.");
}
return createElement(HostIssuesList, {