diff --git a/Makefile b/Makefile index 763a846..d23fd8f 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift index 94dea4f..151a2b1 100644 --- a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift +++ b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift @@ -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 """# diff --git a/Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift b/Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift index cad9184..348d495 100644 --- a/Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift +++ b/Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift @@ -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. diff --git a/Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift b/Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift index 1531bc7..4ed6eb7 100644 --- a/Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift +++ b/Sources/Shellraiser/Infrastructure/Ghostty/LibghosttySurfaceView.swift @@ -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, @@ -73,6 +66,7 @@ final class LibghosttySurfaceView: NSView, NSTextInputClient, NSMenuItemValidati terminalConfig: terminalConfig ) updateScaleAndSize() + applyGhosttyBackgroundStyle() } @available(*, unavailable) @@ -456,8 +450,8 @@ final class LibghosttySurfaceView: NSView, NSTextInputClient, NSMenuItemValidati onChildExited: onChildExited, onProgressReport: onProgressReport ) - applyGhosttyBackgroundStyle() updateScaleAndSize() + applyGhosttyBackgroundStyle() } /// Releases any owned libghostty surface resources. diff --git a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift index d205b27..0894c62 100644 --- a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift +++ b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift @@ -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() diff --git a/Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift b/Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift index e86407b..a824785 100644 --- a/Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift +++ b/Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift @@ -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", @@ -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 ", @@ -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.