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
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ ISOLATED_APP_PATH := $(ISOLATED_DERIVED_DATA)/Build/Products/$(CONFIGURATION)/$(

build-app:
xcodebuild $(BUILD_FLAGS) -derivedDataPath $(DERIVED_DATA) build
@test -d ghostty/zig-out/share/ghostty || (echo "Error: ghostty/zig-out/share/ghostty not found. Run 'zig build' in the ghostty directory first." && exit 1)
@test -d ghostty/zig-out/share/terminfo || (echo "Error: ghostty/zig-out/share/terminfo not found. Run 'zig build' in the ghostty directory first." && exit 1)
rm -rf $(APP_PATH)/Contents/Resources/ghostty $(APP_PATH)/Contents/Resources/terminfo
cp -R ghostty/zig-out/share/ghostty $(APP_PATH)/Contents/Resources/ghostty
cp -R ghostty/zig-out/share/terminfo $(APP_PATH)/Contents/Resources/terminfo

build-isolated:
xcodebuild $(ISOLATED_BUILD_FLAGS) -derivedDataPath $(ISOLATED_DERIVED_DATA) build
@test -d ghostty/zig-out/share/ghostty || (echo "Error: ghostty/zig-out/share/ghostty not found. Run 'zig build' in the ghostty directory first." && exit 1)
@test -d ghostty/zig-out/share/terminfo || (echo "Error: ghostty/zig-out/share/terminfo not found. Run 'zig build' in the ghostty directory first." && exit 1)
rm -rf $(ISOLATED_APP_PATH)/Contents/Resources/ghostty $(ISOLATED_APP_PATH)/Contents/Resources/terminfo
cp -R ghostty/zig-out/share/ghostty $(ISOLATED_APP_PATH)/Contents/Resources/ghostty
cp -R ghostty/zig-out/share/terminfo $(ISOLATED_APP_PATH)/Contents/Resources/terminfo

run: build-app
open $(APP_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,10 @@ final class AgentRuntimeBridge: AgentRuntimeSupporting {
source "$HOME/.zshrc"
fi

if [[ -n "$GHOSTTY_RESOURCES_DIR" ]] && [[ -r "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration" ]]; then
source "$GHOSTTY_RESOURCES_DIR/shell-integration/zsh/ghostty-integration"
fi

export PATH="${SHELLRAISER_WRAPPER_BIN}:${PATH:-${SHELLRAISER_ORIGINAL_PATH}}"
export SHELLRAISER_EVENT_LOG SHELLRAISER_SURFACE_ID SHELLRAISER_HELPER_PATH SHELLRAISER_REAL_CLAUDE SHELLRAISER_REAL_CODEX SHELLRAISER_WRAPPER_BIN SHELLRAISER_ORIGINAL_PATH
"""#
Expand Down
14 changes: 6 additions & 8 deletions Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1164,22 +1164,20 @@ final class GhosttyRuntime {
}

/// Creates the shell command passed into Ghostty surface creation from normalized inputs.
///
/// Working directory and login-shell setup are handled natively by Ghostty via
/// `config.working_directory` and its `login(1)` integration, so no `/bin/sh -lc`
/// wrapper is needed here. Passing the shell directly lets Ghostty's `detectShell()`
/// recognise the executable and activate shell integration.
private static func launchCommand(
shell: String,
workingDirectory: String?,
executable: (command: String, arguments: [String])?
) -> String {
let executableInvocation = escapedExecutableInvocation(
escapedExecutableInvocation(
command: executable?.command ?? shell,
arguments: executable?.arguments ?? []
)

guard let workingDirectory else {
return executableInvocation
}

let script = "cd -- \(workingDirectory.shellEscaped) || exit $?; exec \(executableInvocation)"
return "\("/bin/sh".shellEscaped) -lc \(script.shellEscaped)"
}

/// Resolves the executable used to reopen a managed agent session when its resume artifacts exist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,6 @@ final class LibghosttySurfaceView: NSView, NSTextInputClient, NSMenuItemValidati
self.onProgressReport = onProgressReport
super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600))

wantsLayer = true
let metalLayer = CAMetalLayer()
metalLayer.isOpaque = true
layer = metalLayer
layer?.isOpaque = true
applyGhosttyBackgroundStyle()

GhosttyRuntime.shared.registerSurfaceCallbacks(
surfaceId: surfaceModel.id,
onIdleNotification: onIdleNotification,
Expand All @@ -73,6 +66,7 @@ final class LibghosttySurfaceView: NSView, NSTextInputClient, NSMenuItemValidati
terminalConfig: terminalConfig
)
updateScaleAndSize()
applyGhosttyBackgroundStyle()
}

@available(*, unavailable)
Expand Down Expand Up @@ -456,8 +450,8 @@ final class LibghosttySurfaceView: NSView, NSTextInputClient, NSMenuItemValidati
onChildExited: onChildExited,
onProgressReport: onProgressReport
)
applyGhosttyBackgroundStyle()
updateScaleAndSize()
applyGhosttyBackgroundStyle()
}

/// Releases any owned libghostty surface resources.
Expand Down
18 changes: 18 additions & 0 deletions Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ final class AgentRuntimeBridgeTests: XCTestCase {
XCTAssertTrue(codexWrapperContents.contains("wait \"$monitor_pid\" 2>/dev/null || true"))
}

/// Verifies the zsh shim sources Ghostty shell integration when the runtime is active.
///
/// The `.zshrc` shim must source `ghostty-integration` from `$GHOSTTY_RESOURCES_DIR`
/// so that Ghostty's shell-integration features (title, CWD, marks) work inside
/// Shellraiser-managed surfaces.
func testPrepareRuntimeSupportWritesZshRcShimWithGhosttyIntegrationSourcing() throws {
let bridge = try makeBridge()
let zshRcURL = bridge.zshShimDirectory.appendingPathComponent(".zshrc")

bridge.prepareRuntimeSupport()

let zshRcContents = try String(contentsOf: zshRcURL, encoding: .utf8)

XCTAssertTrue(zshRcContents.contains("GHOSTTY_RESOURCES_DIR"))
XCTAssertTrue(zshRcContents.contains("ghostty-integration"))
XCTAssertTrue(zshRcContents.contains("shell-integration/zsh/ghostty-integration"))
}

/// Verifies the helper can extract Claude hook session identifiers from stdin payloads.
func testPrepareRuntimeSupportWritesHelperWithClaudeHookSessionParsing() throws {
let bridge = try makeBridge()
Expand Down
21 changes: 14 additions & 7 deletions Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import XCTest
/// Covers terminal launch command construction for Ghostty surfaces.
@MainActor
final class GhosttyRuntimeCommandTests: XCTestCase {
/// Verifies Shellraiser wraps the requested shell with an explicit directory change.
/// Verifies Shellraiser passes the shell directly without a working-directory wrapper.
///
/// Working directory is delegated to Ghostty via `config.working_directory`; no
/// `/bin/sh -lc "cd …"` wrapper is needed.
func testLaunchCommandWrapsShellWithWorkingDirectoryChange() {
let config = TerminalPanelConfig(
workingDirectory: "/tmp/project",
Expand All @@ -14,13 +17,16 @@ final class GhosttyRuntimeCommandTests: XCTestCase {
)

let command = GhosttyRuntime.launchCommand(for: config)
XCTAssertTrue(command.contains("/bin/sh"))
XCTAssertTrue(command.contains("cd --"))
XCTAssertTrue(command.contains("/tmp/project"))
XCTAssertTrue(command.contains("/bin/zsh"))
XCTAssertEqual(command, "'/bin/zsh'")
XCTAssertFalse(command.contains("/bin/sh"))
XCTAssertFalse(command.contains("cd --"))
XCTAssertFalse(command.contains("/tmp/project"))
}

/// Verifies working-directory wrapper preserves significant leading and trailing spaces.
/// Verifies working directory is not embedded in the launch command regardless of its value.
///
/// Ghostty receives the working directory via its own config channel; the launch command
/// only carries the shell path.
func testLaunchCommandPreservesUntrimmedWorkingDirectoryInWrapper() {
let config = TerminalPanelConfig(
workingDirectory: " /tmp/project with spaces ",
Expand All @@ -29,7 +35,8 @@ final class GhosttyRuntimeCommandTests: XCTestCase {
)

let command = GhosttyRuntime.launchCommand(for: config)
XCTAssertTrue(command.contains(" /tmp/project with spaces "))
XCTAssertEqual(command, "'/bin/zsh'")
XCTAssertFalse(command.contains("/tmp/project with spaces"))
}

/// Verifies whitespace-only working directories are treated as unspecified.
Expand Down
Loading