继续多语言处理

This commit is contained in:
xintaofei
2026-03-07 15:49:00 +08:00
parent 931f69c421
commit 6e5219cc10
18 changed files with 466 additions and 234 deletions

View File

@@ -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())
}
}

View File

@@ -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())
} }

View File

@@ -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)
} }

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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(() => {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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"

View File

@@ -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

View File

@@ -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(() => {

View File

@@ -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",

View File

@@ -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": "命令",

View File

@@ -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": "命令",

View File

@@ -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

View File

@@ -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(

View File

@@ -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"