mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
## What An Agents feature: hand a task to a coding agent and follow it from the IDE. Agents speak the [Agent Client Protocol](https://agentclientprotocol.com). Built in: Claude Code, Codex, Gemini CLI, GitHub Copilot, Cursor, OpenCode, Qwen Code, Kimi CLI, Goose, Auggie, Kiro, Mistral Vibe. You can add your own in `<config>/agents/<id>.toml` (`name`, `command`, `args`, `[env]`). An agent whose command isn't on `PATH` is listed under "Not installed". - **New Task page.** An editor page with a prompt box. You pick the agent, model, mode and thinking effort, and whether it runs in a new worktree (from a chosen branch) or in the checkout. - **Agents tool window.** Tasks are grouped by what they need: Needs Input, Failed, Running, Ready to Review, Archived. It also shows a stripe badge, a status bar item and notifications. - **Session tab.** - The conversation: Markdown, thinking, tool calls with diff stats, the plan. - Permission requests appear as an editor banner. - The changed files are compared against the task's base commit and open in the diff viewer. - A context-usage meter. - **Composer.** - `@` references files from the project index. A referenced file's text is sent when the agent accepts embedded context, otherwise a `resource_link`. - `/` lists the agent's commands; ↑/↓ recall earlier messages. - Images can be pasted, dropped or picked; Shift+Tab cycles the mode. - While the agent works, Enter queues the message and ⌘↵ interrupts the turn and sends it now. Queued messages can be sent now, edited or removed. - **Send to Agent.** In the editor and Project view context menus. It adds `@file:10-20` references to the message you typed last. - Landing the work is left to the existing Git features: the worktree is a normal branch. ## How - **`crates/ide-agent`**: one ACP agent process per task, running in the task's directory. - It folds `session/update` into a timeline and answers `session/request_permission`. - It tracks the session's settings: `configOptions`, falling back to the older `modes` / `models` fields. The user's choices are applied again on every new or reloaded session. - Tasks are saved as JSON per task. Sessions resume with `session/load` without duplicating history, and old task files are upgraded when read. - **`ide-jsonrpc`**: adds newline-delimited framing (ACP) next to Content-Length (LSP). - **`ide-vcs`**: adds `numstat_with_worktree` for the changes list. - **Editor**: - The Switcher (⌃⇥) lists every open editor, as IDEA does, not just files. - `EditorKindContribution.restorable` lets a kind's tabs be saved with the editor session, so agent tabs survive reloads. - **UI**: `Select` popups now size to fit short option details (they used to cut them off). ## Testing - `cargo test -p ide-agent` (14 tests). They use an in-memory ACP agent and cover permissions, restart with `session/load`, settings, the queue with send-now and stop, and prompt blocks. - There is also an ignored end-to-end test against the real Claude Code adapter: `OPENIDEA_AGENT_E2E=1 cargo test -p ide-agent real_claude -- --ignored`. - `pnpm vitest run`: 2052 tests. - `cargo clippy -p ide-agent -p openidea --all-targets -- -D warnings` and typecheck are clean. - Driven in the running app with Claude Code: - A task in a worktree, with permission answers, changes listed and the diff opened. - A follow-up after restarting the IDE. - `@` reference with embedded content, `/` commands, Shift+Tab mode changes. - A queued message and send-now, an image prompt, Send to Agent from a selection. - The Switcher and restored tabs after a reload. Not covered yet: only Claude Code was driven end to end. The other agents go through the same ACP path but weren't run here. Merged on the maintainer's go-ahead. Merge with main: kept both sides in Cargo.toml (ide-agent + ide-clipboard), the feature registry (agents + vim) and zh.json (no duplicate keys). After the merge: typecheck clean, full vitest 268 files / 2247 tests passed, ide-agent/ide-jsonrpc/ide-vcs 144 passed, bindings unchanged after regeneration.
286 lines
8.4 KiB
Rust
286 lines
8.4 KiB
Rust
//! IPC types: tasks, their timeline entries and the events streamed to the UI.
|
|
|
|
use ide_core::IpcPath;
|
|
use serde::{Deserialize, Serialize};
|
|
use specta::Type;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum AgentStatus {
|
|
/// Creating the worktree / starting the agent.
|
|
Starting,
|
|
Running,
|
|
/// Waiting for a permission answer.
|
|
NeedsInput,
|
|
/// The turn ended; the result can be reviewed or the conversation continued.
|
|
Ready,
|
|
Failed,
|
|
Archived,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum Isolation {
|
|
/// A new git worktree on its own branch.
|
|
Worktree,
|
|
/// The project's own checkout.
|
|
Local,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentTask {
|
|
pub id: String,
|
|
pub title: String,
|
|
/// [`AgentProvider::id`].
|
|
pub provider: String,
|
|
/// Model the task runs on (its `model` setting), when known.
|
|
pub model: Option<String>,
|
|
pub status: AgentStatus,
|
|
pub isolation: Isolation,
|
|
/// Directory the agent works in (the worktree, or the project root).
|
|
pub cwd: IpcPath,
|
|
/// Branch of the task's worktree.
|
|
pub branch: Option<String>,
|
|
/// Branch (or revision) the worktree was created from.
|
|
pub base: Option<String>,
|
|
/// Commit the task's changes are compared against.
|
|
pub base_commit: Option<String>,
|
|
/// Why the task failed, when it did.
|
|
pub error: Option<String>,
|
|
pub created_ms: f64,
|
|
pub updated_ms: f64,
|
|
/// The session's settings (model, mode, thinking effort…) as last reported, with the
|
|
/// user's choices applied.
|
|
pub config: Vec<AgentConfigOption>,
|
|
/// Slash commands the agent offers.
|
|
pub commands: Vec<AgentCommand>,
|
|
/// Context window use, when the agent reports it.
|
|
pub usage: Option<AgentUsage>,
|
|
/// Messages waiting for the current turn to end, oldest first.
|
|
pub queue: Vec<AgentQueued>,
|
|
}
|
|
|
|
/// One value of a select-type session setting.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentConfigValue {
|
|
pub value: String,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// How a setting is changed.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum AgentConfigVia {
|
|
/// `session/set_config_option`.
|
|
#[default]
|
|
Option,
|
|
/// `session/set_mode` (agents that report `modes` instead of a mode config option).
|
|
Mode,
|
|
/// `session/set_model` (agents that report `models` instead of a model config option).
|
|
Model,
|
|
}
|
|
|
|
/// A session setting: an ACP select config option, or the older `modes` / `models`.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentConfigOption {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
/// ACP category: `model`, `mode`, `thought_level`, or agent-specific.
|
|
pub category: Option<String>,
|
|
pub current: Option<String>,
|
|
pub values: Vec<AgentConfigValue>,
|
|
pub via: AgentConfigVia,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentCommand {
|
|
/// Without the leading `/`.
|
|
pub name: String,
|
|
pub description: String,
|
|
/// What to type after the command, when it takes input.
|
|
pub hint: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentUsage {
|
|
/// Tokens in the context window.
|
|
pub used: u32,
|
|
/// Size of the context window.
|
|
pub size: u32,
|
|
/// Session cost so far.
|
|
pub cost: Option<f64>,
|
|
pub currency: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentImage {
|
|
pub mime_type: String,
|
|
/// Base64, without a `data:` prefix.
|
|
pub data: String,
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
/// A message to the agent: text, plus files to look at and images.
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentPrompt {
|
|
pub text: String,
|
|
pub files: Vec<IpcPath>,
|
|
pub images: Vec<AgentImage>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentQueued {
|
|
pub id: String,
|
|
pub prompt: AgentPrompt,
|
|
}
|
|
|
|
/// A setting chosen for a new task, by [`AgentConfigOption::id`].
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentConfigChoice {
|
|
pub id: String,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum AgentToolStatus {
|
|
Pending,
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentToolDiff {
|
|
pub path: IpcPath,
|
|
pub added: u32,
|
|
pub removed: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentPlanItem {
|
|
pub content: String,
|
|
/// `pending`, `in_progress` or `completed`.
|
|
pub status: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentPermissionOption {
|
|
pub id: String,
|
|
pub name: String,
|
|
/// `allow_once`, `allow_always`, `reject_once` or `reject_always`.
|
|
pub kind: String,
|
|
}
|
|
|
|
/// One item of a task's conversation.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
|
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
|
pub enum AgentEntry {
|
|
User {
|
|
text: String,
|
|
/// Names of the files and images sent with it.
|
|
attachments: Vec<String>,
|
|
},
|
|
Agent {
|
|
text: String,
|
|
},
|
|
Thought {
|
|
text: String,
|
|
},
|
|
Tool {
|
|
id: String,
|
|
title: String,
|
|
/// ACP tool kind: `read`, `edit`, `execute`, `search`, `fetch`, `think`…
|
|
kind: String,
|
|
status: AgentToolStatus,
|
|
locations: Vec<IpcPath>,
|
|
/// Text the tool reported (command output…), truncated.
|
|
output: Option<String>,
|
|
diffs: Vec<AgentToolDiff>,
|
|
},
|
|
Plan {
|
|
items: Vec<AgentPlanItem>,
|
|
},
|
|
Permission {
|
|
id: u32,
|
|
title: String,
|
|
options: Vec<AgentPermissionOption>,
|
|
/// Name of the chosen option, or `None` while pending. Cancelled requests read "Cancelled".
|
|
chosen: Option<String>,
|
|
},
|
|
Error {
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentProvider {
|
|
pub id: String,
|
|
pub name: String,
|
|
/// The command line that starts its ACP server.
|
|
pub command: String,
|
|
/// Whether the command was found (on `PATH`).
|
|
pub installed: bool,
|
|
/// Settings (models, modes…) the agent offered last time it ran; empty before its first session.
|
|
pub config: Vec<AgentConfigOption>,
|
|
/// Slash commands it offered last time.
|
|
pub commands: Vec<AgentCommand>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct NewAgentTask {
|
|
pub prompt: AgentPrompt,
|
|
pub provider: String,
|
|
/// Settings for the new session (model, mode…).
|
|
pub config: Vec<AgentConfigChoice>,
|
|
pub isolation: Isolation,
|
|
/// Branch to start the worktree from; the current branch when `None`.
|
|
pub base: Option<String>,
|
|
}
|
|
|
|
/// A file the task changed, relative to [`AgentTask::base_commit`].
|
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentChange {
|
|
pub path: IpcPath,
|
|
pub rel_path: String,
|
|
pub kind: ide_vcs::ChangeKind,
|
|
pub added: u32,
|
|
pub removed: u32,
|
|
}
|
|
|
|
/// Streamed to the window that subscribed (`AgentManager::subscribe`).
|
|
#[derive(Clone, Debug, Serialize, Type)]
|
|
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
|
pub enum AgentEvent {
|
|
/// A task was created or its metadata changed.
|
|
Task {
|
|
task: AgentTask,
|
|
},
|
|
Removed {
|
|
id: String,
|
|
},
|
|
/// Entry `index` of `task` was added or replaced.
|
|
Entry {
|
|
task: String,
|
|
index: u32,
|
|
entry: AgentEntry,
|
|
},
|
|
}
|