feat(coding-agent): added per-command PTY control to bash tool
- Added per-command `pty` parameter to bash tool for fine-grained PTY mode control. - Removed global `bash.virtualTerminal` setting in favor of per-command PTY parameter. - Fixed potential deadlock in shell session cleanup by replacing blocking lock with non-blocking try_lock. - Updated async session key generation to include jobId for improved session isolation.
This commit is contained in:
@@ -205,7 +205,12 @@ async fn run_shell_session(
|
||||
run_task.abort();
|
||||
let _ = run_task.await;
|
||||
}
|
||||
*session.lock().await = None;
|
||||
// Use try_lock to avoid deadlocking if another task holds the session.
|
||||
// If we can't acquire the lock, the session will be cleaned up when the
|
||||
// holding task finishes.
|
||||
if let Ok(mut guard) = session.try_lock() {
|
||||
*guard = None;
|
||||
}
|
||||
return Ok(ShellRunResult {
|
||||
exit_code: None,
|
||||
cancelled: matches!(reason, task::AbortReason::Signal),
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added `pty` parameter to bash tool to enable PTY mode for commands requiring a real terminal (e.g., sudo, ssh, top, less)
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed bash tool to use per-command PTY control instead of global virtual terminal setting
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `bash.virtualTerminal` setting; use the `pty` parameter on individual bash commands instead
|
||||
|
||||
## [12.19.1] - 2026-02-22
|
||||
### Removed
|
||||
|
||||
@@ -756,17 +756,6 @@ export const SETTINGS_SCHEMA = {
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Bash interceptor settings
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
"bash.virtualTerminal": {
|
||||
type: "enum",
|
||||
values: ["on", "off"] as const,
|
||||
default: "off",
|
||||
ui: {
|
||||
tab: "bash",
|
||||
label: "Virtual terminal",
|
||||
description: "Use PTY-backed interactive execution for bash",
|
||||
submenu: true,
|
||||
},
|
||||
},
|
||||
"bashInterceptor.enabled": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
|
||||
@@ -538,7 +538,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
const cwd = getProjectDir();
|
||||
await logger.timeAsync("settings:init", () => Settings.init({ cwd }));
|
||||
if (parsedArgs.noPty) {
|
||||
settings.override("bash.virtualTerminal", "off");
|
||||
Bun.env.PI_NO_PTY = "1";
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -154,11 +154,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
||||
{ value: "tool-only", label: "tool-only", description: "Interrupt only on tool-call argument matches" },
|
||||
{ value: "never", label: "never", description: "Never interrupt; inject warning after completion" },
|
||||
],
|
||||
// Virtual terminal
|
||||
"bash.virtualTerminal": [
|
||||
{ value: "on", label: "On", description: "PTY-backed interactive execution" },
|
||||
{ value: "off", label: "Off", description: "Standard non-interactive execution" },
|
||||
],
|
||||
// Provider options
|
||||
"providers.webSearch": [
|
||||
{
|
||||
|
||||
@@ -34,6 +34,11 @@ const bashSchemaBase = Type.Object({
|
||||
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
|
||||
head: Type.Optional(Type.Number({ description: "Return only first N lines of output" })),
|
||||
tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
|
||||
pty: Type.Optional(
|
||||
Type.Boolean({
|
||||
description: "Run in PTY mode when command needs a real terminal (e.g. sudo/ssh/top/less); default: false",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const bashSchemaWithAsync = Type.Object({
|
||||
@@ -54,6 +59,7 @@ export interface BashToolInput {
|
||||
head?: number;
|
||||
tail?: number;
|
||||
async?: boolean;
|
||||
pty?: boolean;
|
||||
}
|
||||
|
||||
export interface BashToolDetails {
|
||||
@@ -123,7 +129,15 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
{ command: rawCommand, timeout: rawTimeout = 300, cwd, head, tail, async: asyncRequested = false }: BashToolInput,
|
||||
{
|
||||
command: rawCommand,
|
||||
timeout: rawTimeout = 300,
|
||||
cwd,
|
||||
head,
|
||||
tail,
|
||||
async: asyncRequested = false,
|
||||
pty = false,
|
||||
}: BashToolInput,
|
||||
signal?: AbortSignal,
|
||||
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
||||
ctx?: AgentToolContext,
|
||||
@@ -188,7 +202,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
try {
|
||||
const result = await executeBash(command, {
|
||||
cwd: commandCwd,
|
||||
sessionKey: this.session.getSessionId?.() ?? undefined,
|
||||
sessionKey: `${this.session.getSessionId?.() ?? ""}:async:${jobId}`,
|
||||
timeout: timeoutMs,
|
||||
signal: runSignal,
|
||||
env: extraEnv,
|
||||
@@ -229,11 +243,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
const extraEnv = artifactsDir ? { ARTIFACTS: artifactsDir } : undefined;
|
||||
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
||||
|
||||
const usePty =
|
||||
this.session.settings.get("bash.virtualTerminal") === "on" &&
|
||||
$env.PI_NO_PTY !== "1" &&
|
||||
ctx?.hasUI === true &&
|
||||
ctx.ui !== undefined;
|
||||
const usePty = pty && $env.PI_NO_PTY !== "1" && ctx?.hasUI === true && ctx.ui !== undefined;
|
||||
const result: BashResult | BashInteractiveResult = usePty
|
||||
? await runInteractiveBashPty(ctx.ui!, {
|
||||
command,
|
||||
|
||||
Reference in New Issue
Block a user