Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ model exampleModel {
}
```

## Test-Driven Development

Always use TDD when developing new features. Write tests first based on the expected behavior (e.g., spec, protocol, or requirements), then write the implementation to make them pass. Writing tests after the implementation risks asserting what the code does rather than what it should do, which lets bugs slip through.

## Testing the binary

If you need to build the gsh binary for testing, you can just run `make build` or a custom build command outputting to `./bin/gsh`.
Expand Down
23 changes: 23 additions & 0 deletions internal/repl/osc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package repl

import (
"fmt"
"io"
"net/url"
"strings"
)

// emitOSC7 writes an OSC 7 escape sequence to inform the terminal of the
// current working directory. This allows terminal emulators (Terminal.app,
// iTerm2, etc.) to open new tabs/windows in the same directory.
// Format: ESC ] 7 ; file://HOSTNAME/url-encoded-path ESC \
func emitOSC7(w io.Writer, hostname, dir string) {
// Encode each path segment individually to preserve '/' separators.
// url.PathEscape encodes '/' to '%2F' which breaks the file URL.
segments := strings.Split(dir, "/")
for i, seg := range segments {
segments[i] = url.PathEscape(seg)
}
encodedPath := strings.Join(segments, "/")
fmt.Fprintf(w, "\033]7;file://%s%s\033\\", hostname, encodedPath)
}
51 changes: 51 additions & 0 deletions internal/repl/osc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package repl

import (
"bytes"
"testing"
)

func TestEmitOSC7(t *testing.T) {
tests := []struct {
name string
hostname string
dir string
expected string
}{
{
name: "normal path",
hostname: "myhost",
dir: "/Users/test/projects",
expected: "\033]7;file://myhost/Users/test/projects\033\\",
},
{
name: "path with spaces",
hostname: "myhost",
dir: "/Users/test/my projects",
expected: "\033]7;file://myhost/Users/test/my%20projects\033\\",
},
{
name: "path with hash",
hostname: "myhost",
dir: "/Users/test/project#1",
expected: "\033]7;file://myhost/Users/test/project%231\033\\",
},
{
name: "empty hostname",
hostname: "",
dir: "/Users/test",
expected: "\033]7;file:///Users/test\033\\",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
emitOSC7(&buf, tt.hostname, tt.dir)
got := buf.String()
if got != tt.expected {
t.Errorf("emitOSC7(%q, %q) = %q, want %q", tt.hostname, tt.dir, got, tt.expected)
}
})
}
}
9 changes: 9 additions & 0 deletions internal/repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ func (r *REPL) setSigintChannelFactory(factory func() (chan os.Signal, func()))
func (r *REPL) Run(ctx context.Context) error {
r.logger.Info("starting REPL")

// Cache hostname for OSC 7 escape sequences (terminal CWD tracking)
hostname, _ := os.Hostname()

// Emit repl.ready event (welcome screen is handled by event handler in defaults/events/repl.gsh)
r.executor.Interpreter().EmitEvent(interpreter.EventReplReady, interpreter.CreateReplReadyContext())

Expand Down Expand Up @@ -292,6 +295,12 @@ func (r *REPL) Run(ctx context.Context) error {
default:
}

// Emit OSC 7 to tell the terminal the current working directory
// (enables new tabs/windows to open in the same directory)
if dir := r.executor.GetPwd(); dir != "" && term.IsTerminal(int(os.Stdout.Fd())) {
emitOSC7(os.Stdout, hostname, dir)
}

// Get prompt - emits repl.prompt event internally
prompt := r.getPrompt()

Expand Down
Loading