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>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="flex-1 flex flex-col items-center justify-center gap-2 p-4">
|
||||
<!-- Empty state with quick-launch presets -->
|
||||
<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">
|
||||
Select a shell and click Launch to start a local terminal.
|
||||
Select a shell and click Launch, or use a preset:
|
||||
</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">
|
||||
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.
|
||||
Configure presets in Settings → AI Copilot
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,6 +108,10 @@ function startResize(e: PointerEvent): void {
|
||||
document.addEventListener("pointerup", onUp);
|
||||
}
|
||||
|
||||
interface LaunchPreset { name: string; shell: string; command: string; }
|
||||
|
||||
const presets = ref<LaunchPreset[]>([]);
|
||||
|
||||
const shells = ref<ShellInfo[]>([]);
|
||||
const selectedShell = ref("");
|
||||
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> {
|
||||
if (!selectedShell.value) return;
|
||||
sessionEnded.value = false;
|
||||
@ -184,7 +227,10 @@ function cleanup(): void {
|
||||
sessionId = "";
|
||||
}
|
||||
|
||||
onMounted(loadShells);
|
||||
onMounted(() => {
|
||||
loadShells();
|
||||
loadPresets();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (connected.value) kill();
|
||||
|
||||
@ -154,6 +154,56 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<template v-if="activeSection === 'about'">
|
||||
<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 { 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 activeSection = ref<Section>("general");
|
||||
const copilotPresets = ref<CopilotPreset[]>([]);
|
||||
const currentVersion = ref("loading...");
|
||||
|
||||
const sections = [
|
||||
@ -245,6 +298,11 @@ const sections = [
|
||||
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>`,
|
||||
},
|
||||
{
|
||||
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,
|
||||
label: "About",
|
||||
@ -326,6 +384,33 @@ watch(
|
||||
function open(): void {
|
||||
visible.value = true;
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user