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
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ describe("AgentMessageMarkdown - Integration Tests", () => {
const { container } = render(
<ConversationSidePanelContext.Provider
value={{
canGoBack: false,
goBack: vi.fn(),
currentPanel: undefined,
isPanelClosing: false,
openPanel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ vi.mock(
async (importOriginal) => ({
...(await importOriginal()),
useConversationSidePanelContext: () => ({
canGoBack: false,
goBack: vi.fn(),
currentPanel: undefined,
isPanelClosing: false,
openPanel: vi.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,80 @@ describe("ConversationSidePanelProvider selection", () => {
});
});

describe("ConversationSidePanelProvider history", () => {
beforeEach(resetHash);

it("starts with no history", () => {
const probe = renderProvider();

expect(probe.panel.canGoBack).toBe(false);
});

it("goes back to the previously shown panel", () => {
const probe = renderProvider();

act(() => probe.panel.openPanel({ type: "files" }));
expect(probe.panel.canGoBack).toBe(false);

act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "a.md" })
);
expect(probe.panel.canGoBack).toBe(true);

act(() => probe.panel.goBack());
expect(probe.panel.currentPanel).toBe("files");
expect(probe.panel.data).toBe("files");
expect(probe.panel.canGoBack).toBe(false);
});

it("walks back through several panels in order", () => {
const probe = renderProvider();

act(() => probe.panel.openPanel({ type: "files" }));
act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "a.md" })
);
act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "b.md" })
);

act(() => probe.panel.goBack());
expect(probe.panel.data).toBe("a.md");

act(() => probe.panel.goBack());
expect(probe.panel.currentPanel).toBe("files");
expect(probe.panel.canGoBack).toBe(false);
});

it("does not record a history entry when reselecting the same content", () => {
const probe = renderProvider();

act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "a.md" })
);
act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "a.md" })
);

expect(probe.panel.canGoBack).toBe(false);
});

it("clears history once the panel has closed", () => {
const probe = renderProvider();

act(() => probe.panel.openPanel({ type: "files" }));
act(() =>
probe.panel.openPanel({ type: "file_preview", filePath: "a.md" })
);
expect(probe.panel.canGoBack).toBe(true);

act(() => probe.panel.closePanel());
act(() => probe.panel.onPanelClosed());

expect(probe.panel.canGoBack).toBe(false);
});
});

describe("ConversationSidePanelProvider toggle", () => {
beforeEach(resetHash);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ const isSupportedPanelType = (
type === "skill";

interface ConversationSidePanelContextType {
canGoBack: boolean;
currentPanel: ConversationSidePanelType;
// True between closePanel() and the end of the collapse transition. `currentPanel` keeps the
// old value meanwhile so the panel content does not flicker; toggles read this to unselect
// right away.
isPanelClosing: boolean;
goBack: () => void;
openPanel: (params: OpenPanelParams) => void;
togglePanel: (params: OpenPanelParams) => void;
closePanel: () => void;
Expand Down Expand Up @@ -163,6 +165,13 @@ function getPanelData(params: OpenPanelParams): string {
}
}

interface PanelHistoryEntry {
panel: ConversationSidePanelType;
data: string;
}

const MAX_PANEL_HISTORY = 50;

interface ConversationSidePanelProviderProps {
children: React.ReactNode;
}
Expand All @@ -179,6 +188,9 @@ export function ConversationSidePanelProvider({
const previousConversationIdRef = React.useRef(activeConversationId);

const panelRef = React.useRef<ImperativePanelHandle | null>(null);
const [panelHistory, setPanelHistory] = React.useState<PanelHistoryEntry[]>(
[]
);
const [isPanelClosing, setIsPanelClosing] = React.useState(false);
const [virtuosoMsg, setVirtuosoMsg] =
React.useState<AgentMessageWithStreaming | null>(null);
Expand All @@ -196,6 +208,7 @@ export function ConversationSidePanelProvider({
setIsPanelClosing(false);
setData(undefined);
setCurrentPanel(undefined);
setPanelHistory([]);
}, [setData, setCurrentPanel]);

// biome-ignore lint/correctness/useExhaustiveDependencies: ignored using `--suppress`
Expand Down Expand Up @@ -226,6 +239,17 @@ export function ConversationSidePanelProvider({
return;
}

if (
!isSameContent &&
!isPanelClosing &&
data &&
isSupportedPanelType(currentPanel)
) {
setPanelHistory((history) =>
[...history, { panel: currentPanel, data }].slice(-MAX_PANEL_HISTORY)
);
}

setIsPanelClosing(false);
setCurrentPanel(params.type);
setData(nextData);
Expand All @@ -251,6 +275,20 @@ export function ConversationSidePanelProvider({
]
);

const goBack = useCallback(() => {
const previous = panelHistory.at(-1);
if (!previous) {
return;
}

setPanelHistory((history) => history.slice(0, -1));
setIsPanelClosing(false);
setCurrentPanel(previous.panel);
setData(previous.data);
setFullScreenHash(undefined);
panelRef.current?.expand(getDefaultRightPanelSize(previous.panel));
}, [panelHistory, setCurrentPanel, setData, setFullScreenHash]);

// Idempotent open for programmatic callers: a toggle could mis-close during a close→reopen
// transition where `currentPanel` still reads the old value.
const openPanel = useCallback(
Expand Down Expand Up @@ -293,6 +331,8 @@ export function ConversationSidePanelProvider({

const value = useMemo(
() => ({
canGoBack: panelHistory.length > 0,
goBack,
currentPanel: isSupportedPanelType(currentPanel)
? currentPanel
: undefined,
Expand All @@ -308,6 +348,8 @@ export function ConversationSidePanelProvider({
data,
}),
[
panelHistory,
goBack,
currentPanel,
isPanelClosing,
openPanel,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useConversationSidePanelContext } from "@app/components/assistant/conversation/ConversationSidePanelContext";
import { AppLayoutTitle } from "@app/components/sparkle/AppLayoutTitle";
import { Button, XClose } from "@dust-tt/sparkle";
import { ArrowLeft, Button, XClose } from "@dust-tt/sparkle";
import type React from "react";

interface ConversationSidePanelHeaderProps {
Expand All @@ -11,9 +12,20 @@ export function ConversationSidePanelHeader({
children,
onClose,
}: ConversationSidePanelHeaderProps) {
const { canGoBack, goBack } = useConversationSidePanelContext();

return (
<AppLayoutTitle className="bg-panel-background @container">
<div className="flex h-full items-center">
{canGoBack && (
<Button
variant="ghost"
onClick={goBack}
icon={ArrowLeft}
tooltip="Back"
className="text-element-600 hover:text-element-900 mr-1 shrink-0"
/>
)}
{children}
{onClose && (
<Button
Expand Down
4 changes: 4 additions & 0 deletions front/components/markdown/FilePreviewBlock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ function renderWithSidePanel(
...render(
<ConversationSidePanelContext.Provider
value={{
canGoBack: false,
goBack: vi.fn(),
currentPanel: undefined,
isPanelClosing: false,
openPanel,
Expand Down Expand Up @@ -215,6 +217,8 @@ describe("getFilePreviewPlugin", () => {
render(
<ConversationSidePanelContext.Provider
value={{
canGoBack: false,
goBack: vi.fn(),
currentPanel: undefined,
isPanelClosing: false,
openPanel,
Expand Down
Loading