дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(agent): run tasks in folders that are not git repositories (#402)
Agent tasks failed in projects without git because change tracking assumed a repository. Outside a repository there are no changes to compare; worktree isolation is hidden and the task runs in the local checkout, and asking for a worktree there returns a clear error. Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
1 джерело
ed6092a4e9
коміт
6356e85671
4 files changed
+51
-18
No files matched your search
@@ -23,6 +23,7 @@ pub use model::*;
|
||||
use conversation::{Conversation, SavedSubagent, Session};
|
||||
use ide_core::{IdeError, IdeResult, IpcPath};
|
||||
use ide_jsonrpc::{Framing, Handler, Peer, RpcError, codes};
|
||||
use ide_vcs::Repo;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
@@ -364,6 +365,12 @@ impl AgentManager {
|
||||
let root = self.shared.paths.project_root.clone();
|
||||
let (cwd, branch, base, base_commit, before) = match req.isolation {
|
||||
Isolation::Worktree => {
|
||||
// The UI offers worktrees for git projects only; state it when it isn't one.
|
||||
if Repo::discover(&self.shared.paths.project_root)?.is_none() {
|
||||
return Err(IdeError::invalid(
|
||||
"worktrees need a git repository: run the task in the local checkout",
|
||||
));
|
||||
}
|
||||
let dir = self.shared.paths.worktrees_dir.clone();
|
||||
let (t, b) = (title.clone(), req.base.clone());
|
||||
let c = blocking(move || worktree::create(&root, &dir, &t, b.as_deref())).await?;
|
||||
|
||||
@@ -53,8 +53,9 @@ pub(crate) fn remove(project_root: &Path, path: &Path) -> IdeResult<()> {
|
||||
}
|
||||
|
||||
/// Files under `cwd` that differ from `base` (untracked files included), with line counts.
|
||||
/// Outside a repository there is nothing to compare against: no changes.
|
||||
pub(crate) fn changes(cwd: &Path, base: &str) -> IdeResult<Vec<AgentChange>> {
|
||||
let repo = repo_at(cwd)?;
|
||||
let Some(repo) = Repo::discover(cwd)? else { return Ok(Vec::new()) };
|
||||
let root = repo.root().to_path_buf();
|
||||
let stats = repo.numstat_with_worktree(base)?;
|
||||
let stat = |rel: &str| stats.iter().find(|(p, ..)| p == rel).map_or((0, 0), |&(_, a, r)| (a, r));
|
||||
@@ -96,7 +97,7 @@ pub(crate) fn without_untouched(changes: Vec<AgentChange>, before: &BTreeMap<Str
|
||||
}
|
||||
|
||||
pub(crate) fn file_at(cwd: &Path, path: &Path, rev: &str) -> IdeResult<Option<String>> {
|
||||
let repo = repo_at(cwd)?;
|
||||
let Some(repo) = Repo::discover(cwd)? else { return Ok(None) };
|
||||
let encoding = match ide_vfs::load_text(path) {
|
||||
Ok(loaded) => encoding_rs::Encoding::for_label(loaded.encoding.as_bytes()).unwrap_or(encoding_rs::UTF_8),
|
||||
Err(_) => ide_vfs::detect_encoding(&repo.file_bytes_at(path, Some(rev))?.unwrap_or_default(), None),
|
||||
@@ -141,6 +142,20 @@ mod tests {
|
||||
assert_eq!(unique_slug("x", |s| s == "x" || s == "x-2"), "x-3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outside_a_repository_there_is_nothing_to_compare() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dir = tmp.path().join("plain");
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
std::fs::write(dir.join("a.txt"), "one\n").unwrap();
|
||||
|
||||
assert!(changes(&dir, "HEAD").unwrap().is_empty());
|
||||
assert!(snapshot(&dir).unwrap().is_empty());
|
||||
assert_eq!(file_at(&dir, &dir.join("a.txt"), "HEAD").unwrap(), None);
|
||||
assert!(create(&dir, &tmp.path().join("wt"), "Edit A", None).is_err());
|
||||
assert!(remove(&dir, &dir).is_err());
|
||||
}
|
||||
|
||||
fn git(dir: &Path, args: &[&str]) {
|
||||
let ok = std::process::Command::new("git")
|
||||
.args(["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"])
|
||||
|
||||
@@ -48,6 +48,9 @@ export function HomeEditor() {
|
||||
const probe = useProbe(provider?.installed ? provider.id : null);
|
||||
const agentCommands = useAgentCommands(provider?.id, null, provider?.commands ?? NO_COMMANDS);
|
||||
const providerOptions: SelectOption[] = agentOptions(providers, tx, providerIcon);
|
||||
// Without git under the project there are no worktrees: force the local checkout.
|
||||
const isGit = project?.isGit ?? false;
|
||||
const isolation = isGit ? draft.isolation : "local";
|
||||
const base = draft.base ?? current;
|
||||
const canStart = canSend(composer) && !!provider?.installed && !starting;
|
||||
const root = project?.root ?? "";
|
||||
@@ -56,7 +59,7 @@ export function HomeEditor() {
|
||||
if (!canStart) return;
|
||||
setStarting(true);
|
||||
setError(null);
|
||||
startTask()
|
||||
startTask(isolation)
|
||||
.catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)))
|
||||
.finally(() => setStarting(false));
|
||||
};
|
||||
@@ -102,17 +105,24 @@ export function HomeEditor() {
|
||||
/>
|
||||
<div className={styles.where}>
|
||||
<span className={styles.project}>{project?.name}</span>
|
||||
<Select
|
||||
variant="ghost"
|
||||
aria-label="Isolation"
|
||||
value={draft.isolation}
|
||||
options={[
|
||||
{ value: "worktree", label: tx("New worktree"), icon: "branch", detail: tx("away from your changes") },
|
||||
{ value: "local", label: tx("Local checkout"), icon: "folder", detail: tx("edits your files directly") },
|
||||
]}
|
||||
onChange={(isolation) => updateDraft({ isolation })}
|
||||
/>
|
||||
{draft.isolation === "local" && current && (
|
||||
{isGit && (
|
||||
<Select
|
||||
variant="ghost"
|
||||
aria-label="Isolation"
|
||||
value={isolation}
|
||||
options={[
|
||||
{ value: "worktree", label: tx("New worktree"), icon: "branch", detail: tx("away from your changes") },
|
||||
{
|
||||
value: "local",
|
||||
label: tx("Local checkout"),
|
||||
icon: "folder",
|
||||
detail: tx("edits your files directly"),
|
||||
},
|
||||
]}
|
||||
onChange={(isolation) => updateDraft({ isolation })}
|
||||
/>
|
||||
)}
|
||||
{isolation === "local" && current && (
|
||||
<>
|
||||
<span className={styles.note}>
|
||||
<T>on</T>
|
||||
@@ -123,7 +133,7 @@ export function HomeEditor() {
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{draft.isolation === "worktree" && branches.length > 0 && (
|
||||
{isolation === "worktree" && branches.length > 0 && (
|
||||
<>
|
||||
<span className={styles.note}>
|
||||
<T>from</T>
|
||||
|
||||
@@ -346,7 +346,8 @@ export function openHome(prompt?: string) {
|
||||
void editorService().openInput({ kind: HOME_KIND, key: HOME_KEY, data: {} }, { focus: true });
|
||||
}
|
||||
|
||||
export async function startTask(): Promise<void> {
|
||||
/** `isolation` overrides the draft's (a project without git can only use the local checkout). */
|
||||
export async function startTask(isolation: Isolation = useAgents.getState().draft.isolation): Promise<void> {
|
||||
const { draft } = useAgents.getState();
|
||||
const composer = composerDraft(HOME);
|
||||
if (!canSend(composer)) return;
|
||||
@@ -355,8 +356,8 @@ export async function startTask(): Promise<void> {
|
||||
prompt: toPrompt(composer),
|
||||
provider: draft.provider,
|
||||
config: Object.entries(draft.config).map(([id, value]) => ({ id, value })),
|
||||
isolation: draft.isolation,
|
||||
base: draft.isolation === "worktree" ? draft.base : null,
|
||||
isolation,
|
||||
base: isolation === "worktree" ? draft.base : null,
|
||||
}),
|
||||
);
|
||||
// Its events can arrive before this reply: keep the newer state they carried.
|
||||
|
||||
Reference in new issue
Заблокувати користувача