PAP-57: IA Phase 2 — rename user-facing "Issues" → "Tasks" copy

Copy-only sweep across ui/src: nav (sidebar + mobile), Tasks list page
(headings, breadcrumbs, buttons, empty states, filters, column headers,
toasts, aria-labels), task detail, dialogs, command palette, inbox,
search, dashboards, project/agent/workspace pages, recovery/blocked
notices, and design-guide demos.

Routes stay at /issues; internal symbols (Issue, issueId, queryKeys,
kind: "issue"), API fields, and backend-matched protocol strings
(successful-run-handoff notice bodies) are unchanged. Tests updated to
match the new copy. Developer-only *UxLab/*Perf harness pages left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
committed 2026-06-05 15:13:29 -07:00
1 parent b0ca1bebc0
commit 59f2c8fc32
76 files changed
+309 -309

No files matched your search

+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">
@@ -124,11 +124,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
@@ -30,7 +30,7 @@ function BlockerRecoveryIndicator({ 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={`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 />
@@ -130,7 +130,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);
@@ -205,9 +205,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>{" "}
@@ -258,10 +258,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
@@ -501,7 +501,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>
@@ -3534,7 +3534,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>
@@ -3569,12 +3569,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}
@@ -4315,7 +4315,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) {
@@ -4405,10 +4405,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
@@ -394,11 +394,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 () => {
@@ -466,7 +466,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"));
@@ -477,7 +477,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"));
@@ -518,7 +518,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 () => {
@@ -585,7 +585,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>
@@ -134,7 +134,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.");
@@ -173,7 +173,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);
@@ -188,7 +188,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,19 +45,19 @@ export interface IssueRecoveryActionCardProps {
const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
missing_disposition: "Missing Disposition",
stranded_assigned_issue: "Stranded Issue",
stranded_assigned_issue: "Stranded Task",
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.",
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, {
@@ -296,11 +296,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.",
},
{
@@ -311,14 +311,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
@@ -244,7 +244,7 @@ function renderRecoveryChip(action: IssueRecoveryAction, selected: boolean): Rea
tone.className,
selected ? "!border-muted-foreground !text-muted-foreground" : null,
)}
title={`${tone.label} — open the source issue to act.`}
title={`${tone.label} — open the source task to act.`}
>
<Icon className="h-2.5 w-2.5" aria-hidden />
{tone.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;
@@ -454,7 +454,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}
@@ -484,8 +484,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}
@@ -510,8 +510,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) => (
+1 -1
View File
@@ -152,7 +152,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");
await act(async () => {
+1 -1
View File
@@ -97,7 +97,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} />
{showWorkspacesLink ? (
@@ -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>