дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(remote): Run agent tasks on the remote host; show why the agent list cannot load
Цей коміт міститься в:
1 джерело
ec41d3c4a0
коміт
831cd59ab0
7 files changed
+171
-33
No files matched your search
@@ -0,0 +1,114 @@
|
||||
//! Agents tool window: coding agent tasks of the project (over ACP). In a remote
|
||||
//! window they run on the host, next to the project they work on.
|
||||
use super::Emit;
|
||||
use crate::Host;
|
||||
use crate::rpc::Dispatcher;
|
||||
use ide_agent::{
|
||||
AgentChange, AgentCommand, AgentEntry, AgentEvent, AgentPrompt, AgentProvider, AgentQueued, AgentTask, NewAgentTask,
|
||||
};
|
||||
use ide_core::{IdeResult, IpcPath};
|
||||
|
||||
/// Streams the project's agent events to this window; returns every task.
|
||||
pub fn subscribe(host: &Host, window: &str, on_event: Emit<AgentEvent>) -> IdeResult<Vec<AgentTask>> {
|
||||
Ok(host.project(window)?.agents.subscribe(on_event))
|
||||
}
|
||||
|
||||
pub fn providers(host: &Host, window: &str) -> IdeResult<Vec<AgentProvider>> {
|
||||
Ok(host.project(window)?.agents.providers())
|
||||
}
|
||||
|
||||
/// Slash commands to complete for `provider` in `task`'s directory, or in the project
|
||||
/// before a task exists (skills and custom commands on disk included).
|
||||
pub async fn commands(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
provider: String,
|
||||
task: Option<String>,
|
||||
) -> IdeResult<Vec<AgentCommand>> {
|
||||
host.project(window)?.agents.commands(&provider, task.as_deref()).await
|
||||
}
|
||||
|
||||
pub fn entries(host: &Host, window: &str, task: String) -> IdeResult<Vec<AgentEntry>> {
|
||||
host.project(window)?.agents.entries(&task)
|
||||
}
|
||||
|
||||
pub async fn create(host: &Host, window: &str, request: NewAgentTask) -> IdeResult<AgentTask> {
|
||||
host.project(window)?.agents.create(request).await
|
||||
}
|
||||
|
||||
/// Sends a message; while the agent works it is queued, or with `now` it interrupts the turn.
|
||||
pub async fn prompt(host: &Host, window: &str, task: String, prompt: AgentPrompt, now: bool) -> IdeResult<()> {
|
||||
host.project(window)?.agents.prompt(&task, prompt, now).await
|
||||
}
|
||||
|
||||
/// Drops a queued message and returns it (to edit it).
|
||||
pub fn unqueue(host: &Host, window: &str, task: String, queued: String) -> IdeResult<AgentQueued> {
|
||||
host.project(window)?.agents.unqueue(&task, &queued)
|
||||
}
|
||||
|
||||
/// Sends a queued message now, interrupting the current turn.
|
||||
pub fn send_queued_now(host: &Host, window: &str, task: String, queued: String) -> IdeResult<()> {
|
||||
host.project(window)?.agents.send_queued_now(&task, &queued)
|
||||
}
|
||||
|
||||
/// Changes a session setting (model, mode, thinking effort…).
|
||||
pub async fn set_config(host: &Host, window: &str, task: String, option: String, value: String) -> IdeResult<()> {
|
||||
host.project(window)?.agents.set_config(&task, &option, value).await
|
||||
}
|
||||
|
||||
pub fn cancel(host: &Host, window: &str, task: String) -> IdeResult<()> {
|
||||
host.project(window)?.agents.cancel(&task)
|
||||
}
|
||||
|
||||
/// Answers a permission request; `option: None` cancels it.
|
||||
pub fn respond(host: &Host, window: &str, task: String, permission: u32, option: Option<String>) -> IdeResult<()> {
|
||||
host.project(window)?.agents.respond(&task, permission, option)
|
||||
}
|
||||
|
||||
pub fn archive(host: &Host, window: &str, task: String) -> IdeResult<()> {
|
||||
host.project(window)?.agents.archive(&task)
|
||||
}
|
||||
|
||||
pub async fn delete(host: &Host, window: &str, task: String, remove_worktree: bool) -> IdeResult<()> {
|
||||
host.project(window)?.agents.delete(&task, remove_worktree).await
|
||||
}
|
||||
|
||||
pub async fn changes(host: &Host, window: &str, task: String) -> IdeResult<Vec<AgentChange>> {
|
||||
host.project(window)?.agents.changes(&task).await
|
||||
}
|
||||
|
||||
/// Content of `path` when the task started (left side of its diff).
|
||||
pub async fn base_text(host: &Host, window: &str, task: String, path: IpcPath) -> IdeResult<Option<String>> {
|
||||
host.project(window)?.agents.base_text(&task, path.into_path_buf()).await
|
||||
}
|
||||
|
||||
/// Commands of this module the remote server answers.
|
||||
pub(crate) fn register(d: &mut Dispatcher) {
|
||||
d.add("agent_subscribe", |c| async move { subscribe(&c.host, &c.window, c.channel("onEvent")?) });
|
||||
d.add("agent_providers", |c| async move { providers(&c.host, &c.window) });
|
||||
d.add("agent_commands", |c| async move { commands(&c.host, &c.window, c.arg("provider")?, c.arg("task")?).await });
|
||||
d.add("agent_entries", |c| async move { entries(&c.host, &c.window, c.arg("task")?) });
|
||||
d.add("agent_create", |c| async move { create(&c.host, &c.window, c.arg("request")?).await });
|
||||
d.add("agent_prompt", |c| async move {
|
||||
prompt(&c.host, &c.window, c.arg("task")?, c.arg("prompt")?, c.arg("now")?).await
|
||||
});
|
||||
d.add("agent_unqueue", |c| async move { unqueue(&c.host, &c.window, c.arg("task")?, c.arg("queued")?) });
|
||||
d.add(
|
||||
"agent_send_queued_now",
|
||||
|c| async move { send_queued_now(&c.host, &c.window, c.arg("task")?, c.arg("queued")?) },
|
||||
);
|
||||
d.add("agent_set_config", |c| async move {
|
||||
set_config(&c.host, &c.window, c.arg("task")?, c.arg("option")?, c.arg("value")?).await
|
||||
});
|
||||
d.add("agent_cancel", |c| async move { cancel(&c.host, &c.window, c.arg("task")?) });
|
||||
d.add("agent_respond", |c| async move {
|
||||
respond(&c.host, &c.window, c.arg("task")?, c.arg("permission")?, c.arg("option")?)
|
||||
});
|
||||
d.add("agent_archive", |c| async move { archive(&c.host, &c.window, c.arg("task")?) });
|
||||
d.add(
|
||||
"agent_delete",
|
||||
|c| async move { delete(&c.host, &c.window, c.arg("task")?, c.arg("removeWorktree")?).await },
|
||||
);
|
||||
d.add("agent_changes", |c| async move { changes(&c.host, &c.window, c.arg("task")?).await });
|
||||
d.add("agent_base_text", |c| async move { base_text(&c.host, &c.window, c.arg("task")?, c.arg("path")?).await });
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
//! remote server ([`crate::rpc`]): each takes the [`Host`], the calling window
|
||||
//! and the command's arguments, and streams through [`Emit`] callbacks.
|
||||
|
||||
pub mod agent;
|
||||
pub mod analysis;
|
||||
pub mod build;
|
||||
pub mod bytes;
|
||||
@@ -55,6 +56,7 @@ impl<T> Clone for Channel<T> {
|
||||
|
||||
/// Registers the commands of every module with the remote server.
|
||||
pub fn register_all(d: &mut crate::rpc::Dispatcher) {
|
||||
agent::register(d);
|
||||
lsp::register(d);
|
||||
nav::register(d);
|
||||
refactor::register(d);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! IPC commands of `ide_host::ops::agent` (the shared implementation).
|
||||
|
||||
use crate::state::AppState;
|
||||
use ide_agent::{
|
||||
AgentChange, AgentCommand, AgentEntry, AgentEvent, AgentPrompt, AgentProvider, AgentQueued, AgentTask, NewAgentTask,
|
||||
};
|
||||
use ide_core::{IdeResult, IpcPath};
|
||||
use ide_host::ops::agent;
|
||||
use std::sync::Arc;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{State, Window};
|
||||
@@ -15,16 +18,19 @@ pub fn agent_subscribe(
|
||||
state: State<'_, AppState>,
|
||||
on_event: Channel<AgentEvent>,
|
||||
) -> IdeResult<Vec<AgentTask>> {
|
||||
let project = state.project(window.label())?;
|
||||
Ok(project.agents.subscribe(Arc::new(move |e| {
|
||||
let _ = on_event.send(e);
|
||||
})))
|
||||
agent::subscribe(
|
||||
&state.host,
|
||||
window.label(),
|
||||
Arc::new(move |e| {
|
||||
let _ = on_event.send(e);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn agent_providers(window: Window, state: State<'_, AppState>) -> IdeResult<Vec<AgentProvider>> {
|
||||
Ok(state.project(window.label())?.agents.providers())
|
||||
agent::providers(&state.host, window.label())
|
||||
}
|
||||
|
||||
/// Slash commands to complete for `provider` in `task`'s directory, or in the project
|
||||
@@ -37,21 +43,19 @@ pub async fn agent_commands(
|
||||
provider: String,
|
||||
task: Option<String>,
|
||||
) -> IdeResult<Vec<AgentCommand>> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.commands(&provider, task.as_deref()).await
|
||||
agent::commands(&state.host, window.label(), provider, task).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn agent_entries(window: Window, state: State<'_, AppState>, task: String) -> IdeResult<Vec<AgentEntry>> {
|
||||
state.project(window.label())?.agents.entries(&task)
|
||||
agent::entries(&state.host, window.label(), task)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn agent_create(window: Window, state: State<'_, AppState>, request: NewAgentTask) -> IdeResult<AgentTask> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.create(request).await
|
||||
agent::create(&state.host, window.label(), request).await
|
||||
}
|
||||
|
||||
/// Sends a message; while the agent works it is queued, or with `now` it interrupts the turn.
|
||||
@@ -64,8 +68,7 @@ pub async fn agent_prompt(
|
||||
prompt: AgentPrompt,
|
||||
now: bool,
|
||||
) -> IdeResult<()> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.prompt(&task, prompt, now).await
|
||||
agent::prompt(&state.host, window.label(), task, prompt, now).await
|
||||
}
|
||||
|
||||
/// Drops a queued message and returns it (to edit it).
|
||||
@@ -77,7 +80,7 @@ pub fn agent_unqueue(
|
||||
task: String,
|
||||
queued: String,
|
||||
) -> IdeResult<AgentQueued> {
|
||||
state.project(window.label())?.agents.unqueue(&task, &queued)
|
||||
agent::unqueue(&state.host, window.label(), task, queued)
|
||||
}
|
||||
|
||||
/// Sends a queued message now, interrupting the current turn.
|
||||
@@ -89,7 +92,7 @@ pub fn agent_send_queued_now(
|
||||
task: String,
|
||||
queued: String,
|
||||
) -> IdeResult<()> {
|
||||
state.project(window.label())?.agents.send_queued_now(&task, &queued)
|
||||
agent::send_queued_now(&state.host, window.label(), task, queued)
|
||||
}
|
||||
|
||||
/// Changes a session setting (model, mode, thinking effort…).
|
||||
@@ -102,14 +105,13 @@ pub async fn agent_set_config(
|
||||
option: String,
|
||||
value: String,
|
||||
) -> IdeResult<()> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.set_config(&task, &option, value).await
|
||||
agent::set_config(&state.host, window.label(), task, option, value).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn agent_cancel(window: Window, state: State<'_, AppState>, task: String) -> IdeResult<()> {
|
||||
state.project(window.label())?.agents.cancel(&task)
|
||||
agent::cancel(&state.host, window.label(), task)
|
||||
}
|
||||
|
||||
/// Answers a permission request; `option: null` cancels it.
|
||||
@@ -122,13 +124,13 @@ pub fn agent_respond(
|
||||
permission: u32,
|
||||
option: Option<String>,
|
||||
) -> IdeResult<()> {
|
||||
state.project(window.label())?.agents.respond(&task, permission, option)
|
||||
agent::respond(&state.host, window.label(), task, permission, option)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn agent_archive(window: Window, state: State<'_, AppState>, task: String) -> IdeResult<()> {
|
||||
state.project(window.label())?.agents.archive(&task)
|
||||
agent::archive(&state.host, window.label(), task)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -139,15 +141,13 @@ pub async fn agent_delete(
|
||||
task: String,
|
||||
remove_worktree: bool,
|
||||
) -> IdeResult<()> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.delete(&task, remove_worktree).await
|
||||
agent::delete(&state.host, window.label(), task, remove_worktree).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn agent_changes(window: Window, state: State<'_, AppState>, task: String) -> IdeResult<Vec<AgentChange>> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.changes(&task).await
|
||||
agent::changes(&state.host, window.label(), task).await
|
||||
}
|
||||
|
||||
/// Content of `path` when the task started (left side of its diff).
|
||||
@@ -159,6 +159,5 @@ pub async fn agent_base_text(
|
||||
task: String,
|
||||
path: IpcPath,
|
||||
) -> IdeResult<Option<String>> {
|
||||
let project = state.project(window.label())?;
|
||||
project.agents.base_text(&task, path.into_path_buf()).await
|
||||
agent::base_text(&state.host, window.label(), task, path).await
|
||||
}
|
||||
@@ -9,6 +9,14 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
|
||||
import type { AgentStatus as Status, AgentTask } from "@/bindings";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { confirmChecked } from "@/workbench/messages";
|
||||
import { Icon, IconButton, Spinner, T, Tree, showContextMenu, type MenuItem } from "@/ui";
|
||||
import { EmptyState, Icon, IconButton, Spinner, T, Tree, showContextMenu, type MenuItem } from "@/ui";
|
||||
import { age, branchLabel, groupTasks, isBusy, type TaskGroup } from "./model";
|
||||
import { archiveTask, deleteTask, openHome, openTask, useAgents } from "./store";
|
||||
import styles from "./Agents.module.css";
|
||||
@@ -35,13 +35,20 @@ export function TaskStatusIcon({ status }: { status: Status }) {
|
||||
|
||||
export function AgentsView() {
|
||||
const loaded = useAgents((s) => s.loaded);
|
||||
const error = useAgents((s) => s.error);
|
||||
const tasks = useAgents((s) => s.tasks);
|
||||
const providers = useAgents((s) => s.providers);
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(() => new Set(["g:archived"]));
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set());
|
||||
const roots = useMemo<Node[]>(() => groupTasks(tasks.values()).map((group) => ({ kind: "group", group })), [tasks]);
|
||||
const expanded = useMemo(() => new Set(roots.map(nodeKey).filter((k) => !collapsed.has(k))), [roots, collapsed]);
|
||||
if (!loaded) return <Spinner />;
|
||||
if (!loaded)
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
if (error) return <EmptyState title="Cannot load agent tasks">{error}</EmptyState>;
|
||||
const providerName = (id: string) => providers.find((p) => p.id === id)?.name ?? id;
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,6 +31,8 @@ export const HOME = "home";
|
||||
|
||||
interface AgentsState {
|
||||
loaded: boolean;
|
||||
/** Why the tasks could not be loaded. */
|
||||
error: string | null;
|
||||
tasks: Map<string, AgentTask>;
|
||||
/** Entries of the tasks opened in this window. */
|
||||
entries: Map<string, AgentEntry[]>;
|
||||
@@ -69,6 +71,7 @@ const fallbackDraft: Draft = { provider: "claude", config: {}, isolation: "workt
|
||||
|
||||
export const useAgents = create<AgentsState>(() => ({
|
||||
loaded: false,
|
||||
error: null,
|
||||
tasks: new Map(),
|
||||
entries: new Map(),
|
||||
changes: new Map(),
|
||||
@@ -111,13 +114,17 @@ export async function connect(): Promise<void> {
|
||||
channel.onmessage = (e) => {
|
||||
if (gen === generation) onEvent(e);
|
||||
};
|
||||
const [tasks, providers] = await Promise.all([
|
||||
call(commands.agentSubscribe(channel)),
|
||||
call(commands.agentProviders()),
|
||||
]);
|
||||
let tasks: AgentTask[], providers: AgentProvider[];
|
||||
try {
|
||||
[tasks, providers] = await Promise.all([call(commands.agentSubscribe(channel)), call(commands.agentProviders())]);
|
||||
} catch (e) {
|
||||
if (gen === generation) useAgents.setState({ loaded: true, error: e instanceof Error ? e.message : String(e) });
|
||||
return;
|
||||
}
|
||||
if (gen !== generation) return;
|
||||
useAgents.setState({
|
||||
loaded: true,
|
||||
error: null,
|
||||
tasks: new Map(tasks.map((t) => [t.id, t])),
|
||||
entries: new Map(),
|
||||
changes: new Map(),
|
||||
@@ -133,7 +140,7 @@ export async function connect(): Promise<void> {
|
||||
|
||||
export function disconnect() {
|
||||
generation++;
|
||||
useAgents.setState({ loaded: false, tasks: new Map(), entries: new Map(), changes: new Map() });
|
||||
useAgents.setState({ loaded: false, error: null, tasks: new Map(), entries: new Map(), changes: new Map() });
|
||||
}
|
||||
|
||||
function onEvent(e: AgentEvent) {
|
||||
|
||||
@@ -4159,5 +4159,6 @@
|
||||
"Downloading the IDE backend…": "正在下载 IDE 后端…",
|
||||
"Installing the IDE backend on the host…": "正在主机上安装 IDE 后端…",
|
||||
"Starting the IDE backend…": "正在启动 IDE 后端…",
|
||||
"Opening the project…": "正在打开项目…"
|
||||
"Opening the project…": "正在打开项目…",
|
||||
"Cannot load agent tasks": "无法加载智能体任务"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача