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
This commit is contained in:
2026-02-15 02:58:11 -06:00
parent 9f9f335439
commit e9689d6ea8
12 changed files with 1698 additions and 31 deletions

View File

@@ -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<FileCategory, { label: string; icon: string; color: string; order: number }> = {
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<Agent[]>([])
const selectedAgentId = ref<string | null>(null)
const openFile = ref<OpenFile | null>(null)
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
const collapsedCategories = ref<Set<string>>(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<FileCategory, AgentFile[]>()
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
}
})