feat(FN-1885): merge fusion/fn-1885

This commit is contained in:
gsxdsm
2026-04-16 04:38:12 -07:00
parent 589615806a
commit 06383521b3
3 changed files with 670 additions and 60 deletions

View File

@@ -2,7 +2,7 @@ import { useState, useCallback } 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";
import { X, Plus, Pencil, Trash2, BookOpen, Users, Settings2, ChevronDown, ChevronUp } from "lucide-react";
import { X, Plus, Pencil, Trash2, BookOpen, Users, Settings2, ChevronDown, ChevronUp, Maximize2, Minimize2 } from "lucide-react";
/**
* Props for the AgentPromptsManager component.
@@ -135,6 +135,12 @@ export function AgentPromptsManager({
// Override expanded state for accordion behavior
const [expandedOverrides, setExpandedOverrides] = useState<Set<PromptKey>>(new Set());
// Fullscreen state for overrides tab
const [fullscreenOverrideKey, setFullscreenOverrideKey] = useState<PromptKey | null>(null);
// Fullscreen state for templates tab
const [isTemplatePromptFullscreen, setIsTemplatePromptFullscreen] = useState(false);
// Get custom templates from current config
const customTemplates = value?.templates ?? [];
@@ -178,6 +184,7 @@ export function AgentPromptsManager({
setIsCreating(false);
setTemplateForm(EMPTY_TEMPLATE_FORM);
setTemplateIdError(null);
setIsTemplatePromptFullscreen(false);
}, []);
// Handle saving a template (create or update)
@@ -333,6 +340,16 @@ export function AgentPromptsManager({
});
}, []);
// Toggle fullscreen for an override
const toggleFullscreenOverride = useCallback((key: PromptKey) => {
setFullscreenOverrideKey((prev) => (prev === key ? null : key));
}, []);
// Toggle fullscreen for template prompt
const toggleTemplatePromptFullscreen = useCallback(() => {
setIsTemplatePromptFullscreen((prev) => !prev);
}, []);
// Check if a template ID collides with built-in
const isBuiltinId = (id: string): boolean => {
return BUILTIN_AGENT_PROMPTS.some((t) => t.id === id);
@@ -431,18 +448,72 @@ export function AgentPromptsManager({
</div>
<div className="prompt-template-field">
<label htmlFor="template-prompt">Prompt</label>
<textarea
id="template-prompt"
value={templateForm.prompt}
onChange={(e) =>
setTemplateForm((f) => ({ ...f, prompt: e.target.value }))
}
placeholder="Enter the system prompt for this template..."
rows={8}
className="prompt-template-prompt-textarea"
data-testid="template-prompt-input"
/>
<div className="prompt-template-prompt-label-row">
<label htmlFor="template-prompt">Prompt</label>
<button
type="button"
className="prompt-template-fullscreen-btn"
onClick={toggleTemplatePromptFullscreen}
aria-label="Expand prompt to fullscreen"
data-testid="template-prompt-fullscreen"
>
<Maximize2 size={14} />
</button>
</div>
{isTemplatePromptFullscreen ? (
<div
className="prompt-override-fullscreen"
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
setIsTemplatePromptFullscreen(false);
}
}}
>
<div className="prompt-override-fullscreen-header">
<div className="prompt-override-fullscreen-title">Edit Prompt</div>
<button
type="button"
className="prompt-override-fullscreen-close"
onClick={() => setIsTemplatePromptFullscreen(false)}
data-testid="template-prompt-collapse"
>
<Minimize2 size={14} />
Collapse
</button>
</div>
<textarea
id="template-prompt-fullscreen"
aria-label="Template prompt - fullscreen"
className="prompt-template-prompt-textarea"
value={templateForm.prompt}
onChange={(e) =>
setTemplateForm((f) => ({ ...f, prompt: e.target.value }))
}
placeholder="Enter the system prompt for this template..."
rows={30}
autoFocus
data-testid="template-prompt-input-fullscreen"
/>
<div className="prompt-override-footer">
<small className="prompt-override-hint">
{templateForm.prompt.length} characters
</small>
</div>
</div>
) : (
<textarea
id="template-prompt"
value={templateForm.prompt}
onChange={(e) =>
setTemplateForm((f) => ({ ...f, prompt: e.target.value }))
}
placeholder="Enter the system prompt for this template..."
rows={8}
className="prompt-template-prompt-textarea"
data-testid="template-prompt-input"
/>
)}
</div>
{templateIdError && (
@@ -722,18 +793,18 @@ export function AgentPromptsManager({
className="prompt-override-item"
data-testid={`override-${key}`}
>
<div
className="prompt-override-header"
onClick={() => toggleOverrideExpanded(key)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
toggleOverrideExpanded(key);
}
}}
>
<div className="prompt-override-info">
<div className="prompt-override-header">
<div
className="prompt-override-info"
onClick={() => toggleOverrideExpanded(key)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
toggleOverrideExpanded(key);
}
}}
>
<span className="prompt-override-name">
{promptMeta.name}
</span>
@@ -742,53 +813,130 @@ export function AgentPromptsManager({
<span className="prompt-override-badge">customized</span>
)}
</div>
<button
className="prompt-override-expand-btn"
aria-label={isExpanded ? "Collapse" : "Expand"}
data-testid={`expand-${key}`}
>
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
<div className="prompt-override-header-actions">
{isExpanded && (
<button
className="prompt-override-fullscreen-btn"
onClick={(e) => {
e.stopPropagation();
toggleFullscreenOverride(key);
}}
aria-label="Expand to fullscreen"
data-testid={`fullscreen-${key}`}
>
<Maximize2 size={14} />
</button>
)}
<button
className="prompt-override-expand-btn"
aria-label={isExpanded ? "Collapse" : "Expand"}
data-testid={`expand-${key}`}
onClick={(e) => {
e.stopPropagation();
toggleOverrideExpanded(key);
}}
>
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
</div>
</div>
<p className="prompt-override-description">
{promptMeta.description}
</p>
{isExpanded && (
<div className="prompt-override-editor">
<textarea
id={`prompt-${key}`}
aria-label={`${promptMeta.name} prompt override (${key})`}
className="prompt-override-textarea"
value={currentOverride}
onChange={(e) => {
handlePromptOverrideChange(key, e.target.value);
fullscreenOverrideKey === key ? (
<div
className="prompt-override-fullscreen"
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
setFullscreenOverrideKey(null);
}
}}
placeholder={`Default: ${promptMeta.defaultContent.slice(0, 100)}${promptMeta.defaultContent.length > 100 ? "..." : ""}`}
rows={4}
data-testid={`override-input-${key}`}
/>
<div className="prompt-override-footer">
{hasOverride && (
>
<div className="prompt-override-fullscreen-header">
<div className="prompt-override-fullscreen-title">
{promptMeta.name}
<code className="prompt-override-key">{key}</code>
</div>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={(e) => {
e.stopPropagation();
handleResetOverride(key);
}}
data-testid={`reset-${key}`}
className="prompt-override-fullscreen-close"
onClick={() => setFullscreenOverrideKey(null)}
data-testid={`collapse-fullscreen-${key}`}
>
Reset
<Minimize2 size={14} />
Collapse
</button>
)}
<small className="prompt-override-hint">
{hasOverride
? "Custom override active. Click Reset to restore default."
: `No override set. Using built-in default (${promptMeta.defaultContent.length} chars).`}
</small>
</div>
<textarea
id={`prompt-${key}-fullscreen`}
aria-label={`${promptMeta.name} prompt override (${key}) - fullscreen`}
className="prompt-override-textarea"
value={currentOverride}
onChange={(e) => {
handlePromptOverrideChange(key, e.target.value);
}}
placeholder={`Default: ${promptMeta.defaultContent.slice(0, 100)}${promptMeta.defaultContent.length > 100 ? "..." : ""}`}
rows={30}
autoFocus
data-testid={`override-input-fullscreen-${key}`}
/>
<div className="prompt-override-footer">
{hasOverride && (
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => handleResetOverride(key)}
data-testid={`reset-fullscreen-${key}`}
>
Reset
</button>
)}
<small className="prompt-override-hint">
{hasOverride
? "Custom override active. Click Reset to restore default."
: `No override set. Using built-in default (${promptMeta.defaultContent.length} chars).`}
</small>
</div>
</div>
</div>
) : (
<div className="prompt-override-editor">
<textarea
id={`prompt-${key}`}
aria-label={`${promptMeta.name} prompt override (${key})`}
className="prompt-override-textarea"
value={currentOverride}
onChange={(e) => {
handlePromptOverrideChange(key, e.target.value);
}}
placeholder={`Default: ${promptMeta.defaultContent.slice(0, 100)}${promptMeta.defaultContent.length > 100 ? "..." : ""}`}
rows={4}
data-testid={`override-input-${key}`}
/>
<div className="prompt-override-footer">
{hasOverride && (
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={(e) => {
e.stopPropagation();
handleResetOverride(key);
}}
data-testid={`reset-${key}`}
>
Reset
</button>
)}
<small className="prompt-override-hint">
{hasOverride
? "Custom override active. Click Reset to restore default."
: `No override set. Using built-in default (${promptMeta.defaultContent.length} chars).`}
</small>
</div>
</div>
)
)}
</div>
);

View File

@@ -604,4 +604,327 @@ describe("AgentPromptsManager", () => {
expect(newOverrides["executor-welcome"]).toBeNull();
});
});
describe("Fullscreen Expansion", () => {
describe("Overrides tab fullscreen", () => {
it("shows fullscreen button when override is expanded", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand the override
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
// Fullscreen button should be visible
expect(screen.getByTestId("fullscreen-executor-welcome")).toBeTruthy();
});
it("clicking fullscreen button opens fullscreen view", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand and click fullscreen
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("fullscreen-executor-welcome"));
// Fullscreen container should be visible
await waitFor(() => {
expect(screen.getByTestId("override-input-fullscreen-executor-welcome")).toBeTruthy();
expect(screen.getByTestId("collapse-fullscreen-executor-welcome")).toBeTruthy();
});
});
it("fullscreen textarea contains current override value", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{
"executor-welcome": "My custom welcome message",
}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand and go fullscreen
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("fullscreen-executor-welcome"));
// Verify fullscreen textarea has the value
await waitFor(() => {
const textarea = screen.getByTestId("override-input-fullscreen-executor-welcome") as HTMLTextAreaElement;
expect(textarea.value).toBe("My custom welcome message");
});
});
it("clicking collapse button exits fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand, go fullscreen, and collapse
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("fullscreen-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-fullscreen-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("collapse-fullscreen-executor-welcome"));
// Fullscreen should be gone, but accordion should still be expanded
await waitFor(() => {
expect(screen.queryByTestId("override-input-fullscreen-executor-welcome")).toBeNull();
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
});
it("Escape key exits fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand, go fullscreen
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("fullscreen-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-fullscreen-executor-welcome")).toBeTruthy();
});
// Press Escape
const textarea = screen.getByTestId("override-input-fullscreen-executor-welcome") as HTMLTextAreaElement;
fireEvent.keyDown(textarea, { key: "Escape" });
// Fullscreen should be gone
await waitFor(() => {
expect(screen.queryByTestId("override-input-fullscreen-executor-welcome")).toBeNull();
});
});
it("editing in fullscreen fires onPromptOverridesChange", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
// Expand, go fullscreen, and type
await user.click(screen.getByTestId("expand-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-executor-welcome")).toBeTruthy();
});
await user.click(screen.getByTestId("fullscreen-executor-welcome"));
await waitFor(() => {
expect(screen.getByTestId("override-input-fullscreen-executor-welcome")).toBeTruthy();
});
const textarea = screen.getByTestId("override-input-fullscreen-executor-welcome") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: "Fullscreen edit" } });
// Verify onPromptOverridesChange was called with updated value
await waitFor(() => {
expect(onPromptOverridesChange).toHaveBeenCalled();
const lastCall = onPromptOverridesChange.mock.calls[onPromptOverridesChange.mock.calls.length - 1];
const newOverrides = lastCall[0];
expect(newOverrides["executor-welcome"]).toBe("Fullscreen edit");
});
});
});
describe("Templates tab fullscreen", () => {
it("shows fullscreen button in template editor", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Open template editor
await user.click(screen.getByTestId("add-template-btn"));
// Fullscreen button should be visible
expect(screen.getByTestId("template-prompt-fullscreen")).toBeTruthy();
});
it("clicking fullscreen button opens fullscreen view for template prompt", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Open template editor
await user.click(screen.getByTestId("add-template-btn"));
// Click fullscreen button
await user.click(screen.getByTestId("template-prompt-fullscreen"));
// Fullscreen container should be visible
await waitFor(() => {
expect(screen.getByTestId("template-prompt-input-fullscreen")).toBeTruthy();
expect(screen.getByTestId("template-prompt-collapse")).toBeTruthy();
});
});
it("Escape key exits template prompt fullscreen", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Open template editor and go fullscreen
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();
});
// Press Escape
const textarea = screen.getByTestId("template-prompt-input-fullscreen") as HTMLTextAreaElement;
fireEvent.keyDown(textarea, { key: "Escape" });
// Fullscreen should be gone
await waitFor(() => {
expect(screen.queryByTestId("template-prompt-input-fullscreen")).toBeNull();
});
});
it("saving template resets fullscreen state", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Open template editor and go fullscreen
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();
});
// Fill in name and save
await user.type(screen.getByTestId("template-name-input"), "My Template");
await user.click(screen.getByTestId("save-template-btn"));
// Verify fullscreen is gone after save
await waitFor(() => {
expect(screen.queryByTestId("template-prompt-input-fullscreen")).toBeNull();
expect(screen.queryByTestId("template-editor")).toBeNull();
});
});
it("canceling template edit resets fullscreen state", async () => {
const user = userEvent.setup();
render(
<AgentPromptsManager
value={defaultConfig}
onChange={onChange}
promptOverrides={{}}
onPromptOverridesChange={onPromptOverridesChange}
/>,
);
// Open template editor and go fullscreen
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();
});
// Cancel
await user.click(screen.getByTestId("cancel-template-btn"));
// Verify fullscreen is gone after cancel
await waitFor(() => {
expect(screen.queryByTestId("template-prompt-input-fullscreen")).toBeNull();
expect(screen.queryByTestId("template-editor")).toBeNull();
});
});
});
});
});

View File

@@ -15950,6 +15950,136 @@ html .column.drag-over * {
color: var(--text-muted);
}
/* === Prompt Override Fullscreen === */
/* Header actions container for expand + fullscreen buttons */
.prompt-override-header-actions {
display: flex;
align-items: center;
gap: var(--space-xs);
}
/* Expand button in override header (only visible when expanded) */
.prompt-override-fullscreen-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast);
}
.prompt-override-fullscreen-btn:hover {
color: var(--text);
background: var(--surface);
}
/* Fullscreen container — fixed position overlay */
.prompt-override-fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
background: var(--surface);
padding: var(--space-lg);
display: flex;
flex-direction: column;
}
.prompt-override-fullscreen-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
flex-shrink: 0;
}
.prompt-override-fullscreen-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: var(--space-sm);
}
.prompt-override-fullscreen-close {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-xs);
padding: var(--space-xs) var(--space-sm);
background: none;
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-muted);
cursor: pointer;
font-size: 12px;
transition: all var(--transition-fast);
}
.prompt-override-fullscreen-close:hover {
color: var(--text);
border-color: var(--text-muted);
}
/* Fullscreen textarea fills remaining space */
.prompt-override-fullscreen textarea {
flex: 1;
min-height: unset;
resize: none;
border-radius: var(--radius-md);
font-size: 14px;
line-height: 1.6;
}
/* Fullscreen footer at bottom */
.prompt-override-fullscreen .prompt-override-footer {
flex-shrink: 0;
padding-top: var(--space-sm);
}
/* Template prompt label row with fullscreen button */
.prompt-template-prompt-label-row {
display: flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-xs);
}
.prompt-template-prompt-label-row label {
display: block;
}
/* Template prompt fullscreen button */
.prompt-template-fullscreen-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast);
vertical-align: middle;
margin-left: var(--space-xs);
}
.prompt-template-fullscreen-btn:hover {
color: var(--text);
background: var(--surface);
}
/* Responsive */
@media (max-width: 768px) {
.prompt-manager-tabs {
@@ -15979,6 +16109,15 @@ html .column.drag-over * {
.prompt-template-editor-actions .btn {
width: 100%;
}
/* Fullscreen prompt editor mobile overrides */
.prompt-override-fullscreen {
padding: var(--space-md);
}
.prompt-override-fullscreen textarea {
font-size: 16px; /* Prevent iOS zoom */
}
}
.new-task-modal .form-group small {