diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx
index 7b573ef032c..cb981180726 100644
--- a/ui/src/components/IssueProperties.test.tsx
+++ b/ui/src/components/IssueProperties.test.tsx
@@ -18,6 +18,7 @@ import { getLastProjectId } from "../lib/recent-projects";
import { IssueProperties } from "./IssueProperties";
import { applyOptimisticIssueFieldUpdate } from "../lib/optimistic-issue-comments";
import { queryKeys } from "../lib/queryKeys";
+import { ApiError } from "../api/client";
const mockAgentsApi = vi.hoisted(() => ({
list: vi.fn(),
@@ -48,6 +49,10 @@ const mockIssuesApi = vi.hoisted(() => ({
unarchiveFromInbox: vi.fn(),
}));
+const mockRoutinesApi = vi.hoisted(() => ({
+ get: vi.fn(),
+}));
+
const mockAuthApi = vi.hoisted(() => ({
getSession: vi.fn(),
}));
@@ -90,6 +95,10 @@ vi.mock("../api/issues", () => ({
issuesApi: mockIssuesApi,
}));
+vi.mock("../api/routines", () => ({
+ routinesApi: mockRoutinesApi,
+}));
+
vi.mock("../api/auth", () => ({
authApi: mockAuthApi,
}));
@@ -628,6 +637,7 @@ describe("IssueProperties", () => {
document.body.appendChild(container);
mockAgentsApi.list.mockResolvedValue([]);
mockAgentsApi.adapterModels.mockResolvedValue([]);
+ mockRoutinesApi.get.mockReset();
mockProjectsApi.list.mockResolvedValue([]);
mockExecutionWorkspacesApi.list.mockResolvedValue([]);
mockExecutionWorkspacesApi.controlRuntimeCommands.mockReset();
@@ -1141,6 +1151,77 @@ describe("IssueProperties", () => {
act(() => root.unmount());
});
+ it("links a routine-created task back to its source routine under Relationships", async () => {
+ mockRoutinesApi.get.mockResolvedValue({ id: "routine-1", title: "Nightly triage" });
+ const root = renderProperties(container, {
+ issue: createIssue({ originKind: "routine_execution", originId: "routine-1" }),
+ childIssues: [],
+ onUpdate: vi.fn(),
+ inline: true,
+ });
+
+ await waitForAssertion(() => {
+ const row = container
+ .querySelector('[data-property-label="Routine"]')
+ ?.closest('[data-property-row="true"]');
+ const link = row?.querySelector("a");
+ expect(link?.getAttribute("href")).toBe("/routines/routine-1");
+ expect(link?.textContent).toContain("Nightly triage");
+ });
+ expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-1");
+
+ act(() => root.unmount());
+ });
+
+ it("omits the Routine row while the source routine is still loading", async () => {
+ mockRoutinesApi.get.mockReturnValue(new Promise(() => {}));
+ const root = renderProperties(container, {
+ issue: createIssue({ originKind: "routine_execution", originId: "routine-1" }),
+ childIssues: [],
+ onUpdate: vi.fn(),
+ inline: true,
+ });
+ await flush();
+
+ expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-1");
+ expect(container.querySelector('[data-property-label="Routine"]')).toBeNull();
+
+ act(() => root.unmount());
+ });
+
+ it("omits the Routine row when the source routine no longer exists", async () => {
+ mockRoutinesApi.get.mockRejectedValue(new ApiError("Not found", 404, null));
+ const root = renderProperties(container, {
+ issue: createIssue({ originKind: "routine_execution", originId: "routine-gone" }),
+ childIssues: [],
+ onUpdate: vi.fn(),
+ inline: true,
+ });
+
+ await waitForAssertion(() => {
+ expect(mockRoutinesApi.get).toHaveBeenCalledWith("routine-gone");
+ });
+ await flush();
+ expect(container.querySelector('[data-property-label="Routine"]')).toBeNull();
+
+ act(() => root.unmount());
+ });
+
+ it("omits the Routine row for tasks not created by a routine", async () => {
+ const root = renderProperties(container, {
+ issue: createIssue(),
+ childIssues: [],
+ onUpdate: vi.fn(),
+ inline: true,
+ });
+ await flush();
+
+ expect(container.querySelector('[data-property-label="Routine"]')).toBeNull();
+ expect(mockRoutinesApi.get).not.toHaveBeenCalled();
+
+ act(() => root.unmount());
+ });
+
it("groups the assignee picker and gates a live-run reassign behind an interrupt confirm", async () => {
const minimalAgent = (id: string, name: string) =>
({
diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx
index 736987aaa1a..924c7c8eb05 100644
--- a/ui/src/components/issue-properties/IssueProperties.tsx
+++ b/ui/src/components/issue-properties/IssueProperties.tsx
@@ -125,6 +125,7 @@ import {
import { issueReviewPolicyBadge } from "../../lib/review-policy";
import { IssueCasesPanel } from "../IssueCasesPanel";
import { ExpandRelationListButton, RemovableIssueReferencePill } from "./relation-controls";
+import { RoutineOriginRow } from "./routine-origin-row";
import { Badge } from "@/components/ui/badge";
import {
TaskDetailReferencesPanel,
@@ -2447,6 +2448,10 @@ export function IssueProperties({
{parentContent}
+ {issue.originKind === "routine_execution" && issue.originId ? (
+
+ ) : null}
+
{streamlinedPropertiesEnabled ? (
routinesApi.get(routineId),
+ });
+
+ const title = routine?.title?.trim();
+ if (!title) return null;
+
+ return (
+
+
+
+ {title}
+
+
+ );
+}