PAP-10430: split Issue-to-Task copy migration (#7651)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board UI is the operator surface where users create, assign, monitor, and review work items. > - The product language is moving toward "tasks" for user-facing work items while the internal API and database still use "issues". > - PR #7543 bundled this copy migration with broader information-architecture work, which made the branch too large for Greptile review. > - This pull request peels the Issue-to-Task copy migration into a smaller, independently reviewable change. > - The benefit is clearer user-facing terminology, less agent confusion via the Paperclip skill note, and a smaller PR that Greptile can review. ## Linked Issues or Issue Description Refs #7645 Refs #7543 Refs PAP-10430 This PR was split out of #7543 so the Issue-to-Task copy migration can be reviewed separately and the remaining IA PR can fall under Greptile's file limit. ## What Changed - Preserves Scott Tong's original `PAP-57` copy-only commit, with author and co-author credit intact, to rename user-facing "Issues" copy to "Tasks" across the UI while keeping routes/API/internal symbols as `issue`. - Updates onboarding and release-smoke browser selectors from `Create & Open Issue` to `Create & Open Task`. - Adds a terminology note to `skills/paperclip/SKILL.md` clarifying that task and issue refer to the same Paperclip work item. - Resolves the only cherry-pick conflict by keeping current search artifacts support and changing visible search copy to "tasks". ## Verification - `pnpm --filter @paperclipai/ui build` passed. - `NODE_ENV=test pnpm exec vitest run ui/src/components/IssuesList.test.tsx ui/src/components/Sidebar.test.tsx ui/src/components/NewIssueDialog.test.tsx ui/src/pages/IssueDetail.test.tsx` passed: 4 files, 66 tests. - `git diff --check origin/master...HEAD` passed. - Diff is 80 files, below Greptile's 100-file limit. - Before/after UI copy examples: "Issues" -> "Tasks", "New Issue" -> "New Task", "Create & Open Issue" -> "Create & Open Task". ## Risks - Medium copy-risk: this intentionally changes user-facing terminology broadly while keeping internal issue identifiers and routes unchanged. - Some docs and APIs still say `issue`; the skill note clarifies this so agents do not treat task and issue as separate entities. - Browser-level visual validation is expected from CI because this local container is missing usable browser dependencies. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used Scott Tong authored the original `PAP-57` copy migration, assisted by Claude Opus 4.8 and Paperclip agents per the preserved commit metadata. Codex / GPT-5-class coding agent with shell, GitHub CLI, and repository access performed the PR split, conflict resolution, skill note, and verification. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: scotttong <scott.tong@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -73,6 +73,31 @@ if (!embeddedPostgresSupport.supported) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorHasPostgresCode(error: unknown, code: string): boolean {
|
||||||
|
let current: unknown = error;
|
||||||
|
for (let depth = 0; depth < 4; depth += 1) {
|
||||||
|
if (!current || typeof current !== "object") return false;
|
||||||
|
const record = current as { code?: unknown; cause?: unknown };
|
||||||
|
if (record.code === code) return true;
|
||||||
|
current = record.cause;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function truncateCompaniesWithDeadlockRetry(db: ReturnType<typeof createDb>) {
|
||||||
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
|
try {
|
||||||
|
await db.execute(sql.raw(`TRUNCATE TABLE "companies" CASCADE`));
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (!errorHasPostgresCode(error, "40P01") || attempt === 4) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describeEmbeddedPostgres("active-run output watchdog", () => {
|
describeEmbeddedPostgres("active-run output watchdog", () => {
|
||||||
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
||||||
let db: ReturnType<typeof createDb>;
|
let db: ReturnType<typeof createDb>;
|
||||||
@@ -91,7 +116,7 @@ describeEmbeddedPostgres("active-run output watchdog", () => {
|
|||||||
if (activeRuns.length === 0) break;
|
if (activeRuns.length === 0) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||||
}
|
}
|
||||||
await db.execute(sql.raw(`TRUNCATE TABLE "companies" CASCADE`));
|
await truncateCompaniesWithDeadlockRetry(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ description: >
|
|||||||
|
|
||||||
You run in **heartbeats** — short execution windows triggered by Paperclip. Each heartbeat, you wake up, check your work, do something useful, and exit. You do not run continuously.
|
You run in **heartbeats** — short execution windows triggered by Paperclip. Each heartbeat, you wake up, check your work, do something useful, and exit. You do not run continuously.
|
||||||
|
|
||||||
|
## Terminology
|
||||||
|
|
||||||
|
In Paperclip, **task** and **issue** refer to the same work item. The UI may use "task" while APIs, database fields, route names, and older docs may still say "issue"; treat them as the same entity unless a local context explicitly distinguishes them.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL.
|
Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL.
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ test.describe("Onboarding wizard", () => {
|
|||||||
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
|
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
|
||||||
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
|
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Create & Open Issue" }).click();
|
await page.getByRole("button", { name: "Create & Open Task" }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
|
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ test("captures planning mode UI for desktop and mobile", async ({ page }) => {
|
|||||||
await page.getByRole("button", { name: "Next" }).click();
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
await expect(page.locator("h3", { hasText: "Ready to launch" })).toBeVisible({ timeout: 30_000 });
|
await expect(page.locator("h3", { hasText: "Ready to launch" })).toBeVisible({ timeout: 30_000 });
|
||||||
await page.getByRole("button", { name: "Create & Open Issue" }).click();
|
await page.getByRole("button", { name: "Create & Open Task" }).click();
|
||||||
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
|
await expect(page).toHaveURL(/\/issues\//, { timeout: 30_000 });
|
||||||
|
|
||||||
const openedIssueUrl = page.url();
|
const openedIssueUrl = page.url();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ test.describe("Docker authenticated onboarding smoke", () => {
|
|||||||
await expect(page.getByText(AGENT_NAME)).toBeVisible();
|
await expect(page.getByText(AGENT_NAME)).toBeVisible();
|
||||||
await expect(page.getByText(TASK_TITLE)).toBeVisible();
|
await expect(page.getByText(TASK_TITLE)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Create & Open Issue" }).click();
|
await page.getByRole("button", { name: "Create & Open Task" }).click();
|
||||||
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
|
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
|
||||||
|
|
||||||
const baseUrl = new URL(page.url()).origin;
|
const baseUrl = new URL(page.url()).origin;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function RunCardRecoveryChip({ action }: { action: IssueRecoveryAction }) {
|
|||||||
data-recovery-state={state}
|
data-recovery-state={state}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={tone.label}
|
aria-label={tone.label}
|
||||||
title={`${tone.label} — open the source issue to act.`}
|
title={`${tone.label} — open the source task to act.`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium",
|
"inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium",
|
||||||
tone.className,
|
tone.className,
|
||||||
|
|||||||
@@ -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 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);
|
const hasData = Array.from(grouped.values()).some(v => Object.values(v).reduce((a, b) => a + b, 0) > 0);
|
||||||
|
|
||||||
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
|
if (!hasData) return <p className="text-xs text-muted-foreground">No tasks</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 maxValue = Math.max(...Array.from(grouped.values()).map(v => Object.values(v).reduce((a, b) => a + b, 0)), 1);
|
||||||
const hasData = allStatuses.size > 0;
|
const hasData = allStatuses.size > 0;
|
||||||
|
|
||||||
if (!hasData) return <p className="text-xs text-muted-foreground">No issues</p>;
|
if (!hasData) return <p className="text-xs text-muted-foreground">No tasks</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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")}>
|
<div className={cn(cards ? "border border-border rounded-lg p-4 space-y-3" : "px-4 pb-3 space-y-3")}>
|
||||||
<Field
|
<Field
|
||||||
label="Default environment"
|
label="Default environment"
|
||||||
hint="Agent-level default execution target. Project and issue settings can still override this."
|
hint="Agent-level default execution target. Project and task settings can still override this."
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export function BlockedInboxView({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium text-foreground">No work is stopped.</p>
|
<p className="text-sm font-medium text-foreground">No work is stopped.</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Issues that need a decision, recovery, or external action will appear here.
|
Tasks that need a decision, recovery, or external action will appear here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function CommandPalette() {
|
|||||||
if (v && isMobile) setSidebarOpen(false);
|
if (v && isMobile) setSidebarOpen(false);
|
||||||
}}>
|
}}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search issues, agents, projects..."
|
placeholder="Search tasks, agents, projects..."
|
||||||
value={query}
|
value={query}
|
||||||
onValueChange={setQuery}
|
onValueChange={setQuery}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
@@ -135,7 +135,7 @@ export function CommandPalette() {
|
|||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{showSearchAll ? (
|
{showSearchAll ? (
|
||||||
<span>
|
<span>
|
||||||
No quick issue matches. Press{" "}
|
No quick task matches. Press{" "}
|
||||||
<kbd className="rounded border border-border bg-muted px-1 py-0.5 text-[10px]">↵</kbd>{" "}
|
<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.
|
to <span className="font-medium">search all</span> or keep typing to refine.
|
||||||
</span>
|
</span>
|
||||||
@@ -174,7 +174,7 @@ export function CommandPalette() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SquarePen className="mr-2 h-4 w-4" />
|
<SquarePen className="mr-2 h-4 w-4" />
|
||||||
Create new issue
|
Create new task
|
||||||
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -205,7 +205,7 @@ export function CommandPalette() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={() => go("/issues")}>
|
<CommandItem onSelect={() => go("/issues")}>
|
||||||
<CircleDot className="mr-2 h-4 w-4" />
|
<CircleDot className="mr-2 h-4 w-4" />
|
||||||
Issues
|
Tasks
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem onSelect={() => go("/projects")}>
|
<CommandItem onSelect={() => go("/projects")}>
|
||||||
<Hexagon className="mr-2 h-4 w-4" />
|
<Hexagon className="mr-2 h-4 w-4" />
|
||||||
@@ -232,7 +232,7 @@ export function CommandPalette() {
|
|||||||
{visibleIssues.length > 0 && (
|
{visibleIssues.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading="Issues">
|
<CommandGroup heading="Tasks">
|
||||||
{visibleIssues.slice(0, 10).map((issue) => (
|
{visibleIssues.slice(0, 10).map((issue) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function ExecutionWorkspaceCloseDialog({
|
|||||||
<DialogTitle>{actionLabel}</DialogTitle>
|
<DialogTitle>{actionLabel}</DialogTitle>
|
||||||
<DialogDescription className="break-words">
|
<DialogDescription className="break-words">
|
||||||
Archive <span className="font-medium text-foreground">{workspaceName}</span> and clean up any owned workspace
|
Archive <span className="font-medium text-foreground">{workspaceName}</span> and clean up any owned workspace
|
||||||
artifacts. Paperclip keeps the workspace record and issue history, but removes it from active workspace views.
|
artifacts. Paperclip keeps the workspace record and task history, but removes it from active workspace views.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ export function ExecutionWorkspaceCloseDialog({
|
|||||||
|
|
||||||
{blockingIssues.length > 0 ? (
|
{blockingIssues.length > 0 ? (
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h3 className="text-sm font-medium">Blocking issues</h3>
|
<h3 className="text-sm font-medium">Blocking tasks</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{blockingIssues.map((issue) => (
|
{blockingIssues.map((issue) => (
|
||||||
<div key={issue.id} className="rounded-xl border border-destructive/20 bg-destructive/5 px-4 py-3 text-sm">
|
<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 ? (
|
{otherLinkedIssues.length > 0 ? (
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h3 className="text-sm font-medium">Other linked issues</h3>
|
<h3 className="text-sm font-medium">Other linked tasks</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{otherLinkedIssues.map((issue) => (
|
{otherLinkedIssues.map((issue) => (
|
||||||
<div key={issue.id} className="rounded-xl border border-border bg-background px-4 py-3 text-sm">
|
<div key={issue.id} className="rounded-xl border border-border bg-background px-4 py-3 text-sm">
|
||||||
|
|||||||
@@ -136,11 +136,11 @@ describe("IssueBlockedNotice", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(node.textContent).toContain("This issue still needs a next step.");
|
expect(node.textContent).toContain("This task still needs a next step.");
|
||||||
expect(node.textContent).toContain("Corrective wake queued for CodexCoder");
|
expect(node.textContent).toContain("Corrective wake queued for CodexCoder");
|
||||||
expect(node.textContent).toContain("Detected progress: Updated the plan");
|
expect(node.textContent).toContain("Detected progress: Updated the plan");
|
||||||
expect(node.textContent).not.toContain("Retry now");
|
expect(node.textContent).not.toContain("Retry now");
|
||||||
expect(node.textContent).not.toContain("Work on this issue is blocked until");
|
expect(node.textContent).not.toContain("Work on this task is blocked until");
|
||||||
expect(node.querySelector('[data-successful-run-handoff="required"]')).not.toBeNull();
|
expect(node.querySelector('[data-successful-run-handoff="required"]')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function BlockerRecoveryIndicator({ action }: { action: IssueRecoveryAction }) {
|
|||||||
data-recovery-kind={action.kind}
|
data-recovery-kind={action.kind}
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
title={`${label} — open the source issue to act.`}
|
title={`${label} — open the source task to act.`}
|
||||||
className={`inline-flex shrink-0 items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-[10px] font-medium ${tone.className}`}
|
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 />
|
<Icon className="h-2.5 w-2.5" aria-hidden />
|
||||||
@@ -133,7 +133,7 @@ export function IssueBlockedNotice({
|
|||||||
? { issueId, scheduledRetry }
|
? { issueId, scheduledRetry }
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const blockerLabel = blockers.length === 1 ? "the linked issue" : "the linked issues";
|
const blockerLabel = blockers.length === 1 ? "the linked task" : "the linked tasks";
|
||||||
const terminalBlockers = blockers
|
const terminalBlockers = blockers
|
||||||
.flatMap((blocker) => blocker.terminalBlockers ?? [])
|
.flatMap((blocker) => blocker.terminalBlockers ?? [])
|
||||||
.filter((blocker, index, all) => all.findIndex((candidate) => candidate.id === blocker.id) === index);
|
.filter((blocker, index, all) => all.findIndex((candidate) => candidate.id === blocker.id) === index);
|
||||||
@@ -208,9 +208,9 @@ export function IssueBlockedNotice({
|
|||||||
<div className="min-w-0 space-y-1.5">
|
<div className="min-w-0 space-y-1.5">
|
||||||
{showSuccessfulRunHandoff ? (
|
{showSuccessfulRunHandoff ? (
|
||||||
<>
|
<>
|
||||||
<p className="font-medium leading-5">This issue still needs a next step.</p>
|
<p className="font-medium leading-5">This task still needs a next step.</p>
|
||||||
<p className="leading-5">
|
<p className="leading-5">
|
||||||
A run finished successfully, but this issue is still open in{" "}
|
A run finished successfully, but this task is still open in{" "}
|
||||||
<code className="rounded bg-amber-100 px-1 py-0.5 text-[12px] dark:bg-amber-400/15">
|
<code className="rounded bg-amber-100 px-1 py-0.5 text-[12px] dark:bg-amber-400/15">
|
||||||
in_progress
|
in_progress
|
||||||
</code>{" "}
|
</code>{" "}
|
||||||
@@ -261,10 +261,10 @@ export function IssueBlockedNotice({
|
|||||||
{blockers.length > 0
|
{blockers.length > 0
|
||||||
? isStalled
|
? isStalled
|
||||||
? stalledLeafBlockers.length > 1
|
? stalledLeafBlockers.length > 1
|
||||||
? <>Work on this issue is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers.</>
|
? <>Work on this 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 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 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 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 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 issue is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
|
: <>Work on this task is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
|
||||||
</p>
|
</p>
|
||||||
{blockers.length > 0 ? (
|
{blockers.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
|||||||
@@ -1506,7 +1506,7 @@ describe("IssueChatThread", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(container.textContent).toContain("Work on this issue is blocked by the linked issue");
|
expect(container.textContent).toContain("Work on this task is blocked by the linked task");
|
||||||
expect(container.textContent).toContain("Comments still wake the assignee for questions or triage");
|
expect(container.textContent).toContain("Comments still wake the assignee for questions or triage");
|
||||||
expect(container.textContent).toContain("PAP-1723");
|
expect(container.textContent).toContain("PAP-1723");
|
||||||
expect(container.textContent).toContain("QA the install flow");
|
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.querySelector('[data-testid="issue-chat-composer-drop-overlay"]')).not.toBeNull();
|
||||||
expect(container.textContent).toContain("Drop to upload");
|
expect(container.textContent).toContain("Drop to upload");
|
||||||
expect(container.textContent).toContain("Images insert into the reply");
|
expect(container.textContent).toContain("Images insert into the reply");
|
||||||
expect(container.textContent).toContain("Other files are added to this issue");
|
expect(container.textContent).toContain("Other files are added to this task");
|
||||||
expect(composer?.className).toContain("border-primary/45");
|
expect(composer?.className).toContain("border-primary/45");
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -2260,7 +2260,7 @@ describe("IssueChatThread", () => {
|
|||||||
const attachmentList = container.querySelector('[data-testid="issue-chat-composer-attachments"]');
|
const attachmentList = container.querySelector('[data-testid="issue-chat-composer-attachments"]');
|
||||||
expect(attachmentList).not.toBeNull();
|
expect(attachmentList).not.toBeNull();
|
||||||
expect(container.textContent).toContain("report.pdf");
|
expect(container.textContent).toContain("report.pdf");
|
||||||
expect(container.textContent).toContain("Attached to issue");
|
expect(container.textContent).toContain("Attached to task");
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
@@ -2360,7 +2360,7 @@ describe("IssueChatThread", () => {
|
|||||||
expect(attachmentList).not.toBeNull();
|
expect(attachmentList).not.toBeNull();
|
||||||
expect(attachmentList?.className).toContain("mb-3");
|
expect(attachmentList?.className).toContain("mb-3");
|
||||||
expect(container.textContent).toContain("report.pdf");
|
expect(container.textContent).toContain("report.pdf");
|
||||||
expect(container.textContent).toContain("Attached to issue");
|
expect(container.textContent).toContain("Attached to task");
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
root.unmount();
|
root.unmount();
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ function IssueChatFallbackThread({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="font-medium">Chat renderer hit an internal state error.</p>
|
<p className="font-medium">Chat renderer hit an internal state error.</p>
|
||||||
<p className="text-xs opacity-80">
|
<p className="text-xs opacity-80">
|
||||||
Showing a safe fallback transcript instead of crashing the issues page.
|
Showing a safe fallback transcript instead of crashing the tasks page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3549,7 +3549,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium text-foreground">Drop to upload</div>
|
<div className="text-sm font-medium text-foreground">Drop to upload</div>
|
||||||
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
|
||||||
Images insert into the reply. Other files are added to this issue.
|
Images insert into the reply. Other files are added to this task.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3584,12 +3584,12 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
|
|||||||
const sizeLabel = formatAttachmentSize(attachment.size);
|
const sizeLabel = formatAttachmentSize(attachment.size);
|
||||||
const statusLabel =
|
const statusLabel =
|
||||||
attachment.status === "uploading"
|
attachment.status === "uploading"
|
||||||
? "Uploading to issue"
|
? "Uploading to task"
|
||||||
: attachment.status === "error"
|
: attachment.status === "error"
|
||||||
? attachment.error ?? "Upload failed"
|
? attachment.error ?? "Upload failed"
|
||||||
: attachment.inline
|
: attachment.inline
|
||||||
? "Inserted inline"
|
? "Inserted inline"
|
||||||
: "Attached to issue";
|
: "Attached to task";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={attachment.id}
|
key={attachment.id}
|
||||||
@@ -4330,7 +4330,7 @@ export function IssueChatThread({
|
|||||||
const resolvedEmptyMessage = emptyMessage
|
const resolvedEmptyMessage = emptyMessage
|
||||||
?? (variant === "embedded"
|
?? (variant === "embedded"
|
||||||
? "No run output yet."
|
? "No run output yet."
|
||||||
: "This issue conversation is empty. Start with a message below.");
|
: "This task conversation is empty. Start with a message below.");
|
||||||
const previousErrorBoundaryMessagesRef = useRef<readonly ThreadMessage[] | null>(null);
|
const previousErrorBoundaryMessagesRef = useRef<readonly ThreadMessage[] | null>(null);
|
||||||
const errorBoundaryResetVersionRef = useRef(0);
|
const errorBoundaryResetVersionRef = useRef(0);
|
||||||
if (previousErrorBoundaryMessagesRef.current !== messages) {
|
if (previousErrorBoundaryMessagesRef.current !== messages) {
|
||||||
@@ -4420,10 +4420,10 @@ export function IssueChatThread({
|
|||||||
{legacyRecoverySourceIssue ? (
|
{legacyRecoverySourceIssue ? (
|
||||||
<SystemNotice
|
<SystemNotice
|
||||||
tone="info"
|
tone="info"
|
||||||
label="Legacy recovery issue"
|
label="Legacy recovery task"
|
||||||
body={
|
body={
|
||||||
<span>
|
<span>
|
||||||
Legacy recovery issue. Newer recovery actions live on the source issue
|
Legacy recovery task. Newer recovery actions live on the source task
|
||||||
{legacyRecoverySourceIssue.identifier ? (
|
{legacyRecoverySourceIssue.identifier ? (
|
||||||
<>
|
<>
|
||||||
{" — "}
|
{" — "}
|
||||||
|
|||||||
@@ -28,19 +28,19 @@ const issueColumnLabels: Record<InboxIssueColumn, string> = {
|
|||||||
assignee: "Assignee",
|
assignee: "Assignee",
|
||||||
project: "Project",
|
project: "Project",
|
||||||
workspace: "Workspace",
|
workspace: "Workspace",
|
||||||
parent: "Parent issue",
|
parent: "Parent task",
|
||||||
labels: "Tags",
|
labels: "Tags",
|
||||||
updated: "Last updated",
|
updated: "Last updated",
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueColumnDescriptions: Record<InboxIssueColumn, string> = {
|
const issueColumnDescriptions: Record<InboxIssueColumn, string> = {
|
||||||
status: "Issue state chip on the left edge.",
|
status: "Task state chip on the left edge.",
|
||||||
id: "Ticket identifier like PAP-1009.",
|
id: "Ticket identifier like PAP-1009.",
|
||||||
assignee: "Assigned agent or board user.",
|
assignee: "Assigned agent or board user.",
|
||||||
project: "Linked project pill with its color.",
|
project: "Linked project pill with its color.",
|
||||||
workspace: "Execution or project workspace used for the issue.",
|
workspace: "Execution or project workspace used for the task.",
|
||||||
parent: "Parent issue identifier and title.",
|
parent: "Parent task identifier and title.",
|
||||||
labels: "Issue labels and tags.",
|
labels: "Task labels and tags.",
|
||||||
updated: "Latest visible activity time.",
|
updated: "Latest visible activity time.",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export function IssueColumnPicker({
|
|||||||
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">
|
<DropdownMenuLabel className="px-2 pb-1 pt-1.5">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
<div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
||||||
Desktop issue rows
|
Desktop task rows
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-medium text-foreground">
|
<div className="text-sm font-medium text-foreground">
|
||||||
{title}
|
{title}
|
||||||
@@ -369,7 +369,7 @@ export function InboxIssueTrailingColumns({
|
|||||||
{parentIdentifier ? (
|
{parentIdentifier ? (
|
||||||
<span className="font-mono">{parentIdentifier}</span>
|
<span className="font-mono">{parentIdentifier}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="italic">Sub-issue</span>
|
<span className="italic">Sub-task</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export const IssueLinkQuicklook = React.forwardRef<
|
|||||||
<div className="h-4 w-full rounded bg-accent/40" />
|
<div className="h-4 w-full rounded bg-accent/40" />
|
||||||
<div className="h-4 w-3/4 rounded bg-accent/30" />
|
<div className="h-4 w-3/4 rounded bg-accent/30" />
|
||||||
{!isLoading ? (
|
{!isLoading ? (
|
||||||
<p className="text-xs text-muted-foreground">Unable to load issue preview.</p>
|
<p className="text-xs text-muted-foreground">Unable to load task preview.</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function IssuePlanDecompositionsSection({
|
|||||||
<span className="text-xs text-muted-foreground/70">·</span>
|
<span className="text-xs text-muted-foreground/70">·</span>
|
||||||
<span className="inline-flex items-center gap-1 text-xs text-foreground">
|
<span className="inline-flex items-center gap-1 text-xs text-foreground">
|
||||||
<GitBranch className="h-3 w-3 text-muted-foreground" />
|
<GitBranch className="h-3 w-3 text-muted-foreground" />
|
||||||
{created} of {requested} child {requested === 1 ? "issue" : "issues"} created
|
{created} of {requested} child {requested === 1 ? "task" : "tasks"} created
|
||||||
</span>
|
</span>
|
||||||
{record.status === "completed" && requested > 0 ? (
|
{record.status === "completed" && requested > 0 ? (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -395,11 +395,11 @@ describe("IssueProperties", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).toContain("Sub-issues");
|
expect(container.textContent).toContain("Sub-tasks");
|
||||||
expect(container.textContent).toContain("Add sub-issue");
|
expect(container.textContent).toContain("Add sub-task");
|
||||||
|
|
||||||
const addButton = Array.from(container.querySelectorAll("button"))
|
const addButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Add sub-issue"));
|
.find((button) => button.textContent?.includes("Add sub-task"));
|
||||||
expect(addButton).not.toBeUndefined();
|
expect(addButton).not.toBeUndefined();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -467,7 +467,7 @@ describe("IssueProperties", () => {
|
|||||||
expect(blockerLink?.textContent).toContain("PAP-2");
|
expect(blockerLink?.textContent).toContain("PAP-2");
|
||||||
expect(blockerLink?.closest("button")).toBeNull();
|
expect(blockerLink?.closest("button")).toBeNull();
|
||||||
expect(container.textContent).toContain("Add blocker");
|
expect(container.textContent).toContain("Add blocker");
|
||||||
expect(container.querySelector('input[placeholder="Search issues..."]')).toBeNull();
|
expect(container.querySelector('input[placeholder="Search tasks..."]')).toBeNull();
|
||||||
|
|
||||||
const addButton = Array.from(container.querySelectorAll("button"))
|
const addButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Add blocker"));
|
.find((button) => button.textContent?.includes("Add blocker"));
|
||||||
@@ -478,7 +478,7 @@ describe("IssueProperties", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.querySelector('input[placeholder="Search issues..."]')).not.toBeNull();
|
expect(container.querySelector('input[placeholder="Search tasks..."]')).not.toBeNull();
|
||||||
|
|
||||||
const candidateButton = Array.from(container.querySelectorAll("button"))
|
const candidateButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("PAP-3 New blocker"));
|
.find((button) => button.textContent?.includes("PAP-3 New blocker"));
|
||||||
@@ -519,7 +519,7 @@ describe("IssueProperties", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const searchInput = container.querySelector('input[aria-label="Search issues to add as blockers"]') as HTMLInputElement | null;
|
const searchInput = container.querySelector('input[aria-label="Search tasks to add as blockers"]') as HTMLInputElement | null;
|
||||||
expect(searchInput).not.toBeNull();
|
expect(searchInput).not.toBeNull();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -586,7 +586,7 @@ describe("IssueProperties", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this issue.");
|
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this task.");
|
||||||
const confirmButton = Array.from(document.body.querySelectorAll("button"))
|
const confirmButton = Array.from(document.body.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Remove blocker"));
|
.find((button) => button.textContent?.includes("Remove blocker"));
|
||||||
expect(confirmButton).not.toBeUndefined();
|
expect(confirmButton).not.toBeUndefined();
|
||||||
|
|||||||
@@ -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",
|
"inline-flex items-center gap-1 rounded-full border border-border py-0.5 pl-1 pr-2 text-xs",
|
||||||
)}
|
)}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
aria-label={`Issue ${issueLabel}: ${issue.title}`}
|
aria-label={`Task ${issueLabel}: ${issue.title}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -283,7 +283,7 @@ function RemovableIssueReferencePill({
|
|||||||
<Link
|
<Link
|
||||||
to={`/issues/${issueLabel}`}
|
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"
|
className="inline-flex min-w-0 items-center gap-1 no-underline hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
|
||||||
aria-label={`Issue ${issueLabel}: ${issue.title}`}
|
aria-label={`Task ${issueLabel}: ${issue.title}`}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -296,7 +296,7 @@ function RemovableIssueReferencePill({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Remove blocker?</DialogTitle>
|
<DialogTitle>Remove blocker?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Remove {confirmLabel} as a blocker for this issue.
|
Remove {confirmLabel} as a blocker for this task.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -766,7 +766,7 @@ export function IssueProperties({
|
|||||||
<div className="w-full space-y-2 p-2">
|
<div className="w-full space-y-2 p-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{assignee
|
{assignee
|
||||||
? "This assignee's adapter does not expose editable issue overrides."
|
? "This assignee's adapter does not expose editable task overrides."
|
||||||
: "Select a compatible agent assignee to edit these overrides."}
|
: "Select a compatible agent assignee to edit these overrides."}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -1621,7 +1621,7 @@ export function IssueProperties({
|
|||||||
<>
|
<>
|
||||||
<input
|
<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"
|
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
|
||||||
placeholder="Search issues..."
|
placeholder="Search tasks..."
|
||||||
value={parentSearch}
|
value={parentSearch}
|
||||||
onChange={(e) => setParentSearch(e.target.value)}
|
onChange={(e) => setParentSearch(e.target.value)}
|
||||||
autoFocus={!inline}
|
autoFocus={!inline}
|
||||||
@@ -1693,11 +1693,11 @@ export function IssueProperties({
|
|||||||
<>
|
<>
|
||||||
<input
|
<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"
|
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
|
||||||
placeholder="Search issues..."
|
placeholder="Search tasks..."
|
||||||
value={blockedBySearch}
|
value={blockedBySearch}
|
||||||
onChange={(e) => setBlockedBySearch(e.target.value)}
|
onChange={(e) => setBlockedBySearch(e.target.value)}
|
||||||
autoFocus={!inline}
|
autoFocus={!inline}
|
||||||
aria-label="Search issues to add as blockers"
|
aria-label="Search tasks to add as blockers"
|
||||||
/>
|
/>
|
||||||
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
<div className="max-h-48 overflow-y-auto overscroll-contain">
|
||||||
<button
|
<button
|
||||||
@@ -1734,9 +1734,9 @@ export function IssueProperties({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{blockerOptionsLoading ? (
|
{blockerOptionsLoading ? (
|
||||||
<div className="px-2 py-2 text-xs text-muted-foreground">Searching issues...</div>
|
<div className="px-2 py-2 text-xs text-muted-foreground">Searching tasks...</div>
|
||||||
) : blockerOptions.length === 0 ? (
|
) : blockerOptions.length === 0 ? (
|
||||||
<div className="px-2 py-2 text-xs text-muted-foreground">No matching issues.</div>
|
<div className="px-2 py-2 text-xs text-muted-foreground">No matching tasks.</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1913,7 +1913,7 @@ export function IssueProperties({
|
|||||||
) : null}
|
) : null}
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
|
|
||||||
<PropertyRow label="Sub-issues">
|
<PropertyRow label="Sub-tasks">
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{childIssues.length > 0
|
{childIssues.length > 0
|
||||||
? childIssues.map((child) => (
|
? childIssues.map((child) => (
|
||||||
@@ -1927,7 +1927,7 @@ export function IssueProperties({
|
|||||||
onClick={onAddSubIssue}
|
onClick={onAddSubIssue}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
Add sub-issue
|
Add sub-task
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ describe("IssueRecoveryActionCard", () => {
|
|||||||
expect(node.textContent).toContain("RECOVERY NEEDED");
|
expect(node.textContent).toContain("RECOVERY NEEDED");
|
||||||
expect(node.textContent).toContain("Missing Disposition");
|
expect(node.textContent).toContain("Missing Disposition");
|
||||||
expect(node.textContent).not.toContain("missing_disposition");
|
expect(node.textContent).not.toContain("missing_disposition");
|
||||||
expect(node.textContent).toContain("This issue's run finished, but no next step was chosen.");
|
expect(node.textContent).toContain("This task's run finished, but no next step was chosen.");
|
||||||
expect(node.textContent).toContain("ClaudeCoder");
|
expect(node.textContent).toContain("ClaudeCoder");
|
||||||
expect(node.textContent).toContain("CodexCoder");
|
expect(node.textContent).toContain("CodexCoder");
|
||||||
expect(node.textContent).toContain("Choose and record a valid issue disposition.");
|
expect(node.textContent).toContain("Choose and record a valid issue disposition.");
|
||||||
@@ -190,7 +190,7 @@ describe("IssueRecoveryActionCard", () => {
|
|||||||
expect(node.textContent).toContain("Workspace Validation");
|
expect(node.textContent).toContain("Workspace Validation");
|
||||||
expect(node.textContent).not.toContain("workspace_validation\n");
|
expect(node.textContent).not.toContain("workspace_validation\n");
|
||||||
expect(node.textContent).toContain(
|
expect(node.textContent).toContain(
|
||||||
"Paperclip stopped this run because the issue's git workspace could not be validated.",
|
"Paperclip stopped this run because the task's git workspace could not be validated.",
|
||||||
);
|
);
|
||||||
expect(node.textContent).toContain("Repair the source issue workspace link");
|
expect(node.textContent).toContain("Repair the source issue workspace link");
|
||||||
expect(node.textContent).toContain("Manual repair required");
|
expect(node.textContent).toContain("Manual repair required");
|
||||||
@@ -212,7 +212,7 @@ describe("IssueRecoveryActionCard", () => {
|
|||||||
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
|
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
|
||||||
|
|
||||||
expect(document.body.textContent).toContain("Try again");
|
expect(document.body.textContent).toContain("Try again");
|
||||||
expect(document.body.textContent).toContain("Mark issue done");
|
expect(document.body.textContent).toContain("Mark task done");
|
||||||
expect(document.body.textContent).not.toContain("Mark blocked");
|
expect(document.body.textContent).not.toContain("Mark blocked");
|
||||||
expect(document.body.textContent).not.toContain("Delegate follow-up issue");
|
expect(document.body.textContent).not.toContain("Delegate follow-up issue");
|
||||||
click([...document.body.querySelectorAll("button")].find((button) => button.textContent?.includes("Try again")) ?? null);
|
click([...document.body.querySelectorAll("button")].find((button) => button.textContent?.includes("Try again")) ?? null);
|
||||||
@@ -227,7 +227,7 @@ describe("IssueRecoveryActionCard", () => {
|
|||||||
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
|
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
|
||||||
|
|
||||||
expect(document.body.textContent).toContain("Try again");
|
expect(document.body.textContent).toContain("Try again");
|
||||||
expect(document.body.textContent).toContain("Mark issue done");
|
expect(document.body.textContent).toContain("Mark task done");
|
||||||
expect(document.body.textContent).toContain("Send for review");
|
expect(document.body.textContent).toContain("Send for review");
|
||||||
expect(document.body.textContent).toContain("False positive, done");
|
expect(document.body.textContent).toContain("False positive, done");
|
||||||
expect(document.body.textContent).toContain("False positive, review");
|
expect(document.body.textContent).toContain("False positive, review");
|
||||||
|
|||||||
@@ -45,22 +45,22 @@ export interface IssueRecoveryActionCardProps {
|
|||||||
|
|
||||||
const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
|
const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
|
||||||
missing_disposition: "Missing Disposition",
|
missing_disposition: "Missing Disposition",
|
||||||
stranded_assigned_issue: "Stranded Issue",
|
stranded_assigned_issue: "Stranded Task",
|
||||||
workspace_validation: "Workspace Validation",
|
workspace_validation: "Workspace Validation",
|
||||||
active_run_watchdog: "Active Watchdog",
|
active_run_watchdog: "Active Watchdog",
|
||||||
issue_graph_liveness: "Graph Liveness",
|
issue_graph_liveness: "Graph Liveness",
|
||||||
};
|
};
|
||||||
|
|
||||||
const KIND_HEADLINE: Record<IssueRecoveryActionKind, string> = {
|
const KIND_HEADLINE: Record<IssueRecoveryActionKind, string> = {
|
||||||
missing_disposition: "This issue's run finished, but no next step was chosen.",
|
missing_disposition: "This task's run finished, but no next step was chosen.",
|
||||||
stranded_assigned_issue:
|
stranded_assigned_issue:
|
||||||
"Paperclip retried this issue's last run and it still has no live execution path.",
|
"Paperclip retried this task's last run and it still has no live execution path.",
|
||||||
workspace_validation:
|
workspace_validation:
|
||||||
"Paperclip stopped this run because the issue's git workspace could not be validated.",
|
"Paperclip stopped this run because the task's git workspace could not be validated.",
|
||||||
active_run_watchdog:
|
active_run_watchdog:
|
||||||
"The active run has been silent. Recovery is observing without interrupting it.",
|
"The active run has been silent. Recovery is observing without interrupting it.",
|
||||||
issue_graph_liveness:
|
issue_graph_liveness:
|
||||||
"Paperclip detected this issue lost a live action path. A recovery owner needs to act.",
|
"Paperclip detected this task lost a live action path. A recovery owner needs to act.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATE_TONE: Record<RecoveryCardCardState, {
|
const STATE_TONE: Record<RecoveryCardCardState, {
|
||||||
@@ -300,11 +300,11 @@ const RESOLVE_OPTIONS: Array<{
|
|||||||
{
|
{
|
||||||
outcome: "todo",
|
outcome: "todo",
|
||||||
label: "Try again",
|
label: "Try again",
|
||||||
description: "Dismiss recovery and return the source issue to todo.",
|
description: "Dismiss recovery and return the source task to todo.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outcome: "done",
|
outcome: "done",
|
||||||
label: "Mark issue done",
|
label: "Mark task done",
|
||||||
description: "Restore by recording the requested work as complete.",
|
description: "Restore by recording the requested work as complete.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -315,14 +315,14 @@ const RESOLVE_OPTIONS: Array<{
|
|||||||
{
|
{
|
||||||
outcome: "false_positive_done",
|
outcome: "false_positive_done",
|
||||||
label: "False positive, done",
|
label: "False positive, done",
|
||||||
description: "Dismiss recovery and mark the source issue complete.",
|
description: "Dismiss recovery and mark the source task complete.",
|
||||||
destructive: true,
|
destructive: true,
|
||||||
boardOnly: true,
|
boardOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outcome: "false_positive_in_review",
|
outcome: "false_positive_in_review",
|
||||||
label: "False positive, review",
|
label: "False positive, review",
|
||||||
description: "Dismiss recovery and send the source issue for review.",
|
description: "Dismiss recovery and send the source task for review.",
|
||||||
destructive: true,
|
destructive: true,
|
||||||
boardOnly: true,
|
boardOnly: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function IssueReferencePill({
|
|||||||
data-mention-kind="issue"
|
data-mention-kind="issue"
|
||||||
className={classNames}
|
className={classNames}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
aria-label={`Issue: ${issue.title}`}
|
aria-label={`Task: ${issue.title}`}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</span>
|
</span>
|
||||||
@@ -50,7 +50,7 @@ export function IssueReferencePill({
|
|||||||
data-mention-kind="issue"
|
data-mention-kind="issue"
|
||||||
className={classNames}
|
className={classNames}
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
aria-label={`Issue ${issueLabel}: ${issue.title}`}
|
aria-label={`Task ${issueLabel}: ${issue.title}`}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ describe("IssueRelatedWorkPanel", () => {
|
|||||||
expect(html).toContain("Referenced by");
|
expect(html).toContain("Referenced by");
|
||||||
expect(html).toContain("PAP-22");
|
expect(html).toContain("PAP-22");
|
||||||
expect(html).toContain("PAP-33");
|
expect(html).toContain("PAP-33");
|
||||||
expect(html).toContain('aria-label="Issue PAP-22: Downstream task"');
|
expect(html).toContain('aria-label="Task PAP-22: Downstream task"');
|
||||||
expect(html).toContain('aria-label="Issue PAP-33: Upstream task"');
|
expect(html).toContain('aria-label="Task PAP-33: Upstream task"');
|
||||||
expect(html).toContain("plan");
|
expect(html).toContain("plan");
|
||||||
expect(html).toContain("comment");
|
expect(html).toContain("comment");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,15 +95,15 @@ export function IssueRelatedWorkPanel({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Section
|
<Section
|
||||||
title="References"
|
title="References"
|
||||||
description="Other tasks this issue currently points at in its title, description, comments, or documents."
|
description="Other tasks this task currently points at in its title, description, comments, or documents."
|
||||||
items={outbound}
|
items={outbound}
|
||||||
emptyLabel="This issue does not reference any other tasks yet."
|
emptyLabel="This task does not reference any other tasks yet."
|
||||||
/>
|
/>
|
||||||
<Section
|
<Section
|
||||||
title="Referenced by"
|
title="Referenced by"
|
||||||
description="Other tasks that currently point at this issue."
|
description="Other tasks that currently point at this task."
|
||||||
items={inbound}
|
items={inbound}
|
||||||
emptyLabel="No other tasks reference this issue yet."
|
emptyLabel="No other tasks reference this task yet."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ function renderRecoveryChip(action: IssueRecoveryAction, selected: boolean): Rea
|
|||||||
tone.className,
|
tone.className,
|
||||||
selected ? "!border-muted-foreground !text-muted-foreground" : null,
|
selected ? "!border-muted-foreground !text-muted-foreground" : null,
|
||||||
)}
|
)}
|
||||||
title={`${label} — open the source issue to act.`}
|
title={`${label} — open the source task to act.`}
|
||||||
>
|
>
|
||||||
<Icon className="h-2.5 w-2.5" aria-hidden />
|
<Icon className="h-2.5 w-2.5" aria-hidden />
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const LIVENESS_COPY: Record<RunLivenessState, LivenessCopy> = {
|
|||||||
completed: {
|
completed: {
|
||||||
label: "Completed",
|
label: "Completed",
|
||||||
tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
||||||
description: "Issue reached a terminal state.",
|
description: "Task reached a terminal state.",
|
||||||
},
|
},
|
||||||
advanced: {
|
advanced: {
|
||||||
label: "Advanced",
|
label: "Advanced",
|
||||||
@@ -95,7 +95,7 @@ const LIVENESS_COPY: Record<RunLivenessState, LivenessCopy> = {
|
|||||||
blocked: {
|
blocked: {
|
||||||
label: "Blocked",
|
label: "Blocked",
|
||||||
tone: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300",
|
tone: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300",
|
||||||
description: "Run or issue declared a blocker.",
|
description: "Run or task declared a blocker.",
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
label: "Failed",
|
label: "Failed",
|
||||||
@@ -535,7 +535,7 @@ export function IssueRunLedgerContent({
|
|||||||
}, [activityEvents, canRenderActivityEvents, ledgerRuns]);
|
}, [activityEvents, canRenderActivityEvents, ledgerRuns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3" aria-label="Issue run ledger">
|
<section className="space-y-3" aria-label="Task run ledger">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">Run ledger</h3>
|
<h3 className="text-sm font-medium text-muted-foreground">Run ledger</h3>
|
||||||
@@ -678,8 +678,8 @@ export function IssueRunLedgerContent({
|
|||||||
{feedItems.length === 0 ? (
|
{feedItems.length === 0 ? (
|
||||||
<div className="rounded-md border border-dashed border-border px-3 py-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-dashed border-border px-3 py-3 text-sm text-muted-foreground">
|
||||||
{renderActivityEvent
|
{renderActivityEvent
|
||||||
? "Runs and activity will appear here once this issue has history."
|
? "Runs and activity will appear here once this task has history."
|
||||||
: "Historical runs without liveness metadata will appear here once linked to this issue."}
|
: "Historical runs without liveness metadata will appear here once linked to this task."}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe("IssueSiblingNavigation", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const nav = node.querySelector("nav");
|
const nav = node.querySelector("nav");
|
||||||
expect(nav?.getAttribute("aria-label")).toBe("Sub-issue navigation");
|
expect(nav?.getAttribute("aria-label")).toBe("Sub-task navigation");
|
||||||
expect(nav?.className).toContain("sm:grid-cols-2");
|
expect(nav?.className).toContain("sm:grid-cols-2");
|
||||||
expect(nav?.className).not.toContain("border-t");
|
expect(nav?.className).not.toContain("border-t");
|
||||||
|
|
||||||
@@ -93,13 +93,13 @@ describe("IssueSiblingNavigation", () => {
|
|||||||
expect(links[0].textContent).toContain("Previous");
|
expect(links[0].textContent).toContain("Previous");
|
||||||
expect(links[0].textContent).toContain("PAP-1");
|
expect(links[0].textContent).toContain("PAP-1");
|
||||||
expect(links[0].textContent).toContain("Previous sibling title");
|
expect(links[0].textContent).toContain("Previous sibling title");
|
||||||
expect(links[0].getAttribute("aria-label")).toBe("Previous sub-issue: PAP-1 - Previous sibling title");
|
expect(links[0].getAttribute("aria-label")).toBe("Previous sub-task: PAP-1 - Previous sibling title");
|
||||||
expect(links[0].getAttribute("data-quicklook-align")).toBe("start");
|
expect(links[0].getAttribute("data-quicklook-align")).toBe("start");
|
||||||
|
|
||||||
expect(links[1].textContent).toContain("Next");
|
expect(links[1].textContent).toContain("Next");
|
||||||
expect(links[1].textContent).toContain("PAP-3");
|
expect(links[1].textContent).toContain("PAP-3");
|
||||||
expect(links[1].textContent).toContain("Next sibling title");
|
expect(links[1].textContent).toContain("Next sibling title");
|
||||||
expect(links[1].getAttribute("aria-label")).toBe("Next sub-issue: PAP-3 - Next sibling title");
|
expect(links[1].getAttribute("aria-label")).toBe("Next sub-task: PAP-3 - Next sibling title");
|
||||||
expect(links[1].getAttribute("data-quicklook-align")).toBe("end");
|
expect(links[1].getAttribute("data-quicklook-align")).toBe("end");
|
||||||
expect(links[1].className).toContain("sm:text-right");
|
expect(links[1].className).toContain("sm:text-right");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function IssueSiblingNavigation({ navigation, linkState }: IssueSiblingNa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
aria-label="Sub-issue navigation"
|
aria-label="Sub-task navigation"
|
||||||
className="mt-4 flex flex-col gap-3 sm:mt-6 sm:grid sm:grid-cols-2"
|
className="mt-4 flex flex-col gap-3 sm:mt-6 sm:grid sm:grid-cols-2"
|
||||||
>
|
>
|
||||||
{navigation.previous ? (
|
{navigation.previous ? (
|
||||||
@@ -47,7 +47,7 @@ function SiblingLink({
|
|||||||
}) {
|
}) {
|
||||||
const issuePathId = issue.identifier ?? issue.id;
|
const issuePathId = issue.identifier ?? issue.id;
|
||||||
const label = direction === "previous" ? "Previous" : "Next";
|
const label = direction === "previous" ? "Previous" : "Next";
|
||||||
const ariaDirection = direction === "previous" ? "Previous sub-issue" : "Next sub-issue";
|
const ariaDirection = direction === "previous" ? "Previous sub-task" : "Next sub-task";
|
||||||
const identifier = issue.identifier ?? issue.id.slice(0, 8);
|
const identifier = issue.identifier ?? issue.id.slice(0, 8);
|
||||||
const Icon = direction === "previous" ? ChevronLeft : ChevronRight;
|
const Icon = direction === "previous" ? ChevronLeft : ChevronRight;
|
||||||
|
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ function SuggestTasksCard({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>{totalTasks === 1 ? "1 draft issue" : `${totalTasks} draft issues`}</span>
|
<span>{totalTasks === 1 ? "1 draft task" : `${totalTasks} draft tasks`}</span>
|
||||||
{interaction.payload.defaultParentId ? (
|
{interaction.payload.defaultParentId ? (
|
||||||
<TaskField label="Default parent" value={interaction.payload.defaultParentId} tone="subtle" />
|
<TaskField label="Default parent" value={interaction.payload.defaultParentId} tone="subtle" />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -497,8 +497,8 @@ function SuggestTasksCard({
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-1 leading-6">
|
<p className="mt-1 leading-6">
|
||||||
{skippedCount > 0
|
{skippedCount > 0
|
||||||
? `Created ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"} and skipped ${skippedCount} during review.`
|
? `Created ${createdCount} draft ${createdCount === 1 ? "task" : "tasks"} and skipped ${skippedCount} during review.`
|
||||||
: `Created all ${createdCount} draft ${createdCount === 1 ? "issue" : "issues"}.`}
|
: `Created all ${createdCount} draft ${createdCount === 1 ? "task" : "tasks"}.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -523,8 +523,8 @@ function SuggestTasksCard({
|
|||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{selectedCount === totalTasks
|
{selectedCount === totalTasks
|
||||||
? `All ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} selected`
|
? `All ${totalTasks} draft ${totalTasks === 1 ? "task" : "tasks"} selected`
|
||||||
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "issue" : "issues"} selected`}
|
: `${selectedCount} of ${totalTasks} draft ${totalTasks === 1 ? "task" : "tasks"} selected`}
|
||||||
</span>
|
</span>
|
||||||
{selectedCount < totalTasks ? (
|
{selectedCount < totalTasks ? (
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -438,10 +438,10 @@ export function IssueWorkspaceCard({
|
|||||||
{!workspace && (
|
{!workspace && (
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{currentSelection === "isolated_workspace"
|
{currentSelection === "isolated_workspace"
|
||||||
? "A fresh isolated workspace will be created when this issue runs."
|
? "A fresh isolated workspace will be created when this task runs."
|
||||||
: currentSelection === "reuse_existing"
|
: currentSelection === "reuse_existing"
|
||||||
? "This issue will reuse an existing workspace when it runs."
|
? "This task will reuse an existing workspace when it runs."
|
||||||
: "This issue will use the project default workspace configuration when it runs."}
|
: "This task will use the project default workspace configuration when it runs."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentSelection === "reuse_existing" && selectedReusableExecutionWorkspace && (
|
{currentSelection === "reuse_existing" && selectedReusableExecutionWorkspace && (
|
||||||
|
|||||||
@@ -452,12 +452,12 @@ describe("IssuesList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue in Feature Branch"]');
|
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New task in Feature Branch"]');
|
||||||
expect(button).not.toBeNull();
|
expect(button).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New issue in Feature Branch"]');
|
const button = container.querySelector<HTMLButtonElement>('button[aria-label="New task in Feature Branch"]');
|
||||||
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
@@ -884,7 +884,7 @@ describe("IssuesList", () => {
|
|||||||
container,
|
container,
|
||||||
);
|
);
|
||||||
|
|
||||||
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
|
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
||||||
expect(valueSetter).toBeTypeOf("function");
|
expect(valueSetter).toBeTypeOf("function");
|
||||||
@@ -1157,7 +1157,7 @@ describe("IssuesList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
expect(container.textContent).toContain("Some board columns are showing up to 200 issues. Refine filters or search to reveal the rest.");
|
expect(container.textContent).toContain("Some board columns are showing up to 200 tasks. Refine filters or search to reveal the rest.");
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -1187,7 +1187,7 @@ describe("IssuesList", () => {
|
|||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(100);
|
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(100);
|
||||||
expect(container.textContent).toContain("Rendering 100 of 220 issues");
|
expect(container.textContent).toContain("Rendering 100 of 220 tasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -1226,7 +1226,7 @@ describe("IssuesList", () => {
|
|||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(250);
|
expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(250);
|
||||||
expect(container.textContent).toContain("Rendering 250 of 420 issues");
|
expect(container.textContent).toContain("Rendering 250 of 420 tasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -1669,13 +1669,13 @@ describe("IssuesList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
|
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
input?.focus();
|
input?.focus();
|
||||||
expect(document.activeElement).toBe(input);
|
expect(document.activeElement).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement;
|
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
|
||||||
act(() => {
|
act(() => {
|
||||||
input.dispatchEvent(new KeyboardEvent("keydown", {
|
input.dispatchEvent(new KeyboardEvent("keydown", {
|
||||||
key: "Enter",
|
key: "Enter",
|
||||||
@@ -1705,13 +1705,13 @@ describe("IssuesList", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement | null;
|
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement | null;
|
||||||
expect(input).not.toBeNull();
|
expect(input).not.toBeNull();
|
||||||
input?.focus();
|
input?.focus();
|
||||||
expect(document.activeElement).toBe(input);
|
expect(document.activeElement).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = container.querySelector('input[aria-label="Search issues"]') as HTMLInputElement;
|
const input = container.querySelector('input[aria-label="Search tasks"]') as HTMLInputElement;
|
||||||
act(() => {
|
act(() => {
|
||||||
input.dispatchEvent(new KeyboardEvent("keydown", {
|
input.dispatchEvent(new KeyboardEvent("keydown", {
|
||||||
key: "Escape",
|
key: "Escape",
|
||||||
|
|||||||
@@ -470,9 +470,9 @@ function IssueSearchInput({
|
|||||||
e.currentTarget.blur();
|
e.currentTarget.blur();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Search issues..."
|
placeholder="Search tasks..."
|
||||||
className="pl-7 text-xs sm:text-sm"
|
className="pl-7 text-xs sm:text-sm"
|
||||||
aria-label="Search issues"
|
aria-label="Search tasks"
|
||||||
data-page-search-target="true"
|
data-page-search-target="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -531,7 +531,7 @@ function SubIssueProgressSummaryStrip({
|
|||||||
className="text-muted-foreground tabular-nums"
|
className="text-muted-foreground tabular-nums"
|
||||||
title={`${costSummary.runCount.toLocaleString()} run${
|
title={`${costSummary.runCount.toLocaleString()} run${
|
||||||
costSummary.runCount === 1 ? "" : "s"
|
costSummary.runCount === 1 ? "" : "s"
|
||||||
} across ${costSummary.issueCount} sub-issue${
|
} across ${costSummary.issueCount} sub-task${
|
||||||
costSummary.issueCount === 1 ? "" : "s"
|
costSummary.issueCount === 1 ? "" : "s"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -545,7 +545,7 @@ function SubIssueProgressSummaryStrip({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-label="Sub-issues completion progress"
|
aria-label="Sub-tasks completion progress"
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuenow={summary.doneCount}
|
aria-valuenow={summary.doneCount}
|
||||||
aria-valuemax={summary.totalCount}
|
aria-valuemax={summary.totalCount}
|
||||||
@@ -582,11 +582,11 @@ function SubIssueProgressSummaryStrip({
|
|||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : summary.totalCount === 0 ? (
|
) : summary.totalCount === 0 ? (
|
||||||
<div className="text-sm font-medium text-foreground">No active sub-issues</div>
|
<div className="text-sm font-medium text-foreground">No active sub-tasks</div>
|
||||||
) : summary.doneCount === summary.totalCount ? (
|
) : summary.doneCount === summary.totalCount ? (
|
||||||
<div className="text-sm font-medium text-foreground">All sub-issues done</div>
|
<div className="text-sm font-medium text-foreground">All sub-tasks done</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm font-medium text-foreground">No actionable sub-issues</div>
|
<div className="text-sm font-medium text-foreground">No actionable sub-tasks</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1289,8 +1289,8 @@ export function IssuesList({
|
|||||||
viewState.groupBy,
|
viewState.groupBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Issue";
|
const createActionLabel = createIssueLabel ? `Create ${createIssueLabel}` : "Create Task";
|
||||||
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Issue";
|
const createButtonLabel = createIssueLabel ? `New ${createIssueLabel}` : "New Task";
|
||||||
const openCreateIssueDialog = useCallback((group?: { key: string; items: Issue[] }) => {
|
const openCreateIssueDialog = useCallback((group?: { key: string; items: Issue[] }) => {
|
||||||
openNewIssue(newIssueDefaults(group));
|
openNewIssue(newIssueDefaults(group));
|
||||||
}, [newIssueDefaults, openNewIssue]);
|
}, [newIssueDefaults, openNewIssue]);
|
||||||
@@ -1461,7 +1461,7 @@ export function IssuesList({
|
|||||||
visibleColumnSet={visibleIssueColumnSet}
|
visibleColumnSet={visibleIssueColumnSet}
|
||||||
onToggleColumn={toggleIssueColumn}
|
onToggleColumn={toggleIssueColumn}
|
||||||
onResetColumns={() => setIssueColumns(DEFAULT_INBOX_ISSUE_COLUMNS)}
|
onResetColumns={() => setIssueColumns(DEFAULT_INBOX_ISSUE_COLUMNS)}
|
||||||
title="Choose which issue columns stay visible"
|
title="Choose which task columns stay visible"
|
||||||
iconOnly
|
iconOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -1539,7 +1539,7 @@ export function IssuesList({
|
|||||||
["assignee", "Assignee"],
|
["assignee", "Assignee"],
|
||||||
["project", "Project"],
|
["project", "Project"],
|
||||||
["workspace", "Workspace"],
|
["workspace", "Workspace"],
|
||||||
["parent", "Parent Issue"],
|
["parent", "Parent Task"],
|
||||||
["none", "None"],
|
["none", "None"],
|
||||||
] as const).map(([value, label]) => (
|
] as const).map(([value, label]) => (
|
||||||
<button
|
<button
|
||||||
@@ -1569,13 +1569,13 @@ export function IssuesList({
|
|||||||
)}
|
)}
|
||||||
{boardColumnLimitReached && (
|
{boardColumnLimitReached && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} issues. Refine filters or search to reveal the rest.
|
Some board columns are showing up to {ISSUE_BOARD_COLUMN_RESULT_LIMIT} tasks. Refine filters or search to reveal the rest.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!isLoading && filtered.length === 0 && viewState.viewMode === "list" && (
|
{!isLoading && filtered.length === 0 && viewState.viewMode === "list" && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={CircleDot}
|
icon={CircleDot}
|
||||||
message="No issues match the current filters or search."
|
message="No tasks match the current filters or search."
|
||||||
action={createActionLabel}
|
action={createActionLabel}
|
||||||
onAction={() => openCreateIssueDialog()}
|
onAction={() => openCreateIssueDialog()}
|
||||||
/>
|
/>
|
||||||
@@ -1625,8 +1625,8 @@ export function IssuesList({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
className="-mr-2 text-muted-foreground"
|
className="-mr-2 text-muted-foreground"
|
||||||
title={`New issue in ${group.label}`}
|
title={`New task in ${group.label}`}
|
||||||
aria-label={`New issue in ${group.label}`}
|
aria-label={`New task in ${group.label}`}
|
||||||
onClick={() => openCreateIssueDialog(group)}
|
onClick={() => openCreateIssueDialog(group)}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
@@ -1771,7 +1771,7 @@ export function IssuesList({
|
|||||||
<span
|
<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"
|
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"
|
aria-label="Needs next step"
|
||||||
title="This issue needs a next step"
|
title="This task needs a next step"
|
||||||
>
|
>
|
||||||
<CircleDot className="h-3 w-3" />
|
<CircleDot className="h-3 w-3" />
|
||||||
Needs next step
|
Needs next step
|
||||||
@@ -1959,10 +1959,10 @@ export function IssuesList({
|
|||||||
<div className="py-2" data-testid="issues-load-more-sentinel">
|
<div className="py-2" data-testid="issues-load-more-sentinel">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{isLoadingMoreIssues
|
{isLoadingMoreIssues
|
||||||
? "Loading more issues..."
|
? "Loading more tasks..."
|
||||||
: remainingIssueRowCount > 0
|
: remainingIssueRowCount > 0
|
||||||
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} issues`
|
? `Rendering ${Math.min(renderedIssueRowLimit, filtered.length)} of ${filtered.length} tasks`
|
||||||
: "Scroll to load more issues"}
|
: "Scroll to load more tasks"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ function KanbanCard({
|
|||||||
{isSuccessfulRunHandoffRequired(issue) ? (
|
{isSuccessfulRunHandoffRequired(issue) ? (
|
||||||
<span
|
<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"
|
className="inline-flex items-center gap-1 rounded-full border border-amber-400/45 bg-amber-50/60 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:border-amber-300/35 dark:bg-amber-400/10 dark:text-amber-300"
|
||||||
title="This issue needs a next step"
|
title="This task needs a next step"
|
||||||
aria-label="Needs next step"
|
aria-label="Needs next step"
|
||||||
>
|
>
|
||||||
<AlertTriangle className="h-3 w-3" />
|
<AlertTriangle className="h-3 w-3" />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const sections: ShortcutSection[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Issue detail",
|
title: "Task detail",
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{ keys: ["y"], label: "Quick-archive back to inbox" },
|
{ keys: ["y"], label: "Quick-archive back to inbox" },
|
||||||
{ keys: ["g", "i"], label: "Go to inbox" },
|
{ keys: ["g", "i"], label: "Go to inbox" },
|
||||||
@@ -39,7 +39,7 @@ const sections: ShortcutSection[] = [
|
|||||||
title: "Global",
|
title: "Global",
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{ keys: ["/"], label: "Search current page or quick search" },
|
{ keys: ["/"], label: "Search current page or quick search" },
|
||||||
{ keys: ["c"], label: "New issue" },
|
{ keys: ["c"], label: "New task" },
|
||||||
{ keys: ["["], label: "Toggle sidebar" },
|
{ keys: ["["], label: "Toggle sidebar" },
|
||||||
{ keys: ["]"], label: "Toggle panel" },
|
{ keys: ["]"], label: "Toggle panel" },
|
||||||
{ keys: ["?"], label: "Show keyboard shortcuts" },
|
{ keys: ["?"], label: "Show keyboard shortcuts" },
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||||||
Live Runs
|
Live Runs
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
Uses the shared chat-style run surface from issue activity.
|
Uses the shared chat-style run surface from task activity.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function MobileBottomNav({ visible }: MobileBottomNavProps) {
|
|||||||
const items = useMemo<MobileNavItem[]>(
|
const items = useMemo<MobileNavItem[]>(
|
||||||
() => [
|
() => [
|
||||||
{ type: "link", to: "/dashboard", label: "Home", icon: House },
|
{ type: "link", to: "/dashboard", label: "Home", icon: House },
|
||||||
{ type: "link", to: "/issues", label: "Issues", icon: CircleDot },
|
{ type: "link", to: "/issues", label: "Tasks", icon: CircleDot },
|
||||||
{ type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() },
|
{ type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() },
|
||||||
{ type: "link", to: "/agents/all", label: "Agents", icon: Users },
|
{ type: "link", to: "/agents/all", label: "Agents", icon: Users },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -352,11 +352,11 @@ describe("NewIssueDialog", () => {
|
|||||||
const { root } = renderDialog(container);
|
const { root } = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).toContain("New sub-issue");
|
expect(container.textContent).toContain("New sub-task");
|
||||||
expect(container.textContent).toContain("Sub-issue of");
|
expect(container.textContent).toContain("Sub-task of");
|
||||||
expect(container.textContent).toContain("PAP-1");
|
expect(container.textContent).toContain("PAP-1");
|
||||||
expect(container.textContent).toContain("Parent issue");
|
expect(container.textContent).toContain("Parent issue");
|
||||||
expect(container.textContent).toContain("Create Sub-Issue");
|
expect(container.textContent).toContain("Create Sub-Task");
|
||||||
|
|
||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
|
|
||||||
@@ -364,9 +364,9 @@ describe("NewIssueDialog", () => {
|
|||||||
const rerendered = renderDialog(container);
|
const rerendered = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).toContain("New issue");
|
expect(container.textContent).toContain("New task");
|
||||||
expect(container.textContent).toContain("Create Issue");
|
expect(container.textContent).toContain("Create Task");
|
||||||
expect(container.textContent).not.toContain("Sub-issue of");
|
expect(container.textContent).not.toContain("Sub-task of");
|
||||||
|
|
||||||
act(() => rerendered.root.unmount());
|
act(() => rerendered.root.unmount());
|
||||||
});
|
});
|
||||||
@@ -421,7 +421,7 @@ describe("NewIssueDialog", () => {
|
|||||||
expect(mockExecutionWorkspacesApi.list).not.toHaveBeenCalled();
|
expect(mockExecutionWorkspacesApi.list).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Sub-Issue"));
|
.find((button) => button.textContent?.includes("Create Sub-Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
||||||
@@ -460,7 +460,7 @@ describe("NewIssueDialog", () => {
|
|||||||
expect(planningButton?.className).toContain("bg-accent");
|
expect(planningButton?.className).toContain("bg-accent");
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Issue"));
|
.find((button) => button.textContent?.includes("Create Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
||||||
@@ -531,14 +531,14 @@ describe("NewIssueDialog", () => {
|
|||||||
const { root } = renderDialog(container);
|
const { root } = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).toContain("New issue");
|
expect(container.textContent).toContain("New task");
|
||||||
expect(container.textContent).not.toContain("New sub-issue");
|
expect(container.textContent).not.toContain("New sub-task");
|
||||||
await waitForAssertion(() => {
|
await waitForAssertion(() => {
|
||||||
expect(container.textContent).toContain("Reusing PAP-100");
|
expect(container.textContent).toContain("Reusing PAP-100");
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Issue"));
|
.find((button) => button.textContent?.includes("Create Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -578,7 +578,7 @@ describe("NewIssueDialog", () => {
|
|||||||
const { root } = renderDialog(container);
|
const { root } = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
|
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
|
||||||
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
|
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
|
||||||
expect(titleInput).not.toBeNull();
|
expect(titleInput).not.toBeNull();
|
||||||
expect(descriptionInput).not.toBeNull();
|
expect(descriptionInput).not.toBeNull();
|
||||||
@@ -601,7 +601,7 @@ describe("NewIssueDialog", () => {
|
|||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Issue"));
|
.find((button) => button.textContent?.includes("Create Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
||||||
@@ -635,7 +635,7 @@ describe("NewIssueDialog", () => {
|
|||||||
const { root } = renderDialog(container);
|
const { root } = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
|
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
|
||||||
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
|
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]') as HTMLTextAreaElement | null;
|
||||||
expect(titleInput).not.toBeNull();
|
expect(titleInput).not.toBeNull();
|
||||||
expect(descriptionInput).not.toBeNull();
|
expect(descriptionInput).not.toBeNull();
|
||||||
@@ -644,7 +644,7 @@ describe("NewIssueDialog", () => {
|
|||||||
await typeTextareaValue(descriptionInput!, description);
|
await typeTextareaValue(descriptionInput!, description);
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Issue"));
|
.find((button) => button.textContent?.includes("Create Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
||||||
@@ -671,7 +671,7 @@ describe("NewIssueDialog", () => {
|
|||||||
const { root } = renderDialog(container);
|
const { root } = renderDialog(container);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const titleInput = container.querySelector('textarea[placeholder="Issue title"]') as HTMLTextAreaElement | null;
|
const titleInput = container.querySelector('textarea[placeholder="Task title"]') as HTMLTextAreaElement | null;
|
||||||
expect(titleInput).not.toBeNull();
|
expect(titleInput).not.toBeNull();
|
||||||
await typeTextareaValue(titleInput!, "Plan this first");
|
await typeTextareaValue(titleInput!, "Plan this first");
|
||||||
|
|
||||||
@@ -683,7 +683,7 @@ describe("NewIssueDialog", () => {
|
|||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Issue"));
|
.find((button) => button.textContent?.includes("Create Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
expect(submitButton?.hasAttribute("disabled")).toBe(false);
|
||||||
@@ -720,7 +720,7 @@ describe("NewIssueDialog", () => {
|
|||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const submitButton = Array.from(container.querySelectorAll("button"))
|
const submitButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.includes("Create Sub-Issue"));
|
.find((button) => button.textContent?.includes("Create Sub-Task"));
|
||||||
expect(submitButton).not.toBeUndefined();
|
expect(submitButton).not.toBeUndefined();
|
||||||
|
|
||||||
await act(async () => {
|
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-top)");
|
||||||
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-bottom)");
|
expect(dialogContent?.getAttribute("style")).toContain("env(safe-area-inset-bottom)");
|
||||||
|
|
||||||
const titleInput = container.querySelector('textarea[placeholder="Issue title"]');
|
const titleInput = container.querySelector('textarea[placeholder="Task title"]');
|
||||||
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]');
|
const descriptionInput = container.querySelector('textarea[aria-label="Add description..."]');
|
||||||
const bodyScrollRegion = Array.from(container.querySelectorAll("div")).find((element) =>
|
const bodyScrollRegion = Array.from(container.querySelectorAll("div")).find((element) =>
|
||||||
typeof element.className === "string" && element.className.includes("overscroll-contain"),
|
typeof element.className === "string" && element.className.includes("overscroll-contain"),
|
||||||
@@ -862,7 +862,7 @@ describe("NewIssueDialog", () => {
|
|||||||
await flush();
|
await flush();
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain("will no longer use the parent issue workspace");
|
expect(container.textContent).not.toContain("will no longer use the parent task workspace");
|
||||||
|
|
||||||
const selects = Array.from(container.querySelectorAll("select"));
|
const selects = Array.from(container.querySelectorAll("select"));
|
||||||
const modeSelect = selects[0] as HTMLSelectElement | undefined;
|
const modeSelect = selects[0] as HTMLSelectElement | undefined;
|
||||||
@@ -874,7 +874,7 @@ describe("NewIssueDialog", () => {
|
|||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
expect(container.textContent).toContain("will no longer use the parent issue workspace");
|
expect(container.textContent).toContain("will no longer use the parent task workspace");
|
||||||
expect(container.textContent).toContain("Parent workspace");
|
expect(container.textContent).toContain("Parent workspace");
|
||||||
|
|
||||||
act(() => root.unmount());
|
act(() => root.unmount());
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ const IssueTitleTextarea = memo(function IssueTitleTextarea({
|
|||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full text-lg font-semibold bg-transparent outline-none resize-none overflow-hidden placeholder:text-muted-foreground/50"
|
className="w-full text-lg font-semibold bg-transparent outline-none resize-none overflow-hidden placeholder:text-muted-foreground/50"
|
||||||
placeholder="Issue title"
|
placeholder="Task title"
|
||||||
rows={1}
|
rows={1}
|
||||||
value={draftValue}
|
value={draftValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1145,7 +1145,7 @@ export function NewIssueDialog() {
|
|||||||
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
|
const hasSavedDraft = Boolean(savedDraft?.title.trim() || savedDraft?.description.trim());
|
||||||
const canDiscardDraft = hasDraft || hasSavedDraft;
|
const canDiscardDraft = hasDraft || hasSavedDraft;
|
||||||
const createIssueErrorMessage =
|
const createIssueErrorMessage =
|
||||||
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create issue. Try again.";
|
createIssue.error instanceof Error ? createIssue.error.message : "Failed to create task. Try again.";
|
||||||
const stagedDocuments = stagedFiles.filter((file) => file.kind === "document");
|
const stagedDocuments = stagedFiles.filter((file) => file.kind === "document");
|
||||||
const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment");
|
const stagedAttachments = stagedFiles.filter((file) => file.kind === "attachment");
|
||||||
|
|
||||||
@@ -1294,7 +1294,7 @@ export function NewIssueDialog() {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<span className="text-muted-foreground/60">›</span>
|
<span className="text-muted-foreground/60">›</span>
|
||||||
<span>{isSubIssueMode ? "New sub-issue" : "New issue"}</span>
|
<span>{isSubIssueMode ? "New sub-task" : "New task"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<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="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">
|
<div className="flex items-center gap-1.5">
|
||||||
<ListTree className="h-3.5 w-3.5 shrink-0" />
|
<ListTree className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="shrink-0">Sub-issue of</span>
|
<span className="shrink-0">Sub-task of</span>
|
||||||
<span className="font-medium text-foreground">{parentIssueLabel}</span>
|
<span className="font-medium text-foreground">{parentIssueLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
{newIssueDefaults.parentTitle ? (
|
{newIssueDefaults.parentTitle ? (
|
||||||
@@ -1596,7 +1596,7 @@ export function NewIssueDialog() {
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-xs font-medium">Execution workspace</div>
|
<div className="text-xs font-medium">Execution workspace</div>
|
||||||
<div className="text-[11px] text-muted-foreground">
|
<div className="text-[11px] text-muted-foreground">
|
||||||
Control whether this issue runs in the shared workspace, a new isolated workspace, or an existing one.
|
Control whether this task runs in the shared workspace, a new isolated workspace, or an existing one.
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
|
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 ? (
|
{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">
|
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/30 dark:text-amber-100">
|
||||||
Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
|
Warning: this sub-task will no longer use the parent task workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -1694,7 +1694,7 @@ export function NewIssueDialog() {
|
|||||||
<p className="text-[11px] text-muted-foreground">Runs on the agent's primary model.</p>
|
<p className="text-[11px] text-muted-foreground">Runs on the agent's primary model.</p>
|
||||||
)}
|
)}
|
||||||
{assigneeModelLane === "custom" && (
|
{assigneeModelLane === "custom" && (
|
||||||
<p className="text-[11px] text-muted-foreground">Override the model and effort for this issue only.</p>
|
<p className="text-[11px] text-muted-foreground">Override the model and effort for this task only.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{assigneeModelLane === "custom" && (
|
{assigneeModelLane === "custom" && (
|
||||||
@@ -2079,7 +2079,7 @@ export function NewIssueDialog() {
|
|||||||
>
|
>
|
||||||
<span className="inline-flex items-center justify-center gap-1.5">
|
<span className="inline-flex items-center justify-center gap-1.5">
|
||||||
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
{createIssue.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
||||||
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Issue" : "Create Issue"}</span>
|
<span>{createIssue.isPending ? "Creating..." : isSubIssueMode ? "Create Sub-Task" : "Create Task"}</span>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1134,7 +1134,7 @@ export function OnboardingWizard() {
|
|||||||
<h3 className="font-medium">Ready to launch</h3>
|
<h3 className="font-medium">Ready to launch</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Everything is set up. Launching now will create the
|
Everything is set up. Launching now will create the
|
||||||
starter task, wake the agent, and open the issue.
|
starter task, wake the agent, and open the task.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1249,7 +1249,7 @@ export function OnboardingWizard() {
|
|||||||
) : (
|
) : (
|
||||||
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
<ArrowRight className="h-3.5 w-3.5 mr-1" />
|
||||||
)}
|
)}
|
||||||
{loading ? "Creating..." : "Create & Open Issue"}
|
{loading ? "Creating..." : "Create & Open Task"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
onChange={(env) => commitField("env", { env: env ?? null })}
|
onChange={(env) => commitField("env", { env: env ?? null })}
|
||||||
/>
|
/>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
Applied to all runs for issues in this project. Project values override agent env on key conflicts.
|
Applied to all runs for tasks in this project. Project values override agent env on key conflicts.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
@@ -925,7 +925,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
Project-owned defaults for isolated issue checkouts and execution workspace behavior.
|
Project-owned defaults for isolated task checkouts and execution workspace behavior.
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -933,11 +933,11 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium">
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
<span>Enable isolated issue checkouts</span>
|
<span>Enable isolated task checkouts</span>
|
||||||
<SaveIndicator state={fieldState("execution_workspace_enabled")} />
|
<SaveIndicator state={fieldState("execution_workspace_enabled")} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Let issues choose between the project's primary checkout and an isolated execution workspace.
|
Let tasks choose between the project's primary checkout and an isolated execution workspace.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{onUpdate || onFieldUpdate ? (
|
{onUpdate || onFieldUpdate ? (
|
||||||
@@ -961,11 +961,11 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<span>New issues default to isolated checkout</span>
|
<span>New tasks default to isolated checkout</span>
|
||||||
<SaveIndicator state={fieldState("execution_workspace_default_mode")} />
|
<SaveIndicator state={fieldState("execution_workspace_default_mode")} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[11px] text-muted-foreground">
|
<div className="text-[11px] text-muted-foreground">
|
||||||
If disabled, new issues stay on the project's primary checkout unless someone opts in.
|
If disabled, new tasks stay on the project's primary checkout unless someone opts in.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe("ProjectWorkspaceSummaryCard", () => {
|
|||||||
expect(container.textContent).toContain("Branch");
|
expect(container.textContent).toContain("Branch");
|
||||||
expect(container.textContent).toContain("Path");
|
expect(container.textContent).toContain("Path");
|
||||||
expect(container.textContent).toContain("Service");
|
expect(container.textContent).toContain("Service");
|
||||||
expect(container.textContent).toContain("Linked issues");
|
expect(container.textContent).toContain("Linked tasks");
|
||||||
expect(container.textContent).toContain("Start services");
|
expect(container.textContent).toContain("Start services");
|
||||||
expect(container.textContent).toContain("Close workspace");
|
expect(container.textContent).toContain("Close workspace");
|
||||||
expect(container.textContent).toContain("+1 more");
|
expect(container.textContent).toContain("+1 more");
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export function ProjectWorkspaceSummaryCard({
|
|||||||
{summary.issues.length > 0 ? (
|
{summary.issues.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
<div className="text-[11px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||||
Linked issues
|
Linked tasks
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{visibleIssues.map((issue) => (
|
{visibleIssues.map((issue) => (
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("Sidebar", () => {
|
|||||||
expect(workSection?.textContent).toContain("Plugin launcher outlet");
|
expect(workSection?.textContent).toContain("Plugin launcher outlet");
|
||||||
const workSectionContainer = workSection?.parentElement?.parentElement;
|
const workSectionContainer = workSection?.parentElement?.parentElement;
|
||||||
expect(workSectionContainer?.textContent).toContain("Work");
|
expect(workSectionContainer?.textContent).toContain("Work");
|
||||||
expect(workSectionContainer?.textContent).toContain("Issues");
|
expect(workSectionContainer?.textContent).toContain("Tasks");
|
||||||
expect(workSectionContainer?.textContent).toContain("Goals");
|
expect(workSectionContainer?.textContent).toContain("Goals");
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
@@ -167,19 +167,19 @@ describe("Sidebar", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("streamlined (flag ON): keeps Issue wording, top-level Projects link, no per-project collapsible", async () => {
|
it("streamlined (flag ON): keeps Task wording, top-level Projects link, no per-project collapsible", async () => {
|
||||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||||
enableIsolatedWorkspaces: false,
|
enableIsolatedWorkspaces: false,
|
||||||
enableStreamlinedLeftNavigation: true,
|
enableStreamlinedLeftNavigation: true,
|
||||||
});
|
});
|
||||||
const root = await renderSidebar();
|
const root = await renderSidebar();
|
||||||
|
|
||||||
expect(container.textContent).toContain("New Issue");
|
expect(container.textContent).toContain("New Task");
|
||||||
expect(container.textContent).not.toContain("New Task");
|
expect(container.textContent).not.toContain("New Issue");
|
||||||
|
|
||||||
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
|
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
|
||||||
expect(navLabels).toContain("Issues");
|
expect(navLabels).toContain("Tasks");
|
||||||
expect(navLabels).not.toContain("Tasks");
|
expect(navLabels).not.toContain("Issues");
|
||||||
|
|
||||||
const projectsLink = [...container.querySelectorAll("nav a")].find((a) => a.textContent?.trim() === "Projects");
|
const projectsLink = [...container.querySelectorAll("nav a")].find((a) => a.textContent?.trim() === "Projects");
|
||||||
expect(projectsLink?.getAttribute("href")).toBe("/projects");
|
expect(projectsLink?.getAttribute("href")).toBe("/projects");
|
||||||
@@ -194,19 +194,19 @@ describe("Sidebar", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classic (flag OFF): New Issue button, Issues label, per-project collapsible, no top-level Projects link", async () => {
|
it("classic (flag OFF): New Task button, Tasks label, per-project collapsible, no top-level Projects link", async () => {
|
||||||
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
mockInstanceSettingsApi.getExperimental.mockResolvedValue({
|
||||||
enableIsolatedWorkspaces: false,
|
enableIsolatedWorkspaces: false,
|
||||||
enableStreamlinedLeftNavigation: false,
|
enableStreamlinedLeftNavigation: false,
|
||||||
});
|
});
|
||||||
const root = await renderSidebar();
|
const root = await renderSidebar();
|
||||||
|
|
||||||
expect(container.textContent).toContain("New Issue");
|
expect(container.textContent).toContain("New Task");
|
||||||
expect(container.textContent).not.toContain("New Task");
|
expect(container.textContent).not.toContain("New Issue");
|
||||||
|
|
||||||
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
|
const navLabels = [...container.querySelectorAll("nav a")].map((a) => a.textContent?.trim());
|
||||||
expect(navLabels).toContain("Issues");
|
expect(navLabels).toContain("Tasks");
|
||||||
expect(navLabels).not.toContain("Tasks");
|
expect(navLabels).not.toContain("Issues");
|
||||||
// No top-level Projects nav link in classic mode (D5 option A).
|
// No top-level Projects nav link in classic mode (D5 option A).
|
||||||
expect(navLabels).not.toContain("Projects");
|
expect(navLabels).not.toContain("Projects");
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
<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">
|
<div className="flex flex-col gap-0.5">
|
||||||
{/* New Issue button aligned with nav items */}
|
{/* New Task button aligned with nav items */}
|
||||||
<button
|
<button
|
||||||
onClick={() => openNewIssue()}
|
onClick={() => openNewIssue()}
|
||||||
data-slot="icon-button"
|
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"
|
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" />
|
<SquarePen className="h-4 w-4 shrink-0" />
|
||||||
<span className="truncate">New Issue</span>
|
<span className="truncate">New Task</span>
|
||||||
</button>
|
</button>
|
||||||
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} />
|
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} />
|
||||||
<SidebarNavItem
|
<SidebarNavItem
|
||||||
@@ -101,7 +101,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarSection label="Work">
|
<SidebarSection label="Work">
|
||||||
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
|
<SidebarNavItem to="/issues" label="Tasks" icon={CircleDot} />
|
||||||
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
|
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
|
||||||
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
|
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
|
||||||
<SidebarNavItem to="/artifacts" label="Artifacts" icon={Package} />
|
<SidebarNavItem to="/artifacts" label="Artifacts" icon={Package} />
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function SourceResolvedFoldCallout({
|
|||||||
"[&>*]:border-emerald-300/40 dark:[&>*]:border-emerald-500/20",
|
"[&>*]:border-emerald-300/40 dark:[&>*]:border-emerald-500/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MetaRow label="Source issue">
|
<MetaRow label="Source task">
|
||||||
<span className="inline-flex flex-wrap items-center gap-1.5">
|
<span className="inline-flex flex-wrap items-center gap-1.5">
|
||||||
<Link
|
<Link
|
||||||
to={issueLink(fold.sourceIssueId, fold.sourceIssueIdentifier)}
|
to={issueLink(fold.sourceIssueId, fold.sourceIssueIdentifier)}
|
||||||
@@ -160,7 +160,7 @@ export function SourceResolvedFoldCallout({
|
|||||||
</span>
|
</span>
|
||||||
</MetaRow>
|
</MetaRow>
|
||||||
{fold.evaluationIssueId ? (
|
{fold.evaluationIssueId ? (
|
||||||
<MetaRow label="Evaluation issue">
|
<MetaRow label="Evaluation task">
|
||||||
<Link
|
<Link
|
||||||
to={issueLink(fold.evaluationIssueId, fold.evaluationIssueIdentifier)}
|
to={issueLink(fold.evaluationIssueId, fold.evaluationIssueIdentifier)}
|
||||||
className="rounded-sm font-medium underline-offset-2 hover:underline"
|
className="rounded-sm font-medium underline-offset-2 hover:underline"
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ describe("StatusIcon", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain('data-blocker-attention-state="covered"');
|
expect(html).toContain('data-blocker-attention-state="covered"');
|
||||||
expect(html).toContain('aria-label="Blocked · waiting on active sub-issue PAP-2"');
|
expect(html).toContain('aria-label="Blocked · waiting on active sub-task PAP-2"');
|
||||||
expect(html).toContain('title="Blocked · waiting on active sub-issue PAP-2"');
|
expect(html).toContain('title="Blocked · waiting on active sub-task PAP-2"');
|
||||||
expect(html).toContain("border-cyan-600");
|
expect(html).toContain("border-cyan-600");
|
||||||
expect(html).not.toContain("border-red-600");
|
expect(html).not.toContain("border-red-600");
|
||||||
expect(html).not.toContain("border-dashed");
|
expect(html).not.toContain("border-dashed");
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ function blockedAttentionLabel(blockerAttention: IssueBlockerAttention | null |
|
|||||||
if (blockerAttention.reason === "active_child") {
|
if (blockerAttention.reason === "active_child") {
|
||||||
const count = blockerAttention.coveredBlockerCount;
|
const count = blockerAttention.coveredBlockerCount;
|
||||||
if (count === 1 && blockerAttention.sampleBlockerIdentifier) {
|
if (count === 1 && blockerAttention.sampleBlockerIdentifier) {
|
||||||
return `Blocked · waiting on active sub-issue ${blockerAttention.sampleBlockerIdentifier}`;
|
return `Blocked · waiting on active sub-task ${blockerAttention.sampleBlockerIdentifier}`;
|
||||||
}
|
}
|
||||||
if (count === 1) return "Blocked · waiting on 1 active sub-issue";
|
if (count === 1) return "Blocked · waiting on 1 active sub-task";
|
||||||
return `Blocked · waiting on ${count} active sub-issues`;
|
return `Blocked · waiting on ${count} active sub-tasks`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockerAttention.reason === "active_dependency") {
|
if (blockerAttention.reason === "active_dependency") {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function IssueOutputSection({ workProducts, resolveCreatorName }: IssueOu
|
|||||||
const creatorFor = (item: IssueOutputItem) => resolveCreatorName?.(item) ?? null;
|
const creatorFor = (item: IssueOutputItem) => resolveCreatorName?.(item) ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3" aria-label="Issue outputs">
|
<section className="space-y-3" aria-label="Task outputs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Play className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
|
<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>
|
<h3 className="text-sm font-medium text-muted-foreground">Output</h3>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function resolveIssueToastContext(
|
|||||||
readString(details?.identifier) ??
|
readString(details?.identifier) ??
|
||||||
readString(details?.issueIdentifier) ??
|
readString(details?.issueIdentifier) ??
|
||||||
cachedIssue?.identifier ??
|
cachedIssue?.identifier ??
|
||||||
`Issue ${shortId(issueId)}`;
|
`Task ${shortId(issueId)}`;
|
||||||
const title =
|
const title =
|
||||||
readString(details?.title) ??
|
readString(details?.title) ??
|
||||||
readString(details?.issueTitle) ??
|
readString(details?.issueTitle) ??
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ function formatIssueReferenceLabel(reference: ActivityIssueReference): string {
|
|||||||
if (reference.identifier) return reference.identifier;
|
if (reference.identifier) return reference.identifier;
|
||||||
if (reference.title) return reference.title;
|
if (reference.title) return reference.title;
|
||||||
if (reference.id) return reference.id.slice(0, 8);
|
if (reference.id) return reference.id.slice(0, 8);
|
||||||
return "issue";
|
return "task";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChangedEntityLabel(
|
function formatChangedEntityLabel(
|
||||||
@@ -276,7 +276,7 @@ function formatIssueUpdatedAction(details: ActivityDetails, options: ActivityFor
|
|||||||
}
|
}
|
||||||
if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) {
|
if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) {
|
||||||
const assigneeName = formatAssigneeName(details, options);
|
const assigneeName = formatAssigneeName(details, options);
|
||||||
parts.push(assigneeName ? `assigned the issue to ${assigneeName}` : "unassigned the issue");
|
parts.push(assigneeName ? `assigned the task to ${assigneeName}` : "unassigned the task");
|
||||||
}
|
}
|
||||||
if (details.title !== undefined) parts.push("updated the title");
|
if (details.title !== undefined) parts.push("updated the title");
|
||||||
if (details.description !== undefined) parts.push("updated the description");
|
if (details.description !== undefined) parts.push("updated the description");
|
||||||
|
|||||||
@@ -1274,7 +1274,7 @@ describe("inbox helpers", () => {
|
|||||||
|
|
||||||
expect(groupInboxWorkItems(items, "none")).toEqual([{ key: "__all", label: null, items }]);
|
expect(groupInboxWorkItems(items, "none")).toEqual([{ key: "__all", label: null, items }]);
|
||||||
expect(groupInboxWorkItems(items, "type")).toEqual([
|
expect(groupInboxWorkItems(items, "type")).toEqual([
|
||||||
{ key: "issue", label: "Issues", items: [items[1], items[2]] },
|
{ key: "issue", label: "Tasks", items: [items[1], items[2]] },
|
||||||
{ key: "approval", label: "Approvals", items: [items[0]] },
|
{ key: "approval", label: "Approvals", items: [items[0]] },
|
||||||
{ key: "failed_run", label: "Failed runs", items: [items[3]] },
|
{ key: "failed_run", label: "Failed runs", items: [items[3]] },
|
||||||
{ key: "join_request", label: "Join requests", items: [items[4]] },
|
{ key: "join_request", label: "Join requests", items: [items[4]] },
|
||||||
|
|||||||
+1
-1
@@ -821,7 +821,7 @@ const inboxWorkItemKindOrder: InboxWorkItem["kind"][] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const inboxWorkItemKindLabels: Record<InboxWorkItem["kind"], string> = {
|
const inboxWorkItemKindLabels: Record<InboxWorkItem["kind"], string> = {
|
||||||
issue: "Issues",
|
issue: "Tasks",
|
||||||
approval: "Approvals",
|
approval: "Approvals",
|
||||||
failed_run: "Failed runs",
|
failed_run: "Failed runs",
|
||||||
join_request: "Join requests",
|
join_request: "Join requests",
|
||||||
|
|||||||
@@ -125,13 +125,13 @@ function inferIssueDetailSource(
|
|||||||
if (isIssueDetailSource(state?.issueDetailSource)) return state.issueDetailSource;
|
if (isIssueDetailSource(state?.issueDetailSource)) return state.issueDetailSource;
|
||||||
if (!breadcrumb) return null;
|
if (!breadcrumb) return null;
|
||||||
if (breadcrumb.label === "Inbox" || breadcrumb.href.includes("/inbox")) return "inbox";
|
if (breadcrumb.label === "Inbox" || breadcrumb.href.includes("/inbox")) return "inbox";
|
||||||
if (breadcrumb.label === "Issues" || breadcrumb.href.includes("/issues")) return "issues";
|
if (breadcrumb.label === "Tasks" || breadcrumb.href.includes("/issues")) return "issues";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function breadcrumbForSource(source: IssueDetailSource): IssueDetailBreadcrumb {
|
function breadcrumbForSource(source: IssueDetailSource): IssueDetailBreadcrumb {
|
||||||
if (source === "inbox") return { label: "Inbox", href: "/inbox" };
|
if (source === "inbox") return { label: "Inbox", href: "/inbox" };
|
||||||
return { label: "Issues", href: "/issues" };
|
return { label: "Tasks", href: "/issues" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createIssueDetailLocationState(
|
export function createIssueDetailLocationState(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type RunRetryStateSummary = {
|
|||||||
|
|
||||||
const RETRY_REASON_LABELS: Record<string, string> = {
|
const RETRY_REASON_LABELS: Record<string, string> = {
|
||||||
transient_failure: "Transient failure",
|
transient_failure: "Transient failure",
|
||||||
missing_issue_comment: "Missing issue comment",
|
missing_issue_comment: "Missing task comment",
|
||||||
process_lost: "Process lost",
|
process_lost: "Process lost",
|
||||||
assignment_recovery: "Assignment recovery",
|
assignment_recovery: "Assignment recovery",
|
||||||
issue_continuation_needed: "Continuation needed",
|
issue_continuation_needed: "Continuation needed",
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ function mapMetadataRow(
|
|||||||
case "issue_link": {
|
case "issue_link": {
|
||||||
const identifier = row.identifier ?? null;
|
const identifier = row.identifier ?? null;
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
return { kind: "text", label: metadataRowText(row, "Issue"), value: row.title ?? "unknown" };
|
return { kind: "text", label: metadataRowText(row, "Task"), value: row.title ?? "unknown" };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
kind: "issue",
|
kind: "issue",
|
||||||
label: metadataRowText(row, "Issue"),
|
label: metadataRowText(row, "Task"),
|
||||||
identifier,
|
identifier,
|
||||||
href: `/issues/${identifier}`,
|
href: `/issues/${identifier}`,
|
||||||
title: row.title ?? undefined,
|
title: row.title ?? undefined,
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ export function ApprovalDetail() {
|
|||||||
? {
|
? {
|
||||||
label:
|
label:
|
||||||
(linkedIssues?.length ?? 0) > 1
|
(linkedIssues?.length ?? 0) > 1
|
||||||
? "Review linked issues"
|
? "Review linked tasks"
|
||||||
: "Review linked issue",
|
: "Review linked task",
|
||||||
to: `/issues/${primaryLinkedIssue.identifier ?? primaryLinkedIssue.id}`,
|
to: `/issues/${primaryLinkedIssue.identifier ?? primaryLinkedIssue.id}`,
|
||||||
}
|
}
|
||||||
: linkedAgentId
|
: linkedAgentId
|
||||||
@@ -183,7 +183,7 @@ export function ApprovalDetail() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-green-800 dark:text-green-100 font-medium">Approval confirmed</p>
|
<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">
|
<p className="text-xs text-green-700 dark:text-green-200/90">
|
||||||
Requesting agent was notified to review this approval and linked issues.
|
Requesting agent was notified to review this approval and linked tasks.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +240,7 @@ export function ApprovalDetail() {
|
|||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
{linkedIssues && linkedIssues.length > 0 && (
|
{linkedIssues && linkedIssues.length > 0 && (
|
||||||
<div className="pt-2 border-t border-border/60">
|
<div className="pt-2 border-t border-border/60">
|
||||||
<p className="text-xs text-muted-foreground mb-1.5">Linked Issues</p>
|
<p className="text-xs text-muted-foreground mb-1.5">Linked Tasks</p>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{linkedIssues.map((issue) => (
|
{linkedIssues.map((issue) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -256,7 +256,7 @@ export function ApprovalDetail() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-muted-foreground mt-2">
|
<p className="text-[11px] text-muted-foreground mt-2">
|
||||||
Linked issues remain open until the requesting agent follows up and closes them.
|
Linked tasks remain open until the requesting agent follows up and closes them.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function Companies() {
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CircleDot className="h-3.5 w-3.5" />
|
<CircleDot className="h-3.5 w-3.5" />
|
||||||
<span>
|
<span>
|
||||||
{issueCount} {issueCount === 1 ? "issue" : "issues"}
|
{issueCount} {issueCount === 1 ? "task" : "tasks"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 tabular-nums">
|
<div className="flex items-center gap-1.5 tabular-nums">
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export function CompanyAccess() {
|
|||||||
title: "Member removed",
|
title: "Member removed",
|
||||||
body:
|
body:
|
||||||
result.reassignedIssueCount > 0
|
result.reassignedIssueCount > 0
|
||||||
? `${result.reassignedIssueCount} assigned issue${result.reassignedIssueCount === 1 ? "" : "s"} cleaned up.`
|
? `${result.reassignedIssueCount} assigned task${result.reassignedIssueCount === 1 ? "" : "s"} cleaned up.`
|
||||||
: undefined,
|
: undefined,
|
||||||
tone: "success",
|
tone: "success",
|
||||||
});
|
});
|
||||||
@@ -449,14 +449,14 @@ export function CompanyAccess() {
|
|||||||
<div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div>
|
<div className="text-sm text-muted-foreground">{removingMember.user?.email || removingMember.principalId}</div>
|
||||||
<div className="mt-2 text-sm text-muted-foreground">
|
<div className="mt-2 text-sm text-muted-foreground">
|
||||||
{assignedIssuesQuery.isLoading
|
{assignedIssuesQuery.isLoading
|
||||||
? "Checking assigned issues..."
|
? "Checking assigned tasks..."
|
||||||
: `${assignedIssues.length} open assigned issue${assignedIssues.length === 1 ? "" : "s"}`}
|
: `${assignedIssues.length} open assigned task${assignedIssues.length === 1 ? "" : "s"}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assignedIssues.length > 0 ? (
|
{assignedIssues.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium">Issue reassignment</div>
|
<div className="text-sm font-medium">Task reassignment</div>
|
||||||
<select
|
<select
|
||||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||||
value={reassignmentTarget}
|
value={reassignmentTarget}
|
||||||
@@ -491,7 +491,7 @@ export function CompanyAccess() {
|
|||||||
))}
|
))}
|
||||||
{assignedIssues.length > 6 ? (
|
{assignedIssues.length > 6 ? (
|
||||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
<div className="px-3 py-2 text-sm text-muted-foreground">
|
||||||
{assignedIssues.length - 6} more issue{assignedIssues.length - 6 === 1 ? "" : "s"}
|
{assignedIssues.length - 6} more task{assignedIssues.length - 6 === 1 ? "" : "s"}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ export function CompanyEnvironments() {
|
|||||||
<h1 className="text-lg font-semibold">Company Environments</h1>
|
<h1 className="text-lg font-semibold">Company Environments</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="max-w-3xl text-sm text-muted-foreground">
|
<p className="max-w-3xl text-sm text-muted-foreground">
|
||||||
Define reusable execution targets for projects, issue workspaces, and remote-capable adapters.
|
Define reusable execution targets for projects, task workspaces, and remote-capable adapters.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1005,7 +1005,7 @@ export function CompanyExport() {
|
|||||||
onClick={() => setTaskLimit((prev) => prev + TASKS_PAGE_SIZE)}
|
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"
|
className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
Show more issues ({visibleTaskChildren} of {totalTaskChildren})
|
Show more tasks ({visibleTaskChildren} of {totalTaskChildren})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ export function Costs() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="px-5 pt-5 pb-2">
|
<CardHeader className="px-5 pt-5 pb-2">
|
||||||
<CardTitle className="text-base">By project</CardTitle>
|
<CardTitle className="text-base">By project</CardTitle>
|
||||||
<CardDescription>Run costs attributed through project-linked issues.</CardDescription>
|
<CardDescription>Run costs attributed through project-linked tasks.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 px-5 pb-5 pt-2">
|
<CardContent className="space-y-2 px-5 pb-5 pt-2">
|
||||||
{(spendData?.byProject.length ?? 0) === 0 ? (
|
{(spendData?.byProject.length ?? 0) === 0 ? (
|
||||||
|
|||||||
@@ -295,10 +295,10 @@ export function Dashboard() {
|
|||||||
<ChartCard title="Run Activity" subtitle="Last 14 days">
|
<ChartCard title="Run Activity" subtitle="Last 14 days">
|
||||||
<RunActivityChart activity={data.runActivity} />
|
<RunActivityChart activity={data.runActivity} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
<ChartCard title="Issues by Priority" subtitle="Last 14 days">
|
<ChartCard title="Tasks by Priority" subtitle="Last 14 days">
|
||||||
<PriorityChart issues={issues ?? []} />
|
<PriorityChart issues={issues ?? []} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
<ChartCard title="Issues by Status" subtitle="Last 14 days">
|
<ChartCard title="Tasks by Status" subtitle="Last 14 days">
|
||||||
<IssueStatusChart issues={issues ?? []} />
|
<IssueStatusChart issues={issues ?? []} />
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
<ChartCard title="Success Rate" subtitle="Last 14 days">
|
<ChartCard title="Success Rate" subtitle="Last 14 days">
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ export function DesignGuide() {
|
|||||||
|
|
||||||
<SubSection title="With icons">
|
<SubSection title="With icons">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Button><Plus /> New Issue</Button>
|
<Button><Plus /> New Task</Button>
|
||||||
<Button variant="outline"><Upload /> Upload</Button>
|
<Button variant="outline"><Upload /> Upload</Button>
|
||||||
<Button variant="destructive"><Trash2 /> Delete</Button>
|
<Button variant="destructive"><Trash2 /> Delete</Button>
|
||||||
<Button size="sm"><Plus /> Add</Button>
|
<Button size="sm"><Plus /> Add</Button>
|
||||||
@@ -727,11 +727,11 @@ export function DesignGuide() {
|
|||||||
checked={menuChecked}
|
checked={menuChecked}
|
||||||
onCheckedChange={(value) => setMenuChecked(value === true)}
|
onCheckedChange={(value) => setMenuChecked(value === true)}
|
||||||
>
|
>
|
||||||
Watch issue
|
Watch task
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
<DropdownMenuItem variant="destructive">
|
<DropdownMenuItem variant="destructive">
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
Delete issue
|
Delete task
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -784,7 +784,7 @@ export function DesignGuide() {
|
|||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="right">
|
<SheetContent side="right">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>Issue Properties</SheetTitle>
|
<SheetTitle>Task Properties</SheetTitle>
|
||||||
<SheetDescription>Edit metadata without leaving the current page.</SheetDescription>
|
<SheetDescription>Edit metadata without leaving the current page.</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="space-y-4 px-4">
|
<div className="space-y-4 px-4">
|
||||||
@@ -836,7 +836,7 @@ export function DesignGuide() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem>
|
<CommandItem>
|
||||||
<CircleDot className="h-4 w-4" />
|
<CircleDot className="h-4 w-4" />
|
||||||
Issues
|
Tasks
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
@@ -847,7 +847,7 @@ export function DesignGuide() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem>
|
<CommandItem>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Create new issue
|
Create new task
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
@@ -870,7 +870,7 @@ export function DesignGuide() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>Issue List</BreadcrumbPage>
|
<BreadcrumbPage>Task List</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@@ -899,7 +899,7 @@ export function DesignGuide() {
|
|||||||
<SubSection title="Metric Cards">
|
<SubSection title="Metric Cards">
|
||||||
<div className="grid md:grid-cols-2 xl:grid-cols-4 gap-4">
|
<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={Bot} value={12} label="Active Agents" description="+3 this week" />
|
||||||
<MetricCard icon={CircleDot} value={48} label="Open Issues" />
|
<MetricCard icon={CircleDot} value={48} label="Open Tasks" />
|
||||||
<MetricCard icon={DollarSign} value="$1,234" label="Monthly Cost" description="Under budget" />
|
<MetricCard icon={DollarSign} value="$1,234" label="Monthly Cost" description="Under budget" />
|
||||||
<MetricCard icon={Zap} value="99.9%" label="Uptime" />
|
<MetricCard icon={Zap} value="99.9%" label="Uptime" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1298,7 +1298,7 @@ export function DesignGuide() {
|
|||||||
</div>
|
</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">
|
<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" />
|
<CircleDot className="h-4 w-4" />
|
||||||
Issues
|
Tasks
|
||||||
<span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-1.5 py-0.5">
|
<span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-1.5 py-0.5">
|
||||||
12
|
12
|
||||||
</span>
|
</span>
|
||||||
@@ -1331,7 +1331,7 @@ export function DesignGuide() {
|
|||||||
{/* ============================================================ */}
|
{/* ============================================================ */}
|
||||||
{/* GROUPED LIST (Issues pattern) */}
|
{/* GROUPED LIST (Issues pattern) */}
|
||||||
{/* ============================================================ */}
|
{/* ============================================================ */}
|
||||||
<Section title="Grouped List (Issues pattern)">
|
<Section title="Grouped List (Tasks pattern)">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-muted/50 rounded-t-md">
|
<div className="flex items-center gap-2 px-4 py-2 bg-muted/50 rounded-t-md">
|
||||||
<StatusIcon status="in_progress" />
|
<StatusIcon status="in_progress" />
|
||||||
@@ -1588,7 +1588,7 @@ export function DesignGuide() {
|
|||||||
<div className="border border-border rounded-md divide-y divide-border text-sm">
|
<div className="border border-border rounded-md divide-y divide-border text-sm">
|
||||||
{[
|
{[
|
||||||
["Cmd+K / Ctrl+K", "Open Command Palette"],
|
["Cmd+K / Ctrl+K", "Open Command Palette"],
|
||||||
["C", "New Issue (outside inputs)"],
|
["C", "New Task (outside inputs)"],
|
||||||
["[", "Toggle Sidebar"],
|
["[", "Toggle Sidebar"],
|
||||||
["]", "Toggle Properties Panel"],
|
["]", "Toggle Properties Panel"],
|
||||||
|
|
||||||
@@ -1604,7 +1604,7 @@ export function DesignGuide() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Issue Output Surface">
|
<Section title="Task Output Surface">
|
||||||
<SubSection title="Multiple outputs (primary video + 'Also produced')">
|
<SubSection title="Multiple outputs (primary video + 'Also produced')">
|
||||||
<IssueOutputSection workProducts={DESIGN_GUIDE_OUTPUTS} />
|
<IssueOutputSection workProducts={DESIGN_GUIDE_OUTPUTS} />
|
||||||
</SubSection>
|
</SubSection>
|
||||||
@@ -1613,7 +1613,7 @@ export function DesignGuide() {
|
|||||||
</SubSection>
|
</SubSection>
|
||||||
<SubSection title="Empty state">
|
<SubSection title="Empty state">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
When an issue has produced no artifact work products, the Output section renders nothing
|
When a task has produced no artifact work products, the Output section renders nothing
|
||||||
at all (no placeholder card).
|
at all (no placeholder card).
|
||||||
</p>
|
</p>
|
||||||
</SubSection>
|
</SubSection>
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ describe("ExecutionWorkspaceDetail plugin slots", () => {
|
|||||||
await render();
|
await render();
|
||||||
|
|
||||||
expect(container.textContent).toContain("Workspace plugin tab is not available.");
|
expect(container.textContent).toContain("Workspace plugin tab is not available.");
|
||||||
expect(container.querySelector('a[href="/execution-workspaces/workspace-1/issues"]')?.textContent).toBe("Back to issues");
|
expect(container.querySelector('a[href="/execution-workspaces/workspace-1/issues"]')?.textContent).toBe("Back to tasks");
|
||||||
expect(container.textContent).not.toContain("Workspace routines");
|
expect(container.textContent).not.toContain("Workspace routines");
|
||||||
expect(container.querySelector('[data-testid="plugin-slot-mount"]')).toBeNull();
|
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);
|
const tabLabels = Array.from(container.querySelectorAll("[data-tab-value]")).map((tab) => tab.textContent);
|
||||||
expect(tabLabels).toEqual([
|
expect(tabLabels).toEqual([
|
||||||
"Issues",
|
"Tasks",
|
||||||
"Services",
|
"Services",
|
||||||
"Changes",
|
"Changes",
|
||||||
"Configuration",
|
"Configuration",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ type OrderedExecutionWorkspaceTabItem = {
|
|||||||
|
|
||||||
const DEFAULT_PLUGIN_DETAIL_TAB_ORDER = 100;
|
const DEFAULT_PLUGIN_DETAIL_TAB_ORDER = 100;
|
||||||
const EXECUTION_WORKSPACE_BASE_TAB_ITEMS: OrderedExecutionWorkspaceTabItem[] = [
|
const EXECUTION_WORKSPACE_BASE_TAB_ITEMS: OrderedExecutionWorkspaceTabItem[] = [
|
||||||
{ value: "issues", label: "Issues", order: 10 },
|
{ value: "issues", label: "Tasks", order: 10 },
|
||||||
{ value: "services", label: "Services", order: 20 },
|
{ value: "services", label: "Services", order: 20 },
|
||||||
{ value: "configuration", label: "Configuration", order: 30 },
|
{ value: "configuration", label: "Configuration", order: 30 },
|
||||||
{ value: "runtime_logs", label: "Runtime logs", order: 40 },
|
{ value: "runtime_logs", label: "Runtime logs", order: 40 },
|
||||||
@@ -1077,7 +1077,7 @@ export function ExecutionWorkspaceDetail() {
|
|||||||
"None"
|
"None"
|
||||||
)}
|
)}
|
||||||
</DetailRow>
|
</DetailRow>
|
||||||
<DetailRow label="Source issue">
|
<DetailRow label="Source task">
|
||||||
{sourceIssue ? (
|
{sourceIssue ? (
|
||||||
<Link to={issueUrl(sourceIssue)} className="hover:underline">
|
<Link to={issueUrl(sourceIssue)} className="hover:underline">
|
||||||
{sourceIssue.identifier ?? sourceIssue.id} · {sourceIssue.title}
|
{sourceIssue.identifier ?? sourceIssue.id} · {sourceIssue.title}
|
||||||
@@ -1218,7 +1218,7 @@ export function ExecutionWorkspaceDetail() {
|
|||||||
) : isExecutionWorkspacePluginTab(activeTab) ? (
|
) : isExecutionWorkspacePluginTab(activeTab) ? (
|
||||||
<MissingPluginTabPlaceholder
|
<MissingPluginTabPlaceholder
|
||||||
defaultTabHref={executionWorkspaceTabPath(workspace.id, "issues")}
|
defaultTabHref={executionWorkspaceTabPath(workspace.id, "issues")}
|
||||||
defaultTabLabel="Back to issues"
|
defaultTabLabel="Back to tasks"
|
||||||
/>
|
/>
|
||||||
) : activeTab === "routines" ? (
|
) : activeTab === "routines" ? (
|
||||||
<ExecutionWorkspaceRoutinesList
|
<ExecutionWorkspaceRoutinesList
|
||||||
|
|||||||
@@ -1495,7 +1495,7 @@ export function Inbox() {
|
|||||||
return { previousData };
|
return { previousData };
|
||||||
},
|
},
|
||||||
onError: (err, id, context) => {
|
onError: (err, id, context) => {
|
||||||
setActionError(err instanceof Error ? err.message : "Failed to archive issue");
|
setActionError(err instanceof Error ? err.message : "Failed to archive task");
|
||||||
setArchivingIssueIds((prev) => {
|
setArchivingIssueIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.delete(id);
|
next.delete(id);
|
||||||
@@ -2208,7 +2208,7 @@ export function Inbox() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="everything">All categories</SelectItem>
|
<SelectItem value="everything">All categories</SelectItem>
|
||||||
<SelectItem value="issues_i_touched">My recent issues</SelectItem>
|
<SelectItem value="issues_i_touched">My recent tasks</SelectItem>
|
||||||
<SelectItem value="join_requests">Join requests</SelectItem>
|
<SelectItem value="join_requests">Join requests</SelectItem>
|
||||||
<SelectItem value="approvals">Approvals</SelectItem>
|
<SelectItem value="approvals">Approvals</SelectItem>
|
||||||
<SelectItem value="failed_runs">Failed runs</SelectItem>
|
<SelectItem value="failed_runs">Failed runs</SelectItem>
|
||||||
@@ -2454,8 +2454,8 @@ export function Inbox() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
className="-mr-2 text-muted-foreground"
|
className="-mr-2 text-muted-foreground"
|
||||||
title={`New issue in ${group.label}`}
|
title={`New task in ${group.label}`}
|
||||||
aria-label={`New issue in ${group.label}`}
|
aria-label={`New task in ${group.label}`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openCreateIssueForGroup(group);
|
openCreateIssueForGroup(group);
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ export function InstanceExperimentalSettings() {
|
|||||||
<h2 className="text-sm font-semibold">Enable Isolated Workspaces</h2>
|
<h2 className="text-sm font-semibold">Enable Isolated Workspaces</h2>
|
||||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||||
Show execution workspace controls in project configuration and allow isolated workspace behavior for new
|
Show execution workspace controls in project configuration and allow isolated workspace behavior for new
|
||||||
and existing issue runs.
|
and existing task runs.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -328,9 +328,9 @@ export function InstanceExperimentalSettings() {
|
|||||||
<section className="rounded-xl border border-border bg-card p-5">
|
<section className="rounded-xl border border-border bg-card p-5">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<h2 className="text-sm font-semibold">Issue Plan Decomposition Panel</h2>
|
<h2 className="text-sm font-semibold">Task Plan Decomposition Panel</h2>
|
||||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||||
Show accepted-plan decomposition history on issue detail pages. Intended for debugging and validating
|
Show accepted-plan decomposition history on task detail pages. Intended for debugging and validating
|
||||||
subtask creation behavior while the presentation is still being refined.
|
subtask creation behavior while the presentation is still being refined.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,7 +342,7 @@ export function InstanceExperimentalSettings() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={toggleMutation.isPending}
|
disabled={toggleMutation.isPending}
|
||||||
aria-label="Toggle issue plan decomposition panel experimental setting"
|
aria-label="Toggle task plan decomposition panel experimental setting"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -387,9 +387,9 @@ export function InstanceExperimentalSettings() {
|
|||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<h2 className="text-sm font-semibold">Auto-Create Issue Recovery Tasks</h2>
|
<h2 className="text-sm font-semibold">Auto-Create Recovery Tasks</h2>
|
||||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||||
Let the heartbeat scheduler create recovery issues for issue dependency chains found inside the
|
Let the heartbeat scheduler create recovery tasks for task dependency chains found inside the
|
||||||
configured lookback window.
|
configured lookback window.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,7 +403,7 @@ export function InstanceExperimentalSettings() {
|
|||||||
previewForEnable();
|
previewForEnable();
|
||||||
}}
|
}}
|
||||||
disabled={recoveryActionPending}
|
disabled={recoveryActionPending}
|
||||||
aria-label="Toggle issue graph liveness auto-recovery"
|
aria-label="Toggle task graph liveness auto-recovery"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function InstanceGeneralSettings() {
|
|||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<h2 className="text-sm font-semibold">Keyboard shortcuts</h2>
|
<h2 className="text-sm font-semibold">Keyboard shortcuts</h2>
|
||||||
<p className="max-w-2xl text-sm text-muted-foreground">
|
<p className="max-w-2xl text-sm text-muted-foreground">
|
||||||
Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating issues or
|
Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating tasks or
|
||||||
toggling panels. This is off by default.
|
toggling panels. This is off by default.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1028,7 +1028,7 @@ describe("IssueDetail", () => {
|
|||||||
|
|
||||||
expect(container.textContent).toContain("Plan decomposition");
|
expect(container.textContent).toContain("Plan decomposition");
|
||||||
expect(container.textContent).toContain("Plan revision 2");
|
expect(container.textContent).toContain("Plan revision 2");
|
||||||
expect(container.textContent).toContain("2 of 2 child issues created");
|
expect(container.textContent).toContain("2 of 2 child tasks created");
|
||||||
expect(container.textContent).toContain("First child issue");
|
expect(container.textContent).toContain("First child issue");
|
||||||
expect(mockIssuesApi.listAcceptedPlanDecompositions).toHaveBeenCalledWith("issue-1");
|
expect(mockIssuesApi.listAcceptedPlanDecompositions).toHaveBeenCalledWith("issue-1");
|
||||||
});
|
});
|
||||||
@@ -1081,8 +1081,8 @@ describe("IssueDetail", () => {
|
|||||||
parentId: "parent-1",
|
parentId: "parent-1",
|
||||||
includeBlockedBy: true,
|
includeBlockedBy: true,
|
||||||
});
|
});
|
||||||
expect(container.querySelector('a[aria-label="Previous sub-issue: PAP-1 - Previous sibling"]')).toBeTruthy();
|
expect(container.querySelector('a[aria-label="Previous sub-task: PAP-1 - Previous sibling"]')).toBeTruthy();
|
||||||
expect(container.querySelector('a[aria-label="Next sub-issue: PAP-3 - Next sibling"]')).toBeTruthy();
|
expect(container.querySelector('a[aria-label="Next sub-task: PAP-3 - Next sibling"]')).toBeTruthy();
|
||||||
expect(container.textContent).toContain("Previous");
|
expect(container.textContent).toContain("Previous");
|
||||||
expect(container.textContent).toContain("Previous sibling");
|
expect(container.textContent).toContain("Previous sibling");
|
||||||
expect(container.textContent).toContain("Next");
|
expect(container.textContent).toContain("Next");
|
||||||
@@ -1137,7 +1137,7 @@ describe("IssueDetail", () => {
|
|||||||
descendantOf: "issue-parent",
|
descendantOf: "issue-parent",
|
||||||
includeBlockedBy: true,
|
includeBlockedBy: true,
|
||||||
});
|
});
|
||||||
expect(container.querySelector('a[aria-label="Next sub-issue: PAP-11 - First child"]')).toBeTruthy();
|
expect(container.querySelector('a[aria-label="Next sub-task: PAP-11 - First child"]')).toBeTruthy();
|
||||||
expect(container.textContent).toContain("Next");
|
expect(container.textContent).toContain("Next");
|
||||||
expect(container.textContent).toContain("First child");
|
expect(container.textContent).toContain("First child");
|
||||||
expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0].footer).toBeTruthy();
|
expect(mockIssueChatThreadRender.mock.calls.at(-1)?.[0].footer).toBeTruthy();
|
||||||
@@ -1314,7 +1314,7 @@ describe("IssueDetail", () => {
|
|||||||
await flushReact();
|
await flushReact();
|
||||||
await flushReact();
|
await flushReact();
|
||||||
|
|
||||||
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
|
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
|
||||||
expect(moreButton).toBeTruthy();
|
expect(moreButton).toBeTruthy();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -1424,7 +1424,7 @@ describe("IssueDetail", () => {
|
|||||||
metadata: { source: "issue_active_run_control", runId: "run-active-1" },
|
metadata: { source: "issue_active_run_control", runId: "run-active-1" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
|
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
|
||||||
expect(moreButton).toBeTruthy();
|
expect(moreButton).toBeTruthy();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
moreButton!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
moreButton!.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||||
@@ -1690,7 +1690,7 @@ describe("IssueDetail", () => {
|
|||||||
await flushReact();
|
await flushReact();
|
||||||
await flushReact();
|
await flushReact();
|
||||||
|
|
||||||
const moreButton = container.querySelector('button[aria-label="More issue actions"]') as HTMLButtonElement | null;
|
const moreButton = container.querySelector('button[aria-label="More task actions"]') as HTMLButtonElement | null;
|
||||||
expect(moreButton).toBeTruthy();
|
expect(moreButton).toBeTruthy();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -1712,11 +1712,11 @@ describe("IssueDetail", () => {
|
|||||||
mode: "restore",
|
mode: "restore",
|
||||||
releasePolicy: { strategy: "manual" },
|
releasePolicy: { strategy: "manual" },
|
||||||
});
|
});
|
||||||
expect(container.textContent).toContain("Restore issues cancelled by this subtree operation so work can resume.");
|
expect(container.textContent).toContain("Restore tasks cancelled by this subtree operation so work can resume.");
|
||||||
expect(container.textContent).toContain("Cancelled child");
|
expect(container.textContent).toContain("Cancelled child");
|
||||||
|
|
||||||
const restoreApplyButton = Array.from(container.querySelectorAll("button"))
|
const restoreApplyButton = Array.from(container.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.trim() === "Restore 1 issues");
|
.find((button) => button.textContent?.trim() === "Restore 1 tasks");
|
||||||
expect(restoreApplyButton).toBeTruthy();
|
expect(restoreApplyButton).toBeTruthy();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -1792,7 +1792,7 @@ describe("IssueDetail", () => {
|
|||||||
expect(bodyScrollRegion?.className).toContain("overscroll-contain");
|
expect(bodyScrollRegion?.className).toContain("overscroll-contain");
|
||||||
|
|
||||||
const cancelApplyButton = Array.from(dialogContent!.querySelectorAll("button"))
|
const cancelApplyButton = Array.from(dialogContent!.querySelectorAll("button"))
|
||||||
.find((button) => button.textContent?.trim() === "Cancel 24 issues") as HTMLButtonElement | undefined;
|
.find((button) => button.textContent?.trim() === "Cancel 24 tasks") as HTMLButtonElement | undefined;
|
||||||
expect(cancelApplyButton).toBeTruthy();
|
expect(cancelApplyButton).toBeTruthy();
|
||||||
expect(cancelApplyButton!.disabled).toBe(true);
|
expect(cancelApplyButton!.disabled).toBe(true);
|
||||||
|
|
||||||
|
|||||||
@@ -192,14 +192,14 @@ const LEAF_WORK_CONTROL_MODE_LABEL: Partial<Record<IssueTreeControlMode, string>
|
|||||||
resume: "Resume work",
|
resume: "Resume work",
|
||||||
};
|
};
|
||||||
const TREE_CONTROL_MODE_HELP_TEXT: Record<IssueTreeControlMode, string> = {
|
const TREE_CONTROL_MODE_HELP_TEXT: Record<IssueTreeControlMode, string> = {
|
||||||
pause: "Pause active execution in this issue subtree until an explicit resume.",
|
pause: "Pause active execution in this task subtree until an explicit resume.",
|
||||||
resume: "Release the active subtree pause hold so held work can continue.",
|
resume: "Release the active subtree pause hold so held work can continue.",
|
||||||
cancel: "Cancel non-terminal issues in this subtree and stop queued/running work where possible.",
|
cancel: "Cancel non-terminal tasks in this subtree and stop queued/running work where possible.",
|
||||||
restore: "Restore issues cancelled by this subtree operation so work can resume.",
|
restore: "Restore tasks cancelled by this subtree operation so work can resume.",
|
||||||
};
|
};
|
||||||
const LEAF_WORK_CONTROL_MODE_HELP_TEXT: Partial<Record<IssueTreeControlMode, string>> = {
|
const LEAF_WORK_CONTROL_MODE_HELP_TEXT: Partial<Record<IssueTreeControlMode, string>> = {
|
||||||
pause: "Pause active execution on this issue until an explicit resume.",
|
pause: "Pause active execution on this task until an explicit resume.",
|
||||||
resume: "Release the active pause hold so this issue can continue.",
|
resume: "Release the active pause hold so this task can continue.",
|
||||||
};
|
};
|
||||||
function issueTreeControlLabel(mode: IssueTreeControlMode, scope: "leaf" | "subtree") {
|
function issueTreeControlLabel(mode: IssueTreeControlMode, scope: "leaf" | "subtree") {
|
||||||
return scope === "leaf"
|
return scope === "leaf"
|
||||||
@@ -217,7 +217,7 @@ function treeControlPreviewErrorCopy(error: unknown): string {
|
|||||||
if (error instanceof ApiError) {
|
if (error instanceof ApiError) {
|
||||||
if (error.status === 403) return "Only board users can preview subtree controls.";
|
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 === 409) return "Preview is stale because subtree hold state changed. Retry to refresh.";
|
||||||
if (error.status === 422) return "This subtree action is currently invalid for the selected issues.";
|
if (error.status === 422) return "This subtree action is currently invalid for the selected tasks.";
|
||||||
}
|
}
|
||||||
return error instanceof Error ? error.message : "Unable to load preview.";
|
return error instanceof Error ? error.message : "Unable to load preview.";
|
||||||
}
|
}
|
||||||
@@ -606,7 +606,7 @@ function InboxMobileToolbar({
|
|||||||
onClick={() => { onHide(); setMenuOpen(false); }}
|
onClick={() => { onHide(); setMenuOpen(false); }}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-3 w-3" />
|
<EyeOff className="h-3 w-3" />
|
||||||
Hide this issue
|
Hide this task
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@@ -1122,7 +1122,7 @@ function IssueDetailActivityTab({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-1 text-xs text-muted-foreground tabular-nums">
|
<div className="space-y-1 text-xs text-muted-foreground tabular-nums">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<span className="font-medium text-foreground">This issue</span>
|
<span className="font-medium text-foreground">This task</span>
|
||||||
{issueCostSummary.hasCost ? (
|
{issueCostSummary.hasCost ? (
|
||||||
<span className="font-medium text-foreground">
|
<span className="font-medium text-foreground">
|
||||||
${issueCostSummary.cost.toFixed(4)}
|
${issueCostSummary.cost.toFixed(4)}
|
||||||
@@ -1149,7 +1149,7 @@ function IssueDetailActivityTab({
|
|||||||
{hasIssueTreeCost && issueTreeCostSummary ? (
|
{hasIssueTreeCost && issueTreeCostSummary ? (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<span className="font-medium text-foreground">
|
<span className="font-medium text-foreground">
|
||||||
Including sub-issues {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
|
Including sub-tasks {(issueTreeCostSummary.costCents / 100).toLocaleString(undefined, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
minimumFractionDigits: 4,
|
minimumFractionDigits: 4,
|
||||||
@@ -1168,7 +1168,7 @@ function IssueDetailActivityTab({
|
|||||||
{` (${issueTreeCostSummary.runCount} run${issueTreeCostSummary.runCount === 1 ? "" : "s"})`}
|
{` (${issueTreeCostSummary.runCount} run${issueTreeCostSummary.runCount === 1 ? "" : "s"})`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span>{issueTreeCostSummary.issueCount} issue{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
|
<span>{issueTreeCostSummary.issueCount} task{issueTreeCostSummary.issueCount === 1 ? "" : "s"}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -1382,7 +1382,7 @@ export function IssueDetail() {
|
|||||||
}
|
}
|
||||||
}, [hasLiveRuns, locallyQueuedCommentRunIds.size]);
|
}, [hasLiveRuns, locallyQueuedCommentRunIds.size]);
|
||||||
const sourceBreadcrumb = useMemo(
|
const sourceBreadcrumb = useMemo(
|
||||||
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Issues", href: "/issues" },
|
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Tasks", href: "/issues" },
|
||||||
[issueId, location.state, location.search],
|
[issueId, location.state, location.search],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1633,7 +1633,7 @@ export function IssueDetail() {
|
|||||||
() => mergeIssueComments(comments ?? [], optimisticComments),
|
() => mergeIssueComments(comments ?? [], optimisticComments),
|
||||||
[comments, optimisticComments],
|
[comments, optimisticComments],
|
||||||
);
|
);
|
||||||
const breadcrumbTitle = issue?.title ?? issueId ?? "Issue";
|
const breadcrumbTitle = issue?.title ?? issueId ?? "Task";
|
||||||
const issueCacheRefs = useMemo(() => {
|
const issueCacheRefs = useMemo(() => {
|
||||||
const refs = new Set<string>();
|
const refs = new Set<string>();
|
||||||
if (issueId) refs.add(issueId);
|
if (issueId) refs.add(issueId);
|
||||||
@@ -1808,8 +1808,8 @@ export function IssueDetail() {
|
|||||||
queryClient.setQueryData(queryKeys.issues.list(context.selectedCompanyId), context.previousList);
|
queryClient.setQueryData(queryKeys.issues.list(context.selectedCompanyId), context.previousList);
|
||||||
}
|
}
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Issue update failed",
|
title: "Task update failed",
|
||||||
body: err instanceof Error ? err.message : "Unable to save issue changes",
|
body: err instanceof Error ? err.message : "Unable to save task changes",
|
||||||
tone: "error",
|
tone: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1886,7 +1886,7 @@ export function IssueDetail() {
|
|||||||
? treeControlScope === "leaf" ? "Work paused" : "Subtree paused"
|
? treeControlScope === "leaf" ? "Work paused" : "Subtree paused"
|
||||||
: `${modeLabel} applied`,
|
: `${modeLabel} applied`,
|
||||||
body: result.kind === "release"
|
body: result.kind === "release"
|
||||||
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active issue pause released." : "Active subtree pause released."))
|
? (result.hold.releaseReason?.trim() || (treeControlScope === "leaf" ? "Active task pause released." : "Active subtree pause released."))
|
||||||
: result.hold.mode === "pause"
|
: result.hold.mode === "pause"
|
||||||
? treeControlScope === "leaf"
|
? treeControlScope === "leaf"
|
||||||
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
|
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
|
||||||
@@ -1945,7 +1945,7 @@ export function IssueDetail() {
|
|||||||
title: "Work paused",
|
title: "Work paused",
|
||||||
body: cancelCount > 0
|
body: cancelCount > 0
|
||||||
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
|
? `Work paused. ${cancelCount} run${cancelCount === 1 ? "" : "s"} cancelled.`
|
||||||
: "Work paused. This issue is held until resume.",
|
: "Work paused. This task is held until resume.",
|
||||||
tone: "success",
|
tone: "success",
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -1982,8 +1982,8 @@ export function IssueDetail() {
|
|||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Issue update failed",
|
title: "Task update failed",
|
||||||
body: err instanceof Error ? err.message : "Unable to save sub-issue changes",
|
body: err instanceof Error ? err.message : "Unable to save sub-task changes",
|
||||||
tone: "error",
|
tone: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -2666,12 +2666,12 @@ export function IssueDetail() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateIssueCollections();
|
invalidateIssueCollections();
|
||||||
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
|
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
|
||||||
pushToast({ title: "Issue archived from inbox", tone: "success" });
|
pushToast({ title: "Task archived from inbox", tone: "success" });
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Archive failed",
|
title: "Archive failed",
|
||||||
body: err instanceof Error ? err.message : "Unable to archive this issue from the inbox",
|
body: err instanceof Error ? err.message : "Unable to archive this task from the inbox",
|
||||||
tone: "error",
|
tone: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -3261,9 +3261,9 @@ export function IssueDetail() {
|
|||||||
? "Pause work"
|
? "Pause work"
|
||||||
: "Pause and stop work"
|
: "Pause and stop work"
|
||||||
: treeControlMode === "cancel"
|
: treeControlMode === "cancel"
|
||||||
? `Cancel ${previewAffectedIssueCount} issues`
|
? `Cancel ${previewAffectedIssueCount} tasks`
|
||||||
: treeControlMode === "restore"
|
: treeControlMode === "restore"
|
||||||
? `Restore ${previewAffectedIssueCount} issues`
|
? `Restore ${previewAffectedIssueCount} tasks`
|
||||||
: treeControlScope === "leaf"
|
: treeControlScope === "leaf"
|
||||||
? "Resume work"
|
? "Resume work"
|
||||||
: "Resume subtree";
|
: "Resume subtree";
|
||||||
@@ -3362,7 +3362,7 @@ export function IssueDetail() {
|
|||||||
{issue.hiddenAt && (
|
{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">
|
<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" />
|
<EyeOff className="h-4 w-4 shrink-0" />
|
||||||
This issue is hidden
|
This task is hidden
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activePauseHold && (
|
{activePauseHold && (
|
||||||
@@ -3375,13 +3375,13 @@ export function IssueDetail() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-amber-900/80 dark:text-amber-100/80">
|
<span className="text-xs text-amber-900/80 dark:text-amber-100/80">
|
||||||
{childIssues.length === 0
|
{childIssues.length === 0
|
||||||
? "Issue execution is held until resume. Human comments can still wake the assignee for triage."
|
? "Task execution is held until resume. Human comments can still wake the assignee for triage."
|
||||||
: "Root and descendant execution is held until resume. Human comments can still wake assignees for triage."}
|
: "Root and descendant execution is held until resume. Human comments can still wake assignees for triage."}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-amber-900/80 dark:text-amber-100/80">
|
<div className="text-xs text-amber-900/80 dark:text-amber-100/80">
|
||||||
{childIssues.length === 0
|
{childIssues.length === 0
|
||||||
? "1 issue held"
|
? "1 task held"
|
||||||
: `${heldDescendantCount} descendant${heldDescendantCount === 1 ? "" : "s"} held`}
|
: `${heldDescendantCount} descendant${heldDescendantCount === 1 ? "" : "s"} held`}
|
||||||
{activeRootPauseHold?.createdAt ? ` · started ${relativeTime(activeRootPauseHold.createdAt)}` : ""}
|
{activeRootPauseHold?.createdAt ? ` · started ${relativeTime(activeRootPauseHold.createdAt)}` : ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -3427,7 +3427,7 @@ export function IssueDetail() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
This issue is paused by ancestor{" "}
|
This task is paused by ancestor{" "}
|
||||||
{activePauseHoldRoot?.identifier ? (
|
{activePauseHoldRoot?.identifier ? (
|
||||||
<Link to={createIssueDetailPath(activePauseHoldRoot.identifier)} className="underline">
|
<Link to={createIssueDetailPath(activePauseHoldRoot.identifier)} className="underline">
|
||||||
{activePauseHoldRoot.identifier}
|
{activePauseHoldRoot.identifier}
|
||||||
@@ -3435,7 +3435,7 @@ export function IssueDetail() {
|
|||||||
) : (
|
) : (
|
||||||
activePauseHold.rootIssueId.slice(0, 8)
|
activePauseHold.rootIssueId.slice(0, 8)
|
||||||
)}
|
)}
|
||||||
. Resume from the root issue to deliver deferred work.
|
. Resume from the root task to deliver deferred work.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -3491,7 +3491,7 @@ export function IssueDetail() {
|
|||||||
{issue.workMode === "planning" ? (
|
{issue.workMode === "planning" ? (
|
||||||
<span
|
<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"
|
className="inline-flex items-center rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-300 shrink-0"
|
||||||
title="This issue is in planning mode."
|
title="This task is in planning mode."
|
||||||
>
|
>
|
||||||
Planning
|
Planning
|
||||||
</span>
|
</span>
|
||||||
@@ -3550,7 +3550,7 @@ export function IssueDetail() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={copyIssueToClipboard}
|
onClick={copyIssueToClipboard}
|
||||||
title="Copy issue as markdown"
|
title="Copy task as markdown"
|
||||||
>
|
>
|
||||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3584,7 +3584,7 @@ export function IssueDetail() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={copyIssueToClipboard}
|
onClick={copyIssueToClipboard}
|
||||||
title="Copy issue as markdown"
|
title="Copy task as markdown"
|
||||||
>
|
>
|
||||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3607,8 +3607,8 @@ export function IssueDetail() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
aria-label="More issue actions"
|
aria-label="More task actions"
|
||||||
title="More issue actions"
|
title="More task actions"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -3716,7 +3716,7 @@ export function IssueDetail() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-3 w-3" />
|
<EyeOff className="h-3 w-3" />
|
||||||
Hide this Issue
|
Hide this task
|
||||||
</button>
|
</button>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -3793,7 +3793,7 @@ export function IssueDetail() {
|
|||||||
{showRichSubIssuesSection ? (
|
{showRichSubIssuesSection ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">Sub-issues</h3>
|
<h3 className="text-sm font-medium text-muted-foreground">Sub-tasks</h3>
|
||||||
</div>
|
</div>
|
||||||
<IssuesList
|
<IssuesList
|
||||||
issues={childIssues}
|
issues={childIssues}
|
||||||
@@ -3809,7 +3809,7 @@ export function IssueDetail() {
|
|||||||
searchFilters={{ descendantOf: issue.id, includeBlockedBy: true }}
|
searchFilters={{ descendantOf: issue.id, includeBlockedBy: true }}
|
||||||
searchWithinLoadedIssues
|
searchWithinLoadedIssues
|
||||||
baseCreateIssueDefaults={buildSubIssueDefaultsForViewer(issue, currentUserId)}
|
baseCreateIssueDefaults={buildSubIssueDefaultsForViewer(issue, currentUserId)}
|
||||||
createIssueLabel="Sub-issue"
|
createIssueLabel="Sub-task"
|
||||||
defaultSortField="workflow"
|
defaultSortField="workflow"
|
||||||
showProgressSummary
|
showProgressSummary
|
||||||
parentIssueIdForCostSummary={issue.id}
|
parentIssueIdForCostSummary={issue.id}
|
||||||
@@ -3820,7 +3820,7 @@ export function IssueDetail() {
|
|||||||
<div className="flex flex-wrap items-center justify-end gap-2 min-w-0">
|
<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">
|
<Button variant="outline" size="sm" onClick={openNewSubIssue} className="shrink-0 shadow-none">
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
New Sub-issue
|
New Sub-task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -4062,7 +4062,7 @@ export function IssueDetail() {
|
|||||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-6 py-4">
|
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-6 py-4">
|
||||||
{treeControlMode === "cancel" ? (
|
{treeControlMode === "cancel" ? (
|
||||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
|
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-xs text-destructive">
|
||||||
Cancelling a subtree is destructive. Non-terminal issues will be marked cancelled, and running or queued work will be interrupted where possible.
|
Cancelling a subtree is destructive. Non-terminal tasks will be marked cancelled, and running or queued work will be interrupted where possible.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -4120,7 +4120,7 @@ export function IssueDetail() {
|
|||||||
checked={treeControlCancelConfirmed}
|
checked={treeControlCancelConfirmed}
|
||||||
onChange={(event) => setTreeControlCancelConfirmed(event.target.checked)}
|
onChange={(event) => setTreeControlCancelConfirmed(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
<span>I understand this will cancel {previewAffectedIssueCount} issues.</span>
|
<span>I understand this will cancel {previewAffectedIssueCount} tasks.</span>
|
||||||
</label>
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function Issues() {
|
|||||||
const issueLinkState = useMemo(
|
const issueLinkState = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createIssueDetailLocationState(
|
createIssueDetailLocationState(
|
||||||
"Issues",
|
"Tasks",
|
||||||
`${location.pathname}${location.search}${location.hash}`,
|
`${location.pathname}${location.search}${location.hash}`,
|
||||||
"issues",
|
"issues",
|
||||||
),
|
),
|
||||||
@@ -116,7 +116,7 @@ export function Issues() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbs([{ label: "Issues" }]);
|
setBreadcrumbs([{ label: "Tasks" }]);
|
||||||
}, [setBreadcrumbs]);
|
}, [setBreadcrumbs]);
|
||||||
|
|
||||||
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
|
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
|
||||||
@@ -175,7 +175,7 @@ export function Issues() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!selectedCompanyId) {
|
if (!selectedCompanyId) {
|
||||||
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
|
return <EmptyState icon={CircleDot} message="Select a company to view tasks." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function MyIssues() {
|
|||||||
const { setBreadcrumbs } = useBreadcrumbs();
|
const { setBreadcrumbs } = useBreadcrumbs();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBreadcrumbs([{ label: "My Issues" }]);
|
setBreadcrumbs([{ label: "My Tasks" }]);
|
||||||
}, [setBreadcrumbs]);
|
}, [setBreadcrumbs]);
|
||||||
|
|
||||||
const { data: issues, isLoading, error } = useQuery({
|
const { data: issues, isLoading, error } = useQuery({
|
||||||
@@ -27,7 +27,7 @@ export function MyIssues() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!selectedCompanyId) {
|
if (!selectedCompanyId) {
|
||||||
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
|
return <EmptyState icon={ListTodo} message="Select a company to view your tasks." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -44,7 +44,7 @@ export function MyIssues() {
|
|||||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
{myIssues.length === 0 && (
|
{myIssues.length === 0 && (
|
||||||
<EmptyState icon={ListTodo} message="No issues assigned to you." />
|
<EmptyState icon={ListTodo} message="No tasks assigned to you." />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{myIssues.length > 0 && (
|
{myIssues.length > 0 && (
|
||||||
|
|||||||
@@ -822,7 +822,7 @@ export function ProjectDetail() {
|
|||||||
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
|
<Tabs value={activeTab ?? "list"} onValueChange={(value) => handleTabChange(value as ProjectTab)}>
|
||||||
<PageTabBar
|
<PageTabBar
|
||||||
items={[
|
items={[
|
||||||
{ value: "list", label: "Issues" },
|
{ value: "list", label: "Tasks" },
|
||||||
{ value: "overview", label: "Overview" },
|
{ value: "overview", label: "Overview" },
|
||||||
...(project.managedByPlugin ? [{ value: "plugin-operations", label: "Plugin operations" }] : []),
|
...(project.managedByPlugin ? [{ value: "plugin-operations", label: "Plugin operations" }] : []),
|
||||||
...(showWorkspacesTab ? [{ value: "workspaces", label: "Workspaces" }] : []),
|
...(showWorkspacesTab ? [{ value: "workspaces", label: "Workspaces" }] : []),
|
||||||
|
|||||||
@@ -376,9 +376,9 @@ export function ProjectWorkspaceDetail() {
|
|||||||
request.action === "run"
|
request.action === "run"
|
||||||
? "Workspace job completed."
|
? "Workspace job completed."
|
||||||
: request.action === "stop"
|
: request.action === "stop"
|
||||||
? "Workspace service stopped. Issue execution is not paused."
|
? "Workspace service stopped. Task execution is not paused."
|
||||||
: request.action === "restart"
|
: request.action === "restart"
|
||||||
? "Workspace service restarted. Issue execution is not paused."
|
? "Workspace service restarted. Task execution is not paused."
|
||||||
: "Workspace service started.",
|
: "Workspace service started.",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1264,7 +1264,7 @@ export function RoutineDetail() {
|
|||||||
|
|
||||||
<TabsContent value="secrets" className="space-y-3">
|
<TabsContent value="secrets" className="space-y-3">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Routine secrets apply to every issue this routine creates. They override matching keys in
|
Routine secrets apply to every task this routine creates. They override matching keys in
|
||||||
project and agent env. <span className="font-mono">PAPERCLIP_*</span> variables are reserved.
|
project and agent env. <span className="font-mono">PAPERCLIP_*</span> variables are reserved.
|
||||||
</p>
|
</p>
|
||||||
<EnvVarEditor
|
<EnvVarEditor
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ export function Routines() {
|
|||||||
Routines
|
Routines
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Recurring work definitions that materialize into auditable execution issues.
|
Recurring work definitions that materialize into auditable execution tasks.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setComposerOpen(true)}>
|
<Button onClick={() => setComposerOpen(true)}>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const IDENTIFIER_PATTERN = /^[A-Z]+-\d+$/;
|
|||||||
|
|
||||||
const SCOPE_LABELS: Record<CompanySearchScope, string> = {
|
const SCOPE_LABELS: Record<CompanySearchScope, string> = {
|
||||||
all: "All",
|
all: "All",
|
||||||
issues: "Issues",
|
issues: "Tasks",
|
||||||
comments: "Comments",
|
comments: "Comments",
|
||||||
documents: "Documents",
|
documents: "Documents",
|
||||||
artifacts: "Artifacts",
|
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_ORDER: SubGroupKey[] = ["issues", "comments", "documents", "artifacts", "agents", "projects"];
|
||||||
|
|
||||||
const SUBGROUP_LABELS: Record<SubGroupKey, string> = {
|
const SUBGROUP_LABELS: Record<SubGroupKey, string> = {
|
||||||
issues: "Issues",
|
issues: "Tasks",
|
||||||
comments: "Comments",
|
comments: "Comments",
|
||||||
documents: "Documents",
|
documents: "Documents",
|
||||||
artifacts: "Artifacts",
|
artifacts: "Artifacts",
|
||||||
@@ -346,7 +346,7 @@ export function Search() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Search issues, comments, documents, agents, projects…"
|
placeholder="Search tasks, comments, documents, artifacts, agents, projects…"
|
||||||
aria-label="Search query"
|
aria-label="Search query"
|
||||||
className="h-10 pl-9 pr-20 text-sm"
|
className="h-10 pl-9 pr-20 text-sm"
|
||||||
/>
|
/>
|
||||||
@@ -456,7 +456,7 @@ function SearchTabContent({
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Type to search company memory.</h2>
|
<h2 className="text-lg font-semibold">Type to search company memory.</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Issues, comments, plan documents, agents, projects — same surface, ranked by relevance.
|
Tasks, comments, plan documents, artifacts, agents, projects — same surface, ranked by relevance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{recentSearches.length > 0 ? (
|
{recentSearches.length > 0 ? (
|
||||||
@@ -483,7 +483,7 @@ function SearchTabContent({
|
|||||||
<ul className="space-y-1 text-xs text-muted-foreground">
|
<ul className="space-y-1 text-xs text-muted-foreground">
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">Identifier lookup:</span> type{" "}
|
<span className="font-medium text-foreground">Identifier lookup:</span> type{" "}
|
||||||
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">PAP-123</code> to jump straight to an issue.
|
<code className="rounded bg-muted px-1 py-0.5 text-[11px]">PAP-123</code> to jump straight to a task.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="font-medium text-foreground">Quoted phrases:</span> wrap a phrase in quotes to match the
|
<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>
|
<div className="text-base font-semibold">Couldn’t run that search</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{status ? `The server returned ${status}.` : "The request failed."} Your input and filters are still here, so
|
{status ? `The server returned ${status}.` : "The request failed."} Your input and filters are still here, so
|
||||||
you can retry or fall back to the Issues filter.
|
you can retry or fall back to the Tasks filter.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||||
<Button onClick={refetch} variant="default" size="sm">
|
<Button onClick={refetch} variant="default" size="sm">
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={navigateIssuesFallback} variant="outline" size="sm">
|
<Button onClick={navigateIssuesFallback} variant="outline" size="sm">
|
||||||
Open Issues filter view
|
Open Tasks filter view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -561,10 +561,10 @@ function SearchTabContent({
|
|||||||
) : null}
|
) : null}
|
||||||
<Button onClick={openNewIssue} size="sm" variant="default">
|
<Button onClick={openNewIssue} size="sm" variant="default">
|
||||||
<Plus className="mr-1.5 h-4 w-4" />
|
<Plus className="mr-1.5 h-4 w-4" />
|
||||||
Create issue from this query
|
Create task from this query
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={navigateIssuesFallback} size="sm" variant="ghost">
|
<Button onClick={navigateIssuesFallback} size="sm" variant="ghost">
|
||||||
Open Issues filter view
|
Open Tasks filter view
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul className="mt-2 space-y-0.5 text-xs text-muted-foreground">
|
<ul className="mt-2 space-y-0.5 text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -1645,7 +1645,7 @@ function SecretsHowToUse() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Paperclip resolves the value server-side when the run starts and injects it as that env var. Project env
|
Paperclip resolves the value server-side when the run starts and injects it as that env var. Project env
|
||||||
applies to every issue in the project and overrides agent env on matching keys.
|
applies to every task in the project and overrides agent env on matching keys.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export function UserProfile() {
|
|||||||
(data?.topAgents ?? []).map((row) => ({
|
(data?.topAgents ?? []).map((row) => ({
|
||||||
key: row.agentId ?? "unknown",
|
key: row.agentId ?? "unknown",
|
||||||
label: row.agentName ?? (row.agentId ? row.agentId.slice(0, 8) : "unknown"),
|
label: row.agentName ?? (row.agentId ? row.agentId.slice(0, 8) : "unknown"),
|
||||||
sublabel: "Issue-linked usage",
|
sublabel: "Task-linked usage",
|
||||||
costCents: row.costCents,
|
costCents: row.costCents,
|
||||||
inputTokens: row.inputTokens,
|
inputTokens: row.inputTokens,
|
||||||
cachedInputTokens: row.cachedInputTokens,
|
cachedInputTokens: row.cachedInputTokens,
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ function PluginSdkIssuesList({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!companyId) {
|
if (!companyId) {
|
||||||
return createElement("div", { className: "text-sm text-muted-foreground" }, "Select a company to view issues.");
|
return createElement("div", { className: "text-sm text-muted-foreground" }, "Select a company to view tasks.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return createElement(HostIssuesList, {
|
return createElement(HostIssuesList, {
|
||||||
|
|||||||
Reference in New Issue
Block a user