From bce77e0932e73f23c9bfd7c084844da3a9598e52 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Tue, 17 Mar 2026 14:00:24 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20wire=20SFTP=20CWD=20following=20?= =?UTF-8?q?=E2=80=94=20listen=20for=20OSC=207=20events=20+=20inject=20shel?= =?UTF-8?q?l=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two missing halves of CWD tracking: 1. Frontend: useSftp now listens for ssh:cwd:{sessionId} Wails events and calls navigateTo() when followTerminal is enabled (default: on). 2. Backend: re-added shell integration injection with stty -echo to suppress visible command output. Leading space keeps it out of shell history. Handles both bash (PROMPT_COMMAND) and zsh (precmd). Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/composables/useSftp.ts | 28 +++++++++++++++++++++++++--- internal/ssh/service.go | 21 +++++++++++++++++---- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/frontend/src/composables/useSftp.ts b/frontend/src/composables/useSftp.ts index 4e12b91..0f59338 100644 --- a/frontend/src/composables/useSftp.ts +++ b/frontend/src/composables/useSftp.ts @@ -1,5 +1,5 @@ -import { ref, type Ref } from "vue"; -import { Call } from "@wailsio/runtime"; +import { ref, onBeforeUnmount, type Ref } from "vue"; +import { Call, Events } from "@wailsio/runtime"; const SFTP = "github.com/vstockwell/wraith/internal/sftp.SFTPService"; @@ -30,7 +30,7 @@ export function useSftp(sessionId: string): UseSftpReturn { const currentPath = ref("/"); const entries = ref([]); const isLoading = ref(false); - const followTerminal = ref(false); + const followTerminal = ref(true); async function listDirectory(path: string): Promise { try { @@ -66,6 +66,28 @@ export function useSftp(sessionId: string): UseSftpReturn { await navigateTo(currentPath.value); } + // Listen for CWD changes from the Go backend (OSC 7 tracking) + const cleanupCwd = Events.On(`ssh:cwd:${sessionId}`, (event: any) => { + if (!followTerminal.value) return; + let newPath: string; + if (typeof event === "string") { + newPath = event; + } else if (event?.data && typeof event.data === "string") { + newPath = event.data; + } else if (Array.isArray(event?.data)) { + newPath = String(event.data[0] ?? ""); + } else { + return; + } + if (newPath && newPath !== currentPath.value) { + navigateTo(newPath); + } + }); + + onBeforeUnmount(() => { + if (cleanupCwd) cleanupCwd(); + }); + // Load home directory on init navigateTo("/home"); diff --git a/internal/ssh/service.go b/internal/ssh/service.go index d60677e..db81178 100644 --- a/internal/ssh/service.go +++ b/internal/ssh/service.go @@ -142,10 +142,23 @@ func (s *SSHService) Connect(hostname string, port int, username string, authMet // Launch goroutine to read stdout and forward data via the output handler go s.readLoop(sessionID, stdout) - // CWD tracking via OSC 7 is handled passively — the CWDTracker in readLoop - // parses OSC 7 sequences if the remote shell already emits them. Automatic - // PROMPT_COMMAND injection is deferred until we have a non-echoing mechanism - // (e.g., writing to a second SSH channel or modifying .bashrc). + // Inject shell integration for CWD tracking (OSC 7). + // Uses stty -echo to suppress the command from appearing in the terminal, + // then restores echo. A leading space keeps it out of shell history. + go func() { + time.Sleep(500 * time.Millisecond) + // Suppress echo, set PROMPT_COMMAND for bash (zsh uses precmd), + // restore echo, then clear the current line so no visual artifact remains. + injection := " stty -echo 2>/dev/null; " + + ShellIntegrationCommand("bash") + "; " + + "if [ -n \"$ZSH_VERSION\" ]; then " + ShellIntegrationCommand("zsh") + "; fi; " + + "stty echo 2>/dev/null\n" + sshSession.mu.Lock() + if sshSession.Stdin != nil { + _, _ = sshSession.Stdin.Write([]byte(injection)) + } + sshSession.mu.Unlock() + }() return sessionID, nil }