feat(FN-2193): add fullscreen template preview for agent prompts

- Render full template prompt text in cards with a scrollable preview area instead of truncation
- Add expand controls for built-in and custom templates to open a single fullscreen dialog view with Escape/collapse handling
- Align template expand buttons to shared icon-button styling for consistent UI behavior
- Extend AgentPromptsManager tests to cover preview overflow styles and fullscreen interactions across built-in/custom templates
This commit is contained in:
Fusion
2026-04-19 19:30:07 -07:00
committed by gsxdsm
parent a929b729b9
commit 0aa1360a18
3 changed files with 391 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import { useState, useCallback, useEffect, useRef } from "react";
import { BUILTIN_AGENT_PROMPTS, PROMPT_KEY_CATALOG } from "../utils/builtinPrompts";
import type { AgentPromptTemplate, AgentPromptsConfig, AgentCapability } from "@fusion/core";
import type { PromptKey } from "@fusion/core";
@@ -66,6 +66,11 @@ const EMPTY_TEMPLATE_FORM: TemplateFormData = {
prompt: "",
};
type FullscreenTemplateView = {
source: "builtin" | "custom";
id: string;
};
/**
* Generate a kebab-case ID from a template name.
* If collision exists with built-in or existing custom IDs, append -2, -3, etc.
@@ -99,14 +104,6 @@ function generateTemplateId(
return `${baseId}-${counter}`;
}
/**
* Truncate text to a specified length with ellipsis.
*/
function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 3) + "...";
}
/**
* AgentPromptsManager - A unified component for managing agent prompt templates,
* role assignments, and prompt segment overrides.
@@ -140,6 +137,8 @@ export function AgentPromptsManager({
// Fullscreen state for templates tab
const [isTemplatePromptFullscreen, setIsTemplatePromptFullscreen] = useState(false);
const [fullscreenViewTemplate, setFullscreenViewTemplate] = useState<FullscreenTemplateView | null>(null);
const fullscreenViewContainerRef = useRef<HTMLDivElement | null>(null);
// Get custom templates from current config
const customTemplates = value?.templates ?? [];
@@ -147,6 +146,13 @@ export function AgentPromptsManager({
// Get role assignments from current config
const roleAssignments = value?.roleAssignments ?? {};
const fullscreenTemplate =
fullscreenViewTemplate === null
? null
: fullscreenViewTemplate.source === "builtin"
? BUILTIN_AGENT_PROMPTS.find((template) => template.id === fullscreenViewTemplate.id)
: customTemplates.find((template) => template.id === fullscreenViewTemplate.id);
// Get templates for a specific role (both built-in and custom)
const getTemplatesForRole = useCallback(
(role: AgentCapability): AgentPromptTemplate[] => {
@@ -157,12 +163,19 @@ export function AgentPromptsManager({
[customTemplates],
);
useEffect(() => {
if (fullscreenViewTemplate !== null) {
fullscreenViewContainerRef.current?.focus();
}
}, [fullscreenViewTemplate]);
// Handle starting template creation
const handleStartCreate = useCallback(() => {
setIsCreating(true);
setEditingTemplateId(null);
setTemplateForm(EMPTY_TEMPLATE_FORM);
setTemplateIdError(null);
setFullscreenViewTemplate(null);
}, []);
// Handle starting template edit
@@ -176,6 +189,7 @@ export function AgentPromptsManager({
prompt: template.prompt,
});
setTemplateIdError(null);
setFullscreenViewTemplate(null);
}, []);
// Handle canceling template edit
@@ -184,6 +198,7 @@ export function AgentPromptsManager({
setIsCreating(false);
setTemplateForm(EMPTY_TEMPLATE_FORM);
setTemplateIdError(null);
setFullscreenViewTemplate(null);
setIsTemplatePromptFullscreen(false);
}, []);
@@ -345,8 +360,17 @@ export function AgentPromptsManager({
setFullscreenOverrideKey((prev) => (prev === key ? null : key));
}, []);
const openTemplateViewFullscreen = useCallback(
(source: FullscreenTemplateView["source"], id: string) => {
setIsTemplatePromptFullscreen(false);
setFullscreenViewTemplate({ source, id });
},
[],
);
// Toggle fullscreen for template prompt
const toggleTemplatePromptFullscreen = useCallback(() => {
setFullscreenViewTemplate(null);
setIsTemplatePromptFullscreen((prev) => !prev);
}, []);
@@ -452,7 +476,7 @@ export function AgentPromptsManager({
<label htmlFor="template-prompt">Prompt</label>
<button
type="button"
className="prompt-template-fullscreen-btn"
className="btn-icon prompt-template-fullscreen-btn"
onClick={toggleTemplatePromptFullscreen}
aria-label="Expand prompt to fullscreen"
data-testid="template-prompt-fullscreen"
@@ -568,17 +592,32 @@ export function AgentPromptsManager({
</span>
<span
className="prompt-template-badge-role"
style={{ backgroundColor: ROLE_COLORS[template.role] + "20", color: ROLE_COLORS[template.role] }}
style={{
backgroundColor: ROLE_COLORS[template.role] + "20",
color: ROLE_COLORS[template.role],
}}
>
{ROLE_LABELS[template.role]}
</span>
</div>
<div className="prompt-template-card-actions">
<button
type="button"
className="btn-icon prompt-template-fullscreen-btn"
onClick={() => openTemplateViewFullscreen("builtin", template.id)}
title="View full prompt"
aria-label={`View full prompt for ${template.name}`}
data-testid={`expand-view-${template.id}`}
>
<Maximize2 size={14} />
</button>
</div>
</div>
<p className="prompt-template-card-description">
{template.description}
</p>
<div className="prompt-template-card-preview">
<code>{truncate(template.prompt, 200)}</code>
<code>{template.prompt}</code>
</div>
</div>
))}
@@ -653,6 +692,16 @@ export function AgentPromptsManager({
)}
</div>
<div className="prompt-template-card-actions">
<button
type="button"
className="btn-icon prompt-template-fullscreen-btn"
onClick={() => openTemplateViewFullscreen("custom", template.id)}
title="View full prompt"
aria-label={`View full prompt for ${template.name}`}
data-testid={`expand-view-${template.id}`}
>
<Maximize2 size={14} />
</button>
<button
className="btn-icon"
onClick={() => handleStartEdit(template)}
@@ -677,7 +726,7 @@ export function AgentPromptsManager({
{template.description}
</p>
<div className="prompt-template-card-preview">
<code>{truncate(template.prompt, 200)}</code>
<code>{template.prompt}</code>
</div>
</>
)}
@@ -698,6 +747,47 @@ export function AgentPromptsManager({
</button>
)}
</div>
{fullscreenTemplate && (
<div
ref={fullscreenViewContainerRef}
className="prompt-override-fullscreen"
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
setFullscreenViewTemplate(null);
}
}}
>
<div className="prompt-override-fullscreen-header">
<div className="prompt-override-fullscreen-title">
{fullscreenTemplate.name}
<span
className="prompt-template-badge-role"
style={{
backgroundColor: ROLE_COLORS[fullscreenTemplate.role] + "20",
color: ROLE_COLORS[fullscreenTemplate.role],
}}
>
{ROLE_LABELS[fullscreenTemplate.role]}
</span>
</div>
<button
type="button"
className="prompt-override-fullscreen-close"
onClick={() => setFullscreenViewTemplate(null)}
data-testid={`collapse-view-${fullscreenTemplate.id}`}
>
<Minimize2 size={14} />
Collapse
</button>
</div>
<pre className="prompt-template-fullscreen-pre">{fullscreenTemplate.prompt}</pre>
</div>
)}
</div>
)}

View File

@@ -1,8 +1,11 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AgentPromptsManager } from "../AgentPromptsManager";
import type { AgentPromptsConfig, AgentPromptTemplate } from "@fusion/core";
import { BUILTIN_AGENT_PROMPTS } from "../../utils/builtinPrompts";
import type { AgentPromptsConfig } from "@fusion/core";
// Mock the builtinPrompts utility to avoid importing the large prompt texts
vi.mock("../../utils/builtinPrompts", () => ({
@@ -12,7 +15,7 @@ vi.mock("../../utils/builtinPrompts", () => ({
name: "Default Executor",
description: "Standard task execution agent with full tooling.",
role: "executor",
prompt: "You are a task execution agent...",
prompt: "You are a task execution agent responsible for implementing scoped tasks with precision. Always read PROMPT.md, run tests, keep git history clean, and verify lint, tests, and build pass before calling task_done.",
builtIn: true,
},
{
@@ -63,8 +66,19 @@ const defaultConfig: AgentPromptsConfig = {};
const onChange = vi.fn();
const onPromptOverridesChange = vi.fn();
const stylesPath = resolve(__dirname, "../../styles.css");
const stylesContent = readFileSync(stylesPath, "utf8");
const testStylesId = "agent-prompts-manager-test-styles";
beforeEach(() => {
vi.clearAllMocks();
if (!document.getElementById(testStylesId)) {
const styleElement = document.createElement("style");
styleElement.id = testStylesId;
styleElement.textContent = stylesContent;
document.head.appendChild(styleElement);
}
});
describe("AgentPromptsManager", () => {
@@ -155,6 +169,50 @@ describe("AgentPromptsManager", () => {
expect(screen.getAllByText("Merger Agent").length).toBe(1);
});
it("template preview shows full prompt text (not truncated)", () => {
const defaultExecutorTemplate = BUILTIN_AGENT_PROMPTS.find(
(template) => template.id === "default-executor",
);
if (!defaultExecutorTemplate) {
throw new Error("default-executor template is required for this test");
}
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
const card = screen.getByTestId("builtin-template-default-executor");
const previewCode = card.querySelector(".prompt-template-card-preview code");
expect(previewCode).toBeTruthy();
expect(previewCode?.textContent).toBe(defaultExecutorTemplate.prompt);
expect(defaultExecutorTemplate.prompt.length).toBeGreaterThan(200);
});
it("template preview area has scrollable overflow", () => {
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
const card = screen.getByTestId("builtin-template-default-executor");
const previewCode = card.querySelector(".prompt-template-card-preview code") as HTMLElement | null;
expect(previewCode).toBeTruthy();
expect(window.getComputedStyle(previewCode!).maxHeight).toBe("120px");
expect(window.getComputedStyle(previewCode!).overflowY).toBe("auto");
});
it("shows custom templates section", () => {
render(
<AgentPromptsManager
@@ -799,6 +857,209 @@ describe("AgentPromptsManager", () => {
});
describe("Templates tab fullscreen", () => {
it("each built-in template card has an expand button", () => {
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
for (const template of BUILTIN_AGENT_PROMPTS) {
expect(screen.getByTestId(`expand-view-${template.id}`)).toBeTruthy();
}
});
it("clicking expand button opens fullscreen view", async () => {
const user = userEvent.setup();
const defaultExecutorTemplate = BUILTIN_AGENT_PROMPTS.find(
(template) => template.id === "default-executor",
);
if (!defaultExecutorTemplate) {
throw new Error("default-executor template is required for this test");
}
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
await user.click(screen.getByTestId("expand-view-default-executor"));
await waitFor(() => {
expect(screen.getByTestId("collapse-view-default-executor")).toBeTruthy();
expect(screen.getByRole("dialog").textContent).toContain(defaultExecutorTemplate.prompt);
});
});
it("clicking collapse button exits fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
await user.click(screen.getByTestId("expand-view-default-executor"));
await waitFor(() => {
expect(screen.getByTestId("collapse-view-default-executor")).toBeTruthy();
});
await user.click(screen.getByTestId("collapse-view-default-executor"));
await waitFor(() => {
expect(screen.queryByTestId("collapse-view-default-executor")).toBeNull();
});
});
it("Escape key exits fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
await user.click(screen.getByTestId("expand-view-default-executor"));
await waitFor(() => {
expect(screen.getByTestId("collapse-view-default-executor")).toBeTruthy();
});
fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
await waitFor(() => {
expect(screen.queryByTestId("collapse-view-default-executor")).toBeNull();
});
});
it("custom template card has expand button", () => {
const config: AgentPromptsConfig = {
templates: [
{
id: "my-custom-template",
name: "My Custom Template",
description: "Custom template for testing",
role: "executor",
prompt: "Custom prompt content",
builtIn: false,
},
],
};
render(
<AgentPromptsManager
value={config}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
expect(screen.getByTestId("expand-view-my-custom-template")).toBeTruthy();
});
it("only one template fullscreen at a time", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
await user.click(screen.getByTestId("expand-view-default-executor"));
await waitFor(() => {
expect(screen.getByTestId("collapse-view-default-executor")).toBeTruthy();
});
await user.click(screen.getByTestId("expand-view-default-triage"));
await waitFor(() => {
expect(screen.queryByTestId("collapse-view-default-executor")).toBeNull();
expect(screen.getByTestId("collapse-view-default-triage")).toBeTruthy();
});
});
it("fullscreen view remains singular when custom template overrides a built-in ID", async () => {
const user = userEvent.setup();
const config: AgentPromptsConfig = {
templates: [
{
id: "default-executor",
name: "Custom Override Executor",
description: "Overrides built-in template",
role: "executor",
prompt: "Custom override prompt body for executor role.",
builtIn: false,
},
],
};
render(
<AgentPromptsManager
value={config}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
const expandButtons = screen.getAllByTestId("expand-view-default-executor");
expect(expandButtons.length).toBe(2);
await user.click(expandButtons[0]);
await waitFor(() => {
expect(screen.getAllByRole("dialog").length).toBe(1);
expect(screen.getByRole("dialog").textContent).toContain("Default Executor");
});
await user.click(expandButtons[1]);
await waitFor(() => {
expect(screen.getAllByRole("dialog").length).toBe(1);
expect(screen.getByRole("dialog").textContent).toContain("Custom Override Executor");
});
});
it("opening template card fullscreen closes template editor fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
await user.click(screen.getByTestId("add-template-btn"));
await user.click(screen.getByTestId("template-prompt-fullscreen"));
await waitFor(() => {
expect(screen.getByTestId("template-prompt-input-fullscreen")).toBeTruthy();
});
await user.click(screen.getByTestId("expand-view-default-executor"));
await waitFor(() => {
expect(screen.queryByTestId("template-prompt-input-fullscreen")).toBeNull();
expect(screen.getByTestId("collapse-view-default-executor")).toBeTruthy();
});
});
it("shows fullscreen button in template editor", async () => {
const user = userEvent.setup();
render(