`;
}
container.appendChild(agentElement);
});
// Add one "add item" indicator at the end of all agents
const addAgentIndicator = document.createElement('div');
addAgentIndicator.className = 'add-agent-indicator';
addAgentIndicator.innerHTML = `
Add Next Item
`;
container.appendChild(addAgentIndicator);
} else {
if (dropZone) dropZone.style.display = 'flex';
}
}
updateWorkflowStats() {
const stats = { agents: 0, commands: 0, mcps: 0 };
let totalStepsCount = 0;
// Count from hierarchical structure
const agents = this.workflowState.agents || [];
agents.forEach(agentData => {
const agentType = agentData.agent.type;
if (agentType === 'agent') {
stats.agents++;
} else if (agentType === 'command') {
stats.commands++;
} else if (agentType === 'mcp') {
stats.mcps++;
}
totalStepsCount++; // Count main level item
// Count sub-items (commands/MCPs inside agents)
if (agentData.subItems && agentData.subItems.length > 0) {
agentData.subItems.forEach(subItem => {
totalStepsCount++; // Count each sub-item for total
// Count sub-items in their respective categories
if (subItem.type === 'command') {
stats.commands++;
} else if (subItem.type === 'mcp') {
stats.mcps++;
}
});
}
});
const agentCount = document.getElementById('agentCount');
const commandCount = document.getElementById('commandCount');
const mcpCount = document.getElementById('mcpCount');
const totalSteps = document.getElementById('totalSteps');
if (agentCount) agentCount.textContent = stats.agents;
if (commandCount) commandCount.textContent = stats.commands;
if (mcpCount) mcpCount.textContent = stats.mcps;
if (totalSteps) totalSteps.textContent = totalStepsCount;
}
handleComponentSearch(event) {
const searchTerm = event.target.value.toLowerCase();
document.querySelectorAll('.tree-file').forEach(file => {
const match = file.dataset.componentName.toLowerCase().includes(searchTerm);
file.style.display = match ? '' : 'none';
});
}
clearWorkflow() {
if (confirm('Are you sure you want to clear the workflow?')) {
this.workflowState.steps = [];
this.workflowState.agents = [];
this.renderWorkflowSteps();
this.updateWorkflowStats();
}
}
openPropertiesModal() {
const agents = this.workflowState.agents || [];
if (agents.length === 0) {
this.showError('Add at least one agent to the workflow.');
return;
}
const modal = document.getElementById('propertiesModal');
if (modal) modal.style.display = 'block';
}
closePropertiesModal() {
const modal = document.getElementById('propertiesModal');
if (modal) modal.style.display = 'none';
}
saveAndGenerateWorkflow() {
const nameInput = document.getElementById('workflowName');
const descInput = document.getElementById('workflowDescription');
const tagsInput = document.getElementById('workflowTags');
const promptInput = document.getElementById('workflowPrompt');
if (nameInput) this.workflowState.properties.name = nameInput.value;
if (descInput) this.workflowState.properties.description = descInput.value;
if (tagsInput) {
this.workflowState.properties.tags = tagsInput.value.split(',').map(t => t.trim());
}
if (promptInput) this.workflowState.properties.prompt = promptInput.value.trim();
if (!this.workflowState.properties.name) {
this.showError('Workflow name is required.');
return;
}
this.closePropertiesModal();
this.generateWorkflow();
}
async generateWorkflow() {
try {
const workflowCommand = await this.generateWorkflowHash();
const yamlContent = this.generateWorkflowYAML();
this.showGenerateModal(workflowCommand, yamlContent);
} catch (error) {
console.error('Error generating workflow:', error);
this.showError('Failed to generate workflow.');
}
}
async generateWorkflowHash() {
// NEW APPROACH: Generate component-based command instead of embedded hash
const agents = this.workflowState.steps.filter(step => step.type === 'agent').map(step => step.path.replace(/\.md$/, ''));
const commands = this.workflowState.steps.filter(step => step.type === 'command').map(step => step.path.replace(/\.md$/, ''));
const mcps = this.workflowState.steps.filter(step => step.type === 'mcp').map(step => step.path.replace(/\.json$/, ''));
// Generate YAML and encode it
const yamlContent = this.generateWorkflowYAML();
const encodedYaml = btoa(unescape(encodeURIComponent(yamlContent)));
// Build command components for Method 1 (full command with embedded YAML)
const commandParts = [];
if (agents.length > 0) {
commandParts.push(`--agent ${agents.join(',')}`);
}
if (commands.length > 0) {
commandParts.push(`--command ${commands.join(',')}`);
}
if (mcps.length > 0) {
commandParts.push(`--mcp ${mcps.join(',')}`);
}
// Add prompt parameter if provided
if (this.workflowState.properties.prompt) {
commandParts.push(`--prompt "${this.workflowState.properties.prompt}"`);
}
// Method 1: Full command with embedded YAML
const fullCommand = `npx claude-code-templates@latest ${commandParts.join(' ')} --workflow ${encodedYaml}`;
// Method 2: Components only command (without embedded YAML)
// Remove prompt from components-only command since YAML isn't included
const componentsParts = commandParts.filter(part => !part.startsWith('--prompt'));
const componentsOnlyCommand = componentsParts.length > 0 ?
`npx claude-code-templates@latest ${componentsParts.join(' ')}` :
'# No components to install';
// Create a short hash for reference
const shortHash = CryptoJS.SHA256(fullCommand).toString(CryptoJS.enc.Hex).substring(0, 8);
// Store locally for reference
localStorage.setItem(`workflow_${shortHash}`, JSON.stringify({
fullCommand: fullCommand,
componentsCommand: componentsOnlyCommand,
yaml: yamlContent,
metadata: this.workflowState.properties,
timestamp: new Date().toISOString()
}));
return {
fullCommand: fullCommand,
componentsCommand: componentsOnlyCommand,
yaml: yamlContent
};
}
// Simple and reliable string compression with Unicode support
compressString(str) {
try {
// Simple approach: remove redundant whitespace and use efficient encoding
let compressed = str
// Remove unnecessary whitespace
.replace(/\s+/g, ' ')
// Remove whitespace around common JSON characters
.replace(/\s*([{}[\]:,])\s*/g, '$1')
// Remove quotes around simple keys (where safe)
.replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, '$1:');
// Use proper Unicode-safe Base64 encoding
return btoa(unescape(encodeURIComponent(compressed)));
} catch (error) {
console.warn('Compression failed, using fallback:', error);
// Last resort: just encode the original string
try {
return btoa(unescape(encodeURIComponent(str)));
} catch (finalError) {
// If even that fails, return the original string (will cause error later)
console.error('All encoding methods failed:', finalError);
return str;
}
}
}
// Decompress string (for CLI)
decompressString(compressed) {
try {
// Since we're using simple Base64 encoding, just decode it
return decodeURIComponent(escape(atob(compressed)));
} catch (error) {
throw new Error('Failed to decompress data: ' + error.message);
}
}
async getComponentsData() {
const componentsData = {};
// Group components by type and get their full content
for (const step of this.workflowState.steps) {
if (!componentsData[step.type]) {
componentsData[step.type] = [];
}
// Find the component in the loaded data
const componentsList = this.workflowState.components[step.type + 's'] || [];
const fullComponent = componentsList.find(c => c.name === step.name);
if (fullComponent) {
componentsData[step.type].push({
name: step.name,
path: step.path,
category: step.category,
content: fullComponent.content,
type: step.type
});
}
}
return componentsData;
}
generateWorkflowYAML() {
// Ensure agents array exists and has valid structure
const agents = this.workflowState.agents || [];
const totalSteps = agents.reduce((total, agent) => total + 1 + (agent.subItems ? agent.subItems.length : 0), 0);
const promptComment = this.workflowState.properties.prompt
? `# Initial Prompt: ${this.workflowState.properties.prompt}\n`
: '';
return `# Workflow: ${this.workflowState.properties.name}
# Description: ${this.workflowState.properties.description}
${promptComment}# Generated: ${new Date().toISOString()}
# Agents: ${agents.length} total
# Components: ${totalSteps} total
name: "${this.workflowState.properties.name}"
description: "${this.workflowState.properties.description}"
tags: [${this.workflowState.properties.tags.map(tag => `"${tag}"`).join(', ')}]
version: "2.0.0"${this.workflowState.properties.prompt ? '\nprompt: "' + this.workflowState.properties.prompt + '"' : ''}
# Hierarchical workflow structure: Agents â Commands/MCPs
agents:
${agents.map((agentData, i) => ` - agent: ${i + 1}
type: ${agentData.agent.type}
name: "${agentData.agent.name}"
path: "${agentData.agent.type === 'mcp' ? `.mcp.json#${agentData.agent.name}` : `.claude/${agentData.agent.type}s/${agentData.agent.path}`}"
category: "${agentData.agent.category}"
description: "${agentData.agent.description}"
tasks:${(agentData.subItems && agentData.subItems.length > 0) ? agentData.subItems.map((subItem, j) => `
- task: ${j + 1}
type: ${subItem.type}
name: "${subItem.name}"
path: "${subItem.type === 'mcp' ? `.mcp.json#${subItem.name}` : `.claude/${subItem.type}s/${subItem.path}`}"
category: "${subItem.category}"
description: "${subItem.description}"`).join('') : `
- task: 1
type: "default"
name: "No specific tasks assigned"
description: "This agent has no specific commands or MCPs assigned"`}`).join('\n')}
execution:
mode: "hierarchical"
on_error: "stop"
sequence: "agent-by-agent"
components:
agents: [${agents.filter(a => a.agent.type === 'agent').map(a => `".claude/agents/${a.agent.path}"`).join(', ')}]
commands: [${[...agents.filter(a => a.agent.type === 'command').map(a => `".claude/commands/${a.agent.path}"`), ...agents.flatMap(a => (a.subItems || []).filter(s => s.type === 'command').map(s => `".claude/commands/${s.path}`))].join(', ')}]
mcps: [${[...agents.filter(a => a.agent.type === 'mcp').map(a => `".mcp.json#${a.agent.name}"`), ...agents.flatMap(a => (a.subItems || []).filter(s => s.type === 'mcp').map(s => `".mcp.json#${s.name}`))].join(', ')}]
# Instructions for Claude Code:
# This hierarchical workflow contains ${agents.length} agents with their assigned tasks.
# Each agent should be loaded first, then its associated commands and MCPs should be executed in sequence.`;
}
showGenerateModal(commandData, yaml) {
const { fullCommand, componentsCommand } = commandData;
const fullCommandEl = document.getElementById('fullCommand');
const componentsOnlyCommandEl = document.getElementById('componentsOnlyCommand');
const yamlEl = document.getElementById('yamlContent');
const workflowFileNameEl = document.getElementById('workflowFileName');
const modal = document.getElementById('generateModal');
if (fullCommandEl) fullCommandEl.textContent = fullCommand;
if (componentsOnlyCommandEl) componentsOnlyCommandEl.textContent = componentsCommand;
if (yamlEl) yamlEl.textContent = yaml;
// Update workflow file name dynamically
if (workflowFileNameEl) {
const workflowName = this.workflowState.properties.name || 'workflow';
const fileName = `${workflowName.replace(/[^a-z0-9]/gi, '-').toLowerCase()}.yaml`;
workflowFileNameEl.textContent = fileName;
}
// Show prompt indicator if prompt is provided
const promptIndicator = document.getElementById('promptIndicator');
const promptText = document.getElementById('promptText');
if (promptIndicator && promptText) {
if (this.workflowState.properties.prompt) {
promptText.textContent = `"${this.workflowState.properties.prompt}"`;
promptIndicator.style.display = 'flex';
} else {
promptIndicator.style.display = 'none';
}
}
if (modal) modal.style.display = 'block';
// Setup close modal functionality
const closeModalBtn = document.getElementById('closeModal');
if (closeModalBtn) {
closeModalBtn.onclick = () => modal.style.display = 'none';
}
// Close modal when clicking outside
window.onclick = (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
};
// Add copy functionality to the buttons in the modal
const copyFullCommandBtn = document.getElementById('copyFullCommand');
if (copyFullCommandBtn) {
copyFullCommandBtn.onclick = () => copyToClipboard(fullCommand, 'Full command copied!');
}
const copyComponentsCommandBtn = document.getElementById('copyComponentsCommand');
if (copyComponentsCommandBtn) {
copyComponentsCommandBtn.onclick = () => copyToClipboard(componentsCommand, 'Components command copied!');
}
// Original YAML buttons (in accordion)
const copyYamlBtn = document.getElementById('copyYaml');
if (copyYamlBtn) {
copyYamlBtn.onclick = () => copyToClipboard(yamlEl.textContent, 'YAML content copied!');
}
const downloadYamlBtn = document.getElementById('downloadYaml');
if (downloadYamlBtn) {
downloadYamlBtn.onclick = () => {
const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}.yaml`;
const blob = new Blob([yaml], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('YAML downloaded successfully!', 'success');
};
}
// Manual method YAML buttons
const copyYamlManualBtn = document.getElementById('copyYamlManual');
if (copyYamlManualBtn) {
copyYamlManualBtn.onclick = () => copyToClipboard(yamlEl.textContent, 'YAML content copied!');
}
const downloadYamlManualBtn = document.getElementById('downloadYamlManual');
if (downloadYamlManualBtn) {
downloadYamlManualBtn.onclick = () => {
const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}.yaml`;
const blob = new Blob([yaml], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('YAML downloaded successfully!', 'success');
};
}
const shareWorkflowBtn = document.getElementById('shareWorkflow');
if (shareWorkflowBtn) {
shareWorkflowBtn.onclick = () => {
// For the new approach, we share the command directly
navigator.clipboard.writeText(fullCommand).then(() => {
showNotification('Installation command copied to clipboard!', 'success');
}).catch(err => {
console.error('Failed to copy command: ', err);
showNotification('Failed to copy command', 'error');
});
};
}
}
previewWorkflow() {
const agents = this.workflowState.agents || [];
if (agents.length === 0) {
this.showError('Add at least one component to preview the workflow.');
return;
}
// Set default properties if not set
if (!this.workflowState.properties.name) {
this.workflowState.properties.name = 'Untitled Workflow';
}
if (!this.workflowState.properties.description) {
this.workflowState.properties.description = 'Generated workflow preview';
}
if (!this.workflowState.properties.tags.length) {
this.workflowState.properties.tags = ['preview'];
}
// Generate YAML content
const yamlContent = this.generateWorkflowYAML();
// Show in modal
this.showPreviewModal(yamlContent);
}
showPreviewModal(yamlContent) {
const modal = document.getElementById('previewModal');
const yamlElement = document.getElementById('previewYamlContent');
const lineNumbersElement = document.getElementById('previewLineNumbers');
if (!modal || !yamlElement || !lineNumbersElement) return;
// Apply syntax highlighting to YAML content
const highlightedYaml = this.highlightYaml(yamlContent);
yamlElement.innerHTML = highlightedYaml;
yamlElement.className = 'yaml-syntax';
// Generate line numbers
const lines = yamlContent.split('\n');
lineNumbersElement.innerHTML = lines.map((_, index) =>
`${index + 1}`
).join('');
// Show modal
modal.style.display = 'block';
// Setup event listeners
this.setupPreviewModalListeners(yamlContent);
}
highlightYaml(yamlContent) {
return yamlContent
.split('\n')
.map(line => this.highlightYamlLine(line))
.join('\n');
}
highlightYamlLine(line) {
// Handle comments (everything after #)
if (line.trim().startsWith('#')) {
return `${this.escapeHtml(line)}`;
}
// Handle lines with comments
const commentIndex = line.indexOf('#');
let mainPart = line;
let commentPart = '';
if (commentIndex > -1) {
mainPart = line.substring(0, commentIndex);
commentPart = `${this.escapeHtml(line.substring(commentIndex))}`;
}
// Handle key-value pairs
const keyValueMatch = mainPart.match(/^(\s*)([\w-]+)(\s*:\s*)(.*)/);
if (keyValueMatch) {
const [, indent, key, colon, value] = keyValueMatch;
let highlightedValue = value;
// Highlight different value types
if (value.trim().match(/^".*"$/)) {
// String in quotes
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim().match(/^[\d.]+$/)) {
// Numbers
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim().match(/^(true|false|yes|no|on|off)$/i)) {
// Booleans
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim().match(/^[\w\/-]+\.(md|json|yaml|yml)$/)) {
// File paths
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim().startsWith('[') && value.trim().endsWith(']')) {
// Arrays
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim()) {
// Regular strings
highlightedValue = `${this.escapeHtml(value)}`;
}
return `${this.escapeHtml(indent)}${this.escapeHtml(key)}${this.escapeHtml(colon)}${highlightedValue}${commentPart}`;
}
// Handle list items
const listMatch = mainPart.match(/^(\s*-\s*)(.*)/);
if (listMatch) {
const [, listMarker, content] = listMatch;
// Check if it's a key-value in list
const listKeyValueMatch = content.match(/^([\w-]+)(\s*:\s*)(.*)/);
if (listKeyValueMatch) {
const [, key, colon, value] = listKeyValueMatch;
let highlightedValue = value;
if (value.trim().match(/^".*"$/)) {
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim().match(/^[\d.]+$/)) {
highlightedValue = `${this.escapeHtml(value)}`;
} else if (value.trim()) {
highlightedValue = `${this.escapeHtml(value)}`;
}
return `${this.escapeHtml(listMarker)}${this.escapeHtml(key)}${this.escapeHtml(colon)}${highlightedValue}${commentPart}`;
} else {
return `${this.escapeHtml(listMarker)}${this.escapeHtml(content)}${commentPart}`;
}
}
// Handle section headers (main keys without indentation)
const sectionMatch = mainPart.match(/^([\w-]+)(\s*:\s*)(.*)/);
if (sectionMatch && !mainPart.startsWith(' ')) {
const [, key, colon, value] = sectionMatch;
return `${this.escapeHtml(key)}${this.escapeHtml(colon)}${this.escapeHtml(value)}${commentPart}`;
}
// Default: return line as is
return `${this.escapeHtml(mainPart)}${commentPart}`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setupPreviewModalListeners(yamlContent) {
// Close modal events
const closeBtn = document.getElementById('closePreviewModal');
const closeModalBtn = document.getElementById('closePreviewModalBtn');
const modal = document.getElementById('previewModal');
if (closeBtn) {
closeBtn.onclick = () => modal.style.display = 'none';
}
if (closeModalBtn) {
closeModalBtn.onclick = () => modal.style.display = 'none';
}
// Copy YAML
const copyBtn = document.getElementById('copyPreviewYaml');
if (copyBtn) {
copyBtn.onclick = () => {
navigator.clipboard.writeText(yamlContent).then(() => {
showNotification('YAML copied to clipboard!', 'success');
}).catch(() => {
showNotification('Failed to copy YAML', 'error');
});
};
}
// Download YAML
const downloadBtn = document.getElementById('downloadPreviewYaml');
if (downloadBtn) {
downloadBtn.onclick = () => {
const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}-preview.yaml`;
const blob = new Blob([yamlContent], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showNotification('YAML downloaded successfully!', 'success');
};
}
// Close modal when clicking outside
window.onclick = (event) => {
if (event.target === modal) {
modal.style.display = 'none';
}
};
}
showError(message) {
showNotification(message, 'error');
}
}
// Global functions for workflow events (called from onclick)
function showWorkflowComponentDetails(type, name, path, category) {
const component = window.dataLoader.findComponent(name, type);
if (component) {
showComponentModal(component);
} else {
console.warn('Component not found:', type, name);
}
}
function addComponentFromButton(event) {
const treeFile = event.target.closest('.tree-file');
const componentData = { ...treeFile.dataset };
if (window.workflowManager) {
window.workflowManager.addWorkflowStep(componentData);
}
}
function removeWorkflowStep(stepId) {
if (window.workflowManager) {
window.workflowManager.workflowState.steps =
window.workflowManager.workflowState.steps.filter(step => step.id !== stepId);
window.workflowManager.renderWorkflowSteps();
window.workflowManager.updateWorkflowStats();
}
}
function removeAgent(agentId) {
if (window.workflowManager && confirm('Remove this agent and all its associated commands/MCPs?')) {
// Remove from agents array
window.workflowManager.workflowState.agents =
window.workflowManager.workflowState.agents.filter(agentData => agentData.agent.id !== agentId);
// Remove from legacy steps array
window.workflowManager.workflowState.steps =
window.workflowManager.workflowState.steps.filter(step => step.id !== agentId);
window.workflowManager.renderWorkflowSteps();
window.workflowManager.updateWorkflowStats();
}
}
function removeSubItem(agentId, subItemId) {
if (window.workflowManager) {
const agentIndex = window.workflowManager.workflowState.agents.findIndex(
agentData => agentData.agent.id === agentId
);
if (agentIndex !== -1) {
window.workflowManager.workflowState.agents[agentIndex].subItems =
window.workflowManager.workflowState.agents[agentIndex].subItems.filter(
subItem => subItem.id !== subItemId
);
// Remove from legacy steps array
window.workflowManager.workflowState.steps =
window.workflowManager.workflowState.steps.filter(step => step.id !== subItemId);
window.workflowManager.renderWorkflowSteps();
window.workflowManager.updateWorkflowStats();
}
}
}
// Helper function to scroll to drop zone
function scrollToDropZone() {
const dropZone = document.querySelector('.drop-zone');
if (dropZone) {
dropZone.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Add a brief highlight effect
dropZone.classList.add('drag-over');
setTimeout(() => {
dropZone.classList.remove('drag-over');
}, 1000);
}
}
// Helper functions for drag and drop
function allowDrop(event) {
event.preventDefault();
}
function dropOnAgent(event, agentId) {
event.preventDefault();
event.stopPropagation(); // Prevent the main drop handler from also firing
// Remove drag-over class
event.target.classList.remove('drag-over');
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'));
if (componentData.componentType === 'command' || componentData.componentType === 'mcp') {
window.workflowManager.addWorkflowStep(componentData, agentId);
}
} catch (error) {
console.error('Error handling agent drop:', error);
}
}
function dropOnAddItem(event) {
event.preventDefault();
event.stopPropagation(); // Prevent the main drop handler from also firing
// Remove drag-over class
event.target.classList.remove('drag-over');
event.target.closest('.add-agent-btn')?.classList.remove('drag-over');
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'));
// Add as main level item (no targetAgentId)
window.workflowManager.addWorkflowStep(componentData);
} catch (error) {
console.error('Error handling add item drop:', error);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.workflowManager = new WorkflowManager();
window.workflowManager.init();
});