From a32d08919953549839001a055a40eab0108bed4c Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Sat, 9 May 2026 18:31:52 +0200 Subject: [PATCH] tool-ui: route HITL imports through chat-messages slice. --- .../confluence/create-confluence-page.tsx | 11 +- .../confluence/delete-confluence-page.tsx | 5 +- .../confluence/update-confluence-page.tsx | 11 +- .../components/tool-ui/doom-loop-approval.tsx | 187 ------------- .../tool-ui/dropbox/create-file.tsx | 11 +- .../components/tool-ui/dropbox/trash-file.tsx | 5 +- .../tool-ui/generic-hitl-approval.tsx | 263 ------------------ .../components/tool-ui/gmail/create-draft.tsx | 12 +- .../components/tool-ui/gmail/send-email.tsx | 12 +- .../components/tool-ui/gmail/trash-email.tsx | 5 +- .../components/tool-ui/gmail/update-draft.tsx | 12 +- .../tool-ui/google-calendar/create-event.tsx | 12 +- .../tool-ui/google-calendar/delete-event.tsx | 5 +- .../tool-ui/google-calendar/update-event.tsx | 12 +- .../tool-ui/google-drive/create-file.tsx | 11 +- .../tool-ui/google-drive/trash-file.tsx | 5 +- .../tool-ui/jira/create-jira-issue.tsx | 11 +- .../tool-ui/jira/delete-jira-issue.tsx | 5 +- .../tool-ui/jira/update-jira-issue.tsx | 11 +- .../tool-ui/linear/create-linear-issue.tsx | 11 +- .../tool-ui/linear/delete-linear-issue.tsx | 5 +- .../tool-ui/linear/update-linear-issue.tsx | 11 +- .../tool-ui/notion/create-notion-page.tsx | 11 +- .../tool-ui/notion/delete-notion-page.tsx | 5 +- .../tool-ui/notion/update-notion-page.tsx | 11 +- .../tool-ui/onedrive/create-file.tsx | 11 +- .../tool-ui/onedrive/trash-file.tsx | 5 +- 27 files changed, 130 insertions(+), 546 deletions(-) delete mode 100644 surfsense_web/components/tool-ui/doom-loop-approval.tsx delete mode 100644 surfsense_web/components/tool-ui/generic-hitl-approval.tsx diff --git a/surfsense_web/components/tool-ui/confluence/create-confluence-page.tsx b/surfsense_web/components/tool-ui/confluence/create-confluence-page.tsx index 1bef1f008..d5add7b8b 100644 --- a/surfsense_web/components/tool-ui/confluence/create-confluence-page.tsx +++ b/surfsense_web/components/tool-ui/confluence/create-confluence-page.tsx @@ -4,7 +4,6 @@ import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, Pencil } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; @@ -15,9 +14,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; +import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; +import { + isInterruptResult, + openHitlEditPanelAtom, + useHitlDecision, + useHitlPhase, +} from "@/features/chat-messages/hitl"; interface ConfluenceAccount { id: number; diff --git a/surfsense_web/components/tool-ui/confluence/delete-confluence-page.tsx b/surfsense_web/components/tool-ui/confluence/delete-confluence-page.tsx index 74326cf87..68ecca6a4 100644 --- a/surfsense_web/components/tool-ui/confluence/delete-confluence-page.tsx +++ b/surfsense_web/components/tool-ui/confluence/delete-confluence-page.tsx @@ -6,9 +6,8 @@ import { useCallback, useEffect, useState } from "react"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; +import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; +import { isInterruptResult, useHitlDecision, useHitlPhase } from "@/features/chat-messages/hitl"; type DeleteConfluencePageInterruptContext = { account?: { diff --git a/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx b/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx index c30357fb6..436d69532 100644 --- a/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx +++ b/surfsense_web/components/tool-ui/confluence/update-confluence-page.tsx @@ -4,13 +4,16 @@ import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, Pencil } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; -import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; +import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; +import { + isInterruptResult, + openHitlEditPanelAtom, + useHitlDecision, + useHitlPhase, +} from "@/features/chat-messages/hitl"; type UpdateConfluencePageInterruptContext = { account?: { diff --git a/surfsense_web/components/tool-ui/doom-loop-approval.tsx b/surfsense_web/components/tool-ui/doom-loop-approval.tsx deleted file mode 100644 index 6132a71ed..000000000 --- a/surfsense_web/components/tool-ui/doom-loop-approval.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client"; - -import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; -import { CornerDownLeftIcon, OctagonAlert } from "lucide-react"; -import { useCallback, useEffect, useMemo } from "react"; -import { TextShimmerLoader } from "@/components/prompt-kit/loader"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; - -/** - * Specialized HITL card for ``DoomLoopMiddleware`` interrupts. The - * backend signals these by setting ``context.permission === "doom_loop"`` - * on the ``permission_ask`` interrupt. - * - * The card replaces the generic "approve/reject" framing with a - * "continue/stop" affordance that better matches the user's mental - * model: the agent is stuck repeating itself, not asking permission - * for a destructive action. - */ -function DoomLoopCard({ - toolName, - args, - interruptData, - onDecision, -}: { - toolName: string; - args: Record; - interruptData: InterruptResult; - onDecision: (decision: HitlDecision) => void; -}) { - const { phase, setProcessing, setRejected } = useHitlPhase(interruptData); - - const context = (interruptData.context ?? {}) as Record; - const threshold = typeof context.threshold === "number" ? context.threshold : 3; - const stuckTool = (typeof context.tool === "string" && context.tool) || toolName; - const recentSignatures = Array.isArray(context.recent_signatures) - ? (context.recent_signatures as string[]) - : []; - const displayName = stuckTool.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); - - const argPreview = useMemo(() => { - if (!args || Object.keys(args).length === 0) return null; - try { - const json = JSON.stringify(args, null, 2); - return json.length > 600 ? `${json.slice(0, 600)}…` : json; - } catch { - return null; - } - }, [args]); - - const handleContinue = useCallback(() => { - if (phase !== "pending") return; - setProcessing(); - onDecision({ type: "approve" }); - }, [phase, setProcessing, onDecision]); - - const handleStop = useCallback(() => { - if (phase !== "pending") return; - setRejected(); - onDecision({ type: "reject", message: "Doom loop: user requested stop." }); - }, [phase, setRejected, onDecision]); - - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (phase !== "pending") return; - if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey) { - e.preventDefault(); - handleStop(); - } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [phase, handleStop]); - - const isResolved = phase === "complete" || phase === "rejected"; - - return ( - - - - - {phase === "rejected" - ? "Stopped" - : phase === "processing" - ? "Continuing…" - : phase === "complete" - ? "Continued" - : "I might be stuck"} - - {!isResolved && ( - - doom-loop - - )} - - - {phase === "processing" ? ( - - ) : phase === "rejected" ? ( -

- I stopped retrying {displayName} as you asked. -

- ) : phase === "complete" ? ( -

- Continuing to call {displayName} as you asked. -

- ) : ( -

- I called {displayName} {threshold} times in a row - with similar arguments. Should I keep going or stop and rethink? -

- )} - - {argPreview && phase === "pending" && ( - <> - -
-

- Last arguments -

-
-								{argPreview}
-							
-
- - )} - - {recentSignatures.length > 0 && phase === "pending" && ( -
- - Show repeated signatures ({recentSignatures.length}) - -
    - {recentSignatures.map((sig) => ( -
  • - {sig} -
  • - ))} -
-
- )} - - {phase === "pending" && ( -
- - -
- )} -
-
- ); -} - -export const DoomLoopApprovalToolUI: ToolCallMessagePartComponent = ({ - toolName, - args, - result, -}) => { - const { dispatch } = useHitlDecision(); - - if (!result || !isInterruptResult(result)) return null; - - return ( - } - interruptData={result} - onDecision={(decision) => dispatch([decision])} - /> - ); -}; - -export function isDoomLoopInterrupt(result: unknown): boolean { - if (!isInterruptResult(result)) return false; - const ctx = (result.context ?? {}) as Record; - return ctx.permission === "doom_loop"; -} diff --git a/surfsense_web/components/tool-ui/dropbox/create-file.tsx b/surfsense_web/components/tool-ui/dropbox/create-file.tsx index f76a45f62..d810f88c2 100644 --- a/surfsense_web/components/tool-ui/dropbox/create-file.tsx +++ b/surfsense_web/components/tool-ui/dropbox/create-file.tsx @@ -4,7 +4,6 @@ import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { useSetAtom } from "jotai"; import { CornerDownLeftIcon, FileIcon, Pencil } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { openHitlEditPanelAtom } from "@/atoms/chat/hitl-edit-panel.atom"; import { PlateEditor } from "@/components/editor/plate-editor"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; @@ -15,9 +14,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; +import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; +import { + isInterruptResult, + openHitlEditPanelAtom, + useHitlDecision, + useHitlPhase, +} from "@/features/chat-messages/hitl"; interface DropboxAccount { id: number; diff --git a/surfsense_web/components/tool-ui/dropbox/trash-file.tsx b/surfsense_web/components/tool-ui/dropbox/trash-file.tsx index 0356da41e..57dff9d38 100644 --- a/surfsense_web/components/tool-ui/dropbox/trash-file.tsx +++ b/surfsense_web/components/tool-ui/dropbox/trash-file.tsx @@ -6,9 +6,8 @@ import { useCallback, useEffect, useState } from "react"; import { TextShimmerLoader } from "@/components/prompt-kit/loader"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; +import type { HitlDecision, InterruptResult } from "@/features/chat-messages/hitl"; +import { isInterruptResult, useHitlDecision, useHitlPhase } from "@/features/chat-messages/hitl"; interface DropboxAccount { id: number; diff --git a/surfsense_web/components/tool-ui/generic-hitl-approval.tsx b/surfsense_web/components/tool-ui/generic-hitl-approval.tsx deleted file mode 100644 index a584084ff..000000000 --- a/surfsense_web/components/tool-ui/generic-hitl-approval.tsx +++ /dev/null @@ -1,263 +0,0 @@ -"use client"; - -import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; -import { CornerDownLeftIcon, Pencil } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { TextShimmerLoader } from "@/components/prompt-kit/loader"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { getToolDisplayName } from "@/contracts/enums/toolIcons"; -import { useHitlPhase } from "@/hooks/use-hitl-phase"; -import { connectorsApiService } from "@/lib/apis/connectors-api.service"; -import type { HitlDecision, InterruptResult } from "@/lib/hitl"; -import { isInterruptResult, useHitlDecision } from "@/lib/hitl"; - -function ParamEditor({ - params, - onChange, - disabled, -}: { - params: Record; - onChange: (updated: Record) => void; - disabled: boolean; -}) { - const entries = Object.entries(params); - if (entries.length === 0) return null; - - return ( -
- {entries.map(([key, value]) => { - const strValue = value == null ? "" : String(value); - const isLong = strValue.length > 120; - const fieldId = `hitl-param-${key}`; - - return ( -
- - {isLong ? ( -