feat: copilot QoL batch — resizable, SFTP, context, errors, presets
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 15s
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Failing after 15s
Resizable panel: - Drag handle on left border, pointer events, 320px–1200px range SFTP MCP tools: - sftp_list, sftp_read, sftp_write — full HTTP endpoints + bridge tools - SftpService now Clone for MCP server sharing Active session context: - mcp_get_session_context — last 20 lines of any session's scrollback Error watcher: - Background scanner every 2s across all sessions - 20+ patterns: permission denied, OOM, segfault, disk full, etc. - mcp:error events emitted to frontend - Sessions auto-registered on SSH connect Configurable launch presets: - Settings → AI Copilot section with preset editor - Name + command pairs, stored in settings table as JSON - One-click preset buttons in copilot panel empty state - Defaults: Claude Code, Gemini CLI, Codex CLI - User can set custom commands (e.g. claude --dangerously-skip-permissions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc608b0683
commit
216cd0cf34
@ -54,15 +54,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state with quick-launch presets -->
|
||||||
<div v-else class="flex-1 flex flex-col items-center justify-center gap-2 p-4">
|
<div v-else class="flex-1 flex flex-col items-center justify-center gap-3 p-4">
|
||||||
<p class="text-xs text-[var(--wraith-text-muted)] text-center">
|
<p class="text-xs text-[var(--wraith-text-muted)] text-center">
|
||||||
Select a shell and click Launch to start a local terminal.
|
Select a shell and click Launch, or use a preset:
|
||||||
</p>
|
</p>
|
||||||
|
<div v-if="presets.length" class="flex flex-col gap-1.5 w-full max-w-[200px]">
|
||||||
|
<button
|
||||||
|
v-for="preset in presets"
|
||||||
|
:key="preset.name"
|
||||||
|
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-bg-tertiary)] border border-[var(--wraith-border)] text-[var(--wraith-text-secondary)] hover:text-[var(--wraith-text-primary)] hover:border-[var(--wraith-accent-blue)] transition-colors cursor-pointer text-left"
|
||||||
|
@click="launchPreset(preset)"
|
||||||
|
>
|
||||||
|
{{ preset.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p class="text-[10px] text-[var(--wraith-text-muted)] text-center">
|
<p class="text-[10px] text-[var(--wraith-text-muted)] text-center">
|
||||||
Run <code class="text-[var(--wraith-accent-blue)]">claude</code>,
|
Configure presets in Settings → AI Copilot
|
||||||
<code class="text-[var(--wraith-accent-blue)]">gemini</code>, or
|
|
||||||
<code class="text-[var(--wraith-accent-blue)]">codex</code> here.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -100,6 +108,10 @@ function startResize(e: PointerEvent): void {
|
|||||||
document.addEventListener("pointerup", onUp);
|
document.addEventListener("pointerup", onUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LaunchPreset { name: string; shell: string; command: string; }
|
||||||
|
|
||||||
|
const presets = ref<LaunchPreset[]>([]);
|
||||||
|
|
||||||
const shells = ref<ShellInfo[]>([]);
|
const shells = ref<ShellInfo[]>([]);
|
||||||
const selectedShell = ref("");
|
const selectedShell = ref("");
|
||||||
const connected = ref(false);
|
const connected = ref(false);
|
||||||
@ -121,6 +133,37 @@ async function loadShells(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadPresets(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await invoke<string | null>("get_setting", { key: "copilot_presets" });
|
||||||
|
if (raw) {
|
||||||
|
presets.value = JSON.parse(raw);
|
||||||
|
} else {
|
||||||
|
// Seed with sensible defaults
|
||||||
|
presets.value = [
|
||||||
|
{ name: "Claude Code", shell: "", command: "claude" },
|
||||||
|
{ name: "Gemini CLI", shell: "", command: "gemini" },
|
||||||
|
{ name: "Codex CLI", shell: "", command: "codex" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
presets.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function launchPreset(preset: LaunchPreset): Promise<void> {
|
||||||
|
const shell = preset.shell || selectedShell.value;
|
||||||
|
if (!shell) return;
|
||||||
|
selectedShell.value = shell;
|
||||||
|
await launch();
|
||||||
|
// After shell spawns, send the preset command
|
||||||
|
if (sessionId && connected.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
invoke("pty_write", { sessionId, data: preset.command + "\n" }).catch(() => {});
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function launch(): Promise<void> {
|
async function launch(): Promise<void> {
|
||||||
if (!selectedShell.value) return;
|
if (!selectedShell.value) return;
|
||||||
sessionEnded.value = false;
|
sessionEnded.value = false;
|
||||||
@ -184,7 +227,10 @@ function cleanup(): void {
|
|||||||
sessionId = "";
|
sessionId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadShells);
|
onMounted(() => {
|
||||||
|
loadShells();
|
||||||
|
loadPresets();
|
||||||
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (connected.value) kill();
|
if (connected.value) kill();
|
||||||
|
|||||||
@ -154,6 +154,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- AI Copilot -->
|
||||||
|
<template v-if="activeSection === 'copilot'">
|
||||||
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">Launch Presets</h4>
|
||||||
|
<p class="text-[10px] text-[var(--wraith-text-muted)] mb-3">
|
||||||
|
Configure quick-launch buttons for the AI copilot panel. Each preset spawns a shell and runs the command.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(preset, idx) in copilotPresets"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="preset.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
class="w-24 px-2 py-1 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)]"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="preset.command"
|
||||||
|
type="text"
|
||||||
|
placeholder="Command (e.g. claude --dangerously-skip-permissions)"
|
||||||
|
class="flex-1 px-2 py-1 text-xs rounded bg-[#0d1117] border border-[#30363d] text-[var(--wraith-text-primary)] outline-none focus:border-[var(--wraith-accent-blue)] font-mono"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="text-[var(--wraith-text-muted)] hover:text-[var(--wraith-accent-red)] transition-colors cursor-pointer text-sm"
|
||||||
|
@click="copilotPresets.splice(idx, 1)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-xs rounded border border-[#30363d] text-[var(--wraith-text-secondary)] hover:bg-[#30363d] transition-colors cursor-pointer"
|
||||||
|
@click="copilotPresets.push({ name: '', shell: '', command: '' })"
|
||||||
|
>
|
||||||
|
+ Add Preset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1.5 text-xs rounded bg-[var(--wraith-accent-blue)] text-black font-bold transition-colors cursor-pointer"
|
||||||
|
@click="saveCopilotPresets"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- About -->
|
<!-- About -->
|
||||||
<template v-if="activeSection === 'about'">
|
<template v-if="activeSection === 'about'">
|
||||||
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">About</h4>
|
<h4 class="text-xs font-semibold text-[var(--wraith-text-muted)] uppercase tracking-wider mb-3">About</h4>
|
||||||
@ -223,10 +273,13 @@ import { ref, watch, onMounted } from "vue";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { open as shellOpen } from "@tauri-apps/plugin-shell";
|
import { open as shellOpen } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
type Section = "general" | "terminal" | "vault" | "about";
|
type Section = "general" | "terminal" | "vault" | "copilot" | "about";
|
||||||
|
|
||||||
|
interface CopilotPreset { name: string; shell: string; command: string; }
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const activeSection = ref<Section>("general");
|
const activeSection = ref<Section>("general");
|
||||||
|
const copilotPresets = ref<CopilotPreset[]>([]);
|
||||||
const currentVersion = ref("loading...");
|
const currentVersion = ref("loading...");
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
@ -245,6 +298,11 @@ const sections = [
|
|||||||
label: "Vault",
|
label: "Vault",
|
||||||
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M4 4v2h-.25A1.75 1.75 0 0 0 2 7.75v5.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0 0 14 13.25v-5.5A1.75 1.75 0 0 0 12.25 6H12V4a4 4 0 1 0-8 0Zm6.5 2V4a2.5 2.5 0 0 0-5 0v2ZM8 9.5a1.5 1.5 0 0 1 .5 2.915V13.5a.5.5 0 0 1-1 0v-1.085A1.5 1.5 0 0 1 8 9.5Z"/></svg>`,
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M4 4v2h-.25A1.75 1.75 0 0 0 2 7.75v5.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0 0 14 13.25v-5.5A1.75 1.75 0 0 0 12.25 6H12V4a4 4 0 1 0-8 0Zm6.5 2V4a2.5 2.5 0 0 0-5 0v2ZM8 9.5a1.5 1.5 0 0 1 .5 2.915V13.5a.5.5 0 0 1-1 0v-1.085A1.5 1.5 0 0 1 8 9.5Z"/></svg>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "copilot" as const,
|
||||||
|
label: "AI Copilot",
|
||||||
|
icon: `<svg viewBox="0 0 16 16" fill="currentColor" class="w-3.5 h-3.5"><path d="M5.5 8.5 9 5l-2-.5L4 7.5l1.5 1ZM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Z"/></svg>`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "about" as const,
|
id: "about" as const,
|
||||||
label: "About",
|
label: "About",
|
||||||
@ -326,6 +384,33 @@ watch(
|
|||||||
function open(): void {
|
function open(): void {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
activeSection.value = "general";
|
activeSection.value = "general";
|
||||||
|
loadCopilotPresets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCopilotPresets(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await invoke<string | null>("get_setting", { key: "copilot_presets" });
|
||||||
|
if (raw) {
|
||||||
|
copilotPresets.value = JSON.parse(raw);
|
||||||
|
} else {
|
||||||
|
copilotPresets.value = [
|
||||||
|
{ name: "Claude Code", shell: "", command: "claude" },
|
||||||
|
{ name: "Gemini CLI", shell: "", command: "gemini" },
|
||||||
|
{ name: "Codex CLI", shell: "", command: "codex" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
copilotPresets.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCopilotPresets(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const json = JSON.stringify(copilotPresets.value.filter(p => p.name && p.command));
|
||||||
|
await invoke("set_setting", { key: "copilot_presets", value: json });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save copilot presets:", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): void {
|
function close(): void {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user