Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 15s
Resizable panel: - Drag handle on left border of copilot panel - Pointer events for smooth resize (320px–1200px range) SFTP MCP tools: - sftp_list: list remote directories - sftp_read: read remote files - sftp_write: write remote files - Full HTTP endpoints + bridge tool definitions Active session context: - mcp_get_session_context command returns last 20 lines of scrollback - Frontend can call on tab switch to keep AI informed Error watcher: - Background scanner runs every 2 seconds across all sessions - 20+ error patterns (permission denied, OOM, segfault, disk full, etc.) - Emits mcp:error events to frontend with session ID and matched line - Sessions auto-registered with watcher on connect Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193 lines
5.8 KiB
Vue
193 lines
5.8 KiB
Vue
<template>
|
|
<div class="flex h-full relative">
|
|
<!-- Drag handle for resizing -->
|
|
<div
|
|
class="w-1 cursor-col-resize hover:bg-[var(--wraith-accent-blue)] active:bg-[var(--wraith-accent-blue)] transition-colors shrink-0"
|
|
@pointerdown="startResize"
|
|
/>
|
|
<div
|
|
class="flex flex-col h-full bg-[var(--wraith-bg-secondary)] border-l border-[var(--wraith-border)] flex-1 min-w-0"
|
|
:style="{ width: panelWidth + 'px' }"
|
|
>
|
|
<!-- Header -->
|
|
<div class="p-3 border-b border-[var(--wraith-border)] flex items-center justify-between gap-2">
|
|
<span class="text-xs font-bold tracking-widest text-[var(--wraith-accent-blue)]">AI COPILOT</span>
|
|
<div class="flex items-center gap-1.5">
|
|
<select
|
|
v-model="selectedShell"
|
|
class="bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] rounded px-1.5 py-0.5 text-[10px] text-[var(--wraith-text-secondary)] outline-none"
|
|
:disabled="connected"
|
|
>
|
|
<option v-for="shell in shells" :key="shell.path" :value="shell.path">
|
|
{{ shell.name }}
|
|
</option>
|
|
</select>
|
|
<button
|
|
v-if="!connected"
|
|
class="px-2 py-0.5 text-[10px] font-bold rounded bg-[var(--wraith-accent-blue)] text-black cursor-pointer"
|
|
:disabled="!selectedShell"
|
|
@click="launch"
|
|
>
|
|
Launch
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="px-2 py-0.5 text-[10px] font-bold rounded bg-[var(--wraith-accent-red,#f85149)] text-white cursor-pointer"
|
|
@click="kill"
|
|
>
|
|
Kill
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Terminal area -->
|
|
<div v-if="connected" ref="containerRef" class="flex-1 min-h-0" />
|
|
|
|
<!-- Session ended prompt -->
|
|
<div v-else-if="sessionEnded" class="flex-1 flex flex-col items-center justify-center gap-3 p-4">
|
|
<p class="text-xs text-[var(--wraith-text-muted)]">Session ended</p>
|
|
<button
|
|
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-accent-blue)] text-black font-bold cursor-pointer"
|
|
@click="launch"
|
|
>
|
|
Relaunch
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-else class="flex-1 flex flex-col items-center justify-center gap-2 p-4">
|
|
<p class="text-xs text-[var(--wraith-text-muted)] text-center">
|
|
Select a shell and click Launch to start a local terminal.
|
|
</p>
|
|
<p class="text-[10px] text-[var(--wraith-text-muted)] text-center">
|
|
Run <code class="text-[var(--wraith-accent-blue)]">claude</code>,
|
|
<code class="text-[var(--wraith-accent-blue)]">gemini</code>, or
|
|
<code class="text-[var(--wraith-accent-blue)]">codex</code> here.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
import { useTerminal } from "@/composables/useTerminal";
|
|
|
|
interface ShellInfo { name: string; path: string; }
|
|
|
|
// Resizable panel
|
|
const panelWidth = ref(640);
|
|
|
|
function startResize(e: PointerEvent): void {
|
|
e.preventDefault();
|
|
const startX = e.clientX;
|
|
const startWidth = panelWidth.value;
|
|
|
|
function onMove(ev: PointerEvent): void {
|
|
// Dragging left increases width (panel is on the right side)
|
|
const delta = startX - ev.clientX;
|
|
panelWidth.value = Math.max(320, Math.min(1200, startWidth + delta));
|
|
}
|
|
|
|
function onUp(): void {
|
|
document.removeEventListener("pointermove", onMove);
|
|
document.removeEventListener("pointerup", onUp);
|
|
}
|
|
|
|
document.addEventListener("pointermove", onMove);
|
|
document.addEventListener("pointerup", onUp);
|
|
}
|
|
|
|
const shells = ref<ShellInfo[]>([]);
|
|
const selectedShell = ref("");
|
|
const connected = ref(false);
|
|
const sessionEnded = ref(false);
|
|
const containerRef = ref<HTMLElement | null>(null);
|
|
|
|
let sessionId = "";
|
|
let terminalInstance: ReturnType<typeof useTerminal> | null = null;
|
|
let closeUnlisten: UnlistenFn | null = null;
|
|
|
|
async function loadShells(): Promise<void> {
|
|
try {
|
|
shells.value = await invoke<ShellInfo[]>("list_available_shells");
|
|
if (shells.value.length > 0 && !selectedShell.value) {
|
|
selectedShell.value = shells.value[0].path;
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to list shells:", err);
|
|
}
|
|
}
|
|
|
|
async function launch(): Promise<void> {
|
|
if (!selectedShell.value) return;
|
|
sessionEnded.value = false;
|
|
|
|
try {
|
|
sessionId = await invoke<string>("spawn_local_shell", {
|
|
shellPath: selectedShell.value,
|
|
cols: 80,
|
|
rows: 24,
|
|
});
|
|
connected.value = true;
|
|
|
|
await nextTick();
|
|
|
|
if (containerRef.value) {
|
|
terminalInstance = useTerminal(sessionId, "pty");
|
|
terminalInstance.mount(containerRef.value);
|
|
|
|
// Fit after mount to get real dimensions, then resize the PTY
|
|
setTimeout(() => {
|
|
if (terminalInstance) {
|
|
terminalInstance.fit();
|
|
const term = terminalInstance.terminal;
|
|
invoke("pty_resize", {
|
|
sessionId,
|
|
cols: term.cols,
|
|
rows: term.rows,
|
|
}).catch(() => {});
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
// Listen for shell exit
|
|
closeUnlisten = await listen(`pty:close:${sessionId}`, () => {
|
|
cleanup();
|
|
sessionEnded.value = true;
|
|
});
|
|
} catch (err) {
|
|
console.error("Failed to spawn shell:", err);
|
|
connected.value = false;
|
|
}
|
|
}
|
|
|
|
function kill(): void {
|
|
if (sessionId) {
|
|
invoke("disconnect_pty", { sessionId }).catch(() => {});
|
|
}
|
|
cleanup();
|
|
}
|
|
|
|
function cleanup(): void {
|
|
if (terminalInstance) {
|
|
terminalInstance.destroy();
|
|
terminalInstance = null;
|
|
}
|
|
if (closeUnlisten) {
|
|
closeUnlisten();
|
|
closeUnlisten = null;
|
|
}
|
|
connected.value = false;
|
|
sessionId = "";
|
|
}
|
|
|
|
onMounted(loadShells);
|
|
|
|
onBeforeUnmount(() => {
|
|
if (connected.value) kill();
|
|
});
|
|
</script>
|