Address operator QoL PR feedback

This commit is contained in:
Dotta committed 2026-05-06 06:09:30 -05:00
1 parent 5901b13a1b
commit 6610a15435
5 files changed
+125 -79

No files matched your search

+19 -17
View File
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { Router, type Request, type Response } from "express";
import multer from "multer";
import { z } from "zod";
import { and, desc, eq, inArray, sql } from "drizzle-orm";
import { and, desc, eq, inArray } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { activityLog, issueExecutionDecisions } from "@paperclipai/db";
import {
@@ -189,25 +189,27 @@ async function listSuccessfulRunHandoffStates(
issueIds: string[],
): Promise<Map<string, SuccessfulRunHandoffState>> {
if (issueIds.length === 0) return new Map();
const result = await db.execute(sql`
SELECT DISTINCT ON (${activityLog.entityId})
${activityLog.entityId} AS "entityId",
${activityLog.action} AS "action",
${activityLog.agentId} AS "agentId",
${activityLog.runId} AS "runId",
${activityLog.details} AS "details",
${activityLog.createdAt} AS "createdAt"
FROM ${activityLog}
WHERE ${activityLog.companyId} = ${companyId}
AND ${activityLog.entityType} = 'issue'
AND ${activityLog.entityId} IN (${sql.join(issueIds.map((id) => sql`${id}`), sql`, `)})
AND ${activityLog.action} IN (${sql.join(SUCCESSFUL_RUN_HANDOFF_ACTIONS.map((action) => sql`${action}`), sql`, `)})
ORDER BY ${activityLog.entityId}, ${activityLog.createdAt} DESC, ${activityLog.id} DESC
`);
const rows = Array.from(result as Iterable<SuccessfulRunHandoffActivityRow>);
const rows = await db
.select({
entityId: activityLog.entityId,
action: activityLog.action,
agentId: activityLog.agentId,
runId: activityLog.runId,
details: activityLog.details,
createdAt: activityLog.createdAt,
})
.from(activityLog)
.where(and(
eq(activityLog.companyId, companyId),
eq(activityLog.entityType, "issue"),
inArray(activityLog.entityId, issueIds),
inArray(activityLog.action, [...SUCCESSFUL_RUN_HANDOFF_ACTIONS]),
))
.orderBy(activityLog.entityId, desc(activityLog.createdAt), desc(activityLog.id)) as SuccessfulRunHandoffActivityRow[];
const states = new Map<string, SuccessfulRunHandoffState>();
for (const row of rows) {
if (states.has(row.entityId)) continue;
const state = successfulRunHandoffStateFromActivity(row);
if (state) states.set(row.entityId, state);
}
+22 -26
View File
@@ -193,7 +193,7 @@ export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) {
)
`;
const issueTreeIdsTextCte = sql`
const runSummarySql = sql`
WITH RECURSIVE issue_tree(id) AS (
${cteSeedText}
UNION ALL
@@ -203,7 +203,23 @@ export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) {
WHERE ${childIssues.companyId} = ${companyId}
AND ${childIssues.hiddenAt} IS NULL
)
SELECT id FROM issue_tree
SELECT
count(distinct ${heartbeatRuns.id})::int AS "runCount",
coalesce(sum(extract(epoch from (coalesce(${heartbeatRuns.finishedAt}, now()) - ${heartbeatRuns.startedAt})) * 1000), 0)::double precision AS "runtimeMs"
FROM ${heartbeatRuns}
WHERE ${heartbeatRuns.companyId} = ${companyId}
AND ${heartbeatRuns.startedAt} IS NOT NULL
AND (
${heartbeatRuns.contextSnapshot} ->> 'issueId' IN (SELECT id FROM issue_tree)
OR EXISTS (
SELECT 1
FROM ${activityLog}
JOIN issue_tree ON ${activityLog.entityId} = issue_tree.id
WHERE ${activityLog.companyId} = ${companyId}
AND ${activityLog.entityType} = 'issue'
AND ${activityLog.runId} = ${heartbeatRuns.id}
)
)
`;
// Run cost-event aggregation and run-duration aggregation in parallel.
@@ -233,33 +249,13 @@ export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) {
issueTreeCondition,
),
),
db
.select({
runCount: sql<number>`count(distinct ${heartbeatRuns.id})::int`,
runtimeMs: sql<number>`coalesce(sum(extract(epoch from (coalesce(${heartbeatRuns.finishedAt}, now()) - ${heartbeatRuns.startedAt})) * 1000), 0)::double precision`,
})
.from(heartbeatRuns)
.where(
and(
eq(heartbeatRuns.companyId, companyId),
isNotNull(heartbeatRuns.startedAt),
sql`(
${heartbeatRuns.contextSnapshot} ->> 'issueId' IN (${issueTreeIdsTextCte})
OR EXISTS (
SELECT 1
FROM ${activityLog}
WHERE ${activityLog.companyId} = ${companyId}
AND ${activityLog.entityType} = 'issue'
AND ${activityLog.runId} = ${heartbeatRuns.id}
AND ${activityLog.entityId} IN (${issueTreeIdsTextCte})
)
)`,
),
),
db.execute(runSummarySql),
]);
const costRow = costRowResult[0];
const runRow = runRowResult[0];
const runRow = Array.isArray(runRowResult)
? (runRowResult[0] as { runCount?: number | string | null; runtimeMs?: number | string | null } | undefined)
: undefined;
return {
issueId,
+10 -3
View File
@@ -478,7 +478,6 @@ describe("IssueProperties", () => {
it("removes a blocked-by issue from the chip remove action after confirmation", async () => {
const onUpdate = vi.fn();
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
const root = renderProperties(container, {
issue: createIssue({
blockedBy: [
@@ -514,11 +513,19 @@ describe("IssueProperties", () => {
await act(async () => {
removeButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flush();
expect(document.body.textContent).toContain("Remove PAP-2: Existing blocker as a blocker for this issue.");
const confirmButton = Array.from(document.body.querySelectorAll("button"))
.find((button) => button.textContent?.includes("Remove blocker"));
expect(confirmButton).not.toBeUndefined();
await act(async () => {
confirmButton!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(confirmSpy).toHaveBeenCalledWith("Remove PAP-2: Existing blocker as a blocker?");
expect(onUpdate).toHaveBeenCalledWith({ blockedByIssueIds: ["issue-4"] });
confirmSpy.mockRestore();
act(() => root.unmount());
});
+63 -29
View File
@@ -31,6 +31,16 @@ import { Identity } from "./Identity";
import { IssueReferencePill } from "./IssueReferencePill";
import { formatDate, cn, projectUrl } from "../lib/utils";
import { timeAgo } from "../lib/timeAgo";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { User, Hexagon, ArrowUpRight, Tag, Plus, GitBranch, FolderOpen, Check, ExternalLink, X, Clock } from "lucide-react";
@@ -148,6 +158,7 @@ function RemovableIssueReferencePill({
issue: NonNullable<Issue["blockedBy"]>[number];
onRemove: (issueId: string) => void;
}) {
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const issueLabel = issue.identifier ?? issue.title;
const confirmLabel = issue.identifier ? `${issue.identifier}: ${issue.title}` : issue.title;
const content = (
@@ -160,41 +171,64 @@ function RemovableIssueReferencePill({
const handleRemove = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
if (!window.confirm(`Remove ${confirmLabel} as a blocker?`)) return;
setIsConfirmOpen(true);
};
const confirmRemove = () => {
onRemove(issue.id);
setIsConfirmOpen(false);
};
return (
<span
data-mention-kind="issue"
className={cn(
"paperclip-mention-chip paperclip-mention-chip--issue group",
"inline-flex items-center gap-1 rounded-full border border-border py-0.5 pl-1 pr-2 text-xs",
)}
title={issue.title}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
>
<button
type="button"
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground opacity-0 transition-colors transition-opacity hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring group-hover:opacity-100"
aria-label={removeLabel}
title={removeLabel}
onClick={handleRemove}
<>
<span
data-mention-kind="issue"
className={cn(
"paperclip-mention-chip paperclip-mention-chip--issue group",
"inline-flex items-center gap-1 rounded-full border border-border py-0.5 pl-1 pr-2 text-xs",
)}
title={issue.title}
aria-label={`Issue ${issueLabel}: ${issue.title}`}
>
<X className="h-3 w-3" />
</button>
{issue.identifier ? (
<Link
to={`/issues/${issueLabel}`}
className="inline-flex min-w-0 items-center gap-1 no-underline hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
aria-label={`Issue ${issueLabel}: ${issue.title}`}
<button
type="button"
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground opacity-0 transition-colors transition-opacity hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring group-hover:opacity-100"
aria-label={removeLabel}
title={removeLabel}
onClick={handleRemove}
>
{content}
</Link>
) : (
<span className="inline-flex min-w-0 items-center gap-1">{content}</span>
)}
</span>
<X className="h-3 w-3" />
</button>
{issue.identifier ? (
<Link
to={`/issues/${issueLabel}`}
className="inline-flex min-w-0 items-center gap-1 no-underline hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
aria-label={`Issue ${issueLabel}: ${issue.title}`}
>
{content}
</Link>
) : (
<span className="inline-flex min-w-0 items-center gap-1">{content}</span>
)}
</span>
<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Remove blocker?</DialogTitle>
<DialogDescription>
Remove {confirmLabel} as a blocker for this issue.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">Cancel</Button>
</DialogClose>
<Button type="button" variant="destructive" onClick={confirmRemove}>
Remove blocker
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+11 -4
View File
@@ -74,6 +74,16 @@ function executionWorkspaceTabPath(workspaceId: string, tab: ExecutionWorkspaceT
return `/execution-workspaces/${workspaceId}/${segment}`;
}
function LegacyWorkspaceTabRedirect({ workspaceId }: { workspaceId: string }) {
useEffect(() => {
try {
localStorage.removeItem(`paperclip:execution-workspace-tab:${workspaceId}`);
} catch {}
}, [workspaceId]);
return <Navigate to={executionWorkspaceTabPath(workspaceId, "issues")} replace />;
}
function isSafeExternalUrl(value: string | null | undefined) {
if (!value) return false;
try {
@@ -675,13 +685,10 @@ export function ExecutionWorkspaceDetail() {
const pendingRuntimeAction = controlRuntimeServices.isPending ? controlRuntimeServices.variables ?? null : null;
if (workspaceId && activeTab === null) {
return <Navigate to={executionWorkspaceTabPath(workspaceId, "issues")} replace />;
return <LegacyWorkspaceTabRedirect workspaceId={workspaceId} />;
}
const handleTabChange = (tab: ExecutionWorkspaceTab) => {
try {
localStorage.setItem(`paperclip:execution-workspace-tab:${workspace.id}`, tab);
} catch {}
navigate(executionWorkspaceTabPath(workspace.id, tab));
};