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 }