mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 10:05:18 +00:00
feat: implement TUI control queue and response handling
This commit is contained in:
parent
8c7cfd12b3
commit
2b1ddc2e02
7 changed files with 469 additions and 21 deletions
1
.turbo
Symbolic link
1
.turbo
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/nathan/sandbox-agent/.turbo
|
||||
1
dist
Symbolic link
1
dist
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/nathan/sandbox-agent/dist
|
||||
1
node_modules
Symbolic link
1
node_modules
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/nathan/sandbox-agent/node_modules
|
||||
|
|
@ -460,6 +460,51 @@ struct DirectoryQuery {
|
|||
directory: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiAppendPromptRequest {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiExecuteCommandRequest {
|
||||
command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiShowToastRequest {
|
||||
title: Option<String>,
|
||||
message: String,
|
||||
variant: String,
|
||||
duration: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiSelectSessionRequest {
|
||||
#[serde(rename = "sessionID")]
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiPublishRequest {
|
||||
#[serde(rename = "type")]
|
||||
event_type: String,
|
||||
properties: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TuiControlResponseRequest {
|
||||
#[serde(rename = "requestID")]
|
||||
request_id: String,
|
||||
body: Option<Value>,
|
||||
error: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
struct ToolQuery {
|
||||
directory: Option<String>,
|
||||
|
|
@ -4088,29 +4133,79 @@ async fn oc_skill_list() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_next() -> impl IntoResponse {
|
||||
async fn oc_tui_next(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
let next = state
|
||||
.inner
|
||||
.session_manager()
|
||||
.next_tui_control(directory)
|
||||
.await;
|
||||
if let Some(request) = next {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"path": request.path,
|
||||
"body": request.body,
|
||||
"requestID": request.id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
(StatusCode::OK, Json(json!({"path": "", "body": {}})))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/control/response",
|
||||
request_body = String,
|
||||
request_body = TuiControlResponseRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_response() -> impl IntoResponse {
|
||||
bool_ok(true)
|
||||
async fn oc_tui_response(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiControlResponseRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
let response = json!({
|
||||
"body": body.body,
|
||||
"error": body.error,
|
||||
});
|
||||
let accepted = state
|
||||
.inner
|
||||
.session_manager()
|
||||
.respond_tui_control(directory, &body.request_id, response)
|
||||
.await;
|
||||
bool_ok(accepted)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/append-prompt",
|
||||
request_body = String,
|
||||
request_body = TuiAppendPromptRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_append_prompt() -> impl IntoResponse {
|
||||
async fn oc_tui_append_prompt(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiAppendPromptRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(
|
||||
directory,
|
||||
"/tui/append-prompt".to_string(),
|
||||
json!({"text": body.text}),
|
||||
)
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -4120,7 +4215,17 @@ async fn oc_tui_append_prompt() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_open_help() -> impl IntoResponse {
|
||||
async fn oc_tui_open_help(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/open-help".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -4130,7 +4235,17 @@ async fn oc_tui_open_help() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_open_sessions() -> impl IntoResponse {
|
||||
async fn oc_tui_open_sessions(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/open-sessions".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -4140,7 +4255,17 @@ async fn oc_tui_open_sessions() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_open_themes() -> impl IntoResponse {
|
||||
async fn oc_tui_open_themes(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/open-themes".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -4150,18 +4275,37 @@ async fn oc_tui_open_themes() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_open_models() -> impl IntoResponse {
|
||||
async fn oc_tui_open_models(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/open-models".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/submit-prompt",
|
||||
request_body = String,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_submit_prompt() -> impl IntoResponse {
|
||||
async fn oc_tui_submit_prompt(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/submit-prompt".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
@ -4171,51 +4315,179 @@ async fn oc_tui_submit_prompt() -> impl IntoResponse {
|
|||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_clear_prompt() -> impl IntoResponse {
|
||||
async fn oc_tui_clear_prompt(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, "/tui/clear-prompt".to_string(), json!({}))
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/execute-command",
|
||||
request_body = String,
|
||||
request_body = TuiExecuteCommandRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_execute_command() -> impl IntoResponse {
|
||||
async fn oc_tui_execute_command(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiExecuteCommandRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(
|
||||
directory,
|
||||
"/tui/execute-command".to_string(),
|
||||
json!({"command": body.command}),
|
||||
)
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/show-toast",
|
||||
request_body = String,
|
||||
request_body = TuiShowToastRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_show_toast() -> impl IntoResponse {
|
||||
async fn oc_tui_show_toast(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiShowToastRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(
|
||||
directory,
|
||||
"/tui/show-toast".to_string(),
|
||||
json!({
|
||||
"title": body.title,
|
||||
"message": body.message,
|
||||
"variant": body.variant,
|
||||
"duration": body.duration,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/publish",
|
||||
request_body = String,
|
||||
request_body = TuiPublishRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_publish() -> impl IntoResponse {
|
||||
async fn oc_tui_publish(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiPublishRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
let (path, payload) = match body.event_type.as_str() {
|
||||
"tui.prompt.append" => {
|
||||
let Some(text) = body.properties.get("text").and_then(|v| v.as_str()) else {
|
||||
return bad_request("text is required").into_response();
|
||||
};
|
||||
("/tui/append-prompt", json!({"text": text}))
|
||||
}
|
||||
"tui.command.execute" => {
|
||||
let Some(command) = body
|
||||
.properties
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
else {
|
||||
return bad_request("command is required").into_response();
|
||||
};
|
||||
("/tui/execute-command", json!({"command": command}))
|
||||
}
|
||||
"tui.toast.show" => {
|
||||
let Some(message) = body.properties.get("message").and_then(|v| v.as_str()) else {
|
||||
return bad_request("message is required").into_response();
|
||||
};
|
||||
let Some(variant) = body.properties.get("variant").and_then(|v| v.as_str()) else {
|
||||
return bad_request("variant is required").into_response();
|
||||
};
|
||||
let title = body.properties.get("title").cloned();
|
||||
let duration = body.properties.get("duration").cloned();
|
||||
(
|
||||
"/tui/show-toast",
|
||||
json!({
|
||||
"title": title,
|
||||
"message": message,
|
||||
"variant": variant,
|
||||
"duration": duration,
|
||||
}),
|
||||
)
|
||||
}
|
||||
"tui.session.select" => {
|
||||
let Some(session_id) = body
|
||||
.properties
|
||||
.get("sessionID")
|
||||
.and_then(|v| v.as_str())
|
||||
else {
|
||||
return bad_request("sessionID is required").into_response();
|
||||
};
|
||||
let sessions = state.opencode.sessions.lock().await;
|
||||
if !sessions.contains_key(session_id) {
|
||||
return not_found("session not found").into_response();
|
||||
}
|
||||
("/tui/select-session", json!({"sessionID": session_id}))
|
||||
}
|
||||
_ => return bad_request("unsupported TUI event").into_response(),
|
||||
};
|
||||
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(directory, path.to_string(), payload)
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tui/select-session",
|
||||
request_body = String,
|
||||
request_body = TuiSelectSessionRequest,
|
||||
responses((status = 200)),
|
||||
tag = "opencode"
|
||||
)]
|
||||
async fn oc_tui_select_session() -> impl IntoResponse {
|
||||
async fn oc_tui_select_session(
|
||||
State(state): State<Arc<OpenCodeAppState>>,
|
||||
Query(query): Query<DirectoryQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<TuiSelectSessionRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let directory = state.opencode.directory_for(&headers, query.directory.as_ref());
|
||||
let sessions = state.opencode.sessions.lock().await;
|
||||
if !sessions.contains_key(&body.session_id) {
|
||||
return not_found("session not found").into_response();
|
||||
}
|
||||
state
|
||||
.inner
|
||||
.session_manager()
|
||||
.enqueue_tui_control(
|
||||
directory,
|
||||
"/tui/select-session".to_string(),
|
||||
json!({"sessionID": body.session_id}),
|
||||
)
|
||||
.await;
|
||||
bool_ok(true)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ use sandbox_agent_agent_management::credentials::{
|
|||
|
||||
const MOCK_EVENT_DELAY_MS: u64 = 200;
|
||||
static USER_MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
static TUI_CONTROL_COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppState {
|
||||
|
|
@ -360,6 +361,20 @@ struct PendingQuestion {
|
|||
options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TuiControlRequest {
|
||||
pub(crate) id: String,
|
||||
pub(crate) path: String,
|
||||
pub(crate) body: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TuiControlQueue {
|
||||
pending: VecDeque<TuiControlRequest>,
|
||||
inflight: HashMap<String, TuiControlRequest>,
|
||||
responses: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
fn new(
|
||||
session_id: String,
|
||||
|
|
@ -818,6 +833,7 @@ pub(crate) struct SessionManager {
|
|||
sessions: Mutex<Vec<SessionState>>,
|
||||
server_manager: Arc<AgentServerManager>,
|
||||
http_client: Client,
|
||||
tui_controls: Mutex<HashMap<String, TuiControlQueue>>,
|
||||
}
|
||||
|
||||
/// Shared Codex app-server process that handles multiple sessions via JSON-RPC.
|
||||
|
|
@ -1538,6 +1554,7 @@ impl SessionManager {
|
|||
sessions: Mutex::new(Vec::new()),
|
||||
server_manager,
|
||||
http_client: Client::new(),
|
||||
tui_controls: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1960,6 +1977,59 @@ impl SessionManager {
|
|||
items
|
||||
}
|
||||
|
||||
pub(crate) async fn enqueue_tui_control(
|
||||
&self,
|
||||
directory: String,
|
||||
path: String,
|
||||
body: Value,
|
||||
) -> String {
|
||||
let id = format!(
|
||||
"tui_{}",
|
||||
TUI_CONTROL_COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
);
|
||||
let request = TuiControlRequest {
|
||||
id: id.clone(),
|
||||
path,
|
||||
body,
|
||||
};
|
||||
let mut controls = self.tui_controls.lock().await;
|
||||
let queue = controls.entry(directory).or_default();
|
||||
queue.pending.push_back(request);
|
||||
id
|
||||
}
|
||||
|
||||
pub(crate) async fn next_tui_control(
|
||||
&self,
|
||||
directory: String,
|
||||
) -> Option<TuiControlRequest> {
|
||||
let mut controls = self.tui_controls.lock().await;
|
||||
let queue = controls.entry(directory).or_default();
|
||||
if let Some(request) = queue.pending.pop_front() {
|
||||
queue
|
||||
.inflight
|
||||
.insert(request.id.clone(), request.clone());
|
||||
return Some(request);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn respond_tui_control(
|
||||
&self,
|
||||
directory: String,
|
||||
request_id: &str,
|
||||
response: Value,
|
||||
) -> bool {
|
||||
let mut controls = self.tui_controls.lock().await;
|
||||
let Some(queue) = controls.get_mut(&directory) else {
|
||||
return false;
|
||||
};
|
||||
if queue.inflight.remove(request_id).is_some() {
|
||||
queue.responses.insert(request_id.to_string(), response);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn subscribe_for_turn(
|
||||
&self,
|
||||
session_id: &str,
|
||||
|
|
|
|||
102
server/packages/sandbox-agent/tests/opencode-compat/tui.test.ts
Normal file
102
server/packages/sandbox-agent/tests/opencode-compat/tui.test.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Tests for OpenCode-compatible TUI control endpoints.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk";
|
||||
import { spawnSandboxAgent, buildSandboxAgent, type SandboxAgentHandle } from "./helpers/spawn";
|
||||
|
||||
type TuiClient = {
|
||||
appendPrompt: (args: unknown) => Promise<{ data?: unknown }>;
|
||||
executeCommand: (args: unknown) => Promise<{ data?: unknown }>;
|
||||
showToast: (args: unknown) => Promise<{ data?: unknown }>;
|
||||
control: {
|
||||
next: (args?: unknown) => Promise<{ data?: unknown }>;
|
||||
response: (args: unknown) => Promise<{ data?: unknown }>;
|
||||
};
|
||||
};
|
||||
|
||||
describe("OpenCode-compatible TUI Control API", () => {
|
||||
let handle: SandboxAgentHandle;
|
||||
let client: OpencodeClient;
|
||||
let tui: TuiClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
await buildSandboxAgent();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
handle = await spawnSandboxAgent({ opencodeCompat: true });
|
||||
client = createOpencodeClient({
|
||||
baseUrl: `${handle.baseUrl}/opencode`,
|
||||
headers: { Authorization: `Bearer ${handle.token}` },
|
||||
});
|
||||
tui = client.tui as unknown as TuiClient;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await handle?.dispose();
|
||||
});
|
||||
|
||||
it("queues TUI control requests in order", async () => {
|
||||
await tui.appendPrompt({ body: { text: "First" } });
|
||||
await tui.executeCommand({ body: { command: "prompt.clear" } });
|
||||
await tui.showToast({ body: { message: "Hello", variant: "info" } });
|
||||
|
||||
const first = (await tui.control.next({})).data as {
|
||||
path: string;
|
||||
body: { text?: string };
|
||||
requestID?: string;
|
||||
};
|
||||
const second = (await tui.control.next({})).data as {
|
||||
path: string;
|
||||
body: { command?: string };
|
||||
requestID?: string;
|
||||
};
|
||||
const third = (await tui.control.next({})).data as {
|
||||
path: string;
|
||||
body: { message?: string };
|
||||
requestID?: string;
|
||||
};
|
||||
|
||||
expect(first.path).toBe("/tui/append-prompt");
|
||||
expect(first.body.text).toBe("First");
|
||||
expect(first.requestID).toBeDefined();
|
||||
|
||||
expect(second.path).toBe("/tui/execute-command");
|
||||
expect(second.body.command).toBe("prompt.clear");
|
||||
expect(second.requestID).toBeDefined();
|
||||
|
||||
expect(third.path).toBe("/tui/show-toast");
|
||||
expect(third.body.message).toBe("Hello");
|
||||
expect(third.requestID).toBeDefined();
|
||||
|
||||
const empty = (await tui.control.next({})).data as {
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
};
|
||||
expect(empty.path).toBe("");
|
||||
expect(empty.body).toEqual({});
|
||||
});
|
||||
|
||||
it("handles control responses with request IDs", async () => {
|
||||
await tui.appendPrompt({ body: { text: "Ack me" } });
|
||||
const next = (await tui.control.next({})).data as { requestID?: string };
|
||||
expect(next.requestID).toBeDefined();
|
||||
|
||||
const accepted = await tui.control.response({
|
||||
body: { requestID: next.requestID, body: { ok: true } },
|
||||
});
|
||||
expect(accepted.data).toBe(true);
|
||||
|
||||
const duplicate = await tui.control.response({
|
||||
body: { requestID: next.requestID, body: { ok: false } },
|
||||
});
|
||||
expect(duplicate.data).toBe(false);
|
||||
|
||||
const missing = await tui.control.response({
|
||||
body: { requestID: "tui_missing" },
|
||||
});
|
||||
expect(missing.data).toBe(false);
|
||||
});
|
||||
});
|
||||
1
target
Symbolic link
1
target
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/nathan/sandbox-agent/target
|
||||
Loading…
Add table
Add a link
Reference in a new issue