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

This reverts commit 59f2c8fc32.
This commit is contained in:
Dotta committed 2026-06-06 13:32:14 +00:00
1 parent 34d3ea97ca
commit 898cdca91c
76 files changed
+314 -323

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 task to act.`}
title={`${tone.label} — open the source issue 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 tasks</p>;
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</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 tasks</p>;
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</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 task settings can still override this."
hint="Agent-level default execution target. Project and issue 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">
Tasks that need a decision, recovery, or external action will appear here.
Issues 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 tasks, agents, projects..."
placeholder="Search issues, agents, projects..."
value={query}
onValueChange={setQuery}
onKeyDown={(event) => {
@@ -135,7 +135,7 @@ export function CommandPalette() {
<CommandEmpty>
{showSearchAll ? (
<span>
No quick task matches. Press{" "}
No quick issue 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 task
Create new issue
<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" />
Tasks
Issues
</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="Tasks">
<CommandGroup heading="Issues">
{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 task history, but removes it from active workspace views.
artifacts. Paperclip keeps the workspace record and issue 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 tasks</h3>
<h3 className="text-sm font-medium">Blocking issues</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 tasks</h3>
<h3 className="text-sm font-medium">Other linked issues</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 task still needs a next step.");
expect(node.textContent).toContain("This issue 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 task is blocked until");
expect(node.textContent).not.toContain("Work on this issue 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 task to act.`}
title={`${tone.label} — open the source issue 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 task" : "the linked tasks";
const blockerLabel = blockers.length === 1 ? "the linked issue" : "the linked issues";
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 task still needs a next step.</p>
<p className="font-medium leading-5">This issue still needs a next step.</p>
<p className="leading-5">
A run finished successfully, but this task is still open in{" "}
A run finished successfully, but this issue 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 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.</>}
? <>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.</>}
</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 task is blocked by the linked task");
expect(container.textContent).toContain("Work on this issue is blocked by the linked issue");
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 task");
expect(container.textContent).toContain("Other files are added to this issue");
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 task");
expect(container.textContent).toContain("Attached to issue");
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 task");
expect(container.textContent).toContain("Attached to issue");
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 tasks page.
Showing a safe fallback transcript instead of crashing the issues 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 task.
Images insert into the reply. Other files are added to this issue.
</div>
</div>
</div>
@@ -3569,12 +3569,12 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
const sizeLabel = formatAttachmentSize(attachment.size);
const statusLabel =
attachment.status === "uploading"
? "Uploading to task"
? "Uploading to issue"
: attachment.status === "error"
? attachment.error ?? "Upload failed"
: attachment.inline
? "Inserted inline"
: "Attached to task";
: "Attached to issue";
return (
<div
key={attachment.id}
@@ -4315,7 +4315,7 @@ export function IssueChatThread({
const resolvedEmptyMessage = emptyMessage
?? (variant === "embedded"
? "No run output yet."
: "This task conversation is empty. Start with a message below.");
: "This issue 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 task"
label="Legacy recovery issue"
body={
<span>
Legacy recovery task. Newer recovery actions live on the source task
Legacy recovery issue. Newer recovery actions live on the source issue
{legacyRecoverySourceIssue.identifier ? (
<>
{" — "}
+7 -7
View File
@@ -28,19 +28,19 @@ const issueColumnLabels: Record<InboxIssueColumn, string> = {
assignee: "Assignee",
project: "Project",
workspace: "Workspace",
parent: "Parent task",
parent: "Parent issue",
labels: "Tags",
updated: "Last updated",
};
const issueColumnDescriptions: Record<InboxIssueColumn, string> = {
status: "Task state chip on the left edge.",
status: "Issue 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 task.",
parent: "Parent task identifier and title.",
labels: "Task labels and tags.",
workspace: "Execution or project workspace used for the issue.",
parent: "Parent issue identifier and title.",
labels: "Issue 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 task rows
Desktop issue 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-task</span>
<span className="italic">Sub-issue</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 task preview.</p>
<p className="text-xs text-muted-foreground">Unable to load issue 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 ? "task" : "tasks"} created
{created} of {requested} child {requested === 1 ? "issue" : "issues"} 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-tasks");
expect(container.textContent).toContain("Add sub-task");
expect(container.textContent).toContain("Sub-issues");
expect(container.textContent).toContain("Add sub-issue");
const addButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Add sub-task"));
.find((button) => button.textContent?.includes("Add sub-issue"));
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 tasks..."]')).toBeNull();
expect(container.querySelector('input[placeholder="Search issues..."]')).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 tasks..."]')).not.toBeNull();
expect(container.querySelector('input[placeholder="Search issues..."]')).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 tasks to add as blockers"]') as HTMLInputElement | null;
const searchInput = container.querySelector('input[aria-label="Search issues 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 task.");
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this issue.");
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={`Task ${issueLabel}: ${issue.title}`}
aria-label={`Issue ${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={`Task ${issueLabel}: ${issue.title}`}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
>
{content}
</Link>
@@ -296,7 +296,7 @@ function RemovableIssueReferencePill({
<DialogHeader>
<DialogTitle>Remove blocker?</DialogTitle>
<DialogDescription>
Remove {confirmLabel} as a blocker for this task.
Remove {confirmLabel} as a blocker for this issue.
</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 task overrides."
? "This assignee's adapter does not expose editable issue 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 tasks..."
placeholder="Search issues..."
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 tasks..."
placeholder="Search issues..."
value={blockedBySearch}
onChange={(e) => setBlockedBySearch(e.target.value)}
autoFocus={!inline}
aria-label="Search tasks to add as blockers"
aria-label="Search issues 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 tasks...</div>
<div className="px-2 py-2 text-xs text-muted-foreground">Searching issues...</div>
) : blockerOptions.length === 0 ? (
<div className="px-2 py-2 text-xs text-muted-foreground">No matching tasks.</div>
<div className="px-2 py-2 text-xs text-muted-foreground">No matching issues.</div>
) : null}
</div>
</>
@@ -1913,7 +1913,7 @@ export function IssueProperties({
) : null}
</PropertyRow>
<PropertyRow label="Sub-tasks">
<PropertyRow label="Sub-issues">
<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-task
Add sub-issue
</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 task's run finished, but no next step was chosen.");
expect(node.textContent).toContain("This issue'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 task done");
expect(document.body.textContent).toContain("Mark issue 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 task done");
expect(document.body.textContent).toContain("Mark issue 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 Task",
stranded_assigned_issue: "Stranded Issue",
active_run_watchdog: "Active Watchdog",
issue_graph_liveness: "Graph Liveness",
};
const KIND_HEADLINE: Record<IssueRecoveryActionKind, string> = {
missing_disposition: "This task's run finished, but no next step was chosen.",
missing_disposition: "This issue's run finished, but no next step was chosen.",
stranded_assigned_issue:
"Paperclip retried this task's last run and it still has no live execution path.",
"Paperclip retried this issue'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 task lost a live action path. A recovery owner needs to act.",
"Paperclip detected this issue 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 task to todo.",
description: "Dismiss recovery and return the source issue to todo.",
},
{
outcome: "done",
label: "Mark task done",
label: "Mark issue 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 task complete.",
description: "Dismiss recovery and mark the source issue complete.",
destructive: true,
boardOnly: true,
},
{
outcome: "false_positive_in_review",
label: "False positive, review",
description: "Dismiss recovery and send the source task for review.",
description: "Dismiss recovery and send the source issue 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={`Task: ${issue.title}`}
aria-label={`Issue: ${issue.title}`}
>
{content}
</span>
@@ -50,7 +50,7 @@ export function IssueReferencePill({
data-mention-kind="issue"
className={classNames}
title={issue.title}
aria-label={`Task ${issueLabel}: ${issue.title}`}
aria-label={`Issue ${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="Task PAP-22: Downstream task"');
expect(html).toContain('aria-label="Task PAP-33: Upstream task"');
expect(html).toContain('aria-label="Issue PAP-22: Downstream task"');
expect(html).toContain('aria-label="Issue 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 task currently points at in its title, description, comments, or documents."
description="Other tasks this issue currently points at in its title, description, comments, or documents."
items={outbound}
emptyLabel="This task does not reference any other tasks yet."
emptyLabel="This issue does not reference any other tasks yet."
/>
<Section
title="Referenced by"
description="Other tasks that currently point at this task."
description="Other tasks that currently point at this issue."
items={inbound}
emptyLabel="No other tasks reference this task yet."
emptyLabel="No other tasks reference this issue 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 task to act.`}
title={`${tone.label} — open the source issue 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: "Task reached a terminal state.",
description: "Issue 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 task declared a blocker.",
description: "Run or issue declared a blocker.",
},
failed: {
label: "Failed",
@@ -535,7 +535,7 @@ export function IssueRunLedgerContent({
}, [activityEvents, canRenderActivityEvents, ledgerRuns]);
return (
<section className="space-y-3" aria-label="Task run ledger">
<section className="space-y-3" aria-label="Issue 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 task has history."
: "Historical runs without liveness metadata will appear here once linked to this task."}
? "Runs and activity will appear here once this issue has history."
: "Historical runs without liveness metadata will appear here once linked to this issue."}
</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-task navigation");
expect(nav?.getAttribute("aria-label")).toBe("Sub-issue 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-task: PAP-1 - Previous sibling title");
expect(links[0].getAttribute("aria-label")).toBe("Previous sub-issue: 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-task: PAP-3 - Next sibling title");
expect(links[1].getAttribute("aria-label")).toBe("Next sub-issue: 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-task navigation"
aria-label="Sub-issue 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-task" : "Next sub-task";
const ariaDirection = direction === "previous" ? "Previous sub-issue" : "Next sub-issue";
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 task" : `${totalTasks} draft tasks`}</span>
<span>{totalTasks === 1 ? "1 draft issue" : `${totalTasks} draft issues`}</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 ? "task" : "tasks"} and skipped ${skippedCount} during review.`
: `Created all ${createdCount} draft ${createdCount === 1 ? "task" : "tasks"}.`}
? `Created ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"} and skipped ${skippedCount} during review.`
: `Created all ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"}.`}
</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 ? "task" : "tasks"} selected`
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "task" : "tasks"} selected`}
? `All ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} selected`
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} 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 task runs."
? "A fresh isolated workspace will be created when this issue runs."
: currentSelection === "reuse_existing"
? "This task will reuse an existing workspace when it runs."
: "This task will use the project default workspace configuration when it runs."}
? "This issue will reuse an existing workspace when it runs."
: "This issue 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 task in Feature Branch"]');
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue in Feature Branch"]');
expect(button).not.toBeNull();
});
await act(async () => {
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New task in Feature Branch"]');
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue 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 tasks"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search issues"]') 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 tasks. Refine filters or search to reveal the rest.");
expect(container.textContent).toContain("Some board columns are showing up to 200 issues. 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 tasks");
expect(container.textContent).toContain("Rendering 100 of 220 issues");
});
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 tasks");
expect(container.textContent).toContain("Rendering 250 of 420 issues");
});
act(() => {
@@ -1669,13 +1669,13 @@ describe("IssuesList", () => {
);
await waitForAssertion(() => {
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
input?.focus();
expect(document.activeElement).toBe(input);
});
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
const input = container.querySelector('input[aria-label="Search issues"]') 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 tasks"]') as HTMLInputElement | null;
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
expect(input).not.toBeNull();
input?.focus();
expect(document.activeElement).toBe(input);
});
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
const input = container.querySelector('input[aria-label="Search issues"]') 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 tasks..."
placeholder="Search issues..."
className="pl-7 text-xs sm:text-sm"
aria-label="Search tasks"
aria-label="Search issues"
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-task${
} across ${costSummary.issueCount} sub-issue${
costSummary.issueCount === 1 ? "" : "s"
}`}
>
@@ -545,7 +545,7 @@ function SubIssueProgressSummaryStrip({
</div>
<div
role="progressbar"
aria-label="Sub-tasks completion progress"
aria-label="Sub-issues 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-tasks</div>
<div className="text-sm font-medium text-foreground">No active sub-issues</div>
) : summary.doneCount === summary.totalCount ? (
<div className="text-sm font-medium text-foreground">All sub-tasks done</div>
<div className="text-sm font-medium text-foreground">All sub-issues done</div>
) : (
<div className="text-sm font-medium text-foreground">No actionable sub-tasks</div>
<div className="text-sm font-medium text-foreground">No actionable sub-issues</div>
)}
</div>
</div>
@@ -1289,8 +1289,8 @@ export function IssuesList({
viewState.groupBy,
]);
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Task";
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Task";
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Issue";
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Issue";
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 task columns stay visible"
title="Choose which issue columns stay visible"
iconOnly
/>
@@ -1539,7 +1539,7 @@ export function IssuesList({
["assignee", "Assignee"],
["project", "Project"],
["workspace", "Workspace"],
["parent", "Parent Task"],
["parent", "Parent Issue"],
["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} tasks. Refine filters or search to reveal the rest.
Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} issues. Refine filters or search to reveal the rest.
</p>
)}
{!isLoading && filtered.length === 0 && viewState.viewMode === "list" && (
<EmptyState
icon={CircleDot}
message="No tasks match the current filters or search."
message="No issues 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 task in ${group.label}`}
aria-label={`New task in ${group.label}`}
title={`New issue in ${group.label}`}
aria-label={`New issue 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 task needs a next step"
title="This issue 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 tasks..."
? "Loading more issues..."
: remainingIssueRowCount > 0
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} tasks`
: "Scroll to load more tasks"}
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} issues`
: "Scroll to load more issues"}
</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 task needs a next step"
title="This issue needs a next step"
aria-label="Needs next step"
>
<AlertTriangle className="h-3 w-3" />
@@ -28,7 +28,7 @@ const sections: ShortcutSection[] = [
],
},
{
title: "Task detail",
title: "Issue 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 task" },
{ keys: ["c"], label: "New issue" },
{ 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 task activity.
Uses the shared chat-style run surface from issue activity.
</div>
</div>
+2 -11
View File
@@ -1,5 +1,4 @@
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { NavLink, useLocation } from "@/lib/router";
import {
House,
@@ -10,8 +9,6 @@ import {
} from "lucide-react";
import { useCompany } from "../context/CompanyContext";
import { useDialogActions } from "../context/DialogContext";
import { instanceSettingsApi } from "../api/instanceSettings";
import { queryKeys } from "../lib/queryKeys";
import { SIDEBAR_SCROLL_RESET_STATE } from "../lib/navigation-scroll";
import { cn } from "../lib/utils";
import { useInboxBadge } from "../hooks/useInboxBadge";
@@ -42,17 +39,11 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
const { selectedCompanyId } = useCompany();
const { openNewIssue } = useDialogActions();
const inboxBadge = useInboxBadge(selectedCompanyId);
const { data: experimentalSettings } = useQuery({
queryKey: queryKeys.instance.experimentalSettings,
queryFn: () => instanceSettingsApi.getExperimental(),
});
// IA flag (PAP-89): mirror the sidebar's classic/streamlined label set.
const streamlined = experimentalSettings?.enableStreamlinedLeftNavigation === true;
const items = useMemo<MobileNavItem[]>(
() => [
{ type: "link", to: "/dashboard", label: "Home", icon: House },
{ type: "link", to: "/issues", label: streamlined ? "Tasks" : "Issues", icon: CircleDot },
{ type: "link", to: "/issues", label: "Issues", icon: CircleDot },
{ type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() },
{ type: "link", to: "/agents/all", label: "Agents", icon: Users },
{
@@ -63,7 +54,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
badge: inboxBadge.inbox,
},
],
[openNewIssue, inboxBadge.inbox, streamlined],
[openNewIssue, inboxBadge.inbox],
);
return (
+21 -21
View File
@@ -352,11 +352,11 @@ describe("NewIssueDialog", () => {
const { root } = renderDialog(container);
await flush();
expect(container.textContent).toContain("New sub-task");
expect(container.textContent).toContain("Sub-task of");
expect(container.textContent).toContain("New sub-issue");
expect(container.textContent).toContain("Sub-issue of");
expect(container.textContent).toContain("PAP-1");
expect(container.textContent).toContain("Parent issue");
expect(container.textContent).toContain("Create Sub-Task");
expect(container.textContent).toContain("Create Sub-Issue");
act(() => root.unmount());
@@ -364,9 +364,9 @@ describe("NewIssueDialog", () => {
const rerendered = renderDialog(container);
await flush();
expect(container.textContent).toContain("New task");
expect(container.textContent).toContain("Create Task");
expect(container.textContent).not.toContain("Sub-task of");
expect(container.textContent).toContain("New issue");
expect(container.textContent).toContain("Create Issue");
expect(container.textContent).not.toContain("Sub-issue 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-Task"));
.find((button) => button.textContent?.includes("Create Sub-Issue"));
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 Task"));
.find((button) => button.textContent?.includes("Create Issue"));
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 task");
expect(container.textContent).not.toContain("New sub-task");
expect(container.textContent).toContain("New issue");
expect(container.textContent).not.toContain("New sub-issue");
await waitForAssertion(() => {
expect(container.textContent).toContain("Reusing PAP-100");
});
const submitButton = Array.from(container.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Create Task"));
.find((button) => button.textContent?.includes("Create Issue"));
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="Task title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Issue 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 Task"));
.find((button) => button.textContent?.includes("Create Issue"));
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="Task title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Issue 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 Task"));
.find((button) => button.textContent?.includes("Create Issue"));
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="Task title"]') as HTMLTextAreaElement | null;
const titleInput = container.querySelector('textarea[placeholder="Issue 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 Task"));
.find((button) => button.textContent?.includes("Create Issue"));
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-Task"));
.find((button) => button.textContent?.includes("Create Sub-Issue"));
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="Task title"]');
const titleInput = container.querySelector('textarea[placeholder="Issue 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 task workspace");
expect(container.textContent).not.toContain("will no longer use the parent issue 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 task workspace");
expect(container.textContent).toContain("will no longer use the parent issue 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="Task title"
placeholder="Issue 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 task. Try again.";
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. 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-task" : "New task"}</span>
<span>{isSubIssueMode ? "New sub-issue" : "New issue"}</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-task of</span>
<span className="shrink-0">Sub-issue 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 task runs in the shared workspace, a new isolated workspace, or an existing one.
Control whether this issue 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-task will no longer use the parent task workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
Warning: this sub-issue will no longer use the parent issue 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 task only.</p>
<p className="text-[11px] text-muted-foreground">Override the model and effort for this issue 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-Task" : "Create Task"}</span>
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Issue" : "Create Issue"}</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 task.
starter task, wake the agent, and open the issue.
</p>
</div>
</div>
@@ -1249,7 +1249,7 @@ export function OnboardingWizard() {
) : (
<ArrowRight className="h-3.5 w-3.5 mr-1" />
)}
{loading ? "Creating..." : "Create & Open Task"}
{loading ? "Creating..." : "Create & Open Issue"}
</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 tasks in this project. Project values override agent env on key conflicts.
Applied to all runs for issues 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 task checkouts and execution workspace behavior.
Project-owned defaults for isolated issue 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 task checkouts</span>
<span>Enable isolated issue checkouts</span>
<SaveIndicator state={fieldState("execution_workspace_enabled")} />
</div>
<div className="text-xs text-muted-foreground">
Let tasks choose between the project's primary checkout and an isolated execution workspace.
Let issues 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 tasks default to isolated checkout</span>
<span>New issues default to isolated checkout</span>
<SaveIndicator state={fieldState("execution_workspace_default_mode")} />
</div>
<div className="text-[11px] text-muted-foreground">
If disabled, new tasks stay on the project's primary checkout unless someone opts in.
If disabled, new issues 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 tasks");
expect(container.textContent).toContain("Linked issues");
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 tasks
Linked issues
</div>
<div className="flex flex-wrap gap-2">
{visibleIssues.map((issue) => (
+1 -1
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("Tasks");
expect(workSectionContainer?.textContent).toContain("Issues");
expect(workSectionContainer?.textContent).toContain("Goals");
flushSync(() => {
+5 -5
View File
@@ -49,8 +49,8 @@ export function Sidebar() {
const liveRunCount = liveRuns?.length ?? 0;
const showWorkspacesLink = experimentalSettings?.enableIsolatedWorkspaces === true;
// IA flag (PAP-89): branch the sidebar nav presentation. Default OFF = classic
// ("New Issue", "Issues", per-project collapsible, no Projects nav link).
// ON = streamlined (current: "New Task", "Tasks", top-level Projects link).
// (per-project collapsible, no Projects nav link). ON = streamlined
// (top-level Projects link). Issue/Task wording is split to PR #7651.
// Gating is navigation-only; all routes stay registered in both modes.
const streamlined = experimentalSettings?.enableStreamlinedLeftNavigation === true;
@@ -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 Task / New Issue button aligned with nav items */}
{/* New Issue 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">{streamlined ? "New Task" : "New Issue"}</span>
<span className="truncate">New Issue</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={streamlined ? "Tasks" : "Issues"} icon={CircleDot} />
<SidebarNavItem to="/issues" label="Issues" 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 task">
<MetaRow label="Source issue">
<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 task">
<MetaRow label="Evaluation issue">
<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-task PAP-2"');
expect(html).toContain('title="Blocked · waiting on active sub-task PAP-2"');
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("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-task ${blockerAttention.sampleBlockerIdentifier}`;
return `Blocked · waiting on active sub-issue ${blockerAttention.sampleBlockerIdentifier}`;
}
if (count === 1) return "Blocked · waiting on 1 active sub-task";
return `Blocked · waiting on ${count} active sub-tasks`;
if (count === 1) return "Blocked · waiting on 1 active sub-issue";
return `Blocked · waiting on ${count} active sub-issues`;
}
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="Task outputs">
<section className="space-y-3" aria-label="Issue 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 ??
`Task ${shortId(issueId)}`;
`Issue ${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 "task";
return "issue";
}
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 task to ${assigneeName}` : "unassigned the task");
parts.push(assigneeName ? `assigned the issue to ${assigneeName}` : "unassigned the issue");
}
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: "Tasks", items: [items[1], items[2]] },
{ key: "issue", label: "Issues", 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: "Tasks",
issue: "Issues",
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 === "Tasks" || breadcrumb.href.includes("/issues")) return "issues";
if (breadcrumb.label === "Issues" || breadcrumb.href.includes("/issues")) return "issues";
return null;
}
function breadcrumbForSource(source: IssueDetailSource): IssueDetailBreadcrumb {
if (source === "inbox") return { label: "Inbox", href: "/inbox" };
return { label: "Tasks", href: "/issues" };
return { label: "Issues", 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 task comment",
missing_issue_comment: "Missing issue 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, "Task"), value: row.title ?? "unknown" };
return { kind: "text", label: metadataRowText(row, "Issue"), value: row.title ?? "unknown" };
}
return {
kind: "issue",
label: metadataRowText(row, "Task"),
label: metadataRowText(row, "Issue"),
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 tasks"
: "Review linked task",
? "Review linked issues"
: "Review linked issue",
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 tasks.
Requesting agent was notified to review this approval and linked issues.
</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 Tasks</p>
<p className="text-xs text-muted-foreground mb-1.5">Linked Issues</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 tasks remain open until the requesting agent follows up and closes them.
Linked issues 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 ? "task" : "tasks"}
{issueCount} {issueCount === 1 ? "issue" : "issues"}
</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 task${result.reassignedIssueCount === 1 ? "" : "s"} cleaned up.`
? `${result.reassignedIssueCount} assigned issue${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 tasks..."
: `${assignedIssues.length} open assigned task${assignedIssues.length === 1 ? "" : "s"}`}
? "Checking assigned issues..."
: `${assignedIssues.length} open assigned issue${assignedIssues.length === 1 ? "" : "s"}`}
</div>
</div>
{assignedIssues.length > 0 ? (
<div className="space-y-2">
<div className="text-sm font-medium">Task reassignment</div>
<div className="text-sm font-medium">Issue 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 task{assignedIssues.length - 6 === 1 ? "" : "s"}
{assignedIssues.length - 6} more issue{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, task workspaces, and remote-capable adapters.
Define reusable execution targets for projects, issue 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 tasks ({visibleTaskChildren} of {totalTaskChildren})
Show more issues ({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 tasks.</CardDescription>
<CardDescription>Run costs attributed through project-linked issues.</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="Tasks by Priority" subtitle="Last 14 days">
<ChartCard title="Issues by Priority" subtitle="Last 14 days">
<PriorityChart issues={issues ?? []} />
</ChartCard>
<ChartCard title="Tasks by Status" subtitle="Last 14 days">
<ChartCard title="Issues 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 Task</Button>
<Button><Plus /> New Issue</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 task
Watch issue
</DropdownMenuCheckboxItem>
<DropdownMenuItem variant="destructive">
<Trash2 className="h-4 w-4" />
Delete task
Delete issue
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -784,7 +784,7 @@ export function DesignGuide() {
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Task Properties</SheetTitle>
<SheetTitle>Issue 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" />
Tasks
Issues
</CommandItem>
</CommandGroup>
<CommandSeparator />
@@ -847,7 +847,7 @@ export function DesignGuide() {
</CommandItem>
<CommandItem>
<Plus className="h-4 w-4" />
Create new task
Create new issue
</CommandItem>
</CommandGroup>
</CommandList>
@@ -870,7 +870,7 @@ export function DesignGuide() {
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Task List</BreadcrumbPage>
<BreadcrumbPage>Issue 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 Tasks" />
<MetricCard icon={CircleDot} value={48} label="Open Issues" />
<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" />
Tasks
Issues
<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 (Tasks pattern)">
<Section title="Grouped List (Issues 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 Task (outside inputs)"],
["C", "New Issue (outside inputs)"],
["[", "Toggle Sidebar"],
["]", "Toggle Properties Panel"],
@@ -1604,7 +1604,7 @@ export function DesignGuide() {
</div>
</Section>
<Section title="Task Output Surface">
<Section title="Issue 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 a task has produced no artifact work products, the Output section renders nothing
When an issue 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 tasks");
expect(container.querySelector('a[href="/execution-workspaces/workspace-1/issues"]')?.textContent).toBe("Back to issues");
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([
"Tasks",
"Issues",
"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: "Tasks", order: 10 },
{ value: "issues", label: "Issues", 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 task">
<DetailRow label="Source issue">
{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 tasks"
defaultTabLabel="Back to issues"
/>
) : 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 task");
setActionError(err instanceof Error ? err.message : "Failed to archive issue");
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 tasks</SelectItem>
<SelectItem value="issues_i_touched">My recent issues</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 task in ${group.label}`}
aria-label={`New task in ${group.label}`}
title={`New issue in ${group.label}`}
aria-label={`New issue 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 task runs.
and existing issue 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">Task Plan Decomposition Panel</h2>
<h2 className="text-sm font-semibold">Issue Plan Decomposition Panel</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Show accepted-plan decomposition history on task detail pages. Intended for debugging and validating
Show accepted-plan decomposition history on issue 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 task plan decomposition panel experimental setting"
aria-label="Toggle issue 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 Recovery Tasks</h2>
<h2 className="text-sm font-semibold">Auto-Create Issue Recovery Tasks</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Let the heartbeat scheduler create recovery tasks for task dependency chains found inside the
Let the heartbeat scheduler create recovery issues for issue dependency chains found inside the
configured lookback window.
</p>
</div>
@@ -403,7 +403,7 @@ export function InstanceExperimentalSettings() {
previewForEnable();
}}
disabled={recoveryActionPending}
aria-label="Toggle task graph liveness auto-recovery"
aria-label="Toggle issue 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 tasks or
Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating issues 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 tasks created");
expect(container.textContent).toContain("2 of 2 child issues 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-task: PAP-1 - Previous sibling"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Next sub-task: PAP-3 - Next sibling"]')).toBeTruthy();
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.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-task: PAP-11 - First child"]')).toBeTruthy();
expect(container.querySelector('a[aria-label="Next sub-issue: 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 task actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More issue 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 task actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More issue 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 task actions"]') as HTMLButtonElement | null;
const moreButton = container.querySelector('button[aria-label="More issue 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 tasks cancelled by this subtree operation so work can resume.");
expect(container.textContent).toContain("Restore issues 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 tasks");
.find((button) => button.textContent?.trim() === "Restore 1 issues");
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 tasks") as HTMLButtonElement | undefined;
.find((button) => button.textContent?.trim() === "Cancel 24 issues") as HTMLButtonElement | undefined;
expect(cancelApplyButton).toBeTruthy();
expect(cancelApplyButton!.disabled).toBe(true);
+38 -38
View File
@@ -188,14 +188,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 task subtree until an explicit resume.",
pause: "Pause active execution in this issue subtree until an explicit resume.",
resume: "Release the active subtree pause hold so held work can continue.",
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.",
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.",
};
const LEAF_WORK_CONTROL_MODE_HELP_TEXT: Partial<Record<IssueTreeControlMode, string>> = {
pause: "Pause active execution on this task until an explicit resume.",
resume: "Release the active pause hold so this task can continue.",
pause: "Pause active execution on this issue until an explicit resume.",
resume: "Release the active pause hold so this issue can continue.",
};
function issueTreeControlLabel(mode: IssueTreeControlMode, scope: "leaf" | "subtree") {
return scope === "leaf"
@@ -213,7 +213,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 tasks.";
if (error.status === 422) return "This subtree action is currently invalid for the selected issues.";
}
return error instanceof Error ? error.message : "Unable to load preview.";
}
@@ -602,7 +602,7 @@ function InboxMobileToolbar({
onClick={() => { onHide(); setMenuOpen(false); }}
>
<EyeOff className="h-3 w-3" />
Hide this task
Hide this issue
</button>
)}
</PopoverContent>
@@ -1117,7 +1117,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 task</span>
<span className="font-medium text-foreground">This issue</span>
{issueCostSummary.hasCost ? (
<span className="font-medium text-foreground">
${issueCostSummary.cost.toFixed(4)}
@@ -1144,7 +1144,7 @@ function IssueDetailActivityTab({
{hasIssueTreeCost && issueTreeCostSummary ? (
<div className="flex flex-wrap gap-3">
<span className="font-medium text-foreground">
Including sub-tasks {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
Including sub-issues {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
style: "currency",
currency: "USD",
minimumFractionDigits: 4,
@@ -1163,7 +1163,7 @@ function IssueDetailActivityTab({
{` (${issueTreeCostSummary.runCount} run${issueTreeCostSummary.runCount === 1 ? "" : "s"})`}
</span>
) : null}
<span>{issueTreeCostSummary.issueCount} task{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
<span>{issueTreeCostSummary.issueCount} issue{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
</div>
) : null}
</div>
@@ -1377,7 +1377,7 @@ export function IssueDetail() {
}
}, [hasLiveRuns, locallyQueuedCommentRunIds.size]);
const sourceBreadcrumb = useMemo(
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Tasks", href: "/issues" },
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Issues", href: "/issues" },
[issueId, location.state, location.search],
);
@@ -1628,7 +1628,7 @@ export function IssueDetail() {
() => mergeIssueComments(comments ?? [], optimisticComments),
[comments, optimisticComments],
);
const breadcrumbTitle = issue?.title ?? issueId ?? "Task";
const breadcrumbTitle = issue?.title ?? issueId ?? "Issue";
const issueCacheRefs = useMemo(() => {
const refs = new Set<string>();
if (issueId) refs.add(issueId);
@@ -1803,8 +1803,8 @@ export function IssueDetail() {
queryClient.setQueryData(queryKeys.issues.list(context.selectedCompanyId), context.previousList);
}
pushToast({
title: "Task update failed",
body: err instanceof Error ? err.message : "Unable to save task changes",
title: "Issue update failed",
body: err instanceof Error ? err.message : "Unable to save issue changes",
tone: "error",
});
},
@@ -1881,7 +1881,7 @@ export function IssueDetail() {
? treeControlScope === "leaf" ? "Work paused" : "Subtree paused"
: `${modeLabel} applied`,
body: result.kind === "release"
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active task pause released." : "Active subtree pause released."))
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active issue pause released." : "Active subtree pause released."))
: result.hold.mode === "pause"
? treeControlScope === "leaf"
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
@@ -1940,7 +1940,7 @@ export function IssueDetail() {
title: "Work paused",
body: cancelCount > 0
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
: "Work paused. This task is held until resume.",
: "Work paused. This issue is held until resume.",
tone: "success",
});
await Promise.all([
@@ -1977,8 +1977,8 @@ export function IssueDetail() {
},
onError: (err) => {
pushToast({
title: "Task update failed",
body: err instanceof Error ? err.message : "Unable to save sub-task changes",
title: "Issue update failed",
body: err instanceof Error ? err.message : "Unable to save sub-issue changes",
tone: "error",
});
},
@@ -2657,12 +2657,12 @@ export function IssueDetail() {
onSuccess: () => {
invalidateIssueCollections();
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
pushToast({ title: "Task archived from inbox", tone: "success" });
pushToast({ title: "Issue archived from inbox", tone: "success" });
},
onError: (err) => {
pushToast({
title: "Archive failed",
body: err instanceof Error ? err.message : "Unable to archive this task from the inbox",
body: err instanceof Error ? err.message : "Unable to archive this issue from the inbox",
tone: "error",
});
},
@@ -3251,9 +3251,9 @@ export function IssueDetail() {
? "Pause work"
: "Pause and stop work"
: treeControlMode === "cancel"
? `Cancel ${previewAffectedIssueCount} tasks`
? `Cancel ${previewAffectedIssueCount} issues`
: treeControlMode === "restore"
? `Restore ${previewAffectedIssueCount} tasks`
? `Restore ${previewAffectedIssueCount} issues`
: treeControlScope === "leaf"
? "Resume work"
: "Resume subtree";
@@ -3352,7 +3352,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 task is hidden
This issue is hidden
</div>
)}
{activePauseHold && (
@@ -3365,13 +3365,13 @@ export function IssueDetail() {
</span>
<span className="text-xs text-amber-900/80 dark:text-amber-100/80">
{childIssues.length === 0
? "Task execution is held until resume. Human comments can still wake the assignee for triage."
? "Issue 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 task held"
? "1 issue held"
: `${heldDescendantCount} descendant${heldDescendantCount === 1 ? "" : "s"} held`}
{activeRootPauseHold?.createdAt ? ` · started ${relativeTime(activeRootPauseHold.createdAt)}` : ""}
</div>
@@ -3417,7 +3417,7 @@ export function IssueDetail() {
</div>
) : (
<div className="text-xs">
This task is paused by ancestor{" "}
This issue is paused by ancestor{" "}
{activePauseHoldRoot?.identifier ? (
<Link to={createIssueDetailPath(activePauseHoldRoot.identifier)} className="underline">
{activePauseHoldRoot.identifier}
@@ -3425,7 +3425,7 @@ export function IssueDetail() {
) : (
activePauseHold.rootIssueId.slice(0, 8)
)}
. Resume from the root task to deliver deferred work.
. Resume from the root issue to deliver deferred work.
</div>
)}
</div>
@@ -3481,7 +3481,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 task is in planning mode."
title="This issue is in planning mode."
>
Planning
</span>
@@ -3540,7 +3540,7 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
onClick={copyIssueToClipboard}
title="Copy task as markdown"
title="Copy issue as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
@@ -3574,7 +3574,7 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
onClick={copyIssueToClipboard}
title="Copy task as markdown"
title="Copy issue as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
@@ -3597,8 +3597,8 @@ export function IssueDetail() {
variant="ghost"
size="icon-xs"
className="shrink-0"
aria-label="More task actions"
title="More task actions"
aria-label="More issue actions"
title="More issue actions"
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
@@ -3706,7 +3706,7 @@ export function IssueDetail() {
}}
>
<EyeOff className="h-3 w-3" />
Hide this Task
Hide this Issue
</button>
</PopoverContent>
</Popover>
@@ -3783,7 +3783,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-tasks</h3>
<h3 className="text-sm font-medium text-muted-foreground">Sub-issues</h3>
</div>
<IssuesList
issues={childIssues}
@@ -3799,7 +3799,7 @@ export function IssueDetail() {
searchFilters={{ descendantOf: issue.id, includeBlockedBy: true }}
searchWithinLoadedIssues
baseCreateIssueDefaults={buildSubIssueDefaultsForViewer(issue, currentUserId)}
createIssueLabel="Sub-task"
createIssueLabel="Sub-issue"
defaultSortField="workflow"
showProgressSummary
parentIssueIdForCostSummary={issue.id}
@@ -3810,7 +3810,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-task
New Sub-issue
</Button>
</div>
)}
@@ -4052,7 +4052,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 tasks will be marked cancelled, and running or queued work will be interrupted where possible.
Cancelling a subtree is destructive. Non-terminal issues will be marked cancelled, and running or queued work will be interrupted where possible.
</div>
) : null}
@@ -4110,7 +4110,7 @@ export function IssueDetail() {
checked={treeControlCancelConfirmed}
onChange={(event) => setTreeControlCancelConfirmed(event.target.checked)}
/>
<span>I understand this will cancel {previewAffectedIssueCount} tasks.</span>
<span>I understand this will cancel {previewAffectedIssueCount} issues.</span>
</label>
) : null}
+3 -3
View File
@@ -108,7 +108,7 @@ export function Issues() {
const issueLinkState = useMemo(
() =>
createIssueDetailLocationState(
"Tasks",
"Issues",
`${location.pathname}${location.search}${location.hash}`,
"issues",
),
@@ -116,7 +116,7 @@ export function Issues() {
);
useEffect(() => {
setBreadcrumbs([{ label: "Tasks" }]);
setBreadcrumbs([{ label: "Issues" }]);
}, [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 tasks." />;
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
}
return (
+3 -3
View File
@@ -17,7 +17,7 @@ export function MyIssues() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "My Tasks" }]);
setBreadcrumbs([{ label: "My Issues" }]);
}, [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 tasks." />;
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
}
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 tasks assigned to you." />
<EmptyState icon={ListTodo} message="No issues 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: "Tasks" },
{ value: "list", label: "Issues" },
{ 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. Task execution is not paused."
? "Workspace service stopped. Issue execution is not paused."
: request.action === "restart"
? "Workspace service restarted. Task execution is not paused."
? "Workspace service restarted. Issue 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 task this routine creates. They override matching keys in
Routine secrets apply to every issue 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 tasks.
Recurring work definitions that materialize into auditable execution issues.
</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: "Tasks",
issues: "Issues",
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: "Tasks",
issues: "Issues",
comments: "Comments",
documents: "Documents",
artifacts: "Artifacts",
@@ -346,7 +346,7 @@ export function Search() {
}
}
}}
placeholder="Search tasks, comments, documents, agents, projects…"
placeholder="Search issues, comments, documents, 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">
Tasks, comments, plan documents, agents, projects — same surface, ranked by relevance.
Issues, comments, plan documents, 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 a task.
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">PAP-123</code> to jump straight to an issue.
</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">Couldn’t 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 Tasks filter.
you can retry or fall back to the Issues 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 Tasks filter view
Open Issues 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 task from this query
Create issue from this query
</Button>
<Button onClick={navigateIssuesFallback} size="sm" variant="ghost">
Open Tasks filter view
Open Issues 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 task in the project and overrides agent env on matching keys.
applies to every issue 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: "Task-linked usage",
sublabel: "Issue-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 tasks.");
return createElement("div", { className: "text-sm text-muted-foreground" }, "Select a company to view issues.");
}
return createElement(HostIssuesList, {