diff --git a/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx b/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
index ec45ff2aa..beddbc74d 100644
--- a/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
+++ b/surfsense_web/components/tool-ui/linear/create-linear-issue.tsx
@@ -1,10 +1,9 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
-import { AlertTriangleIcon, CheckIcon, Loader2Icon, Pen, XIcon } from "lucide-react";
-import { useMemo, useState } from "react";
+import { CornerDownLeftIcon, Pen } from "lucide-react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -12,7 +11,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Textarea } from "@/components/ui/textarea";
+import { PlateEditor } from "@/components/editor/plate-editor";
+import { Spinner } from "@/components/ui/spinner";
+import { useSetAtom } from "jotai";
+import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@@ -125,9 +127,9 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
- const [isEditing, setIsEditing] = useState(false);
- const [editedTitle, setEditedTitle] = useState(args.title ?? "");
- const [editedDescription, setEditedDescription] = useState(args.description ?? "");
+ const [isPanelOpen, setIsPanelOpen] = useState(false);
+ const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
+
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
const [selectedTeamId, setSelectedTeamId] = useState("");
const [selectedStateId, setSelectedStateId] = useState("__none__");
@@ -147,17 +149,17 @@ function ApprovalCard({
[selectedWorkspace, selectedTeamId]
);
- const isTitleValid = editedTitle.trim().length > 0;
+ const isTitleValid = (args.title ?? "").trim().length > 0;
const canApprove = !!selectedWorkspaceId && !!selectedTeamId && isTitleValid;
const reviewConfig = interruptData.review_configs[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
const canEdit = allowedDecisions.includes("edit");
- function buildFinalArgs() {
+ const buildFinalArgs = useCallback((overrides?: { title?: string; description?: string }) => {
return {
- title: editedTitle,
- description: editedDescription || null,
+ title: overrides?.title ?? args.title,
+ description: overrides?.description ?? args.description ?? null,
connector_id: selectedWorkspaceId ? Number(selectedWorkspaceId) : null,
team_id: selectedTeamId || null,
state_id: selectedStateId === "__none__" ? null : selectedStateId,
@@ -165,372 +167,322 @@ function ApprovalCard({
priority: Number(selectedPriority),
label_ids: selectedLabelIds,
};
- }
+ }, [args.title, args.description, selectedWorkspaceId, selectedTeamId, selectedStateId, selectedAssigneeId, selectedPriority, selectedLabelIds]);
+
+ const handleApprove = useCallback(() => {
+ if (decided || isPanelOpen || !canApprove) return;
+ if (!allowedDecisions.includes("approve")) return;
+ setDecided("approve");
+ onDecision({
+ type: "approve",
+ edited_action: {
+ name: interruptData.action_requests[0].name,
+ args: buildFinalArgs(),
+ },
+ });
+ }, [decided, isPanelOpen, canApprove, allowedDecisions, onDecision, interruptData, buildFinalArgs]);
+
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ handleApprove();
+ }
+ };
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [handleApprove]);
return (
-
+
{/* Header */}
-
-
-
-
Create Linear Issue
-
- {isEditing ? "You can edit the arguments below" : "Requires your approval to proceed"}
+
+
+
+ {decided === "reject"
+ ? "Linear Issue Rejected"
+ : decided === "approve" || decided === "edit"
+ ? "Linear Issue Approved"
+ : "Create Linear Issue"}
+
+
+ {decided === "reject"
+ ? "Issue creation was cancelled"
+ : decided === "edit"
+ ? "Issue creation is in progress with your changes"
+ : decided === "approve"
+ ? "Issue creation is in progress"
+ : "Requires your approval to proceed"}
+ {!decided && canEdit && (
+
{
+ setIsPanelOpen(true);
+ openHitlEditPanel({
+ title: args.title ?? "",
+ content: args.description ?? "",
+ toolName: "Linear Issue",
+ onSave: (newTitle, newDescription) => {
+ setIsPanelOpen(false);
+ setDecided("edit");
+ onDecision({
+ type: "edit",
+ edited_action: {
+ name: interruptData.action_requests[0].name,
+ args: buildFinalArgs({ title: newTitle, description: newDescription }),
+ },
+ });
+ },
+ });
+ }}
+ >
+
+ Edit
+
+ )}
{/* Context section */}
{!decided && (
-
- {interruptData.context?.error ? (
-
{interruptData.context.error}
- ) : (
- <>
- {workspaces.length > 0 && (
-
-
- Linear Account *
-
-
{
- setSelectedWorkspaceId(v);
- setSelectedTeamId("");
- setSelectedStateId("__none__");
- setSelectedAssigneeId("__none__");
- setSelectedPriority("0");
- setSelectedLabelIds([]);
- }}
- >
-
-
-
-
- {workspaces.map((w) => (
-
- {w.name}
-
- ))}
-
-
-
- )}
-
- {selectedWorkspace && (
- <>
-
-
- Team *
-
+ <>
+
+
+ {interruptData.context?.error ? (
+
{interruptData.context.error}
+ ) : (
+ <>
+ {workspaces.length > 0 && (
+
+
+ Linear Account *
+
{
- setSelectedTeamId(v);
- const newTeam = selectedWorkspace.teams.find((t) => t.id === v);
- setSelectedStateId(newTeam?.states?.[0]?.id ?? "__none__");
+ setSelectedWorkspaceId(v);
+ setSelectedTeamId("");
+ setSelectedStateId("__none__");
setSelectedAssigneeId("__none__");
+ setSelectedPriority("0");
setSelectedLabelIds([]);
}}
>
-
+
- {selectedWorkspace.teams.map((t) => (
-
- {t.name} ({t.key})
+ {workspaces.map((w) => (
+
+ {w.name}
))}
+ )}
- {selectedTeam && (
- <>
-
-
State
-
-
-
-
-
- {selectedTeam.states.map((s) => (
-
- {s.name}
-
- ))}
-
-
-
+ {selectedWorkspace && (
+ <>
+
+
+ Team *
+
+
{
+ setSelectedTeamId(v);
+ const newTeam = selectedWorkspace.teams.find((t) => t.id === v);
+ setSelectedStateId(newTeam?.states?.[0]?.id ?? "__none__");
+ setSelectedAssigneeId("__none__");
+ setSelectedLabelIds([]);
+ }}
+ >
+
+
+
+
+ {selectedWorkspace.teams.map((t) => (
+
+ {t.name} ({t.key})
+
+ ))}
+
+
+
-
-
Assignee
-
-
-
-
-
- Unassigned
- {selectedTeam.members
- .filter((m) => m.active)
- .map((m) => (
-
- {m.name} ({m.email})
+ {selectedTeam && (
+ <>
+
+
State
+
+
+
+
+
+ {selectedTeam.states.map((s) => (
+
+ {s.name}
))}
-
-
-
-
-
-
Priority
-
-
-
-
-
- {selectedWorkspace.priorities.map((p) => (
-
- {p.label}
-
- ))}
-
-
-
-
- {selectedTeam.labels.length > 0 && (
-
-
Labels
-
- {selectedTeam.labels.map((label) => {
- const isSelected = selectedLabelIds.includes(label.id);
- return (
-
- setSelectedLabelIds((prev) =>
- isSelected
- ? prev.filter((id) => id !== label.id)
- : [...prev, label.id]
- )
- }
- className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
- isSelected
- ? "opacity-100 ring-2 ring-foreground/30"
- : "opacity-50 hover:opacity-80"
- }`}
- style={{
- backgroundColor: `${label.color}33`,
- color: label.color,
- }}
- >
-
- {label.name}
-
- );
- })}
-
+
+
- )}
- >
- )}
- >
- )}
- >
- )}
-
- )}
- {/* Display mode */}
- {!isEditing && (
-
-
- {args.description && (
-
-
Description
-
- {args.description}
-
-
- )}
-
- )}
+
+
Assignee
+
+
+
+
+
+ Unassigned
+ {selectedTeam.members
+ .filter((m) => m.active)
+ .map((m) => (
+
+ {m.name} ({m.email})
+
+ ))}
+
+
+
- {/* Edit mode */}
- {isEditing && !decided && (
-
-
-
- Title *
-
-
setEditedTitle(e.target.value)}
- placeholder="Enter issue title"
- className={!isTitleValid ? "border-destructive" : ""}
- />
- {!isTitleValid &&
Title is required
}
-
-
-
- Description
-
-
-
- )}
+
+
Priority
+
+
+
+
+
+ {selectedWorkspace.priorities.map((p) => (
+
+ {p.label}
+
+ ))}
+
+
+
- {/* Action buttons */}
-
- {decided ? (
-
- {decided === "approve" || decided === "edit" ? (
- <>
-
- {decided === "edit" ? "Approved with Changes" : "Approved"}
- >
- ) : (
- <>
-
- Rejected
+ {selectedTeam.labels.length > 0 && (
+
+
Labels
+
+ {selectedTeam.labels.map((label) => {
+ const isSelected = selectedLabelIds.includes(label.id);
+ return (
+
+ setSelectedLabelIds((prev) =>
+ isSelected
+ ? prev.filter((id) => id !== label.id)
+ : [...prev, label.id]
+ )
+ }
+ className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
+ isSelected
+ ? "opacity-100 ring-2 ring-foreground/30"
+ : "opacity-50 hover:opacity-80"
+ }`}
+ style={{
+ backgroundColor: `${label.color}33`,
+ color: label.color,
+ }}
+ >
+
+ {label.name}
+
+ );
+ })}
+
+
+ )}
+ >
+ )}
+ >
+ )}
>
)}
-
- ) : isEditing ? (
- <>
-
{
- setDecided("edit");
- setIsEditing(false);
- onDecision({
- type: "edit",
- edited_action: {
- name: interruptData.action_requests[0].name,
- args: buildFinalArgs(),
- },
- });
- }}
- disabled={!canApprove}
- >
-
- Approve with Changes
-
-
{
- setIsEditing(false);
- setEditedTitle(args.title ?? "");
- setEditedDescription(args.description ?? "");
- }}
- >
- Cancel
-
- >
- ) : (
- <>
+
+ >
+ )}
+
+ {/* Content preview */}
+
+
+ {args.title != null && (
+
{args.title}
+ )}
+ {args.description != null && (
+
+ )}
+
+
+ {/* Action buttons - only shown when pending */}
+ {!decided && (
+ <>
+
+
{allowedDecisions.includes("approve") && (
{
- setDecided("approve");
- onDecision({
- type: "approve",
- edited_action: {
- name: interruptData.action_requests[0].name,
- args: buildFinalArgs(),
- },
- });
- }}
+ className="rounded-lg gap-1.5"
+ onClick={handleApprove}
disabled={!canApprove}
>
-
Approve
-
- )}
- {canEdit && (
-
setIsEditing(true)}>
-
- Edit
+
)}
{allowedDecisions.includes("reject") && (
{
setDecided("reject");
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
-
Reject
)}
- >
- )}
-
+
+ >
+ )}
);
}
function ErrorCard({ result }: { result: ErrorResult }) {
return (
-
-
-
-
-
-
-
Failed to create Linear issue
-
+
+
+
Failed to create Linear issue
-
@@ -539,18 +491,14 @@ function ErrorCard({ result }: { result: ErrorResult }) {
function SuccessCard({ result }: { result: SuccessResult }) {
return (
-
-
-
-
-
-
-
- {result.message || "Linear issue created successfully"}
-
-
+
+
+
+ {result.message || "Linear issue created successfully"}
+
-
+
+
Identifier:
{result.identifier}
@@ -580,8 +528,8 @@ export const CreateLinearIssueToolUI = makeAssistantToolUI<
render: function CreateLinearIssueUI({ args, result, status }) {
if (status.type === "running") {
return (
-
-
+
+
Preparing Linear issue...
);
diff --git a/surfsense_web/components/tool-ui/linear/delete-linear-issue.tsx b/surfsense_web/components/tool-ui/linear/delete-linear-issue.tsx
index 2b21cd564..add3a8483 100644
--- a/surfsense_web/components/tool-ui/linear/delete-linear-issue.tsx
+++ b/surfsense_web/components/tool-ui/linear/delete-linear-issue.tsx
@@ -1,16 +1,10 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
-import {
- AlertTriangleIcon,
- CheckIcon,
- InfoIcon,
- Loader2Icon,
- TriangleAlertIcon,
- XIcon,
-} from "lucide-react";
-import { useState } from "react";
+import { CornerDownLeftIcon, InfoIcon, TriangleAlertIcon } from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
+import { Spinner } from "@/components/ui/spinner";
interface InterruptResult {
__interrupt__: true;
@@ -127,163 +121,153 @@ function ApprovalCard({
typeof actionArgs.delete_from_kb === "boolean" ? actionArgs.delete_from_kb : false
);
+ const handleApprove = useCallback(() => {
+ if (decided) return;
+ setDecided("approve");
+ onDecision({
+ type: "approve",
+ edited_action: {
+ name: interruptData.action_requests[0].name,
+ args: {
+ issue_id: issue?.id,
+ connector_id: context?.workspace?.id,
+ delete_from_kb: deleteFromKb,
+ },
+ },
+ });
+ }, [decided, onDecision, interruptData, issue?.id, context?.workspace?.id, deleteFromKb]);
+
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ handleApprove();
+ }
+ };
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [handleApprove]);
+
return (
-
+
{/* Header */}
-
-
-
-
Delete Linear Issue
-
- Requires your approval to proceed
+
+
+
+ {decided === "reject"
+ ? "Linear Issue Deletion Rejected"
+ : decided === "approve"
+ ? "Linear Issue Deletion Approved"
+ : "Delete Linear Issue"}
+
+
+ {decided === "reject"
+ ? "Issue deletion was cancelled"
+ : decided === "approve"
+ ? "Issue deletion is in progress"
+ : "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}
+ <>
+
+
+ {context?.error ? (
+
{context.error}
+ ) : (
+ <>
+ {context?.workspace && (
+
+
Linear Account
+
+ {context.workspace.organization_name}
- {issue.state && (
-
{issue.state}
- )}
-
- )}
- >
- )}
-
+ )}
+
+ {issue && (
+
+
Issue to Archive
+
+
+ {issue.identifier}: {issue.title}
+
+ {issue.state && (
+
{issue.state}
+ )}
+
+
+ )}
+ >
+ )}
+
+ >
)}
{/* delete_from_kb toggle */}
{!decided && (
-
-
- setDeleteFromKb(e.target.checked)}
- className="mt-0.5"
- />
-
-
Also remove from knowledge base
-
- ⚠️ This will permanently delete the issue from your knowledge base (cannot be undone)
-
-
-
-
+ <>
+
+
+
+ setDeleteFromKb(e.target.checked)}
+ className="mt-0.5"
+ />
+
+
Also remove from knowledge base
+
+ This will permanently delete the issue from your knowledge base (cannot be undone)
+
+
+
+
+ >
)}
- {/* Action buttons */}
-
- {decided ? (
-
- {decided === "approve" ? (
- <>
-
- Approved
- >
- ) : (
- <>
-
- Rejected
- >
- )}
-
- ) : (
- <>
+ {/* Action buttons - only shown when pending */}
+ {!decided && (
+ <>
+
+
{
- setDecided("approve");
- onDecision({
- type: "approve",
- edited_action: {
- name: interruptData.action_requests[0].name,
- args: {
- issue_id: issue?.id,
- connector_id: context?.workspace?.id,
- delete_from_kb: deleteFromKb,
- },
- },
- });
- }}
+ className="rounded-lg gap-1.5"
+ onClick={handleApprove}
>
-
Approve
+
{
setDecided("reject");
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
-
Reject
- >
- )}
-
+
+ >
+ )}
);
}
function ErrorCard({ result }: { result: ErrorResult }) {
return (
-
-
-
-
-
-
-
Failed to delete Linear issue
-
+
+
+
Failed to delete Linear issue
-
@@ -292,14 +276,10 @@ function ErrorCard({ result }: { result: ErrorResult }) {
function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
-
-
-
-
-
-
+
);
@@ -307,16 +287,12 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
function WarningCard({ result }: { result: WarningResult }) {
return (
-
-
-
-
-
-
+
+
-
@@ -325,23 +301,21 @@ function WarningCard({ result }: { result: WarningResult }) {
function SuccessCard({ result }: { result: SuccessResult }) {
return (
-
-
-
-
-
-
-
- {result.message || "Linear issue archived successfully"}
-
-
+
+
+
+ {result.message || "Linear issue archived successfully"}
+
{result.deleted_from_kb && (
-
-
- ✓ Also removed from knowledge base
-
-
+ <>
+
+
+
+ Also removed from knowledge base
+
+
+ >
)}
);
@@ -355,8 +329,8 @@ export const DeleteLinearIssueToolUI = makeAssistantToolUI<
render: function DeleteLinearIssueUI({ result, status }) {
if (status.type === "running") {
return (
-
-
+
+
Preparing Linear issue deletion...
);
diff --git a/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx b/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
index 8a68a1176..da30e7e3f 100644
--- a/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
+++ b/surfsense_web/components/tool-ui/linear/update-linear-issue.tsx
@@ -1,10 +1,9 @@
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
-import { AlertTriangleIcon, CheckIcon, InfoIcon, Loader2Icon, Pen, XIcon } from "lucide-react";
-import { useState } from "react";
+import { CornerDownLeftIcon, InfoIcon, Pen } from "lucide-react";
+import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
@@ -12,7 +11,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Textarea } from "@/components/ui/textarea";
+import { PlateEditor } from "@/components/editor/plate-editor";
+import { Spinner } from "@/components/ui/spinner";
+import { useSetAtom } from "jotai";
+import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom";
interface LinearLabel {
id: string;
@@ -166,8 +168,9 @@ function ApprovalCard({
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
interruptData.__decided__ ?? null
);
- const [isEditing, setIsEditing] = useState(false);
+ const [isPanelOpen, setIsPanelOpen] = useState(false);
const [editedArgs, setEditedArgs] = useState(initialEditState);
+ const openHitlEditPanel = useSetAtom(openHitlEditPanelAtom);
const reviewConfig = interruptData.review_configs[0];
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
@@ -193,7 +196,7 @@ function ApprovalCard({
return ids.map((id) => team?.labels.find((l) => l.id === id)).filter(Boolean) as LinearLabel[];
}
- function buildFinalArgs() {
+ const buildFinalArgs = useCallback(() => {
const labelsWereProposed = Array.isArray(actionArgs.new_label_ids);
return {
issue_id: issue?.id,
@@ -207,7 +210,7 @@ function ApprovalCard({
new_label_ids:
labelsWereProposed || editedArgs.labelIds.length > 0 ? editedArgs.labelIds : null,
};
- }
+ }, [actionArgs.new_label_ids, issue?.id, issue?.document_id, context?.workspace?.id, editedArgs]);
const proposedStateName = resolveStateName(
actionArgs.new_state_id ? String(actionArgs.new_state_id) : null
@@ -230,423 +233,381 @@ function ApprovalCard({
proposedPriorityLabel ||
proposedLabelObjects.length > 0;
+ const handleApprove = useCallback(() => {
+ if (decided || isPanelOpen) return;
+ if (!allowedDecisions.includes("approve")) return;
+ setDecided("approve");
+ onDecision({
+ type: "approve",
+ edited_action: {
+ name: interruptData.action_requests[0].name,
+ args: buildFinalArgs(),
+ },
+ });
+ }, [decided, isPanelOpen, allowedDecisions, onDecision, interruptData, buildFinalArgs]);
+
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ handleApprove();
+ }
+ };
+ window.addEventListener("keydown", handler);
+ return () => window.removeEventListener("keydown", handler);
+ }, [handleApprove]);
+
return (
-
+
{/* Header */}
-
-
-
-
Update Linear Issue
-
- {isEditing ? "You can edit the arguments below" : "Requires your approval to proceed"}
+
+
+
+ {decided === "reject"
+ ? "Linear Issue Update Rejected"
+ : decided === "approve" || decided === "edit"
+ ? "Linear Issue Update Approved"
+ : "Update Linear Issue"}
+
+
+ {decided === "reject"
+ ? "Issue update was cancelled"
+ : decided === "edit"
+ ? "Issue update is in progress with your changes"
+ : decided === "approve"
+ ? "Issue update is in progress"
+ : "Requires your approval to proceed"}
+ {!decided && canEdit && (
+
{
+ setIsPanelOpen(true);
+ openHitlEditPanel({
+ title: editedArgs.title,
+ content: editedArgs.description,
+ toolName: "Linear Issue",
+ onSave: (newTitle, newDescription) => {
+ setIsPanelOpen(false);
+ setEditedArgs((prev) => ({
+ ...prev,
+ title: newTitle,
+ description: newDescription,
+ }));
+ setDecided("edit");
+ onDecision({
+ type: "edit",
+ edited_action: {
+ name: interruptData.action_requests[0].name,
+ args: {
+ ...buildFinalArgs(),
+ new_title: newTitle || null,
+ new_description: newDescription || null,
+ },
+ },
+ });
+ },
+ });
+ }}
+ >
+
+ Edit
+
+ )}
{/* Context section — workspace + current issue (read-only) */}
{!decided && (
-
- {context?.error ? (
-
{context.error}
- ) : (
- <>
- {context?.workspace && (
-
-
Linear Account
-
- {context.workspace.organization_name}
-
-
- )}
-
- {issue && (
-
-
Current Issue
-
-
- {issue.identifier}: {issue.title}
-
-
- {issue.current_state && (
-
- {issue.current_state.name}
-
- )}
- {issue.current_assignee && {issue.current_assignee.name} }
- {priorities.find((p) => p.priority === issue.priority) && (
- {priorities.find((p) => p.priority === issue.priority)?.label}
- )}
-
- {issue.current_labels && issue.current_labels.length > 0 && (
-
- {issue.current_labels.map((label) => (
-
- {label.name}
-
- ))}
-
- )}
- {issue.url && (
-
- Open in Linear ↗
-
- )}
-
-
- )}
- >
- )}
-
- )}
-
- {/* Display mode — proposed changes */}
- {!isEditing && (
-
- {hasProposedChanges ? (
- <>
- {actionArgs.new_title && (
-
-
New Title
-
{String(actionArgs.new_title)}
-
- )}
- {actionArgs.new_description && (
-
-
New Description
-
- {String(actionArgs.new_description)}
-
-
- )}
- {proposedStateName && (
-
-
New State
-
{proposedStateName}
-
- )}
- {proposedAssigneeName && (
-
-
New Assignee
-
{proposedAssigneeName}
-
- )}
- {proposedPriorityLabel && (
-
-
New Priority
-
{proposedPriorityLabel}
-
- )}
- {proposedLabelObjects.length > 0 && (
-
-
New Labels
-
- {proposedLabelObjects.map((label) => (
-
- {label.name}
-
- ))}
-
-
- )}
- >
- ) : (
-
No changes proposed
- )}
-
- )}
-
- {/* Edit mode */}
- {isEditing && !decided && (
-
-
-
- New Title
-
- setEditedArgs({ ...editedArgs, title: e.target.value })}
- placeholder="Issue title"
- />
-
-
-
-
- Description
-
-
-
- {team && (
- <>
-
-
State
-
setEditedArgs({ ...editedArgs, stateId: v })}
- >
-
-
-
-
- {team.states.map((s) => (
-
- {s.name}
-
- ))}
-
-
-
-
-
-
Assignee
-
setEditedArgs({ ...editedArgs, assigneeId: v })}
- >
-
-
-
-
- Unassigned
- {team.members
- .filter((m) => m.active)
- .map((m) => (
-
- {m.name} ({m.email})
-
- ))}
-
-
-
-
-
-
Priority
-
setEditedArgs({ ...editedArgs, priority: v })}
- >
-
-
-
-
- {priorities.map((p) => (
-
- {p.label}
-
- ))}
-
-
-
-
- {team.labels.length > 0 && (
-
-
Labels
-
- {team.labels.map((label) => {
- const isSelected = editedArgs.labelIds.includes(label.id);
- return (
-
- setEditedArgs({
- ...editedArgs,
- labelIds: isSelected
- ? editedArgs.labelIds.filter((id) => id !== label.id)
- : [...editedArgs.labelIds, label.id],
- })
- }
- className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
- isSelected
- ? "opacity-100 ring-2 ring-foreground/30"
- : "opacity-50 hover:opacity-80"
- }`}
- style={{
- backgroundColor: `${label.color}33`,
- color: label.color,
- }}
- >
-
- {label.name}
-
- );
- })}
-
-
- )}
- >
- )}
-
- )}
-
- {/* Action buttons */}
-
- {decided ? (
-
- {decided === "approve" || decided === "edit" ? (
- <>
-
- {decided === "edit" ? "Approved with Changes" : "Approved"}
- >
+ <>
+
+
+ {context?.error ? (
+
{context.error}
) : (
<>
-
- Rejected
+ {context?.workspace && (
+
+
Linear Account
+
+ {context.workspace.organization_name}
+
+
+ )}
+
+ {issue && (
+
+
Current Issue
+
+
+ {issue.identifier}: {issue.title}
+
+
+ {issue.current_state && (
+
+ {issue.current_state.name}
+
+ )}
+ {issue.current_assignee && {issue.current_assignee.name} }
+ {priorities.find((p) => p.priority === issue.priority) && (
+ {priorities.find((p) => p.priority === issue.priority)?.label}
+ )}
+
+ {issue.current_labels && issue.current_labels.length > 0 && (
+
+ {issue.current_labels.map((label) => (
+
+ {label.name}
+
+ ))}
+
+ )}
+ {issue.url && (
+
+ Open in Linear ↗
+
+ )}
+
+
+ )}
+
+ {/* Editable context selects for state, assignee, priority, labels */}
+ {team && (
+ <>
+
+
State
+
setEditedArgs({ ...editedArgs, stateId: v })}
+ >
+
+
+
+
+ {team.states.map((s) => (
+
+ {s.name}
+
+ ))}
+
+
+
+
+
+
Assignee
+
setEditedArgs({ ...editedArgs, assigneeId: v })}
+ >
+
+
+
+
+ Unassigned
+ {team.members
+ .filter((m) => m.active)
+ .map((m) => (
+
+ {m.name} ({m.email})
+
+ ))}
+
+
+
+
+
+
Priority
+
setEditedArgs({ ...editedArgs, priority: v })}
+ >
+
+
+
+
+ {priorities.map((p) => (
+
+ {p.label}
+
+ ))}
+
+
+
+
+ {team.labels.length > 0 && (
+
+
Labels
+
+ {team.labels.map((label) => {
+ const isSelected = editedArgs.labelIds.includes(label.id);
+ return (
+
+ setEditedArgs({
+ ...editedArgs,
+ labelIds: isSelected
+ ? editedArgs.labelIds.filter((id) => id !== label.id)
+ : [...editedArgs.labelIds, label.id],
+ })
+ }
+ className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
+ isSelected
+ ? "opacity-100 ring-2 ring-foreground/30"
+ : "opacity-50 hover:opacity-80"
+ }`}
+ style={{
+ backgroundColor: `${label.color}33`,
+ color: label.color,
+ }}
+ >
+
+ {label.name}
+
+ );
+ })}
+
+
+ )}
+ >
+ )}
>
)}
-
- ) : isEditing ? (
+
+ >
+ )}
+
+ {/* Content preview — proposed changes */}
+
+
+ {hasProposedChanges ? (
<>
-
{
- setDecided("edit");
- setIsEditing(false);
- onDecision({
- type: "edit",
- edited_action: {
- name: interruptData.action_requests[0].name,
- args: buildFinalArgs(),
- },
- });
- }}
- >
-
- Approve with Changes
-
-
{
- setIsEditing(false);
- setEditedArgs(initialEditState); // Reset to original args
- }}
- >
- Cancel
-
+ {actionArgs.new_title && (
+
{String(actionArgs.new_title)}
+ )}
+ {actionArgs.new_description && (
+
+ )}
+ {proposedStateName && (
+
+ State →
+ {proposedStateName}
+
+ )}
+ {proposedAssigneeName && (
+
+ Assignee →
+ {proposedAssigneeName}
+
+ )}
+ {proposedPriorityLabel && (
+
+ Priority →
+ {proposedPriorityLabel}
+
+ )}
+ {proposedLabelObjects.length > 0 && (
+
+ {proposedLabelObjects.map((label) => (
+
+ {label.name}
+
+ ))}
+
+ )}
>
) : (
- <>
+
No changes proposed
+ )}
+
+
+ {/* Action buttons - only shown when pending */}
+ {!decided && (
+ <>
+
+
{allowedDecisions.includes("approve") && (
{
- setDecided("approve");
- onDecision({
- type: "approve",
- edited_action: {
- name: interruptData.action_requests[0].name,
- args: buildFinalArgs(),
- },
- });
- }}
+ className="rounded-lg gap-1.5"
+ onClick={handleApprove}
>
-
Approve
-
- )}
- {canEdit && (
-
setIsEditing(true)}>
-
- Edit
+
)}
{allowedDecisions.includes("reject") && (
{
setDecided("reject");
onDecision({ type: "reject", message: "User rejected the action." });
}}
>
-
Reject
)}
- >
- )}
-
+
+ >
+ )}
);
}
function ErrorCard({ result }: { result: ErrorResult }) {
return (
-
-
-
-
-
-
-
Failed to update Linear issue
-
+
+
+
Failed to update Linear issue
-
@@ -655,14 +616,10 @@ function ErrorCard({ result }: { result: ErrorResult }) {
function NotFoundCard({ result }: { result: NotFoundResult }) {
return (
-
-
-
-
-
-
+
);
@@ -670,18 +627,14 @@ function NotFoundCard({ result }: { result: NotFoundResult }) {
function SuccessCard({ result }: { result: SuccessResult }) {
return (
-
-
-
-
-
-
-
- {result.message || "Linear issue updated successfully"}
-
-
+
+
+
+ {result.message || "Linear issue updated successfully"}
+
-
+
+
Identifier:
{result.identifier}
@@ -719,8 +672,8 @@ export const UpdateLinearIssueToolUI = makeAssistantToolUI<
render: function UpdateLinearIssueUI({ result, status }) {
if (status.type === "running") {
return (
-
-
+
+
Preparing Linear issue update...
);