mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: replace action log sheet with dialog component and update related references
This commit is contained in:
parent
c580addc04
commit
5bcda6b83b
11 changed files with 168 additions and 91 deletions
|
|
@ -451,7 +451,7 @@ export default function NewChatPage() {
|
||||||
}, [params.search_space_id]);
|
}, [params.search_space_id]);
|
||||||
|
|
||||||
// Unified store for agent-action rows (the same react-query cache
|
// 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
|
// per-turn Revert button all read). Hydrates from
|
||||||
// ``GET /threads/{id}/actions`` and is updated incrementally by the
|
// ``GET /threads/{id}/actions`` and is updated incrementally by the
|
||||||
// SSE handlers + revert-batch results below — no atom side-channel.
|
// SSE handlers + revert-batch results below — no atom side-channel.
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ const FLAG_GROUPS: FlagGroup[] = [
|
||||||
{
|
{
|
||||||
id: "tier5",
|
id: "tier5",
|
||||||
title: "Tier 5 — Audit + revert",
|
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: [
|
flags: [
|
||||||
{
|
{
|
||||||
key: "enable_action_log",
|
key: "enable_action_log",
|
||||||
|
|
|
||||||
19
surfsense_web/atoms/agent/action-log-dialog.atom.ts
Normal file
19
surfsense_web/atoms/agent/action-log-dialog.atom.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
interface ActionLogDialogState {
|
||||||
|
open: boolean;
|
||||||
|
threadId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionLogDialogAtom = atom<ActionLogDialogState>({
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
interface ActionLogSheetState {
|
|
||||||
open: boolean;
|
|
||||||
threadId: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actionLogSheetAtom = atom<ActionLogSheetState>({
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { Workflow } from "lucide-react";
|
import { Workflow } from "lucide-react";
|
||||||
import { useCallback } from "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 { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
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:
|
* thread. Renders nothing when:
|
||||||
* - the action log feature flag is off (graceful no-op for older
|
* - the action log feature flag is off (graceful no-op for older
|
||||||
* deployments), OR
|
* deployments), OR
|
||||||
|
|
@ -21,7 +21,7 @@ interface ActionLogButtonProps {
|
||||||
*/
|
*/
|
||||||
export function ActionLogButton({ threadId }: ActionLogButtonProps) {
|
export function ActionLogButton({ threadId }: ActionLogButtonProps) {
|
||||||
const { data: flags } = useAtomValue(agentFlagsAtom);
|
const { data: flags } = useAtomValue(agentFlagsAtom);
|
||||||
const open = useSetAtom(openActionLogSheetAtom);
|
const open = useSetAtom(openActionLogDialogAtom);
|
||||||
|
|
||||||
const enabled = !!flags?.enable_action_log && !flags?.disable_new_agent_stack;
|
const enabled = !!flags?.enable_action_log && !flags?.disable_new_agent_stack;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@
|
||||||
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { RefreshCcw, Workflow, } from "lucide-react";
|
import { RefreshCcw, Workflow } from "lucide-react";
|
||||||
import { useCallback } from "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 { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Dialog,
|
||||||
SheetContent,
|
DialogContent,
|
||||||
SheetDescription,
|
DialogDescription,
|
||||||
SheetHeader,
|
DialogTitle,
|
||||||
SheetTitle,
|
} from "@/components/ui/dialog";
|
||||||
} from "@/components/ui/sheet";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { agentActionsQueryKey, useAgentActionsQuery } from "@/hooks/use-agent-actions-query";
|
import { agentActionsQueryKey, useAgentActionsQuery } from "@/hooks/use-agent-actions-query";
|
||||||
import { ActionLogItem } from "./action-log-item";
|
import { ActionLogItem } from "./action-log-item";
|
||||||
|
|
@ -64,8 +64,8 @@ function LoadingState() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionLogSheet() {
|
export function ActionLogDialog() {
|
||||||
const [state, setState] = useAtom(actionLogSheetAtom);
|
const [state, setState] = useAtom(actionLogDialogAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: flags } = useAtomValue(agentFlagsAtom);
|
const { data: flags } = useAtomValue(agentFlagsAtom);
|
||||||
|
|
@ -78,6 +78,13 @@ export function ActionLogSheet() {
|
||||||
{ enabled: state.open && actionLogEnabled }
|
{ enabled: state.open && actionLogEnabled }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
setState((current) => (open ? { ...current, open } : { open: false, threadId: null }));
|
||||||
|
},
|
||||||
|
[setState]
|
||||||
|
);
|
||||||
|
|
||||||
const handleRevertSuccess = useCallback(() => {
|
const handleRevertSuccess = useCallback(() => {
|
||||||
if (threadId !== null) {
|
if (threadId !== null) {
|
||||||
queryClient.invalidateQueries({ queryKey: agentActionsQueryKey(threadId) });
|
queryClient.invalidateQueries({ queryKey: agentActionsQueryKey(threadId) });
|
||||||
|
|
@ -85,24 +92,22 @@ export function ActionLogSheet() {
|
||||||
}, [queryClient, threadId]);
|
}, [queryClient, threadId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={state.open} onOpenChange={(open) => setState((s) => ({ ...s, open }))}>
|
<Dialog open={state.open} onOpenChange={handleOpenChange}>
|
||||||
<SheetContent
|
<DialogContent className="select-none flex h-[90vh] max-h-[640px] w-[95vw] max-w-[900px] flex-col gap-0 overflow-hidden p-0 [--card:var(--popover)] md:h-[80vh]">
|
||||||
side="right"
|
<div className="shrink-0 px-6 pb-3 pt-6 pr-28">
|
||||||
className="flex h-full w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-md select-none"
|
<div className="flex items-center gap-2">
|
||||||
>
|
<DialogTitle className="text-lg font-semibold">Agent actions</DialogTitle>
|
||||||
<SheetHeader className="shrink-0 px-4 py-4">
|
{data?.total !== undefined && data.total > 0 ? (
|
||||||
<div className="flex items-center gap-2 pr-24">
|
|
||||||
<SheetTitle className="text-base font-semibold">Agent actions</SheetTitle>
|
|
||||||
{data?.total !== undefined && data.total > 0 && (
|
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
{data.total}
|
{data.total}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<SheetDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
Audit trail of every tool call the agent made in this thread.
|
Audit trail of every tool call the agent made in this thread.
|
||||||
</SheetDescription>
|
</DialogDescription>
|
||||||
</SheetHeader>
|
<Separator className="mt-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -135,7 +140,7 @@ export function ActionLogSheet() {
|
||||||
) : items.length === 0 ? (
|
) : items.length === 0 ? (
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 p-3">
|
<div className="flex flex-col gap-2 px-4 pb-4">
|
||||||
{items.map((action) => (
|
{items.map((action) => (
|
||||||
<ActionLogItem
|
<ActionLogItem
|
||||||
key={action.id}
|
key={action.id}
|
||||||
|
|
@ -144,15 +149,15 @@ export function ActionLogSheet() {
|
||||||
onRevertSuccess={handleRevertSuccess}
|
onRevertSuccess={handleRevertSuccess}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{data?.has_more && (
|
{data?.has_more ? (
|
||||||
<p className="py-2 text-center text-[11px] text-muted-foreground">
|
<p className="py-2 text-center text-[11px] text-muted-foreground">
|
||||||
Showing {items.length} of {data.total}. Older actions are paginated.
|
Showing {items.length} of {data.total}. Older actions are paginated.
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</DialogContent>
|
||||||
</Sheet>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronRight, RotateCcw, ShieldOff, Undo2 } from "lucide-react";
|
import { Check, ChevronRight, Copy, RotateCcw, Undo2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
|
@ -16,7 +16,6 @@ import {
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { getToolDisplayName, getToolIcon } from "@/contracts/enums/toolIcons";
|
import { getToolDisplayName, getToolIcon } from "@/contracts/enums/toolIcons";
|
||||||
import { type AgentAction, agentActionsApiService } from "@/lib/apis/agent-actions-api.service";
|
import { type AgentAction, agentActionsApiService } from "@/lib/apis/agent-actions-api.service";
|
||||||
import { AppError } from "@/lib/error";
|
import { AppError } from "@/lib/error";
|
||||||
|
|
@ -29,10 +28,55 @@ interface ActionLogItemProps {
|
||||||
onRevertSuccess: () => void;
|
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 (
|
||||||
|
<pre className="mt-2 whitespace-pre-wrap break-words bg-popover px-4 py-3 text-[11px] leading-relaxed text-popover-foreground/80">
|
||||||
|
{formatted}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="mt-1 break-words font-mono text-[11px] leading-relaxed text-popover-foreground/80">
|
||||||
|
{formatted}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StructuredArguments({ args }: { args: Record<string, unknown> }) {
|
||||||
|
return (
|
||||||
|
<div className="divide-y divide-popover-border border-t border-popover-border">
|
||||||
|
{Object.entries(args).map(([key, value]) => (
|
||||||
|
<div key={key} className="bg-popover">
|
||||||
|
<div className="px-4 py-3">
|
||||||
|
<p className="font-mono text-[10px] font-medium text-muted-foreground">{key}</p>
|
||||||
|
<ArgumentValue value={value} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogItemProps) {
|
export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogItemProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [isReverting, setIsReverting] = useState(false);
|
const [isReverting, setIsReverting] = useState(false);
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [copiedSection, setCopiedSection] = useState<"arguments" | null>(null);
|
||||||
|
|
||||||
const isAlreadyReverted = action.reverted_by_action_id !== null;
|
const isAlreadyReverted = action.reverted_by_action_id !== null;
|
||||||
const isRevertAction = action.is_revert_action;
|
const isRevertAction = action.is_revert_action;
|
||||||
|
|
@ -42,11 +86,22 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
const displayName = getToolDisplayName(action.tool_name);
|
const displayName = getToolDisplayName(action.tool_name);
|
||||||
|
|
||||||
const argsPreview = action.args ? JSON.stringify(action.args, null, 2) : null;
|
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 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 () => {
|
const handleRevert = async () => {
|
||||||
setIsReverting(true);
|
setIsReverting(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -70,7 +125,7 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border bg-card transition-colors",
|
"overflow-hidden rounded-lg border border-popover-border bg-popover text-popover-foreground transition-colors",
|
||||||
isAlreadyReverted && "opacity-70"
|
isAlreadyReverted && "opacity-70"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -78,10 +133,10 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setIsExpanded((v) => !v)}
|
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}
|
aria-expanded={isExpanded}
|
||||||
>
|
>
|
||||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-accent">
|
||||||
{isRevertAction ? (
|
{isRevertAction ? (
|
||||||
<Undo2 className="size-4 text-muted-foreground" />
|
<Undo2 className="size-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -102,7 +157,10 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!isRevertAction && action.reversible && !isAlreadyReverted && (
|
{!isRevertAction && action.reversible && !isAlreadyReverted && (
|
||||||
<Badge variant="outline" className="text-[10px]">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-0 bg-neutral-200 px-1.5 py-0.5 text-[10px] text-neutral-700 dark:bg-neutral-700 dark:text-neutral-200"
|
||||||
|
>
|
||||||
Reversible
|
Reversible
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
@ -116,55 +174,69 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 shrink-0 text-muted-foreground transition-transform",
|
"size-4 shrink-0 self-center text-muted-foreground transition-transform",
|
||||||
isExpanded && "rotate-90"
|
isExpanded && "rotate-90"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="flex flex-col gap-3 border-t bg-muted/20 p-3">
|
<div className="flex flex-col border-t border-popover-border bg-accent/80">
|
||||||
{truncatedArgs && (
|
{action.args && argsPreview && (
|
||||||
<div>
|
<div className="border-b border-popover-border">
|
||||||
<p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
<div className="flex items-center justify-between px-4 py-2">
|
||||||
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Arguments
|
Arguments
|
||||||
</p>
|
</p>
|
||||||
<pre className="max-h-48 overflow-auto rounded-md bg-background p-2 text-[11px] text-foreground/80">
|
<Button
|
||||||
{truncatedArgs}
|
type="button"
|
||||||
</pre>
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleCopyArguments}
|
||||||
|
className="size-6 rounded-lg p-0 text-muted-foreground hover:bg-popover hover:text-popover-foreground"
|
||||||
|
aria-label={
|
||||||
|
copiedSection === "arguments" ? "Arguments copied" : "Copy arguments"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copiedSection === "arguments" ? (
|
||||||
|
<Check className="size-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="size-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<StructuredArguments args={action.args} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{action.error && (
|
{action.error && (
|
||||||
<div>
|
<div className="border-b border-popover-border">
|
||||||
<p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
<p className="px-4 py-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Error
|
Error
|
||||||
</p>
|
</p>
|
||||||
<pre className="max-h-32 overflow-auto rounded-md bg-destructive/10 p-2 text-[11px] text-destructive">
|
<pre className="max-h-32 overflow-auto border-t border-popover-border bg-destructive/10 px-4 py-3 text-[11px] text-destructive">
|
||||||
{JSON.stringify(action.error, null, 2)}
|
{JSON.stringify(action.error, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{action.reverse_descriptor && (
|
{action.reverse_descriptor && (
|
||||||
<div>
|
<div className="border-b border-popover-border">
|
||||||
<p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
<p className="px-4 py-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
Reverse plan
|
Reverse plan
|
||||||
</p>
|
</p>
|
||||||
<pre className="max-h-32 overflow-auto rounded-md bg-background p-2 text-[11px] text-foreground/80">
|
<pre className="max-h-32 overflow-auto border-t border-popover-border bg-popover px-4 py-3 text-[11px] text-popover-foreground/80">
|
||||||
{JSON.stringify(action.reverse_descriptor, null, 2)}
|
{JSON.stringify(action.reverse_descriptor, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator />
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
Action ID: <span className="font-mono">{action.id}</span>
|
Action ID: <span className="font-mono">{action.id}</span>
|
||||||
</p>
|
</p>
|
||||||
{canRevert ? (
|
{canRevert ? (
|
||||||
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button size="sm" variant="outline" className="gap-1.5">
|
<Button size="sm" variant="secondary" className="gap-1.5">
|
||||||
<RotateCcw className="size-3.5" />
|
<RotateCcw className="size-3.5" />
|
||||||
Revert
|
Revert
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -186,6 +258,7 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
handleRevert();
|
handleRevert();
|
||||||
}}
|
}}
|
||||||
disabled={isReverting}
|
disabled={isReverting}
|
||||||
|
className="bg-secondary text-secondary-foreground hover:bg-secondary/80 focus-visible:ring-0"
|
||||||
>
|
>
|
||||||
{isReverting ? "Reverting…" : "Revert"}
|
{isReverting ? "Reverting…" : "Revert"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
|
|
@ -194,7 +267,6 @@ export function ActionLogItem({ action, threadId, onRevertSuccess }: ActionLogIt
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||||
<ShieldOff className="size-3.5" />
|
|
||||||
{isAlreadyReverted
|
{isAlreadyReverted
|
||||||
? "Already reverted"
|
? "Already reverted"
|
||||||
: isRevertAction
|
: isRevertAction
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* assistant turn that has at least one reversible action.
|
* assistant turn that has at least one reversible action.
|
||||||
*
|
*
|
||||||
* The button reads from the unified ``useAgentActionsQuery`` cache
|
* 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
|
* Revert button consume) filtered by ``chat_turn_id``. It shows a
|
||||||
* confirmation dialog summarising "N reversible / M total" and, on
|
* confirmation dialog summarising "N reversible / M total" and, on
|
||||||
* confirm, calls ``POST /threads/{id}/revert-turn/{chat_turn_id}``.
|
* confirm, calls ``POST /threads/{id}/revert-turn/{chat_turn_id}``.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
} from "@/atoms/settings/settings-dialog.atoms";
|
} from "@/atoms/settings/settings-dialog.atoms";
|
||||||
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
|
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
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 { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
|
||||||
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
|
||||||
import { TeamDialog } from "@/components/settings/team-dialog";
|
import { TeamDialog } from "@/components/settings/team-dialog";
|
||||||
|
|
@ -893,8 +893,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
|
||||||
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
<TeamDialog searchSpaceId={Number(searchSpaceId)} />
|
||||||
<AnnouncementsDialog />
|
<AnnouncementsDialog />
|
||||||
|
|
||||||
{/* Agent action log + revert sheet */}
|
{/* Agent action log + revert dialog */}
|
||||||
<ActionLogSheet />
|
<ActionLogDialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { useAgentActionsQuery } from "@/hooks/use-agent-actions-query";
|
||||||
* Resolve the ``AgentActionLog`` row for a given tool-call card. Tries
|
* Resolve the ``AgentActionLog`` row for a given tool-call card. Tries
|
||||||
* three lookup strategies, in priority order, against the unified
|
* three lookup strategies, in priority order, against the unified
|
||||||
* ``useAgentActionsQuery`` cache (the same react-query cache the
|
* ``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
|
* lockstep across reload, navigation, live stream, post-stream
|
||||||
* reversibility flips, and explicit revert clicks).
|
* reversibility flips, and explicit revert clicks).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,12 @@ const dbg = (...args: unknown[]) => {
|
||||||
* * the per-turn "Revert turn" button under each assistant message
|
* * the per-turn "Revert turn" button under each assistant message
|
||||||
* * the edit-from-position pre-flight that decides whether to show
|
* * the edit-from-position pre-flight that decides whether to show
|
||||||
* the confirmation dialog
|
* the confirmation dialog
|
||||||
* * the agent-actions sheet
|
* * the agent-actions dialog
|
||||||
*
|
*
|
||||||
* The cache is hydrated by ``GET /threads/{id}/actions`` (sized to
|
* The cache is hydrated by ``GET /threads/{id}/actions`` (sized to
|
||||||
* 200, the server max) and updated incrementally by helpers that turn
|
* 200, the server max) and updated incrementally by helpers that turn
|
||||||
* SSE events / revert RPC responses into ``setQueryData`` mutations.
|
* 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,
|
* page reload, navigation, live stream, post-stream reversibility flip,
|
||||||
* and explicit revert clicks.
|
* and explicit revert clicks.
|
||||||
*/
|
*/
|
||||||
|
|
@ -72,7 +72,7 @@ export interface ActionLogSseEvent {
|
||||||
*
|
*
|
||||||
* The SSE payload is a strict subset of ``AgentAction``; missing
|
* The SSE payload is a strict subset of ``AgentAction``; missing
|
||||||
* fields (``args``, ``reverse_descriptor``, ``user_id``) are filled
|
* 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
|
* focus, route stale) backfills them — but the inline Revert button
|
||||||
* only reads the fields the SSE payload carries, so it lights up
|
* only reads the fields the SSE payload carries, so it lights up
|
||||||
* immediately.
|
* 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.
|
* the edit-from-position pre-flight.
|
||||||
*
|
*
|
||||||
* Returns the raw query state plus convenience selectors so consumers
|
* Returns the raw query state plus convenience selectors so consumers
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue