fix(wrapper): follow the shell working directory in the wrapper process (#129)

Fixes #124.

Since #78 the shell reports `IRIS_CWD` on `chpwd`, but the wrapper only
passes it to
`spec.SetCWD` for suggestions. Its own working directory never moves, so
tools that
read the pane's foreground process to locate the shell resolve the wrong
path after a
`cd`. I hit this with herdr, which stopped detecting the git repository
of a pane once
IRIS was in it. tmux uses the same lookup for `pane_current_path`.

The absolute path check matches what `spec.SetCWD` already does, so both
treat a
malformed value the same way. The `os.Chdir` error is dropped because
the path comes
from the shell integration, the user cannot act on the failure, and
keeping the last
known directory is better than stopping the loop.

`go test ./...` passes. I tested this end to end on macOS only: the
wrapper followed
the `cd`, and herdr picked the repository back up.

One limitation: the outer launcher process keeps its start directory,
only the wrapper
running `runWrapper` follows. That was enough for herdr, which reads the
wrapper.
This commit is contained in:
Cedric
2026-08-06 19:36:48 +07:00
committed by GitHub
parent fc1634b007
commit 4a4ab3c102
2 changed files with 57 additions and 0 deletions
+8
View File
@@ -117,6 +117,13 @@ func restoreTerminal() {
} }
} }
func syncProcessCWD(cwd string) {
if !filepath.IsAbs(cwd) {
return
}
_ = os.Chdir(cwd)
}
// runWrapper sets up the pty environment, launches the shell, // runWrapper sets up the pty environment, launches the shell,
// and manages the main input loop to provide real-time suggestions // and manages the main input loop to provide real-time suggestions
// it handles raw terminal mode to intercept keystrokes and // it handles raw terminal mode to intercept keystrokes and
@@ -494,6 +501,7 @@ func runWrapper() {
if cwd, ok := strings.CutPrefix(query, "IRIS_CWD:"); ok { if cwd, ok := strings.CutPrefix(query, "IRIS_CWD:"); ok {
spec.SetCWD(cwd) spec.SetCWD(cwd)
syncProcessCWD(cwd)
continue continue
} }
+49
View File
@@ -0,0 +1,49 @@
package root
import (
"os"
"path/filepath"
"testing"
)
func wantDir(t *testing.T, path string) string {
t.Helper()
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
t.Fatal(err)
}
return resolved
}
func currentDir(t *testing.T) string {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return wantDir(t, cwd)
}
func TestSyncProcessCWDFollowsShell(t *testing.T) {
shellDir := t.TempDir()
t.Chdir(t.TempDir())
syncProcessCWD(shellDir)
if got := currentDir(t); got != wantDir(t, shellDir) {
t.Fatalf("os.Getwd() = %q, want %q", got, wantDir(t, shellDir))
}
}
func TestSyncProcessCWDKeepsDirectoryOnBadPath(t *testing.T) {
launcherDir := t.TempDir()
t.Chdir(launcherDir)
for _, cwd := range []string{"", "relative/path", filepath.Join(launcherDir, "does-not-exist")} {
syncProcessCWD(cwd)
if got := currentDir(t); got != wantDir(t, launcherDir) {
t.Fatalf("syncProcessCWD(%q) moved the process to %q, want %q", cwd, got, wantDir(t, launcherDir))
}
}
}