diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 491014a3cdb..4935257e25d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -670,8 +670,8 @@ export { requestConfirmationResultSchema, createIssueThreadInteractionSchema, acceptIssueThreadInteractionSchema, - cancelIssueThreadInteractionSchema, rejectIssueThreadInteractionSchema, + cancelIssueThreadInteractionSchema, respondIssueThreadInteractionSchema, linkIssueApprovalSchema, createIssueAttachmentMetadataSchema, @@ -705,8 +705,8 @@ export { type AddIssueComment, type CreateIssueThreadInteraction, type AcceptIssueThreadInteraction, - type CancelIssueThreadInteraction, type RejectIssueThreadInteraction, + type CancelIssueThreadInteraction, type RespondIssueThreadInteraction, type LinkIssueApproval, type CreateIssueAttachmentMetadata, diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index 8a1621f3e95..dfa28d726ba 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -176,8 +176,8 @@ export { requestConfirmationResultSchema, createIssueThreadInteractionSchema, acceptIssueThreadInteractionSchema, - cancelIssueThreadInteractionSchema, rejectIssueThreadInteractionSchema, + cancelIssueThreadInteractionSchema, respondIssueThreadInteractionSchema, linkIssueApprovalSchema, createIssueAttachmentMetadataSchema, @@ -194,8 +194,8 @@ export { type AddIssueComment, type CreateIssueThreadInteraction, type AcceptIssueThreadInteraction, - type CancelIssueThreadInteraction, type RejectIssueThreadInteraction, + type CancelIssueThreadInteraction, type RespondIssueThreadInteraction, type LinkIssueApproval, type CreateIssueAttachmentMetadata, diff --git a/server/src/services/costs.ts b/server/src/services/costs.ts index 3cdb88a1b5a..abea444e2f1 100644 --- a/server/src/services/costs.ts +++ b/server/src/services/costs.ts @@ -1,4 +1,5 @@ import { and, desc, eq, gte, isNotNull, isNull, lt, lte, sql } from "drizzle-orm"; +import { alias } from "drizzle-orm/pg-core"; import type { Db } from "@paperclipai/db"; import { activityLog, agents, companies, costEvents, issues, projects } from "@paperclipai/db"; import { notFound, unprocessable } from "../errors.js"; @@ -135,6 +136,9 @@ export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) { }, issueTreeSummary: async (companyId: string, issueId: string) => { + // Callers must resolve and authorize a visible root issue before invoking this. + // The route does that so zero counts are not mistaken for a missing root. + const childIssues = alias(issues, "child"); const issueTreeCondition = sql` ${issues.id} IN ( WITH RECURSIVE issue_tree(id) AS ( @@ -143,12 +147,12 @@ export function costService(db: Db, budgetHooks: BudgetServiceHooks = {}) { WHERE ${issues.companyId} = ${companyId} AND ${issues.id} = ${issueId} AND ${issues.hiddenAt} IS NULL - UNION - SELECT child.id - FROM ${issues} child - JOIN issue_tree ON child.parent_id = issue_tree.id - WHERE child.company_id = ${companyId} - AND child.hidden_at IS NULL + UNION ALL + SELECT ${childIssues.id} + FROM ${issues} ${childIssues} + JOIN issue_tree ON ${childIssues.parentId} = issue_tree.id + WHERE ${childIssues.companyId} = ${companyId} + AND ${childIssues.hiddenAt} IS NULL ) SELECT id FROM issue_tree ) diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index e26afc30b07..82e50fb3a4d 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -3495,6 +3495,11 @@ export function IssueChatThread({ return; } + if (typeof document === "undefined") { + finish(); + return; + } + const el = document.getElementById(latestCommentAnchor); if (!el) { // Row hasn't been rendered into the virtualizer's buffer yet — nudge diff --git a/ui/src/components/IssueRunLedger.tsx b/ui/src/components/IssueRunLedger.tsx index 9a28e389c25..7b0a5b5ee9b 100644 --- a/ui/src/components/IssueRunLedger.tsx +++ b/ui/src/components/IssueRunLedger.tsx @@ -460,6 +460,7 @@ export function IssueRunLedgerContent({ [ledgerRuns], ); const children = childIssueSummary(childIssues); + const canRenderActivityEvents = Boolean(renderActivityEvent); const feedItems = useMemo(() => { const items: LedgerFeedItem[] = []; for (const run of ledgerRuns) { @@ -470,12 +471,14 @@ export function IssueRunLedgerContent({ run, }); } - if (renderActivityEvent) { + if (canRenderActivityEvents) { for (const event of activityEvents ?? []) { items.push({ kind: "activity", id: event.id, - timestamp: String(event.createdAt), + timestamp: event.createdAt instanceof Date + ? event.createdAt.toISOString() + : String(event.createdAt), event, }); } @@ -487,7 +490,7 @@ export function IssueRunLedgerContent({ if (a.kind !== b.kind) return a.kind === "run" ? -1 : 1; return b.id.localeCompare(a.id); }); - }, [activityEvents, ledgerRuns, renderActivityEvent]); + }, [activityEvents, canRenderActivityEvents, ledgerRuns]); return (
diff --git a/ui/src/components/IssueThreadInteractionCard.test.tsx b/ui/src/components/IssueThreadInteractionCard.test.tsx index fe9d14d7183..b289c72a7b3 100644 --- a/ui/src/components/IssueThreadInteractionCard.test.tsx +++ b/ui/src/components/IssueThreadInteractionCard.test.tsx @@ -91,6 +91,25 @@ describe("IssueThreadInteractionCard", () => { expect(host.querySelectorAll('[role="checkbox"]')).toHaveLength(3); }); + it("only shows question cancellation when a cancel handler is wired", () => { + const withoutHandler = renderCard({ + interaction: pendingAskUserQuestionsInteraction, + onSubmitInteractionAnswers: vi.fn(), + }); + expect(withoutHandler.textContent).not.toContain("Cancel question"); + + act(() => root?.unmount()); + withoutHandler.remove(); + root = null; + + const withHandler = renderCard({ + interaction: pendingAskUserQuestionsInteraction, + onCancelInteraction: vi.fn(), + onSubmitInteractionAnswers: vi.fn(), + }); + expect(withHandler.textContent).toContain("Cancel question"); + }); + it("makes child tasks explicit in suggested task trees", () => { const host = renderCard({ interaction: pendingSuggestedTasksInteraction, diff --git a/ui/src/components/IssueThreadInteractionCard.tsx b/ui/src/components/IssueThreadInteractionCard.tsx index cf5e037a13c..d48bd5b1d3f 100644 --- a/ui/src/components/IssueThreadInteractionCard.tsx +++ b/ui/src/components/IssueThreadInteractionCard.tsx @@ -792,21 +792,23 @@ function AskUserQuestionsCard({ Submit once after you finish the full form.
- + {onCancelInteraction ? ( + + ) : null}