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:
can1357
2026-02-22 16:58:46 +01:00
parent 0197693cb1
commit d912e692d8
6 changed files with 34 additions and 25 deletions
+6 -1
View File
@@ -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),
+11
View File
@@ -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,
-1
View File
@@ -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": [
{
+17 -7
View File
@@ -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,