feat: Help menu + fix tab detach rendering
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m48s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m48s
Help menu (File → Help): - Getting Started guide (connections, SFTP, copilot, tabs) - Keyboard Shortcuts reference table - MCP Integration page (setup command, all 18 tools documented, bridge path auto-populated, architecture explanation) - About page with version and tech stack - Opens as a tabbed popup window Tab detach fixes: - Added detached-*, editor-*, help-* to capabilities window list (detached windows had no event permissions — silent failure) - SessionContainer filters out detached sessions (active=false) so the main window stops rendering the terminal when detached - Terminal now only renders in the detached popup window Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f22f85ac00
commit
9c3afa39bd
@ -1,7 +1,7 @@
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the main Wraith window",
|
||||
"windows": ["main", "tool-*"],
|
||||
"windows": ["main", "tool-*", "detached-*", "editor-*", "help-*"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:default",
|
||||
|
||||
@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*"],"permissions":["core:default","core:event:default","core:window:default","core:window:allow-create","core:webview:default","core:webview:allow-create-webview-window","shell:allow-open","updater:default"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the main Wraith window","local":true,"windows":["main","tool-*","detached-*","editor-*","help-*"],"permissions":["core:default","core:event:default","core:window:default","core:window:allow-create","core:webview:default","core:webview:allow-create-webview-window","shell:allow-open","updater:default"]}}
|
||||
@ -91,16 +91,17 @@ function setTerminalRef(sessionId: string, el: unknown): void {
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
// Only render sessions that are active (not detached to separate windows)
|
||||
const sshSessions = computed(() =>
|
||||
sessionStore.sessions.filter((s) => s.protocol === "ssh"),
|
||||
sessionStore.sessions.filter((s) => s.protocol === "ssh" && s.active),
|
||||
);
|
||||
|
||||
const localSessions = computed(() =>
|
||||
sessionStore.sessions.filter((s) => s.protocol === "local"),
|
||||
sessionStore.sessions.filter((s) => s.protocol === "local" && s.active),
|
||||
);
|
||||
|
||||
const rdpSessions = computed(() =>
|
||||
sessionStore.sessions.filter((s) => s.protocol === "rdp"),
|
||||
sessionStore.sessions.filter((s) => s.protocol === "rdp" && s.active),
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
219
src/components/tools/HelpWindow.vue
Normal file
219
src/components/tools/HelpWindow.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Tabs -->
|
||||
<div class="flex items-center gap-1 px-4 py-2 bg-[#161b22] border-b border-[#30363d] shrink-0">
|
||||
<button v-for="t in tabs" :key="t.id"
|
||||
class="px-3 py-1 text-xs rounded cursor-pointer transition-colors"
|
||||
:class="activeTab === t.id ? 'bg-[#58a6ff] text-black font-bold' : 'text-[#8b949e] hover:text-white'"
|
||||
@click="activeTab = t.id"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<!-- Getting Started -->
|
||||
<div v-if="activeTab === 'guide'" class="prose-wraith">
|
||||
<h2>Getting Started with Wraith</h2>
|
||||
<p>Wraith is a native desktop SSH/SFTP/RDP client with an integrated AI copilot.</p>
|
||||
|
||||
<h3>Creating a Connection</h3>
|
||||
<ol>
|
||||
<li>Click <strong>File → New Connection</strong> or the <strong>+ Host</strong> button in the sidebar</li>
|
||||
<li>Enter hostname, port, and protocol (SSH or RDP)</li>
|
||||
<li>Optionally link a credential from the vault</li>
|
||||
<li>Double-click the connection to connect</li>
|
||||
</ol>
|
||||
|
||||
<h3>Quick Connect</h3>
|
||||
<p>Type <code>user@host:port</code> in the Quick Connect bar and press Enter.</p>
|
||||
|
||||
<h3>AI Copilot</h3>
|
||||
<p>Press <strong>Ctrl+Shift+G</strong> to open the AI copilot panel. Select a shell, click Launch, and run your AI CLI (Claude Code, Gemini, Codex).</p>
|
||||
<p>Configure one-click launch presets in <strong>Settings → AI Copilot</strong>.</p>
|
||||
|
||||
<h3>Local Terminals</h3>
|
||||
<p>Click the <strong>+</strong> button in the tab bar to open a local shell (PowerShell, CMD, Git Bash, WSL, bash, zsh).</p>
|
||||
|
||||
<h3>SFTP Browser</h3>
|
||||
<p>Switch to the <strong>SFTP</strong> tab in the sidebar. It follows the active SSH session and tracks the current working directory.</p>
|
||||
<p>Right-click files for Edit, Download, Rename, Delete.</p>
|
||||
|
||||
<h3>Tab Management</h3>
|
||||
<ul>
|
||||
<li><strong>Drag tabs</strong> to reorder</li>
|
||||
<li><strong>Right-click tab</strong> → Detach to Window (pop out to separate window)</li>
|
||||
<li>Close the detached window to reattach</li>
|
||||
<li>Tabs pulse blue when there's new activity in the background</li>
|
||||
</ul>
|
||||
|
||||
<h3>Remote Monitoring</h3>
|
||||
<p>Every SSH session shows a monitoring bar at the bottom with CPU, RAM, disk, and network stats — polled every 5 seconds. No agent needed.</p>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts -->
|
||||
<div v-if="activeTab === 'shortcuts'" class="prose-wraith">
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<table>
|
||||
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><kbd>Ctrl+K</kbd></td><td>Command Palette</td></tr>
|
||||
<tr><td><kbd>Ctrl+Shift+G</kbd></td><td>Toggle AI Copilot</td></tr>
|
||||
<tr><td><kbd>Ctrl+B</kbd></td><td>Toggle Sidebar</td></tr>
|
||||
<tr><td><kbd>Ctrl+W</kbd></td><td>Close Active Tab</td></tr>
|
||||
<tr><td><kbd>Ctrl+Tab</kbd></td><td>Next Tab</td></tr>
|
||||
<tr><td><kbd>Ctrl+Shift+Tab</kbd></td><td>Previous Tab</td></tr>
|
||||
<tr><td><kbd>Ctrl+1-9</kbd></td><td>Switch to Tab N</td></tr>
|
||||
<tr><td><kbd>Ctrl+F</kbd></td><td>Find in Terminal</td></tr>
|
||||
<tr><td><kbd>Ctrl+S</kbd></td><td>Save (in editor windows)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Terminal</h3>
|
||||
<table>
|
||||
<thead><tr><th>Action</th><th>How</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Copy</td><td>Select text (auto-copies)</td></tr>
|
||||
<tr><td>Paste</td><td>Right-click</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- MCP Integration -->
|
||||
<div v-if="activeTab === 'mcp'" class="prose-wraith">
|
||||
<h2>MCP Integration (AI Tool Access)</h2>
|
||||
<p>Wraith includes an MCP (Model Context Protocol) server that gives AI CLI tools programmatic access to your active sessions.</p>
|
||||
|
||||
<h3>Setup</h3>
|
||||
<p>The MCP bridge binary is automatically downloaded to:</p>
|
||||
<pre>{{ bridgePath }}</pre>
|
||||
<p>Register with Claude Code:</p>
|
||||
<pre>claude mcp add wraith -- "{{ bridgePath }}"</pre>
|
||||
|
||||
<h3>Available MCP Tools (18)</h3>
|
||||
|
||||
<h4>Session Management</h4>
|
||||
<table>
|
||||
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>list_sessions</code></td><td>List all active SSH/RDP/PTY sessions</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Terminal</h4>
|
||||
<table>
|
||||
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>terminal_read</code></td><td>Read recent terminal output (ANSI stripped)</td></tr>
|
||||
<tr><td><code>terminal_execute</code></td><td>Run a command and capture output</td></tr>
|
||||
<tr><td><code>terminal_screenshot</code></td><td>Capture RDP frame as PNG</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>SFTP</h4>
|
||||
<table>
|
||||
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>sftp_list</code></td><td>List remote directory</td></tr>
|
||||
<tr><td><code>sftp_read</code></td><td>Read remote file</td></tr>
|
||||
<tr><td><code>sftp_write</code></td><td>Write remote file</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Network</h4>
|
||||
<table>
|
||||
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>network_scan</code></td><td>ARP + ping sweep subnet discovery</td></tr>
|
||||
<tr><td><code>port_scan</code></td><td>TCP port scan</td></tr>
|
||||
<tr><td><code>ping</code></td><td>Ping a host</td></tr>
|
||||
<tr><td><code>traceroute</code></td><td>Traceroute to host</td></tr>
|
||||
<tr><td><code>dns_lookup</code></td><td>DNS query (A, MX, TXT, etc.)</td></tr>
|
||||
<tr><td><code>whois</code></td><td>Whois lookup</td></tr>
|
||||
<tr><td><code>wake_on_lan</code></td><td>Send WoL magic packet</td></tr>
|
||||
<tr><td><code>bandwidth_test</code></td><td>Internet speed test</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Utilities (no session needed)</h4>
|
||||
<table>
|
||||
<thead><tr><th>Tool</th><th>Description</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>subnet_calc</code></td><td>Subnet calculator</td></tr>
|
||||
<tr><td><code>generate_ssh_key</code></td><td>Generate SSH key pair</td></tr>
|
||||
<tr><td><code>generate_password</code></td><td>Generate secure password</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>How It Works</h3>
|
||||
<ol>
|
||||
<li>Wraith starts an HTTP server on <code>localhost</code> (random port)</li>
|
||||
<li>Port written to <code>mcp-port</code> in data directory</li>
|
||||
<li>Bridge binary reads the port and proxies JSON-RPC over stdio</li>
|
||||
<li>AI CLI spawns the bridge as an MCP server</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- About -->
|
||||
<div v-if="activeTab === 'about'" class="prose-wraith">
|
||||
<h2>About Wraith</h2>
|
||||
<p class="text-2xl font-bold tracking-widest text-[#58a6ff]">WRAITH</p>
|
||||
<p>Exists everywhere, all at once.</p>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>Version</td><td>{{ version }}</td></tr>
|
||||
<tr><td>Runtime</td><td>Tauri v2 + Rust</td></tr>
|
||||
<tr><td>Frontend</td><td>Vue 3 + TypeScript</td></tr>
|
||||
<tr><td>Terminal</td><td>xterm.js 6</td></tr>
|
||||
<tr><td>SSH</td><td>russh 0.48</td></tr>
|
||||
<tr><td>RDP</td><td>ironrdp 0.14</td></tr>
|
||||
<tr><td>License</td><td>Proprietary</td></tr>
|
||||
<tr><td>Publisher</td><td>Vigilance Cyber / Vigilsynth</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
|
||||
const tabs = [
|
||||
{ id: "guide", label: "Getting Started" },
|
||||
{ id: "shortcuts", label: "Shortcuts" },
|
||||
{ id: "mcp", label: "MCP Integration" },
|
||||
{ id: "about", label: "About" },
|
||||
];
|
||||
|
||||
const activeTab = ref("guide");
|
||||
const bridgePath = ref("loading...");
|
||||
const version = ref("loading...");
|
||||
|
||||
onMounted(async () => {
|
||||
// Read initial tab from URL
|
||||
const params = new URLSearchParams(window.location.hash.split("?")[1] || "");
|
||||
const page = params.get("page");
|
||||
if (page && tabs.some(t => t.id === page)) activeTab.value = page;
|
||||
|
||||
try { version.value = await getVersion(); } catch { version.value = "unknown"; }
|
||||
try { bridgePath.value = await invoke<string>("mcp_bridge_path"); } catch { bridgePath.value = "unknown"; }
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.prose-wraith h2 { font-size: 16px; font-weight: 700; color: #e0e0e0; margin-bottom: 12px; }
|
||||
.prose-wraith h3 { font-size: 13px; font-weight: 600; color: #8b949e; margin-top: 20px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.prose-wraith h4 { font-size: 12px; font-weight: 600; color: #58a6ff; margin-top: 16px; margin-bottom: 6px; }
|
||||
.prose-wraith p { font-size: 12px; color: #8b949e; margin-bottom: 8px; line-height: 1.6; }
|
||||
.prose-wraith ol, .prose-wraith ul { font-size: 12px; color: #8b949e; margin-bottom: 8px; padding-left: 20px; }
|
||||
.prose-wraith li { margin-bottom: 4px; line-height: 1.5; }
|
||||
.prose-wraith code { background: #161b22; border: 1px solid #30363d; border-radius: 4px; padding: 1px 5px; font-size: 11px; color: #e0e0e0; }
|
||||
.prose-wraith pre { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 14px; font-size: 11px; color: #e0e0e0; overflow-x: auto; margin-bottom: 8px; font-family: 'Cascadia Mono', monospace; }
|
||||
.prose-wraith kbd { background: #21262d; border: 1px solid #484f58; border-radius: 3px; padding: 1px 5px; font-size: 10px; color: #e0e0e0; }
|
||||
.prose-wraith table { width: 100%; font-size: 12px; border-collapse: collapse; margin-bottom: 12px; }
|
||||
.prose-wraith th { text-align: left; padding: 6px 10px; background: #161b22; color: #8b949e; font-weight: 500; border-bottom: 1px solid #30363d; }
|
||||
.prose-wraith td { padding: 5px 10px; color: #e0e0e0; border-bottom: 1px solid #21262d; }
|
||||
.prose-wraith strong { color: #e0e0e0; }
|
||||
</style>
|
||||
@ -13,6 +13,7 @@
|
||||
<FileEditor v-else-if="tool === 'editor'" :session-id="sessionId" />
|
||||
<SshKeyGen v-else-if="tool === 'ssh-keygen'" />
|
||||
<PasswordGen v-else-if="tool === 'password-gen'" />
|
||||
<HelpWindow v-else-if="tool === 'help'" />
|
||||
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
|
||||
Unknown tool: {{ tool }}
|
||||
</div>
|
||||
@ -33,6 +34,7 @@ import DockerPanel from "./DockerPanel.vue";
|
||||
import FileEditor from "./FileEditor.vue";
|
||||
import SshKeyGen from "./SshKeyGen.vue";
|
||||
import PasswordGen from "./PasswordGen.vue";
|
||||
import HelpWindow from "./HelpWindow.vue";
|
||||
|
||||
defineProps<{
|
||||
tool: string;
|
||||
|
||||
@ -141,6 +141,47 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help menu -->
|
||||
<div class="relative">
|
||||
<button
|
||||
class="text-xs text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer px-2 py-1 rounded hover:bg-[var(--wraith-bg-tertiary)]"
|
||||
@click="showHelpMenu = !showHelpMenu"
|
||||
@blur="closeHelpMenuDeferred"
|
||||
>
|
||||
Help
|
||||
</button>
|
||||
<div
|
||||
v-if="showHelpMenu"
|
||||
class="absolute top-full left-0 mt-0.5 w-56 bg-[#161b22] border border-[#30363d] rounded-lg shadow-2xl overflow-hidden z-50 py-1"
|
||||
>
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@mousedown.prevent="handleHelpAction('guide')"
|
||||
>
|
||||
<span class="flex-1">Getting Started</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@mousedown.prevent="handleHelpAction('shortcuts')"
|
||||
>
|
||||
<span class="flex-1">Keyboard Shortcuts</span>
|
||||
</button>
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@mousedown.prevent="handleHelpAction('mcp')"
|
||||
>
|
||||
<span class="flex-1">MCP Integration</span>
|
||||
</button>
|
||||
<div class="border-t border-[#30363d] my-1" />
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-4 py-2 text-xs text-left text-[var(--wraith-text-secondary)] hover:bg-[#30363d] hover:text-[var(--wraith-text-primary)] transition-colors cursor-pointer"
|
||||
@mousedown.prevent="handleHelpAction('about')"
|
||||
>
|
||||
<span class="flex-1">About Wraith</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Connect -->
|
||||
@ -309,6 +350,7 @@ const sessionContainer = ref<InstanceType<typeof SessionContainer> | null>(null)
|
||||
|
||||
const showFileMenu = ref(false);
|
||||
const showToolsMenu = ref(false);
|
||||
const showHelpMenu = ref(false);
|
||||
|
||||
function closeFileMenuDeferred(): void {
|
||||
setTimeout(() => { showFileMenu.value = false; }, 150);
|
||||
@ -318,6 +360,24 @@ function closeToolsMenuDeferred(): void {
|
||||
setTimeout(() => { showToolsMenu.value = false; }, 150);
|
||||
}
|
||||
|
||||
function closeHelpMenuDeferred(): void {
|
||||
setTimeout(() => { showHelpMenu.value = false; }, 150);
|
||||
}
|
||||
|
||||
async function handleHelpAction(page: string): Promise<void> {
|
||||
showHelpMenu.value = false;
|
||||
const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
|
||||
const label = `help-${page}-${Date.now()}`;
|
||||
new WebviewWindow(label, {
|
||||
title: `Wraith — Help`,
|
||||
width: 750,
|
||||
height: 600,
|
||||
resizable: true,
|
||||
center: true,
|
||||
url: `index.html#/tool/help?page=${page}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleToolAction(tool: string): Promise<void> {
|
||||
showToolsMenu.value = false;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user