feat: Introduce Nucleo as the main AI agent identity
- Add Nucleo atom logo with animated orbiting electrons - Redesign FAB with glassmorphism effect and purple gradient - Add connection indicator (green dot) when terminal is open - Update FloatingTerminal header with Nucleo branding - Add PermissionRequest hook support with red alert animation - Document Nucleo in README with visual states table - Create official Nucleo logo SVG in docs/
This commit is contained in:
44
README.md
44
README.md
@@ -2,6 +2,50 @@
|
|||||||
|
|
||||||
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
|
Dynamic canvas interface for Claude Code interaction via MCP (Model Context Protocol).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nucleo
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/nucleo-logo.svg" alt="Nucleo" width="120"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
**Nucleo** is the main AI agent powering Agent UI. It serves as the bridge between Claude Code and your visual interface, providing real-time status feedback through an animated FAB (Floating Action Button).
|
||||||
|
|
||||||
|
### Visual States
|
||||||
|
|
||||||
|
Nucleo communicates its current state through distinct animations:
|
||||||
|
|
||||||
|
| State | Color | Animation | Trigger |
|
||||||
|
|-------|-------|-----------|---------|
|
||||||
|
| **Idle** | Purple | Rotating atom | Default state |
|
||||||
|
| **Processing** | Orange | Pulsing dots | User prompt submitted |
|
||||||
|
| **Reading** | Cyan | Eye icon + scan | Reading files (Read/Glob/Grep) |
|
||||||
|
| **Writing** | Green | Pencil icon + pulse | Writing files (Edit/Write) |
|
||||||
|
| **Subagent** | Purple | Orbital ring | Task tool spawned |
|
||||||
|
| **Permission** | Red | Alert + shake | Awaiting user permission |
|
||||||
|
| **Session Start** | Green | Ripple waves | Session initialized |
|
||||||
|
| **Notification** | Yellow | Bounce | System notification |
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
|
||||||
|
Nucleo's status is synchronized via Claude Code hooks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"UserPromptSubmit": [{ "hooks": [{ "command": "... status: processing ..." }] }],
|
||||||
|
"PreToolUse": [{ "hooks": [{ "command": "... status: toolUse ..." }] }],
|
||||||
|
"PostToolUse": [{ "hooks": [{ "command": "... status: toolDone ..." }] }],
|
||||||
|
"Stop": [{ "hooks": [{ "command": "... status: idle ..." }] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The FAB receives these status updates via WebSocket and displays the corresponding animation, giving you real-time visibility into what Claude is doing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces.
|
Agent UI provides a visual canvas where Claude Code can render dynamic Vue 3 components, HTML content, and interactive UIs in real-time. It bridges the gap between CLI-based AI assistance and rich visual interfaces.
|
||||||
|
|||||||
61
docs/nucleo-logo.svg
Normal file
61
docs/nucleo-logo.svg
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="coreGradient" cx="50%" cy="30%" r="60%">
|
||||||
|
<stop offset="0%" stop-color="#c4b5fd"/>
|
||||||
|
<stop offset="50%" stop-color="#a78bfa"/>
|
||||||
|
<stop offset="100%" stop-color="#6366f1"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="orbitGradient1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818cf8" stop-opacity="0.9"/>
|
||||||
|
<stop offset="50%" stop-color="#a78bfa" stop-opacity="0.5"/>
|
||||||
|
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="electronGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#e0e7ff"/>
|
||||||
|
<stop offset="100%" stop-color="#a78bfa"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="coreGlow" x="-100%" y="-100%" width="300%" height="300%">
|
||||||
|
<feGaussianBlur stdDeviation="6" result="blur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background circle (subtle) -->
|
||||||
|
<circle cx="60" cy="60" r="55" fill="none" stroke="#4f46e5" stroke-width="1" opacity="0.2"/>
|
||||||
|
|
||||||
|
<!-- Orbital rings -->
|
||||||
|
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(-30 60 60)" opacity="0.7"/>
|
||||||
|
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(30 60 60)" opacity="0.5"/>
|
||||||
|
<ellipse cx="60" cy="60" rx="45" ry="18" stroke="url(#orbitGradient1)" stroke-width="2" fill="none" transform="rotate(90 60 60)" opacity="0.6"/>
|
||||||
|
|
||||||
|
<!-- Core nucleus with glow -->
|
||||||
|
<circle cx="60" cy="60" r="18" fill="url(#coreGradient)" filter="url(#coreGlow)"/>
|
||||||
|
<circle cx="60" cy="60" r="14" fill="url(#coreGradient)" opacity="0.8"/>
|
||||||
|
<circle cx="55" cy="55" r="5" fill="white" opacity="0.4"/>
|
||||||
|
|
||||||
|
<!-- Electrons with glow -->
|
||||||
|
<circle cx="60" cy="15" r="7" fill="url(#electronGradient)" filter="url(#glow)">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="4s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<circle cx="100" cy="70" r="6" fill="url(#electronGradient)" filter="url(#glow)">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="120 60 60" to="480 60 60" dur="5s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<circle cx="20" cy="70" r="6" fill="url(#electronGradient)" filter="url(#glow)">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="240 60 60" to="600 60 60" dur="4.5s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Small accent electrons -->
|
||||||
|
<circle cx="60" cy="105" r="4" fill="#c4b5fd" opacity="0.6">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="180 60 60" to="540 60 60" dur="6s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -25,17 +25,18 @@ const responseRef = ref<InstanceType<typeof FloatingResponse> | null>(null)
|
|||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
// Claude status state (for FAB animations)
|
// Claude status state (for FAB animations)
|
||||||
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'thinking'
|
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
|
||||||
|
|
||||||
const claudeStatus = ref<ClaudeStatus>('idle')
|
const claudeStatus = ref<ClaudeStatus>('idle')
|
||||||
const claudeTool = ref<string | null>(null)
|
const claudeTool = ref<string | null>(null)
|
||||||
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
|
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
|
||||||
const isReading = ref(false) // Reading files
|
const isReading = ref(false) // Reading files
|
||||||
const isWriting = ref(false) // Writing files
|
const isWriting = ref(false) // Writing files
|
||||||
const hasSubagent = ref(false) // Subagent active
|
const hasSubagent = ref(false) // Subagent active
|
||||||
const showSessionStart = ref(false) // Session start animation (3s)
|
const awaitingPermission = ref(false) // Waiting for user permission (highest priority)
|
||||||
const showNotification = ref(false) // Notification pulse (2s)
|
const showSessionStart = ref(false) // Session start animation (3s)
|
||||||
const showToolFlash = ref(false) // Tool use flash (500ms)
|
const showNotification = ref(false) // Notification pulse (2s)
|
||||||
|
const showToolFlash = ref(false) // Tool use flash (500ms)
|
||||||
|
|
||||||
let statusWs: WebSocket | null = null
|
let statusWs: WebSocket | null = null
|
||||||
let statusReconnectTimeout: number | null = null
|
let statusReconnectTimeout: number | null = null
|
||||||
@@ -78,9 +79,14 @@ function connectStatusWs() {
|
|||||||
isProcessing.value = false
|
isProcessing.value = false
|
||||||
isReading.value = false
|
isReading.value = false
|
||||||
isWriting.value = false
|
isWriting.value = false
|
||||||
|
awaitingPermission.value = false
|
||||||
if (processingTimeout) clearTimeout(processingTimeout)
|
if (processingTimeout) clearTimeout(processingTimeout)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'permissionRequest':
|
||||||
|
awaitingPermission.value = true
|
||||||
|
break
|
||||||
|
|
||||||
case 'reading':
|
case 'reading':
|
||||||
isReading.value = true
|
isReading.value = true
|
||||||
triggerToolFlash()
|
triggerToolFlash()
|
||||||
@@ -98,6 +104,7 @@ function connectStatusWs() {
|
|||||||
case 'toolDone':
|
case 'toolDone':
|
||||||
isReading.value = false
|
isReading.value = false
|
||||||
isWriting.value = false
|
isWriting.value = false
|
||||||
|
awaitingPermission.value = false
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'sessionStart':
|
case 'sessionStart':
|
||||||
@@ -278,12 +285,13 @@ watch(() => route.name, (newPage) => {
|
|||||||
reading: isReading,
|
reading: isReading,
|
||||||
writing: isWriting,
|
writing: isWriting,
|
||||||
subagent: hasSubagent,
|
subagent: hasSubagent,
|
||||||
|
permission: awaitingPermission,
|
||||||
'session-start': showSessionStart,
|
'session-start': showSessionStart,
|
||||||
notification: showNotification,
|
notification: showNotification,
|
||||||
'tool-flash': showToolFlash
|
'tool-flash': showToolFlash
|
||||||
}"
|
}"
|
||||||
@click="showTerminal = !showTerminal"
|
@click="showTerminal = !showTerminal"
|
||||||
:title="isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'"
|
:title="awaitingPermission ? `Permiso requerido: ${claudeTool || 'herramienta'}` : isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'"
|
||||||
>
|
>
|
||||||
<!-- Subagent orbital ring -->
|
<!-- Subagent orbital ring -->
|
||||||
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
|
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
|
||||||
@@ -297,8 +305,15 @@ watch(() => route.name, (newPage) => {
|
|||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Permission request icon (alert/hand) - highest priority -->
|
||||||
|
<svg v-if="awaitingPermission" class="permission-icon" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- Processing animation (three dots) -->
|
<!-- Processing animation (three dots) -->
|
||||||
<div v-if="isProcessing && !isReading && !isWriting" class="thinking-dots">
|
<div v-else-if="isProcessing && !isReading && !isWriting" class="thinking-dots">
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -320,14 +335,26 @@ watch(() => route.name, (newPage) => {
|
|||||||
|
|
||||||
<!-- Normal icons -->
|
<!-- Normal icons -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<svg v-if="!showTerminal" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<!-- Terminal closed: Nucleo atom icon -->
|
||||||
<polyline points="4 17 10 11 4 5"></polyline>
|
<svg v-if="!showTerminal" class="nucleo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
<!-- Core nucleus -->
|
||||||
</svg>
|
<circle cx="12" cy="12" r="3.5" fill="white" opacity="0.95"/>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<!-- Orbital rings -->
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.7" transform="rotate(-30 12 12)"/>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.5" transform="rotate(30 12 12)"/>
|
||||||
|
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.6" transform="rotate(90 12 12)"/>
|
||||||
|
<!-- Electrons -->
|
||||||
|
<circle cx="12" cy="4" r="1.8" fill="white" opacity="0.9"/>
|
||||||
|
<circle cx="19" cy="14" r="1.8" fill="white" opacity="0.7"/>
|
||||||
|
<circle cx="5" cy="14" r="1.8" fill="white" opacity="0.8"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<!-- Terminal open: Minimize chevron with connection indicator -->
|
||||||
|
<template v-else>
|
||||||
|
<span class="connection-dot"></span>
|
||||||
|
<svg class="minimize-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -419,35 +446,102 @@ watch(() => route.name, (newPage) => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 56px;
|
width: 58px;
|
||||||
height: 56px;
|
height: 58px;
|
||||||
border-radius: 16px;
|
border-radius: 18px;
|
||||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
background: linear-gradient(145deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
|
box-shadow:
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
0 4px 15px rgba(124, 58, 237, 0.4),
|
||||||
|
0 8px 30px rgba(99, 102, 241, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
z-index: 9998;
|
z-index: 9998;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 21px;
|
||||||
|
background: linear-gradient(145deg, rgba(139, 92, 246, 0.5), rgba(99, 102, 241, 0.2));
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab:hover {
|
.terminal-fab:hover {
|
||||||
transform: scale(1.05);
|
transform: translateY(-3px) scale(1.05);
|
||||||
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.5);
|
box-shadow:
|
||||||
|
0 8px 25px rgba(124, 58, 237, 0.5),
|
||||||
|
0 15px 40px rgba(99, 102, 241, 0.35),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-fab:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nucleo atom icon animation */
|
||||||
|
.nucleo-icon {
|
||||||
|
animation: nucleo-orbit 8s linear infinite;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nucleo-icon ellipse {
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal active - Connected state */
|
||||||
.terminal-fab.active {
|
.terminal-fab.active {
|
||||||
background: #ef4444;
|
background: linear-gradient(145deg, #6366f1 0%, #4f46e5 50%, #7c3aed 100%);
|
||||||
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
transform: rotate(90deg);
|
box-shadow:
|
||||||
|
0 4px 15px rgba(99, 102, 241, 0.4),
|
||||||
|
0 8px 30px rgba(79, 70, 229, 0.3),
|
||||||
|
0 0 20px rgba(16, 185, 129, 0.2),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab.active:hover {
|
.terminal-fab.active:hover {
|
||||||
box-shadow: 0 12px 32px rgba(239, 68, 68, 0.5);
|
transform: translateY(-2px);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 20px rgba(99, 102, 241, 0.5),
|
||||||
|
0 12px 35px rgba(79, 70, 229, 0.35),
|
||||||
|
0 0 25px rgba(16, 185, 129, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection indicator dot */
|
||||||
|
.connection-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #10b981;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
|
||||||
|
animation: connection-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minimize icon */
|
||||||
|
.minimize-icon {
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab.active:hover .minimize-icon {
|
||||||
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Processing state (UserPromptSubmit → Stop) - Orange pulsing */
|
/* Processing state (UserPromptSubmit → Stop) - Orange pulsing */
|
||||||
@@ -492,6 +586,25 @@ watch(() => route.name, (newPage) => {
|
|||||||
animation: notification-bounce 0.5s ease-in-out 4 !important;
|
animation: notification-bounce 0.5s ease-in-out 4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Permission Request - HIGHEST PRIORITY - Red pulsing alert */
|
||||||
|
.terminal-fab.permission {
|
||||||
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important;
|
||||||
|
animation: permission-pulse 1s ease-in-out infinite !important;
|
||||||
|
transform: rotate(0deg) scale(1.1) !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab.permission:hover {
|
||||||
|
transform: scale(1.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Permission icon animation */
|
||||||
|
.permission-icon {
|
||||||
|
animation: permission-shake 0.5s ease-in-out infinite;
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
/* Tool flash - Quick white flash */
|
/* Tool flash - Quick white flash */
|
||||||
.terminal-fab.tool-flash::after {
|
.terminal-fab.tool-flash::after {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -643,6 +756,48 @@ watch(() => route.name, (newPage) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes permission-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7), 0 8px 24px rgba(239, 68, 68, 0.5);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 15px rgba(239, 68, 68, 0), 0 8px 40px rgba(239, 68, 68, 0.8);
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes permission-shake {
|
||||||
|
0%, 100% { transform: rotate(0deg); }
|
||||||
|
25% { transform: rotate(-5deg); }
|
||||||
|
75% { transform: rotate(5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nucleo-orbit {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.9));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes connection-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 12px rgba(16, 185, 129, 1), 0 0 20px rgba(16, 185, 129, 0.4);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Voice FAB */
|
/* Voice FAB */
|
||||||
.voice-fab {
|
.voice-fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -681,13 +836,20 @@ watch(() => route.name, (newPage) => {
|
|||||||
.terminal-fab {
|
.terminal-fab {
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
width: 52px;
|
width: 54px;
|
||||||
height: 52px;
|
height: 54px;
|
||||||
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-fab.active:not(.processing):not(.reading):not(.writing) {
|
.terminal-fab::before {
|
||||||
opacity: 0;
|
border-radius: 19px;
|
||||||
pointer-events: none;
|
}
|
||||||
|
|
||||||
|
.connection-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.orbital-ring {
|
.orbital-ring {
|
||||||
|
|||||||
@@ -498,10 +498,40 @@ defineExpose({
|
|||||||
<!-- Titlebar -->
|
<!-- Titlebar -->
|
||||||
<div class="titlebar" @mousedown="startDrag">
|
<div class="titlebar" @mousedown="startDrag">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
<!-- Nucleo Logo -->
|
||||||
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
|
<div class="nucleo-logo">
|
||||||
</svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||||
<span>Terminal</span>
|
<!-- Core nucleus -->
|
||||||
|
<circle cx="12" cy="12" r="4" fill="url(#nucleoGradient)"/>
|
||||||
|
<!-- Orbital rings -->
|
||||||
|
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(-30 12 12)"/>
|
||||||
|
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(30 12 12)"/>
|
||||||
|
<ellipse cx="12" cy="12" rx="9" ry="4" stroke="url(#orbitGradient)" stroke-width="1.2" fill="none" transform="rotate(90 12 12)"/>
|
||||||
|
<!-- Electrons -->
|
||||||
|
<circle cx="12" cy="3" r="1.5" fill="#a78bfa">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="3s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<circle cx="20" cy="14" r="1.5" fill="#818cf8">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="120 12 12" to="480 12 12" dur="4s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<circle cx="4" cy="14" r="1.5" fill="#c4b5fd">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="240 12 12" to="600 12 12" dur="3.5s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
<!-- Gradients -->
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="nucleoGradient" cx="50%" cy="30%" r="60%">
|
||||||
|
<stop offset="0%" stop-color="#a78bfa"/>
|
||||||
|
<stop offset="100%" stop-color="#6366f1"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="orbitGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818cf8" stop-opacity="0.8"/>
|
||||||
|
<stop offset="50%" stop-color="#a78bfa" stop-opacity="0.4"/>
|
||||||
|
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.8"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="nucleo-name">Nucleo</span>
|
||||||
<i class="dot" :class="{ on: connected, wait: connecting }"></i>
|
<i class="dot" :class="{ on: connected, wait: connecting }"></i>
|
||||||
<a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a>
|
<a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -565,11 +595,28 @@ defineExpose({
|
|||||||
.left {
|
.left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 6px;
|
||||||
color: #222;
|
color: #222;
|
||||||
font: 500 10px/1 system-ui, sans-serif;
|
font: 500 10px/1 system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nucleo-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(99, 102, 241, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nucleo-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
width: 5px; height: 5px;
|
width: 5px; height: 5px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { PORT_TERMINAL } from '../config'
|
|||||||
|
|
||||||
type ClaudeStatus =
|
type ClaudeStatus =
|
||||||
| 'idle'
|
| 'idle'
|
||||||
| 'processing' // UserPromptSubmit - Claude is processing user input
|
| 'processing' // UserPromptSubmit - Claude is processing user input
|
||||||
| 'toolUse' // PreToolUse - Using a tool (generic)
|
| 'toolUse' // PreToolUse - Using a tool (generic)
|
||||||
| 'toolDone' // PostToolUse - Tool finished
|
| 'toolDone' // PostToolUse - Tool finished
|
||||||
| 'reading' // PreToolUse(Read/Glob/Grep) - Reading files
|
| 'reading' // PreToolUse(Read/Glob/Grep) - Reading files
|
||||||
| 'writing' // PreToolUse(Edit/Write) - Writing files
|
| 'writing' // PreToolUse(Edit/Write) - Writing files
|
||||||
| 'sessionStart' // SessionStart - Session just started
|
| 'sessionStart' // SessionStart - Session just started
|
||||||
| 'subagentStart' // SubagentStart - Spawning subagent
|
| 'subagentStart' // SubagentStart - Spawning subagent
|
||||||
| 'subagentStop' // SubagentStop - Subagent finished
|
| 'subagentStop' // SubagentStop - Subagent finished
|
||||||
| 'notification' // Notification - Claude sent notification
|
| 'notification' // Notification - Claude sent notification
|
||||||
| 'thinking' // Legacy support
|
| 'permissionRequest' // PermissionRequest - Waiting for user permission
|
||||||
|
| 'thinking' // Legacy support
|
||||||
|
|
||||||
interface ClaudeStatusPayload {
|
interface ClaudeStatusPayload {
|
||||||
status: ClaudeStatus
|
status: ClaudeStatus
|
||||||
@@ -27,7 +28,8 @@ export async function handleClaudeStatus(req: Request): Promise<Response | null>
|
|||||||
|
|
||||||
const validStatuses: ClaudeStatus[] = [
|
const validStatuses: ClaudeStatus[] = [
|
||||||
'idle', 'processing', 'toolUse', 'toolDone', 'reading', 'writing',
|
'idle', 'processing', 'toolUse', 'toolDone', 'reading', 'writing',
|
||||||
'sessionStart', 'subagentStart', 'subagentStop', 'notification', 'thinking'
|
'sessionStart', 'subagentStart', 'subagentStop', 'notification',
|
||||||
|
'permissionRequest', 'thinking'
|
||||||
]
|
]
|
||||||
if (!body.status || !validStatuses.includes(body.status)) {
|
if (!body.status || !validStatuses.includes(body.status)) {
|
||||||
return errorResponse(`Invalid status. Must be one of: ${validStatuses.join(', ')}`, 400)
|
return errorResponse(`Invalid status. Must be one of: ${validStatuses.join(', ')}`, 400)
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function startTerminalServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Claude status types
|
// Claude status types
|
||||||
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'thinking'
|
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
|
||||||
|
|
||||||
// Broadcast Claude status to ALL clients across ALL sessions
|
// Broadcast Claude status to ALL clients across ALL sessions
|
||||||
export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) {
|
export function broadcastClaudeStatus(status: ClaudeStatus, tool?: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user