fix(coding-agent): synced bash cwd after cd
Updated interactive bash execution to query the persistent shell PWD after commands and move the session cwd when it changes, keeping the status line and session-scoped settings aligned with shell navigation. Added regression coverage for syncing persistent shell directory changes back to the owning session. Fixes #3958
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the interactive bash status line staying on the old working directory after `cd` changed the persistent shell directory. ([#3958](https://github.com/can1357/oh-my-pi/issues/3958))
|
||||
|
||||
## [16.2.11] - 2026-07-01
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
|
||||
import { type BashResult, executeBash } from "./bash-executor";
|
||||
|
||||
const CWD_QUERY_TIMEOUT_MS = 5_000;
|
||||
|
||||
export interface BashCwdSyncOptions {
|
||||
/** Persistent bash session key whose current directory should be queried. */
|
||||
sessionKey: string;
|
||||
/** Session cwd before the user bash command ran. */
|
||||
currentCwd: string;
|
||||
/** Run through the configured user shell when the original command did. */
|
||||
useUserShell?: boolean;
|
||||
/** Apply the discovered cwd to the owning session. */
|
||||
applyCwd: (cwd: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/** Synchronize a persistent bash session's PWD back into the owning session. */
|
||||
export async function syncBashSessionCwd(options: BashCwdSyncOptions): Promise<string | null> {
|
||||
let result: BashResult;
|
||||
try {
|
||||
result = await executeBash("pwd", {
|
||||
sessionKey: options.sessionKey,
|
||||
timeout: CWD_QUERY_TIMEOUT_MS,
|
||||
useUserShell: options.useUserShell,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug("Failed to query bash session cwd", { error: String(error) });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.cancelled || result.exitCode !== 0) return null;
|
||||
const nextCwd = result.output
|
||||
.split(/\r?\n/)
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.at(-1);
|
||||
if (!nextCwd || !path.isAbsolute(nextCwd)) return null;
|
||||
if (path.resolve(nextCwd) === path.resolve(options.currentCwd)) return null;
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(nextCwd);
|
||||
if (!stat.isDirectory()) return null;
|
||||
await options.applyCwd(nextCwd);
|
||||
return nextCwd;
|
||||
} catch (error) {
|
||||
logger.debug("Failed to apply bash session cwd", { cwd: nextCwd, error: String(error) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@ import {
|
||||
prompt,
|
||||
relativePathWithinRoot,
|
||||
Snowflake,
|
||||
setProjectDir,
|
||||
withTimeout,
|
||||
} from "@oh-my-pi/pi-utils";
|
||||
import * as snapcompact from "@oh-my-pi/snapcompact";
|
||||
@@ -172,6 +173,7 @@ import {
|
||||
} from "../config/model-resolver";
|
||||
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
||||
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
||||
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
||||
import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
|
||||
import type { Settings, SkillsSettings } from "../config/settings";
|
||||
import { getDefault, onAppendOnlyModeChanged, validateProviderMaxInFlightRequests } from "../config/settings";
|
||||
@@ -188,6 +190,7 @@ import {
|
||||
} from "../eval/py/executor";
|
||||
import { disposeRubyKernelSessionsByOwner } from "../eval/rb/executor";
|
||||
import { defaultEvalSessionId } from "../eval/session-id";
|
||||
import { syncBashSessionCwd } from "../exec/bash-cwd-sync";
|
||||
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
||||
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
||||
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
||||
@@ -12774,6 +12777,19 @@ export class AgentSession {
|
||||
useUserShell: options?.useUserShell,
|
||||
});
|
||||
|
||||
await syncBashSessionCwd({
|
||||
sessionKey: this.sessionId,
|
||||
currentCwd: cwd,
|
||||
useUserShell: options?.useUserShell,
|
||||
applyCwd: async nextCwd => {
|
||||
await this.sessionManager.moveTo(nextCwd);
|
||||
setProjectDir(nextCwd);
|
||||
await this.settings.reloadForCwd(nextCwd);
|
||||
applyProviderGlobalsFromSettings(this.settings);
|
||||
resetCapabilities();
|
||||
},
|
||||
});
|
||||
|
||||
this.recordBashResult(command, result, options);
|
||||
return result;
|
||||
} finally {
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { resetSettingsForTest, Settings, type ShellMinimizerSettings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { syncBashSessionCwd } from "@oh-my-pi/pi-coding-agent/exec/bash-cwd-sync";
|
||||
import { buildMinimizerOptions, executeBash } from "@oh-my-pi/pi-coding-agent/exec/bash-executor";
|
||||
import { DEFAULT_MAX_BYTES } from "@oh-my-pi/pi-coding-agent/session/streaming-output";
|
||||
import * as shellSnapshot from "@oh-my-pi/pi-coding-agent/utils/shell-snapshot";
|
||||
@@ -121,6 +122,26 @@ describe("executeBash", () => {
|
||||
expect(result.output.trim()).toBe(fs.realpathSync(tempDir));
|
||||
});
|
||||
|
||||
it("syncs persistent shell directory changes back to the session", async () => {
|
||||
const childDir = path.join(tempDir, "child");
|
||||
fs.mkdirSync(childDir);
|
||||
const sessionKey = `cwd-sync-${Date.now()}`;
|
||||
await executeBash(`cd ${shellQuote(childDir)}`, { sessionKey, timeout: 5000, useUserShell: true });
|
||||
|
||||
const applied: string[] = [];
|
||||
const synced = await syncBashSessionCwd({
|
||||
sessionKey,
|
||||
currentCwd: tempDir,
|
||||
useUserShell: true,
|
||||
applyCwd: async cwd => {
|
||||
applied.push(cwd);
|
||||
},
|
||||
});
|
||||
|
||||
expect(synced).toBe(childDir);
|
||||
expect(applied).toEqual([childDir]);
|
||||
});
|
||||
|
||||
it("canonicalizes symlinked cwd before execution", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user