Skip to content

[codex] fix remote agent ssh working directories#1119

Open
jtalborough wants to merge 6 commits into
RunMaestro:rcfrom
jtalborough:codex/fix-ssh-remote-cwd
Open

[codex] fix remote agent ssh working directories#1119
jtalborough wants to merge 6 commits into
RunMaestro:rcfrom
jtalborough:codex/fix-ssh-remote-cwd

Conversation

@jtalborough

@jtalborough jtalborough commented Jun 22, 2026

Copy link
Copy Markdown

Summary

Fixes SSH remote-agent working directory handling so remote agents use the configured remote cwd instead of a locally expanded path. This covers both the generic SSH spawn wrapper and the desktop process:spawn SSH path, plus the new/edit agent UI paths that persist remote cwd overrides.

Root Cause

The desktop spawn handler still passed the local cwd into the SSH command builder even when sessionSshRemoteConfig.workingDirOverride was set. New remote-agent creation also stored the locally expanded working directory as workingDirOverride, so inputs like ~/git-projects could become /Users/.../git-projects and fail on the remote host.

Changes

  • Honor workingDirOverride in process:spawn and wrapSpawnWithSsh.
  • Expand remote ~/... paths through $HOME when building SSH commands.
  • Preserve the raw remote working directory from the new-agent form.
  • Allow editing the remote working directory for SSH agents in EditAgentModal.
  • Add regressions for command building, spawn wrapping, process IPC, new-agent creation, and edit-agent saves.

Validation

  • git diff --check
  • npm test -- NewInstanceModal.test.tsx process.test.ts ssh-command-builder.test.ts ssh-spawn-wrapper.test.ts
  • npm run build:main && npm run build:preload && npm run build:renderer && npm run build:cli
  • Packaged and codesigned locally before this PR prep, then user confirmed the rebuilt app fixed their remote-agent launch issue.

Notes

npm run build:renderer still emits the existing Vite CSS/chunk warnings; the build exits successfully.

Summary by CodeRabbit

  • New Features

    • Added an editable Remote Working Directory for SSH-enabled sessions, with required validation and support for tilde (~) paths.
  • Bug Fixes

    • Improved SSH process:spawn so remote execution uses the resolved SSH working directory (correct ~ / $HOME expansion) and avoids leaking the local cwd.
    • Normalized SSH -C / --cwd=<path> handling to preserve directory semantics (. when appropriate).
    • Updated SSH flow to fail clearly when SSH is enabled but no remote resolves; aligned file tree and git refresh to the SSH directory.
  • Tests

    • Expanded coverage for tilde expansion, remote cwd propagation, spawn/wrapper behavior, UI edits/validation, and session migration.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5627d19-fa83-4393-9614-8472f0cea035

📥 Commits

Reviewing files that changed from the base of the PR and between affc58f and b2c31ca.

📒 Files selected for processing (4)
  • src/__tests__/main/ipc/handlers/process.test.ts
  • src/__tests__/renderer/components/NewInstanceModal.test.tsx
  • src/main/ipc/handlers/process.ts
  • src/renderer/components/NewInstanceModal/NewInstanceModal.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/components/NewInstanceModal/NewInstanceModal.tsx
  • src/main/ipc/handlers/process.ts
  • src/tests/main/ipc/handlers/process.test.ts

📝 Walkthrough

Walkthrough

SSH remote working-directory overrides now flow through renderer forms, session save/reset logic, process spawning, SSH wrapper normalization, command building, and file-tree root selection. Tests were added or updated across the same paths.

Changes

SSH remote working directory flow

Layer / File(s) Summary
Renderer remote working directory
src/renderer/components/NewInstanceModal/EditAgentModal.tsx, src/renderer/components/NewInstanceModal/NewInstanceModal.tsx, src/__tests__/renderer/components/NewInstanceModal.test.tsx, src/__tests__/renderer/components/NewInstanceModal/EditAgentModal.test.tsx
EditAgentModal and NewInstanceModal now compute, trim, validate, and persist SSH remote working-directory values. Renderer tests update tilde-path expectations and add SSH edit and validation coverage.
Session save resets SSH-derived state
src/renderer/hooks/session/useSessionLifecycle.ts, src/__tests__/renderer/hooks/useSessionLifecycle.test.ts
handleSaveEditAgent updates cwd/fullPath/remoteCwd when the SSH remote working directory changes and clears SSH, file-tree, and Git caches; the lifecycle test asserts the resulting session state resets.
Tilde-aware SSH path escaping
src/main/utils/ssh-command-builder.ts, src/__tests__/main/utils/ssh-command-builder.test.ts
shellEscapeRemotePath converts tilde-prefixed remote paths into $HOME-based forms for SSH cd commands, and both SSH command builders now use it; tests cover direct remote commands and stdin-script generation.
SSH spawn wrapper cwd normalization
src/main/utils/ssh-spawn-wrapper.ts, src/__tests__/main/utils/ssh-spawn-wrapper.test.ts
wrapSpawnWithSsh now computes remoteCwd from workingDirOverride, normalizes local cwd arguments to . for working-directory flags, and threads the normalized args and remote cwd through both SSH execution paths; wrapper tests cover remote cwd propagation and cwd-flag normalization.
process:spawn SSH cwd handling
src/main/ipc/handlers/process.ts, src/__tests__/main/ipc/handlers/process.test.ts
process:spawn now uses an SSH-specific agent cwd, resolves SSH config from sessionSshRemoteConfig, derives remoteCwd, and passes it into SSH script building and logging; the handler tests cover unresolved remote failure, SSH remote cwd rendering, and Codex cwd normalization.
SSH remote file-tree roots
src/renderer/hooks/git/useFileTreeManagement.ts, src/__tests__/renderer/hooks/useFileTreeManagement.test.ts
useFileTreeManagement now selects tree and Git roots from SSH remote cwd when available, and the SSH tests update refresh and initial-load expectations to use the remote project path.

Sequence Diagram(s)

sequenceDiagram
  participant EditAgentModal
  participant HandleSaveEditAgent
  participant ProcessSpawn
  participant WrapSpawnWithSsh
  participant BuildSshCommandWithStdin
  EditAgentModal->>HandleSaveEditAgent: Save SSH workingDirOverride
  HandleSaveEditAgent->>ProcessSpawn: Session state with remoteCwd
  ProcessSpawn->>WrapSpawnWithSsh: Spawn config and SSH settings
  WrapSpawnWithSsh->>BuildSshCommandWithStdin: remoteArgs and remoteCwd
  BuildSshCommandWithStdin->>ProcessSpawn: sshStdinScript with remote cd
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • RunMaestro/Maestro#553: Both PRs modify the SSH stdin-script/command generation path, including buildSshCommandWithStdin.
  • RunMaestro/Maestro#793: Both PRs adjust SSH remote workingDirOverride/cwd handling and remote cd generation.

Suggested labels

ready to merge

🐇 I hop through paths both near and far,
Til ~ becomes $HOME where the remote roots are.
My burrow keeps tidy, my SSH trail is clear,
With cwd in the right place, I cheer and I cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing SSH remote agent working directory handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jtalborough jtalborough force-pushed the codex/fix-ssh-remote-cwd branch from 8ac3b0b to f5cacd9 Compare June 22, 2026 13:19
@jtalborough jtalborough marked this pull request as ready for review June 22, 2026 13:45
@jtalborough jtalborough force-pushed the codex/fix-ssh-remote-cwd branch from f5cacd9 to fbba3d1 Compare June 22, 2026 13:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/utils/ssh-spawn-wrapper.ts (1)

20-29: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Handle inline working-dir flag forms in SSH arg normalization.

normalizeSshWorkingDirArgs() only rewrites two-token forms (e.g., --cwd /path). Inline forms like --cwd=/path or --working-directory=/path bypass this and can still leak local paths into remote commands.

Suggested patch
 const WORKING_DIR_VALUE_FLAGS = new Set(['-C', '--cwd', '--working-directory', '--directory']);
+const WORKING_DIR_INLINE_PREFIXES = ['--cwd=', '--working-directory=', '--directory='];

 function normalizeSshWorkingDirArgs(args: string[], localCwd: string): string[] {
 	return args.map((arg, index) => {
 		if (index > 0 && WORKING_DIR_VALUE_FLAGS.has(args[index - 1]) && arg === localCwd) {
 			return '.';
 		}
+		for (const prefix of WORKING_DIR_INLINE_PREFIXES) {
+			if (arg.startsWith(prefix) && arg.slice(prefix.length) === localCwd) {
+				return `${prefix}.`;
+			}
+		}
 		return arg;
 	});
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/utils/ssh-spawn-wrapper.ts` around lines 20 - 29, The
normalizeSshWorkingDirArgs function currently only handles two-token forms (flag
and value as separate arguments) but misses inline forms where the flag and
value are combined with an equals sign (e.g., --cwd=/path). Update the function
to also detect inline flag-value pairs by checking if the current argument
contains one of the WORKING_DIR_VALUE_FLAGS followed by an equals sign, extract
the path portion after the equals sign, and if it matches localCwd, replace it
with a dot while preserving the flag portion of the argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/main/ipc/handlers/process.test.ts`:
- Around line 2181-2184: The test assertion for the SSH stdin script is checking
for the old behavior where tilde paths remain as written (cd '~/git-projects'),
but the SSH command builder has been updated to expand tilde to the $HOME
environment variable. Update the .toContain() assertion that checks
spawnCall.sshStdinScript to expect the new format where ~/git-projects is
rendered as "$HOME"/git-projects instead of the literal tilde path.

In `@src/main/ipc/handlers/process.ts`:
- Around line 920-933: The code silently falls through when getSshRemoteConfig
returns a null config, allowing the spawn to run locally despite SSH being
explicitly enabled at the session level. After calling getSshRemoteConfig with
the sshStoreAdapter and sessionSshRemoteConfig parameters, add a validation
check to ensure sshResult.config is not null. If the config is null while SSH is
enabled (isSshRemoteSpawn is true and sessionSshRemoteConfig is present), throw
an error or log a fatal error and exit the process to fail loudly as per the
coding guidelines, rather than silently falling back to local execution.

---

Nitpick comments:
In `@src/main/utils/ssh-spawn-wrapper.ts`:
- Around line 20-29: The normalizeSshWorkingDirArgs function currently only
handles two-token forms (flag and value as separate arguments) but misses inline
forms where the flag and value are combined with an equals sign (e.g.,
--cwd=/path). Update the function to also detect inline flag-value pairs by
checking if the current argument contains one of the WORKING_DIR_VALUE_FLAGS
followed by an equals sign, extract the path portion after the equals sign, and
if it matches localCwd, replace it with a dot while preserving the flag portion
of the argument.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83988e9c-727d-463c-9c2d-7f06913443ac

📥 Commits

Reviewing files that changed from the base of the PR and between 8a4cccf and fbba3d1.

📒 Files selected for processing (12)
  • src/__tests__/main/ipc/handlers/process.test.ts
  • src/__tests__/main/utils/ssh-command-builder.test.ts
  • src/__tests__/main/utils/ssh-spawn-wrapper.test.ts
  • src/__tests__/renderer/components/NewInstanceModal.test.tsx
  • src/__tests__/renderer/components/NewInstanceModal/EditAgentModal.test.tsx
  • src/__tests__/renderer/hooks/useFileTreeManagement.test.ts
  • src/main/ipc/handlers/process.ts
  • src/main/utils/ssh-command-builder.ts
  • src/main/utils/ssh-spawn-wrapper.ts
  • src/renderer/components/NewInstanceModal/EditAgentModal.tsx
  • src/renderer/components/NewInstanceModal/NewInstanceModal.tsx
  • src/renderer/hooks/git/useFileTreeManagement.ts

Comment thread src/__tests__/main/ipc/handlers/process.test.ts
Comment thread src/main/ipc/handlers/process.ts
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes SSH remote-agent working directory handling across the spawn IPC handler, the generic spawn wrapper, and the new/edit agent UI, so that workingDirOverride (including raw ~/... tilde paths) is used for the remote cd instead of the locally-expanded path.

  • Core fix (process.ts / ssh-spawn-wrapper.ts): remoteCwd is now derived from workingDirOverride and passed to buildSshCommandWithStdin/buildSshCommand; agent working-dir args (e.g., Codex -C) are normalized to . so the local path never appears on the remote.
  • Tilde expansion (ssh-command-builder.ts): A new shellEscapeRemotePath helper converts ~/… paths to \"$HOME\"/'rest' for safe, portable remote shell expansion.
  • UI (NewInstanceModal.tsx / EditAgentModal.tsx): workingDirOverride is now persisted as the raw user input (not locally expanded); the Edit modal gains an editable "Remote Working Directory" field for SSH agents.

Confidence Score: 3/5

The spawn-path and builder fixes are correct and well-tested, but the new editable Remote Working Directory field in EditAgentModal introduces a save path where clearing the input silently persists the locally-expanded projectRoot as workingDirOverride, causing every subsequent SSH agent spawn to fail with a cd error on the remote host.

The IPC handler and ssh-command-builder changes are solid. The EditAgentModal save logic has a gap: clearing the remote dir field produces a blank workingDirOverride that silently falls back to session.projectRoot (a locally-expanded path), which breaks the next agent spawn on the remote. Until that path is guarded or the empty-field case is handled explicitly, a user editing an SSH agent could unknowingly put it in a broken state with no warning in the UI.

src/renderer/components/NewInstanceModal/EditAgentModal.tsx — the handleSave fallback from empty workingDirOverride to session.projectRoot needs attention.

Important Files Changed

Filename Overview
src/main/ipc/handlers/process.ts Adds isSshRemoteSpawn flag to route working-dir arg and SSH cwd independently; now uses workingDirOverride as remoteCwd. Core logic is correct but the fallback `
src/main/utils/ssh-command-builder.ts Adds shellEscapeRemotePath to convert ~/... paths to "$HOME"/'rest' for correct remote expansion; applied in both buildRemoteCommand and buildSshCommandWithStdin. Logic and escaping are correct.
src/main/utils/ssh-spawn-wrapper.ts Adds normalizeSshWorkingDirArgs to replace local cwd-value flags with '.' and reads remoteCwd from workingDirOverride. The added else branch correctly normalizes args when there is no prompt. No issues found.
src/renderer/components/NewInstanceModal/EditAgentModal.tsx Adds editable Remote Working Directory field for SSH agents. The handleSave fallback sshRemoteConfig.workingDirOverride?.trim()
src/renderer/components/NewInstanceModal/NewInstanceModal.tsx Correctly stores workingDirOverride as the raw trimmed working dir (not locally-expanded), fixing the root bug for new-agent creation.
src/tests/main/ipc/handlers/process.test.ts New tests verify workingDirOverride routing and cwd='.' for agent args. The mock for buildSshCommandWithStdin wraps cwd naively in single quotes, so the tilde-path assertion checks mock behavior (cd '~/git-projects') rather than real behavior (cd "$HOME"/'git-projects').
src/tests/main/utils/ssh-command-builder.test.ts Adds correct assertions for tilde path expansion in both buildRemoteCommand and buildSshCommandWithStdin; assertions match the real shellEscapeRemotePath output.
src/tests/main/utils/ssh-spawn-wrapper.test.ts New test file covering workingDirOverride routing and arg normalization for both stdin and command-line prompt paths. Good coverage.
src/tests/renderer/components/NewInstanceModal/EditAgentModal.test.tsx New test exercises the editable remote working dir field through save; correctly asserts the raw user input is persisted as workingDirOverride.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as NewInstanceModal / EditAgentModal
    participant IPC as process:spawn handler
    participant Wrapper as wrapSpawnWithSsh
    participant Builder as ssh-command-builder
    participant SSH as SSH process

    UI->>IPC: "spawn cwd=localPath workingDirOverride=~/git-projects"
    IPC->>IPC: "agentWorkingDirArgCwd = dot (SSH spawn)"
    IPC->>IPC: "buildAgentArgs with cwd=dot produces args with -C dot"
    IPC->>IPC: "remoteCwd = workingDirOverride.trim() OR config.cwd"
    IPC->>Builder: "buildSshCommandWithStdin cwd=remoteCwd"
    Builder->>Builder: shellEscapeRemotePath produces dollar HOME slash git-projects
    Builder-->>IPC: stdinScript with cd dollar HOME git-projects OR exit 1
    IPC->>SSH: spawn ssh args stdinScript
    SSH->>SSH: bash -s reads stdinScript expands HOME executes agent

    Note over UI,IPC: wrapSpawnWithSsh path used by Group Chat etc.
    UI->>Wrapper: "wrapSpawnWithSsh args include -C localPath workingDirOverride=/remote/path"
    Wrapper->>Wrapper: normalizeSshWorkingDirArgs replaces localPath with dot
    Wrapper->>Wrapper: "remoteCwd = workingDirOverride"
    Wrapper->>Builder: "buildSshCommand cwd=remoteCwd args include -C dot"
    Builder-->>Wrapper: SSH command with cd /remote/path
    Wrapper-->>UI: wrapped spawn config
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as NewInstanceModal / EditAgentModal
    participant IPC as process:spawn handler
    participant Wrapper as wrapSpawnWithSsh
    participant Builder as ssh-command-builder
    participant SSH as SSH process

    UI->>IPC: "spawn cwd=localPath workingDirOverride=~/git-projects"
    IPC->>IPC: "agentWorkingDirArgCwd = dot (SSH spawn)"
    IPC->>IPC: "buildAgentArgs with cwd=dot produces args with -C dot"
    IPC->>IPC: "remoteCwd = workingDirOverride.trim() OR config.cwd"
    IPC->>Builder: "buildSshCommandWithStdin cwd=remoteCwd"
    Builder->>Builder: shellEscapeRemotePath produces dollar HOME slash git-projects
    Builder-->>IPC: stdinScript with cd dollar HOME git-projects OR exit 1
    IPC->>SSH: spawn ssh args stdinScript
    SSH->>SSH: bash -s reads stdinScript expands HOME executes agent

    Note over UI,IPC: wrapSpawnWithSsh path used by Group Chat etc.
    UI->>Wrapper: "wrapSpawnWithSsh args include -C localPath workingDirOverride=/remote/path"
    Wrapper->>Wrapper: normalizeSshWorkingDirArgs replaces localPath with dot
    Wrapper->>Wrapper: "remoteCwd = workingDirOverride"
    Wrapper->>Builder: "buildSshCommand cwd=remoteCwd args include -C dot"
    Builder-->>Wrapper: SSH command with cd /remote/path
    Wrapper-->>UI: wrapped spawn config
Loading

Reviews (1): Last reviewed commit: "fix remote agent ssh working directories" | Re-trigger Greptile

Comment on lines +336 to +337
workingDirOverride:
sshRemoteConfig.workingDirOverride || session?.projectRoot || undefined,
sshRemoteConfig.workingDirOverride?.trim() || session?.projectRoot || undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Clearing the remote dir falls back to the locally-expanded path

When a user opens this modal for an SSH session, edits the Remote Working Directory field, and then clears it, setSshRemoteConfig stores workingDirOverride: ''. At save time ''.trim() is '' (falsy), so the expression falls through to session?.projectRoot. For any session created with a tilde path such as ~/git-projects, session.projectRoot holds the locally-expanded value (e.g., /Users/jta/git-projects). That path is then stored as workingDirOverride, and on the next spawn the SSH script runs cd '/Users/jta/git-projects' || exit 1 on the remote host, which exits 1 and kills the agent.

Consider treating an empty field as a validation error (disable Save) or rejecting an empty submission rather than silently saving with the wrong path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ecf0c7690. EditAgentModal now treats an empty SSH remote working directory as invalid, disables Save, shows Remote working directory is required, and handleSave returns before calling onSave. When valid, it stores the trimmed remote directory directly instead of falling back to session.projectRoot, so clearing the field cannot persist a local Mac path. Added regression coverage in EditAgentModal.test.tsx; validation passed with focused tests, npm run lint, npm run lint:eslint, and git diff --check.

});
mockProcessManager.spawn.mockReturnValue({ pid: 12345, success: true });

const handler = handlers.get('process:spawn');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test assertion diverges from real shellEscapeRemotePath output

The buildSshCommandWithStdin mock (line 113) generates cd '${remoteOptions.cwd}' || exit 1 — bare single-quoted path — rather than calling shellEscapeRemotePath. For a tilde path the real implementation produces cd "$HOME"/'git-projects' || exit 1, not cd '~/git-projects' || exit 1. This test verifies that the handler correctly routes workingDirOverride to the builder (good), but the assertion as written would still pass even if shellEscapeRemotePath were removed or broken. Tilde-expansion correctness is already covered in ssh-command-builder.test.ts.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ecf0c7690. The buildSshCommandWithStdin mock now matches the real shellEscapeRemotePath tilde output for ~/git-projects, and the assertion expects cd "$HOME"/'git-projects' || exit 1. The production tilde contract remains covered in ssh-command-builder.test.ts; this handler test now verifies routing while staying faithful to the builder output.

@pedramamini

Copy link
Copy Markdown
Collaborator

@jtalborough thanks for the contribution, this is a clean, well-scoped fix for a genuinely annoying SSH remote-cwd bug.

I reviewed the PR alongside the Greptile and CodeRabbit reviews, and verified each finding against the current code rather than just the addressed-in replies:

  • Greptile P1 (EditAgentModal empty field): Resolved. Clearing the Remote Working Directory now disables Save, surfaces a "Remote working directory is required" error, and handleSave bails early. workingDirOverride is persisted as the trimmed value, so the old silent fallback to the locally-expanded session.projectRoot is gone.
  • CodeRabbit (fail loudly when SSH enabled but remote unresolved): Resolved. process:spawn now throws when getSshRemoteConfig returns no config instead of falling through to local execution.
  • Greptile P2 / CodeRabbit (test assertion drift): Resolved. The process.test.ts mock mirrors shellEscapeRemotePath and asserts cd "$HOME"/'git-projects', keeping the tilde-expansion contract covered.
  • Core fix: shellEscapeRemotePath (tilde to $HOME), normalizeSshWorkingDirArgs, the agentWorkingDirArgCwd = '.' routing, and the useFileTreeManagement getTreeRoot/getGitRoot helpers all hang together and target the remote cwd consistently.

I also ran the focused suite locally (process.test.ts, ssh-command-builder.test.ts, ssh-spawn-wrapper.test.ts, EditAgentModal.test.tsx, NewInstanceModal.test.tsx, useFileTreeManagement.test.ts) and all 300 tests pass. No merge conflicts against main.

Happy with this. Approving and tagging approved. Thanks again!

@jSydorowicz21

Copy link
Copy Markdown
Contributor

@codex review please

@jSydorowicz21

jSydorowicz21 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

In the mean time @jtalborough please retarget to rc branch. We give everything cook time in RC before we merge to main to avoid any wide spread issues

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48c4f82856

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// projectRoot (which is the remote path the user originally configured).
workingDirOverride:
sshRemoteConfig.workingDirOverride || session?.projectRoot || undefined,
workingDirOverride: trimmedRemoteWorkingDir,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep shared history on the saved remote cwd

When an SSH agent has syncHistory enabled and the user changes this new Remote Working Directory field, saving only updates sessionSshRemoteConfig.workingDirOverride; handleSaveEditAgent leaves session.cwd unchanged, and buildSharedHistoryContext() still uses session.cwd as the remote history path. The next turn will run the agent in trimmedRemoteWorkingDir, but .maestro/history is read and written under the old cwd, so synced history disappears or goes to the wrong remote project after an edit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in affc58f62. When an enabled SSH agent saves a changed workingDirOverride, handleSaveEditAgent now updates cwd, fullPath, and remoteCwd to the saved remote cwd. That keeps buildSharedHistoryContext() pointed at the same remote directory the agent will run in, including syncHistory sessions. Added lifecycle regression coverage for this path.

// projectRoot (which is the remote path the user originally configured).
workingDirOverride:
sshRemoteConfig.workingDirOverride || session?.projectRoot || undefined,
workingDirOverride: trimmedRemoteWorkingDir,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh cached project state after remote cwd edits

When an SSH session already has a loaded file tree or git refs, changing this new remote working directory only saves the override. The existing fileTree, fileTreeStats, isGitRepo, branches, and tags stay attached to the session, and useFileTreeManagement skips its initial load once fileTreeStats is present, so the Files/Git UI keeps showing the old remote directory until a manual refresh or restart. Clear those cached fields or trigger a refresh when the saved remote cwd changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in affc58f62. The same SSH remote cwd change path now clears stale project state: fileTree, file-tree stats/loading/error/retry fields, expanded/scroll state, git repo status, cached branches, cached tags, and git ref cache time. It also clears stale runtime SSH identity so file/git operations re-resolve from sessionSshRemoteConfig. Added lifecycle regression coverage for the cache reset.

@jtalborough jtalborough changed the base branch from main to rc June 24, 2026 11:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: affc58f623

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +192 to +196
const sshProjectContextChanged =
!!nextRemoteCwd &&
(nextRemoteCwd !== currentSavedRemoteCwd ||
nextRemoteCwd !== s.cwd ||
sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset local state when disabling SSH

When an SSH agent whose cwd was moved by this new edit path is later switched to Local Execution, nextRemoteCwd is undefined, so sshProjectContextChanged is false and the reset block below never runs. That leaves the remote cwd/remoteCwd and any existing sshRemoteId on the session while sessionSshRemoteConfig.enabled is false; the next local spawn uses a remote-only cwd and file/git helpers can still dispatch through the stale remote id. Clear the SSH fields and restore a local cwd, or prevent disabling without choosing a local path.

Useful? React with 👍 / 👎.

// Use it as workingDirOverride so SSH terminals cd to the right place.
workingDirOverride:
sshRemoteConfig.workingDirOverride || expandedWorkingDir || undefined,
sshRemoteConfig.workingDirOverride?.trim() || remoteWorkingDir || undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep new SSH session cwd aligned with the remote override

When creating an SSH agent with syncHistory enabled and a home-relative remote path such as ~/repo, this line now preserves the raw remote path in workingDirOverride, but onCreate below still receives expandedWorkingDir for session.cwd. The agent spawn uses the override, while buildSharedHistoryContext() uses session.cwd, so shared history is read and written under the locally expanded path on the remote instead of the project the agent actually runs in. Pass the remote path through as the session cwd for SSH creation or make shared history use the override.

Useful? React with 👍 / 👎.

}

function getTreeRoot(session: Session, sshContext?: SshContext): string {
return sshContext?.remoteCwd || session.projectRoot || session.cwd;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the file tree rooted at the agent project

For an SSH agent, session.remoteCwd is also updated when the user runs cd in the Command Terminal, so prioritizing it here makes the Files panel scan whatever terminal directory was last visited instead of the configured project root. For example, after cd /tmp, the next refresh loads /tmp over SSH and the project files disappear until another cwd change resets remoteCwd. Use the configured working directory for the tree root, not the mutable terminal cwd.

Useful? React with 👍 / 👎.

Comment on lines +199 to +202
Object.assign(updatedFields, {
cwd: nextRemoteCwd,
fullPath: nextRemoteCwd,
remoteCwd: nextRemoteCwd,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repoint Auto Run when editing the remote cwd

When this block moves an SSH agent to a new remote working directory, it updates cwd and remoteCwd but leaves autoRunFolderPath pointing at the old project. The Right Bar passes that saved folder together with the SSH remote id, so after a remote cwd edit the Auto Run panel continues reading and writing playbooks in the previous remote project instead of the directory the agent now runs in. Update or clear the Auto Run folder path when the project context changes.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/__tests__/renderer/hooks/useSessionLifecycle.test.ts (1)

195-291: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for SSH disable / remoteId: null transitions.

Line 195 currently covers only “SSH stays enabled + workingDir changes”. Please add a case where SSH is turned off (or remoteId is cleared) to assert SSH/file-tree/git state is also reset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/renderer/hooks/useSessionLifecycle.test.ts` around lines 195 -
291, Add a regression test in useSessionLifecycle.test.ts for the SSH-disable
transition by covering handleSaveEditAgent when the sessionSshRemoteConfig
switches from enabled to disabled or remoteId becomes null. Reuse
createMockSession, useSessionStore, and useSessionLifecycle to set up an SSH
session, then assert the updated session clears sshRemote and sshRemoteId,
resets cwd/fullPath/remoteCwd appropriately, and wipes stale fileTree, explorer,
and git cache fields just like the existing SSH edit test does for
handleSaveEditAgent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/hooks/session/useSessionLifecycle.ts`:
- Around line 192-221: The SSH cleanup in useSessionLifecycle should not depend
on nextRemoteCwd being truthy, because disabling SSH or clearing remoteId can
leave stale session state. Update the sshProjectContextChanged check and the
surrounding reset logic so it also runs when sessionSshRemoteConfig changes to
null/disabled or its remoteId changes, and ensure the updatedFields reset branch
still clears sshRemote, sshRemoteId, and related file tree state in those
transitions.

---

Nitpick comments:
In `@src/__tests__/renderer/hooks/useSessionLifecycle.test.ts`:
- Around line 195-291: Add a regression test in useSessionLifecycle.test.ts for
the SSH-disable transition by covering handleSaveEditAgent when the
sessionSshRemoteConfig switches from enabled to disabled or remoteId becomes
null. Reuse createMockSession, useSessionStore, and useSessionLifecycle to set
up an SSH session, then assert the updated session clears sshRemote and
sshRemoteId, resets cwd/fullPath/remoteCwd appropriately, and wipes stale
fileTree, explorer, and git cache fields just like the existing SSH edit test
does for handleSaveEditAgent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ca7eaf71-6d4b-4960-aa89-4727a283ff13

📥 Commits

Reviewing files that changed from the base of the PR and between 48c4f82 and affc58f.

📒 Files selected for processing (3)
  • src/__tests__/main/utils/ssh-command-builder.test.ts
  • src/__tests__/renderer/hooks/useSessionLifecycle.test.ts
  • src/renderer/hooks/session/useSessionLifecycle.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/main/utils/ssh-command-builder.test.ts

Comment on lines +192 to +221
const sshProjectContextChanged =
!!nextRemoteCwd &&
(nextRemoteCwd !== currentSavedRemoteCwd ||
nextRemoteCwd !== s.cwd ||
sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId);

if (sshProjectContextChanged) {
Object.assign(updatedFields, {
cwd: nextRemoteCwd,
fullPath: nextRemoteCwd,
remoteCwd: nextRemoteCwd,
sshRemote: undefined,
sshRemoteId: undefined,
fileTree: [],
fileExplorerExpanded: [],
fileExplorerScrollPos: 0,
fileTreeStats: undefined,
fileTreeTruncated: undefined,
fileTreeLoadedCap: undefined,
fileTreeLoading: undefined,
fileTreeLoadingProgress: undefined,
fileTreeLastScanTime: undefined,
fileTreeError: undefined,
fileTreeRetryAt: undefined,
isGitRepo: false,
gitBranches: undefined,
gitTags: undefined,
gitRefsCacheTime: undefined,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

SSH context reset is skipped when SSH is disabled or remoteId is cleared.

Line 193 gates the reset on !!nextRemoteCwd, so transitions like enabled: true -> false never enter the cache/SSH cleanup branch, leaving stale remote session state.

Suggested fix
-const sshProjectContextChanged =
-  !!nextRemoteCwd &&
-  (nextRemoteCwd !== currentSavedRemoteCwd ||
-    nextRemoteCwd !== s.cwd ||
-    sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId);
+const nextSshEnabled = !!(sessionSshRemoteConfig?.enabled && sessionSshRemoteConfig.remoteId);
+const prevSshEnabled = !!(s.sessionSshRemoteConfig?.enabled && s.sessionSshRemoteConfig.remoteId);
+const sshProjectContextChanged =
+  nextSshEnabled !== prevSshEnabled ||
+  sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId ||
+  nextRemoteCwd !== currentSavedRemoteCwd ||
+  nextRemoteCwd !== s.cwd;

 if (sshProjectContextChanged) {
   Object.assign(updatedFields, {
-    cwd: nextRemoteCwd,
-    fullPath: nextRemoteCwd,
-    remoteCwd: nextRemoteCwd,
+    ...(nextRemoteCwd
+      ? { cwd: nextRemoteCwd, fullPath: nextRemoteCwd, remoteCwd: nextRemoteCwd }
+      : { remoteCwd: undefined }),
     sshRemote: undefined,
     sshRemoteId: undefined,
     fileTree: [],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sshProjectContextChanged =
!!nextRemoteCwd &&
(nextRemoteCwd !== currentSavedRemoteCwd ||
nextRemoteCwd !== s.cwd ||
sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId);
if (sshProjectContextChanged) {
Object.assign(updatedFields, {
cwd: nextRemoteCwd,
fullPath: nextRemoteCwd,
remoteCwd: nextRemoteCwd,
sshRemote: undefined,
sshRemoteId: undefined,
fileTree: [],
fileExplorerExpanded: [],
fileExplorerScrollPos: 0,
fileTreeStats: undefined,
fileTreeTruncated: undefined,
fileTreeLoadedCap: undefined,
fileTreeLoading: undefined,
fileTreeLoadingProgress: undefined,
fileTreeLastScanTime: undefined,
fileTreeError: undefined,
fileTreeRetryAt: undefined,
isGitRepo: false,
gitBranches: undefined,
gitTags: undefined,
gitRefsCacheTime: undefined,
});
}
const nextSshEnabled = !!(sessionSshRemoteConfig?.enabled && sessionSshRemoteConfig.remoteId);
const prevSshEnabled = !!(s.sessionSshRemoteConfig?.enabled && s.sessionSshRemoteConfig.remoteId);
const sshProjectContextChanged =
nextSshEnabled !== prevSshEnabled ||
sessionSshRemoteConfig?.remoteId !== s.sessionSshRemoteConfig?.remoteId ||
nextRemoteCwd !== currentSavedRemoteCwd ||
nextRemoteCwd !== s.cwd;
if (sshProjectContextChanged) {
Object.assign(updatedFields, {
...(nextRemoteCwd
? { cwd: nextRemoteCwd, fullPath: nextRemoteCwd, remoteCwd: nextRemoteCwd }
: { remoteCwd: undefined }),
sshRemote: undefined,
sshRemoteId: undefined,
fileTree: [],
fileExplorerExpanded: [],
fileExplorerScrollPos: 0,
fileTreeStats: undefined,
fileTreeTruncated: undefined,
fileTreeLoadedCap: undefined,
fileTreeLoading: undefined,
fileTreeLoadingProgress: undefined,
fileTreeLastScanTime: undefined,
fileTreeError: undefined,
fileTreeRetryAt: undefined,
isGitRepo: false,
gitBranches: undefined,
gitTags: undefined,
gitRefsCacheTime: undefined,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/session/useSessionLifecycle.ts` around lines 192 - 221,
The SSH cleanup in useSessionLifecycle should not depend on nextRemoteCwd being
truthy, because disabling SSH or clearing remoteId can leave stale session
state. Update the sshProjectContextChanged check and the surrounding reset logic
so it also runs when sessionSshRemoteConfig changes to null/disabled or its
remoteId changes, and ensure the updatedFields reset branch still clears
sshRemote, sshRemoteId, and related file tree state in those transitions.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2c31ca1a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +441 to +443
const isSshRemoteSpawn =
config.toolType !== 'terminal' && !!config.sessionSshRemoteConfig?.enabled;
const agentWorkingDirArgCwd = isSshRemoteSpawn ? '.' : config.cwd;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize SSH cwd args in tab naming too

This fixes the main process:spawn path by passing . into agent working-directory args for SSH, but the automatic tab-naming SSH path still builds args with config.cwd before wrapping, as checked in src/main/ipc/handlers/tabNaming.ts. For SSH Codex or Factory Droid agents whose workingDirOverride is ~/repo but cwd is the locally expanded /Users/..., chat turns now run in the right remote directory while tab naming still invokes the remote agent with -C /Users/... or --cwd /Users/..., so naming fails or runs against the wrong remote path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants