继续多语言处理
This commit is contained in:
@@ -8,6 +8,8 @@ use crate::db::error::DbError;
|
|||||||
pub enum AppErrorCode {
|
pub enum AppErrorCode {
|
||||||
Unknown,
|
Unknown,
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
ConfigurationMissing,
|
||||||
|
ConfigurationInvalid,
|
||||||
NotFound,
|
NotFound,
|
||||||
AlreadyExists,
|
AlreadyExists,
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
@@ -81,3 +83,15 @@ impl From<DbError> for AppCommandError {
|
|||||||
Self::db(value)
|
Self::db(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<String> for AppCommandError {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::new(AppErrorCode::Unknown, "Operation failed").with_detail(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for AppCommandError {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::from(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,13 +148,18 @@ fn parse_count_from_output(stdout: &[u8]) -> Option<usize> {
|
|||||||
String::from_utf8_lossy(stdout).trim().parse::<usize>().ok()
|
String::from_utf8_lossy(stdout).trim().parse::<usize>().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_head_hash(path: &str) -> Result<Option<String>, String> {
|
fn git_command_error(operation: &str, stderr: &[u8]) -> AppCommandError {
|
||||||
|
let stderr = String::from_utf8_lossy(stderr).trim().to_string();
|
||||||
|
AppCommandError::external_command(format!("git {operation} failed"), stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_head_hash(path: &str) -> Result<Option<String>, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -167,17 +172,16 @@ async fn get_head_hash(path: &str) -> Result<Option<String>, String> {
|
|||||||
Ok(Some(head))
|
Ok(Some(head))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_files_in_commit(path: &str, commit: &str) -> Result<usize, String> {
|
async fn count_files_in_commit(path: &str, commit: &str) -> Result<usize, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["show", "--name-only", "--pretty=format:", commit])
|
.args(["show", "--name-only", "--pretty=format:", commit])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("show", &output.stderr));
|
||||||
return Err(format!("git show failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(count_non_empty_lines(&String::from_utf8_lossy(
|
Ok(count_non_empty_lines(&String::from_utf8_lossy(
|
||||||
@@ -185,18 +189,21 @@ async fn count_files_in_commit(path: &str, commit: &str) -> Result<usize, String
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn count_changed_files_between(path: &str, base: &str, head: &str) -> Result<usize, String> {
|
async fn count_changed_files_between(
|
||||||
|
path: &str,
|
||||||
|
base: &str,
|
||||||
|
head: &str,
|
||||||
|
) -> Result<usize, AppCommandError> {
|
||||||
let range = format!("{}..{}", base, head);
|
let range = format!("{}..{}", base, head);
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["diff", "--name-only", &range])
|
.args(["diff", "--name-only", &range])
|
||||||
.current_dir(path)
|
.current_dir(path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("diff", &output.stderr));
|
||||||
return Err(format!("git diff failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(count_non_empty_lines(&String::from_utf8_lossy(
|
Ok(count_non_empty_lines(&String::from_utf8_lossy(
|
||||||
@@ -337,8 +344,8 @@ pub async fn save_folder_opened_conversations(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_folder_directory(path: String) -> Result<(), String> {
|
pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError> {
|
||||||
std::fs::create_dir_all(&path).map_err(|e| e.to_string())
|
std::fs::create_dir_all(&path).map_err(AppCommandError::io)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -427,13 +434,13 @@ fn classify_git_clone_error(stderr: &str) -> AppCommandError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_git_branch(path: String) -> Result<Option<String>, String> {
|
pub async fn get_git_branch(path: String) -> Result<Option<String>, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -447,23 +454,22 @@ pub async fn get_git_branch(path: String) -> Result<Option<String>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_init(path: String) -> Result<(), String> {
|
pub async fn git_init(path: String) -> Result<(), AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["init"])
|
.args(["init"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("init", &output.stderr));
|
||||||
return Err(format!("git init failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_pull(path: String) -> Result<GitPullResult, String> {
|
pub async fn git_pull(path: String) -> Result<GitPullResult, AppCommandError> {
|
||||||
let head_before = get_head_hash(&path).await?;
|
let head_before = get_head_hash(&path).await?;
|
||||||
|
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
@@ -471,11 +477,10 @@ pub async fn git_pull(path: String) -> Result<GitPullResult, String> {
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("pull", &output.stderr));
|
||||||
return Err(format!("git pull failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let head_after = get_head_hash(&path).await?;
|
let head_after = get_head_hash(&path).await?;
|
||||||
@@ -491,23 +496,22 @@ pub async fn git_pull(path: String) -> Result<GitPullResult, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_fetch(path: String) -> Result<String, String> {
|
pub async fn git_fetch(path: String) -> Result<String, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["fetch", "--all"])
|
.args(["fetch", "--all"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("fetch --all", &output.stderr));
|
||||||
return Err(format!("git fetch failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_push(path: String) -> Result<GitPushResult, String> {
|
pub async fn git_push(path: String) -> Result<GitPushResult, AppCommandError> {
|
||||||
let pushed_commits = estimate_push_commit_count(&path).await;
|
let pushed_commits = estimate_push_commit_count(&path).await;
|
||||||
|
|
||||||
// Check if the current branch has an upstream configured
|
// Check if the current branch has an upstream configured
|
||||||
@@ -516,7 +520,7 @@ pub async fn git_push(path: String) -> Result<GitPushResult, String> {
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
let has_upstream = upstream_check.status.success();
|
let has_upstream = upstream_check.status.success();
|
||||||
|
|
||||||
@@ -527,7 +531,7 @@ pub async fn git_push(path: String) -> Result<GitPushResult, String> {
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
let branch = String::from_utf8_lossy(&branch_output.stdout)
|
let branch = String::from_utf8_lossy(&branch_output.stdout)
|
||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
@@ -537,19 +541,18 @@ pub async fn git_push(path: String) -> Result<GitPushResult, String> {
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(AppCommandError::io)?
|
||||||
} else {
|
} else {
|
||||||
crate::process::tokio_command("git")
|
crate::process::tokio_command("git")
|
||||||
.args(["push"])
|
.args(["push"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(AppCommandError::io)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("push", &output.stderr));
|
||||||
return Err(format!("git push failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GitPushResult {
|
Ok(GitPushResult {
|
||||||
@@ -563,7 +566,7 @@ pub async fn git_new_branch(
|
|||||||
path: String,
|
path: String,
|
||||||
branch_name: String,
|
branch_name: String,
|
||||||
start_point: Option<String>,
|
start_point: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppCommandError> {
|
||||||
let mut args = vec!["checkout".to_string(), "-b".to_string(), branch_name];
|
let mut args = vec!["checkout".to_string(), "-b".to_string(), branch_name];
|
||||||
if let Some(start_point) = start_point {
|
if let Some(start_point) = start_point {
|
||||||
let trimmed = start_point.trim();
|
let trimmed = start_point.trim();
|
||||||
@@ -577,11 +580,10 @@ pub async fn git_new_branch(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("checkout -b", &output.stderr));
|
||||||
return Err(format!("git checkout -b failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -591,7 +593,7 @@ pub async fn git_worktree_add(
|
|||||||
path: String,
|
path: String,
|
||||||
branch_name: String,
|
branch_name: String,
|
||||||
worktree_path: String,
|
worktree_path: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), AppCommandError> {
|
||||||
// 校验分支是否已存在
|
// 校验分支是否已存在
|
||||||
let check = crate::process::tokio_command("git")
|
let check = crate::process::tokio_command("git")
|
||||||
.args([
|
.args([
|
||||||
@@ -602,14 +604,22 @@ pub async fn git_worktree_add(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
if check.status.success() {
|
if check.status.success() {
|
||||||
return Err(format!("分支 '{}' 已存在", branch_name));
|
return Err(AppCommandError::new(
|
||||||
|
AppErrorCode::AlreadyExists,
|
||||||
|
"Branch already exists",
|
||||||
|
)
|
||||||
|
.with_detail(branch_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验目录是否已存在
|
// 校验目录是否已存在
|
||||||
if std::path::Path::new(&worktree_path).exists() {
|
if std::path::Path::new(&worktree_path).exists() {
|
||||||
return Err(format!("目录 '{}' 已存在", worktree_path));
|
return Err(AppCommandError::new(
|
||||||
|
AppErrorCode::AlreadyExists,
|
||||||
|
"Worktree directory already exists",
|
||||||
|
)
|
||||||
|
.with_detail(worktree_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行 git worktree add -b <branch> <path>
|
// 执行 git worktree add -b <branch> <path>
|
||||||
@@ -618,43 +628,40 @@ pub async fn git_worktree_add(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("worktree add", &output.stderr));
|
||||||
return Err(format!("git worktree add failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_checkout(path: String, branch_name: String) -> Result<(), String> {
|
pub async fn git_checkout(path: String, branch_name: String) -> Result<(), AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["checkout", &branch_name])
|
.args(["checkout", &branch_name])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("checkout", &output.stderr));
|
||||||
return Err(format!("git checkout failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_list_branches(path: String) -> Result<Vec<String>, String> {
|
pub async fn git_list_branches(path: String) -> Result<Vec<String>, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["branch", "--format=%(refname:short)"])
|
.args(["branch", "--format=%(refname:short)"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("branch", &output.stderr));
|
||||||
return Err(format!("git branch failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let branches = String::from_utf8_lossy(&output.stdout)
|
let branches = String::from_utf8_lossy(&output.stdout)
|
||||||
@@ -666,49 +673,46 @@ pub async fn git_list_branches(path: String) -> Result<Vec<String>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_stash(path: String) -> Result<String, String> {
|
pub async fn git_stash(path: String) -> Result<String, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["stash"])
|
.args(["stash"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("stash", &output.stderr));
|
||||||
return Err(format!("git stash failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_stash_pop(path: String) -> Result<String, String> {
|
pub async fn git_stash_pop(path: String) -> Result<String, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["stash", "pop"])
|
.args(["stash", "pop"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("stash pop", &output.stderr));
|
||||||
return Err(format!("git stash pop failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_status(path: String) -> Result<Vec<GitStatusEntry>, String> {
|
pub async fn git_status(path: String) -> Result<Vec<GitStatusEntry>, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["status", "--porcelain=v1", "-uall"])
|
.args(["status", "--porcelain=v1", "-uall"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("status", &output.stderr));
|
||||||
return Err(format!("git status failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = String::from_utf8_lossy(&output.stdout)
|
let entries = String::from_utf8_lossy(&output.stdout)
|
||||||
@@ -724,7 +728,7 @@ pub async fn git_status(path: String) -> Result<Vec<GitStatusEntry>, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_is_tracked(path: String, file: String) -> Result<bool, String> {
|
pub async fn git_is_tracked(path: String, file: String) -> Result<bool, AppCommandError> {
|
||||||
let literal_file = to_git_literal_pathspec(&file);
|
let literal_file = to_git_literal_pathspec(&file);
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["ls-files", "--error-unmatch", "--"])
|
.args(["ls-files", "--error-unmatch", "--"])
|
||||||
@@ -732,13 +736,13 @@ pub async fn git_is_tracked(path: String, file: String) -> Result<bool, String>
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
Ok(output.status.success())
|
Ok(output.status.success())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_diff(path: String, file: Option<String>) -> Result<String, String> {
|
pub async fn git_diff(path: String, file: Option<String>) -> Result<String, AppCommandError> {
|
||||||
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
||||||
let mut args = vec!["diff".to_string(), "HEAD".to_string()];
|
let mut args = vec!["diff".to_string(), "HEAD".to_string()];
|
||||||
if let Some(ref f) = literal_file {
|
if let Some(ref f) = literal_file {
|
||||||
@@ -751,7 +755,7 @@ pub async fn git_diff(path: String, file: Option<String>) -> Result<String, Stri
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
// For new repos with no HEAD, fall back to diff --cached
|
// For new repos with no HEAD, fall back to diff --cached
|
||||||
@@ -765,7 +769,7 @@ pub async fn git_diff(path: String, file: Option<String>) -> Result<String, Stri
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
return Ok(String::from_utf8_lossy(&fallback.stdout).to_string());
|
return Ok(String::from_utf8_lossy(&fallback.stdout).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,10 +781,13 @@ pub async fn git_diff_with_branch(
|
|||||||
path: String,
|
path: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppCommandError> {
|
||||||
let target_branch = branch.trim();
|
let target_branch = branch.trim();
|
||||||
if target_branch.is_empty() {
|
if target_branch.is_empty() {
|
||||||
return Err("Branch name cannot be empty".to_string());
|
return Err(AppCommandError::new(
|
||||||
|
AppErrorCode::InvalidInput,
|
||||||
|
"Branch name cannot be empty",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
||||||
@@ -799,15 +806,14 @@ pub async fn git_diff_with_branch(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
return Err(format!(
|
return Err(
|
||||||
"git diff {} failed: {}",
|
AppCommandError::external_command("git diff failed", stderr)
|
||||||
target_branch,
|
.with_detail(format!("branch={target_branch}")),
|
||||||
stderr.trim()
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
@@ -818,7 +824,7 @@ pub async fn git_show_diff(
|
|||||||
path: String,
|
path: String,
|
||||||
commit: String,
|
commit: String,
|
||||||
file: Option<String>,
|
file: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppCommandError> {
|
||||||
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
let literal_file = file.as_deref().map(to_git_literal_pathspec);
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"show".to_string(),
|
"show".to_string(),
|
||||||
@@ -836,11 +842,10 @@ pub async fn git_show_diff(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("show", &output.stderr));
|
||||||
return Err(format!("git show failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
@@ -851,7 +856,7 @@ pub async fn git_show_file(
|
|||||||
path: String,
|
path: String,
|
||||||
file: String,
|
file: String,
|
||||||
ref_name: Option<String>,
|
ref_name: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppCommandError> {
|
||||||
let git_ref = ref_name.unwrap_or_else(|| "HEAD".to_string());
|
let git_ref = ref_name.unwrap_or_else(|| "HEAD".to_string());
|
||||||
let file_spec = format!("{}:{}", git_ref, file);
|
let file_spec = format!("{}:{}", git_ref, file);
|
||||||
|
|
||||||
@@ -860,7 +865,7 @@ pub async fn git_show_file(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
// File doesn't exist at this ref (e.g. new/untracked file) — return empty
|
// File doesn't exist at this ref (e.g. new/untracked file) — return empty
|
||||||
@@ -869,7 +874,11 @@ pub async fn git_show_file(
|
|||||||
|
|
||||||
let bytes = &output.stdout;
|
let bytes = &output.stdout;
|
||||||
if bytes.iter().take(2048).any(|b| *b == 0) {
|
if bytes.iter().take(2048).any(|b| *b == 0) {
|
||||||
return Err("Binary files are not supported".to_string());
|
return Err(AppCommandError::new(
|
||||||
|
AppErrorCode::InvalidInput,
|
||||||
|
"Binary files are not supported",
|
||||||
|
)
|
||||||
|
.with_detail(file_spec));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(bytes).to_string())
|
Ok(String::from_utf8_lossy(bytes).to_string())
|
||||||
@@ -882,7 +891,7 @@ pub async fn git_commit(
|
|||||||
path: String,
|
path: String,
|
||||||
message: String,
|
message: String,
|
||||||
files: Vec<String>,
|
files: Vec<String>,
|
||||||
) -> Result<GitCommitResult, String> {
|
) -> Result<GitCommitResult, AppCommandError> {
|
||||||
// Stage selected files
|
// Stage selected files
|
||||||
let mut add_args = vec!["add".to_string(), "--".to_string()];
|
let mut add_args = vec!["add".to_string(), "--".to_string()];
|
||||||
add_args.extend(files.iter().map(|file| to_git_literal_pathspec(file)));
|
add_args.extend(files.iter().map(|file| to_git_literal_pathspec(file)));
|
||||||
@@ -892,11 +901,10 @@ pub async fn git_commit(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !add_output.status.success() {
|
if !add_output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&add_output.stderr);
|
return Err(git_command_error("add", &add_output.stderr));
|
||||||
return Err(format!("git add failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
@@ -905,11 +913,10 @@ pub async fn git_commit(
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !commit_output.status.success() {
|
if !commit_output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&commit_output.stderr);
|
return Err(git_command_error("commit", &commit_output.stderr));
|
||||||
return Err(format!("git commit failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let committed_files = count_files_in_commit(&path, "HEAD")
|
let committed_files = count_files_in_commit(&path, "HEAD")
|
||||||
@@ -934,10 +941,13 @@ pub async fn git_commit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_rollback_file(path: String, file: String) -> Result<(), String> {
|
pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppCommandError> {
|
||||||
let target = file.trim();
|
let target = file.trim();
|
||||||
if target.is_empty() {
|
if target.is_empty() {
|
||||||
return Err("File path cannot be empty".to_string());
|
return Err(AppCommandError::new(
|
||||||
|
AppErrorCode::InvalidInput,
|
||||||
|
"File path cannot be empty",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let literal_file = to_git_literal_pathspec(target);
|
let literal_file = to_git_literal_pathspec(target);
|
||||||
@@ -953,7 +963,7 @@ pub async fn git_rollback_file(path: String, file: String) -> Result<(), String>
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if restore_output.status.success() {
|
if restore_output.status.success() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -969,7 +979,10 @@ pub async fn git_rollback_file(path: String, file: String) -> Result<(), String>
|
|||||||
&& !restore_stderr_lower.contains("did you mean");
|
&& !restore_stderr_lower.contains("did you mean");
|
||||||
|
|
||||||
if supports_restore {
|
if supports_restore {
|
||||||
return Err(format!("git restore failed: {}", restore_stderr));
|
return Err(AppCommandError::external_command(
|
||||||
|
"git restore failed",
|
||||||
|
restore_stderr,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = crate::process::tokio_command("git")
|
let _ = crate::process::tokio_command("git")
|
||||||
@@ -977,25 +990,24 @@ pub async fn git_rollback_file(path: String, file: String) -> Result<(), String>
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
let checkout_output = crate::process::tokio_command("git")
|
let checkout_output = crate::process::tokio_command("git")
|
||||||
.args(["checkout", "--", &literal_file])
|
.args(["checkout", "--", &literal_file])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !checkout_output.status.success() {
|
if !checkout_output.status.success() {
|
||||||
let checkout_stderr = String::from_utf8_lossy(&checkout_output.stderr);
|
return Err(git_command_error("checkout --", &checkout_output.stderr));
|
||||||
return Err(format!("git checkout failed: {}", checkout_stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_add_files(path: String, files: Vec<String>) -> Result<(), String> {
|
pub async fn git_add_files(path: String, files: Vec<String>) -> Result<(), AppCommandError> {
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -1008,18 +1020,17 @@ pub async fn git_add_files(path: String, files: Vec<String>) -> Result<(), Strin
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("add", &output.stderr));
|
||||||
return Err(format!("git add failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, String> {
|
pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCommandError> {
|
||||||
let local_fut = crate::process::tokio_command("git")
|
let local_fut = crate::process::tokio_command("git")
|
||||||
.args(["branch", "--format=%(refname:short)"])
|
.args(["branch", "--format=%(refname:short)"])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
@@ -1037,10 +1048,9 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, String
|
|||||||
|
|
||||||
let (local_output, remote_output, wt_output) = tokio::join!(local_fut, remote_fut, wt_fut);
|
let (local_output, remote_output, wt_output) = tokio::join!(local_fut, remote_fut, wt_fut);
|
||||||
|
|
||||||
let local_output = local_output.map_err(|e| e.to_string())?;
|
let local_output = local_output.map_err(AppCommandError::io)?;
|
||||||
if !local_output.status.success() {
|
if !local_output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&local_output.stderr);
|
return Err(git_command_error("branch", &local_output.stderr));
|
||||||
return Err(format!("git branch failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let local: Vec<String> = String::from_utf8_lossy(&local_output.stdout)
|
let local: Vec<String> = String::from_utf8_lossy(&local_output.stdout)
|
||||||
@@ -1094,14 +1104,14 @@ pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, String
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_merge(path: String, branch_name: String) -> Result<GitMergeResult, String> {
|
pub async fn git_merge(path: String, branch_name: String) -> Result<GitMergeResult, AppCommandError> {
|
||||||
// Count commits to be merged before performing merge
|
// Count commits to be merged before performing merge
|
||||||
let count_output = crate::process::tokio_command("git")
|
let count_output = crate::process::tokio_command("git")
|
||||||
.args(["rev-list", "--count", &format!("HEAD..{}", branch_name)])
|
.args(["rev-list", "--count", &format!("HEAD..{}", branch_name)])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
let merged_commits = if count_output.status.success() {
|
let merged_commits = if count_output.status.success() {
|
||||||
String::from_utf8_lossy(&count_output.stdout)
|
String::from_utf8_lossy(&count_output.stdout)
|
||||||
@@ -1117,27 +1127,25 @@ pub async fn git_merge(path: String, branch_name: String) -> Result<GitMergeResu
|
|||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("merge", &output.stderr));
|
||||||
return Err(format!("git merge failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(GitMergeResult { merged_commits })
|
Ok(GitMergeResult { merged_commits })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn git_rebase(path: String, branch_name: String) -> Result<String, String> {
|
pub async fn git_rebase(path: String, branch_name: String) -> Result<String, AppCommandError> {
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["rebase", &branch_name])
|
.args(["rebase", &branch_name])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error("rebase", &output.stderr));
|
||||||
return Err(format!("git rebase failed: {}", stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
@@ -1147,18 +1155,17 @@ pub async fn git_delete_branch(
|
|||||||
path: String,
|
path: String,
|
||||||
branch_name: String,
|
branch_name: String,
|
||||||
force: bool,
|
force: bool,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, AppCommandError> {
|
||||||
let flag = if force { "-D" } else { "-d" };
|
let flag = if force { "-D" } else { "-d" };
|
||||||
let output = crate::process::tokio_command("git")
|
let output = crate::process::tokio_command("git")
|
||||||
.args(["branch", flag, &branch_name])
|
.args(["branch", flag, &branch_name])
|
||||||
.current_dir(&path)
|
.current_dir(&path)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(AppCommandError::io)?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
return Err(git_command_error(&format!("branch {flag}"), &output.stderr));
|
||||||
return Err(format!("git branch {} failed: {}", flag, stderr.trim()));
|
|
||||||
}
|
}
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ fn normalize_proxy_settings(
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
AppCommandError::new(
|
AppCommandError::new(
|
||||||
AppErrorCode::InvalidInput,
|
AppErrorCode::ConfigurationMissing,
|
||||||
"Proxy URL is required when proxy is enabled",
|
"Proxy URL is required when proxy is enabled",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
reqwest::Proxy::all(proxy_url).map_err(|e| {
|
reqwest::Proxy::all(proxy_url).map_err(|e| {
|
||||||
AppCommandError::new(AppErrorCode::InvalidInput, "Invalid proxy URL")
|
AppCommandError::new(AppErrorCode::ConfigurationInvalid, "Invalid proxy URL")
|
||||||
.with_detail(e.to_string())
|
.with_detail(e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ pub(crate) async fn load_system_proxy_settings(
|
|||||||
|
|
||||||
let parsed = serde_json::from_str::<SystemProxySettings>(&raw).map_err(|e| {
|
let parsed = serde_json::from_str::<SystemProxySettings>(&raw).map_err(|e| {
|
||||||
AppCommandError::new(
|
AppCommandError::new(
|
||||||
AppErrorCode::InvalidInput,
|
AppErrorCode::ConfigurationInvalid,
|
||||||
"Failed to parse stored proxy settings",
|
"Failed to parse stored proxy settings",
|
||||||
)
|
)
|
||||||
.with_detail(e.to_string())
|
.with_detail(e.to_string())
|
||||||
@@ -84,7 +84,7 @@ pub(crate) async fn load_system_language_settings(
|
|||||||
|
|
||||||
serde_json::from_str::<SystemLanguageSettings>(&raw).map_err(|e| {
|
serde_json::from_str::<SystemLanguageSettings>(&raw).map_err(|e| {
|
||||||
AppCommandError::new(
|
AppCommandError::new(
|
||||||
AppErrorCode::InvalidInput,
|
AppErrorCode::ConfigurationInvalid,
|
||||||
"Failed to parse stored language settings",
|
"Failed to parse stored language settings",
|
||||||
)
|
)
|
||||||
.with_detail(e.to_string())
|
.with_detail(e.to_string())
|
||||||
@@ -116,13 +116,7 @@ pub async fn update_system_proxy_settings(
|
|||||||
.await
|
.await
|
||||||
.map_err(AppCommandError::from)?;
|
.map_err(AppCommandError::from)?;
|
||||||
|
|
||||||
proxy::apply_system_proxy_settings(&normalized).map_err(|e| {
|
proxy::apply_system_proxy_settings(&normalized)?;
|
||||||
AppCommandError::new(
|
|
||||||
AppErrorCode::ExternalCommandFailed,
|
|
||||||
"Failed to apply system proxy settings",
|
|
||||||
)
|
|
||||||
.with_detail(e)
|
|
||||||
})?;
|
|
||||||
Ok(normalized)
|
Ok(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::app_error::{AppCommandError, AppErrorCode};
|
||||||
use crate::models::SystemProxySettings;
|
use crate::models::SystemProxySettings;
|
||||||
|
|
||||||
const PROXY_ENV_KEYS: [&str; 6] = [
|
const PROXY_ENV_KEYS: [&str; 6] = [
|
||||||
@@ -9,14 +10,19 @@ const PROXY_ENV_KEYS: [&str; 6] = [
|
|||||||
"all_proxy",
|
"all_proxy",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn apply_system_proxy_settings(settings: &SystemProxySettings) -> Result<(), String> {
|
pub fn apply_system_proxy_settings(settings: &SystemProxySettings) -> Result<(), AppCommandError> {
|
||||||
if settings.enabled {
|
if settings.enabled {
|
||||||
let proxy_url = settings
|
let proxy_url = settings
|
||||||
.proxy_url
|
.proxy_url
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or_else(|| "proxy url is required when proxy is enabled".to_string())?;
|
.ok_or_else(|| {
|
||||||
|
AppCommandError::new(
|
||||||
|
AppErrorCode::ConfigurationMissing,
|
||||||
|
"Proxy URL is required when proxy is enabled",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
for key in PROXY_ENV_KEYS {
|
for key in PROXY_ENV_KEYS {
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -15,8 +15,16 @@ export const LiveMessageBlock = memo(function LiveMessageBlock({
|
|||||||
message,
|
message,
|
||||||
}: LiveMessageBlockProps) {
|
}: LiveMessageBlockProps) {
|
||||||
const t = useTranslations("Folder.chat.liveMessageBlock")
|
const t = useTranslations("Folder.chat.liveMessageBlock")
|
||||||
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const hasContent = message.content.length > 0
|
const hasContent = message.content.length > 0
|
||||||
const adapted = useMemo(() => adaptLiveMessageFromAcp(message), [message])
|
const adapted = useMemo(
|
||||||
|
() =>
|
||||||
|
adaptLiveMessageFromAcp(message, {
|
||||||
|
toolCallFailedText: sharedT("toolCallFailed"),
|
||||||
|
planUpdatedText: sharedT("planUpdated"),
|
||||||
|
}),
|
||||||
|
[message, sharedT]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Message from="assistant">
|
<Message from="assistant">
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ export function WelcomeInputPanel({
|
|||||||
isActive = true,
|
isActive = true,
|
||||||
}: WelcomeInputPanelProps) {
|
}: WelcomeInputPanelProps) {
|
||||||
const t = useTranslations("Folder.chat.welcomeInputPanel")
|
const t = useTranslations("Folder.chat.welcomeInputPanel")
|
||||||
|
const tabT = useTranslations("Folder.tabContext")
|
||||||
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const fallbackContextId = useMemo(() => crypto.randomUUID(), [])
|
const fallbackContextId = useMemo(() => crypto.randomUUID(), [])
|
||||||
const contextKey = tabId ?? `new-${fallbackContextId}`
|
const contextKey = tabId ?? `new-${fallbackContextId}`
|
||||||
|
|
||||||
@@ -159,7 +161,10 @@ export function WelcomeInputPanel({
|
|||||||
const detail = await getFolderConversation(conversationId)
|
const detail = await getFolderConversation(conversationId)
|
||||||
if (refreshSeq !== statsRefreshSeqRef.current) return
|
if (refreshSeq !== statsRefreshSeqRef.current) return
|
||||||
|
|
||||||
const messages = adaptMessageTurns(detail.turns)
|
const messages = adaptMessageTurns(detail.turns, {
|
||||||
|
attachedResources: sharedT("attachedResources"),
|
||||||
|
toolCallFailed: sharedT("toolCallFailed"),
|
||||||
|
})
|
||||||
const stats = detail.session_stats ?? null
|
const stats = detail.session_stats ?? null
|
||||||
latestMessages = messages
|
latestMessages = messages
|
||||||
latestStats = stats
|
latestStats = stats
|
||||||
@@ -196,7 +201,7 @@ export function WelcomeInputPanel({
|
|||||||
applySessionStats(latestStats)
|
applySessionStats(latestStats)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[applySessionStats, hasAssistantUsage, hasTokenStats]
|
[applySessionStats, hasAssistantUsage, hasTokenStats, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -354,6 +359,8 @@ export function WelcomeInputPanel({
|
|||||||
if (conn.liveMessage && conn.liveMessage.content.length > 0) {
|
if (conn.liveMessage && conn.liveMessage.content.length > 0) {
|
||||||
const adapted = adaptLiveMessageFromAcp(conn.liveMessage, {
|
const adapted = adaptLiveMessageFromAcp(conn.liveMessage, {
|
||||||
isLiveStreaming: false,
|
isLiveStreaming: false,
|
||||||
|
toolCallFailedText: sharedT("toolCallFailed"),
|
||||||
|
planUpdatedText: sharedT("planUpdated"),
|
||||||
})
|
})
|
||||||
|
|
||||||
setHistory((h) => [...h, adapted])
|
setHistory((h) => [...h, adapted])
|
||||||
@@ -376,7 +383,7 @@ export function WelcomeInputPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- conn.liveMessage, lifecycleSend intentionally omitted: effect only fires on status transitions
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- conn.liveMessage, lifecycleSend intentionally omitted: effect only fires on status transitions
|
||||||
}, [connStatus, refreshConversations, refreshConversationFromDb])
|
}, [connStatus, refreshConversations, refreshConversationFromDb, sharedT])
|
||||||
|
|
||||||
// When connection becomes "connected" and we have a pending prompt, send it
|
// When connection becomes "connected" and we have a pending prompt, send it
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -394,7 +401,7 @@ export function WelcomeInputPanel({
|
|||||||
const tid = tabIdRef.current
|
const tid = tabIdRef.current
|
||||||
const convId = dbConvIdRef.current
|
const convId = dbConvIdRef.current
|
||||||
const agent = selectedAgentRef.current
|
const agent = selectedAgentRef.current
|
||||||
const title = convTitleRef.current || "Untitled"
|
const title = convTitleRef.current || tabT("untitledConversation")
|
||||||
const canonicalContextKey = `conv-${agent}-${convId}`
|
const canonicalContextKey = `conv-${agent}-${convId}`
|
||||||
|
|
||||||
// Keep in-flight stream/state attached when this new-conversation view
|
// Keep in-flight stream/state attached when this new-conversation view
|
||||||
@@ -410,6 +417,7 @@ export function WelcomeInputPanel({
|
|||||||
refreshConversations,
|
refreshConversations,
|
||||||
migrateContextKey,
|
migrateContextKey,
|
||||||
contextKey,
|
contextKey,
|
||||||
|
tabT,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Update conversation status on disconnect/error + promote tab
|
// Update conversation status on disconnect/error + promote tab
|
||||||
@@ -466,11 +474,17 @@ export function WelcomeInputPanel({
|
|||||||
// Welcome phase: submit first message.
|
// Welcome phase: submit first message.
|
||||||
const handleWelcomeSend = useCallback(
|
const handleWelcomeSend = useCallback(
|
||||||
(draft: PromptDraft, selectedModeId?: string | null) => {
|
(draft: PromptDraft, selectedModeId?: string | null) => {
|
||||||
const displayText = getPromptDraftDisplayText(draft)
|
const displayText = getPromptDraftDisplayText(
|
||||||
|
draft,
|
||||||
|
sharedT("attachedResources")
|
||||||
|
)
|
||||||
const userMsg: AdaptedMessage = {
|
const userMsg: AdaptedMessage = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
role: "user",
|
role: "user",
|
||||||
content: buildUserMessageTextPartsFromDraft(draft),
|
content: buildUserMessageTextPartsFromDraft(
|
||||||
|
draft,
|
||||||
|
sharedT("attachedResources")
|
||||||
|
),
|
||||||
userResources: extractUserResourcesFromDraft(draft),
|
userResources: extractUserResourcesFromDraft(draft),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
@@ -536,6 +550,7 @@ export function WelcomeInputPanel({
|
|||||||
trySaveExternalId,
|
trySaveExternalId,
|
||||||
applySessionStats,
|
applySessionStats,
|
||||||
newConversationDraftStorageKey,
|
newConversationDraftStorageKey,
|
||||||
|
sharedT,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -545,7 +560,10 @@ export function WelcomeInputPanel({
|
|||||||
const userMsg: AdaptedMessage = {
|
const userMsg: AdaptedMessage = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
role: "user",
|
role: "user",
|
||||||
content: buildUserMessageTextPartsFromDraft(draft),
|
content: buildUserMessageTextPartsFromDraft(
|
||||||
|
draft,
|
||||||
|
sharedT("attachedResources")
|
||||||
|
),
|
||||||
userResources: extractUserResourcesFromDraft(draft),
|
userResources: extractUserResourcesFromDraft(draft),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
@@ -562,7 +580,7 @@ export function WelcomeInputPanel({
|
|||||||
statusUpdatedRef.current = false
|
statusUpdatedRef.current = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[lifecycleSend, refreshConversations]
|
[lifecycleSend, refreshConversations, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleOpenAgentsSettings = useCallback(() => {
|
const handleOpenAgentsSettings = useCallback(() => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const ExistingConversationView = memo(function ExistingConversationView({
|
|||||||
reloadSignal,
|
reloadSignal,
|
||||||
}: ExistingConversationViewProps) {
|
}: ExistingConversationViewProps) {
|
||||||
const t = useTranslations("Folder.conversation")
|
const t = useTranslations("Folder.conversation")
|
||||||
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const { refreshConversations, folder } = useFolderContext()
|
const { refreshConversations, folder } = useFolderContext()
|
||||||
const contextKey = `conv-${agentType}-${conversationId}`
|
const contextKey = `conv-${agentType}-${conversationId}`
|
||||||
|
|
||||||
@@ -114,7 +115,10 @@ const ExistingConversationView = memo(function ExistingConversationView({
|
|||||||
{
|
{
|
||||||
id: `pending-${Date.now()}`,
|
id: `pending-${Date.now()}`,
|
||||||
role: "user",
|
role: "user",
|
||||||
content: buildUserMessageTextPartsFromDraft(draft),
|
content: buildUserMessageTextPartsFromDraft(
|
||||||
|
draft,
|
||||||
|
sharedT("attachedResources")
|
||||||
|
),
|
||||||
userResources: extractUserResourcesFromDraft(draft),
|
userResources: extractUserResourcesFromDraft(draft),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
@@ -125,7 +129,7 @@ const ExistingConversationView = memo(function ExistingConversationView({
|
|||||||
statusUpdatedRef.current = false
|
statusUpdatedRef.current = false
|
||||||
handleSend(draft, selectedModeId)
|
handleSend(draft, selectedModeId)
|
||||||
},
|
},
|
||||||
[conversationId, handleSend, refreshConversations]
|
[conversationId, handleSend, refreshConversations, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update status on turn complete
|
// Update status on turn complete
|
||||||
|
|||||||
@@ -1660,6 +1660,7 @@ const CODE_FIELDS = new Set([
|
|||||||
const HIDDEN_FIELDS = new Set(["dangerouslyDisableSandbox"])
|
const HIDDEN_FIELDS = new Set(["dangerouslyDisableSandbox"])
|
||||||
|
|
||||||
function GenericToolInput({ input }: { input: string }) {
|
function GenericToolInput({ input }: { input: string }) {
|
||||||
|
const t = useTranslations("Folder.chat.contentParts")
|
||||||
const parsed = tryParseJson(input)
|
const parsed = tryParseJson(input)
|
||||||
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
@@ -1677,6 +1678,9 @@ function GenericToolInput({ input }: { input: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{entries.map(([key, value]) => {
|
{entries.map(([key, value]) => {
|
||||||
|
const labelKey = fieldLabelKey(key)
|
||||||
|
const label = labelKey ? t(labelKey) : key
|
||||||
|
|
||||||
if (CODE_FIELDS.has(key) && typeof value === "string") {
|
if (CODE_FIELDS.has(key) && typeof value === "string") {
|
||||||
const lang =
|
const lang =
|
||||||
key === "command"
|
key === "command"
|
||||||
@@ -1685,7 +1689,7 @@ function GenericToolInput({ input }: { input: string }) {
|
|||||||
? ("log" as const)
|
? ("log" as const)
|
||||||
: ("log" as const)
|
: ("log" as const)
|
||||||
return (
|
return (
|
||||||
<FieldBlock key={key} label={fieldLabel(key)}>
|
<FieldBlock key={key} label={label}>
|
||||||
<CodeBlock code={value} language={lang} />
|
<CodeBlock code={value} language={lang} />
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
)
|
)
|
||||||
@@ -1694,29 +1698,23 @@ function GenericToolInput({ input }: { input: string }) {
|
|||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
if (value.length > 200) {
|
if (value.length > 200) {
|
||||||
return (
|
return (
|
||||||
<FieldBlock key={key} label={fieldLabel(key)}>
|
<FieldBlock key={key} label={label}>
|
||||||
<pre className="whitespace-pre-wrap break-all rounded-md bg-muted/50 p-3 text-xs">
|
<pre className="whitespace-pre-wrap break-all rounded-md bg-muted/50 p-3 text-xs">
|
||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
</FieldBlock>
|
</FieldBlock>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <FieldInline key={key} label={fieldLabel(key)} value={value} />
|
return <FieldInline key={key} label={label} value={value} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
return (
|
return <FieldInline key={key} label={label} value={String(value)} />
|
||||||
<FieldInline
|
|
||||||
key={key}
|
|
||||||
label={fieldLabel(key)}
|
|
||||||
value={String(value)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
return (
|
return (
|
||||||
<FieldBlock key={key} label={fieldLabel(key)}>
|
<FieldBlock key={key} label={label}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={JSON.stringify(value, null, 2)}
|
code={JSON.stringify(value, null, 2)}
|
||||||
language="json"
|
language="json"
|
||||||
@@ -1836,41 +1834,45 @@ function FieldBlock({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldLabel(key: string): string {
|
const FIELD_LABEL_KEYS = {
|
||||||
const map: Record<string, string> = {
|
file_path: "field.file",
|
||||||
file_path: "File",
|
notebook_path: "field.notebook",
|
||||||
notebook_path: "Notebook",
|
command: "field.command",
|
||||||
command: "Command",
|
cmd: "field.command",
|
||||||
cmd: "Command",
|
old_string: "field.old",
|
||||||
old_string: "Old",
|
new_string: "field.new",
|
||||||
new_string: "New",
|
pattern: "field.pattern",
|
||||||
pattern: "Pattern",
|
path: "field.path",
|
||||||
path: "Path",
|
query: "field.query",
|
||||||
query: "Query",
|
url: "field.url",
|
||||||
url: "URL",
|
description: "field.description",
|
||||||
description: "Description",
|
content: "field.content",
|
||||||
content: "Content",
|
new_source: "field.source",
|
||||||
new_source: "Source",
|
prompt: "field.prompt",
|
||||||
prompt: "Prompt",
|
subject: "field.subject",
|
||||||
subject: "Subject",
|
taskId: "field.taskId",
|
||||||
taskId: "Task ID",
|
status: "field.status",
|
||||||
status: "Status",
|
skill: "field.skill",
|
||||||
skill: "Skill",
|
args: "field.args",
|
||||||
args: "Args",
|
offset: "field.offset",
|
||||||
offset: "Offset",
|
limit: "field.limit",
|
||||||
limit: "Limit",
|
glob: "field.glob",
|
||||||
glob: "Glob",
|
type: "field.type",
|
||||||
type: "Type",
|
output_mode: "field.output",
|
||||||
output_mode: "Output",
|
replace_all: "field.replaceAll",
|
||||||
replace_all: "Replace All",
|
language: "field.language",
|
||||||
language: "Language",
|
timeout: "field.timeout",
|
||||||
timeout: "Timeout",
|
run_in_background: "field.background",
|
||||||
run_in_background: "Background",
|
subagent_type: "field.agentType",
|
||||||
subagent_type: "Agent Type",
|
libraryName: "field.library",
|
||||||
libraryName: "Library",
|
libraryId: "field.libraryId",
|
||||||
libraryId: "Library ID",
|
} as const
|
||||||
}
|
|
||||||
return map[key] ?? key
|
function fieldLabelKey(
|
||||||
|
key: string
|
||||||
|
): (typeof FIELD_LABEL_KEYS)[keyof typeof FIELD_LABEL_KEYS] | null {
|
||||||
|
const translationKey = FIELD_LABEL_KEYS[key as keyof typeof FIELD_LABEL_KEYS]
|
||||||
|
return translationKey ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function commandOutputFromJsonString(output: string): string | null {
|
function commandOutputFromJsonString(output: string): string | null {
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export function MessageListView({
|
|||||||
isActive = true,
|
isActive = true,
|
||||||
}: MessageListViewProps) {
|
}: MessageListViewProps) {
|
||||||
const t = useTranslations("Folder.chat.messageList")
|
const t = useTranslations("Folder.chat.messageList")
|
||||||
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const { detail, loading, error, refetch } = useDbMessageDetail(conversationId)
|
const { detail, loading, error, refetch } = useDbMessageDetail(conversationId)
|
||||||
const turnCount = detail?.turns.length ?? 0
|
const turnCount = detail?.turns.length ?? 0
|
||||||
|
|
||||||
@@ -213,8 +214,14 @@ export function MessageListView({
|
|||||||
const shouldUseSmoothResize = !(isActive && !loading && detail)
|
const shouldUseSmoothResize = !(isActive && !loading && detail)
|
||||||
|
|
||||||
const messages = useMemo(
|
const messages = useMemo(
|
||||||
() => (detail ? adaptMessageTurns(detail.turns) : []),
|
() =>
|
||||||
[detail]
|
detail
|
||||||
|
? adaptMessageTurns(detail.turns, {
|
||||||
|
attachedResources: sharedT("attachedResources"),
|
||||||
|
toolCallFailed: sharedT("toolCallFailed"),
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
[detail, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
const groups = useMemo(() => groupAdaptedMessages(messages), [messages])
|
const groups = useMemo(() => groupAdaptedMessages(messages), [messages])
|
||||||
@@ -234,15 +241,17 @@ export function MessageListView({
|
|||||||
)
|
)
|
||||||
const resolvedGroups = useMemo(
|
const resolvedGroups = useMemo(
|
||||||
() =>
|
() =>
|
||||||
groups.map((group) => resolveMessageGroup(group, t("attachedResources"))),
|
groups.map((group) =>
|
||||||
[groups, t]
|
resolveMessageGroup(group, sharedT("attachedResources"))
|
||||||
|
),
|
||||||
|
[groups, sharedT]
|
||||||
)
|
)
|
||||||
const resolvedPendingGroups = useMemo(
|
const resolvedPendingGroups = useMemo(
|
||||||
() =>
|
() =>
|
||||||
pendingGroups.map((group) =>
|
pendingGroups.map((group) =>
|
||||||
resolveMessageGroup(group, t("attachedResources"))
|
resolveMessageGroup(group, sharedT("attachedResources"))
|
||||||
),
|
),
|
||||||
[pendingGroups, t]
|
[pendingGroups, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
const showLiveMessage = Boolean(
|
const showLiveMessage = Boolean(
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ function stripClonePrefix(message: string): string {
|
|||||||
function mapCommonCodeToKey(code: string): WelcomeErrorKey {
|
function mapCommonCodeToKey(code: string): WelcomeErrorKey {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case "invalid_input":
|
case "invalid_input":
|
||||||
|
case "configuration_missing":
|
||||||
|
case "configuration_invalid":
|
||||||
return "errors.invalidInput"
|
return "errors.invalidInput"
|
||||||
case "not_found":
|
case "not_found":
|
||||||
return "errors.notFound"
|
return "errors.notFound"
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ type Action =
|
|||||||
contextKey: string
|
contextKey: string
|
||||||
tool_call_id: string
|
tool_call_id: string
|
||||||
title: string | null
|
title: string | null
|
||||||
|
fallback_title: string
|
||||||
|
fallback_kind: string
|
||||||
status: string | null
|
status: string | null
|
||||||
content: string | null
|
content: string | null
|
||||||
raw_input: string | null
|
raw_input: string | null
|
||||||
@@ -135,6 +137,8 @@ type Action =
|
|||||||
contextKey: string
|
contextKey: string
|
||||||
request_id: string
|
request_id: string
|
||||||
tool_call: unknown
|
tool_call: unknown
|
||||||
|
fallback_title: string
|
||||||
|
fallback_kind: string
|
||||||
options: PermissionOptionInfo[]
|
options: PermissionOptionInfo[]
|
||||||
}
|
}
|
||||||
| { type: "PERMISSION_CLEARED"; contextKey: string }
|
| { type: "PERMISSION_CLEARED"; contextKey: string }
|
||||||
@@ -216,28 +220,28 @@ function serializePermissionToolCall(toolCall: unknown): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPermissionToolTitle(toolCall: unknown): string {
|
function extractPermissionToolTitle(toolCall: unknown): string | null {
|
||||||
const record = asRecord(toolCall)
|
const record = asRecord(toolCall)
|
||||||
if (!record) return "Tool"
|
if (!record) return null
|
||||||
const candidates = [record.title, record.tool_name, record.name, record.type]
|
const candidates = [record.title, record.tool_name, record.name, record.type]
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Tool"
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPermissionToolKind(toolCall: unknown): string {
|
function extractPermissionToolKind(toolCall: unknown): string | null {
|
||||||
const record = asRecord(toolCall)
|
const record = asRecord(toolCall)
|
||||||
if (!record) return "tool"
|
if (!record) return null
|
||||||
const candidates = [record.kind, record.tool_name, record.name, record.type]
|
const candidates = [record.kind, record.tool_name, record.name, record.type]
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "tool"
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameModes(
|
function sameModes(
|
||||||
@@ -572,8 +576,8 @@ function connectionsReducer(
|
|||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
info: {
|
info: {
|
||||||
tool_call_id: action.tool_call_id,
|
tool_call_id: action.tool_call_id,
|
||||||
title: action.title ?? "Tool",
|
title: action.title ?? action.fallback_title,
|
||||||
kind: "tool",
|
kind: action.fallback_kind,
|
||||||
status:
|
status:
|
||||||
action.status ??
|
action.status ??
|
||||||
(normalizedRawOutput ? "in_progress" : "pending"),
|
(normalizedRawOutput ? "in_progress" : "pending"),
|
||||||
@@ -665,8 +669,12 @@ function connectionsReducer(
|
|||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
info: {
|
info: {
|
||||||
tool_call_id: permissionCallId,
|
tool_call_id: permissionCallId,
|
||||||
title: extractPermissionToolTitle(action.tool_call),
|
title:
|
||||||
kind: extractPermissionToolKind(action.tool_call),
|
extractPermissionToolTitle(action.tool_call) ??
|
||||||
|
action.fallback_title,
|
||||||
|
kind:
|
||||||
|
extractPermissionToolKind(action.tool_call) ??
|
||||||
|
action.fallback_kind,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
content: null,
|
content: null,
|
||||||
raw_input: permissionToolInput,
|
raw_input: permissionToolInput,
|
||||||
@@ -1247,6 +1255,8 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
contextKey,
|
contextKey,
|
||||||
tool_call_id: e.tool_call_id,
|
tool_call_id: e.tool_call_id,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
|
fallback_title: t("toolFallbackTitle"),
|
||||||
|
fallback_kind: "tool",
|
||||||
status: e.status,
|
status: e.status,
|
||||||
content: e.content,
|
content: e.content,
|
||||||
raw_input: e.raw_input,
|
raw_input: e.raw_input,
|
||||||
@@ -1261,6 +1271,8 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
contextKey,
|
contextKey,
|
||||||
request_id: e.request_id,
|
request_id: e.request_id,
|
||||||
tool_call: e.tool_call,
|
tool_call: e.tool_call,
|
||||||
|
fallback_title: t("toolFallbackTitle"),
|
||||||
|
fallback_kind: "tool",
|
||||||
options: e.options,
|
options: e.options,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
@@ -1322,7 +1334,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
case "error":
|
case "error":
|
||||||
flushStreamingQueue()
|
flushStreamingQueue()
|
||||||
dispatch({ type: "ERROR", contextKey, message: e.message })
|
dispatch({ type: "ERROR", contextKey, message: e.message })
|
||||||
pushAlertRef.current("error", "Agent 错误", e.message)
|
pushAlertRef.current("error", t("eventErrorTitle"), e.message)
|
||||||
break
|
break
|
||||||
case "available_commands":
|
case "available_commands":
|
||||||
flushStreamingQueue()
|
flushStreamingQueue()
|
||||||
@@ -1334,7 +1346,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, enqueueStreamingAction, flushStreamingQueue]
|
[dispatch, enqueueStreamingAction, flushStreamingQueue, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Single global event listener
|
// Single global event listener
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function useConnectionLifecycle({
|
|||||||
sessionId,
|
sessionId,
|
||||||
}: UseConnectionLifecycleOptions): UseConnectionLifecycleReturn {
|
}: UseConnectionLifecycleOptions): UseConnectionLifecycleReturn {
|
||||||
const t = useTranslations("Folder.chat.connectionLifecycle")
|
const t = useTranslations("Folder.chat.connectionLifecycle")
|
||||||
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const { setActiveKey, touchActivity } = useAcpActions()
|
const { setActiveKey, touchActivity } = useAcpActions()
|
||||||
const { addTask, updateTask, removeTask } = useTaskContext()
|
const { addTask, updateTask, removeTask } = useTaskContext()
|
||||||
const conn = useConnection(contextKey)
|
const conn = useConnection(contextKey)
|
||||||
@@ -312,7 +313,10 @@ export function useConnectionLifecycle({
|
|||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
(draft: PromptDraft, modeId?: string | null) => {
|
(draft: PromptDraft, modeId?: string | null) => {
|
||||||
touchActivity(contextKey)
|
touchActivity(contextKey)
|
||||||
setPendingPromptText(contextKey, getPromptDraftDisplayText(draft))
|
setPendingPromptText(
|
||||||
|
contextKey,
|
||||||
|
getPromptDraftDisplayText(draft, sharedT("attachedResources"))
|
||||||
|
)
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const currentModeId = modeIdRef.current
|
const currentModeId = modeIdRef.current
|
||||||
if (modeId && modeId !== currentModeId) {
|
if (modeId && modeId !== currentModeId) {
|
||||||
@@ -326,7 +330,7 @@ export function useConnectionLifecycle({
|
|||||||
console.error("[ConnLifecycle] sendPrompt:", e)
|
console.error("[ConnLifecycle] sendPrompt:", e)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[connSetMode, sendPrompt, contextKey, touchActivity]
|
[connSetMode, sendPrompt, contextKey, touchActivity, sharedT]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
|
|||||||
@@ -1079,7 +1079,9 @@
|
|||||||
"preflightCheckFailedDefault": "Preflight checks failed. Check Agent settings.",
|
"preflightCheckFailedDefault": "Preflight checks failed. Check Agent settings.",
|
||||||
"preflightFailedTitle": "{agent} preflight failed",
|
"preflightFailedTitle": "{agent} preflight failed",
|
||||||
"autoLinkPreflightFailed": "Auto-link preflight failed: {message}",
|
"autoLinkPreflightFailed": "Auto-link preflight failed: {message}",
|
||||||
"connectFailedTitle": "{agent} connection failed"
|
"connectFailedTitle": "{agent} connection failed",
|
||||||
|
"toolFallbackTitle": "Tool",
|
||||||
|
"eventErrorTitle": "Agent Error"
|
||||||
},
|
},
|
||||||
"connectionLifecycle": {
|
"connectionLifecycle": {
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -1092,6 +1094,11 @@
|
|||||||
"connectionFailed": "Connection failed"
|
"connectionFailed": "Connection failed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shared": {
|
||||||
|
"attachedResources": "Attached resources",
|
||||||
|
"toolCallFailed": "Tool call failed",
|
||||||
|
"planUpdated": "Plan updated"
|
||||||
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "No messages yet",
|
"emptyTitle": "No messages yet",
|
||||||
"emptyDescription": "Start a conversation to see messages here"
|
"emptyDescription": "Start a conversation to see messages here"
|
||||||
@@ -1222,6 +1229,38 @@
|
|||||||
"subjectLabel": "Subject",
|
"subjectLabel": "Subject",
|
||||||
"taskLabel": "Task",
|
"taskLabel": "Task",
|
||||||
"nameLabel": "Name:",
|
"nameLabel": "Name:",
|
||||||
|
"field": {
|
||||||
|
"file": "File",
|
||||||
|
"notebook": "Notebook",
|
||||||
|
"command": "Command",
|
||||||
|
"old": "Old",
|
||||||
|
"new": "New",
|
||||||
|
"pattern": "Pattern",
|
||||||
|
"path": "Path",
|
||||||
|
"query": "Query",
|
||||||
|
"url": "URL",
|
||||||
|
"description": "Description",
|
||||||
|
"content": "Content",
|
||||||
|
"source": "Source",
|
||||||
|
"prompt": "Prompt",
|
||||||
|
"subject": "Subject",
|
||||||
|
"taskId": "Task ID",
|
||||||
|
"status": "Status",
|
||||||
|
"skill": "Skill",
|
||||||
|
"args": "Args",
|
||||||
|
"offset": "Offset",
|
||||||
|
"limit": "Limit",
|
||||||
|
"glob": "Glob",
|
||||||
|
"type": "Type",
|
||||||
|
"output": "Output",
|
||||||
|
"replaceAll": "Replace All",
|
||||||
|
"language": "Language",
|
||||||
|
"timeout": "Timeout",
|
||||||
|
"background": "Background",
|
||||||
|
"agentType": "Agent Type",
|
||||||
|
"library": "Library",
|
||||||
|
"libraryId": "Library ID"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"command": "Command",
|
"command": "Command",
|
||||||
|
|||||||
@@ -1079,7 +1079,9 @@
|
|||||||
"preflightCheckFailedDefault": "预检查未通过,请检查 Agent 配置。",
|
"preflightCheckFailedDefault": "预检查未通过,请检查 Agent 配置。",
|
||||||
"preflightFailedTitle": "{agent} 预检查失败",
|
"preflightFailedTitle": "{agent} 预检查失败",
|
||||||
"autoLinkPreflightFailed": "自动链接预检查失败:{message}",
|
"autoLinkPreflightFailed": "自动链接预检查失败:{message}",
|
||||||
"connectFailedTitle": "{agent} 连接失败"
|
"connectFailedTitle": "{agent} 连接失败",
|
||||||
|
"toolFallbackTitle": "工具",
|
||||||
|
"eventErrorTitle": "Agent 错误"
|
||||||
},
|
},
|
||||||
"connectionLifecycle": {
|
"connectionLifecycle": {
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -1092,6 +1094,11 @@
|
|||||||
"connectionFailed": "连接失败"
|
"connectionFailed": "连接失败"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shared": {
|
||||||
|
"attachedResources": "附加资源",
|
||||||
|
"toolCallFailed": "工具调用失败",
|
||||||
|
"planUpdated": "计划已更新"
|
||||||
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "暂无消息",
|
"emptyTitle": "暂无消息",
|
||||||
"emptyDescription": "开始一个会话后,消息会显示在这里"
|
"emptyDescription": "开始一个会话后,消息会显示在这里"
|
||||||
@@ -1222,6 +1229,38 @@
|
|||||||
"subjectLabel": "主题",
|
"subjectLabel": "主题",
|
||||||
"taskLabel": "任务",
|
"taskLabel": "任务",
|
||||||
"nameLabel": "名称:",
|
"nameLabel": "名称:",
|
||||||
|
"field": {
|
||||||
|
"file": "文件",
|
||||||
|
"notebook": "Notebook",
|
||||||
|
"command": "命令",
|
||||||
|
"old": "旧内容",
|
||||||
|
"new": "新内容",
|
||||||
|
"pattern": "模式",
|
||||||
|
"path": "路径",
|
||||||
|
"query": "查询",
|
||||||
|
"url": "URL",
|
||||||
|
"description": "描述",
|
||||||
|
"content": "内容",
|
||||||
|
"source": "源内容",
|
||||||
|
"prompt": "提示词",
|
||||||
|
"subject": "主题",
|
||||||
|
"taskId": "任务 ID",
|
||||||
|
"status": "状态",
|
||||||
|
"skill": "Skill",
|
||||||
|
"args": "参数",
|
||||||
|
"offset": "偏移",
|
||||||
|
"limit": "限制",
|
||||||
|
"glob": "Glob",
|
||||||
|
"type": "类型",
|
||||||
|
"output": "输出",
|
||||||
|
"replaceAll": "全部替换",
|
||||||
|
"language": "语言",
|
||||||
|
"timeout": "超时",
|
||||||
|
"background": "后台",
|
||||||
|
"agentType": "Agent 类型",
|
||||||
|
"library": "库",
|
||||||
|
"libraryId": "库 ID"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"command": "命令",
|
"command": "命令",
|
||||||
|
|||||||
@@ -1079,7 +1079,9 @@
|
|||||||
"preflightCheckFailedDefault": "預檢查未通過,請檢查 Agent 設定。",
|
"preflightCheckFailedDefault": "預檢查未通過,請檢查 Agent 設定。",
|
||||||
"preflightFailedTitle": "{agent} 預檢查失敗",
|
"preflightFailedTitle": "{agent} 預檢查失敗",
|
||||||
"autoLinkPreflightFailed": "自動連結預檢查失敗:{message}",
|
"autoLinkPreflightFailed": "自動連結預檢查失敗:{message}",
|
||||||
"connectFailedTitle": "{agent} 連線失敗"
|
"connectFailedTitle": "{agent} 連線失敗",
|
||||||
|
"toolFallbackTitle": "工具",
|
||||||
|
"eventErrorTitle": "Agent 錯誤"
|
||||||
},
|
},
|
||||||
"connectionLifecycle": {
|
"connectionLifecycle": {
|
||||||
"tasks": {
|
"tasks": {
|
||||||
@@ -1092,6 +1094,11 @@
|
|||||||
"connectionFailed": "連線失敗"
|
"connectionFailed": "連線失敗"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shared": {
|
||||||
|
"attachedResources": "附加資源",
|
||||||
|
"toolCallFailed": "工具呼叫失敗",
|
||||||
|
"planUpdated": "計畫已更新"
|
||||||
|
},
|
||||||
"messageThread": {
|
"messageThread": {
|
||||||
"emptyTitle": "暫無訊息",
|
"emptyTitle": "暫無訊息",
|
||||||
"emptyDescription": "開始一個會話後,訊息會顯示在這裡"
|
"emptyDescription": "開始一個會話後,訊息會顯示在這裡"
|
||||||
@@ -1222,6 +1229,38 @@
|
|||||||
"subjectLabel": "主題",
|
"subjectLabel": "主題",
|
||||||
"taskLabel": "任務",
|
"taskLabel": "任務",
|
||||||
"nameLabel": "名稱:",
|
"nameLabel": "名稱:",
|
||||||
|
"field": {
|
||||||
|
"file": "檔案",
|
||||||
|
"notebook": "Notebook",
|
||||||
|
"command": "命令",
|
||||||
|
"old": "舊內容",
|
||||||
|
"new": "新內容",
|
||||||
|
"pattern": "模式",
|
||||||
|
"path": "路徑",
|
||||||
|
"query": "查詢",
|
||||||
|
"url": "URL",
|
||||||
|
"description": "描述",
|
||||||
|
"content": "內容",
|
||||||
|
"source": "來源內容",
|
||||||
|
"prompt": "提示詞",
|
||||||
|
"subject": "主題",
|
||||||
|
"taskId": "任務 ID",
|
||||||
|
"status": "狀態",
|
||||||
|
"skill": "Skill",
|
||||||
|
"args": "參數",
|
||||||
|
"offset": "位移",
|
||||||
|
"limit": "限制",
|
||||||
|
"glob": "Glob",
|
||||||
|
"type": "類型",
|
||||||
|
"output": "輸出",
|
||||||
|
"replaceAll": "全部替換",
|
||||||
|
"language": "語言",
|
||||||
|
"timeout": "逾時",
|
||||||
|
"background": "背景",
|
||||||
|
"agentType": "Agent 類型",
|
||||||
|
"library": "函式庫",
|
||||||
|
"libraryId": "函式庫 ID"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"command": "命令",
|
"command": "命令",
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ export interface AdaptedMessage {
|
|||||||
model?: string | null
|
model?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdapterMessageText {
|
||||||
|
attachedResources: string
|
||||||
|
toolCallFailed: string
|
||||||
|
planUpdated: string
|
||||||
|
}
|
||||||
|
|
||||||
type InlineToolSegment =
|
type InlineToolSegment =
|
||||||
| { kind: "text"; value: string }
|
| { kind: "text"; value: string }
|
||||||
| { kind: "tool_call" | "tool_result"; value: string }
|
| { kind: "tool_call" | "tool_result"; value: string }
|
||||||
@@ -271,7 +277,8 @@ function parseInlineToolResultPayload(payload: string): {
|
|||||||
function expandInlineToolText(
|
function expandInlineToolText(
|
||||||
text: string,
|
text: string,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
blockIndex: number
|
blockIndex: number,
|
||||||
|
toolCallFailedText: string
|
||||||
): AdaptedContentPart[] | null {
|
): AdaptedContentPart[] | null {
|
||||||
const segments = splitInlineToolSegments(text)
|
const segments = splitInlineToolSegments(text)
|
||||||
if (!segments) return null
|
if (!segments) return null
|
||||||
@@ -320,7 +327,7 @@ function expandInlineToolText(
|
|||||||
output = parsedResult.output
|
output = parsedResult.output
|
||||||
if (parsedResult.isError) {
|
if (parsedResult.isError) {
|
||||||
state = "output-error"
|
state = "output-error"
|
||||||
errorText = output ?? "Tool call failed"
|
errorText = output ?? toolCallFailedText
|
||||||
}
|
}
|
||||||
index = lookahead
|
index = lookahead
|
||||||
}
|
}
|
||||||
@@ -345,7 +352,7 @@ function expandInlineToolText(
|
|||||||
toolCallId,
|
toolCallId,
|
||||||
output: parsedResult.output,
|
output: parsedResult.output,
|
||||||
errorText: parsedResult.isError
|
errorText: parsedResult.isError
|
||||||
? (parsedResult.output ?? "Tool call failed")
|
? (parsedResult.output ?? toolCallFailedText)
|
||||||
: undefined,
|
: undefined,
|
||||||
state: parsedResult.isError ? "output-error" : "output-available",
|
state: parsedResult.isError ? "output-error" : "output-available",
|
||||||
})
|
})
|
||||||
@@ -440,7 +447,10 @@ export function extractUserResourcesFromText(text: string): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitUserTextAndResources(parts: AdaptedContentPart[]): {
|
function splitUserTextAndResources(
|
||||||
|
parts: AdaptedContentPart[],
|
||||||
|
attachedResourcesText: string
|
||||||
|
): {
|
||||||
parts: AdaptedContentPart[]
|
parts: AdaptedContentPart[]
|
||||||
resources: UserResourceDisplay[]
|
resources: UserResourceDisplay[]
|
||||||
} {
|
} {
|
||||||
@@ -464,7 +474,7 @@ function splitUserTextAndResources(parts: AdaptedContentPart[]): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextParts.length === 0 && resources.length > 0) {
|
if (nextParts.length === 0 && resources.length > 0) {
|
||||||
nextParts.push({ type: "text", text: "Attached resources" })
|
nextParts.push({ type: "text", text: attachedResourcesText })
|
||||||
}
|
}
|
||||||
|
|
||||||
return { parts: nextParts, resources }
|
return { parts: nextParts, resources }
|
||||||
@@ -545,7 +555,10 @@ function buildToolResultMap(
|
|||||||
* Transform a MessageTurn (from backend) to AdaptedMessage format.
|
* Transform a MessageTurn (from backend) to AdaptedMessage format.
|
||||||
* Same correlation logic as adaptUnifiedMessage but operates on turn.blocks.
|
* Same correlation logic as adaptUnifiedMessage but operates on turn.blocks.
|
||||||
*/
|
*/
|
||||||
export function adaptMessageTurn(turn: MessageTurn): AdaptedMessage {
|
export function adaptMessageTurn(
|
||||||
|
turn: MessageTurn,
|
||||||
|
text: Pick<AdapterMessageText, "attachedResources" | "toolCallFailed">
|
||||||
|
): AdaptedMessage {
|
||||||
const adaptedContent: AdaptedContentPart[] = []
|
const adaptedContent: AdaptedContentPart[] = []
|
||||||
const resultMap = buildToolResultMap(turn.blocks)
|
const resultMap = buildToolResultMap(turn.blocks)
|
||||||
const matchedResultIds = new Set<string>()
|
const matchedResultIds = new Set<string>()
|
||||||
@@ -557,7 +570,12 @@ export function adaptMessageTurn(turn: MessageTurn): AdaptedMessage {
|
|||||||
const block = turn.blocks[index]
|
const block = turn.blocks[index]
|
||||||
|
|
||||||
if (turn.role === "assistant" && block.type === "text") {
|
if (turn.role === "assistant" && block.type === "text") {
|
||||||
const expandedParts = expandInlineToolText(block.text, turn.id, index)
|
const expandedParts = expandInlineToolText(
|
||||||
|
block.text,
|
||||||
|
turn.id,
|
||||||
|
index,
|
||||||
|
text.toolCallFailed
|
||||||
|
)
|
||||||
if (expandedParts) {
|
if (expandedParts) {
|
||||||
adaptedContent.push(...expandedParts)
|
adaptedContent.push(...expandedParts)
|
||||||
continue
|
continue
|
||||||
@@ -641,7 +659,7 @@ export function adaptMessageTurn(turn: MessageTurn): AdaptedMessage {
|
|||||||
|
|
||||||
const userSplit =
|
const userSplit =
|
||||||
turn.role === "user"
|
turn.role === "user"
|
||||||
? splitUserTextAndResources(adaptedContent)
|
? splitUserTextAndResources(adaptedContent, text.attachedResources)
|
||||||
: { parts: adaptedContent, resources: [] as UserResourceDisplay[] }
|
: { parts: adaptedContent, resources: [] as UserResourceDisplay[] }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -661,8 +679,11 @@ export function adaptMessageTurn(turn: MessageTurn): AdaptedMessage {
|
|||||||
* Transform all turns in a conversation to AdaptedMessage[].
|
* Transform all turns in a conversation to AdaptedMessage[].
|
||||||
* Internally computes completedToolIds so callers don't need to.
|
* Internally computes completedToolIds so callers don't need to.
|
||||||
*/
|
*/
|
||||||
export function adaptMessageTurns(turns: MessageTurn[]): AdaptedMessage[] {
|
export function adaptMessageTurns(
|
||||||
return turns.map((turn) => adaptMessageTurn(turn))
|
turns: MessageTurn[],
|
||||||
|
text: Pick<AdapterMessageText, "attachedResources" | "toolCallFailed">
|
||||||
|
): AdaptedMessage[] {
|
||||||
|
return turns.map((turn) => adaptMessageTurn(turn, text))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -942,19 +963,22 @@ function selectLiveToolOutput(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatPlanEntries(
|
function formatPlanEntries(
|
||||||
entries: Array<{ content: string; priority: string; status: string }>
|
entries: Array<{ content: string; priority: string; status: string }>,
|
||||||
|
planUpdatedText: string
|
||||||
): string {
|
): string {
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return "Plan updated"
|
return planUpdatedText
|
||||||
}
|
}
|
||||||
const lines = entries.map(
|
const lines = entries.map(
|
||||||
(entry) => `- [${entry.status}] ${entry.content} (${entry.priority})`
|
(entry) => `- [${entry.status}] ${entry.content} (${entry.priority})`
|
||||||
)
|
)
|
||||||
return `Plan updated:\n${lines.join("\n")}`
|
return `${planUpdatedText}:\n${lines.join("\n")}`
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdaptLiveMessageOptions {
|
interface AdaptLiveMessageOptions {
|
||||||
isLiveStreaming?: boolean
|
isLiveStreaming?: boolean
|
||||||
|
toolCallFailedText: string
|
||||||
|
planUpdatedText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReasoningBlock(block: LiveMessage["content"][number]): boolean {
|
function isReasoningBlock(block: LiveMessage["content"][number]): boolean {
|
||||||
@@ -976,7 +1000,7 @@ function findLastReasoningIndex(message: LiveMessage): number {
|
|||||||
*/
|
*/
|
||||||
export function adaptLiveMessageFromAcp(
|
export function adaptLiveMessageFromAcp(
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
options: AdaptLiveMessageOptions = {}
|
options: AdaptLiveMessageOptions
|
||||||
): AdaptedMessage {
|
): AdaptedMessage {
|
||||||
const isLiveStreaming = options.isLiveStreaming ?? true
|
const isLiveStreaming = options.isLiveStreaming ?? true
|
||||||
const adaptedContent: AdaptedContentPart[] = []
|
const adaptedContent: AdaptedContentPart[] = []
|
||||||
@@ -1034,7 +1058,7 @@ export function adaptLiveMessageFromAcp(
|
|||||||
output,
|
output,
|
||||||
errorText:
|
errorText:
|
||||||
state === "output-error"
|
state === "output-error"
|
||||||
? selectedOutput || "Tool call failed"
|
? selectedOutput || options.toolCallFailedText
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
@@ -1043,7 +1067,7 @@ export function adaptLiveMessageFromAcp(
|
|||||||
case "plan":
|
case "plan":
|
||||||
adaptedContent.push({
|
adaptedContent.push({
|
||||||
type: "reasoning",
|
type: "reasoning",
|
||||||
content: formatPlanEntries(block.entries),
|
content: formatPlanEntries(block.entries, options.planUpdatedText),
|
||||||
isStreaming: index === lastStreamingReasoningIndex,
|
isStreaming: index === lastStreamingReasoningIndex,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -10,15 +10,24 @@ function isResourceLinkBlock(
|
|||||||
return block.type === "resource_link"
|
return block.type === "resource_link"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPromptDraftDisplayText(draft: PromptDraft): string {
|
export function getPromptDraftDisplayText(
|
||||||
|
draft: PromptDraft,
|
||||||
|
attachedResourcesFallback: string
|
||||||
|
): string {
|
||||||
const trimmed = draft.displayText.trim()
|
const trimmed = draft.displayText.trim()
|
||||||
return trimmed || "Attached resources"
|
return trimmed || attachedResourcesFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildUserMessageTextPartsFromDraft(
|
export function buildUserMessageTextPartsFromDraft(
|
||||||
draft: PromptDraft
|
draft: PromptDraft,
|
||||||
|
attachedResourcesFallback: string
|
||||||
): AdaptedContentPart[] {
|
): AdaptedContentPart[] {
|
||||||
return [{ type: "text", text: getPromptDraftDisplayText(draft) }]
|
return [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: getPromptDraftDisplayText(draft, attachedResourcesFallback),
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractUserResourcesFromDraft(
|
export function extractUserResourcesFromDraft(
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export type AgentType =
|
|||||||
export type AppErrorCode =
|
export type AppErrorCode =
|
||||||
| "unknown"
|
| "unknown"
|
||||||
| "invalid_input"
|
| "invalid_input"
|
||||||
|
| "configuration_missing"
|
||||||
|
| "configuration_invalid"
|
||||||
| "not_found"
|
| "not_found"
|
||||||
| "already_exists"
|
| "already_exists"
|
||||||
| "permission_denied"
|
| "permission_denied"
|
||||||
|
|||||||
Reference in New Issue
Block a user