feat: Add persistent terminal with floating UI

- Server keeps PTY sessions alive on client disconnect
- Output buffer replays history on reconnect
- FloatingTerminal component accessible from any page
- Responsive: floating window on desktop, fullscreen on mobile
- macOS-style header with traffic light buttons
This commit is contained in:
2026-02-13 07:33:43 -06:00
parent ccbf542480
commit 2cf869d2e9
4 changed files with 1268 additions and 3 deletions

View File

@@ -1,16 +1,18 @@
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import StatusBar from './components/StatusBar.vue'
import Toolbar from './components/Toolbar.vue'
import ComponentsDropdown from './components/ComponentsDropdown.vue'
import FloatingTerminal from './components/FloatingTerminal.vue'
import { initWebMCP } from './services/webmcp'
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
const route = useRoute()
const router = useRouter()
const showTerminal = ref(false)
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source'
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal'
onMounted(async () => {
// Initialize WebMCP connection
@@ -49,6 +51,26 @@ watch(() => route.name, (newPage) => {
</Transition>
</RouterView>
</main>
<!-- Floating Terminal Toggle Button -->
<button
class="terminal-fab"
:class="{ active: showTerminal }"
@click="showTerminal = !showTerminal"
title="Toggle Terminal"
>
<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>
</svg>
</button>
<!-- Floating Terminal -->
<FloatingTerminal v-model="showTerminal" />
</div>
</template>
@@ -103,4 +125,53 @@ watch(() => route.name, (newPage) => {
opacity: 0;
transform: translateX(-20px);
}
/* Terminal FAB */
.terminal-fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border: none;
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);
z-index: 9998;
}
.terminal-fab:hover {
transform: scale(1.05);
box-shadow: 0 12px 32px rgba(99, 102, 241, 0.5);
}
.terminal-fab.active {
background: #ef4444;
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
transform: rotate(90deg);
}
.terminal-fab.active:hover {
box-shadow: 0 12px 32px rgba(239, 68, 68, 0.5);
}
@media (max-width: 768px) {
.terminal-fab {
bottom: 16px;
right: 16px;
width: 52px;
height: 52px;
}
.terminal-fab.active {
opacity: 0;
pointer-events: none;
}
}
</style>