Smooth codex login (#1155)
* Add codex setup helper Pass exit result Move codex setup to routes FIx fmt Fix finalize * Rename scriptcontext (vibe-kanban 79207902) Rename the gh cli script context to something more general and use it for installs in crates/server/src/routes/task_attempts/cursor_setup.rs Rename scriptcontext (vibe-kanban 79207902) Rename the gh cli script context to something more general and use it for installs in crates/server/src/routes/task_attempts/cursor_setup.rs Fmt * Fix missing overrides for codex
This commit is contained in:
@@ -23,7 +23,7 @@ pub enum ScriptContext {
|
||||
SetupScript,
|
||||
CleanupScript,
|
||||
DevServer,
|
||||
GithubCliSetupScript,
|
||||
ToolInstallScript,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
|
||||
@@ -19,7 +19,7 @@ use workspace_utils::stream_lines::LinesStreamExt;
|
||||
use super::{AcpClient, SessionManager};
|
||||
use crate::{
|
||||
command::CommandParts,
|
||||
executors::{ExecutorError, SpawnedChild, acp::AcpEvent},
|
||||
executors::{ExecutorError, ExecutorExitResult, SpawnedChild, acp::AcpEvent},
|
||||
};
|
||||
|
||||
/// Reusable harness for ACP-based conns (Gemini, Qwen, etc.)
|
||||
@@ -68,7 +68,7 @@ impl AcpAgentHarness {
|
||||
|
||||
let mut child = command.group_spawn()?;
|
||||
|
||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<ExecutorExitResult>();
|
||||
Self::bootstrap_acp_connection(
|
||||
&mut child,
|
||||
current_dir.to_path_buf(),
|
||||
@@ -105,7 +105,7 @@ impl AcpAgentHarness {
|
||||
|
||||
let mut child = command.group_spawn()?;
|
||||
|
||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
let (exit_tx, exit_rx) = tokio::sync::oneshot::channel::<ExecutorExitResult>();
|
||||
Self::bootstrap_acp_connection(
|
||||
&mut child,
|
||||
current_dir.to_path_buf(),
|
||||
@@ -127,7 +127,7 @@ impl AcpAgentHarness {
|
||||
cwd: PathBuf,
|
||||
existing_session: Option<String>,
|
||||
prompt: String,
|
||||
exit_signal: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
exit_signal: Option<tokio::sync::oneshot::Sender<ExecutorExitResult>>,
|
||||
session_namespace: String,
|
||||
) -> Result<(), ExecutorError> {
|
||||
// Take child's stdio for ACP wiring
|
||||
@@ -387,7 +387,7 @@ impl AcpAgentHarness {
|
||||
}
|
||||
// Notify container of completion
|
||||
if let Some(tx) = exit_signal_tx.take() {
|
||||
let _ = tx.send(());
|
||||
let _ = tx.send(ExecutorExitResult::Success);
|
||||
}
|
||||
|
||||
// Cancel session work
|
||||
|
||||
@@ -33,7 +33,8 @@ use crate::{
|
||||
approvals::ExecutorApprovalService,
|
||||
command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides},
|
||||
executors::{
|
||||
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
|
||||
AppendPrompt, AvailabilityInfo, ExecutorError, ExecutorExitResult, SpawnedChild,
|
||||
StandardCodingAgentExecutor,
|
||||
codex::{jsonrpc::ExitSignalSender, normalize_logs::Error},
|
||||
},
|
||||
stdout_dup::create_stdout_pipe_writer,
|
||||
@@ -195,9 +196,13 @@ impl StandardCodingAgentExecutor for Codex {
|
||||
}
|
||||
|
||||
impl Codex {
|
||||
fn build_command_builder(&self) -> CommandBuilder {
|
||||
let mut builder = CommandBuilder::new("npx -y @openai/codex@0.60.1 app-server");
|
||||
pub fn base_command() -> &'static str {
|
||||
"npx -y @openai/codex@0.60.1"
|
||||
}
|
||||
|
||||
fn build_command_builder(&self) -> CommandBuilder {
|
||||
let mut builder = CommandBuilder::new(Self::base_command());
|
||||
builder = builder.extend_params(["app-server"]);
|
||||
if self.oss.unwrap_or(false) {
|
||||
builder = builder.extend_params(["--oss"]);
|
||||
}
|
||||
@@ -328,17 +333,36 @@ impl Codex {
|
||||
)
|
||||
.await
|
||||
{
|
||||
if matches!(&err, ExecutorError::Io(io_err) if io_err.kind() == std::io::ErrorKind::BrokenPipe)
|
||||
{
|
||||
// Broken pipe likely means the parent process exited, so we can ignore it
|
||||
return;
|
||||
match &err {
|
||||
ExecutorError::Io(io_err)
|
||||
if io_err.kind() == std::io::ErrorKind::BrokenPipe =>
|
||||
{
|
||||
// Broken pipe likely means the parent process exited, so we can ignore it
|
||||
return;
|
||||
}
|
||||
ExecutorError::AuthRequired(message) => {
|
||||
log_writer
|
||||
.log_raw(&Error::auth_required(message.clone()).raw())
|
||||
.await
|
||||
.ok();
|
||||
// Send failure signal so the process is marked as failed
|
||||
exit_signal_tx
|
||||
.send_exit_signal(ExecutorExitResult::Failure)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("Codex spawn error: {}", err);
|
||||
log_writer
|
||||
.log_raw(&Error::launch_error(err.to_string()).raw())
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
tracing::error!("Codex spawn error: {}", err);
|
||||
log_writer
|
||||
.log_raw(&Error::launch_error(err.to_string()).raw())
|
||||
.await
|
||||
.ok();
|
||||
exit_signal_tx.send_exit_signal().await;
|
||||
// For other errors, also send failure signal
|
||||
exit_signal_tx
|
||||
.send_exit_signal(ExecutorExitResult::Failure)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -365,6 +389,12 @@ impl Codex {
|
||||
JsonRpcPeer::spawn(child_stdin, child_stdout, client.clone(), exit_signal_tx);
|
||||
client.connect(rpc_peer);
|
||||
client.initialize().await?;
|
||||
let auth_status = client.get_auth_status().await?;
|
||||
if auth_status.auth_method.is_none() {
|
||||
return Err(ExecutorError::AuthRequired(
|
||||
"Codex authentication required".to_string(),
|
||||
));
|
||||
}
|
||||
match resume_session {
|
||||
None => {
|
||||
let params = conversation_params;
|
||||
|
||||
@@ -8,11 +8,11 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::{
|
||||
AddConversationListenerParams, AddConversationSubscriptionResponse, ApplyPatchApprovalResponse,
|
||||
ClientInfo, ClientNotification, ClientRequest, ExecCommandApprovalResponse, InitializeParams,
|
||||
InitializeResponse, InputItem, JSONRPCError, JSONRPCNotification, JSONRPCRequest,
|
||||
JSONRPCResponse, NewConversationParams, NewConversationResponse, RequestId,
|
||||
ResumeConversationParams, ResumeConversationResponse, SendUserMessageParams,
|
||||
SendUserMessageResponse, ServerNotification, ServerRequest,
|
||||
ClientInfo, ClientNotification, ClientRequest, ExecCommandApprovalResponse,
|
||||
GetAuthStatusParams, GetAuthStatusResponse, InitializeParams, InitializeResponse, InputItem,
|
||||
JSONRPCError, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, NewConversationParams,
|
||||
NewConversationResponse, RequestId, ResumeConversationParams, ResumeConversationResponse,
|
||||
SendUserMessageParams, SendUserMessageResponse, ServerNotification, ServerRequest,
|
||||
};
|
||||
use codex_protocol::{ConversationId, protocol::ReviewDecision};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
@@ -131,6 +131,16 @@ impl AppServerClient {
|
||||
self.send_request(request, "sendUserMessage").await
|
||||
}
|
||||
|
||||
pub async fn get_auth_status(&self) -> Result<GetAuthStatusResponse, ExecutorError> {
|
||||
let request = ClientRequest::GetAuthStatus {
|
||||
request_id: self.next_request_id(),
|
||||
params: GetAuthStatusParams {
|
||||
include_token: Some(true),
|
||||
refresh_token: Some(false),
|
||||
},
|
||||
};
|
||||
self.send_request(request, "getAuthStatus").await
|
||||
}
|
||||
async fn handle_server_request(
|
||||
&self,
|
||||
peer: &JsonRpcPeer,
|
||||
@@ -454,6 +464,7 @@ fn request_id(request: &ClientRequest) -> RequestId {
|
||||
match request {
|
||||
ClientRequest::Initialize { request_id, .. }
|
||||
| ClientRequest::NewConversation { request_id, .. }
|
||||
| ClientRequest::GetAuthStatus { request_id, .. }
|
||||
| ClientRequest::ResumeConversation { request_id, .. }
|
||||
| ClientRequest::AddConversationListener { request_id, .. }
|
||||
| ClientRequest::SendUserMessage { request_id, .. } => request_id.clone(),
|
||||
|
||||
@@ -27,7 +27,7 @@ use tokio::{
|
||||
sync::{Mutex, oneshot},
|
||||
};
|
||||
|
||||
use crate::executors::ExecutorError;
|
||||
use crate::executors::{ExecutorError, ExecutorExitResult};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PendingResponse {
|
||||
@@ -38,18 +38,19 @@ pub enum PendingResponse {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExitSignalSender {
|
||||
inner: Arc<Mutex<Option<oneshot::Sender<()>>>>,
|
||||
inner: Arc<Mutex<Option<oneshot::Sender<ExecutorExitResult>>>>,
|
||||
}
|
||||
|
||||
impl ExitSignalSender {
|
||||
pub fn new(sender: oneshot::Sender<()>) -> Self {
|
||||
pub fn new(sender: oneshot::Sender<ExecutorExitResult>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(Some(sender))),
|
||||
}
|
||||
}
|
||||
pub async fn send_exit_signal(&self) {
|
||||
|
||||
pub async fn send_exit_signal(&self, result: ExecutorExitResult) {
|
||||
if let Some(sender) = self.inner.lock().await.take() {
|
||||
let _ = sender.send(());
|
||||
let _ = sender.send(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,7 @@ impl JsonRpcPeer {
|
||||
}
|
||||
}
|
||||
|
||||
exit_tx.send_exit_signal().await;
|
||||
exit_tx.send_exit_signal(ExecutorExitResult::Success).await;
|
||||
let _ = reader_peer.shutdown().await;
|
||||
});
|
||||
|
||||
|
||||
@@ -1035,12 +1035,16 @@ lazy_static! {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Error {
|
||||
LaunchError { error: String },
|
||||
AuthRequired { error: String },
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn launch_error(error: String) -> Self {
|
||||
Self::LaunchError { error }
|
||||
}
|
||||
pub fn auth_required(error: String) -> Self {
|
||||
Self::AuthRequired { error }
|
||||
}
|
||||
|
||||
pub fn raw(&self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_default()
|
||||
@@ -1049,15 +1053,23 @@ impl Error {
|
||||
|
||||
impl ToNormalizedEntry for Error {
|
||||
fn to_normalized_entry(&self) -> NormalizedEntry {
|
||||
NormalizedEntry {
|
||||
timestamp: None,
|
||||
entry_type: NormalizedEntryType::ErrorMessage {
|
||||
error_type: NormalizedEntryError::Other,
|
||||
match self {
|
||||
Error::LaunchError { error } => NormalizedEntry {
|
||||
timestamp: None,
|
||||
entry_type: NormalizedEntryType::ErrorMessage {
|
||||
error_type: NormalizedEntryError::Other,
|
||||
},
|
||||
content: error.clone(),
|
||||
metadata: None,
|
||||
},
|
||||
content: match self {
|
||||
Error::LaunchError { error } => error.clone(),
|
||||
Error::AuthRequired { error } => NormalizedEntry {
|
||||
timestamp: None,
|
||||
entry_type: NormalizedEntryType::ErrorMessage {
|
||||
error_type: NormalizedEntryError::SetupRequired,
|
||||
},
|
||||
content: error.clone(),
|
||||
metadata: None,
|
||||
},
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ pub enum ExecutorError {
|
||||
ExecutableNotFound { program: String },
|
||||
#[error("Setup helper not supported")]
|
||||
SetupHelperNotSupported,
|
||||
#[error("Auth required: {0}")]
|
||||
AuthRequired(String),
|
||||
}
|
||||
|
||||
#[enum_dispatch]
|
||||
@@ -154,10 +156,13 @@ impl CodingAgent {
|
||||
match self {
|
||||
Self::ClaudeCode(_)
|
||||
| Self::Amp(_)
|
||||
| Self::Codex(_)
|
||||
| Self::Gemini(_)
|
||||
| Self::QwenCode(_)
|
||||
| Self::Droid(_) => vec![BaseAgentCapability::SessionFork],
|
||||
Self::Codex(_) => vec![
|
||||
BaseAgentCapability::SessionFork,
|
||||
BaseAgentCapability::SetupHelper,
|
||||
],
|
||||
Self::CursorAgent(_) => vec![BaseAgentCapability::SetupHelper],
|
||||
Self::Opencode(_) | Self::Copilot(_) => vec![],
|
||||
}
|
||||
@@ -217,10 +222,19 @@ pub trait StandardCodingAgentExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result communicated through the exit signal
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ExecutorExitResult {
|
||||
/// Process completed successfully (exit code 0)
|
||||
Success,
|
||||
/// Process should be marked as failed (non-zero exit)
|
||||
Failure,
|
||||
}
|
||||
|
||||
/// Optional exit notification from an executor.
|
||||
/// When this receiver resolves, the container should gracefully stop the process
|
||||
/// and mark it as successful (exit code 0).
|
||||
pub type ExecutorExitSignal = tokio::sync::oneshot::Receiver<()>;
|
||||
/// and mark it according to the result.
|
||||
pub type ExecutorExitSignal = tokio::sync::oneshot::Receiver<ExecutorExitResult>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpawnedChild {
|
||||
|
||||
@@ -28,7 +28,7 @@ use deployment::{DeploymentError, RemoteClientNotConfigured};
|
||||
use executors::{
|
||||
actions::{Executable, ExecutorAction},
|
||||
approvals::{ExecutorApprovalService, NoopExecutorApprovalService},
|
||||
executors::BaseCodingAgent,
|
||||
executors::{BaseCodingAgent, ExecutorExitResult, ExecutorExitSignal},
|
||||
logs::{
|
||||
NormalizedEntryType,
|
||||
utils::{
|
||||
@@ -284,7 +284,7 @@ impl LocalContainerService {
|
||||
pub fn spawn_exit_monitor(
|
||||
&self,
|
||||
exec_id: &Uuid,
|
||||
exit_signal: Option<tokio::sync::oneshot::Receiver<()>>,
|
||||
exit_signal: Option<ExecutorExitSignal>,
|
||||
) -> JoinHandle<()> {
|
||||
let exec_id = *exec_id;
|
||||
let child_store = self.child_store.clone();
|
||||
@@ -299,25 +299,31 @@ impl LocalContainerService {
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut exit_signal_future = exit_signal
|
||||
.map(|rx| rx.map(|_| ()).boxed()) // wait for signal
|
||||
.unwrap_or_else(|| std::future::pending::<()>().boxed()); // no signal, stall forever
|
||||
.map(|rx| rx.boxed()) // wait for result
|
||||
.unwrap_or_else(|| std::future::pending().boxed()); // no signal, stall forever
|
||||
|
||||
let status_result: std::io::Result<std::process::ExitStatus>;
|
||||
|
||||
// Wait for process to exit, or exit signal from executor
|
||||
tokio::select! {
|
||||
// Exit signal.
|
||||
// Exit signal with result.
|
||||
// Some coding agent processes do not automatically exit after processing the user request; instead the executor
|
||||
// signals when processing has finished to gracefully kill the process.
|
||||
_ = &mut exit_signal_future => {
|
||||
// Executor signaled completion: kill group and remember to force Completed(0)
|
||||
exit_result = &mut exit_signal_future => {
|
||||
// Executor signaled completion: kill group and use the provided result
|
||||
if let Some(child_lock) = child_store.read().await.get(&exec_id).cloned() {
|
||||
let mut child = child_lock.write().await ;
|
||||
if let Err(err) = command::kill_process_group(&mut child).await {
|
||||
tracing::error!("Failed to kill process group after exit signal: {} {}", exec_id, err);
|
||||
}
|
||||
}
|
||||
status_result = Ok(success_exit_status());
|
||||
|
||||
// Map the exit result to appropriate exit status
|
||||
status_result = match exit_result {
|
||||
Ok(ExecutorExitResult::Success) => Ok(success_exit_status()),
|
||||
Ok(ExecutorExitResult::Failure) => Ok(failure_exit_status()),
|
||||
Err(_) => Ok(success_exit_status()), // Channel closed, assume success
|
||||
};
|
||||
}
|
||||
// Process exit
|
||||
exit_status_result = &mut process_exit_rx => {
|
||||
@@ -810,6 +816,19 @@ impl LocalContainerService {
|
||||
}
|
||||
}
|
||||
|
||||
fn failure_exit_status() -> std::process::ExitStatus {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
ExitStatusExt::from_raw(256) // Exit code 1 (shifted by 8 bits)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::ExitStatusExt;
|
||||
ExitStatusExt::from_raw(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContainerService for LocalContainerService {
|
||||
fn msg_stores(&self) -> &Arc<RwLock<HashMap<Uuid, Arc<MsgStore>>>> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod codex_setup;
|
||||
pub mod cursor_setup;
|
||||
pub mod drafts;
|
||||
pub mod gh_cli_setup;
|
||||
@@ -191,6 +192,9 @@ pub async fn run_agent_setup(
|
||||
CodingAgent::CursorAgent(_) => {
|
||||
cursor_setup::run_cursor_setup(&deployment, &task_attempt).await?;
|
||||
}
|
||||
CodingAgent::Codex(codex) => {
|
||||
codex_setup::run_codex_setup(&deployment, &task_attempt, &codex).await?;
|
||||
}
|
||||
_ => return Err(ApiError::Executor(ExecutorError::SetupHelperNotSupported)),
|
||||
}
|
||||
|
||||
|
||||
79
crates/server/src/routes/task_attempts/codex_setup.rs
Normal file
79
crates/server/src/routes/task_attempts/codex_setup.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use db::models::{
|
||||
execution_process::{ExecutionProcess, ExecutionProcessRunReason},
|
||||
task_attempt::{TaskAttempt, TaskAttemptError},
|
||||
};
|
||||
use deployment::Deployment;
|
||||
use executors::{actions::ExecutorAction, executors::ExecutorError};
|
||||
#[cfg(unix)]
|
||||
use executors::{
|
||||
actions::{
|
||||
ExecutorActionType,
|
||||
script::{ScriptContext, ScriptRequest, ScriptRequestLanguage},
|
||||
},
|
||||
command::CommandBuilder,
|
||||
command::apply_overrides,
|
||||
executors::codex::Codex,
|
||||
};
|
||||
use services::services::container::ContainerService;
|
||||
|
||||
use crate::{error::ApiError, routes::task_attempts::ensure_worktree_path};
|
||||
|
||||
pub async fn run_codex_setup(
|
||||
deployment: &crate::DeploymentImpl,
|
||||
task_attempt: &TaskAttempt,
|
||||
codex: &Codex,
|
||||
) -> Result<ExecutionProcess, ApiError> {
|
||||
let latest_process = ExecutionProcess::find_latest_by_task_attempt_and_run_reason(
|
||||
&deployment.db().pool,
|
||||
task_attempt.id,
|
||||
&ExecutionProcessRunReason::CodingAgent,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let executor_action = if let Some(latest_process) = latest_process {
|
||||
let latest_action = latest_process
|
||||
.executor_action()
|
||||
.map_err(|e| ApiError::TaskAttempt(TaskAttemptError::ValidationError(e.to_string())))?;
|
||||
get_setup_helper_action(codex)
|
||||
.await?
|
||||
.append_action(latest_action.to_owned())
|
||||
} else {
|
||||
get_setup_helper_action(codex).await?
|
||||
};
|
||||
|
||||
let _ = ensure_worktree_path(deployment, task_attempt).await?;
|
||||
|
||||
let execution_process = deployment
|
||||
.container()
|
||||
.start_execution(
|
||||
task_attempt,
|
||||
&executor_action,
|
||||
&ExecutionProcessRunReason::SetupScript,
|
||||
)
|
||||
.await?;
|
||||
Ok(execution_process)
|
||||
}
|
||||
|
||||
async fn get_setup_helper_action(codex: &Codex) -> Result<ExecutorAction, ApiError> {
|
||||
let mut login_command = CommandBuilder::new(Codex::base_command());
|
||||
login_command = login_command.extend_params(["login"]);
|
||||
login_command = apply_overrides(login_command, &codex.cmd);
|
||||
|
||||
let (program_path, args) = login_command
|
||||
.build_initial()
|
||||
.map_err(|err| ApiError::Executor(ExecutorError::from(err)))?
|
||||
.into_resolved()
|
||||
.await
|
||||
.map_err(ApiError::Executor)?;
|
||||
let login_script = format!("{} {}", program_path.to_string_lossy(), args.join(" "));
|
||||
let login_request = ScriptRequest {
|
||||
script: login_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::ToolInstallScript,
|
||||
};
|
||||
|
||||
Ok(ExecutorAction::new(
|
||||
ExecutorActionType::ScriptRequest(login_request),
|
||||
None,
|
||||
))
|
||||
}
|
||||
@@ -85,7 +85,7 @@ fi"#
|
||||
let install_request = ScriptRequest {
|
||||
script: install_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::SetupScript,
|
||||
context: ScriptContext::ToolInstallScript,
|
||||
};
|
||||
// Second action (chained): Login
|
||||
let login_script = format!(
|
||||
@@ -98,7 +98,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
||||
let login_request = ScriptRequest {
|
||||
script: login_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::SetupScript,
|
||||
context: ScriptContext::ToolInstallScript,
|
||||
};
|
||||
|
||||
// Chain them: install → login
|
||||
|
||||
@@ -71,7 +71,7 @@ fi"#
|
||||
let install_request = ScriptRequest {
|
||||
script: install_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::GithubCliSetupScript,
|
||||
context: ScriptContext::ToolInstallScript,
|
||||
};
|
||||
|
||||
// Auth script
|
||||
@@ -85,7 +85,7 @@ gh auth login --web --git-protocol https --skip-ssh-key
|
||||
let auth_request = ScriptRequest {
|
||||
script: auth_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::GithubCliSetupScript,
|
||||
context: ScriptContext::ToolInstallScript,
|
||||
};
|
||||
|
||||
// Chain them: install → auth
|
||||
|
||||
@@ -125,6 +125,14 @@ pub trait ContainerService {
|
||||
/// - The next action is None (no follow-up actions)
|
||||
/// - The run reason is not DevServer
|
||||
fn should_finalize(&self, ctx: &ExecutionContext) -> bool {
|
||||
// Always finalize failed or killed executions
|
||||
if matches!(
|
||||
ctx.execution_process.status,
|
||||
ExecutionProcessStatus::Failed | ExecutionProcessStatus::Killed
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, finalize only if no next action and not a dev server
|
||||
ctx.execution_process
|
||||
.executor_action()
|
||||
.unwrap()
|
||||
|
||||
@@ -434,7 +434,7 @@ const ToolCallCard: React.FC<{
|
||||
: undefined;
|
||||
|
||||
// Compute defaults from entry
|
||||
const linkifyUrls = entryType?.tool_name === 'GitHub CLI Setup Script';
|
||||
const linkifyUrls = entryType?.tool_name === 'Tool Install Script';
|
||||
const defaultExpanded = linkifyUrls;
|
||||
|
||||
const [expanded, toggle] = useExpandable(
|
||||
|
||||
@@ -326,8 +326,8 @@ export const useConversationHistory = ({
|
||||
case 'CleanupScript':
|
||||
toolName = 'Cleanup Script';
|
||||
break;
|
||||
case 'GithubCliSetupScript':
|
||||
toolName = 'GitHub CLI Setup Script';
|
||||
case 'ToolInstallScript':
|
||||
toolName = 'Tool Install Script';
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
|
||||
@@ -28,7 +28,7 @@ export type McpConfig = { servers: { [key in string]?: JsonValue }, servers_path
|
||||
|
||||
export type ExecutorActionType = { "type": "CodingAgentInitialRequest" } & CodingAgentInitialRequest | { "type": "CodingAgentFollowUpRequest" } & CodingAgentFollowUpRequest | { "type": "ScriptRequest" } & ScriptRequest;
|
||||
|
||||
export type ScriptContext = "SetupScript" | "CleanupScript" | "DevServer" | "GithubCliSetupScript";
|
||||
export type ScriptContext = "SetupScript" | "CleanupScript" | "DevServer" | "ToolInstallScript";
|
||||
|
||||
export type ScriptRequest = { script: string, language: ScriptRequestLanguage, context: ScriptContext, };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user