Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/bashkit-js/__test__/terminal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ test("terminal: wait_for and screen changes", async (t) => {
);
});

test("terminal: sessions share files", async (t) => {
const term = new Terminal();
await term.call("echo hi > /tmp/s<Enter>");
const out = await term.call({ session: "other", input: "cat /tmp/s<Enter>" });
t.is(out.session, "other");
t.is(out.commands[0].output, "hi\n");
t.deepEqual(
out.sessions!.map((s) => s.name),
["main", "other"],
);
t.is((await term.call({ session: "other", close: true })).closed, true);
});

test("terminal: exit and exitCode", async (t) => {
const term = new Terminal();
const out = await term.call("exit 3<Enter>");
Expand Down
37 changes: 23 additions & 14 deletions crates/bashkit-js/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,31 @@ impl Terminal {
cwd: None,
env: None,
});
let mut builder = RustBash::builder();
if let Some(u) = opts.username {
builder = builder.username(u);
}
if let Some(h) = opts.hostname {
builder = builder.hostname(h);
}
if let Some(c) = opts.cwd {
builder = builder.cwd(c);
}
for (k, v) in opts.env.unwrap_or_default() {
builder = builder.env(&k, &v);
}
let size = TerminalSize::new(clamp_dim(opts.rows, 24), clamp_dim(opts.cols, 80));
let (username, hostname, cwd) = (opts.username, opts.hostname, opts.cwd);
let env = opts.env.unwrap_or_default();
// Each `session` passed to `call` gets a shell built the same way.
let factory = move || {
let mut builder = RustBash::builder();
if let Some(u) = &username {
builder = builder.username(u.clone());
}
if let Some(h) = &hostname {
builder = builder.hostname(h.clone());
}
if let Some(c) = &cwd {
builder = builder.cwd(c.clone());
}
for (k, v) in &env {
builder = builder.env(k, v);
}
builder
};
Self {
tool: Arc::new(Mutex::new(TerminalTool::with_size(builder, size))),
tool: Arc::new(Mutex::new(TerminalTool::with_sessions(
size,
Box::new(factory),
))),
}
}

Expand Down
12 changes: 12 additions & 0 deletions crates/bashkit-js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,10 @@ export interface TerminalCallArgs {
wait_for?: string;
/** `full` (default), `changes` (only changed rows) or `none`. */
screen?: "full" | "changes" | "none";
/** Terminal tab to use (default `main`); created on first use, files shared. */
session?: string;
/** Close the named session instead of typing into it. */
close?: boolean;
}

export interface TerminalCallResult {
Expand All @@ -2022,6 +2026,14 @@ export interface TerminalCallResult {
cursor?: { row: number; col: number };
/** Whether `wait_for` matched (only when it was given). */
matched?: boolean;
/** Session this result is for, when more than one is open. */
session?: string;
/** Every open session and its activity, when more than one is open. */
sessions?: Array<{ name: string; activity: string }>;
/** The session had exited and a fresh shell started for this call. */
restarted?: boolean;
/** The session was closed (`close: true`). */
closed?: boolean;
/** `input`: a running command waits for a typed line (see `input_prompt`). */
activity: "prompt" | "continuation" | "running" | "input" | "exited";
/** Question on the cursor line while activity is `input`. */
Expand Down
4 changes: 4 additions & 0 deletions crates/bashkit-python/bashkit/_bashkit.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1937,11 +1937,15 @@ class Terminal:
wait_ms: int | None = None,
wait_for: str | None = None,
screen: Literal["full", "changes", "none"] | None = None,
session: str | None = None,
close: bool | None = None,
) -> dict[str, Any]:
"""Agent step: type ``input`` in Vim key notation, wait, report screen and commands.

``wait_for`` (regex) returns as soon as command output matches.
``screen="changes"`` returns only rows changed since the last call.
``session`` names a terminal tab (created on first use, files shared);
``close=True`` closes it.
"""
...
def screen_text(self) -> str: ...
Expand Down
50 changes: 33 additions & 17 deletions crates/bashkit-python/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,30 @@ impl PyTerminal {
cwd: Option<String>,
env: Option<HashMap<String, String>>,
) -> PyResult<Self> {
let mut builder = Bash::builder();
if let Some(u) = username {
builder = builder.username(u);
}
if let Some(h) = hostname {
builder = builder.hostname(h);
}
if let Some(c) = cwd {
builder = builder.cwd(c);
}
for (k, v) in env.unwrap_or_default() {
builder = builder.env(&k, &v);
}
let env = env.unwrap_or_default();
// Each `session` passed to `call` gets a shell built the same way.
let factory = move || {
let mut builder = Bash::builder();
if let Some(u) = &username {
builder = builder.username(u.clone());
}
if let Some(h) = &hostname {
builder = builder.hostname(h.clone());
}
if let Some(c) = &cwd {
builder = builder.cwd(c.clone());
}
for (k, v) in &env {
builder = builder.env(k, v);
}
builder
};
let rt = make_runtime()?;
// The session future is created inside the runtime so nothing in it
// can observe a missing tokio context.
let tool = {
let _enter = rt.enter();
TerminalTool::with_size(builder, TerminalSize::new(rows, cols))
TerminalTool::with_sessions(TerminalSize::new(rows, cols), Box::new(factory))
};
Ok(Self {
tool: Mutex::new(tool),
Expand Down Expand Up @@ -151,16 +156,21 @@ impl PyTerminal {
/// `"ihello<Esc>:wq<Enter>"`), wait up to `wait_ms`, and return a dict
/// with `screen`, `activity`, `commands` and more. `wait_for` (regex)
/// returns early once command output matches; `screen` is `"full"`,
/// `"changes"` or `"none"`. Argument names match `tool_definition()`, so
/// `call(**tool_args)` forwards a model's call.
#[pyo3(signature = (input="", wait_ms=None, wait_for=None, screen=None))]
/// `"changes"` or `"none"`; `session` names a terminal tab (created on
/// first use, sharing files) and `close=True` closes it. Argument names
/// match `tool_definition()`, so `call(**tool_args)` forwards a model's
/// call.
#[pyo3(signature = (input="", wait_ms=None, wait_for=None, screen=None, session=None, close=None))]
#[allow(clippy::too_many_arguments)]
fn call(
&self,
py: Python<'_>,
input: &str,
wait_ms: Option<u64>,
wait_for: Option<&str>,
screen: Option<&str>,
session: Option<&str>,
close: Option<bool>,
) -> PyResult<Py<PyAny>> {
let mut args = serde_json::json!({ "input": input });
if let Some(ms) = wait_ms {
Expand All @@ -172,6 +182,12 @@ impl PyTerminal {
if let Some(m) = screen {
args["screen"] = m.into();
}
if let Some(s) = session {
args["session"] = s.into();
}
if let Some(c) = close {
args["close"] = c.into();
}
let out = py.detach(|| self.with_tool(|tool| self.rt.block_on(tool.call(args))));
let value = out.map_err(|e| PyValueError::new_err(e.to_string()))?;
json_to_py(py, &value)
Expand Down
10 changes: 10 additions & 0 deletions crates/bashkit-python/tests/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ def test_wait_for_and_screen_changes():
assert [c["text"] for c in out["screen_changes"]][-2:] == ["^C", "$"]


def test_sessions_share_files():
t = Terminal()
t.call("echo hi > /tmp/s<Enter>")
out = t.call("cat /tmp/s<Enter>", session="other")
assert out["session"] == "other"
assert out["commands"][0]["output"] == "hi\n"
assert [s["name"] for s in out["sessions"]] == ["main", "other"]
assert t.call(session="other", close=True)["closed"] is True


def test_exit_and_exit_code():
t = Terminal()
out = t.call("exit 3<Enter>")
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit/src/terminal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Mutex, PoisonError};

pub(crate) use keys::{Key, ScreenGuard, read_key};
pub use tool::{TERMINAL_TOOL_NAME, TerminalTool, TerminalToolError};
pub use tool::{MAIN_SESSION, SessionFactory, TERMINAL_TOOL_NAME, TerminalTool, TerminalToolError};
pub(crate) use tty::{Tty, TtyEvent};

use crate::fs::FileSystem;
Expand Down
Loading
Loading