From e9689d6ea829ba3fd2f35e03381fec1830a80ce1 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sun, 15 Feb 2026 02:58:11 -0600 Subject: [PATCH] feat: Add AgentBar arc dock with per-agent terminal frame and voice modal - Add ui.json configs for Main (purple) and Ejecutor (red) agents - AgentBar: fused arc-shaped dock at bottom with dynamic glow - Quick press opens styled terminal frame mockup - Hold opens voice modal with Web Speech API streaming transcription - Responsive: full-width mobile, max-width on tablet/desktop/4K - Agents API: serve uiConfig from ui.json in agent directories - Agents page: route, store, toolbar integration --- .../plugins/known_marketplaces.json | 2 +- .claude-ejecutor/ui.json | 9 + .claude/settings.local.json | 29 +- .claude/ui.json | 9 + frontend/src/components/AgentBar.vue | 720 ++++++++++++++++++ frontend/src/pages/AgentsPage.vue | 488 ++++++++++++ frontend/src/router/index.ts | 5 + frontend/src/services/toolRegistry.ts | 5 +- .../services/tools/handlers/globalHandlers.ts | 8 +- frontend/src/stores/agents.ts | 204 +++++ server/routes/agents.ts | 239 ++++++ server/routes/index.ts | 11 + 12 files changed, 1698 insertions(+), 31 deletions(-) create mode 100644 .claude-ejecutor/ui.json create mode 100644 .claude/ui.json create mode 100644 frontend/src/components/AgentBar.vue create mode 100644 frontend/src/pages/AgentsPage.vue create mode 100644 frontend/src/stores/agents.ts create mode 100644 server/routes/agents.ts diff --git a/.claude-ejecutor/plugins/known_marketplaces.json b/.claude-ejecutor/plugins/known_marketplaces.json index ab41327..4c9e1c2 100644 --- a/.claude-ejecutor/plugins/known_marketplaces.json +++ b/.claude-ejecutor/plugins/known_marketplaces.json @@ -5,6 +5,6 @@ "repo": "anthropics/claude-plugins-official" }, "installLocation": "C:\\Users\\jodar\\agent-ui\\.claude-ejecutor\\plugins\\marketplaces\\claude-plugins-official", - "lastUpdated": "2026-02-15T04:49:09.493Z" + "lastUpdated": "2026-02-15T08:27:07.485Z" } } \ No newline at end of file diff --git a/.claude-ejecutor/ui.json b/.claude-ejecutor/ui.json new file mode 100644 index 0000000..cfbd4e6 --- /dev/null +++ b/.claude-ejecutor/ui.json @@ -0,0 +1,9 @@ +{ + "label": "Ejecutor", + "shortLabel": "EJ", + "color": "#ef4444", + "gradient": "linear-gradient(135deg, #ef4444, #dc2626)", + "terminalBg": "#0a0f1a", + "terminalBorder": "#ef4444", + "enabled": true +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5132bab..13f8dd5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -79,7 +79,10 @@ "mcp__agent-ui__z590_nucleoriofrio_com-save_vue_component", "mcp__agent-ui__z590_nucleoriofrio_com-resize_window", "mcp__agent-ui__z590_nucleoriofrio_com-save_canvas_snapshot", - "mcp__agent-ui__z590_nucleoriofrio_com-load_canvas_snapshot" + "mcp__agent-ui__z590_nucleoriofrio_com-load_canvas_snapshot", + "mcp__agent-ui__z590_nucleoriofrio_com-list_canvas_snapshots", + "mcp__agent-ui__z590_nucleoriofrio_com-list_canvases", + "mcp__agent-ui__z590_nucleoriofrio_com-list_vue_components" ] }, "enableAllProjectMcpServers": true, @@ -153,30 +156,6 @@ ] } ], - "SubagentStart": [ - { - "matcher": ".*", - "hooks": [ - { - "type": "command", - "command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"subagentStart\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"", - "timeout": 5000 - } - ] - } - ], - "SubagentStop": [ - { - "matcher": ".*", - "hooks": [ - { - "type": "command", - "command": "powershell -NoProfile -Command \"try { Invoke-RestMethod -Uri 'http://localhost:4101/api/claude-status' -Method POST -Body '{\\\"status\\\":\\\"subagentStop\\\"}' -ContentType 'application/json' -TimeoutSec 2 | Out-Null } catch {}\"", - "timeout": 5000 - } - ] - } - ], "PermissionRequest": [ { "matcher": ".*", diff --git a/.claude/ui.json b/.claude/ui.json new file mode 100644 index 0000000..6749019 --- /dev/null +++ b/.claude/ui.json @@ -0,0 +1,9 @@ +{ + "label": "Main", + "shortLabel": "M", + "color": "#6366f1", + "gradient": "linear-gradient(135deg, #6366f1, #8b5cf6)", + "terminalBg": "#0f0a1a", + "terminalBorder": "#6366f1", + "enabled": true +} diff --git a/frontend/src/components/AgentBar.vue b/frontend/src/components/AgentBar.vue new file mode 100644 index 0000000..c43bbe6 --- /dev/null +++ b/frontend/src/components/AgentBar.vue @@ -0,0 +1,720 @@ + + + + + diff --git a/frontend/src/pages/AgentsPage.vue b/frontend/src/pages/AgentsPage.vue new file mode 100644 index 0000000..a7d14ad --- /dev/null +++ b/frontend/src/pages/AgentsPage.vue @@ -0,0 +1,488 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index d633fbe..61df295 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -53,6 +53,11 @@ const router = createRouter({ path: '/git', name: 'git', component: () => import('../pages/GitPage.vue') + }, + { + path: '/agents', + name: 'agents', + component: () => import('../pages/AgentsPage.vue') } ] }) diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 5affc13..6531807 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -32,7 +32,7 @@ import { setRouter } from './tools/handlers/globalHandlers' import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers' import { ALL_TOOL_METAS, getAllToolNames, type ToolCategory } from './tools/toolDefinitions' -export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git' +export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git' | 'agents' // Internal webmcp functions (not exported for external use) let webmcpInstance: any = null @@ -164,7 +164,8 @@ const pageCategories: Record = { source: ['global', 'torch', 'source', 'terminal'], terminal: ['global', 'torch', 'terminal'], tools: ['global', 'torch', 'terminal'], - git: ['global', 'torch', 'git', 'terminal'] + git: ['global', 'torch', 'git', 'terminal'], + agents: ['global', 'torch', 'terminal'] } let currentPage: PageName | null = null diff --git a/frontend/src/services/tools/handlers/globalHandlers.ts b/frontend/src/services/tools/handlers/globalHandlers.ts index 2418ba1..faee5b8 100644 --- a/frontend/src/services/tools/handlers/globalHandlers.ts +++ b/frontend/src/services/tools/handlers/globalHandlers.ts @@ -41,7 +41,8 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo database: 'Database - Explorador de base de datos', source: 'Source - Navegador de codigo fuente', terminal: 'Terminal - Consola de comandos', - tools: 'Tools - Gestion de herramientas MCP' + tools: 'Tools - Gestion de herramientas MCP', + agents: 'Agents - Editor de configuracion de agentes Claude Code' } const pageName = route.name as string || 'unknown' @@ -62,7 +63,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo properties: { page: { type: 'string', - enum: ['home', 'canvas', 'components', 'themes', 'database', 'source', 'terminal', 'tools'], + enum: ['home', 'canvas', 'components', 'themes', 'database', 'source', 'terminal', 'tools', 'agents'], description: 'Pagina a la que navegar' } }, @@ -81,7 +82,8 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo database: '/database', source: '/source', terminal: '/terminal', - tools: '/tools' + tools: '/tools', + agents: '/agents' } const path = routes[args.page] diff --git a/frontend/src/stores/agents.ts b/frontend/src/stores/agents.ts new file mode 100644 index 0000000..ed9962c --- /dev/null +++ b/frontend/src/stores/agents.ts @@ -0,0 +1,204 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +type FileCategory = 'config' | 'instructions' | 'plugins' | 'history' | 'debug' | 'cache' | 'sessions' | 'backups' | 'other' + +interface AgentFile { + name: string + path: string + type: 'json' | 'markdown' | 'text' | 'jsonl' + category: FileCategory + size: number +} + +interface Agent { + id: string + name: string + directory: string + files: AgentFile[] +} + +interface OpenFile { + path: string + name: string + type: string + content: string + originalContent: string + agentId: string + category: FileCategory +} + +export interface CategoryInfo { + key: FileCategory + label: string + icon: string // SVG path + color: string + files: AgentFile[] +} + +const CATEGORY_META: Record = { + config: { label: 'Configuration', icon: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z|M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0-6 0', color: '#6366f1', order: 0 }, + instructions: { label: 'Instructions', icon: 'M4 19.5A2.5 2.5 0 0 1 6.5 17H20|M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z', color: '#3b82f6', order: 1 }, + plugins: { label: 'Plugins', icon: 'M12 2L2 7l10 5 10-5-10-5z|M2 17l10 5 10-5|M2 12l10 5 10-5', color: '#8b5cf6', order: 2 }, + history: { label: 'History', icon: 'M12 8v4l3 3|M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z', color: '#f59e0b', order: 3 }, + debug: { label: 'Debug logs', icon: 'M12 12m-1 0a1 1 0 1 0 2 0 1 1 0 0 0-2 0|M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z', color: '#ef4444', order: 4 }, + cache: { label: 'Cache', icon: 'M22 12H2|M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z', color: '#06b6d4', order: 5 }, + sessions: { label: 'Session data', icon: 'M4 17l6-6 4 4 6-6|M4 17h16', color: '#10b981', order: 6 }, + backups: { label: 'Backups', icon: 'M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8|M21 3v5h-5', color: '#9ca3af', order: 7 }, + other: { label: 'Other files', icon: 'M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z|M14 2v6h6', color: '#78716c', order: 8 } +} + +export { CATEGORY_META } +export type { FileCategory, AgentFile, Agent } + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` +} + +export { formatSize } + +export const useAgentsStore = defineStore('agents', () => { + const agents = ref([]) + const selectedAgentId = ref(null) + const openFile = ref(null) + const loading = ref(false) + const saving = ref(false) + const error = ref(null) + const collapsedCategories = ref>(new Set()) + + const isDirty = computed(() => { + if (!openFile.value) return false + return openFile.value.content !== openFile.value.originalContent + }) + + const selectedAgent = computed(() => { + if (!selectedAgentId.value) return null + return agents.value.find(a => a.id === selectedAgentId.value) || null + }) + + const groupedFiles = computed((): CategoryInfo[] => { + const agent = selectedAgent.value + if (!agent) return [] + + const groups = new Map() + for (const file of agent.files) { + const cat = file.category + if (!groups.has(cat)) groups.set(cat, []) + groups.get(cat)!.push(file) + } + + return Array.from(groups.entries()) + .map(([key, files]) => ({ + key, + label: CATEGORY_META[key].label, + icon: CATEGORY_META[key].icon, + color: CATEGORY_META[key].color, + files + })) + .sort((a, b) => CATEGORY_META[a.key].order - CATEGORY_META[b.key].order) + }) + + async function fetchAgents() { + loading.value = true + error.value = null + try { + const res = await fetch('/api/agents') + if (!res.ok) throw new Error('Failed to fetch agents') + agents.value = await res.json() + if (agents.value.length && !selectedAgentId.value) { + selectedAgentId.value = agents.value[0].id + } + } catch (e: any) { + error.value = e.message + } finally { + loading.value = false + } + } + + function selectAgent(id: string) { + selectedAgentId.value = id + openFile.value = null + } + + async function loadFile(agentId: string, file: AgentFile) { + error.value = null + try { + const res = await fetch(`/api/agents/file?path=${encodeURIComponent(file.path)}`) + if (!res.ok) { + const data = await res.json() + throw new Error(data.error || 'Failed to load file') + } + const data = await res.json() + openFile.value = { + path: file.path, + name: file.name, + type: file.type, + content: data.content, + originalContent: data.content, + agentId, + category: file.category + } + } catch (e: any) { + error.value = e.message + } + } + + async function saveFile() { + if (!openFile.value || !isDirty.value) return + saving.value = true + error.value = null + try { + const res = await fetch('/api/agents/file', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + path: openFile.value.path, + content: openFile.value.content + }) + }) + if (!res.ok) { + const data = await res.json() + throw new Error(data.error || 'Failed to save file') + } + openFile.value.originalContent = openFile.value.content + } catch (e: any) { + error.value = e.message + } finally { + saving.value = false + } + } + + function revertFile() { + if (!openFile.value) return + openFile.value.content = openFile.value.originalContent + } + + function toggleCategory(key: string) { + if (collapsedCategories.value.has(key)) { + collapsedCategories.value.delete(key) + } else { + collapsedCategories.value.add(key) + } + } + + return { + agents, + selectedAgentId, + selectedAgent, + openFile, + loading, + saving, + error, + collapsedCategories, + isDirty, + groupedFiles, + fetchAgents, + selectAgent, + loadFile, + saveFile, + revertFile, + toggleCategory + } +}) diff --git a/server/routes/agents.ts b/server/routes/agents.ts new file mode 100644 index 0000000..43ebf7a --- /dev/null +++ b/server/routes/agents.ts @@ -0,0 +1,239 @@ +import { jsonResponse, errorResponse } from '../utils/cors' +import { existsSync, readdirSync, readFileSync, statSync } from 'fs' +import { join, resolve, basename } from 'path' + +const PROJECT_ROOT = resolve(import.meta.dir, '../..') + +// Allowed config file patterns (for path validation on read/write) +const ALLOWED_PATTERNS = [ + /^\.claude\/.*$/, + /^\.claude-[^/]+\/.*$/, + /^CLAUDE\.md$/, + /^\.mcp\.json$/ +] + +// Skip .git internals (hundreds of binary/pack files, useless for debugging) +const SKIP_DIRS = ['.git'] + +// Sensitive files blocked from read/write via API +const BLOCKED_FILES = ['.credentials.json'] + +type FileCategory = 'config' | 'instructions' | 'plugins' | 'history' | 'debug' | 'cache' | 'sessions' | 'backups' | 'other' + +interface AgentFile { + name: string + path: string + type: 'json' | 'markdown' | 'text' | 'jsonl' + category: FileCategory + size: number +} + +interface UiConfig { + label: string + shortLabel: string + color: string + gradient: string + terminalBg: string + terminalBorder: string + enabled: boolean +} + +interface Agent { + id: string + name: string + directory: string + files: AgentFile[] + uiConfig: UiConfig | null +} + +function getFileType(filename: string): AgentFile['type'] { + if (filename.endsWith('.jsonl')) return 'jsonl' + if (filename.endsWith('.json')) return 'json' + if (filename.endsWith('.md')) return 'markdown' + return 'text' +} + +function categorizeFile(relPath: string, filename: string): FileCategory { + // Backups first (most specific) + if (filename.includes('.backup')) return 'backups' + + // By directory + if (relPath.includes('/plugins/')) return 'plugins' + if (relPath.includes('/debug/')) return 'debug' + if (relPath.includes('/cache/')) return 'cache' + if (relPath.includes('/session-env/') || relPath.includes('/shell-snapshots/') || relPath.includes('/todos/') || relPath.includes('/projects/')) return 'sessions' + + // By filename + if (filename === 'history.jsonl') return 'history' + if (filename.endsWith('.md')) return 'instructions' + if (filename === 'settings.json' || filename === '.claude.json' || filename === '.mcp.json') return 'config' + if (filename.endsWith('.json')) return 'config' + + return 'other' +} + +function isBlocked(filename: string): boolean { + return BLOCKED_FILES.includes(filename) +} + +function isAllowedPath(relativePath: string): boolean { + const normalized = relativePath.replace(/\\/g, '/') + return ALLOWED_PATTERNS.some(p => p.test(normalized)) +} + +function scanDirectory(dir: string, relBase: string): AgentFile[] { + const files: AgentFile[] = [] + if (!existsSync(dir)) return files + + try { + const entries = readdirSync(dir) + for (const entry of entries) { + const fullPath = join(dir, entry) + const relPath = `${relBase}/${entry}` + + let stat + try { stat = statSync(fullPath) } catch { continue } + + if (isBlocked(entry)) continue + + if (stat.isFile()) { + files.push({ + name: entry, + path: relPath, + type: getFileType(entry), + category: categorizeFile(relPath, entry), + size: stat.size + }) + } else if (stat.isDirectory()) { + if (SKIP_DIRS.includes(entry)) continue + files.push(...scanDirectory(fullPath, relPath)) + } + } + } catch { /* permission errors */ } + + return files +} + +function readUiConfig(agentDir: string): UiConfig | null { + const uiPath = join(agentDir, 'ui.json') + if (!existsSync(uiPath)) return null + try { + const raw = readFileSync(uiPath, 'utf-8') + const data = JSON.parse(raw) + return { + label: data.label || '', + shortLabel: data.shortLabel || '', + color: data.color || '#6366f1', + gradient: data.gradient || '', + terminalBg: data.terminalBg || '#0f0a1a', + terminalBorder: data.terminalBorder || '#6366f1', + enabled: data.enabled !== false + } + } catch { + return null + } +} + +function discoverAgents(): Agent[] { + const agents: Agent[] = [] + + // Main agent (.claude/ + root files) + const claudeDir = join(PROJECT_ROOT, '.claude') + if (existsSync(claudeDir)) { + const mainAgent: Agent = { + id: 'main', + name: 'Claude Code (main)', + directory: '.claude', + files: scanDirectory(claudeDir, '.claude'), + uiConfig: readUiConfig(claudeDir) + } + + // Root CLAUDE.md + const claudeMd = join(PROJECT_ROOT, 'CLAUDE.md') + if (existsSync(claudeMd)) { + const stat = statSync(claudeMd) + mainAgent.files.unshift({ + name: 'CLAUDE.md', path: 'CLAUDE.md', type: 'markdown', + category: 'instructions', size: stat.size + }) + } + + // Root .mcp.json + const mcpJson = join(PROJECT_ROOT, '.mcp.json') + if (existsSync(mcpJson)) { + const stat = statSync(mcpJson) + mainAgent.files.push({ + name: '.mcp.json', path: '.mcp.json', type: 'json', + category: 'config', size: stat.size + }) + } + + agents.push(mainAgent) + } + + // Other agents (.claude-*/) + try { + const rootEntries = readdirSync(PROJECT_ROOT) + for (const entry of rootEntries) { + if (!entry.startsWith('.claude-')) continue + const fullPath = join(PROJECT_ROOT, entry) + try { if (!statSync(fullPath).isDirectory()) continue } catch { continue } + + const agentId = entry.replace('.claude-', '') + agents.push({ + id: agentId, + name: agentId.charAt(0).toUpperCase() + agentId.slice(1), + directory: entry, + files: scanDirectory(fullPath, entry), + uiConfig: readUiConfig(fullPath) + }) + } + } catch { /* ignore */ } + + return agents +} + +export async function handleAgents(req: Request): Promise { + if (req.method !== 'GET') return null + return jsonResponse(discoverAgents()) +} + +export async function handleAgentsFile(req: Request, url: URL): Promise { + const filePath = url.searchParams.get('path') + if (!filePath) return errorResponse('Missing "path" query parameter') + + const normalized = filePath.replace(/\\/g, '/') + if (!isAllowedPath(normalized)) return errorResponse('Access denied: path not allowed', 403) + if (isBlocked(basename(normalized))) return errorResponse('Access denied: sensitive file', 403) + + const absolutePath = resolve(PROJECT_ROOT, normalized) + if (!absolutePath.startsWith(PROJECT_ROOT)) return errorResponse('Access denied: path traversal', 403) + + if (req.method === 'GET') { + if (!existsSync(absolutePath)) return errorResponse('File not found', 404) + try { + const content = await Bun.file(absolutePath).text() + return jsonResponse({ path: normalized, content }) + } catch (e: any) { + return errorResponse(`Failed to read file: ${e.message}`, 500) + } + } + + if (req.method === 'POST') { + try { + const body = await req.json() + if (typeof body.content !== 'string') return errorResponse('Missing "content" field') + + if (normalized.endsWith('.json')) { + try { JSON.parse(body.content) } catch { return errorResponse('Invalid JSON content') } + } + + await Bun.write(absolutePath, body.content) + return jsonResponse({ success: true, path: normalized }) + } catch (e: any) { + return errorResponse(`Failed to write file: ${e.message}`, 500) + } + } + + return null +} diff --git a/server/routes/index.ts b/server/routes/index.ts index d2d30a4..5ab95f3 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -12,6 +12,7 @@ import { handleRecordingsRoutes } from './recordings' import { handleClaudeStatus } from './claude-status' import { handleSnapshots, handleSnapshotById } from './snapshots' import { handleGitStatus, handleGitDiff, handleGitLog, handleGitLogCommit, handleGitCompare, handleGitBranches, handleGitCurrentBranch, handleGitTree, handleGitFile } from './git' +import { handleAgents, handleAgentsFile } from './agents' export async function handleRequest(req: Request): Promise { const url = new URL(req.url) @@ -247,5 +248,15 @@ export async function handleRequest(req: Request): Promise { return handleGitFile(url) } + // Agents + if (path === '/api/agents' && req.method === 'GET') { + return handleAgents(req) + } + + if (path === '/api/agents/file') { + const res = await handleAgentsFile(req, url) + if (res) return res + } + return notFoundResponse() }