diff --git a/surfsense_web/components/tool-ui/delete-linear-issue.tsx b/surfsense_web/components/tool-ui/delete-linear-issue.tsx new file mode 100644 index 000000000..df291d4ec --- /dev/null +++ b/surfsense_web/components/tool-ui/delete-linear-issue.tsx @@ -0,0 +1,406 @@ +"use client"; + +import { makeAssistantToolUI } from "@assistant-ui/react"; +import { + AlertTriangleIcon, + CheckIcon, + InfoIcon, + Loader2Icon, + TriangleAlertIcon, + XIcon, +} from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +interface InterruptResult { + __interrupt__: true; + __decided__?: "approve" | "reject"; + action_requests: Array<{ + name: string; + args: Record; + }>; + review_configs: Array<{ + action_name: string; + allowed_decisions: Array<"approve" | "reject">; + }>; + interrupt_type?: string; + context?: { + workspace?: { id: number; organization_name: string }; + issue?: { + id: string; + identifier: string; + title: string; + url?: string; + current_state?: string; + document_id?: number; + indexed_at?: string; + }; + error?: string; + }; +} + +interface SuccessResult { + status: "success"; + deleted_from_kb?: boolean; + message?: string; +} + +interface ErrorResult { + status: "error"; + message: string; +} + +interface NotFoundResult { + status: "not_found"; + message: string; +} + +interface WarningResult { + status: "success"; + warning: string; + message?: string; +} + +type DeleteLinearIssueResult = + | InterruptResult + | SuccessResult + | ErrorResult + | NotFoundResult + | WarningResult; + +function isInterruptResult(result: unknown): result is InterruptResult { + return ( + typeof result === "object" && + result !== null && + "__interrupt__" in result && + (result as InterruptResult).__interrupt__ === true + ); +} + +function isErrorResult(result: unknown): result is ErrorResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as ErrorResult).status === "error" + ); +} + +function isNotFoundResult(result: unknown): result is NotFoundResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as NotFoundResult).status === "not_found" + ); +} + +function isWarningResult(result: unknown): result is WarningResult { + return ( + typeof result === "object" && + result !== null && + "status" in result && + (result as WarningResult).status === "success" && + "warning" in result && + typeof (result as WarningResult).warning === "string" + ); +} + +function ApprovalCard({ + interruptData, + onDecision, +}: { + interruptData: InterruptResult; + onDecision: (decision: { + type: "approve" | "reject"; + message?: string; + edited_action?: { name: string; args: Record }; + }) => void; +}) { + const actionArgs = interruptData.action_requests[0]?.args ?? {}; + const context = interruptData.context; + const issue = context?.issue; + + const [decided, setDecided] = useState<"approve" | "reject" | null>( + interruptData.__decided__ ?? null + ); + const [deleteFromKb, setDeleteFromKb] = useState( + typeof actionArgs.delete_from_kb === "boolean" ? actionArgs.delete_from_kb : false + ); + + return ( +
+ {/* Header */} +
+
+ +
+
+

Delete Linear Issue

+

+ Requires your approval to proceed +

+
+
+ + {/* Context section — workspace + issue info (read-only) */} + {!decided && ( +
+ {context?.error ? ( +

{context.error}

+ ) : ( + <> + {context?.workspace && ( +
+
Linear Account
+
+ {context.workspace.organization_name} +
+
+ )} + + {issue && ( +
+
Issue to Archive
+
+
+ {issue.identifier}: {issue.title} +
+ {issue.current_state && ( +
{issue.current_state}
+ )} + {issue.url && ( + + Open in Linear ↗ + + )} +
+
+ )} + + )} +
+ )} + + {/* delete_from_kb toggle */} + {!decided && ( +
+ +
+ )} + + {/* Action buttons */} +
+ {decided ? ( +

+ {decided === "approve" ? ( + <> + + Approved + + ) : ( + <> + + Rejected + + )} +

+ ) : ( + <> + + + + )} +
+
+ ); +} + +function ErrorCard({ result }: { result: ErrorResult }) { + return ( +
+
+
+ +
+
+

Failed to delete Linear issue

+
+
+
+

{result.message}

+
+
+ ); +} + +function NotFoundCard({ result }: { result: NotFoundResult }) { + return ( +
+
+
+ +
+
+

{result.message}

+
+
+
+ ); +} + +function WarningCard({ result }: { result: WarningResult }) { + return ( +
+
+
+ +
+
+

Partial success

+
+
+
+

{result.warning}

+
+
+ ); +} + +function SuccessCard({ result }: { result: SuccessResult }) { + return ( +
+
+
+ +
+
+

+ {result.message || "Linear issue archived successfully"} +

+
+
+ {result.deleted_from_kb && ( +
+ + ✓ Also removed from knowledge base + +
+ )} +
+ ); +} + +export const DeleteLinearIssueToolUI = makeAssistantToolUI< + { issue_ref: string; delete_from_kb?: boolean }, + DeleteLinearIssueResult +>({ + toolName: "delete_linear_issue", + render: function DeleteLinearIssueUI({ result, status }) { + if (status.type === "running") { + return ( +
+ +

Preparing Linear issue deletion...

+
+ ); + } + + if (!result) return null; + + if (isInterruptResult(result)) { + return ( + { + window.dispatchEvent( + new CustomEvent("hitl-decision", { detail: { decisions: [decision] } }) + ); + }} + /> + ); + } + + if ( + typeof result === "object" && + result !== null && + "status" in result && + (result as { status: string }).status === "rejected" + ) { + return null; + } + + if (isNotFoundResult(result)) return ; + if (isWarningResult(result)) return ; + if (isErrorResult(result)) return ; + + return ; + }, +}); diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 96d1e2502..048f23558 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -16,6 +16,7 @@ export { type SerializableArticle, } from "./article"; export { Audio } from "./audio"; +export { CreateLinearIssueToolUI } from "./create-linear-issue"; export { CreateNotionPageToolUI } from "./create-notion-page"; export { type DeepAgentThinkingArgs, @@ -24,6 +25,7 @@ export { InlineThinkingDisplay, type ThinkingStep, } from "./deepagent-thinking"; +export { DeleteLinearIssueToolUI } from "./delete-linear-issue"; export { type DisplayImageArgs, DisplayImageArgsSchema, @@ -79,6 +81,7 @@ export { ScrapeWebpageResultSchema, ScrapeWebpageToolUI, } from "./scrape-webpage"; +export { UpdateLinearIssueToolUI } from "./update-linear-issue"; export { UpdateNotionPageToolUI } from "./update-notion-page"; export { type MemoryItem,