diff --git a/frontend/src/components/PluginsPanel.jsx b/frontend/src/components/PluginsPanel.jsx index 6f5dec6..7709679 100644 --- a/frontend/src/components/PluginsPanel.jsx +++ b/frontend/src/components/PluginsPanel.jsx @@ -14,7 +14,13 @@ const PLUGIN_ICONS = { export default function PluginsPanel({ sessionId, onClose }) { const [plugins, setPlugins] = useState([]); const [selected, setSelected] = useState(null); - const [input, setInput] = useState(""); + + // Persistent input state initialized from localStorage (#596) + const [input, setInput] = useState(() => { + if (!sessionId) return ""; + return localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""; + }); + const [output, setOutput] = useState(""); const [running, setRunning] = useState(false); const [loading, setLoading] = useState(false); @@ -96,7 +102,7 @@ export default function PluginsPanel({ sessionId, onClose }) { .finally(() => setLoading(false)); fetchLogs(); - }, [selectedPluginId]); + }, [selectedPluginId, sessionId]); function handleSelectPlugin(plugin) { setSelected(plugin); @@ -106,7 +112,23 @@ export default function PluginsPanel({ sessionId, onClose }) { setCopied(false); } - // Close contextual action menu on global click or Escape key + // Re-sync input draft whenever sessionId changes (#596) + useEffect(() => { + if (!sessionId) return; + setInput(localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""); + }, [sessionId]); + + // Persist plugin draft input to localStorage on edit (#596) + useEffect(() => { + if (!sessionId) return; + if (input) { + localStorage.setItem(`localmind_plugin_draft_${sessionId}`, input); + } else { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } + }, [input, sessionId]); + + // Close contextual action menu on global click or Escape key (#605) useEffect(() => { const handleGlobalClick = () => setActiveMenuId(null); const handleKeyDown = (e) => { @@ -178,6 +200,10 @@ export default function PluginsPanel({ sessionId, onClose }) { if (r.success) { setOutput(r.output); await fetchLogs(); + // Clear saved draft from localStorage after successful execution (#596) + if (sessionId) { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } } else { setError(r.error || "Plugin failed"); } @@ -276,7 +302,7 @@ export default function PluginsPanel({ sessionId, onClose }) { )} - {/* Collapsible Panel Section (#592) */} + {/* Collapsible Panel Section */} {!isCollapsed && ( <> {/* Plugin selector row with contextual dropdown menus (#605) */} @@ -357,7 +383,7 @@ export default function PluginsPanel({ sessionId, onClose }) { - {/* Output block with copy feedback button (#594) */} + {/* Output block with copy feedback button */} {output && (
@@ -377,7 +403,7 @@ export default function PluginsPanel({ sessionId, onClose }) { )}
) : ( - /* Empty State Guidance Card (#587) */ + /* Empty State Guidance Card */

No Plugin Selected

diff --git a/frontend/src/components/PluginsPanel.test.jsx b/frontend/src/components/PluginsPanel.test.jsx index 4d047d0..ca66644 100644 --- a/frontend/src/components/PluginsPanel.test.jsx +++ b/frontend/src/components/PluginsPanel.test.jsx @@ -301,4 +301,87 @@ describe("PluginsPanel View State & Persistence Suite (#592)", () => { expect(localStorage.setItem).toHaveBeenCalledWith("plugins-panel-selected:test-session-4", "calculator"); expect(screen.getByText("Performs math evaluation")).toBeInTheDocument(); }); +}); + +describe("PluginsPanel Saved Drafts (#596)", () => { + beforeEach(() => { + localStorage.clear(); + api.getPlugins.mockResolvedValue({ plugins: mockPluginsList }); + api.getPluginLogs.mockResolvedValue({ logs: [] }); + }); + + afterEach(() => { + cleanup(); + }); + + test("restores saved plugin draft from localStorage on render", async () => { + localStorage.setItem("localmind_plugin_draft_session-596", "2 + 2"); + + render(); + + await waitFor(() => { + expect(screen.getByText("Calculator")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText("Calculator")); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + expect(textarea.value).toBe("2 + 2"); + }); + + test("persists plugin draft to localStorage as user types", async () => { + render(); + + await waitFor(() => { + expect(screen.getByText("Calculator")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText("Calculator")); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + fireEvent.change(textarea, { target: { value: "10 * 5" } }); + + expect(localStorage.getItem("localmind_plugin_draft_session-596")).toBe("10 * 5"); + }); + + test("clears saved plugin draft from localStorage upon successful execution", async () => { + api.runPlugin.mockResolvedValue({ success: true, output: "50" }); + + render(); + + await waitFor(() => { + expect(screen.getByText("Calculator")).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText("Calculator")); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + fireEvent.change(textarea, { target: { value: "10 * 5" } }); + + const runBtn = screen.getByRole("button", { name: /Run Calculator/i }); + fireEvent.click(runBtn); + + await waitFor(() => { + expect(screen.getByText("50")).toBeInTheDocument(); + }); + + expect(localStorage.getItem("localmind_plugin_draft_session-596")).toBeNull(); + }); + + test("switches plugin draft dynamically when sessionId changes", async () => { + localStorage.setItem("localmind_plugin_draft_session-A", "Draft A"); + localStorage.setItem("localmind_plugin_draft_session-B", "Draft B"); + + const { rerender } = render(); + + await waitFor(() => expect(screen.getByText("Calculator")).toBeInTheDocument()); + fireEvent.click(screen.getByText("Calculator")); + + const textarea = screen.getByPlaceholderText(/Enter input for Calculator.../i); + expect(textarea.value).toBe("Draft A"); + + rerender(); + + expect(textarea.value).toBe("Draft B"); + }); }); \ No newline at end of file