All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m4s
Four-agent parallel deployment: 1. Settings persistence — all 5 settings wired to SettingsService.Set/Get, theme picker persists, update check calls real UpdateService, external links use Browser.OpenURL, SFTP file open/save calls real service, Quick Connect creates real connection + session, exit uses Wails quit 2. SSH key management — credential dropdown in ConnectionEditDialog, collapsible "Add New Credential" panel with password/SSH key modes, CredentialService proxied through WraithApp (vault-locked guard), new CreateSSHKeyCredential method for atomic key+credential creation 3. RDP frontend wiring — useRdp.ts calls real RDPGetFrame/SendMouse/ SendKey/SendClipboard via Wails bindings, ConnectRDP on WraithApp resolves credentials and builds RDPConfig, session store handles RDP protocol, frame pipeline uses polling at 30fps 4. FreeRDP3 callback registration — PostConnect and BitmapUpdate callbacks via syscall.NewCallback, GDI mode for automatic frame decoding, freerdp_context_new() call added, settings/input/context pointers extracted from struct offsets, BGRA→RGBA channel swap in frame copy, event loop fixed to pass context not instance 11 files changed. Zero build errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
6.9 KiB
Vue
235 lines
6.9 KiB
Vue
<template>
|
|
<div class="flex flex-col border-b border-[var(--wraith-border)] bg-[var(--wraith-bg-secondary)]" :style="{ height: editorHeight + 'px' }">
|
|
<!-- Editor toolbar -->
|
|
<div class="flex items-center justify-between px-3 py-1.5 border-b border-[var(--wraith-border)] shrink-0">
|
|
<div class="flex items-center gap-2 text-xs min-w-0">
|
|
<!-- File icon -->
|
|
<svg class="w-3.5 h-3.5 text-[var(--wraith-text-muted)] shrink-0" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.75 1.5a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25V6H9.75A1.75 1.75 0 0 1 8 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25V1.75z" />
|
|
</svg>
|
|
|
|
<span class="text-[var(--wraith-text-primary)] truncate font-mono text-[11px]">
|
|
{{ filePath }}
|
|
</span>
|
|
|
|
<!-- Unsaved indicator -->
|
|
<span v-if="hasUnsavedChanges" class="w-2 h-2 rounded-full bg-[var(--wraith-accent-yellow)] shrink-0" title="Unsaved changes" />
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<!-- Save button -->
|
|
<button
|
|
class="px-3 py-1 text-xs rounded transition-colors cursor-pointer"
|
|
:class="
|
|
hasUnsavedChanges
|
|
? 'bg-[var(--wraith-accent-blue)] text-white hover:bg-blue-600'
|
|
: 'bg-[var(--wraith-bg-tertiary)] text-[var(--wraith-text-muted)] cursor-not-allowed'
|
|
"
|
|
:disabled="!hasUnsavedChanges"
|
|
@click="handleSave"
|
|
>
|
|
Save
|
|
</button>
|
|
|
|
<!-- Close button -->
|
|
<button
|
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer"
|
|
@click="emit('close')"
|
|
>
|
|
<svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CodeMirror container -->
|
|
<div ref="editorRef" class="flex-1 min-h-0 overflow-hidden" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
import { Call } from "@wailsio/runtime";
|
|
|
|
const SFTP = "github.com/vstockwell/wraith/internal/sftp.SFTPService";
|
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightSpecialChars } from "@codemirror/view";
|
|
import { EditorState } from "@codemirror/state";
|
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language";
|
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
|
|
const props = defineProps<{
|
|
content: string;
|
|
filePath: string;
|
|
sessionId: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
close: [];
|
|
}>();
|
|
|
|
const editorRef = ref<HTMLElement | null>(null);
|
|
const hasUnsavedChanges = ref(false);
|
|
const editorHeight = ref(300);
|
|
|
|
let view: EditorView | null = null;
|
|
|
|
/** Detect language extension from file path. */
|
|
async function getLanguageExtension(path: string) {
|
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
switch (ext) {
|
|
case "js":
|
|
case "jsx":
|
|
case "ts":
|
|
case "tsx": {
|
|
const { javascript } = await import("@codemirror/lang-javascript");
|
|
return javascript({ jsx: ext.includes("x"), typescript: ext.startsWith("t") });
|
|
}
|
|
case "json": {
|
|
const { json } = await import("@codemirror/lang-json");
|
|
return json();
|
|
}
|
|
case "py": {
|
|
const { python } = await import("@codemirror/lang-python");
|
|
return python();
|
|
}
|
|
case "md":
|
|
case "markdown": {
|
|
const { markdown } = await import("@codemirror/lang-markdown");
|
|
return markdown();
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!editorRef.value) return;
|
|
|
|
const langExt = await getLanguageExtension(props.filePath);
|
|
|
|
const extensions = [
|
|
lineNumbers(),
|
|
highlightActiveLine(),
|
|
highlightSpecialChars(),
|
|
history(),
|
|
bracketMatching(),
|
|
closeBrackets(),
|
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
oneDark,
|
|
keymap.of([
|
|
...defaultKeymap,
|
|
...historyKeymap,
|
|
...closeBracketsKeymap,
|
|
]),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
hasUnsavedChanges.value = true;
|
|
}
|
|
}),
|
|
EditorView.theme({
|
|
"&": {
|
|
height: "100%",
|
|
fontSize: "13px",
|
|
},
|
|
".cm-scroller": {
|
|
overflow: "auto",
|
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
|
|
},
|
|
}),
|
|
];
|
|
|
|
if (langExt) {
|
|
extensions.push(langExt);
|
|
}
|
|
|
|
const state = EditorState.create({
|
|
doc: props.content,
|
|
extensions,
|
|
});
|
|
|
|
view = new EditorView({
|
|
state,
|
|
parent: editorRef.value,
|
|
});
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (view) {
|
|
view.destroy();
|
|
view = null;
|
|
}
|
|
});
|
|
|
|
// Re-create editor when content/filePath changes
|
|
watch(
|
|
() => props.filePath,
|
|
async () => {
|
|
if (!view || !editorRef.value) return;
|
|
hasUnsavedChanges.value = false;
|
|
|
|
const langExt = await getLanguageExtension(props.filePath);
|
|
const extensions = [
|
|
lineNumbers(),
|
|
highlightActiveLine(),
|
|
highlightSpecialChars(),
|
|
history(),
|
|
bracketMatching(),
|
|
closeBrackets(),
|
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
oneDark,
|
|
keymap.of([
|
|
...defaultKeymap,
|
|
...historyKeymap,
|
|
...closeBracketsKeymap,
|
|
]),
|
|
EditorView.updateListener.of((update) => {
|
|
if (update.docChanged) {
|
|
hasUnsavedChanges.value = true;
|
|
}
|
|
}),
|
|
EditorView.theme({
|
|
"&": {
|
|
height: "100%",
|
|
fontSize: "13px",
|
|
},
|
|
".cm-scroller": {
|
|
overflow: "auto",
|
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
|
|
},
|
|
}),
|
|
];
|
|
|
|
if (langExt) {
|
|
extensions.push(langExt);
|
|
}
|
|
|
|
view.destroy();
|
|
|
|
const state = EditorState.create({
|
|
doc: props.content,
|
|
extensions,
|
|
});
|
|
|
|
view = new EditorView({
|
|
state,
|
|
parent: editorRef.value,
|
|
});
|
|
},
|
|
);
|
|
|
|
async function handleSave(): Promise<void> {
|
|
if (!view || !hasUnsavedChanges.value) return;
|
|
|
|
const currentContent = view.state.doc.toString();
|
|
try {
|
|
await Call.ByName(`${SFTP}.WriteFile`, props.sessionId, props.filePath, currentContent);
|
|
hasUnsavedChanges.value = false;
|
|
} catch (err) {
|
|
console.error("Failed to save file:", err);
|
|
}
|
|
}
|
|
</script>
|