дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(agents): Dispatch and supervise coding agents over ACP (#72)
## 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.
Цей коміт міститься в:
1 джерело
b4742b730b
коміт
2eaa6cb0c7
75 files changed
+6377
-63
No files matched your search
@@ -0,0 +1,406 @@
|
||||
.session {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
background: var(--bg-editor);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 6px 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.title {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
color: var(--fg-strong);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.headerDetail {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
color: var(--fg-subtle);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.providerIcon {
|
||||
vertical-align: -2px;
|
||||
margin-right: 4px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.muted {
|
||||
color: var(--fg-subtle);
|
||||
}
|
||||
|
||||
/* Pending permission: an editor notification banner. */
|
||||
.banner {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
background: var(--bg-feedback-warning);
|
||||
border-bottom: 1px solid var(--border-feedback-warning);
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.bannerText {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bannerSpacer {
|
||||
flex: 1;
|
||||
}
|
||||
.bannerLink {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--link);
|
||||
font: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bannerLink:hover:not(:disabled) {
|
||||
color: var(--link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.bannerLink:disabled {
|
||||
color: var(--fg-subtle);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stream {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 14px 16px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user {
|
||||
padding: 2px 0 2px 10px;
|
||||
border-left: 2px solid var(--accent);
|
||||
color: var(--fg-strong);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.markdown :global(p) {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.markdown :global(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown :global(code) {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-small);
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
.markdown :global(pre) {
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--bg-elev);
|
||||
overflow: auto;
|
||||
}
|
||||
.markdown :global(pre code) {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.markdown :global(ul),
|
||||
.markdown :global(ol) {
|
||||
margin: 0 0 8px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.markdown :global(a) {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.thought summary,
|
||||
.toolDetails > summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
.thought summary::-webkit-details-marker,
|
||||
.toolDetails > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.thought summary {
|
||||
color: var(--fg-muted);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.thoughtText {
|
||||
margin-top: 4px;
|
||||
padding-left: 10px;
|
||||
border-left: 2px solid var(--border-strong);
|
||||
color: var(--fg-secondary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 2px 8px;
|
||||
margin-left: -8px;
|
||||
border-radius: var(--radius-row);
|
||||
color: var(--fg-secondary);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
.toolDetails > summary.tool:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--fg);
|
||||
}
|
||||
.toolTitle {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-small);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.toolDiff {
|
||||
font: inherit;
|
||||
font-size: var(--fs-small);
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0 4px;
|
||||
border-radius: var(--radius-small);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toolDiff:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.output {
|
||||
margin: 4px 0 0;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-small);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--fs-small);
|
||||
white-space: pre-wrap;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
|
||||
.plan {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
.planTitle {
|
||||
font-weight: 600;
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.planItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.planDone {
|
||||
color: var(--fg-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.permissionLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
color: var(--fg-secondary);
|
||||
}
|
||||
.permissionTitle {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
color: var(--danger);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.working {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
|
||||
.composerArea {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 6px 12px 12px;
|
||||
}
|
||||
|
||||
.queue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.queued {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 24px;
|
||||
padding: 0 4px 0 8px;
|
||||
border-radius: var(--radius-row);
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
.queuedText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--fg-subtle);
|
||||
font-size: var(--fs-small);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.usageBar {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.usageBar > span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--fg-muted);
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
margin-top: 4px;
|
||||
color: var(--fg-muted);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
.attachment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.changes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.changesHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
flex: none;
|
||||
padding: 0 6px 0 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.changesTitle {
|
||||
font-weight: 600;
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.changesSummary {
|
||||
flex: none;
|
||||
padding: 4px 10px;
|
||||
color: var(--fg-subtle);
|
||||
white-space: pre;
|
||||
}
|
||||
.changesList {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.changesList > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.changesEmpty {
|
||||
padding: 8px 10px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.change {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.changeName {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.changeDir {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-left: 4px;
|
||||
color: var(--fg-subtle);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.changeStat {
|
||||
flex: none;
|
||||
padding-right: 8px;
|
||||
font-size: var(--fs-small);
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.added {
|
||||
color: var(--vcs-added);
|
||||
}
|
||||
.removed {
|
||||
color: var(--danger);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача