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:
@@ -25,17 +25,18 @@ const responseRef = ref<InstanceType<typeof FloatingResponse> | null>(null)
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
// 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 claudeTool = ref<string | null>(null)
|
||||
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
|
||||
const isReading = ref(false) // Reading files
|
||||
const isWriting = ref(false) // Writing files
|
||||
const hasSubagent = ref(false) // Subagent active
|
||||
const showSessionStart = ref(false) // Session start animation (3s)
|
||||
const showNotification = ref(false) // Notification pulse (2s)
|
||||
const showToolFlash = ref(false) // Tool use flash (500ms)
|
||||
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
|
||||
const isReading = ref(false) // Reading files
|
||||
const isWriting = ref(false) // Writing files
|
||||
const hasSubagent = ref(false) // Subagent active
|
||||
const awaitingPermission = ref(false) // Waiting for user permission (highest priority)
|
||||
const showSessionStart = ref(false) // Session start animation (3s)
|
||||
const showNotification = ref(false) // Notification pulse (2s)
|
||||
const showToolFlash = ref(false) // Tool use flash (500ms)
|
||||
|
||||
let statusWs: WebSocket | null = null
|
||||
let statusReconnectTimeout: number | null = null
|
||||
@@ -78,9 +79,14 @@ function connectStatusWs() {
|
||||
isProcessing.value = false
|
||||
isReading.value = false
|
||||
isWriting.value = false
|
||||
awaitingPermission.value = false
|
||||
if (processingTimeout) clearTimeout(processingTimeout)
|
||||
break
|
||||
|
||||
case 'permissionRequest':
|
||||
awaitingPermission.value = true
|
||||
break
|
||||
|
||||
case 'reading':
|
||||
isReading.value = true
|
||||
triggerToolFlash()
|
||||
@@ -98,6 +104,7 @@ function connectStatusWs() {
|
||||
case 'toolDone':
|
||||
isReading.value = false
|
||||
isWriting.value = false
|
||||
awaitingPermission.value = false
|
||||
break
|
||||
|
||||
case 'sessionStart':
|
||||
@@ -278,12 +285,13 @@ watch(() => route.name, (newPage) => {
|
||||
reading: isReading,
|
||||
writing: isWriting,
|
||||
subagent: hasSubagent,
|
||||
permission: awaitingPermission,
|
||||
'session-start': showSessionStart,
|
||||
notification: showNotification,
|
||||
'tool-flash': showToolFlash
|
||||
}"
|
||||
@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 -->
|
||||
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
|
||||
@@ -297,8 +305,15 @@ watch(() => route.name, (newPage) => {
|
||||
<span></span>
|
||||
</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) -->
|
||||
<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>
|
||||
@@ -320,14 +335,26 @@ watch(() => route.name, (newPage) => {
|
||||
|
||||
<!-- Normal icons -->
|
||||
<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">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
<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">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
<!-- Terminal closed: Nucleo atom icon -->
|
||||
<svg v-if="!showTerminal" class="nucleo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||
<!-- Core nucleus -->
|
||||
<circle cx="12" cy="12" r="3.5" fill="white" opacity="0.95"/>
|
||||
<!-- Orbital rings -->
|
||||
<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)"/>
|
||||
<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>
|
||||
<!-- 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>
|
||||
</button>
|
||||
|
||||
@@ -419,35 +446,102 @@ watch(() => route.name, (newPage) => {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(145deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
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;
|
||||
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 {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.5);
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
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 {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
|
||||
transform: rotate(90deg);
|
||||
background: linear-gradient(145deg, #6366f1 0%, #4f46e5 50%, #7c3aed 100%);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
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 {
|
||||
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 */
|
||||
@@ -492,6 +586,25 @@ watch(() => route.name, (newPage) => {
|
||||
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 */
|
||||
.terminal-fab.tool-flash::after {
|
||||
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 {
|
||||
position: fixed;
|
||||
@@ -681,13 +836,20 @@ watch(() => route.name, (newPage) => {
|
||||
.terminal-fab {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.terminal-fab.active:not(.processing):not(.reading):not(.writing) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
.terminal-fab::before {
|
||||
border-radius: 19px;
|
||||
}
|
||||
|
||||
.connection-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.orbital-ring {
|
||||
|
||||
@@ -498,10 +498,40 @@ defineExpose({
|
||||
<!-- Titlebar -->
|
||||
<div class="titlebar" @mousedown="startDrag">
|
||||
<div class="left">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
|
||||
</svg>
|
||||
<span>Terminal</span>
|
||||
<!-- Nucleo Logo -->
|
||||
<div class="nucleo-logo">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<!-- 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>
|
||||
<a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a>
|
||||
</div>
|
||||
@@ -565,11 +595,28 @@ defineExpose({
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 6px;
|
||||
color: #222;
|
||||
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 {
|
||||
width: 5px; height: 5px;
|
||||
border-radius: 50%;
|
||||
|
||||
Reference in New Issue
Block a user