feat: Redesign FloatingTerminal with Windows Vista Aero style
- Slim, transparent Aero Glass design with backdrop blur - Draggable window from titlebar - Resizable from corner handle - Ctrl+E toggle to open at cursor position - Remove minimize functionality (close returns to sidebar button) - Add PWA dev-dist files and lock files
This commit is contained in:
1
frontend/dev-dist/registerSW.js
Normal file
1
frontend/dev-dist/registerSW.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||||
0
frontend/dev-dist/suppress-warnings.js
Normal file
0
frontend/dev-dist/suppress-warnings.js
Normal file
94
frontend/dev-dist/sw.js
Normal file
94
frontend/dev-dist/sw.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If the loader is already loaded, just stop.
|
||||||
|
if (!self.define) {
|
||||||
|
let registry = {};
|
||||||
|
|
||||||
|
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||||
|
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||||
|
let nextDefineUri;
|
||||||
|
|
||||||
|
const singleRequire = (uri, parentUri) => {
|
||||||
|
uri = new URL(uri + ".js", parentUri).href;
|
||||||
|
return registry[uri] || (
|
||||||
|
|
||||||
|
new Promise(resolve => {
|
||||||
|
if ("document" in self) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = uri;
|
||||||
|
script.onload = resolve;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} else {
|
||||||
|
nextDefineUri = uri;
|
||||||
|
importScripts(uri);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(() => {
|
||||||
|
let promise = registry[uri];
|
||||||
|
if (!promise) {
|
||||||
|
throw new Error(`Module ${uri} didn’t register its module`);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.define = (depsNames, factory) => {
|
||||||
|
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||||
|
if (registry[uri]) {
|
||||||
|
// Module is already loading or loaded.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let exports = {};
|
||||||
|
const require = depUri => singleRequire(depUri, uri);
|
||||||
|
const specialDeps = {
|
||||||
|
module: { uri },
|
||||||
|
exports,
|
||||||
|
require
|
||||||
|
};
|
||||||
|
registry[uri] = Promise.all(depsNames.map(
|
||||||
|
depName => specialDeps[depName] || require(depName)
|
||||||
|
)).then(deps => {
|
||||||
|
factory(...deps);
|
||||||
|
return exports;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||||
|
|
||||||
|
self.skipWaiting();
|
||||||
|
workbox.clientsClaim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The precacheAndRoute() method efficiently caches and responds to
|
||||||
|
* requests for URLs in the manifest.
|
||||||
|
* See https://goo.gl/S9QRab
|
||||||
|
*/
|
||||||
|
workbox.precacheAndRoute([{
|
||||||
|
"url": "suppress-warnings.js",
|
||||||
|
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
|
}, {
|
||||||
|
"url": "index.html",
|
||||||
|
"revision": "0.24e3u5ntq78"
|
||||||
|
}], {});
|
||||||
|
workbox.cleanupOutdatedCaches();
|
||||||
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
allowlist: [/^\/$/],
|
||||||
|
denylist: [/^\/api\//]
|
||||||
|
}));
|
||||||
|
|
||||||
|
}));
|
||||||
|
//# sourceMappingURL=sw.js.map
|
||||||
1
frontend/dev-dist/sw.js.map
Normal file
1
frontend/dev-dist/sw.js.map
Normal file
File diff suppressed because one or more lines are too long
3396
frontend/dev-dist/workbox-5a5d9309.js
Normal file
3396
frontend/dev-dist/workbox-5a5d9309.js
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/dev-dist/workbox-5a5d9309.js.map
Normal file
1
frontend/dev-dist/workbox-5a5d9309.js.map
Normal file
File diff suppressed because one or more lines are too long
4540
frontend/dev-dist/workbox-c5fd805d.js
Normal file
4540
frontend/dev-dist/workbox-c5fd805d.js
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/dev-dist/workbox-c5fd805d.js.map
Normal file
1
frontend/dev-dist/workbox-c5fd805d.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -23,10 +23,10 @@ const terminalRef = ref<HTMLElement | null>(null)
|
|||||||
const connected = ref(false)
|
const connected = ref(false)
|
||||||
const connecting = ref(false)
|
const connecting = ref(false)
|
||||||
const sessionId = ref<string | null>(null)
|
const sessionId = ref<string | null>(null)
|
||||||
const isMinimized = ref(false)
|
|
||||||
|
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const position = ref({ x: 0, y: 0 })
|
const position = ref({ x: 0, y: 0 })
|
||||||
|
const hasCustomPosition = ref(false)
|
||||||
const dragOffset = ref({ x: 0, y: 0 })
|
const dragOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
// Resize state
|
// Resize state
|
||||||
@@ -40,22 +40,55 @@ let resizeObserver: ResizeObserver | null = null
|
|||||||
|
|
||||||
const WS_URL = `ws://${window.location.hostname}:4103`
|
const WS_URL = `ws://${window.location.hostname}:4103`
|
||||||
|
|
||||||
const fabRef = ref<HTMLElement | null>(null)
|
// Mouse position tracking for Ctrl+E
|
||||||
const dragStartTime = ref(0)
|
const mousePos = ref({ x: 0, y: 0 })
|
||||||
const dragStartPos = ref({ x: 0, y: 0 })
|
let lastToggle = 0
|
||||||
const isDraggingFab = ref(false)
|
|
||||||
|
|
||||||
function startDrag(e: MouseEvent, isFab = false) {
|
function trackMouse(e: MouseEvent) {
|
||||||
if (!isFab && (e.target as HTMLElement).closest('.window-controls')) return
|
mousePos.value = { x: e.clientX, y: e.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTerminal() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastToggle < 150) return // Debounce 150ms
|
||||||
|
lastToggle = now
|
||||||
|
|
||||||
|
if (!isOpen.value) {
|
||||||
|
// Open at mouse position (allow 75% occlusion)
|
||||||
|
const w = size.value.w
|
||||||
|
const h = size.value.h
|
||||||
|
const minX = -w * 0.75
|
||||||
|
const maxX = window.innerWidth - w * 0.25
|
||||||
|
const minY = -h * 0.75
|
||||||
|
const maxY = window.innerHeight - h * 0.25
|
||||||
|
position.value = {
|
||||||
|
x: Math.max(minX, Math.min(mousePos.value.x - w / 2, maxX)),
|
||||||
|
y: Math.max(minY, Math.min(mousePos.value.y - h / 2, maxY))
|
||||||
|
}
|
||||||
|
hasCustomPosition.value = true
|
||||||
|
isOpen.value = true
|
||||||
|
} else {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.ctrlKey && e.key === 'e') {
|
||||||
|
e.preventDefault()
|
||||||
|
toggleTerminal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(e: MouseEvent) {
|
||||||
|
if ((e.target as HTMLElement).closest('.window-controls')) return
|
||||||
|
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
isDraggingFab.value = isFab
|
const rect = terminalRef.value?.getBoundingClientRect()
|
||||||
dragStartTime.value = Date.now()
|
|
||||||
dragStartPos.value = { x: e.clientX, y: e.clientY }
|
|
||||||
|
|
||||||
const element = isFab ? fabRef.value : terminalRef.value
|
|
||||||
const rect = element?.getBoundingClientRect()
|
|
||||||
if (rect) {
|
if (rect) {
|
||||||
|
// Capture actual position if using default bottom/right
|
||||||
|
if (!hasCustomPosition.value) {
|
||||||
|
position.value = { x: rect.left, y: rect.top }
|
||||||
|
}
|
||||||
dragOffset.value = {
|
dragOffset.value = {
|
||||||
x: e.clientX - rect.left,
|
x: e.clientX - rect.left,
|
||||||
y: e.clientY - rect.top
|
y: e.clientY - rect.top
|
||||||
@@ -72,35 +105,26 @@ function onDrag(e: MouseEvent) {
|
|||||||
const newX = e.clientX - dragOffset.value.x
|
const newX = e.clientX - dragOffset.value.x
|
||||||
const newY = e.clientY - dragOffset.value.y
|
const newY = e.clientY - dragOffset.value.y
|
||||||
|
|
||||||
const elementWidth = isDraggingFab.value ? 120 : (terminalRef.value?.offsetWidth || 580)
|
const w = terminalRef.value?.offsetWidth || 580
|
||||||
const elementHeight = isDraggingFab.value ? 28 : (terminalRef.value?.offsetHeight || 360)
|
const h = terminalRef.value?.offsetHeight || 360
|
||||||
const maxX = window.innerWidth - elementWidth
|
|
||||||
const maxY = window.innerHeight - elementHeight
|
// Allow up to 75% occlusion per side (25% must remain visible)
|
||||||
|
const minX = -w * 0.75
|
||||||
|
const maxX = window.innerWidth - w * 0.25
|
||||||
|
const minY = -h * 0.75
|
||||||
|
const maxY = window.innerHeight - h * 0.25
|
||||||
|
|
||||||
position.value = {
|
position.value = {
|
||||||
x: Math.max(0, Math.min(newX, maxX)),
|
x: Math.max(minX, Math.min(newX, maxX)),
|
||||||
y: Math.max(0, Math.min(newY, maxY))
|
y: Math.max(minY, Math.min(newY, maxY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDrag(e: MouseEvent) {
|
function stopDrag() {
|
||||||
const wasDraggingFab = isDraggingFab.value
|
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
hasCustomPosition.value = true
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
|
|
||||||
if (wasDraggingFab) {
|
|
||||||
const elapsed = Date.now() - dragStartTime.value
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(e.clientX - dragStartPos.value.x, 2) +
|
|
||||||
Math.pow(e.clientY - dragStartPos.value.y, 2)
|
|
||||||
)
|
|
||||||
if (elapsed < 200 && distance < 5) {
|
|
||||||
toggleMinimize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isDraggingFab.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize functions
|
// Resize functions
|
||||||
@@ -144,7 +168,7 @@ const terminalStyle = computed(() => {
|
|||||||
width: `${size.value.w}px`,
|
width: `${size.value.w}px`,
|
||||||
height: `${size.value.h}px`
|
height: `${size.value.h}px`
|
||||||
}
|
}
|
||||||
if (position.value.x === 0 && position.value.y === 0) {
|
if (!hasCustomPosition.value) {
|
||||||
return { ...base, bottom: '16px', right: '16px' }
|
return { ...base, bottom: '16px', right: '16px' }
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -156,18 +180,6 @@ const terminalStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const minimizedStyle = computed(() => {
|
|
||||||
if (position.value.x === 0 && position.value.y === 0) {
|
|
||||||
return { bottom: '16px', right: '16px' }
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
top: `${position.value.y}px`,
|
|
||||||
left: `${position.value.x}px`,
|
|
||||||
bottom: 'auto',
|
|
||||||
right: 'auto'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function initTerminal() {
|
function initTerminal() {
|
||||||
if (!terminalContainer.value || terminal) return
|
if (!terminalContainer.value || terminal) return
|
||||||
|
|
||||||
@@ -210,7 +222,7 @@ function initTerminal() {
|
|||||||
nextTick(() => fitAddon?.fit())
|
nextTick(() => fitAddon?.fit())
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
if (fitAddon && terminal && !isMinimized.value) {
|
if (fitAddon && terminal) {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
@@ -228,6 +240,16 @@ function initTerminal() {
|
|||||||
socket.send(JSON.stringify({ type: 'input', data }))
|
socket.send(JSON.stringify({ type: 'input', data }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Capture Ctrl+E even when terminal has focus
|
||||||
|
terminal.attachCustomKeyEventHandler((e) => {
|
||||||
|
if (e.ctrlKey && e.key === 'e') {
|
||||||
|
e.preventDefault()
|
||||||
|
toggleTerminal()
|
||||||
|
return false // Prevent terminal from processing
|
||||||
|
}
|
||||||
|
return true // Let terminal handle other keys
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connect() {
|
async function connect() {
|
||||||
@@ -282,16 +304,6 @@ async function connect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMinimize() {
|
|
||||||
isMinimized.value = !isMinimized.value
|
|
||||||
if (!isMinimized.value) {
|
|
||||||
nextTick(() => {
|
|
||||||
fitAddon?.fit()
|
|
||||||
terminal?.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
}
|
}
|
||||||
@@ -304,18 +316,28 @@ function runClaude() {
|
|||||||
|
|
||||||
watch(isOpen, async (open) => {
|
watch(isOpen, async (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
isMinimized.value = false
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (!terminal) initTerminal()
|
initTerminal()
|
||||||
if (!connected.value && !connecting.value) connect()
|
if (!connected.value && !connecting.value) connect()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
fitAddon?.fit()
|
fitAddon?.fit()
|
||||||
terminal?.focus()
|
terminal?.focus()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// Cleanup when closing
|
||||||
|
resizeObserver?.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
terminal?.dispose()
|
||||||
|
terminal = null
|
||||||
|
fitAddon = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// Global listeners for Ctrl+E
|
||||||
|
document.addEventListener('mousemove', trackMouse)
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
|
||||||
if (isOpen.value) {
|
if (isOpen.value) {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initTerminal()
|
initTerminal()
|
||||||
@@ -331,33 +353,46 @@ onBeforeUnmount(() => {
|
|||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('mousemove', onResize)
|
document.removeEventListener('mousemove', onResize)
|
||||||
document.removeEventListener('mouseup', stopResize)
|
document.removeEventListener('mouseup', stopResize)
|
||||||
|
document.removeEventListener('mousemove', trackMouse)
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expose controls for MCP tools
|
||||||
|
defineExpose({
|
||||||
|
open: (x?: number, y?: number) => {
|
||||||
|
if (x !== undefined && y !== undefined) {
|
||||||
|
position.value = { x, y }
|
||||||
|
hasCustomPosition.value = true
|
||||||
|
}
|
||||||
|
isOpen.value = true
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
isOpen.value = false
|
||||||
|
},
|
||||||
|
toggle: () => {
|
||||||
|
toggleTerminal()
|
||||||
|
},
|
||||||
|
move: (x: number, y: number) => {
|
||||||
|
position.value = { x, y }
|
||||||
|
hasCustomPosition.value = true
|
||||||
|
},
|
||||||
|
resize: (w: number, h: number) => {
|
||||||
|
size.value = { w: Math.max(400, w), h: Math.max(250, h) }
|
||||||
|
nextTick(() => fitAddon?.fit())
|
||||||
|
},
|
||||||
|
getState: () => ({
|
||||||
|
isOpen: isOpen.value,
|
||||||
|
position: position.value,
|
||||||
|
size: size.value
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<!-- Minimized -->
|
|
||||||
<Transition name="fab-pop">
|
|
||||||
<button
|
|
||||||
v-if="isOpen && isMinimized"
|
|
||||||
ref="fabRef"
|
|
||||||
class="aero-btn"
|
|
||||||
:class="{ dragging: isDragging }"
|
|
||||||
:style="minimizedStyle"
|
|
||||||
@mousedown="startDrag($event, true)"
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" 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>
|
|
||||||
<i class="dot" :class="{ on: connected, wait: connecting }"></i>
|
|
||||||
</button>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Window -->
|
|
||||||
<Transition name="win-slide">
|
<Transition name="win-slide">
|
||||||
<div
|
<div
|
||||||
v-if="isOpen && !isMinimized"
|
v-if="isOpen"
|
||||||
ref="terminalRef"
|
ref="terminalRef"
|
||||||
class="aero-win"
|
class="aero-win"
|
||||||
:class="{ dragging: isDragging, resizing: isResizing }"
|
:class="{ dragging: isDragging, resizing: isResizing }"
|
||||||
@@ -365,7 +400,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<div class="glass">
|
<div class="glass">
|
||||||
<!-- Titlebar -->
|
<!-- Titlebar -->
|
||||||
<div class="titlebar" @mousedown="startDrag($event, false)" @dblclick="toggleMinimize">
|
<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">
|
<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"/>
|
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
|
||||||
@@ -376,7 +411,6 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="window-controls">
|
<div class="window-controls">
|
||||||
<button @click="runClaude" title="Claude"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></button>
|
<button @click="runClaude" title="Claude"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></button>
|
||||||
<button @click="toggleMinimize" title="Minimize"><svg width="8" height="1"><rect width="8" height="1" fill="currentColor"/></svg></button>
|
|
||||||
<button class="x" @click="close" title="Close"><svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg></button>
|
<button class="x" @click="close" title="Close"><svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -393,34 +427,6 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aero-btn {
|
|
||||||
position: fixed;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: rgba(255,255,255,0.25);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid rgba(255,255,255,0.5);
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #111;
|
|
||||||
font: 500 10px/1 system-ui, sans-serif;
|
|
||||||
cursor: grab;
|
|
||||||
z-index: 9999;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15), inset 0 1px 0 rgba(255,255,255,0.7);
|
|
||||||
}
|
|
||||||
.aero-btn:hover { background: rgba(255,255,255,0.4); }
|
|
||||||
.aero-btn.dragging { cursor: grabbing; }
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
width: 5px; height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #999;
|
|
||||||
}
|
|
||||||
.dot.on { background: #0a0; box-shadow: 0 0 4px #0a0; }
|
|
||||||
.dot.wait { background: #a80; animation: pulse .8s infinite; }
|
|
||||||
|
|
||||||
.aero-win {
|
.aero-win {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
@@ -467,6 +473,14 @@ onBeforeUnmount(() => {
|
|||||||
font: 500 10px/1 system-ui, sans-serif;
|
font: 500 10px/1 system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 5px; height: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
.dot.on { background: #0a0; box-shadow: 0 0 4px #0a0; }
|
||||||
|
.dot.wait { background: #a80; animation: pulse .8s infinite; }
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
color: #369;
|
color: #369;
|
||||||
@@ -553,9 +567,6 @@ onBeforeUnmount(() => {
|
|||||||
background: rgba(255,255,255,0.25);
|
background: rgba(255,255,255,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab-pop-enter-active, .fab-pop-leave-active { transition: all .12s ease; }
|
|
||||||
.fab-pop-enter-from, .fab-pop-leave-to { opacity: 0; transform: scale(0.9); }
|
|
||||||
|
|
||||||
.win-slide-enter-active, .win-slide-leave-active { transition: all .15s ease; }
|
.win-slide-enter-active, .win-slide-leave-active { transition: all .15s ease; }
|
||||||
.win-slide-enter-from, .win-slide-leave-to { opacity: 0; transform: translateY(16px) scale(0.98); }
|
.win-slide-enter-from, .win-slide-leave-to { opacity: 0; transform: translateY(16px) scale(0.98); }
|
||||||
|
|
||||||
@@ -564,10 +575,9 @@ onBeforeUnmount(() => {
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.aero-win {
|
.aero-win {
|
||||||
inset: auto 0 0 0 !important;
|
inset: auto 0 0 0 !important;
|
||||||
width: 100%;
|
width: 100% !important;
|
||||||
height: 55%;
|
height: 55% !important;
|
||||||
}
|
}
|
||||||
.glass { border-radius: 6px 6px 0 0; }
|
.glass { border-radius: 6px 6px 0 0; }
|
||||||
.aero-btn { bottom: 12px !important; right: 12px !important; left: auto !important; top: auto !important; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
119
frontend/src/services/tools/handlers/terminalHandlers.ts
Normal file
119
frontend/src/services/tools/handlers/terminalHandlers.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Terminal UI control handlers
|
||||||
|
* Controls the FloatingTerminal window (open, close, move, resize)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ToolConfig } from './index'
|
||||||
|
|
||||||
|
export interface TerminalControls {
|
||||||
|
open: (x?: number, y?: number) => void
|
||||||
|
close: () => void
|
||||||
|
toggle: () => void
|
||||||
|
move: (x: number, y: number) => void
|
||||||
|
resize: (width: number, height: number) => void
|
||||||
|
getState: () => { isOpen: boolean; position: { x: number; y: number }; size: { w: number; h: number } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global reference to terminal controls (set by App.vue)
|
||||||
|
let terminalControls: TerminalControls | null = null
|
||||||
|
|
||||||
|
export function setTerminalControls(controls: TerminalControls) {
|
||||||
|
terminalControls = controls
|
||||||
|
;(window as any).__terminalControls = controls
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTerminalControls(): TerminalControls | null {
|
||||||
|
return terminalControls
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTerminalHandlers(): ToolConfig[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'terminal_open',
|
||||||
|
description: 'Abre la ventana flotante del terminal. Opcionalmente en una posicion especifica.',
|
||||||
|
category: 'terminal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
x: { type: 'number', description: 'Posicion X en pixels (opcional)' },
|
||||||
|
y: { type: 'number', description: 'Posicion Y en pixels (opcional)' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: (args: { x?: number; y?: number }) => {
|
||||||
|
if (!terminalControls) return 'Error: Terminal controls not initialized'
|
||||||
|
terminalControls.open(args.x, args.y)
|
||||||
|
const pos = args.x !== undefined && args.y !== undefined
|
||||||
|
? ` en posicion (${args.x}, ${args.y})`
|
||||||
|
: ''
|
||||||
|
return `Terminal abierta${pos}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terminal_close',
|
||||||
|
description: 'Cierra la ventana flotante del terminal.',
|
||||||
|
category: 'terminal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
handler: () => {
|
||||||
|
if (!terminalControls) return 'Error: Terminal controls not initialized'
|
||||||
|
terminalControls.close()
|
||||||
|
return 'Terminal cerrada'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terminal_toggle',
|
||||||
|
description: 'Alterna el estado de la ventana del terminal (abre si esta cerrada, cierra si esta abierta).',
|
||||||
|
category: 'terminal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {}
|
||||||
|
},
|
||||||
|
handler: () => {
|
||||||
|
if (!terminalControls) return 'Error: Terminal controls not initialized'
|
||||||
|
const wasOpen = terminalControls.getState().isOpen
|
||||||
|
terminalControls.toggle()
|
||||||
|
return wasOpen ? 'Terminal cerrada' : 'Terminal abierta'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terminal_move',
|
||||||
|
description: 'Mueve la ventana del terminal a una posicion especifica en pixels.',
|
||||||
|
category: 'terminal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
x: { type: 'number', description: 'Posicion X en pixels' },
|
||||||
|
y: { type: 'number', description: 'Posicion Y en pixels' }
|
||||||
|
},
|
||||||
|
required: ['x', 'y']
|
||||||
|
},
|
||||||
|
handler: (args: { x: number; y: number }) => {
|
||||||
|
if (!terminalControls) return 'Error: Terminal controls not initialized'
|
||||||
|
terminalControls.move(args.x, args.y)
|
||||||
|
return `Terminal movida a (${args.x}, ${args.y})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'terminal_resize',
|
||||||
|
description: 'Cambia el tamano de la ventana del terminal.',
|
||||||
|
category: 'terminal',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
width: { type: 'number', description: 'Ancho en pixels (min 400)' },
|
||||||
|
height: { type: 'number', description: 'Alto en pixels (min 250)' }
|
||||||
|
},
|
||||||
|
required: ['width', 'height']
|
||||||
|
},
|
||||||
|
handler: (args: { width: number; height: number }) => {
|
||||||
|
if (!terminalControls) return 'Error: Terminal controls not initialized'
|
||||||
|
const w = Math.max(400, args.width)
|
||||||
|
const h = Math.max(250, args.height)
|
||||||
|
terminalControls.resize(w, h)
|
||||||
|
return `Terminal redimensionada a ${w}x${h}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
server/bun.lock
Normal file
15
server/bun.lock
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "agent-ui-server",
|
||||||
|
"dependencies": {
|
||||||
|
"@skitee3000/bun-pty": "^0.3.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@skitee3000/bun-pty": ["@skitee3000/bun-pty@0.3.3", "", {}, "sha512-y+kA3435zkFjh11KdQMy0ho/UkKN/iae0t9tTE7iZ762Oi2h31dGOvbRIJWeJZ2qYYTJvAd6NEIzJlZUhI6ukw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
31
server/package-lock.json
generated
Normal file
31
server/package-lock.json
generated
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "agent-ui-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "agent-ui-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"node-pty": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/node-pty": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^7.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user