feat(remote): Run agent tasks on the remote host; show why the agent list cannot load

Цей коміт міститься в:
l0ng-ai committed 2026-10-27 22:12:58 +08:00
1 джерело ec41d3c4a0
коміт 831cd59ab0
7 files changed
+171 -33

No files matched your search

+114
Переглянути файл
@@ -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
Переглянути файл
@@ -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);
+24 -25
Переглянути файл
@@ -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
}
+8
Переглянути файл
@@ -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;
+9 -2
Переглянути файл
@@ -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 (
+12 -5
Переглянути файл
@@ -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) {
+2 -1
Переглянути файл
@@ -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": "无法加载智能体任务"
}