[eric] delete unused Prompts feature, force AskUserQuestion for clarifying questions, clean up dead refs and TS errors

This commit is contained in:
ciregenz
2026-04-07 22:54:18 -07:00
parent ee54e56893
commit 4f50fc2236
31 changed files with 39 additions and 1329 deletions
View File
-32
View File
@@ -1,32 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional, Literal, Any
from uuid import uuid4
class TemplateField(BaseModel):
name: str
type: Literal["str", "int", "float", "select", "multi-select", "literal"]
options: Optional[list[str]] = None
default: Optional[Any] = None
required: bool = True
class PromptTemplate(BaseModel):
id: str = Field(default_factory=lambda: uuid4().hex)
name: str
description: str = ""
template: str # with {{field_name}} placeholders
fields: list[TemplateField] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class PromptTemplateCreate(BaseModel):
name: str
description: str = ""
template: str
fields: list[TemplateField] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class PromptTemplateUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
template: Optional[str] = None
fields: Optional[list[TemplateField]] = None
tags: Optional[list[str]] = None
-95
View File
@@ -1,95 +0,0 @@
import json
import os
import logging
from contextlib import asynccontextmanager
from fastapi import HTTPException
from backend.config.Apps import SubApp
from backend.apps.templates.models import PromptTemplate, PromptTemplateCreate, PromptTemplateUpdate
logger = logging.getLogger(__name__)
from backend.config.paths import TEMPLATES_DIR as DATA_DIR
@asynccontextmanager
async def templates_lifespan():
os.makedirs(DATA_DIR, exist_ok=True)
yield
templates = SubApp("templates", templates_lifespan)
def _load_all() -> list[PromptTemplate]:
result = []
if not os.path.exists(DATA_DIR):
return result
for fname in os.listdir(DATA_DIR):
if fname.endswith(".json"):
with open(os.path.join(DATA_DIR, fname)) as f:
result.append(PromptTemplate(**json.load(f)))
return result
def _save(template: PromptTemplate):
path = os.path.join(DATA_DIR, f"{template.id}.json")
with open(path, "w") as f:
json.dump(template.model_dump(), f, indent=2)
def _load(template_id: str) -> PromptTemplate:
path = os.path.join(DATA_DIR, f"{template_id}.json")
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="Template not found")
with open(path) as f:
return PromptTemplate(**json.load(f))
def _delete(template_id: str):
path = os.path.join(DATA_DIR, f"{template_id}.json")
if os.path.exists(path):
os.remove(path)
@templates.router.get("/list")
async def list_templates():
return {"templates": [t.model_dump() for t in _load_all()]}
@templates.router.get("/{template_id}")
async def get_template(template_id: str):
return _load(template_id).model_dump()
@templates.router.post("/create")
async def create_template(body: PromptTemplateCreate):
template = PromptTemplate(
name=body.name,
description=body.description,
template=body.template,
fields=body.fields,
tags=body.tags,
)
_save(template)
from backend.apps.analytics.collector import record as _analytics
_analytics("feature.used", {"feature": "template.created"})
return {"ok": True, "template": template.model_dump()}
@templates.router.put("/{template_id}")
async def update_template(template_id: str, body: PromptTemplateUpdate):
template = _load(template_id)
update_data = body.model_dump(exclude_none=True)
for k, v in update_data.items():
setattr(template, k, v)
_save(template)
return {"ok": True, "template": template.model_dump()}
@templates.router.delete("/{template_id}")
async def delete_template(template_id: str):
_delete(template_id)
return {"ok": True}
@templates.router.post("/render")
async def render_template(body: dict):
template_id = body.get("template_id", "")
values = body.get("values", {})
template = _load(template_id)
rendered = template.template
for field in template.fields:
placeholder = "{{" + field.name + "}}"
value = values.get(field.name, field.default or "")
rendered = rendered.replace(placeholder, str(value))
from backend.apps.analytics.collector import record as _analytics
_analytics("feature.used", {"feature": "template.used"})
return {"rendered": rendered}
-1
View File
@@ -28,7 +28,6 @@ SESSIONS_DIR = os.path.join(DATA_ROOT, "sessions")
TOOLS_DIR = os.path.join(DATA_ROOT, "tools")
SETTINGS_DIR = os.path.join(DATA_ROOT, "settings")
MODES_DIR = os.path.join(DATA_ROOT, "modes")
TEMPLATES_DIR = os.path.join(DATA_ROOT, "templates")
DASHBOARDS_DIR = os.path.join(DATA_ROOT, "dashboards")
OUTPUTS_DIR = os.path.join(DATA_ROOT, "outputs")
OUTPUTS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "outputs_workspace")
+1 -2
View File
@@ -13,7 +13,6 @@ from backend.config.Apps import MainApp
from backend.apps.health.health import health
from backend.apps.agents.agents import agents
from backend.apps.agents.ws_manager import ws_manager
from backend.apps.templates.templates import templates
from backend.apps.skills.skills import skills
from backend.apps.tools_lib.tools_lib import tools_lib
from backend.apps.modes.modes import modes
@@ -27,7 +26,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi import WebSocket, WebSocketDisconnect
import json
main_app = MainApp([health, agents, templates, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, dashboards, analytics])
main_app = MainApp([health, agents, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, dashboards, analytics])
app = main_app.app
app.add_middleware(
-2
View File
@@ -16,7 +16,6 @@ import {
} from '@/shared/state/updateSlice';
import AppShell from './components/Layout/AppShell';
import DashboardSelection from './pages/DashboardSelection/DashboardSelection';
import Templates from './pages/Templates/Templates';
import Skills from './pages/Skills/Skills';
import Tools from './pages/Tools/Tools';
import Modes from './pages/Modes/Modes';
@@ -253,7 +252,6 @@ const ThemedApp: React.FC = () => {
routes. This route exists only so React Router matches the URL. */}
<Route path="/dashboard/:id" element={null} />
<Route path="/customization" element={<Customization />} />
<Route path="/templates" element={<Templates />} />
<Route path="/skills" element={<Skills />} />
<Route path="/actions" element={<Tools />} />
<Route path="/modes" element={<Modes />} />
@@ -1,84 +0,0 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { updateSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
const AnalyticsOptIn: React.FC = () => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const settings = useAppSelector((s) => s.settings.data);
const loaded = useAppSelector((s) => s.settings.loaded);
if (!loaded || settings.analytics_opt_in !== null) return null;
const handleChoice = (optIn: boolean) => {
dispatch(updateSettings({ ...settings, analytics_opt_in: optIn }));
};
return (
<Box
sx={{
position: 'fixed',
bottom: 24,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1400,
maxWidth: 480,
width: '90%',
}}
>
<Paper
elevation={0}
sx={{
p: 2.5,
bgcolor: c.bg.surface,
border: `1px solid ${c.border.medium}`,
borderRadius: 3,
boxShadow: c.shadow.lg,
}}
>
<Typography sx={{ color: c.text.primary, fontSize: '0.9rem', fontWeight: 600, mb: 0.5 }}>
Help improve OpenSwarm
</Typography>
<Typography sx={{ color: c.text.muted, fontSize: '0.8rem', lineHeight: 1.5, mb: 2 }}>
Share anonymous usage statistics like session counts, feature usage, and model preferences.
No conversations, file paths, or personal information ever.
You can change this anytime in Settings.
</Typography>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button
onClick={() => handleChoice(false)}
sx={{
color: c.text.muted,
textTransform: 'none',
fontSize: '0.82rem',
'&:hover': { bgcolor: `${c.text.tertiary}0A` },
}}
>
No thanks
</Button>
<Button
variant="contained"
onClick={() => handleChoice(true)}
sx={{
bgcolor: c.accent.primary,
'&:hover': { bgcolor: c.accent.pressed },
textTransform: 'none',
fontSize: '0.82rem',
borderRadius: 1.5,
px: 2,
}}
>
Share anonymous data
</Button>
</Box>
</Paper>
</Box>
);
};
export default AnalyticsOptIn;
+4 -20
View File
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import DescriptionIcon from '@mui/icons-material/Description';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
@@ -19,7 +18,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
import { fetchOutputs } from '@/shared/state/outputsSlice';
import { fetchSkills } from '@/shared/state/skillsSlice';
import { fetchTemplates } from '@/shared/state/templatesSlice';
const XLogoIcon: React.FC<{ sx?: object }> = ({ sx }) => (
<SvgIcon sx={sx} viewBox="0 0 24 24">
@@ -58,7 +56,7 @@ export function getToolGroupIcon(groupName: string, size: number = 15): React.Re
export interface CommandPickerItem {
id: string;
type: 'template' | 'skill' | 'mode' | 'context';
type: 'skill' | 'mode' | 'context';
category: string;
name: string;
description: string;
@@ -100,7 +98,6 @@ function highlightMatch(text: string, query: string, color: string): React.React
const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, visible }) => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const templates = useAppSelector((s) => s.templates.items);
const skills = useAppSelector((s) => s.skills.items);
const modesMap = useAppSelector((s) => s.modes.items);
const builtinTools = useAppSelector((s) => s.tools.builtinTools);
@@ -113,30 +110,18 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded);
const outputsLoaded = useAppSelector((s) => s.outputs.loaded);
const skillsLoaded = useAppSelector((s) => s.skills.loaded);
const templatesLoaded = useAppSelector((s) => s.templates.loaded);
useEffect(() => {
if (!builtinLoaded) dispatch(fetchBuiltinTools());
if (!toolsLoaded) dispatch(fetchTools());
if (!outputsLoaded) dispatch(fetchOutputs());
if (!skillsLoaded) dispatch(fetchSkills());
if (!templatesLoaded) dispatch(fetchTemplates());
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded, skillsLoaded, templatesLoaded]);
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded, skillsLoaded]);
const items: CommandPickerItem[] = useMemo(() => {
let all: CommandPickerItem[] = [];
if (trigger === '/') {
const templateItems: CommandPickerItem[] = Object.values(templates).map((t) => ({
id: t.id,
type: 'template' as const,
category: 'Templates',
name: t.name,
description: t.description || `Template with ${t.fields.length} fields`,
command: t.name.toLowerCase().replace(/\s+/g, '-'),
icon: <DescriptionIcon sx={{ fontSize: 15 }} />,
}));
const skillItems: CommandPickerItem[] = Object.values(skills).map((s) => ({
id: s.id,
type: 'skill' as const,
@@ -160,7 +145,7 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
};
});
all = [...templateItems, ...skillItems, ...modeItems];
all = [...skillItems, ...modeItems];
} else {
const atItems: CommandPickerItem[] = [
{
@@ -301,7 +286,7 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
item.command.toLowerCase().includes(lower) ||
item.description.toLowerCase().includes(lower),
);
}, [trigger, templates, skills, modesMap, builtinTools, customTools, outputItems, filter]);
}, [trigger, skills, modesMap, builtinTools, customTools, outputItems, filter]);
const flatItems = useMemo(() => {
const result: { item: CommandPickerItem; isGroupStart: boolean; category: string }[] = [];
@@ -315,7 +300,6 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
const getIconColor = (item: CommandPickerItem): string => {
switch (item.type) {
case 'template': return c.accent.primary;
case 'skill': return c.status.success;
case 'mode': {
const mode = modesMap[item.id];
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useRef, useCallback, useMemo, RefObject } from 'react';
import React, { createContext, useContext, useState, useRef, useCallback, useMemo, MutableRefObject } from 'react';
export interface SelectedElement {
id: string;
@@ -31,7 +31,7 @@ interface ElementSelectionContextValue {
addElementForOwner: (ownerId: string, el: SelectedElement) => void;
removeOwnerElement: (ownerId: string, elementId: string) => void;
clearOwnerElements: (ownerId: string) => void;
iframeRef: RefObject<HTMLIFrameElement | null>;
iframeRef: MutableRefObject<HTMLIFrameElement | null>;
}
const ElementSelectionContext = createContext<ElementSelectionContextValue | null>(null);
@@ -8,7 +8,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
const shortcuts = [
{ key: 'd', description: 'Go to Dashboard' },
{ key: 't', description: 'Go to Templates' },
{ key: '1-9', description: 'Open agent by position' },
{ key: '⌘M', description: 'Add App' },
{ key: '⌘N', description: 'New Browser' },
@@ -14,7 +14,6 @@ import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import InputBase from '@mui/material/InputBase';
import DashboardIcon from '@mui/icons-material/Dashboard';
import DescriptionIcon from '@mui/icons-material/Description';
import PsychologyIcon from '@mui/icons-material/Psychology';
import BuildIcon from '@mui/icons-material/Build';
import TuneIcon from '@mui/icons-material/Tune';
@@ -50,7 +49,6 @@ const SIDEBAR_WIDTH_KEY = 'openswarm-sidebar-width';
const UPDATE_DISMISS_KEY = 'openswarm-update-dismissed';
const CUSTOMIZATION_ITEMS = [
{ label: 'Prompts', path: '/templates', icon: <DescriptionIcon />, onboarding: 'sidebar-prompts' },
{ label: 'Skills', path: '/skills', icon: <PsychologyIcon />, onboarding: 'sidebar-skills' },
{ label: 'Actions', path: '/actions', icon: <BuildIcon />, onboarding: 'sidebar-actions' },
{ label: 'Modes', path: '/modes', icon: <TuneIcon />, onboarding: 'sidebar-modes' },
@@ -333,7 +333,6 @@ const OnboardingModal: React.FC = () => {
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
@@ -353,7 +352,6 @@ const OnboardingModal: React.FC = () => {
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
@@ -399,7 +397,6 @@ const OnboardingModal: React.FC = () => {
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
@@ -446,7 +443,6 @@ const OnboardingModal: React.FC = () => {
'& .MuiOutlinedInput-root': {
fontSize: '0.82rem',
color: c.text.primary,
bgcolor: c.bg.input,
borderRadius: `${c.radius.md}px`,
'& fieldset': { borderColor: c.border.subtle },
'&:hover fieldset': { borderColor: c.border.medium },
@@ -465,7 +461,7 @@ const OnboardingModal: React.FC = () => {
textTransform: 'none', fontSize: '0.82rem', fontWeight: 600,
bgcolor: c.accent.primary, color: '#fff',
borderRadius: `${c.radius.md}px`, py: 1,
'&:hover': { bgcolor: c.accent.primaryHover || c.accent.primary },
'&:hover': { bgcolor: c.accent.hover },
mb: 1,
}}
>
@@ -42,12 +42,6 @@ const STEPS: WalkthroughStep[] = [
description: 'Scroll to zoom in and out. Drag the background to pan around. Click any card to focus on it.',
placement: 'top',
},
{
target: 'sidebar-prompts',
title: 'Saved Prompts',
description: 'Save message templates you use often \u2014 like email formats, report structures, or frequently asked questions.',
placement: 'right',
},
{
target: 'sidebar-skills',
title: 'Skills',
@@ -392,7 +386,7 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
py: 0.75,
fontFamily: c.font.sans,
visibility: currentStep === 0 || step.target === 'new-agent-button' ? 'hidden' : 'visible',
'&:hover': { bgcolor: c.bg.hover || 'rgba(255,255,255,0.05)' },
'&:hover': { bgcolor: 'rgba(255,255,255,0.05)' },
}}
>
Back
@@ -13,9 +13,7 @@ import {
TriggerState,
EMPTY_TRIGGER,
} from '@/app/components/richEditorUtils';
import TemplateInvokeModal from '@/app/pages/AgentChat/TemplateInvokeModal';
import { useAppSelector } from '@/shared/hooks';
import { PromptTemplate } from '@/shared/state/templatesSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
interface RichPromptEditorProps {
@@ -52,9 +50,7 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
const [picker, setPicker] = useState<TriggerState>(EMPTY_TRIGGER);
const [pickerRect, setPickerRect] = useState<DOMRect | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
const templates = useAppSelector((state) => state.templates.items);
const skills = useAppSelector((state) => state.skills.items);
useEffect(() => {
@@ -185,15 +181,7 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
}
if (item.type === 'template') {
const tmpl = templates[item.id];
if (!tmpl) return;
if (tmpl.fields.length === 0) {
document.execCommand('insertText', false, tmpl.template);
} else {
setSelectedTemplate(tmpl);
}
} else if (item.type === 'skill') {
if (item.type === 'skill') {
const skill = skills[item.id];
if (!skill) return;
if (editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return;
@@ -363,22 +351,6 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
</Box>
</Box>
{selectedTemplate && (
<TemplateInvokeModal
template={selectedTemplate}
open={!!selectedTemplate}
onClose={() => setSelectedTemplate(null)}
onApply={(rendered) => {
const editor = editorRef.current;
if (editor) {
document.execCommand('insertText', false, rendered);
}
setSelectedTemplate(null);
updateHasContent();
emitChange();
}}
/>
)}
</Box>
);
};
@@ -1,165 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import DescriptionIcon from '@mui/icons-material/Description';
import PsychologyIcon from '@mui/icons-material/Psychology';
import { useAppSelector } from '@/shared/hooks';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
export interface SlashItem {
id: string;
type: 'template' | 'skill';
name: string;
description: string;
command: string;
}
interface Props {
filter: string;
onSelect: (item: SlashItem) => void;
onClose: () => void;
visible: boolean;
}
const SlashCommandPicker: React.FC<Props> = ({ filter, onSelect, onClose, visible }) => {
const c = useClaudeTokens();
const templates = useAppSelector((state) => state.templates.items);
const skills = useAppSelector((state) => state.skills.items);
const [selectedIndex, setSelectedIndex] = useState(0);
const items: SlashItem[] = useMemo(() => {
const all: SlashItem[] = [
...Object.values(templates).map((t) => ({
id: t.id,
type: 'template' as const,
name: t.name,
description: t.description || `Template with ${t.fields.length} fields`,
command: t.name.toLowerCase().replace(/\s+/g, '-'),
})),
...Object.values(skills).map((s) => ({
id: s.id,
type: 'skill' as const,
name: s.name,
description: s.description || 'Skill',
command: s.command || s.id,
})),
];
if (!filter) return all;
const lower = filter.toLowerCase();
return all.filter(
(item) =>
item.name.toLowerCase().includes(lower) ||
item.command.toLowerCase().includes(lower) ||
item.description.toLowerCase().includes(lower)
);
}, [templates, skills, filter]);
useEffect(() => {
setSelectedIndex(0);
}, [filter]);
useEffect(() => {
if (!visible) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => Math.min(prev + 1, items.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => Math.max(prev - 1, 0));
} else if (e.key === 'Enter' && items[selectedIndex]) {
e.preventDefault();
onSelect(items[selectedIndex]);
} else if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [visible, items, selectedIndex, onSelect, onClose]);
if (!visible || items.length === 0) return null;
return (
<Paper
sx={{
position: 'absolute',
bottom: '100%',
left: 0,
right: 0,
mb: 0.5,
bgcolor: c.bg.surface,
border: `1px solid ${c.border.subtle}`,
borderRadius: 3,
maxHeight: 280,
overflow: 'auto',
zIndex: 1000,
boxShadow: c.shadow.lg,
'&::-webkit-scrollbar': { width: 5 },
'&::-webkit-scrollbar-track': { background: 'transparent' },
'&::-webkit-scrollbar-thumb': {
background: c.border.medium,
borderRadius: 3,
'&:hover': { background: c.border.strong },
},
scrollbarWidth: 'thin',
scrollbarColor: `${c.border.medium} transparent`,
}}
>
<Box sx={{ px: 1.5, py: 1, borderBottom: `0.5px solid ${c.border.medium}` }}>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.7rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }}>
Commands
</Typography>
</Box>
<List sx={{ py: 0.5 }}>
{items.map((item, i) => (
<ListItemButton
key={`${item.type}-${item.id}`}
selected={i === selectedIndex}
onClick={() => onSelect(item)}
sx={{
py: 0.75,
px: 1.5,
'&.Mui-selected': { bgcolor: 'rgba(174,86,48,0.06)' },
'&:hover': { bgcolor: 'rgba(0,0,0,0.04)' },
}}
>
<ListItemIcon sx={{ minWidth: 32 }}>
{item.type === 'template' ? (
<DescriptionIcon sx={{ fontSize: 18, color: c.accent.primary }} />
) : (
<PsychologyIcon sx={{ fontSize: 18, color: c.status.success }} />
)}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{ color: c.text.primary, fontSize: '0.8rem', fontWeight: 500 }}>
/{item.command}
</Typography>
<Typography sx={{ color: c.text.ghost, fontSize: '0.7rem' }}>
{item.type}
</Typography>
</Box>
}
secondary={
<Typography sx={{ color: c.text.tertiary, fontSize: '0.7rem', mt: 0.25 }}>
{item.description}
</Typography>
}
/>
</ListItemButton>
))}
</List>
</Paper>
);
};
export default SlashCommandPicker;
+1 -36
View File
@@ -39,10 +39,8 @@ import {
TriggerState,
EMPTY_TRIGGER,
} from '@/app/components/richEditorUtils';
import TemplateInvokeModal from './TemplateInvokeModal';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { fetchModes } from '@/shared/state/modesSlice';
import { PromptTemplate } from '@/shared/state/templatesSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
export interface AttachedImage {
@@ -155,8 +153,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
attachedSkillsRef.current = attachedSkills;
const [picker, setPicker] = useState<TriggerState>(EMPTY_TRIGGER);
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
const templates = useAppSelector((state) => state.templates.items);
const skills = useAppSelector((state) => state.skills.items);
const modesMap = useAppSelector((state) => state.modes.items);
const modesArr = useMemo(() => Object.values(modesMap), [modesMap]);
@@ -427,15 +423,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
}
if (item.type === 'template') {
const tmpl = templates[item.id];
if (!tmpl) return;
if (tmpl.fields.length === 0) {
document.execCommand('insertText', false, tmpl.template);
} else {
setSelectedTemplate(tmpl);
}
} else if (item.type === 'skill') {
if (item.type === 'skill') {
const skill = skills[item.id];
if (!skill) return;
if (editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return;
@@ -1204,29 +1192,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
)}
</Box>
{selectedTemplate && (
<TemplateInvokeModal
template={selectedTemplate}
open={!!selectedTemplate}
onClose={() => setSelectedTemplate(null)}
onApply={(rendered) => {
const editor = editorRef.current;
if (editor) {
editor.innerHTML = '';
editor.textContent = rendered;
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const sel = window.getSelection();
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
}
setSelectedTemplate(null);
setAttachedSkills({});
setHasContent(!!rendered);
}}
/>
)}
<Modal
open={!!lightboxSrc}
onClose={() => setLightboxSrc(null)}
@@ -1,183 +0,0 @@
import React, { useState } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Select from '@mui/material/Select';
import Checkbox from '@mui/material/Checkbox';
import ListItemText from '@mui/material/ListItemText';
import OutlinedInput from '@mui/material/OutlinedInput';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { PromptTemplate, TemplateField } from '@/shared/state/templatesSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
interface Props {
template: PromptTemplate;
open: boolean;
onClose: () => void;
onApply: (rendered: string) => void;
}
const TemplateInvokeModal: React.FC<Props> = ({ template, open, onClose, onApply }) => {
const c = useClaudeTokens();
const inputSx = {
'& .MuiOutlinedInput-root': {
color: c.text.primary,
'& fieldset': { borderColor: c.border.strong },
'&:hover fieldset': { borderColor: c.text.tertiary },
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
},
'& .MuiInputLabel-root': { color: c.text.tertiary },
'& .MuiInputLabel-root.Mui-focused': { color: c.accent.primary },
};
const [values, setValues] = useState<Record<string, any>>(() => {
const init: Record<string, any> = {};
for (const f of template.fields) {
init[f.name] = f.default ?? (f.type === 'multi-select' ? [] : '');
}
return init;
});
const handleApply = () => {
let rendered = template.template;
for (const f of template.fields) {
const val = values[f.name];
const str = Array.isArray(val) ? val.join(', ') : String(val ?? '');
rendered = rendered.replace(new RegExp(`\\{\\{${f.name}\\}\\}`, 'g'), str);
}
onApply(rendered);
onClose();
};
const renderField = (field: TemplateField) => {
const val = values[field.name];
const update = (v: any) => setValues((prev) => ({ ...prev, [field.name]: v }));
switch (field.type) {
case 'literal':
return (
<Box key={field.name} sx={{ mb: 2 }}>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.75rem', mb: 0.5 }}>{field.name}</Typography>
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontFamily: c.font.mono, bgcolor: c.bg.secondary, p: 1, borderRadius: 1.5 }}>
{field.default || ''}
</Typography>
</Box>
);
case 'select':
return (
<TextField
key={field.name}
select
label={field.name}
value={val || ''}
onChange={(e) => update(e.target.value)}
fullWidth
size="small"
sx={{ ...inputSx, mb: 2 }}
SelectProps={{ MenuProps: { PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } } }}
>
{(field.options || []).map((opt) => (
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
))}
</TextField>
);
case 'multi-select':
return (
<FormControl key={field.name} fullWidth size="small" sx={{ mb: 2, ...inputSx }}>
<InputLabel sx={{ color: c.text.tertiary }}>{field.name}</InputLabel>
<Select
multiple
value={Array.isArray(val) ? val : []}
onChange={(e) => update(e.target.value)}
input={<OutlinedInput label={field.name} />}
renderValue={(selected: string[]) => selected.join(', ')}
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
>
{(field.options || []).map((opt) => (
<MenuItem key={opt} value={opt}>
<Checkbox checked={(val || []).includes(opt)} sx={{ color: c.text.tertiary }} />
<ListItemText primary={opt} />
</MenuItem>
))}
</Select>
</FormControl>
);
case 'int':
case 'float':
return (
<TextField
key={field.name}
label={field.name}
type="number"
value={val ?? ''}
onChange={(e) => update(field.type === 'int' ? parseInt(e.target.value) || '' : parseFloat(e.target.value) || '')}
fullWidth
size="small"
sx={{ ...inputSx, mb: 2 }}
/>
);
default:
return (
<TextField
key={field.name}
label={field.name}
value={val || ''}
onChange={(e) => update(e.target.value)}
fullWidth
size="small"
multiline={field.name.toLowerCase().includes('description') || field.name.toLowerCase().includes('prompt')}
rows={field.name.toLowerCase().includes('description') || field.name.toLowerCase().includes('prompt') ? 3 : 1}
sx={{ ...inputSx, mb: 2 }}
/>
);
}
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
bgcolor: c.bg.surface,
borderRadius: 4,
border: `1px solid ${c.border.subtle}`,
},
}}
>
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600 }}>{template.name}</DialogTitle>
<DialogContent>
{template.description && (
<Typography sx={{ color: c.text.tertiary, fontSize: '0.85rem', mb: 2 }}>{template.description}</Typography>
)}
{template.fields.length === 0 ? (
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem' }}>
This template has no input fields. It will be inserted as-is.
</Typography>
) : (
template.fields.map(renderField)
)}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={onClose} sx={{ color: c.text.tertiary }}>Cancel</Button>
<Button
onClick={handleApply}
variant="contained"
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.hover }, fontWeight: 600 }}
>
Apply Template
</Button>
</DialogActions>
</Dialog>
);
};
export default TemplateInvokeModal;
+9 -27
View File
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Chip from '@mui/material/Chip';
import DescriptionIcon from '@mui/icons-material/Description';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import AlternateEmailIcon from '@mui/icons-material/AlternateEmail';
@@ -17,13 +16,12 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
import { getToolGroupIcon } from '@/app/components/CommandPicker';
import { fetchOutputs } from '@/shared/state/outputsSlice';
import { fetchTemplates } from '@/shared/state/templatesSlice';
import { fetchSkills } from '@/shared/state/skillsSlice';
import { fetchModes } from '@/shared/state/modesSlice';
interface SlashCommand {
id: string;
type: 'template' | 'skill' | 'mode';
type: 'skill' | 'mode';
name: string;
description: string;
command: string;
@@ -46,7 +44,6 @@ interface Shortcut {
const SHORTCUTS: Shortcut[] = [
{ key: 'd', description: 'Go to Dashboard', category: 'navigation' },
{ key: 't', description: 'Go to Templates', category: 'navigation' },
{ key: '1-9', description: 'Open agent by position', category: 'navigation' },
{ key: 'Shift+A', description: 'Approve all pending', category: 'action' },
{ key: 'Shift+D', description: 'Deny all pending', category: 'action' },
@@ -115,14 +112,12 @@ const SectionHeader: React.FC<{
export const CommandsContent: React.FC = () => {
const c = useClaudeTokens();
const dispatch = useAppDispatch();
const templates = useAppSelector((state) => state.templates.items);
const skills = useAppSelector((state) => state.skills.items);
const modesMap = useAppSelector((state) => state.modes.items);
const builtinTools = useAppSelector((state) => state.tools.builtinTools);
const customTools = useAppSelector((state) => state.tools.items);
const outputItems = useAppSelector((state) => state.outputs.items);
const templatesLoaded = useAppSelector((state) => state.templates.loaded);
const skillsLoaded = useAppSelector((state) => state.skills.loaded);
const modesLoaded = useAppSelector((state) => state.modes.loaded);
const builtinLoaded = useAppSelector((state) => state.tools.builtinLoaded);
@@ -130,22 +125,14 @@ export const CommandsContent: React.FC = () => {
const outputsLoaded = useAppSelector((state) => state.outputs.loaded);
useEffect(() => {
if (!templatesLoaded) dispatch(fetchTemplates());
if (!skillsLoaded) dispatch(fetchSkills());
if (!modesLoaded) dispatch(fetchModes());
if (!builtinLoaded) dispatch(fetchBuiltinTools());
if (!toolsLoaded) dispatch(fetchTools());
if (!outputsLoaded) dispatch(fetchOutputs());
}, [dispatch, templatesLoaded, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]);
}, [dispatch, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]);
const slashCommands: SlashCommand[] = useMemo(() => [
...Object.values(templates).map((t) => ({
id: t.id,
type: 'template' as const,
name: t.name,
description: t.description || `Template with ${t.fields.length} fields`,
command: t.name.toLowerCase().replace(/\s+/g, '-'),
})),
...Object.values(skills).map((s) => ({
id: s.id,
type: 'skill' as const,
@@ -160,7 +147,7 @@ export const CommandsContent: React.FC = () => {
description: m.description || 'Switch to this mode',
command: m.name.toLowerCase().replace(/\s+/g, '-'),
})),
], [templates, skills, modesMap]);
], [skills, modesMap]);
const atCommands: AtCommand[] = useMemo(() => {
const items: AtCommand[] = [
@@ -272,7 +259,7 @@ export const CommandsContent: React.FC = () => {
<SectionHeader
icon={<TerminalIcon sx={{ fontSize: 22 }} />}
title="Slash Commands"
subtitle="Type / in chat to invoke templates, skills, and modes"
subtitle="Type / in chat to invoke skills and modes"
count={slashCommands.length}
c={c}
/>
@@ -290,7 +277,7 @@ export const CommandsContent: React.FC = () => {
>
<TerminalIcon sx={{ fontSize: 36, opacity: 0.3 }} />
<Typography sx={{ fontSize: '0.85rem' }}>
No slash commands yet. Create templates, skills, or modes to see them here.
No slash commands yet. Create skills or modes to see them here.
</Typography>
</Box>
) : (
@@ -310,14 +297,11 @@ export const CommandsContent: React.FC = () => {
}}
>
<Box sx={{
color: cmd.type === 'template' ? c.accent.primary
: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
color: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
: c.status.success,
display: 'flex',
}}>
{cmd.type === 'template' ? (
<DescriptionIcon sx={{ fontSize: 18 }} />
) : cmd.type === 'mode' ? (
{cmd.type === 'mode' ? (
<SmartToyOutlinedIcon sx={{ fontSize: 18 }} />
) : (
<PsychologyIcon sx={{ fontSize: 18 }} />
@@ -342,11 +326,9 @@ export const CommandsContent: React.FC = () => {
fontSize: '0.65rem',
fontWeight: 600,
textTransform: 'uppercase',
bgcolor: cmd.type === 'template' ? `${c.accent.primary}12`
: cmd.type === 'mode' ? `${modesMap[cmd.id]?.color || c.accent.primary}15`
bgcolor: cmd.type === 'mode' ? `${modesMap[cmd.id]?.color || c.accent.primary}15`
: `${c.status.success}15`,
color: cmd.type === 'template' ? c.accent.primary
: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
color: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
: c.status.success,
}}
/>
@@ -4,20 +4,12 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import DescriptionIcon from '@mui/icons-material/Description';
import PsychologyIcon from '@mui/icons-material/Psychology';
import BuildIcon from '@mui/icons-material/Build';
import TuneIcon from '@mui/icons-material/Tune';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
const PANELS = [
{
label: 'Prompts',
path: '/templates',
icon: <DescriptionIcon />,
description:
'Create and manage reusable prompt templates with structured input fields that your agents can fill in.',
},
{
label: 'Skills',
path: '/skills',
@@ -138,16 +138,13 @@ const BrowserCard: React.FC<Props> = ({
const [tabLocalStates, setTabLocalStates] = useState<Record<string, TabLocalState>>({});
const updateTabLocal = useCallback((tabId: string, update: Partial<TabLocalState>) => {
setTabLocalStates((prev) => ({
...prev,
[tabId]: {
loading: false,
canGoBack: false,
canGoForward: false,
...prev[tabId],
...update,
},
}));
setTabLocalStates((prev) => {
const existing = prev[tabId] ?? { loading: false, canGoBack: false, canGoForward: false };
return {
...prev,
[tabId]: { ...existing, ...update },
};
});
}, []);
const activeTab = tabs.find((t) => t.id === activeTabId);
@@ -744,7 +741,7 @@ const BrowserCard: React.FC<Props> = ({
transform: isBeingDragged ? `translateX(${dragTabOffset}px)` : 'none',
transition: isBeingDragged ? 'none' : 'background 0.15s ease, transform 0.2s ease',
zIndex: isBeingDragged ? 10 : 1,
'&:hover': { bgcolor: isActive ? c.bg.surface : c.bg.hover },
'&:hover': { bgcolor: isActive ? c.bg.surface : c.bg.secondary },
'&:hover .tab-close': { opacity: 1 },
...(isActive && {
'&::after': {
@@ -262,7 +262,7 @@ const CategoryGroup: React.FC<{
c: ReturnType<typeof useClaudeTokens>;
children: React.ReactNode;
}> = ({ icon, label, count, c, children }) => (
<Box sx={{ '&:not(:first-of-type)': { borderTop: `1px solid ${c.border.light}`, mt: 0.5, pt: 0.5 } }}>
<Box sx={{ '&:not(:first-of-type)': { borderTop: `1px solid ${c.border.subtle}`, mt: 0.5, pt: 0.5 } }}>
<Box
sx={{
display: 'flex',
-2
View File
@@ -42,7 +42,6 @@ import {
Mode,
} from '@/shared/state/modesSlice';
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
import { fetchTemplates } from '@/shared/state/templatesSlice';
import { fetchSkills } from '@/shared/state/skillsSlice';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import ExtensionIcon from '@mui/icons-material/Extension';
@@ -126,7 +125,6 @@ const Modes: React.FC = () => {
dispatch(fetchModes());
dispatch(fetchBuiltinTools());
dispatch(fetchTools());
dispatch(fetchTemplates());
dispatch(fetchSkills());
}, [dispatch]);
@@ -1322,7 +1322,6 @@ const Settings: React.FC = () => {
variant="outlined"
size="small"
onClick={handleCheckForUpdates}
disabled={updateStatus === 'checking'}
startIcon={<SystemUpdateAltIcon sx={{ fontSize: 15 }} />}
sx={{
color: c.text.secondary,
@@ -1,474 +0,0 @@
import React, { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import {
fetchTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
PromptTemplate,
TemplateField,
} from '@/shared/state/templatesSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
const FIELD_TYPES = ['str', 'int', 'float', 'select', 'multi-select', 'literal'] as const;
interface EditorState {
name: string;
description: string;
template: string;
fields: TemplateField[];
tags: string[];
}
const emptyEditor: EditorState = {
name: '',
description: '',
template: '',
fields: [],
tags: [],
};
const Templates: React.FC = () => {
const c = useClaudeTokens();
const inputSx = {
'& .MuiOutlinedInput-root': {
color: c.text.primary,
'& fieldset': { borderColor: c.border.strong },
'&:hover fieldset': { borderColor: c.text.tertiary },
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
},
'& .MuiInputLabel-root': { color: c.text.tertiary },
'& .MuiInputLabel-root.Mui-focused': { color: c.accent.primary },
};
const claudePaperProps = {
sx: {
bgcolor: c.bg.surface,
color: c.text.primary,
borderRadius: 4,
border: `1px solid ${c.border.subtle}`,
maxHeight: '90vh',
},
};
const dispatch = useAppDispatch();
const { items, loading } = useAppSelector((s) => s.templates);
const templates = Object.values(items);
const [editorOpen, setEditorOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editor, setEditor] = useState<EditorState>(emptyEditor);
const [tagInput, setTagInput] = useState('');
useEffect(() => {
dispatch(fetchTemplates());
}, [dispatch]);
const openNew = () => {
setEditingId(null);
setEditor(emptyEditor);
setTagInput('');
setEditorOpen(true);
};
const openEdit = (t: PromptTemplate) => {
setEditingId(t.id);
setEditor({
name: t.name,
description: t.description,
template: t.template,
fields: t.fields.map((f) => ({ ...f })),
tags: [...t.tags],
});
setTagInput('');
setEditorOpen(true);
};
const handleSave = async () => {
if (!editor.name.trim() || !editor.template.trim()) return;
if (editingId) {
await dispatch(updateTemplate({ id: editingId, ...editor }));
} else {
await dispatch(createTemplate(editor));
}
setEditorOpen(false);
};
const handleDelete = async (id: string) => {
await dispatch(deleteTemplate(id));
};
const addField = () => {
setEditor((prev) => ({
...prev,
fields: [...prev.fields, { name: '', type: 'str', required: true }],
}));
};
const updateField = (idx: number, patch: Partial<TemplateField>) => {
setEditor((prev) => ({
...prev,
fields: prev.fields.map((f, i) => (i === idx ? { ...f, ...patch } : f)),
}));
};
const removeField = (idx: number) => {
setEditor((prev) => ({
...prev,
fields: prev.fields.filter((_, i) => i !== idx),
}));
};
const addTag = () => {
const tag = tagInput.trim();
if (tag && !editor.tags.includes(tag)) {
setEditor((prev) => ({ ...prev, tags: [...prev.tags, tag] }));
setTagInput('');
}
};
const removeTag = (tag: string) => {
setEditor((prev) => ({ ...prev, tags: prev.tags.filter((t) => t !== tag) }));
};
return (
<Box sx={{ p: 3, height: '100%', overflow: 'auto' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h5" sx={{ color: c.text.primary, fontWeight: 700, mb: 0.5 }}>
Prompt Templates
</Typography>
<Typography sx={{ color: c.text.tertiary, fontSize: '0.85rem' }}>
Create and manage reusable prompt templates with structured input fields.
</Typography>
</Box>
<Button
startIcon={<AddIcon />}
variant="contained"
onClick={openNew}
sx={{
bgcolor: c.accent.primary,
'&:hover': { bgcolor: c.accent.pressed },
textTransform: 'none',
fontWeight: 600,
borderRadius: 2,
}}
>
New Template
</Button>
</Box>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}>
<CircularProgress sx={{ color: c.accent.primary }} />
</Box>
) : templates.length === 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '50vh',
color: c.text.ghost,
}}
>
<Typography sx={{ fontSize: '1.1rem', mb: 1 }}>No templates yet</Typography>
<Typography sx={{ fontSize: '0.85rem' }}>Click "New Template" to get started.</Typography>
</Box>
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: 2,
}}
>
{templates.map((t) => (
<Box
key={t.id}
sx={{
bgcolor: c.bg.surface,
border: `1px solid ${c.border.subtle}`,
borderRadius: 3,
p: 2.5,
cursor: 'pointer',
boxShadow: c.shadow.sm,
transition: 'border-color 0.2s, box-shadow 0.2s',
'&:hover': {
borderColor: c.accent.primary,
boxShadow: '0 0 0 1px rgba(174,86,48,0.15)',
},
}}
onClick={() => openEdit(t)}
>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1 }}>
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem' }}>
{t.name}
</Typography>
<Box sx={{ display: 'flex', gap: 0.5, ml: 1, flexShrink: 0 }}>
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); openEdit(t); }}
sx={{ color: c.text.tertiary, '&:hover': { color: c.accent.primary } }}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); handleDelete(t.id); }}
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error } }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Box>
{t.description && (
<Typography
sx={{
color: c.text.tertiary,
fontSize: '0.8rem',
mb: 1.5,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{t.description}
</Typography>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Chip
label={`${t.fields.length} field${t.fields.length !== 1 ? 's' : ''}`}
size="small"
sx={{
bgcolor: 'rgba(174,86,48,0.08)',
color: c.accent.primary,
fontSize: '0.7rem',
height: 22,
}}
/>
{t.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
sx={{
bgcolor: c.bg.secondary,
color: c.text.muted,
fontSize: '0.7rem',
height: 22,
}}
/>
))}
</Box>
</Box>
))}
</Box>
)}
{/* Editor Dialog */}
<Dialog
open={editorOpen}
onClose={() => setEditorOpen(false)}
maxWidth="md"
fullWidth
PaperProps={claudePaperProps}
>
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600 }}>
{editingId ? 'Edit Template' : 'New Template'}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '8px !important' }}>
<TextField
label="Name"
value={editor.name}
onChange={(e) => setEditor((p) => ({ ...p, name: e.target.value }))}
fullWidth
size="small"
sx={inputSx}
/>
<TextField
label="Description"
value={editor.description}
onChange={(e) => setEditor((p) => ({ ...p, description: e.target.value }))}
fullWidth
size="small"
multiline
rows={2}
sx={inputSx}
/>
<TextField
label="Template (use {{field_name}} for placeholders)"
value={editor.template}
onChange={(e) => setEditor((p) => ({ ...p, template: e.target.value }))}
fullWidth
size="small"
multiline
rows={5}
sx={{
...inputSx,
'& .MuiOutlinedInput-root': {
...inputSx['& .MuiOutlinedInput-root'],
fontFamily: c.font.mono,
fontSize: '0.85rem',
},
}}
/>
{/* Fields */}
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontWeight: 600 }}>
Fields
</Typography>
<Button
size="small"
startIcon={<AddIcon />}
onClick={addField}
sx={{ color: c.accent.primary, textTransform: 'none', fontSize: '0.8rem' }}
>
Add Field
</Button>
</Box>
{editor.fields.map((field, idx) => (
<Box
key={idx}
sx={{
display: 'flex',
gap: 1,
mb: 1,
alignItems: 'flex-start',
bgcolor: c.bg.elevated,
p: 1.5,
borderRadius: 2,
border: `1px solid ${c.border.subtle}`,
}}
>
<TextField
label="Field name"
value={field.name}
onChange={(e) => updateField(idx, { name: e.target.value })}
size="small"
sx={{ ...inputSx, flex: 1 }}
/>
<TextField
select
label="Type"
value={field.type}
onChange={(e) => updateField(idx, { type: e.target.value as TemplateField['type'] })}
size="small"
sx={{ ...inputSx, minWidth: 130 }}
SelectProps={{ MenuProps: { PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } } }}
>
{FIELD_TYPES.map((ft) => (
<MenuItem key={ft} value={ft}>{ft}</MenuItem>
))}
</TextField>
{(field.type === 'select' || field.type === 'multi-select') && (
<TextField
label="Options (comma-sep)"
value={(field.options || []).join(', ')}
onChange={(e) =>
updateField(idx, {
options: e.target.value.split(',').map((s) => s.trim()).filter(Boolean),
})
}
size="small"
sx={{ ...inputSx, flex: 1 }}
/>
)}
<TextField
label="Default"
value={field.default ?? ''}
onChange={(e) => updateField(idx, { default: e.target.value || undefined })}
size="small"
sx={{ ...inputSx, flex: 0.7 }}
/>
<IconButton
onClick={() => removeField(idx)}
sx={{ color: c.text.tertiary, mt: 0.5, '&:hover': { color: c.status.error } }}
>
<RemoveCircleOutlineIcon fontSize="small" />
</IconButton>
</Box>
))}
</Box>
{/* Tags */}
<Box>
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontWeight: 600, mb: 1 }}>
Tags
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
{editor.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
onDelete={() => removeTag(tag)}
sx={{
bgcolor: c.bg.secondary,
color: c.text.muted,
'& .MuiChip-deleteIcon': { color: c.text.tertiary, '&:hover': { color: c.status.error } },
}}
/>
))}
<TextField
placeholder="Add tag..."
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag();
}
}}
size="small"
sx={{ ...inputSx, width: 140 }}
/>
</Box>
</Box>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => setEditorOpen(false)} sx={{ color: c.text.tertiary }}>
Cancel
</Button>
<Button
onClick={handleSave}
variant="contained"
disabled={!editor.name.trim() || !editor.template.trim()}
sx={{
bgcolor: c.accent.primary,
'&:hover': { bgcolor: c.accent.pressed },
'&.Mui-disabled': { bgcolor: c.bg.secondary, color: c.text.ghost },
textTransform: 'none',
fontWeight: 600,
}}
>
{editingId ? 'Save Changes' : 'Create Template'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default Templates;
+4 -5
View File
@@ -110,7 +110,7 @@ interface Integration {
credentialFields?: CredentialField[];
connectLabel?: string;
connectInstructions?: string;
authType?: 'none' | 'oauth2' | 'env_vars';
authType?: 'none' | 'oauth2' | 'env_vars' | 'device_code';
}
const INTEGRATIONS: Integration[] = [
@@ -186,7 +186,7 @@ const INTEGRATIONS: Integration[] = [
<path d="M11.4 24H0V12.6L11.4 24zM24 24H12.6V12.6L24 24zM11.4 11.4H0V0l11.4 11.4zM24 11.4H12.6V0L24 11.4z" fill="#0078D4"/>
</svg>
),
authType: 'device_code' as any,
authType: 'device_code',
},
{
id: 'notion',
@@ -1179,14 +1179,13 @@ const Tools: React.FC = () => {
<Box sx={{
width: 28, height: 28, borderRadius: 1.5, flexShrink: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
bgcolor: `${out.color}20`,
bgcolor: `${c.accent.primary}20`,
}}>
<ViewQuiltIcon sx={{ fontSize: 14, color: out.color }} />
<ViewQuiltIcon sx={{ fontSize: 14, color: c.accent.primary }} />
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{ color: c.text.primary, fontSize: '0.8rem', fontWeight: 500 }}>{out.name}</Typography>
<Chip label={out.category} size="small" sx={{ bgcolor: `${out.color}15`, color: out.color, fontSize: '0.65rem', height: 18, '& .MuiChip-label': { px: 0.6 } }} />
</Box>
{out.description && (
<Typography sx={{ color: c.text.ghost, fontSize: '0.7rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
+3 -8
View File
@@ -58,17 +58,12 @@ function getFileIcon(filename: string): React.ReactNode {
}
}
function getEditorLanguage(filename: string): string {
function getEditorLanguage(filename: string): 'html' | 'python' | 'json' {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'html': case 'htm': return 'html';
case 'py': return 'python';
case 'json': return 'json';
case 'js': case 'jsx': return 'javascript';
case 'ts': case 'tsx': return 'typescript';
case 'css': case 'scss': return 'css';
case 'md': return 'markdown';
default: return 'plaintext';
default: return 'html';
}
}
@@ -216,7 +211,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ msg, c }) => {
interface AutoRunLogProps {
messages: AgentMessage[];
status: string | null;
logEndRef: React.RefObject<HTMLDivElement | null>;
logEndRef: React.RefObject<HTMLDivElement>;
c: ReturnType<typeof useClaudeTokens>;
}
+2 -2
View File
@@ -15,8 +15,8 @@ export interface BrowserWebview extends HTMLElement {
executeJavaScript: (code: string) => Promise<any>;
sendInputEvent: (event: any) => void;
getWebContentsId: () => number;
addEventListener: (event: string, listener: (...args: any[]) => void) => void;
removeEventListener: (event: string, listener: (...args: any[]) => void) => void;
addEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | AddEventListenerOptions) => void;
removeEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | EventListenerOptions) => void;
}
const registry = new Map<string, BrowserWebview>();
@@ -22,10 +22,6 @@ export function useKeyboardShortcuts() {
navigate('/');
return;
}
if (e.key === 't' && !e.metaKey && !e.ctrlKey) {
navigate('/templates');
return;
}
if (e.key === 'A' && e.shiftKey && !e.metaKey && !e.ctrlKey) {
for (const session of Object.values(sessions)) {
-2
View File
@@ -1,7 +1,6 @@
import { configureStore } from '@reduxjs/toolkit';
import tempStateReducer from './tempStateSlice';
import agentsReducer from './agentsSlice';
import templatesReducer from './templatesSlice';
import skillsReducer from './skillsSlice';
import toolsReducer from './toolsSlice';
import modesReducer from './modesSlice';
@@ -19,7 +18,6 @@ export const store = configureStore({
reducer: {
tempState: tempStateReducer,
agents: agentsReducer,
templates: templatesReducer,
skills: skillsReducer,
tools: toolsReducer,
modes: modesReducer,
-117
View File
@@ -1,117 +0,0 @@
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { API_BASE } from '@/shared/config';
const TEMPLATES_API = `${API_BASE}/templates`;
export interface TemplateField {
name: string;
type: 'str' | 'int' | 'float' | 'select' | 'multi-select' | 'literal';
options?: string[];
default?: any;
required: boolean;
}
export interface PromptTemplate {
id: string;
name: string;
description: string;
template: string;
fields: TemplateField[];
tags: string[];
}
interface TemplatesState {
items: Record<string, PromptTemplate>;
loading: boolean;
loaded: boolean;
}
const initialState: TemplatesState = {
items: {},
loading: false,
loaded: false,
};
export const fetchTemplates = createAsyncThunk(
'templates/fetch',
async () => {
const res = await fetch(`${TEMPLATES_API}/list`);
const data = await res.json();
return data.templates as PromptTemplate[];
},
{ condition: (_, { getState }) => !(getState() as { templates: TemplatesState }).templates.loading },
);
export const createTemplate = createAsyncThunk(
'templates/create',
async (body: Omit<PromptTemplate, 'id'>) => {
const res = await fetch(`${TEMPLATES_API}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
return data.template as PromptTemplate;
}
);
export const updateTemplate = createAsyncThunk(
'templates/update',
async ({ id, ...updates }: Partial<PromptTemplate> & { id: string }) => {
const res = await fetch(`${TEMPLATES_API}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
const data = await res.json();
return data.template as PromptTemplate;
}
);
export const deleteTemplate = createAsyncThunk('templates/delete', async (id: string) => {
await fetch(`${TEMPLATES_API}/${id}`, { method: 'DELETE' });
return id;
});
export const renderTemplate = createAsyncThunk(
'templates/render',
async ({ templateId, values }: { templateId: string; values: Record<string, any> }) => {
const res = await fetch(`${TEMPLATES_API}/render`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ template_id: templateId, values }),
});
const data = await res.json();
return data.rendered as string;
}
);
const templatesSlice = createSlice({
name: 'templates',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchTemplates.pending, (state) => { state.loading = true; })
.addCase(fetchTemplates.fulfilled, (state, action) => {
state.loading = false;
state.loaded = true;
state.items = {};
for (const t of action.payload) {
state.items[t.id] = t;
}
})
.addCase(fetchTemplates.rejected, (state) => { state.loading = false; state.loaded = true; })
.addCase(createTemplate.fulfilled, (state, action) => {
state.items[action.payload.id] = action.payload;
})
.addCase(updateTemplate.fulfilled, (state, action) => {
state.items[action.payload.id] = action.payload;
})
.addCase(deleteTemplate.fulfilled, (state, action) => {
delete state.items[action.payload];
});
},
});
export default templatesSlice.reducer;
File diff suppressed because one or more lines are too long