feat: manual approvals (#748)

* manual user approvals

* refactor implementation

* cleanup

* fix lint errors

* i18n

* remove isLastEntry frontend check

* address fe feedback

* always run claude plan with approvals

* add watchkill script back to plan mode

* update timeout

* tooltip hover

* use response type

* put back watchkill append hack
This commit is contained in:
Gabriel Gordon-Hall
2025-09-22 16:02:42 +01:00
committed by GitHub
parent eaff3dee9e
commit 798bcb80a3
51 changed files with 1808 additions and 198 deletions

View File

@@ -0,0 +1,69 @@
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use uuid::Uuid;
pub const APPROVAL_TIMEOUT_SECONDS: i64 = 3600; // 1 hour
pub const EXIT_PLAN_MODE_TOOL_NAME: &str = "ExitPlanMode";
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct ApprovalRequest {
pub id: String,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub session_id: String,
pub created_at: DateTime<Utc>,
pub timeout_at: DateTime<Utc>,
}
impl ApprovalRequest {
pub fn from_create(request: CreateApprovalRequest) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
tool_name: request.tool_name,
tool_input: request.tool_input,
session_id: request.session_id,
created_at: now,
timeout_at: now + Duration::seconds(APPROVAL_TIMEOUT_SECONDS),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct CreateApprovalRequest {
pub tool_name: String,
pub tool_input: serde_json::Value,
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ApprovalStatus {
Pending,
Approved,
Denied {
#[ts(optional)]
reason: Option<String>,
},
TimedOut,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ApprovalResponse {
pub execution_process_id: Uuid,
pub status: ApprovalStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct ApprovalPendingInfo {
pub approval_id: String,
pub execution_process_id: Uuid,
pub tool_name: String,
pub requested_at: DateTime<Utc>,
pub timeout_at: DateTime<Utc>,
}

View File

@@ -2,6 +2,7 @@ use std::{env, sync::OnceLock};
use directories::ProjectDirs;
pub mod approvals;
pub mod assets;
pub mod browser;
pub mod diff;

View File

@@ -67,6 +67,7 @@ impl MsgStore {
pub fn push_stdout<S: Into<String>>(&self, s: S) {
self.push(LogMsg::Stdout(s.into()));
}
pub fn push_stderr<S: Into<String>>(&self, s: S) {
self.push(LogMsg::Stderr(s.into()));
}
@@ -85,6 +86,7 @@ impl MsgStore {
pub fn get_receiver(&self) -> broadcast::Receiver<LogMsg> {
self.sender.subscribe()
}
pub fn get_history(&self) -> Vec<LogMsg> {
self.inner
.read()

View File

@@ -10,3 +10,25 @@ pub async fn write_port_file(port: u16) -> std::io::Result<PathBuf> {
fs::write(&path, port.to_string()).await?;
Ok(path)
}
pub async fn read_port_file(app_name: &str) -> std::io::Result<u16> {
let base = if cfg!(target_os = "linux") {
match env::var("XDG_RUNTIME_DIR") {
Ok(val) if !val.is_empty() => PathBuf::from(val),
_ => env::temp_dir(),
}
} else {
env::temp_dir()
};
let path = base.join(app_name).join(format!("{app_name}.port"));
tracing::debug!("Reading port from {:?}", path);
let content = fs::read_to_string(&path).await?;
let port: u16 = content
.trim()
.parse()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(port)
}