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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user