Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ tempfile = "=3.27.0"
tokio = { version = "=1.53.1", features = ["test-util"] }

[patch.crates-io]
agentkit-loop = { git = "https://github.com/danielkov/agentkit.git", rev = "513e92bb00465a86e5177084f33a130e85705bad" }
agentkit-acp = { git = "https://github.com/danielkov/agentkit.git", rev = "513e92bb00465a86e5177084f33a130e85705bad" }
agentkit-loop = { git = "https://github.com/danielkov/agentkit.git", rev = "8e4ee26434a3f847e3613da5bb073ae63a262243" }
agentkit-acp = { git = "https://github.com/danielkov/agentkit.git", rev = "8e4ee26434a3f847e3613da5bb073ae63a262243" }
agent-client-protocol = { git = "https://github.com/danielkov/rust-sdk.git", rev = "2f039993d1d6ed8da35b38c31f54a7cbb7338c70" }
agent-client-protocol-http = { git = "https://github.com/danielkov/rust-sdk.git", rev = "2f039993d1d6ed8da35b38c31f54a7cbb7338c70" }

Expand Down
20 changes: 18 additions & 2 deletions docs/user/tui-and-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ Use the `artifact` tool to read spilled output, including memory-only artifacts;
| --- | --- |
| `Enter` | Send a non-empty prompt when idle; steer and finish the current response while active if the agent advertises that capability |
| `Shift+Enter`, `Option+Enter`, `Ctrl+J` | Insert a newline |
| `Esc` | Interrupt a running turn; dismiss a notice when idle |
| `Esc` | Cancel a pending-message edit and restore the previous draft; leave the queue selector; otherwise interrupt a running turn or dismiss an idle notice |
| `F2` | Focus the pending-message queue (or return to the composer) |
| `Up` / `Down`, `Enter`, `Backspace` / `Delete` in the queue | Select a pending message, edit it if supported, or remove it |
| `Command+B` | Move the newest running foreground top-level compose call to the background |
| `Ctrl+C` | Interrupt a running turn; clear a non-empty idle prompt; quit when idle with an empty prompt |
| `Ctrl+D` | Quit when the prompt is empty |
Expand All @@ -63,7 +65,21 @@ Use the `artifact` tool to read spilled output, including memory-only artifacts;

Pasted text is inserted rather than sent. Bracketed paste is used when available; otherwise Kit treats a rapid key burst as a paste, so returns in that burst become line breaks. This keeps a multiline paste in one prompt. Press plain `Enter` afterward to submit it.

When the session is idle, `Enter` starts a normal prompt. While the agent is active, `Enter` uses ACP v2 `steer` injection with `finish` stream behavior only when the agent advertised both capabilities. An accepted injected user message appears in the transcript as part of the current turn. If steering is unavailable, the editor keeps the message and shows `this agent does not support active steering`. Local commands and agent-advertised session commands are available only while idle.
When the session is idle, `Enter` starts a normal prompt. While the agent is active, `Enter` uses ACP v2 `steer` injection with `finish` stream behavior only when the agent advertised both capabilities. An accepted injected user message first appears in the pending queue above the composer. It moves to the transcript when the agent delivers it as part of the current turn. If steering is unavailable, the editor keeps the message and shows `this agent does not support active steering`. Local commands and agent-advertised session commands are available only while idle.

### Edit or remove a pending message

Press `F2` to focus the queue, then `Up` or `Down` to select a message. The queue opens only when at least one message is pending; otherwise Kit shows `no pending messages` and leaves you in the composer. The selected row stays visible even when the queue is long. Press `Enter` to edit its text in the composer, or `Backspace` / `Delete` to request removal (the normal Mac Delete key works). `Esc` or `F2` returns from the selector without changing your draft. When the last message is delivered or removed, or the turn finishes, the selector closes automatically and keyboard focus returns to the composer. An in-progress text edit remains available to copy or cancel rather than being discarded.

After automatic closure, typing, pasting, or moving the composer cursor works normally. Until that fresh composer interaction (or `Esc` to acknowledge focus), Enter and deletion keys are ignored with a visible hint, so a key intended for the disappearing queue cannot send or delete your parked draft. An empty-queue F2 attempt does not enable this safeguard.

While editing, plain `Enter` saves the replacement with the **same message ID and queue position**. `Esc` cancels the edit. Either a successful save or cancellation restores the previous composer draft, cursor, and attachments. Queue edits do not run slash commands. Editing is available only when the agent advertises ACP `session.inject.pending.replace`; removing a pending injection does not require this optional capability.

Save and removal requests run without blocking the UI; updates and `Ctrl+C` cancellation remain available, including when delivery empties the selected queue. Only one change per message can be in flight. You can continue typing or press `Esc` while a save is in progress; `Esc` restores your draft but does not undo a request already sent. A save response never closes a reopened edit or discards text typed after that save began.

If a request fails, Kit retains the replacement text and the parked original draft. You can retry, or press `Esc` to restore the original. If the message has already been delivered or no longer exists, it cannot be changed or removed: the stale queue entry disappears and any in-progress edit stays available to copy before you press `Esc`. A late response never adds that entry back to the queue. Starting or resuming another session clears the queue, selection, and edit state.

Active steering continues to support media attachments. Pending-message **edits are text-only**: messages accepted with image, audio, or other non-text content cannot be edited in the queue, but can still be removed. Kit determines editability from the content actually sent, not stale attachments left in the composer. Media paths pasted during a queue edit are ordinary text, not new attachments. Attachments in the parked original draft remain unchanged when you save or cancel.

### Attach local images and audio

Expand Down
75 changes: 75 additions & 0 deletions src/protocols/acp/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2071,6 +2071,19 @@ pub(crate) fn component(
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_request(
{
let integration = Arc::clone(&state.integration);
async move |request: wire::ReplaceInjectSessionRequest, responder, cx| {
let integration = Arc::clone(&integration);
cx.spawn(async move {
responder.respond_with_result(integration.replace_inject(request).await)
})?;
Ok(())
}
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_request(
{
let state = Arc::clone(&state);
Expand Down Expand Up @@ -4060,6 +4073,68 @@ mod tests {
}
}

#[tokio::test]
async fn v2_router_advertises_and_routes_pending_injection_replacement() {
let root = tempfile::tempdir().unwrap();
let runtime = Runtime::new_with_provider_and_credentials(
root.path(),
"gpt-5.4",
crate::ProviderKind::OpenAiSubscription,
crate::credentials::CredentialStorage::Memory,
)
.unwrap();
let (client_transport, agent_transport) = agent_client_protocol::Channel::duplex();
let router = v2_router(runtime, SessionRegistry::new()).unwrap();
let server = tokio::spawn(async move { router.connect_to(agent_transport).await });
let client =
agent_client_protocol::Client
.v2()
.connect_with(client_transport, async move |cx| {
let initialized = cx
.send_request(wire::InitializeRequest::new(
wire::ProtocolVersion::V2,
wire::Implementation::new("replacement-test", "0"),
))
.block_task()
.await?;
let pending = initialized
.capabilities
.session
.expect("session capabilities")
.inject
.expect("injection capabilities")
.pending
.expect("pending injection capabilities");
assert_eq!(pending.replace, Some(true));

// A domain error, rather than method-not-found, proves that Kit's
// production router forwards replacement requests to AgentKit.
let error = cx
.send_request(wire::ReplaceInjectSessionRequest::new(
"missing-session",
"pending-message",
vec![wire::ContentBlock::Text(wire::TextContent::new(
"replacement",
))],
))
.block_task()
.await
.expect_err("replacement must reject an unknown session");
assert_eq!(
i32::from(error.code),
i32::from(wire::Error::resource_not_found(None).code)
);
assert_eq!(error.data, Some(json!({ "sessionId": "missing-session" })));
Ok(())
});
let result = timeout(Duration::from_secs(2), client).await;
server.abort();
let _ = server.await;
result
.expect("replacement client timed out")
.expect("replacement client failed");
}

#[test]
fn initialize_negotiates_v2_and_advertises_injection_and_authentication() {
let root = tempfile::tempdir().unwrap();
Expand Down
Loading
Loading