claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
1,400 lines (1,213 loc) • 51.4 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code Studio - AI Development Interface</title>
<meta name="description" content="Execute Claude Code locally or in cloud sandbox environments with real-time task management">
<link rel="stylesheet" href="css/styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Additional styles for sandbox interface */
.sandbox-interface {
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
font-family: 'Inter', monospace;
}
.sandbox-header {
padding: 1.5rem 0 1rem;
border-bottom: 1px solid var(--border-secondary);
}
.ascii-title {
margin-bottom: 1rem;
overflow-x: hidden;
width: 100%;
}
.ascii-art {
font-size: 0.65rem;
line-height: 0.8;
color: var(--accent-color);
margin: 0;
text-align: center;
overflow: hidden;
white-space: pre;
width: 100%;
max-width: 100vw;
transform: scale(1);
transform-origin: center;
}
@media (max-width: 768px) {
.ascii-art {
font-size: 0.5rem;
transform: scale(0.85);
}
}
@media (max-width: 480px) {
.ascii-art {
font-size: 0.4rem;
transform: scale(0.7);
}
}
.header-nav {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.nav-links {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.nav-link {
display: flex;
align-items: center;
padding: 0.375rem 0.75rem;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 6px;
color: var(--text-primary);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-link:hover {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
transform: translateY(-1px);
}
.nav-icon {
margin-right: 0.375rem;
font-size: 0.9rem;
}
.nav-separator {
color: var(--text-secondary);
margin: 0 0.25rem;
font-weight: 300;
}
.terminal-subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
text-align: center;
}
.task-input-section {
padding: 2rem 0;
border-bottom: 1px solid var(--border-secondary);
}
.task-input-container {
max-width: 900px;
margin: 0 auto;
}
.main-input-container {
position: relative;
}
.bottom-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
}
.bottom-left {
display: flex;
align-items: center;
}
.bottom-right {
display: flex;
align-items: center;
}
.task-textarea {
width: 100%;
min-height: 200px;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 12px;
padding: 1.5rem;
color: var(--text-primary);
font-family: 'Inter', sans-serif;
font-size: 1rem;
line-height: 1.6;
resize: vertical;
margin-bottom: 0;
}
.task-textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.1);
}
.task-textarea::placeholder {
color: var(--text-secondary);
}
.code-btn {
background: var(--accent-color);
color: white;
border: none;
padding: 1rem 2.5rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1rem;
letter-spacing: 0.025em;
}
.code-btn:hover {
background: #e55555;
transform: translateY(-1px);
}
.code-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
transform: none;
}
.tasks-section {
padding: 2rem 0;
}
.tasks-container {
max-width: 900px;
margin: 0 auto;
}
/* Tasks Tabs */
.tasks-tabs {
display: flex;
gap: 0;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border-secondary);
}
.tab-btn {
background: none;
border: none;
padding: 1rem 2rem;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
display: flex;
align-items: center;
gap: 0.5rem;
}
.tab-btn:hover {
color: var(--text-primary);
}
.tab-btn.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
}
.tab-count {
background: var(--bg-secondary);
color: var(--text-secondary);
padding: 0.2rem 0.5rem;
border-radius: 10px;
font-size: 0.8rem;
min-width: 20px;
text-align: center;
}
.tab-btn.active .tab-count {
background: var(--accent-color);
color: white;
}
/* Tab Content */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.task-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.task-item {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
transition: all 0.2s ease;
cursor: pointer;
}
.task-item:hover {
border-color: var(--accent-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.1);
}
.task-header {
display: flex;
justify-content: between;
align-items: flex-start;
margin-bottom: 1rem;
}
.task-info {
flex: 1;
}
.task-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.task-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.task-status {
padding: 0.2rem 0.6rem;
border-radius: 12px;
}
.task-agent-label {
background: var(--accent-color);
color: white;
padding: 0.2rem 0.5rem;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
margin-left: 0.5rem;
display: inline-block;
}
.status-running {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1px solid #ffc107;
}
.status-completed {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1px solid #28a745;
}
.status-failed {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1px solid #dc3545;
}
.task-progress {
background: var(--bg-primary);
border-radius: 4px;
height: 4px;
margin: 0.5rem 0;
overflow: hidden;
}
.progress-bar {
background: var(--accent-color);
height: 100%;
transition: width 0.3s ease;
animation: progressPulse 2s infinite;
}
@keyframes progressPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.task-details {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-secondary);
}
.task-output {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 4px;
padding: 0.75rem;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
color: var(--text-secondary);
max-height: 200px;
overflow-y: auto;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--text-secondary);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.floating-status {
position: fixed;
top: 2rem;
right: 2rem;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1rem;
display: none;
}
/* Compact Execution Mode Toggle */
.compact-mode-selector {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.mode-toggle-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.mode-info-icon {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: help;
font-size: 0.7rem;
color: var(--text-secondary);
transition: all 0.2s ease;
position: relative;
}
.mode-info-icon:hover {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.mode-info-tooltip {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 0.5rem;
font-size: 0.8rem;
color: var(--text-primary);
white-space: nowrap;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mode-info-icon:hover .mode-info-tooltip {
opacity: 1;
visibility: visible;
}
.compact-toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 28px;
}
.compact-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.compact-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
transition: 0.3s;
border-radius: 14px;
display: flex;
align-items: center;
}
.compact-toggle-slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
background: var(--text-secondary);
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.compact-toggle-switch input:checked + .compact-toggle-slider {
background: rgba(255, 107, 107, 0.1);
border-color: var(--accent-color);
}
.compact-toggle-switch input:checked + .compact-toggle-slider:before {
background: var(--accent-color);
transform: translateX(32px);
}
.toggle-labels-compact {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
.toggle-label-active {
color: var(--text-primary);
font-weight: 500;
}
.api-keys-notice.cloud-only {
display: block;
}
.api-keys-notice.local-hidden {
display: none;
}
/* Agent Autocomplete Styles */
.autocomplete-container {
position: relative;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.autocomplete-item {
padding: 0.75rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-secondary);
}
.autocomplete-item:last-child {
border-bottom: none;
}
.autocomplete-item:hover,
.autocomplete-item.selected {
background: var(--accent-color);
color: white;
}
.autocomplete-item .agent-name {
font-weight: 600;
margin-right: 0.5rem;
}
.autocomplete-item .agent-category {
font-size: 0.7rem;
color: var(--text-secondary);
margin-left: auto;
background: var(--bg-primary);
padding: 0.2rem 0.4rem;
border-radius: 4px;
border: 1px solid var(--border-secondary);
}
.autocomplete-item:hover .agent-category,
.autocomplete-item.selected .agent-category {
color: rgba(255, 255, 255, 0.8);
}
/* Task Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
backdrop-filter: blur(4px);
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 12px;
width: 90%;
max-width: 700px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border-secondary);
}
.modal-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: var(--text-primary);
}
.modal-close {
background: none;
border: none;
font-size: 1.8rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s ease;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: var(--bg-secondary);
color: var(--accent-color);
}
.modal-body {
padding: 1.5rem;
max-height: 60vh;
overflow-y: auto;
}
.task-info-row {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
}
.info-label {
font-weight: 600;
color: var(--text-secondary);
min-width: 80px;
}
.logs-section {
margin-top: 2rem;
}
.logs-section h4 {
margin: 0 0 1rem 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.logs-container {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1rem;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.5;
}
.log-entry {
margin-bottom: 0.5rem;
color: var(--text-primary);
word-wrap: break-word;
}
.log-entry:last-child {
margin-bottom: 0;
}
</style>
</head>
<body class="sandbox-interface">
<div class="container">
<header class="sandbox-header">
<div class="terminal-header">
<div class="ascii-title">
<pre class="ascii-art">
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
██║ ██║ ███████║██║ ██║██║ ██║█████╗ ██║ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ██║ ██║ ██║██║ ██║██╔══╝ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗ ╚██████╗╚██████╔╝██████╔╝███████╗ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ </pre>
</div>
<div class="header-nav">
<div class="nav-links">
<a href="https://www.anthropic.com/claude-code?ref=aitmpl.com" target="_blank" class="nav-link">
Claude Code
</a>
<span class="nav-separator">|</span>
<a href="https://aitmpl.com" target="_blank" class="nav-link">
Templates
</a>
<span class="nav-separator">|</span>
<a href="https://github.com/davila7/claude-code-templates" target="_blank" class="nav-link">
GitHub
</a>
</div>
</div>
</div>
</header>
<section class="task-input-section">
<div class="task-input-container">
<!-- Main Title -->
<div style="text-align: center; margin-bottom: 3rem;">
<h1 style="margin: 0; font-size: 2.5rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.02em;">
What are we coding next?
</h1>
</div>
<!-- Main Input Container -->
<div class="main-input-container">
<div class="autocomplete-container">
<textarea
id="taskInput"
class="task-textarea"
placeholder="Describe what you want to build..."></textarea>
<div id="autocompleteDropdown" class="autocomplete-dropdown">
<!-- Agent suggestions will be populated here -->
</div>
</div>
<!-- Bottom Controls -->
<div class="bottom-controls">
<!-- Left: Execution Mode Toggle -->
<div class="bottom-left">
<div class="compact-mode-selector">
<div class="mode-toggle-group">
<div class="toggle-labels-compact">
<span id="localLabel" class="toggle-label-active">Local</span>
</div>
<label class="compact-toggle-switch">
<input type="checkbox" id="executionModeToggle" onchange="toggleExecutionMode()">
<span class="compact-toggle-slider"></span>
</label>
<div class="toggle-labels-compact">
<span id="cloudLabel">Cloud</span>
</div>
</div>
<div class="mode-info-icon">
<span>i</span>
<div class="mode-info-tooltip">
Switch between Local (Claude Code CLI) and Cloud (E2B Sandbox) execution
</div>
</div>
</div>
</div>
<!-- Right: Code Button -->
<div class="bottom-right">
<button id="codeBtn" class="code-btn" onclick="executeTask()">
Code
</button>
</div>
</div>
</div>
</div>
</section>
<section class="tasks-section">
<div class="tasks-container">
<!-- Tasks Tabs -->
<div class="tasks-tabs">
<button class="tab-btn active" id="tasksTab" onclick="switchTab('tasks')">
Tasks
<span class="tab-count" id="tasksCount">0</span>
</button>
<button class="tab-btn" id="archiveTab" onclick="switchTab('archive')">
Archive
<span class="tab-count" id="archiveCount">0</span>
</button>
</div>
<!-- Tasks Content -->
<div class="tab-content active" id="tasksContent">
<div class="task-list" id="runningTasks">
<div class="empty-state">
<div class="empty-icon">⚡</div>
<p>No active tasks</p>
</div>
</div>
</div>
<!-- Archive Content -->
<div class="tab-content" id="archiveContent">
<div class="task-list" id="completedTasks">
<div class="empty-state">
<div class="empty-icon">📁</div>
<p>No archived tasks</p>
</div>
</div>
</div>
</div>
</section>
<!-- Task Modal -->
<div id="taskModal" class="modal" onclick="closeTaskModal(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h3 id="modalTaskTitle">Task Details</h3>
<button class="modal-close" onclick="closeTaskModal()">×</button>
</div>
<div class="modal-body">
<div class="task-info-row">
<span class="info-label">Status:</span>
<span id="modalTaskStatus" class="task-status">running</span>
</div>
<div class="task-info-row">
<span class="info-label">Agent:</span>
<span id="modalTaskAgent">frontend-developer</span>
</div>
<div class="task-info-row">
<span class="info-label">Mode:</span>
<span id="modalTaskMode">local</span>
</div>
<div class="task-info-row">
<span class="info-label">Started:</span>
<span id="modalTaskTime">--</span>
</div>
<div class="logs-section">
<h4>Execution Logs</h4>
<div class="logs-container" id="modalTaskLogs">
<div class="log-entry">Initializing task...</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Floating status indicator -->
<div class="floating-status" id="floatingStatus">
<div>Status: <span id="statusText">Ready</span></div>
</div>
<script>
let tasks = [];
let taskIdCounter = 1;
let executionMode = 'local'; // Default to local mode
let agents = []; // Will be populated from components.json
let selectedAgentIndex = -1;
function generateTaskId() {
return `task-${Date.now()}-${taskIdCounter++}`;
}
function toggleExecutionMode() {
const toggle = document.getElementById('executionModeToggle');
const localLabel = document.getElementById('localLabel');
const cloudLabel = document.getElementById('cloudLabel');
executionMode = toggle.checked ? 'cloud' : 'local';
// Update label highlighting
if (executionMode === 'cloud') {
localLabel.classList.remove('toggle-label-active');
cloudLabel.classList.add('toggle-label-active');
} else {
localLabel.classList.add('toggle-label-active');
cloudLabel.classList.remove('toggle-label-active');
}
// Execution mode changed - no additional UI updates needed
}
function extractAgentFromPrompt(prompt) {
// Look for @agent-name pattern at the start or after whitespace
const agentMatch = prompt.match(/(?:^|\s)@([a-zA-Z0-9-_]+)/);
if (agentMatch) {
const shortName = agentMatch[1];
const cleanPrompt = prompt.replace(agentMatch[0], '').trim();
// Find the full agent path from the agents list
const agent = agents.find(a => a.name === shortName);
const fullAgentPath = agent ? `${agent.category}/${agent.name}` : shortName;
return { agentName: fullAgentPath, cleanPrompt };
}
return { agentName: null, cleanPrompt: prompt };
}
async function executeTask() {
const taskInput = document.getElementById('taskInput');
const prompt = taskInput.value.trim();
const codeBtn = document.getElementById('codeBtn');
if (!prompt) {
alert('Please describe what you want to create');
return;
}
if (prompt.length < 10) {
alert('Please provide a more detailed description (at least 10 characters)');
return;
}
// Extract agent from prompt if specified
const { agentName, cleanPrompt } = extractAgentFromPrompt(prompt);
// Disable button during execution
codeBtn.disabled = true;
codeBtn.textContent = 'Starting...';
try {
// Send task to server with execution mode and selected agent
const response = await fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: cleanPrompt,
mode: executionMode,
agent: agentName || 'development-team/frontend-developer'
})
});
const result = await response.json();
if (result.success) {
// Clear input and re-enable button
taskInput.value = '';
codeBtn.disabled = false;
codeBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="margin-right: 0.5rem;">
<path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z"/>
</svg>
Code
`;
// Start polling for task updates
startTaskPolling(result.taskId);
} else {
alert('Failed to start task: ' + result.error);
codeBtn.disabled = false;
codeBtn.textContent = 'Code';
}
} catch (error) {
alert('Error starting task: ' + error.message);
codeBtn.disabled = false;
codeBtn.textContent = 'Code';
}
}
function startTaskPolling(taskId) {
// Add task to local tasks array for immediate display
const newTask = {
id: taskId,
title: 'Loading...',
status: 'running',
progress: 0,
output: 'Initializing task...',
startTime: new Date(),
sandboxId: 'pending'
};
tasks.unshift(newTask);
updateTaskLists();
// Start polling for updates
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/task/${taskId}`);
const result = await response.json();
if (result.success) {
const taskIndex = tasks.findIndex(t => t.id === taskId);
if (taskIndex !== -1) {
tasks[taskIndex] = {
id: result.task.id,
title: result.task.title,
status: result.task.status,
progress: result.task.progress,
output: result.task.output,
startTime: new Date(result.task.startTime),
endTime: result.task.endTime ? new Date(result.task.endTime) : null,
sandboxId: result.task.sandboxId || 'pending'
};
updateTaskLists();
// Stop polling if task is completed or failed
if (result.task.status === 'completed' || result.task.status === 'failed') {
clearInterval(pollInterval);
}
}
}
} catch (error) {
console.error('Polling error:', error);
}
}, 2000); // Poll every 2 seconds
}
async function loadAllTasks() {
try {
const response = await fetch('/api/tasks');
const result = await response.json();
if (result.success) {
tasks = result.tasks.map(task => ({
id: task.id,
title: task.title,
status: task.status,
progress: task.progress,
output: task.output || '',
startTime: new Date(task.startTime),
endTime: task.endTime ? new Date(task.endTime) : null,
sandboxId: task.sandboxId || 'unknown'
}));
updateTaskLists();
}
} catch (error) {
console.error('Failed to load tasks:', error);
}
}
// Tab functionality
let currentTab = 'tasks';
function switchTab(tabName) {
const tasksTab = document.getElementById('tasksTab');
const archiveTab = document.getElementById('archiveTab');
const tasksContent = document.getElementById('tasksContent');
const archiveContent = document.getElementById('archiveContent');
// Update tab buttons
tasksTab.classList.toggle('active', tabName === 'tasks');
archiveTab.classList.toggle('active', tabName === 'archive');
// Update content visibility
tasksContent.classList.toggle('active', tabName === 'tasks');
archiveContent.classList.toggle('active', tabName === 'archive');
currentTab = tabName;
}
function updateTaskLists() {
const runningTasks = tasks.filter(t => t.status === 'running');
const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'failed');
updateTaskSection('runningTasks', 'tasksCount', runningTasks);
updateTaskSection('completedTasks', 'archiveCount', completedTasks);
}
function updateTaskSection(containerId, countId, taskList) {
const container = document.getElementById(containerId);
const countElement = document.getElementById(countId);
countElement.textContent = taskList.length;
if (taskList.length === 0) {
const emptyIcon = containerId === 'runningTasks' ? '⚡' : '📁';
const emptyText = containerId === 'runningTasks' ? 'No running tasks' : 'No completed tasks';
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">${emptyIcon}</div>
<p>${emptyText}</p>
</div>
`;
return;
}
container.innerHTML = taskList.map(task => createTaskHTML(task)).join('');
}
// Modal functionality
function openTaskModal(taskId) {
const task = tasks.find(t => t.id === taskId);
if (!task) return;
// Update modal content
document.getElementById('modalTaskTitle').textContent = task.title;
document.getElementById('modalTaskStatus').textContent = task.status;
document.getElementById('modalTaskStatus').className = `task-status status-${task.status}`;
document.getElementById('modalTaskAgent').textContent = task.agent || 'default';
document.getElementById('modalTaskMode').textContent = task.mode || 'local';
document.getElementById('modalTaskTime').textContent = task.startTime.toLocaleString();
// Update logs
const logsContainer = document.getElementById('modalTaskLogs');
if (task.output) {
if (Array.isArray(task.output)) {
// If output is an array, map each line
logsContainer.innerHTML = task.output.map(line =>
`<div class="log-entry">${line}</div>`
).join('');
} else if (typeof task.output === 'string') {
// If output is a string, split by lines and handle \n literals
const cleanOutput = task.output.replace(/\\n/g, '\n'); // Convert literal \n to actual newlines
const lines = cleanOutput.split('\n').filter(line => line.trim());
logsContainer.innerHTML = lines.map(line =>
`<div class="log-entry">${line}</div>`
).join('');
} else {
// If output is something else, convert to string and handle \n
const cleanOutput = String(task.output).replace(/\\n/g, '\n');
const lines = cleanOutput.split('\n').filter(line => line.trim());
logsContainer.innerHTML = lines.map(line =>
`<div class="log-entry">${line}</div>`
).join('');
}
} else {
logsContainer.innerHTML = '<div class="log-entry">No logs available</div>';
}
// Show modal
document.getElementById('taskModal').classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeTaskModal(event) {
if (event && event.target !== event.currentTarget) return;
document.getElementById('taskModal').classList.remove('active');
document.body.style.overflow = 'auto';
}
function createTaskHTML(task) {
const duration = task.endTime ?
Math.round((task.endTime - task.startTime) / 1000) :
Math.round((new Date() - task.startTime) / 1000);
// Create title with agent prefix and truncate
let displayTitle = task.title;
if (task.agent && task.agent !== 'development-team/frontend-developer') {
const agentName = task.agent.split('/').pop(); // Get agent name without category
displayTitle = `@${agentName} ${task.title}`;
}
// Truncate title to max 60 characters
const truncatedTitle = displayTitle.length > 60 ?
displayTitle.substring(0, 57) + '...' : displayTitle;
// Create agent label if agent is specified
const agentLabel = (task.agent && task.agent !== 'development-team/frontend-developer') ?
`<span class="task-agent-label">@${task.agent.split('/').pop()}</span>` : '';
return `
<div class="task-item" onclick="openTaskModal('${task.id}')">
<div class="task-header">
<div class="task-info">
<div class="task-title">${truncatedTitle} ${agentLabel}</div>
<div class="task-meta">
<span>Mode: ${task.mode || 'local'}</span>
<span>Duration: ${duration}s</span>
<span>Started: ${task.startTime.toLocaleTimeString()}</span>
</div>
<div class="task-status status-${task.status}">${task.status}</div>
</div>
</div>
${task.status === 'running' ? `
<div class="task-progress">
<div class="progress-bar" style="width: ${task.progress}%"></div>
</div>
` : ''}
</div>
`;
}
function truncateText(text, maxLength) {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}
// Agent Autocomplete Functionality
async function loadAgents() {
try {
const response = await fetch('/components.json');
const data = await response.json();
agents = data.agents || [];
} catch (error) {
console.warn('Could not load agents list:', error);
agents = [];
}
}
function getCaretPosition(textarea) {
return textarea.selectionStart;
}
function setCaretPosition(textarea, pos) {
textarea.setSelectionRange(pos, pos);
}
function findAtSymbolPosition(text, caretPos) {
// Find the last @ symbol before the caret position
for (let i = caretPos - 1; i >= 0; i--) {
if (text[i] === '@') {
// Check if this @ is at the start or preceded by whitespace
if (i === 0 || /\s/.test(text[i - 1])) {
return i;
}
}
// If we hit whitespace before finding @, stop searching
if (/\s/.test(text[i])) {
break;
}
}
return -1;
}
function extractAgentQuery(text, atPos, caretPos) {
// Extract the text between @ and caret position
return text.substring(atPos + 1, caretPos);
}
function filterAgents(query) {
if (!query) return agents.slice(0, 8); // Show first 8 agents if no query
const lowerQuery = query.toLowerCase();
return agents.filter(agent =>
agent.name.toLowerCase().includes(lowerQuery) ||
agent.category.toLowerCase().includes(lowerQuery)
).slice(0, 8); // Limit to 8 results
}
function showAutocomplete(filteredAgents, query) {
const dropdown = document.getElementById('autocompleteDropdown');
if (filteredAgents.length === 0) {
dropdown.style.display = 'none';
return;
}
dropdown.innerHTML = filteredAgents.map((agent, index) => `
<div class="autocomplete-item" data-index="${index}" onclick="selectAgent('${agent.name}')">
<span class="agent-name">@${agent.name}</span>
<span class="agent-category">[${agent.category}]</span>
</div>
`).join('');
dropdown.style.display = 'block';
selectedAgentIndex = -1; // Reset selection
}
function hideAutocomplete() {
const dropdown = document.getElementById('autocompleteDropdown');
dropdown.style.display = 'none';
selectedAgentIndex = -1;
}
function selectAgent(agentName) {
const textarea = document.getElementById('taskInput');
const text = textarea.value;
const caretPos = getCaretPosition(textarea);
const atPos = findAtSymbolPosition(text, caretPos);
if (atPos !== -1) {
// Replace the @query with the selected agent name
const beforeAt = text.substring(0, atPos);
const afterCaret = text.substring(caretPos);
const newText = beforeAt + '@' + agentName + ' ' + afterCaret;
textarea.value = newText;
// Position caret after the inserted agent name
const newCaretPos = atPos + agentName.length + 2; // +2 for @ and space
setCaretPosition(textarea, newCaretPos);
textarea.focus();
}
hideAutocomplete();
}
function handleAutocomplete() {
const textarea = document.getElementById('taskInput');
const text = textarea.value;
const caretPos = getCaretPosition(textarea);
const atPos = findAtSymbolPosition(text, caretPos);
if (atPos !== -1) {
const query = extractAgentQuery(text, atPos, caretPos);
const filteredAgents = filterAgents(query);
showAutocomplete(filteredAgents, query);
} else {
hideAutocomplete();
}
}
function handleKeyNavigation(event) {
const dropdown = document.getElementById('autocompleteDropdown');
if (dropdown.style.display !== 'block') return;
const items = dropdown.querySelectorAll('.autocomplete-item');
if (event.key === 'ArrowDown') {
event.preventDefault();
selectedAgentIndex = Math.min(selectedAgentIndex + 1, items.length - 1);
updateSelection(items);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selectedAgentIndex = Math.max(selectedAgentIndex - 1, -1);
updateSelection(items);
} else if (event.key === 'Enter' && selectedAgentIndex >= 0) {
event.preventDefault();
const selectedItem = items[selectedAgentIndex];
const agentName = selectedItem.querySelector('.agent-name').textContent.slice(1); // Remove @
selectAgent(agentName);
} else if (event.key === 'Escape') {
event.preventDefault();
hideAutocomplete();
}
}
function updateSelection(items) {
items.forEach((item, index) => {
if (index === selectedAgentIndex) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
// Initialize the interface
document.addEventListener('DOMContentLoaded', function() {
// Load existing tasks from server
loadAllTasks();
// Load agents for autocomplete
loadAgents();
// Auto-resize textarea and handle autocomplete
const textarea = document.getElementById('taskInput');
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.max(200, this.scrollHeight) + 'px';
// Handle agent autocomplete
handleAutocomplete();
});
// Handle keyboard navigation for autocomplete
textarea.addEventListener('keydown', handleKeyNavigation);
// Hide autocomplete when clicking outside
document.addEventListener('click', function(event) {
if (!event.target.closest('.autocomplete-container')) {
hideAutocomplete();
}
});
// Periodically refresh tasks
setInterval(loadAllTasks, 10000); // Refresh every 10 seconds
});
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
if ((event.ctrlKey ||