feat: SFTP editor opens as popup window instead of inline overlay
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 6s

Right-click → Edit now opens a separate Tauri window with the file
content in a monospace editor. Ctrl+S saves back to remote via SFTP.
Tab inserts 4 spaces. Modified indicator in toolbar.

Removed the inline EditorWindow overlay that covered the terminal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-25 00:58:09 -04:00
parent 016906fc9d
commit 3e548ed10e
4 changed files with 124 additions and 13 deletions

View File

@ -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","shell:allow-open"]}} {"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"]}}

View File

@ -0,0 +1,107 @@
<template>
<div class="flex flex-col h-full bg-[#0d1117]">
<!-- Toolbar -->
<div class="flex items-center gap-2 px-3 py-2 bg-[#161b22] border-b border-[#30363d] shrink-0">
<span class="text-xs text-[#8b949e] font-mono truncate flex-1">{{ filePath }}</span>
<span v-if="modified" class="text-[10px] text-[#e3b341]">modified</span>
<button
class="px-3 py-1 text-xs font-bold rounded bg-[#238636] text-white cursor-pointer disabled:opacity-40"
:disabled="saving || !modified"
@click="save"
>
{{ saving ? "Saving..." : "Save" }}
</button>
</div>
<!-- Editor area -->
<div ref="editorContainer" class="flex-1 min-h-0" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
const props = defineProps<{
sessionId: string;
}>();
const filePath = ref("");
const content = ref("");
const modified = ref(false);
const saving = ref(false);
const editorContainer = ref<HTMLElement | null>(null);
let editorContent = "";
onMounted(async () => {
// Parse path from URL
const params = new URLSearchParams(window.location.hash.split("?")[1] || "");
filePath.value = decodeURIComponent(params.get("path") || "");
if (!filePath.value || !props.sessionId) return;
// Load file content
try {
content.value = await invoke<string>("sftp_read_file", {
sessionId: props.sessionId,
path: filePath.value,
});
editorContent = content.value;
} catch (err) {
content.value = `Error loading file: ${err}`;
}
// Create a simple textarea editor (CodeMirror can be added later)
if (editorContainer.value) {
const textarea = document.createElement("textarea");
textarea.value = content.value;
textarea.spellcheck = false;
textarea.style.cssText = `
width: 100%; height: 100%; resize: none; border: none; outline: none;
background: #0d1117; color: #e0e0e0; padding: 12px; font-size: 13px;
font-family: 'Cascadia Mono', 'Cascadia Code', Consolas, 'JetBrains Mono', monospace;
line-height: 1.5; tab-size: 4;
`;
textarea.addEventListener("input", () => {
editorContent = textarea.value;
modified.value = editorContent !== content.value;
});
textarea.addEventListener("keydown", (e) => {
// Ctrl+S to save
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
save();
}
// Tab inserts spaces
if (e.key === "Tab") {
e.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, start) + " " + textarea.value.substring(end);
textarea.selectionStart = textarea.selectionEnd = start + 4;
editorContent = textarea.value;
modified.value = true;
}
});
editorContainer.value.appendChild(textarea);
textarea.focus();
}
});
async function save(): Promise<void> {
if (!modified.value || saving.value) return;
saving.value = true;
try {
await invoke("sftp_write_file", {
sessionId: props.sessionId,
path: filePath.value,
content: editorContent,
});
content.value = editorContent;
modified.value = false;
} catch (err) {
alert(`Save failed: ${err}`);
}
saving.value = false;
}
</script>

View File

@ -9,6 +9,7 @@
<WhoisTool v-else-if="tool === 'whois'" :session-id="sessionId" /> <WhoisTool v-else-if="tool === 'whois'" :session-id="sessionId" />
<BandwidthTest v-else-if="tool === 'bandwidth'" :session-id="sessionId" /> <BandwidthTest v-else-if="tool === 'bandwidth'" :session-id="sessionId" />
<SubnetCalc v-else-if="tool === 'subnet-calc'" /> <SubnetCalc v-else-if="tool === 'subnet-calc'" />
<FileEditor v-else-if="tool === 'editor'" :session-id="sessionId" />
<SshKeyGen v-else-if="tool === 'ssh-keygen'" /> <SshKeyGen v-else-if="tool === 'ssh-keygen'" />
<PasswordGen v-else-if="tool === 'password-gen'" /> <PasswordGen v-else-if="tool === 'password-gen'" />
<div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]"> <div v-else class="flex-1 flex items-center justify-center text-sm text-[#484f58]">
@ -27,6 +28,7 @@ import DnsLookup from "./DnsLookup.vue";
import WhoisTool from "./WhoisTool.vue"; import WhoisTool from "./WhoisTool.vue";
import BandwidthTest from "./BandwidthTest.vue"; import BandwidthTest from "./BandwidthTest.vue";
import SubnetCalc from "./SubnetCalc.vue"; import SubnetCalc from "./SubnetCalc.vue";
import FileEditor from "./FileEditor.vue";
import SshKeyGen from "./SshKeyGen.vue"; import SshKeyGen from "./SshKeyGen.vue";
import PasswordGen from "./PasswordGen.vue"; import PasswordGen from "./PasswordGen.vue";

View File

@ -235,15 +235,6 @@
<!-- Tab bar --> <!-- Tab bar -->
<TabBar /> <TabBar />
<!-- Inline file editor -->
<EditorWindow
v-if="editorFile"
:content="editorFile.content"
:file-path="editorFile.path"
:session-id="editorFile.sessionId"
@close="editorFile = null"
/>
<!-- Session area --> <!-- Session area -->
<SessionContainer ref="sessionContainer" /> <SessionContainer ref="sessionContainer" />
</div> </div>
@ -384,9 +375,20 @@ function handleThemeSelect(theme: ThemeDefinition): void {
async function handleOpenFile(entry: FileEntry): Promise<void> { async function handleOpenFile(entry: FileEntry): Promise<void> {
if (!activeSessionId.value) return; if (!activeSessionId.value) return;
try { try {
const content = await invoke<string>("sftp_read_file", { sessionId: activeSessionId.value, path: entry.path }); const { WebviewWindow } = await import("@tauri-apps/api/webviewWindow");
editorFile.value = { path: entry.path, content, sessionId: activeSessionId.value }; const fileName = entry.path.split("/").pop() || entry.path;
} catch (err) { console.error("Failed to open SFTP file:", err); } const label = `editor-${Date.now()}`;
const sessionId = activeSessionId.value;
new WebviewWindow(label, {
title: `${fileName} — Wraith Editor`,
width: 800,
height: 600,
resizable: true,
center: true,
url: `index.html#/tool/editor?sessionId=${sessionId}&path=${encodeURIComponent(entry.path)}`,
});
} catch (err) { console.error("Failed to open editor:", err); }
} }
async function handleQuickConnect(): Promise<void> { async function handleQuickConnect(): Promise<void> {