Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chat-history-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": minor
---

Add chat history panel: a dedicated History tab in the agent panel header that shows a searchable list of all previous conversations. Clicking a conversation switches back to chat mode with that thread active.
14 changes: 14 additions & 0 deletions packages/core/src/client/MultiTabAssistantChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,20 @@ export function MultiTabAssistantChat({
[openTabIds, switchThread],
);

// Listen for history panel thread selection (from AgentPanel history mode)
const openFromHistoryRef = useRef(openFromHistory);
openFromHistoryRef.current = openFromHistory;
useEffect(() => {
function handleOpenThread(e: Event) {
const threadId = (e as CustomEvent<{ threadId: string }>).detail
?.threadId;
if (threadId) openFromHistoryRef.current(threadId);
}
window.addEventListener("agent-panel:open-thread", handleOpenThread);
return () =>
window.removeEventListener("agent-panel:open-thread", handleOpenThread);
}, []);

// Listen for agent-task-open events (from AgentTaskCard "Open" button)
useEffect(() => {
function handleOpenTask(e: Event) {
Expand Down
58 changes: 58 additions & 0 deletions templates/analytics/actions/keep-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { defineAction } from "@agent-native/core";
import {
getRequestUserEmail,
getRequestOrgId,
} from "@agent-native/core/server";
import { z } from "zod";
import { keepDashboard, keepAnalysis } from "../server/lib/dashboards-store";

function resolveScope() {
const orgId = getRequestOrgId() || null;
const email = getRequestUserEmail();
if (!email) throw new Error("no authenticated user");
return { orgId, email };
}

export default defineAction({
description:
"Mark a dashboard or analysis as 'kept' during the one-time cleanup pass. " +
"All existing resources were made org-visible so teammates can review them. " +
"Resources without a keep mark will be deleted after the pass is complete. " +
"Any org member with read access can keep a resource.",
schema: z.object({
resourceType: z
.enum(["dashboard", "analysis"])
.describe("Whether to keep a dashboard or an analysis"),
resourceId: z.string().describe("The ID of the resource to keep"),
}),
run: async (args) => {
const ctx = resolveScope();
if (args.resourceType === "dashboard") {
const dash = await keepDashboard(args.resourceId, ctx);
if (!dash) {
throw new Error(
`Dashboard "${args.resourceId}" not found (or you don't have access).`,
);
}
return {
id: dash.id,
name: dash.title,
keptAt: dash.keptAt,
message: `Dashboard "${dash.title}" marked as kept — it will survive the cleanup pass.`,
};
} else {
const analysis = await keepAnalysis(args.resourceId, ctx);
if (!analysis) {
throw new Error(
`Analysis "${args.resourceId}" not found (or you don't have access).`,
);
}
return {
id: analysis.id,
name: analysis.name,
keptAt: analysis.keptAt,
message: `Analysis "${analysis.name}" marked as kept — it will survive the cleanup pass.`,
};
}
},
});
83 changes: 83 additions & 0 deletions templates/analytics/app/components/KeepBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { IconBookmark, IconBookmarkFilled, IconX } from "@tabler/icons-react";
import { useActionMutation } from "@agent-native/core/client";
import { toast } from "sonner";
import { cn } from "@/lib/utils";

interface KeepBannerProps {
resourceType: "dashboard" | "analysis";
resourceId: string;
resourceName: string;
keptAt: string | null | undefined;
onKept?: () => void;
}

/**
* Banner shown on dashboards and analyses during the one-time cleanup pass.
* Users click "Keep" to mark a resource as wanted; unclaimed resources will
* be deleted after the pass ends and visibility returns to private-by-default.
*/
export function KeepBanner({
resourceType,
resourceId,
resourceName,
keptAt,
onKept,
}: KeepBannerProps) {
const [dismissed, setDismissed] = useState(false);
const { mutateAsync: keepResource, isPending } =
useActionMutation("keep-resource");

if (dismissed) return null;

if (keptAt) {
return (
<div className="flex items-center gap-2 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-800 dark:border-green-900 dark:bg-green-950/40 dark:text-green-300">
<IconBookmarkFilled className="size-4 shrink-0" />
<span>Marked as kept — this will survive the cleanup pass.</span>
<button
onClick={() => setDismissed(true)}
className="ml-auto text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-100"
aria-label="Dismiss"
>
<IconX className="size-3.5" />
</button>
</div>
);
}

return (
<div
className={cn(
"flex flex-wrap items-center gap-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm dark:border-amber-800 dark:bg-amber-950/40",
)}
>
<IconBookmark className="size-4 shrink-0 text-amber-600 dark:text-amber-400" />
<span className="text-amber-900 dark:text-amber-200">
<strong>Cleanup pass:</strong> all dashboards and analyses are
temporarily org-visible. Click <strong>Keep</strong> to mark this one as
wanted — anything unclaimed will be deleted and everything goes private
again after the pass.
</span>
<Button
size="sm"
variant="outline"
className="ml-auto shrink-0 border-amber-400 bg-amber-100 text-amber-900 hover:bg-amber-200 dark:border-amber-700 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-800/60"
disabled={isPending}
onClick={async () => {
try {
await keepResource({ resourceType, resourceId });
toast.success(`"${resourceName}" marked as kept`);
onKept?.();
} catch (err: any) {
toast.error(err?.message ?? "Failed to mark as kept");
}
}}
>
<IconBookmarkFilled className="mr-1.5 size-3.5" />
Keep
</Button>
</div>
);
}
Loading
Loading