From 5bcda6b83bf84ecd3608fe5c70164242f304e459 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 18 May 2026 01:34:41 +0530 Subject: [PATCH] refactor: replace action log sheet with dialog component and update related references --- .../new-chat/[[...chat_id]]/page.tsx | 2 +- .../components/AgentStatusContent.tsx | 2 +- .../atoms/agent/action-log-dialog.atom.ts | 19 +++ .../atoms/agent/action-log-sheet.atom.ts | 19 --- .../agent-action-log/action-log-button.tsx | 6 +- ...on-log-sheet.tsx => action-log-dialog.tsx} | 63 +++++---- .../agent-action-log/action-log-item.tsx | 130 ++++++++++++++---- .../assistant-ui/revert-turn-button.tsx | 2 +- .../layout/providers/LayoutDataProvider.tsx | 6 +- .../tool-registry/fallback/use-tool-action.ts | 2 +- .../hooks/use-agent-actions-query.ts | 8 +- 11 files changed, 168 insertions(+), 91 deletions(-) create mode 100644 surfsense_web/atoms/agent/action-log-dialog.atom.ts delete mode 100644 surfsense_web/atoms/agent/action-log-sheet.atom.ts rename surfsense_web/components/agent-action-log/{action-log-sheet.tsx => action-log-dialog.tsx} (75%) diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index c3a18ea87..ed4ed6ce1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -451,7 +451,7 @@ export default function NewChatPage() { }, [params.search_space_id]); // Unified store for agent-action rows (the same react-query cache - // the agent-actions sheet, the inline Revert button, and the + // the agent-actions dialog, the inline Revert button, and the // per-turn Revert button all read). Hydrates from // ``GET /threads/{id}/actions`` and is updated incrementally by the // SSE handlers + revert-batch results below — no atom side-channel. diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentStatusContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentStatusContent.tsx index 1c20b79c2..1fa957a6a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentStatusContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentStatusContent.tsx @@ -137,7 +137,7 @@ const FLAG_GROUPS: FlagGroup[] = [ { id: "tier5", title: "Tier 5 — Audit + revert", - subtitle: "Action log + revert route used by the Agent Actions sheet.", + subtitle: "Action log + revert route used by the Agent Actions dialog.", flags: [ { key: "enable_action_log", diff --git a/surfsense_web/atoms/agent/action-log-dialog.atom.ts b/surfsense_web/atoms/agent/action-log-dialog.atom.ts new file mode 100644 index 000000000..bb0c386d8 --- /dev/null +++ b/surfsense_web/atoms/agent/action-log-dialog.atom.ts @@ -0,0 +1,19 @@ +import { atom } from "jotai"; + +interface ActionLogDialogState { + open: boolean; + threadId: number | null; +} + +export const actionLogDialogAtom = atom({ + open: false, + threadId: null, +}); + +export const openActionLogDialogAtom = atom(null, (_get, set, threadId: number) => { + set(actionLogDialogAtom, { open: true, threadId }); +}); + +export const closeActionLogDialogAtom = atom(null, (_get, set) => { + set(actionLogDialogAtom, { open: false, threadId: null }); +}); diff --git a/surfsense_web/atoms/agent/action-log-sheet.atom.ts b/surfsense_web/atoms/agent/action-log-sheet.atom.ts deleted file mode 100644 index f88d3ed1e..000000000 --- a/surfsense_web/atoms/agent/action-log-sheet.atom.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { atom } from "jotai"; - -interface ActionLogSheetState { - open: boolean; - threadId: number | null; -} - -export const actionLogSheetAtom = atom({ - open: false, - threadId: null, -}); - -export const openActionLogSheetAtom = atom(null, (_get, set, threadId: number) => { - set(actionLogSheetAtom, { open: true, threadId }); -}); - -export const closeActionLogSheetAtom = atom(null, (_get, set) => { - set(actionLogSheetAtom, { open: false, threadId: null }); -}); diff --git a/surfsense_web/components/agent-action-log/action-log-button.tsx b/surfsense_web/components/agent-action-log/action-log-button.tsx index 358acc8e7..6e4c72fc5 100644 --- a/surfsense_web/components/agent-action-log/action-log-button.tsx +++ b/surfsense_web/components/agent-action-log/action-log-button.tsx @@ -3,7 +3,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { Workflow } from "lucide-react"; import { useCallback } from "react"; -import { openActionLogSheetAtom } from "@/atoms/agent/action-log-sheet.atom"; +import { openActionLogDialogAtom } from "@/atoms/agent/action-log-dialog.atom"; import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -13,7 +13,7 @@ interface ActionLogButtonProps { } /** - * Header button that opens the agent action log sheet for the current + * Header button that opens the agent action log dialog for the current * thread. Renders nothing when: * - the action log feature flag is off (graceful no-op for older * deployments), OR @@ -21,7 +21,7 @@ interface ActionLogButtonProps { */ export function ActionLogButton({ threadId }: ActionLogButtonProps) { const { data: flags } = useAtomValue(agentFlagsAtom); - const open = useSetAtom(openActionLogSheetAtom); + const open = useSetAtom(openActionLogDialogAtom); const enabled = !!flags?.enable_action_log && !flags?.disable_new_agent_stack; diff --git a/surfsense_web/components/agent-action-log/action-log-sheet.tsx b/surfsense_web/components/agent-action-log/action-log-dialog.tsx similarity index 75% rename from surfsense_web/components/agent-action-log/action-log-sheet.tsx rename to surfsense_web/components/agent-action-log/action-log-dialog.tsx index 1ce5dd20c..75f2408ae 100644 --- a/surfsense_web/components/agent-action-log/action-log-sheet.tsx +++ b/surfsense_web/components/agent-action-log/action-log-dialog.tsx @@ -2,19 +2,19 @@ import { useQueryClient } from "@tanstack/react-query"; import { useAtom, useAtomValue } from "jotai"; -import { RefreshCcw, Workflow, } from "lucide-react"; +import { RefreshCcw, Workflow } from "lucide-react"; import { useCallback } from "react"; -import { actionLogSheetAtom } from "@/atoms/agent/action-log-sheet.atom"; +import { actionLogDialogAtom } from "@/atoms/agent/action-log-dialog.atom"; import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { agentActionsQueryKey, useAgentActionsQuery } from "@/hooks/use-agent-actions-query"; import { ActionLogItem } from "./action-log-item"; @@ -25,7 +25,7 @@ function EmptyState() {

No actions logged yet

- A complete audit trail of every tool the agent uses in this thread will appear here + A complete audit trail of every tool the agent uses in this thread will appear here

@@ -64,8 +64,8 @@ function LoadingState() { ); } -export function ActionLogSheet() { - const [state, setState] = useAtom(actionLogSheetAtom); +export function ActionLogDialog() { + const [state, setState] = useAtom(actionLogDialogAtom); const queryClient = useQueryClient(); const { data: flags } = useAtomValue(agentFlagsAtom); @@ -78,6 +78,13 @@ export function ActionLogSheet() { { enabled: state.open && actionLogEnabled } ); + const handleOpenChange = useCallback( + (open: boolean) => { + setState((current) => (open ? { ...current, open } : { open: false, threadId: null })); + }, + [setState] + ); + const handleRevertSuccess = useCallback(() => { if (threadId !== null) { queryClient.invalidateQueries({ queryKey: agentActionsQueryKey(threadId) }); @@ -85,24 +92,22 @@ export function ActionLogSheet() { }, [queryClient, threadId]); return ( - setState((s) => ({ ...s, open }))}> - - -
- Agent actions - {data?.total !== undefined && data.total > 0 && ( + + +
+
+ Agent actions + {data?.total !== undefined && data.total > 0 ? ( {data.total} - )} + ) : null}
- + Audit trail of every tool call the agent made in this thread. - - + + +
); } diff --git a/surfsense_web/components/agent-action-log/action-log-item.tsx b/surfsense_web/components/agent-action-log/action-log-item.tsx index 954ee9aa5..7b57293ad 100644 --- a/surfsense_web/components/agent-action-log/action-log-item.tsx +++ b/surfsense_web/components/agent-action-log/action-log-item.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronRight, RotateCcw, ShieldOff, Undo2 } from "lucide-react"; +import { Check, ChevronRight, Copy, RotateCcw, Undo2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { @@ -16,7 +16,6 @@ import { } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; import { getToolDisplayName, getToolIcon } from "@/contracts/enums/toolIcons"; import { type AgentAction, agentActionsApiService } from "@/lib/apis/agent-actions-api.service"; import { AppError } from "@/lib/error"; @@ -29,10 +28,55 @@ interface ActionLogItemProps { onRevertSuccess: () => void; } +function formatPrimitiveValue(value: unknown) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value, null, 2); +} + +function ArgumentValue({ value }: { value: unknown }) { + const formatted = formatPrimitiveValue(value); + const isBlockValue = + typeof value === "object" || + (typeof value === "string" && (value.includes("\n") || value.length > 120)); + + if (isBlockValue) { + return ( +
+				{formatted}
+			
+ ); + } + + return ( +

+ {formatted} +

+ ); +} + +function StructuredArguments({ args }: { args: Record }) { + return ( +
+ {Object.entries(args).map(([key, value]) => ( +
+
+

{key}

+ +
+
+ ))} +
+ ); +} + export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogItemProps) { const [isExpanded, setIsExpanded] = useState(false); const [isReverting, setIsReverting] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); + const [copiedSection, setCopiedSection] = useState<"arguments" | null>(null); const isAlreadyReverted = action.reverted_by_action_id !== null; const isRevertAction = action.is_revert_action; @@ -42,11 +86,22 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt const displayName = getToolDisplayName(action.tool_name); const argsPreview = action.args ? JSON.stringify(action.args, null, 2) : null; - const truncatedArgs = - argsPreview && argsPreview.length > 600 ? `${argsPreview.slice(0, 600)}…` : argsPreview; const canRevert = action.reversible && !isAlreadyReverted && !isRevertAction && !hasError; + const handleCopyArguments = async () => { + if (!argsPreview) return; + + try { + await navigator.clipboard.writeText(argsPreview); + setCopiedSection("arguments"); + toast.success("Arguments copied"); + window.setTimeout(() => setCopiedSection(null), 1200); + } catch { + toast.error("Failed to copy arguments."); + } + }; + const handleRevert = async () => { setIsReverting(true); try { @@ -70,7 +125,7 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt return (
@@ -78,10 +133,10 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt type="button" variant="ghost" onClick={() => setIsExpanded((v) => !v)} - className="h-auto w-full items-start justify-start gap-3 p-3 text-left hover:bg-accent hover:text-accent-foreground" + className="h-auto w-full items-start justify-start gap-3 rounded-none p-3 text-left hover:bg-accent hover:text-accent-foreground" aria-expanded={isExpanded} > -
+
{isRevertAction ? ( ) : ( @@ -102,7 +157,10 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt )} {!isRevertAction && action.reversible && !isAlreadyReverted && ( - + Reversible )} @@ -116,55 +174,69 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
{isExpanded && ( -
- {truncatedArgs && ( -
-

- Arguments -

-
-								{truncatedArgs}
-							
+
+ {action.args && argsPreview && ( +
+
+

+ Arguments +

+ +
+
)} {action.error && ( -
-

+

+

Error

-
+							
 								{JSON.stringify(action.error, null, 2)}
 							
)} {action.reverse_descriptor && ( -
-

+

+

Reverse plan

-
+							
 								{JSON.stringify(action.reverse_descriptor, null, 2)}
 							
)} - - -
+

Action ID: {action.id}

{canRevert ? ( - @@ -186,6 +258,7 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt handleRevert(); }} disabled={isReverting} + className="bg-secondary text-secondary-foreground hover:bg-secondary/80 focus-visible:ring-0" > {isReverting ? "Reverting…" : "Revert"} @@ -194,7 +267,6 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt ) : (
- {isAlreadyReverted ? "Already reverted" : isRevertAction diff --git a/surfsense_web/components/assistant-ui/revert-turn-button.tsx b/surfsense_web/components/assistant-ui/revert-turn-button.tsx index 7b2725989..7e733fd99 100644 --- a/surfsense_web/components/assistant-ui/revert-turn-button.tsx +++ b/surfsense_web/components/assistant-ui/revert-turn-button.tsx @@ -5,7 +5,7 @@ * assistant turn that has at least one reversible action. * * The button reads from the unified ``useAgentActionsQuery`` cache - * (the SAME react-query cache the agent-actions sheet and the inline + * (the SAME react-query cache the agent-actions dialog and the inline * Revert button consume) filtered by ``chat_turn_id``. It shows a * confirmation dialog summarising "N reversible / M total" and, on * confirm, calls ``POST /threads/{id}/revert-turn/{chat_turn_id}``. diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index d774ae862..db44e8895 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -22,7 +22,7 @@ import { } from "@/atoms/settings/settings-dialog.atoms"; import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { ActionLogSheet } from "@/components/agent-action-log/action-log-sheet"; +import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog"; import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog"; import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog"; import { TeamDialog } from "@/components/settings/team-dialog"; @@ -893,8 +893,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid - {/* Agent action log + revert sheet */} - + {/* Agent action log + revert dialog */} + ); } diff --git a/surfsense_web/features/chat-messages/timeline/tool-registry/fallback/use-tool-action.ts b/surfsense_web/features/chat-messages/timeline/tool-registry/fallback/use-tool-action.ts index 9e34724b9..cd5265a25 100644 --- a/surfsense_web/features/chat-messages/timeline/tool-registry/fallback/use-tool-action.ts +++ b/surfsense_web/features/chat-messages/timeline/tool-registry/fallback/use-tool-action.ts @@ -10,7 +10,7 @@ import { useAgentActionsQuery } from "@/hooks/use-agent-actions-query"; * Resolve the ``AgentActionLog`` row for a given tool-call card. Tries * three lookup strategies, in priority order, against the unified * ``useAgentActionsQuery`` cache (the same react-query cache the - * agent-actions sheet consumes — keeps the card and the sheet in + * agent-actions dialog consumes — keeps the card and the dialog in * lockstep across reload, navigation, live stream, post-stream * reversibility flips, and explicit revert clicks). * diff --git a/surfsense_web/hooks/use-agent-actions-query.ts b/surfsense_web/hooks/use-agent-actions-query.ts index 114c79567..1e2f45a8a 100644 --- a/surfsense_web/hooks/use-agent-actions-query.ts +++ b/surfsense_web/hooks/use-agent-actions-query.ts @@ -35,12 +35,12 @@ const dbg = (...args: unknown[]) => { * * the per-turn "Revert turn" button under each assistant message * * the edit-from-position pre-flight that decides whether to show * the confirmation dialog - * * the agent-actions sheet + * * the agent-actions dialog * * The cache is hydrated by ``GET /threads/{id}/actions`` (sized to * 200, the server max) and updated incrementally by helpers that turn * SSE events / revert RPC responses into ``setQueryData`` mutations. - * That keeps the card and the sheet in lockstep on every code path — + * That keeps the card and the dialog in lockstep on every code path — * page reload, navigation, live stream, post-stream reversibility flip, * and explicit revert clicks. */ @@ -72,7 +72,7 @@ export interface ActionLogSseEvent { * * The SSE payload is a strict subset of ``AgentAction``; missing * fields (``args``, ``reverse_descriptor``, ``user_id``) are filled - * with ``null`` placeholders. The next refetch (sheet open, user + * with ``null`` placeholders. The next refetch (dialog open, user * focus, route stale) backfills them — but the inline Revert button * only reads the fields the SSE payload carries, so it lights up * immediately. @@ -251,7 +251,7 @@ export function applyRevertTurnResultsToCache( } /** - * Read-side hook used by the card, the turn button, the sheet, and + * Read-side hook used by the card, the turn button, the dialog, and * the edit-from-position pre-flight. * * Returns the raw query state plus convenience selectors so consumers