diff --git a/Cargo.toml b/Cargo.toml index 86160b45..f56ea16e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ unstable = [ "unstable_cancel_request", "unstable_elicitation", "unstable_logout", + "unstable_nes", "unstable_session_fork", "unstable_session_model", "unstable_session_resume", @@ -31,6 +32,7 @@ unstable_auth_methods = [] unstable_cancel_request = [] unstable_elicitation = [] unstable_logout = [] +unstable_nes = [] unstable_session_fork = [] unstable_session_model = [] unstable_session_resume = [] diff --git a/docs/protocol/draft/schema.mdx b/docs/protocol/draft/schema.mdx index d9a81347..8dc97309 100644 --- a/docs/protocol/draft/schema.mdx +++ b/docs/protocol/draft/schema.mdx @@ -62,6 +62,183 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte + +### document/didChange + +**UNSTABLE** + +Notification sent when a file is edited. + +#### DocumentDidChangeNotification + +Notification sent when a file is edited. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + TextDocumentContentChangeEvent[] + + } + required +> + The content changes. + +SessionId} + required +> + The session ID for this notification. + + + The URI of the changed document. + + + The new version number of the document. + + + +### document/didClose + +**UNSTABLE** + +Notification sent when a file is closed. + +#### DocumentDidCloseNotification + +Notification sent when a file is closed. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + +SessionId} + required +> + The session ID for this notification. + + + The URI of the closed document. + + + +### document/didFocus + +**UNSTABLE** + +Notification sent when a file becomes the active editor tab. + +#### DocumentDidFocusNotification + +Notification sent when a file becomes the active editor tab. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + +Position} required> + The current cursor position. + +SessionId} + required +> + The session ID for this notification. + + + The URI of the focused document. + + + The version number of the document. + +Range} required> + The portion of the file currently visible in the editor viewport. + + + +### document/didOpen + +**UNSTABLE** + +Notification sent when a file is opened in the editor. + +#### DocumentDidOpenNotification + +Notification sent when a file is opened in the editor. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + The language identifier of the document (e.g., "rust", "python"). + +SessionId} + required +> + The session ID for this notification. + + + The full text content of the document. + + + The URI of the opened document. + + + The version number of the document. + + + +### document/didSave + +**UNSTABLE** + +Notification sent when a file is saved. + +#### DocumentDidSaveNotification + +Notification sent when a file is saved. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + +SessionId} + required +> + The session ID for this notification. + + + The URI of the saved document. + + ### initialize Establishes the connection with a client and negotiates protocol capabilities. @@ -213,71 +390,53 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte - -### session/cancel - -Cancels ongoing operations for a session. - -This is a notification sent by the client to cancel an ongoing prompt turn. - -Upon receiving this notification, the Agent SHOULD: - -- Stop all language model requests as soon as possible -- Abort all tool call invocations in progress -- Send any pending `session/update` notifications -- Respond to the original `session/prompt` request with `StopReason::Cancelled` + +### nes/accept -See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) +**UNSTABLE** -#### CancelNotification +Notification sent when a suggestion is accepted. -Notification to cancel ongoing operations for a session. +#### NesAcceptNotification -See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) +Notification sent when a suggestion is accepted. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - + + The _meta property is reserved by ACP. -SessionId} required> - The ID of the session to cancel operations for. + + The ID of the accepted suggestion. + +SessionId} + required +> + The session ID for this notification. - -### session/close + +### nes/close **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. -Closes an active session and frees up any resources associated with it. - -This method is only available if the agent advertises the `session.close` capability. - -The agent must cancel any ongoing work (as if `session/cancel` was called) -and then free up any resources associated with the session. - -#### CloseSessionRequest - -**UNSTABLE** +Closes an active NES session and frees up any resources associated with it. -This capability is not part of the spec yet, and may be removed or changed at any point. +The agent must cancel any ongoing work and then free up any resources +associated with the NES session. -Request parameters for closing an active session. +#### NesCloseRequest -If supported, the agent **must** cancel any ongoing work related to the session -(treat it as if `session/cancel` was called) and then free up any resources -associated with the session. +Request to close an NES session. -Only available if the Agent supports the `session.close` capability. +The agent **must** cancel any ongoing work related to the NES session +and then free up any resources associated with the session. **Type:** Object @@ -292,16 +451,12 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte SessionId} required> - The ID of the session to close. + The ID of the NES session to close. -#### CloseSessionResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. +#### NesCloseResponse -Response from closing a session. +Response from closing an NES session. **Type:** Object @@ -316,103 +471,406 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte - -### session/fork + +### nes/reject **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. +Notification sent when a suggestion is rejected. -Forks an existing session to create a new independent session. +#### NesRejectNotification -This method is only available if the agent advertises the `session.fork` capability. +Notification sent when a suggestion is rejected. -The agent should create a new session with the same conversation context as the -original, allowing operations like generating summaries without affecting the -original session's history. +**Type:** Object -#### ForkSessionRequest +**Properties:** + + + The _meta property is reserved by ACP. + + + The ID of the rejected suggestion. + + + + NesRejectReason + + | null + + } +> + The reason for rejection. + +SessionId} + required +> + The session ID for this notification. + + + +### nes/start **UNSTABLE** This capability is not part of the spec yet, and may be removed or changed at any point. -Request parameters for forking an existing session. +Starts an NES session. -Creates a new session based on the context of an existing one, allowing -operations like generating summaries without affecting the original session's history. +#### NesStartRequest -Only available if the Agent supports the `session.fork` capability. +Request to start an NES session. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - + + The _meta property is reserved by ACP. - - The working directory for this session. + + + NesRepository + + | null + + } +> + Repository metadata, if the workspace is a git repository. -McpServer[]} > - List of MCP servers to connect to for this session. + + + WorkspaceFolder[] + + | null + + } +> + The workspace folders. -SessionId} required> - The ID of the session to fork. + + The root URI of the workspace. -#### ForkSessionResponse - -**UNSTABLE** - -This capability is not part of the spec yet, and may be removed or changed at any point. +#### NesStartResponse -Response from forking an existing session. +Response to `nes/start`. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. - -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - + + The _meta property is reserved by ACP. -SessionConfigOption[] | null} > - Initial session configuration options if supported by the Agent. +SessionId} + required +> + The session ID for the newly started NES session. -SessionModelState | null} > - **UNSTABLE** -This capability is not part of the spec yet, and may be removed or changed at any point. + +### nes/suggest -Initial model state if supported by the Agent +**UNSTABLE** - -SessionModeState | null} > - Initial mode state if supported by the Agent +This capability is not part of the spec yet, and may be removed or changed at any point. -See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes) +Requests a code suggestion. - -SessionId} required> - Unique identifier for the newly created forked session. - +#### NesSuggestRequest - -### session/list +Request for a code suggestion. -Lists existing sessions known to the agent. +**Type:** Object -This method is only available if the agent advertises the `sessionCapabilities.list` capability. +**Properties:** + + + The _meta property is reserved by ACP. + + + + NesSuggestContext + + | null + + } +> + Context for the suggestion, included based on agent capabilities. + +Position} required> + The current cursor position. + + + + Range + + | null + + } +> + The current text selection range, if any. + +SessionId} + required +> + The session ID for this request. + +NesTriggerKind} + required +> + What triggered this suggestion request. + + + The URI of the document to suggest for. + + + The version number of the document. + + +#### NesSuggestResponse + +Response to `nes/suggest`. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + +NesSuggestion[]} + required +> + The list of suggestions. + + + +### session/cancel + +Cancels ongoing operations for a session. + +This is a notification sent by the client to cancel an ongoing prompt turn. + +Upon receiving this notification, the Agent SHOULD: + +- Stop all language model requests as soon as possible +- Abort all tool call invocations in progress +- Send any pending `session/update` notifications +- Respond to the original `session/prompt` request with `StopReason::Cancelled` + +See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) + +#### CancelNotification + +Notification to cancel ongoing operations for a session. + +See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + +SessionId} required> + The ID of the session to cancel operations for. + + + +### session/close + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Closes an active session and frees up any resources associated with it. + +This method is only available if the agent advertises the `session.close` capability. + +The agent must cancel any ongoing work (as if `session/cancel` was called) +and then free up any resources associated with the session. + +#### CloseSessionRequest + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Request parameters for closing an active session. + +If supported, the agent **must** cancel any ongoing work related to the session +(treat it as if `session/cancel` was called) and then free up any resources +associated with the session. + +Only available if the Agent supports the `session.close` capability. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + +SessionId} required> + The ID of the session to close. + + +#### CloseSessionResponse + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Response from closing a session. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + + + +### session/fork + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Forks an existing session to create a new independent session. + +This method is only available if the agent advertises the `session.fork` capability. + +The agent should create a new session with the same conversation context as the +original, allowing operations like generating summaries without affecting the +original session's history. + +#### ForkSessionRequest + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Request parameters for forking an existing session. + +Creates a new session based on the context of an existing one, allowing +operations like generating summaries without affecting the original session's history. + +Only available if the Agent supports the `session.fork` capability. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + + + The working directory for this session. + +McpServer[]} > + List of MCP servers to connect to for this session. + +SessionId} required> + The ID of the session to fork. + + +#### ForkSessionResponse + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Response from forking an existing session. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + +SessionConfigOption[] | null} > + Initial session configuration options if supported by the Agent. + +SessionModelState | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Initial model state if supported by the Agent + + +SessionModeState | null} > + Initial mode state if supported by the Agent + +See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes) + + +SessionId} required> + Unique identifier for the newly created forked session. + + + +### session/list + +Lists existing sessions known to the agent. + +This method is only available if the agent advertises the `sessionCapabilities.list` capability. The agent should return metadata about sessions with optional filtering and pagination support. @@ -1799,6 +2257,22 @@ Authentication-related capabilities supported by the agent. - Default: `{"http":false,"sse":false}` + +NesCapabilities | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +NES (Next Edit Suggestions) capabilities supported by the agent. + + +PositionEncodingKind | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +The position encoding selected by the agent from the client's supported encodings. + PromptCapabilities} > Prompt capabilities supported by the agent. @@ -2312,13 +2786,82 @@ Determines which file operations the agent can request. - Default: `{"readTextFile":false,"writeTextFile":false}` - - Whether the Client support all `terminal/*` methods. +ClientNesCapabilities | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +NES (Next Edit Suggestions) capabilities supported by the client. + + +PositionEncodingKind[]} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +The position encodings supported by the client, in order of preference. + + + + Whether the Client support all `terminal/*` methods. - Default: `false` +## ClientNesCapabilities + +NES capabilities advertised by the client during initialization. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + + NesJumpCapabilities + + | null + + } +> + Whether the client supports the `jump` suggestion kind. + + + + NesRenameActionCapabilities + + | null + + } +> + Whether the client supports the `rename` suggestion kind. + + + + + NesSearchAndReplaceActionCapabilities + + + | null + + } +> + Whether the client supports the `searchAndReplace` suggestion kind. + + ## ConfigOptionUpdate Session configuration options have been updated. @@ -3660,127 +4203,1111 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte Human-readable name identifying this MCP server. -## ModelId +## ModelId + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +A unique identifier for a model. + +**Type:** `string` + +## ModelInfo + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Information about a selectable model. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach additional +metadata to their interactions. Implementations MUST NOT make assumptions about values at +these keys. + +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + + + Optional description of the model. + +ModelId} required> + Unique identifier for the model. + + + Human-readable name of the model. + + +## MultiSelectItems + +Items for a multi-select (array) property schema. + +**Type:** Union + + +Untitled multi-select items with plain string values. + + + + + "string" + [] + + } + required +> + Allowed enum values. + +ElicitationStringType} + required +> + Item type discriminator. Must be `"string"`. + + + + + + +Titled multi-select items with human-readable labels. + + + +EnumOption[]} + required +> + Titled enum options. + + + + + +## MultiSelectPropertySchema + +Schema for multi-select (array) properties in an elicitation form. + +**Type:** Object + +**Properties:** + +<>"string"[] | null} > + Default selected values. + + + Human-readable description. + +MultiSelectItems} required> + The items definition describing allowed values. + + + Maximum number of items to select. + + - Minimum: `0` + + + + Minimum number of items to select. + + - Minimum: `0` + + + + Optional title for the property. + + +## NesCapabilities + +NES capabilities advertised by the agent during initialization. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. + + + + NesContextCapabilities + + | null + + } +> + Context the agent wants attached to each suggestion request. + + + + NesEventCapabilities + + | null + + } +> + Events the agent wants to receive. + + +## NesContextCapabilities + +Context capabilities the agent wants attached to each suggestion request. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + + NesDiagnosticsCapabilities + + | null + + } +> + Whether the agent wants diagnostics context. + + + + NesEditHistoryCapabilities + + | null + + } +> + Whether the agent wants edit history context. + + + + NesOpenFilesCapabilities + + | null + + } +> + Whether the agent wants open files context. + + + + NesRecentFilesCapabilities + + | null + + } +> + Whether the agent wants recent files context. + + + + + NesRelatedSnippetsCapabilities + + + | null + + } +> + Whether the agent wants related snippets context. + + + + NesUserActionsCapabilities + + | null + + } +> + Whether the agent wants user actions context. + + +## NesDiagnostic + +A diagnostic (error, warning, etc.). + +**Type:** Object + +**Properties:** + + + The diagnostic message. + +Range} required> + The range of the diagnostic. + +NesDiagnosticSeverity} + required +> + The severity of the diagnostic. + + + The URI of the file containing the diagnostic. + + +## NesDiagnosticSeverity + +Severity of a diagnostic. + +**Type:** Union + + + An error. + + + + A warning. + + + + An informational message. + + + + A hint. + + +## NesDiagnosticsCapabilities + +Capabilities for diagnostics context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesDocumentDidChangeCapabilities + +Capabilities for `document/didChange` events. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + +TextDocumentSyncKind} + required +> + The sync kind the agent wants: `"full"` or `"incremental"`. + + +## NesDocumentDidCloseCapabilities + +Marker for `document/didClose` capability support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesDocumentDidFocusCapabilities + +Marker for `document/didFocus` capability support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesDocumentDidOpenCapabilities + +Marker for `document/didOpen` capability support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesDocumentDidSaveCapabilities + +Marker for `document/didSave` capability support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesDocumentEventCapabilities + +Document event capabilities the agent wants to receive. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + + + NesDocumentDidChangeCapabilities + + + | null + + } +> + Whether the agent wants `document/didChange` events, and the sync kind. + + + + + NesDocumentDidCloseCapabilities + + + | null + + } +> + Whether the agent wants `document/didClose` events. + + + + + NesDocumentDidFocusCapabilities + + + | null + + } +> + Whether the agent wants `document/didFocus` events. + + + + + NesDocumentDidOpenCapabilities + + + | null + + } +> + Whether the agent wants `document/didOpen` events. + + + + + NesDocumentDidSaveCapabilities + + + | null + + } +> + Whether the agent wants `document/didSave` events. + + +## NesEditHistoryCapabilities + +Capabilities for edit history context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + Maximum number of edit history entries the agent can use. + + - Minimum: `0` + + + +## NesEditHistoryEntry + +An entry in the edit history. + +**Type:** Object + +**Properties:** + + + A diff representing the edit. + + + The URI of the edited file. + + +## NesEditSuggestion + +A text edit suggestion. + +**Type:** Object + +**Properties:** + + + + Position + + | null + + } +> + Optional suggested cursor position after applying edits. + +NesTextEdit[]} + required +> + The text edits to apply. + + + Unique identifier for accept/reject tracking. + + + The URI of the file to edit. + + +## NesEventCapabilities + +Event capabilities the agent can consume. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + + NesDocumentEventCapabilities + + | null + + } +> + Document event capabilities. + + +## NesExcerpt + +A code excerpt from a file. + +**Type:** Object + +**Properties:** + + + The end line of the excerpt (zero-based). + + - Minimum: `0` + + + + The start line of the excerpt (zero-based). + + - Minimum: `0` + + + + The text content of the excerpt. + + +## NesJumpCapabilities + +Marker for jump suggestion support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesJumpSuggestion + +A jump-to-location suggestion. + +**Type:** Object + +**Properties:** + + + Unique identifier for accept/reject tracking. + +Position} required> + The target position within the file. + + + The file to navigate to. + + +## NesOpenFile + +An open file in the editor. + +**Type:** Object + +**Properties:** + + + The language identifier. + + + Timestamp in milliseconds since epoch of when the file was last focused. + + - Minimum: `0` + + + + The URI of the file. + +Range | null} > + The visible range in the editor, if any. + + +## NesOpenFilesCapabilities + +Capabilities for open files context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesRecentFile + +A recently accessed file. + +**Type:** Object + +**Properties:** + + + The language identifier. + + + The full text content of the file. + + + The URI of the file. + + +## NesRecentFilesCapabilities + +Capabilities for recent files context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + Maximum number of recent files the agent can use. + + - Minimum: `0` + + + +## NesRejectReason + +The reason a suggestion was rejected. + +**Type:** Union + + + The user explicitly dismissed the suggestion. + + + + The suggestion was shown but the user continued editing without interacting. + + + + The suggestion was superseded by a newer suggestion. + + + + The request was cancelled before the agent returned a response. + + +## NesRelatedSnippet + +A related code snippet from a file. + +**Type:** Object + +**Properties:** + +NesExcerpt[]} + required +> + The code excerpts. + + + The URI of the file containing the snippets. + + +## NesRelatedSnippetsCapabilities + +Capabilities for related snippets context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesRenameActionCapabilities + +Marker for rename action support. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + +## NesRenameActionSuggestion + +A rename symbol suggestion. + +**Type:** Object + +**Properties:** + + + Unique identifier for accept/reject tracking. + + + The new name for the symbol. + +Position} required> + The position of the symbol to rename. + + + The file URI containing the symbol. + + +## NesRepository + +Repository metadata for an NES session. + +**Type:** Object + +**Properties:** -**UNSTABLE** + + The repository name. + + + The repository owner. + + + The remote URL of the repository. + -This capability is not part of the spec yet, and may be removed or changed at any point. +## NesSearchAndReplaceActionCapabilities -A unique identifier for a model. +Marker for search and replace action support. -**Type:** `string` +**Type:** Object -## ModelInfo +**Properties:** -**UNSTABLE** + + The _meta property is reserved by ACP. + -This capability is not part of the spec yet, and may be removed or changed at any point. +## NesSearchAndReplaceActionSuggestion -Information about a selectable model. +A search-and-replace suggestion. **Type:** Object **Properties:** - - The _meta property is reserved by ACP to allow clients and agents to attach additional -metadata to their interactions. Implementations MUST NOT make assumptions about values at -these keys. + + Unique identifier for accept/reject tracking. + + + Whether `search` is a regular expression. Defaults to `false`. + + + The replacement text. + + + The text or pattern to find. + + + The file URI to search within. + -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) +## NesSuggestContext + +Context attached to a suggestion request. + +**Type:** Object +**Properties:** + + + The _meta property is reserved by ACP. - - Optional description of the model. + + + NesDiagnostic[] + + | null + + } +> + Current diagnostics (errors, warnings). -ModelId} required> - Unique identifier for the model. + + + NesEditHistoryEntry[] + + | null + + } +> + Recent edit history. - - Human-readable name of the model. + + + NesOpenFile[] + + | null + + } +> + Currently open files in the editor. + + + + NesRecentFile[] + + | null + + } +> + Recently accessed files. + + + + NesRelatedSnippet[] + + | null + + } +> + Related code snippets. + + + + NesUserAction[] + + | null + + } +> + Recent user actions (typing, navigation, etc.). -## MultiSelectItems +## NesSuggestion -Items for a multi-select (array) property schema. +A suggestion returned by the agent. **Type:** Union - -Untitled multi-select items with plain string values. + +A text edit suggestion. - "string" - [] + + Position + + | null } - required > - Allowed enum values. + Optional suggested cursor position after applying edits. ElicitationStringType} + name="edits" + type={NesTextEdit[]} required > - Item type discriminator. Must be `"string"`. + The text edits to apply. + + + Unique identifier for accept/reject tracking. + + + The discriminator value. Must be `"edit"`. + + + The URI of the file to edit. - -Titled multi-select items with human-readable labels. + +A jump-to-location suggestion. -EnumOption[]} - required -> - Titled enum options. + + Unique identifier for accept/reject tracking. + + + The discriminator value. Must be `"jump"`. + +Position} required> + The target position within the file. + + + The file to navigate to. -## MultiSelectPropertySchema + +A rename symbol suggestion. -Schema for multi-select (array) properties in an elicitation form. + + + + Unique identifier for accept/reject tracking. + + + The discriminator value. Must be `"rename"`. + + + The new name for the symbol. + +Position} required> + The position of the symbol to rename. + + + The file URI containing the symbol. + + + + + + +A search-and-replace suggestion. + + + + + Unique identifier for accept/reject tracking. + + + Whether `search` is a regular expression. Defaults to `false`. + + + The discriminator value. Must be `"searchAndReplace"`. + + + The replacement text. + + + The text or pattern to find. + + + The file URI to search within. + + + + + +## NesTextEdit + +A text edit within a suggestion. **Type:** Object **Properties:** -<>"string"[] | null} > - Default selected values. + + The replacement text. - - Human-readable description. +Range} required> + The range to replace. -MultiSelectItems} required> - The items definition describing allowed values. + +## NesTriggerKind + +What triggered the suggestion request. + +**Type:** Union + + + Triggered by user typing or cursor movement. - - Maximum number of items to select. - - Minimum: `0` + + Triggered by a diagnostic appearing at or near the cursor. + + + Triggered by an explicit user action (keyboard shortcut). - - Minimum number of items to select. + +## NesUserAction + +A user action (typing, cursor movement, etc.). + +**Type:** Object + +**Properties:** + + + The kind of action (e.g., "insertChar", "cursorMovement"). + +Position} required> + The position where the action occurred. + + + Timestamp in milliseconds since epoch. - Minimum: `0` - - Optional title for the property. + + The URI of the file where the action occurred. + + +## NesUserActionsCapabilities + +Capabilities for user actions context. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP. + + + Maximum number of user actions the agent can use. + + - Minimum: `0` + ## NumberPropertySchema @@ -3967,6 +5494,49 @@ See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent The task has been successfully completed. +## Position + +A zero-based position in a text document. + +The meaning of `character` depends on the negotiated position encoding. + +**Type:** Object + +**Properties:** + + + Zero-based character offset (encoding-dependent). + + - Minimum: `0` + + + + Zero-based line number. + + - Minimum: `0` + + + +## PositionEncodingKind + +The encoding used for character offsets in positions. + +Follows the same conventions as LSP 3.17. The default is UTF-16. + +**Type:** Union + + + Character offsets count UTF-16 code units. This is the default. + + + + Character offsets count Unicode code points. + + + + Character offsets count UTF-8 code units (bytes). + + ## PromptCapabilities Prompt capabilities supported by the agent in `session/prompt` requests. @@ -4030,6 +5600,21 @@ Non-breaking changes should be introduced via capabilities. | Minimum | `0` | | Maximum | `65535` | +## Range + +A range in a text document, expressed as start and end positions. + +**Type:** Object + +**Properties:** + +Position} required> + The end position (exclusive). + +Position} required> + The start position (inclusive). + + ## RequestId JSON RPC Request Id @@ -5199,6 +6784,49 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte +## TextDocumentContentChangeEvent + +A content change event for a document. + +When `range` is `None`, `text` is the full content of the document. +When `range` is `Some`, `text` replaces the given range. + +**Type:** Object + +**Properties:** + + + + Range + + | null + + } +> + The range of the document that changed. If `None`, the entire content is + replaced. + + + The new text for the range, or the full document content if `range` is `None`. + + +## TextDocumentSyncKind + +How the agent wants document changes delivered. + +**Type:** Union + + + Client sends the entire file content on each change. + + + + Client sends only the changed ranges. + + ## TextResourceContents Text-based resource contents. @@ -5668,3 +7296,18 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte - Minimum: `0` + +## WorkspaceFolder + +A workspace folder. + +**Type:** Object + +**Properties:** + + + The display name of the folder. + + + The URI of the folder. + diff --git a/docs/rfds/next-edit-suggestions.mdx b/docs/rfds/next-edit-suggestions.mdx index ece81a25..8c5eb273 100644 --- a/docs/rfds/next-edit-suggestions.mdx +++ b/docs/rfds/next-edit-suggestions.mdx @@ -29,11 +29,11 @@ The client inspects these declarations and provides only what was requested, min ### Capability advertisement -During `initialize`, the agent includes a `nes` field in its capabilities: +During `initialize`, the agent includes a `nes` field in `agentCapabilities`: ```json { - "capabilities": { + "agentCapabilities": { "nes": { "events": { "document": { @@ -69,22 +69,21 @@ All fields under `events` and `context` are optional — an agent advertises onl #### Client capabilities -The **client** advertises its own NES-related capabilities in the `initialize` request. Currently, the client can declare which well-known IDE actions it supports by listing their IDs. The agent reads these and may later include `"action"` kind suggestions that reference them. +The **client** advertises its own NES-related capabilities in the `initialize` request. The client declares which suggestion kinds it supports beyond the basic `edit` kind. Agents should only suggest kinds that the client has advertised. ```json { - "capabilities": { + "clientCapabilities": { "nes": { - "ideActions": { - "rename": {}, - "searchAndReplace": {} - } + "jump": {}, + "rename": {}, + "searchAndReplace": {} } } } ``` -Each entry in `ideActions` is the ID of a well-known action (see [Well-known IDE actions](#well-known-ide-actions) below). Agents should only suggest actions that the client has advertised. +Each entry corresponds to a suggestion kind (see [Suggestion kinds](#suggestion-kinds) below). If a kind is absent, the agent must not produce suggestions of that kind. ### Session lifecycle @@ -106,16 +105,14 @@ Three encoding kinds are supported: - `"utf-32"` — character offsets count Unicode code points. - `"utf-8"` — character offsets count UTF-8 code units (bytes). -**Negotiation:** The client declares which encodings it supports in the `initialize` request via `general.positionEncodings`, listed in order of preference. The agent selects one from the client's list and declares it in its `initialize` response as `positionEncoding`. If the client omits `positionEncodings`, or the agent omits `positionEncoding` in its response, both sides default to `"utf-16"`. +**Negotiation:** The client may declare the position encodings it supports in the `initialize` request via `clientCapabilities.positionEncodings`, listed in order of preference. The agent selects one from the client's list and declares it in its `initialize` response as `agentCapabilities.positionEncoding`. If the client omits `positionEncodings`, or the agent omits `positionEncoding` in its response, both sides default to `"utf-16"`. Client `initialize` request (excerpt): ```json { - "capabilities": { - "general": { - "positionEncodings": ["utf-32", "utf-16"] - } + "clientCapabilities": { + "positionEncodings": ["utf-32", "utf-16"] } } ``` @@ -124,7 +121,7 @@ Agent `initialize` response (excerpt): ```json { - "capabilities": { + "agentCapabilities": { "nes": { ... }, "positionEncoding": "utf-32" } @@ -135,7 +132,7 @@ The negotiated encoding applies to all `Position` and `Range` values exchanged w ### Events -Events are fire-and-forget notifications from client to agent. The client sends them only if the corresponding key is present in the agent's advertised `events` capability (e.g. `nes.events.document` for NES). +Events are fire-and-forget notifications from client to agent. Every event is scoped to the NES session identified by the `sessionId` returned from `nes/start`. The client sends them only if the corresponding key is present in the agent's advertised `events` capability (e.g. `nes.events.document` for NES). #### `document/didOpen` @@ -146,6 +143,7 @@ Sent when a file is opened in the editor. "jsonrpc": "2.0", "method": "document/didOpen", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs", "languageId": "rust", "version": 1, @@ -168,6 +166,7 @@ Sent when a file is edited. Supports two sync modes declared by the agent: "jsonrpc": "2.0", "method": "document/didChange", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs", "version": 2, "contentChanges": [ @@ -190,6 +189,7 @@ Sent when a file is edited. Supports two sync modes declared by the agent: "jsonrpc": "2.0", "method": "document/didChange", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs", "version": 2, "contentChanges": [ @@ -210,6 +210,7 @@ Sent when a file is closed. "jsonrpc": "2.0", "method": "document/didClose", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs" } } @@ -224,6 +225,7 @@ Sent when a file is saved. "jsonrpc": "2.0", "method": "document/didSave", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs" } } @@ -238,6 +240,7 @@ Sent when a file becomes the active editor tab. Unlike `document/didOpen` (which "jsonrpc": "2.0", "method": "document/didFocus", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs", "version": 2, "position": { "line": 5, "character": 12 }, @@ -261,6 +264,7 @@ The client requests a suggestion by calling `nes/suggest`. Context fields are in "id": 42, "method": "nes/suggest", "params": { + "sessionId": "session_abc123", "uri": "file:///path/to/file.rs", "version": 2, "position": { "line": 5, "character": 12 }, @@ -299,15 +303,13 @@ The client requests a suggestion by calling `nes/suggest`. Context fields are in { "action": "insertChar", "uri": "file:///path/to/file.rs", - "line": 5, - "offset": 12, + "position": { "line": 5, "character": 12 }, "timestampMs": 1719400000000 }, { "action": "cursorMovement", "uri": "file:///path/to/file.rs", - "line": 10, - "offset": 0, + "position": { "line": 10, "character": 0 }, "timestampMs": 1719400001200 } ], @@ -354,7 +356,7 @@ The client requests a suggestion by calling `nes/suggest`. Context fields are in ### Suggestion response -A suggestion is one of three kinds: an **edit** (text changes), a **jump** (navigate to a different file), or an **action** (trigger an IDE action). +A suggestion is one of several kinds, each identified by the `kind` field. The `edit` kind is always supported; other kinds (`jump`, `rename`, `searchAndReplace`) require the client to advertise support in its capabilities. **Edit suggestion:** @@ -403,7 +405,7 @@ A suggestion is one of three kinds: an **edit** (text changes), a **jump** (navi } ``` -**Action suggestion:** +**Rename suggestion:** ```json { @@ -413,33 +415,47 @@ A suggestion is one of three kinds: an **edit** (text changes), a **jump** (navi "suggestions": [ { "id": "sugg_003", - "kind": "action", - "actionId": "rename", - "arguments": { - "uri": "file:///path/to/file.rs", - "position": { "line": 5, "character": 10 }, - "newName": "calculateTotal" - } + "kind": "rename", + "uri": "file:///path/to/file.rs", + "position": { "line": 5, "character": 10 }, + "newName": "calculateTotal" } ] } } ``` -Action suggestions reference an IDE action that the client previously advertised in its capabilities: +**Search-and-replace suggestion:** -- `actionId` — matches an `id` from the client's advertised `ideActions`. -- `arguments` — matches the parameter schema declared by the client for that action. +```json +{ + "jsonrpc": "2.0", + "id": 42, + "result": { + "suggestions": [ + { + "id": "sugg_004", + "kind": "searchAndReplace", + "uri": "file:///path/to/file.rs", + "search": "oldFunction", + "replace": "newFunction", + "isRegex": false + } + ] + } +} +``` -A response may contain a mix of edit, jump, and action suggestions. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). +A response may contain a mix of suggestion kinds. The client decides how to present them (e.g. inline ghost text for edits, a navigation hint for jumps). Agents must only include suggestion kinds that the client has advertised in its capabilities (except `edit`, which is always supported). Each suggestion contains: - `id` — unique identifier for accept/reject tracking. -- `kind` — `"edit"`, `"jump"`, or `"action"`. +- `kind` — the suggestion kind (see [Suggestion kinds](#suggestion-kinds) below). Edit suggestions additionally contain: +- `uri` — the file to edit. - `edits` — one or more text edits to apply. - `cursorPosition` — optional suggested cursor position after applying edits. @@ -448,10 +464,18 @@ Jump suggestions additionally contain: - `uri` — the file to navigate to. - `position` — the target position within that file. -Action suggestions additionally contain: +Rename suggestions additionally contain: + +- `uri` — the file URI containing the symbol. +- `position` — the position of the symbol to rename. +- `newName` — the new name for the symbol. -- `actionId` — the IDE action to perform (must match a client-advertised action `id`). -- `arguments` — action parameters matching the schema from the client's capability. +Search-and-replace suggestions additionally contain: + +- `uri` — the file URI to search within. Can be a folder, then operation is performed in all the files in this folder. +- `search` — the text or pattern to find. +- `replace` — the replacement text. +- `isRegex` (`boolean`, optional) — whether `search` is a regular expression. Defaults to `false`. ### Accept / Reject @@ -460,6 +484,7 @@ Action suggestions additionally contain: "jsonrpc": "2.0", "method": "nes/accept", "params": { + "sessionId": "session_abc123", "id": "sugg_001" } } @@ -470,6 +495,7 @@ Action suggestions additionally contain: "jsonrpc": "2.0", "method": "nes/reject", "params": { + "sessionId": "session_abc123", "id": "sugg_001", "reason": "rejected" } @@ -520,11 +546,13 @@ Response: "jsonrpc": "2.0", "id": 1, "result": { - "sessionId": "nes_abc123" + "sessionId": "session_abc123" } } ``` +The returned `sessionId` scopes all subsequent NES events, requests, and notifications for that session. + #### Error handling The agent may reject `nes/start` with an error. In particular, agents that require authentication may return an `auth_required` error: @@ -545,28 +573,16 @@ The agent may reject `nes/start` with an error. In particular, agents that requi Clients **must** be prepared to handle `auth_required` errors on `nes/start`. The recommended behavior is to prompt the user to authenticate (e.g. sign in or provide credentials) and retry the `nes/start` call after authentication succeeds. Clients should not silently ignore this error or assume NES is unavailable — the agent may be fully functional once the user authenticates. -### Well-known IDE actions +### Suggestion kinds -The following actions are well-known and have standardized parameter schemas. Clients that support these actions should use the IDs and parameter shapes defined here. +The `kind` field in each suggestion identifies its type. The following kinds are defined: -**`rename`** — Rename a symbol across the workspace. - -Parameters: - -- `uri` (`string`) — the file URI containing the symbol. -- `position` (`Position`) — the position of the symbol to rename. -- `newName` (`string`) — the new name for the symbol. - -**`searchAndReplace`** — Search and replace text within a file. - -Parameters: - -- `uri` (`string`) — the file URI to search within. -- `search` (`string`) — the text or pattern to find. -- `replace` (`string`) — the replacement text. -- `isRegex` (`boolean`, optional) — whether `search` is a regular expression. Defaults to `false`. +- **`edit`** — A text edit suggestion. Always supported; does not require a client capability. +- **`jump`** — Navigate to a different file/position. Requires `jump` in client capabilities. +- **`rename`** — Rename a symbol across the workspace. Requires `rename` in client capabilities. +- **`searchAndReplace`** — Search and replace text within a file/folder. Requires `searchAndReplace` in client capabilities. -Additional well-known actions may be added to the protocol in the future. Agents should only suggest actions whose `id` matches an entry the client has advertised. +Additional suggestion kinds may be added to the protocol in the future. Agents must only produce suggestions whose `kind` the client has advertised (except `edit`, which is always supported). ### Config options diff --git a/schema/meta.unstable.json b/schema/meta.unstable.json index 6198214a..167bba2f 100644 --- a/schema/meta.unstable.json +++ b/schema/meta.unstable.json @@ -1,8 +1,18 @@ { "agentMethods": { "authenticate": "authenticate", + "document_did_change": "document/didChange", + "document_did_close": "document/didClose", + "document_did_focus": "document/didFocus", + "document_did_open": "document/didOpen", + "document_did_save": "document/didSave", "initialize": "initialize", "logout": "logout", + "nes_accept": "nes/accept", + "nes_close": "nes/close", + "nes_reject": "nes/reject", + "nes_start": "nes/start", + "nes_suggest": "nes/suggest", "session_cancel": "session/cancel", "session_close": "session/close", "session_fork": "session/fork", diff --git a/schema/schema.unstable.json b/schema/schema.unstable.json index 081e1a1f..49088669 100644 --- a/schema/schema.unstable.json +++ b/schema/schema.unstable.json @@ -56,6 +56,28 @@ }, "description": "MCP capabilities supported by the agent." }, + "nes": { + "anyOf": [ + { + "$ref": "#/$defs/NesCapabilities" + }, + { + "type": "null" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNES (Next Edit Suggestions) capabilities supported by the agent." + }, + "positionEncoding": { + "anyOf": [ + { + "$ref": "#/$defs/PositionEncodingKind" + }, + { + "type": "null" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe position encoding selected by the agent from the client's supported encodings." + }, "promptCapabilities": { "allOf": [ { @@ -357,6 +379,30 @@ ], "title": "SetSessionModelResponse" }, + { + "allOf": [ + { + "$ref": "#/$defs/NesStartResponse" + } + ], + "title": "NesStartResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesSuggestResponse" + } + ], + "title": "NesSuggestResponse" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesCloseResponse" + } + ], + "title": "NesCloseResponse" + }, { "allOf": [ { @@ -850,6 +896,24 @@ }, "description": "File system capabilities supported by the client.\nDetermines which file operations the agent can request." }, + "nes": { + "anyOf": [ + { + "$ref": "#/$defs/ClientNesCapabilities" + }, + { + "type": "null" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNES (Next Edit Suggestions) capabilities supported by the client." + }, + "positionEncodings": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe position encodings supported by the client, in order of preference.", + "items": { + "$ref": "#/$defs/PositionEncodingKind" + }, + "type": "array" + }, "terminal": { "default": false, "description": "Whether the Client support all `terminal/*` methods.", @@ -858,6 +922,50 @@ }, "type": "object" }, + "ClientNesCapabilities": { + "description": "NES capabilities advertised by the client during initialization.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "jump": { + "anyOf": [ + { + "$ref": "#/$defs/NesJumpCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports the `jump` suggestion kind." + }, + "rename": { + "anyOf": [ + { + "$ref": "#/$defs/NesRenameActionCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports the `rename` suggestion kind." + }, + "searchAndReplace": { + "anyOf": [ + { + "$ref": "#/$defs/NesSearchAndReplaceActionCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports the `searchAndReplace` suggestion kind." + } + }, + "type": "object" + }, "ClientNotification": { "properties": { "method": { @@ -876,6 +984,69 @@ "description": "Cancels ongoing operations for a session.\n\nThis is a notification sent by the client to cancel an ongoing prompt turn.\n\nUpon receiving this notification, the Agent SHOULD:\n- Stop all language model requests as soon as possible\n- Abort all tool call invocations in progress\n- Send any pending `session/update` notifications\n- Respond to the original `session/prompt` request with `StopReason::Cancelled`\n\nSee protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)", "title": "CancelNotification" }, + { + "allOf": [ + { + "$ref": "#/$defs/DocumentDidOpenNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a file is opened in the editor.", + "title": "DocumentDidOpenNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/DocumentDidChangeNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a file is edited.", + "title": "DocumentDidChangeNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/DocumentDidCloseNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a file is closed.", + "title": "DocumentDidCloseNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/DocumentDidSaveNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a file is saved.", + "title": "DocumentDidSaveNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/DocumentDidFocusNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a file becomes the active editor tab.", + "title": "DocumentDidFocusNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesAcceptNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a suggestion is accepted.", + "title": "NesAcceptNotification" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesRejectNotification" + } + ], + "description": "**UNSTABLE**\n\nNotification sent when a suggestion is rejected.", + "title": "NesRejectNotification" + }, { "allOf": [ { @@ -1027,6 +1198,33 @@ "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nSelect a model for a given session.", "title": "SetSessionModelRequest" }, + { + "allOf": [ + { + "$ref": "#/$defs/NesStartRequest" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nStarts an NES session.", + "title": "NesStartRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesSuggestRequest" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequests a code suggestion.", + "title": "NesSuggestRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/NesCloseRequest" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCloses an active NES session and frees up any resources associated with it.\n\nThe agent must cancel any ongoing work and then free up any resources\nassociated with the NES session.", + "title": "NesCloseRequest" + }, { "allOf": [ { @@ -1476,104 +1674,280 @@ "required": ["path", "newText"], "type": "object" }, - "ElicitationAcceptAction": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe user accepted the elicitation and provided content.", + "DocumentDidChangeNotification": { + "description": "Notification sent when a file is edited.", "properties": { - "content": { - "additionalProperties": { - "$ref": "#/$defs/ElicitationContentValue" - }, - "description": "The user-provided content, if any, as an object matching the requested schema.", + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", "type": ["object", "null"] + }, + "contentChanges": { + "description": "The content changes.", + "items": { + "$ref": "#/$defs/TextDocumentContentChangeEvent" + }, + "type": "array" + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + }, + "uri": { + "description": "The URI of the changed document.", + "type": "string" + }, + "version": { + "description": "The new version number of the document.", + "format": "int64", + "type": "integer" } }, - "type": "object" + "required": ["sessionId", "uri", "version", "contentChanges"], + "type": "object", + "x-method": "document/didChange", + "x-side": "agent" }, - "ElicitationAction": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe user's action in response to an elicitation.", - "discriminator": { - "propertyName": "action" - }, - "oneOf": [ - { + "DocumentDidCloseNotification": { + "description": "Notification sent when a file is closed.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "sessionId": { "allOf": [ { - "$ref": "#/$defs/ElicitationAcceptAction" + "$ref": "#/$defs/SessionId" } ], - "description": "The user accepted and provided content.", - "properties": { - "action": { - "const": "accept", - "type": "string" - } - }, - "required": ["action"], - "type": "object" + "description": "The session ID for this notification." }, - { - "description": "The user declined the elicitation.", - "properties": { - "action": { - "const": "decline", - "type": "string" - } - }, - "required": ["action"], - "type": "object" - }, - { - "description": "The elicitation was cancelled.", - "properties": { - "action": { - "const": "cancel", - "type": "string" - } - }, - "required": ["action"], - "type": "object" + "uri": { + "description": "The URI of the closed document.", + "type": "string" } - ] + }, + "required": ["sessionId", "uri"], + "type": "object", + "x-method": "document/didClose", + "x-side": "agent" }, - "ElicitationCapabilities": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nElicitation capabilities supported by the client.", + "DocumentDidFocusNotification": { + "description": "Notification sent when a file becomes the active editor tab.", "properties": { "_meta": { "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "description": "The _meta property is reserved by ACP.", "type": ["object", "null"] }, - "form": { - "anyOf": [ - { - "$ref": "#/$defs/ElicitationFormCapabilities" - }, + "position": { + "allOf": [ { - "type": "null" + "$ref": "#/$defs/Position" } ], - "description": "Whether the client supports form-based elicitation." + "description": "The current cursor position." }, - "url": { - "anyOf": [ + "sessionId": { + "allOf": [ { - "$ref": "#/$defs/ElicitationUrlCapabilities" - }, + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + }, + "uri": { + "description": "The URI of the focused document.", + "type": "string" + }, + "version": { + "description": "The version number of the document.", + "format": "int64", + "type": "integer" + }, + "visibleRange": { + "allOf": [ { - "type": "null" + "$ref": "#/$defs/Range" } ], - "description": "Whether the client supports URL-based elicitation." + "description": "The portion of the file currently visible in the editor viewport." } }, - "type": "object" + "required": ["sessionId", "uri", "version", "position", "visibleRange"], + "type": "object", + "x-method": "document/didFocus", + "x-side": "agent" }, - "ElicitationCompleteNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification sent by the agent when a URL-based elicitation is complete.", + "DocumentDidOpenNotification": { + "description": "Notification sent when a file is opened in the editor.", "properties": { "_meta": { "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "languageId": { + "description": "The language identifier of the document (e.g., \"rust\", \"python\").", + "type": "string" + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + }, + "text": { + "description": "The full text content of the document.", + "type": "string" + }, + "uri": { + "description": "The URI of the opened document.", + "type": "string" + }, + "version": { + "description": "The version number of the document.", + "format": "int64", + "type": "integer" + } + }, + "required": ["sessionId", "uri", "languageId", "version", "text"], + "type": "object", + "x-method": "document/didOpen", + "x-side": "agent" + }, + "DocumentDidSaveNotification": { + "description": "Notification sent when a file is saved.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + }, + "uri": { + "description": "The URI of the saved document.", + "type": "string" + } + }, + "required": ["sessionId", "uri"], + "type": "object", + "x-method": "document/didSave", + "x-side": "agent" + }, + "ElicitationAcceptAction": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe user accepted the elicitation and provided content.", + "properties": { + "content": { + "additionalProperties": { + "$ref": "#/$defs/ElicitationContentValue" + }, + "description": "The user-provided content, if any, as an object matching the requested schema.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "ElicitationAction": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe user's action in response to an elicitation.", + "discriminator": { + "propertyName": "action" + }, + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationAcceptAction" + } + ], + "description": "The user accepted and provided content.", + "properties": { + "action": { + "const": "accept", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + }, + { + "description": "The user declined the elicitation.", + "properties": { + "action": { + "const": "decline", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + }, + { + "description": "The elicitation was cancelled.", + "properties": { + "action": { + "const": "cancel", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + } + ] + }, + "ElicitationCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nElicitation capabilities supported by the client.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "form": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitationFormCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports form-based elicitation." + }, + "url": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitationUrlCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports URL-based elicitation." + } + }, + "type": "object" + }, + "ElicitationCompleteNotification": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification sent by the agent when a URL-based elicitation is complete.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", "type": ["object", "null"] }, "elicitationId": { @@ -2719,154 +3093,1285 @@ "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", "type": ["object", "null"] }, - "headers": { - "description": "HTTP headers to set when making requests to the MCP server.", - "items": { - "$ref": "#/$defs/HttpHeader" - }, - "type": "array" + "headers": { + "description": "HTTP headers to set when making requests to the MCP server.", + "items": { + "$ref": "#/$defs/HttpHeader" + }, + "type": "array" + }, + "name": { + "description": "Human-readable name identifying this MCP server.", + "type": "string" + }, + "url": { + "description": "URL to the MCP server.", + "type": "string" + } + }, + "required": ["name", "url", "headers"], + "type": "object" + }, + "McpServerStdio": { + "description": "Stdio transport configuration for MCP.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "args": { + "description": "Command-line arguments to pass to the MCP server.", + "items": { + "type": "string" + }, + "type": "array" + }, + "command": { + "description": "Path to the MCP server executable.", + "type": "string" + }, + "env": { + "description": "Environment variables to set when launching the MCP server.", + "items": { + "$ref": "#/$defs/EnvVariable" + }, + "type": "array" + }, + "name": { + "description": "Human-readable name identifying this MCP server.", + "type": "string" + } + }, + "required": ["name", "command", "args", "env"], + "type": "object" + }, + "ModelId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for a model.", + "type": "string" + }, + "ModelInfo": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nInformation about a selectable model.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "description": { + "description": "Optional description of the model.", + "type": ["string", "null"] + }, + "modelId": { + "allOf": [ + { + "$ref": "#/$defs/ModelId" + } + ], + "description": "Unique identifier for the model." + }, + "name": { + "description": "Human-readable name of the model.", + "type": "string" + } + }, + "required": ["modelId", "name"], + "type": "object" + }, + "MultiSelectItems": { + "anyOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/UntitledMultiSelectItems" + } + ], + "description": "Untitled multi-select items with plain string values.", + "title": "Untitled" + }, + { + "allOf": [ + { + "$ref": "#/$defs/TitledMultiSelectItems" + } + ], + "description": "Titled multi-select items with human-readable labels.", + "title": "Titled" + } + ], + "description": "Items for a multi-select (array) property schema." + }, + "MultiSelectPropertySchema": { + "description": "Schema for multi-select (array) properties in an elicitation form.", + "properties": { + "default": { + "description": "Default selected values.", + "items": { + "type": "string" + }, + "type": ["array", "null"] + }, + "description": { + "description": "Human-readable description.", + "type": ["string", "null"] + }, + "items": { + "allOf": [ + { + "$ref": "#/$defs/MultiSelectItems" + } + ], + "description": "The items definition describing allowed values." + }, + "maxItems": { + "description": "Maximum number of items to select.", + "format": "uint64", + "minimum": 0, + "type": ["integer", "null"] + }, + "minItems": { + "description": "Minimum number of items to select.", + "format": "uint64", + "minimum": 0, + "type": ["integer", "null"] + }, + "title": { + "description": "Optional title for the property.", + "type": ["string", "null"] + } + }, + "required": ["items"], + "type": "object" + }, + "NesAcceptNotification": { + "description": "Notification sent when a suggestion is accepted.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "id": { + "description": "The ID of the accepted suggestion.", + "type": "string" + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + } + }, + "required": ["sessionId", "id"], + "type": "object", + "x-method": "nes/accept", + "x-side": "agent" + }, + "NesCapabilities": { + "description": "NES capabilities advertised by the agent during initialization.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions.", + "type": ["object", "null"] + }, + "context": { + "anyOf": [ + { + "$ref": "#/$defs/NesContextCapabilities" + }, + { + "type": "null" + } + ], + "description": "Context the agent wants attached to each suggestion request." + }, + "events": { + "anyOf": [ + { + "$ref": "#/$defs/NesEventCapabilities" + }, + { + "type": "null" + } + ], + "description": "Events the agent wants to receive." + } + }, + "type": "object" + }, + "NesCloseRequest": { + "description": "Request to close an NES session.\n\nThe agent **must** cancel any ongoing work related to the NES session\nand then free up any resources associated with the session.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The ID of the NES session to close." + } + }, + "required": ["sessionId"], + "type": "object", + "x-method": "nes/close", + "x-side": "agent" + }, + "NesCloseResponse": { + "description": "Response from closing an NES session.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + } + }, + "type": "object", + "x-method": "nes/close", + "x-side": "agent" + }, + "NesContextCapabilities": { + "description": "Context capabilities the agent wants attached to each suggestion request.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "diagnostics": { + "anyOf": [ + { + "$ref": "#/$defs/NesDiagnosticsCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants diagnostics context." + }, + "editHistory": { + "anyOf": [ + { + "$ref": "#/$defs/NesEditHistoryCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants edit history context." + }, + "openFiles": { + "anyOf": [ + { + "$ref": "#/$defs/NesOpenFilesCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants open files context." + }, + "recentFiles": { + "anyOf": [ + { + "$ref": "#/$defs/NesRecentFilesCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants recent files context." + }, + "relatedSnippets": { + "anyOf": [ + { + "$ref": "#/$defs/NesRelatedSnippetsCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants related snippets context." + }, + "userActions": { + "anyOf": [ + { + "$ref": "#/$defs/NesUserActionsCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants user actions context." + } + }, + "type": "object" + }, + "NesDiagnostic": { + "description": "A diagnostic (error, warning, etc.).", + "properties": { + "message": { + "description": "The diagnostic message.", + "type": "string" + }, + "range": { + "allOf": [ + { + "$ref": "#/$defs/Range" + } + ], + "description": "The range of the diagnostic." + }, + "severity": { + "allOf": [ + { + "$ref": "#/$defs/NesDiagnosticSeverity" + } + ], + "description": "The severity of the diagnostic." + }, + "uri": { + "description": "The URI of the file containing the diagnostic.", + "type": "string" + } + }, + "required": ["uri", "range", "severity", "message"], + "type": "object" + }, + "NesDiagnosticSeverity": { + "description": "Severity of a diagnostic.", + "oneOf": [ + { + "const": "error", + "description": "An error.", + "type": "string" + }, + { + "const": "warning", + "description": "A warning.", + "type": "string" + }, + { + "const": "information", + "description": "An informational message.", + "type": "string" + }, + { + "const": "hint", + "description": "A hint.", + "type": "string" + } + ] + }, + "NesDiagnosticsCapabilities": { + "description": "Capabilities for diagnostics context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesDocumentDidChangeCapabilities": { + "description": "Capabilities for `document/didChange` events.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "syncKind": { + "allOf": [ + { + "$ref": "#/$defs/TextDocumentSyncKind" + } + ], + "description": "The sync kind the agent wants: `\"full\"` or `\"incremental\"`." + } + }, + "required": ["syncKind"], + "type": "object" + }, + "NesDocumentDidCloseCapabilities": { + "description": "Marker for `document/didClose` capability support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesDocumentDidFocusCapabilities": { + "description": "Marker for `document/didFocus` capability support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesDocumentDidOpenCapabilities": { + "description": "Marker for `document/didOpen` capability support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesDocumentDidSaveCapabilities": { + "description": "Marker for `document/didSave` capability support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesDocumentEventCapabilities": { + "description": "Document event capabilities the agent wants to receive.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "didChange": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentDidChangeCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants `document/didChange` events, and the sync kind." + }, + "didClose": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentDidCloseCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants `document/didClose` events." + }, + "didFocus": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentDidFocusCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants `document/didFocus` events." + }, + "didOpen": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentDidOpenCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants `document/didOpen` events." + }, + "didSave": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentDidSaveCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the agent wants `document/didSave` events." + } + }, + "type": "object" + }, + "NesEditHistoryCapabilities": { + "description": "Capabilities for edit history context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "maxCount": { + "description": "Maximum number of edit history entries the agent can use.", + "format": "uint32", + "minimum": 0, + "type": ["integer", "null"] + } + }, + "type": "object" + }, + "NesEditHistoryEntry": { + "description": "An entry in the edit history.", + "properties": { + "diff": { + "description": "A diff representing the edit.", + "type": "string" + }, + "uri": { + "description": "The URI of the edited file.", + "type": "string" + } + }, + "required": ["uri", "diff"], + "type": "object" + }, + "NesEditSuggestion": { + "description": "A text edit suggestion.", + "properties": { + "cursorPosition": { + "anyOf": [ + { + "$ref": "#/$defs/Position" + }, + { + "type": "null" + } + ], + "description": "Optional suggested cursor position after applying edits." + }, + "edits": { + "description": "The text edits to apply.", + "items": { + "$ref": "#/$defs/NesTextEdit" + }, + "type": "array" + }, + "id": { + "description": "Unique identifier for accept/reject tracking.", + "type": "string" + }, + "uri": { + "description": "The URI of the file to edit.", + "type": "string" + } + }, + "required": ["id", "uri", "edits"], + "type": "object" + }, + "NesEventCapabilities": { + "description": "Event capabilities the agent can consume.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "document": { + "anyOf": [ + { + "$ref": "#/$defs/NesDocumentEventCapabilities" + }, + { + "type": "null" + } + ], + "description": "Document event capabilities." + } + }, + "type": "object" + }, + "NesExcerpt": { + "description": "A code excerpt from a file.", + "properties": { + "endLine": { + "description": "The end line of the excerpt (zero-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "startLine": { + "description": "The start line of the excerpt (zero-based).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "text": { + "description": "The text content of the excerpt.", + "type": "string" + } + }, + "required": ["startLine", "endLine", "text"], + "type": "object" + }, + "NesJumpCapabilities": { + "description": "Marker for jump suggestion support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesJumpSuggestion": { + "description": "A jump-to-location suggestion.", + "properties": { + "id": { + "description": "Unique identifier for accept/reject tracking.", + "type": "string" + }, + "position": { + "allOf": [ + { + "$ref": "#/$defs/Position" + } + ], + "description": "The target position within the file." + }, + "uri": { + "description": "The file to navigate to.", + "type": "string" + } + }, + "required": ["id", "uri", "position"], + "type": "object" + }, + "NesOpenFile": { + "description": "An open file in the editor.", + "properties": { + "languageId": { + "description": "The language identifier.", + "type": "string" + }, + "lastFocusedMs": { + "description": "Timestamp in milliseconds since epoch of when the file was last focused.", + "format": "uint64", + "minimum": 0, + "type": ["integer", "null"] + }, + "uri": { + "description": "The URI of the file.", + "type": "string" + }, + "visibleRange": { + "anyOf": [ + { + "$ref": "#/$defs/Range" + }, + { + "type": "null" + } + ], + "description": "The visible range in the editor, if any." + } + }, + "required": ["uri", "languageId"], + "type": "object" + }, + "NesOpenFilesCapabilities": { + "description": "Capabilities for open files context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesRecentFile": { + "description": "A recently accessed file.", + "properties": { + "languageId": { + "description": "The language identifier.", + "type": "string" + }, + "text": { + "description": "The full text content of the file.", + "type": "string" + }, + "uri": { + "description": "The URI of the file.", + "type": "string" + } + }, + "required": ["uri", "languageId", "text"], + "type": "object" + }, + "NesRecentFilesCapabilities": { + "description": "Capabilities for recent files context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "maxCount": { + "description": "Maximum number of recent files the agent can use.", + "format": "uint32", + "minimum": 0, + "type": ["integer", "null"] + } + }, + "type": "object" + }, + "NesRejectNotification": { + "description": "Notification sent when a suggestion is rejected.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "id": { + "description": "The ID of the rejected suggestion.", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "$ref": "#/$defs/NesRejectReason" + }, + { + "type": "null" + } + ], + "description": "The reason for rejection." + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this notification." + } + }, + "required": ["sessionId", "id"], + "type": "object", + "x-method": "nes/reject", + "x-side": "agent" + }, + "NesRejectReason": { + "description": "The reason a suggestion was rejected.", + "oneOf": [ + { + "const": "rejected", + "description": "The user explicitly dismissed the suggestion.", + "type": "string" + }, + { + "const": "ignored", + "description": "The suggestion was shown but the user continued editing without interacting.", + "type": "string" + }, + { + "const": "replaced", + "description": "The suggestion was superseded by a newer suggestion.", + "type": "string" + }, + { + "const": "cancelled", + "description": "The request was cancelled before the agent returned a response.", + "type": "string" + } + ] + }, + "NesRelatedSnippet": { + "description": "A related code snippet from a file.", + "properties": { + "excerpts": { + "description": "The code excerpts.", + "items": { + "$ref": "#/$defs/NesExcerpt" + }, + "type": "array" + }, + "uri": { + "description": "The URI of the file containing the snippets.", + "type": "string" + } + }, + "required": ["uri", "excerpts"], + "type": "object" + }, + "NesRelatedSnippetsCapabilities": { + "description": "Capabilities for related snippets context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesRenameActionCapabilities": { + "description": "Marker for rename action support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesRenameActionSuggestion": { + "description": "A rename symbol suggestion.", + "properties": { + "id": { + "description": "Unique identifier for accept/reject tracking.", + "type": "string" + }, + "newName": { + "description": "The new name for the symbol.", + "type": "string" + }, + "position": { + "allOf": [ + { + "$ref": "#/$defs/Position" + } + ], + "description": "The position of the symbol to rename." + }, + "uri": { + "description": "The file URI containing the symbol.", + "type": "string" + } + }, + "required": ["id", "uri", "position", "newName"], + "type": "object" + }, + "NesRepository": { + "description": "Repository metadata for an NES session.", + "properties": { + "name": { + "description": "The repository name.", + "type": "string" + }, + "owner": { + "description": "The repository owner.", + "type": "string" + }, + "remoteUrl": { + "description": "The remote URL of the repository.", + "type": "string" + } + }, + "required": ["name", "owner", "remoteUrl"], + "type": "object" + }, + "NesSearchAndReplaceActionCapabilities": { + "description": "Marker for search and replace action support.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + } + }, + "type": "object" + }, + "NesSearchAndReplaceActionSuggestion": { + "description": "A search-and-replace suggestion.", + "properties": { + "id": { + "description": "Unique identifier for accept/reject tracking.", + "type": "string" + }, + "isRegex": { + "description": "Whether `search` is a regular expression. Defaults to `false`.", + "type": ["boolean", "null"] + }, + "replace": { + "description": "The replacement text.", + "type": "string" + }, + "search": { + "description": "The text or pattern to find.", + "type": "string" + }, + "uri": { + "description": "The file URI to search within.", + "type": "string" + } + }, + "required": ["id", "uri", "search", "replace"], + "type": "object" + }, + "NesStartRequest": { + "description": "Request to start an NES session.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "repository": { + "anyOf": [ + { + "$ref": "#/$defs/NesRepository" + }, + { + "type": "null" + } + ], + "description": "Repository metadata, if the workspace is a git repository." + }, + "workspaceFolders": { + "description": "The workspace folders.", + "items": { + "$ref": "#/$defs/WorkspaceFolder" + }, + "type": ["array", "null"] + }, + "workspaceUri": { + "description": "The root URI of the workspace.", + "type": ["string", "null"] + } + }, + "type": "object", + "x-method": "nes/start", + "x-side": "agent" + }, + "NesStartResponse": { + "description": "Response to `nes/start`.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for the newly started NES session." + } + }, + "required": ["sessionId"], + "type": "object", + "x-method": "nes/start", + "x-side": "agent" + }, + "NesSuggestContext": { + "description": "Context attached to a suggestion request.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "diagnostics": { + "description": "Current diagnostics (errors, warnings).", + "items": { + "$ref": "#/$defs/NesDiagnostic" + }, + "type": ["array", "null"] + }, + "editHistory": { + "description": "Recent edit history.", + "items": { + "$ref": "#/$defs/NesEditHistoryEntry" + }, + "type": ["array", "null"] + }, + "openFiles": { + "description": "Currently open files in the editor.", + "items": { + "$ref": "#/$defs/NesOpenFile" + }, + "type": ["array", "null"] + }, + "recentFiles": { + "description": "Recently accessed files.", + "items": { + "$ref": "#/$defs/NesRecentFile" + }, + "type": ["array", "null"] + }, + "relatedSnippets": { + "description": "Related code snippets.", + "items": { + "$ref": "#/$defs/NesRelatedSnippet" + }, + "type": ["array", "null"] + }, + "userActions": { + "description": "Recent user actions (typing, navigation, etc.).", + "items": { + "$ref": "#/$defs/NesUserAction" + }, + "type": ["array", "null"] + } + }, + "type": "object" + }, + "NesSuggestRequest": { + "description": "Request for a code suggestion.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "context": { + "anyOf": [ + { + "$ref": "#/$defs/NesSuggestContext" + }, + { + "type": "null" + } + ], + "description": "Context for the suggestion, included based on agent capabilities." + }, + "position": { + "allOf": [ + { + "$ref": "#/$defs/Position" + } + ], + "description": "The current cursor position." + }, + "selection": { + "anyOf": [ + { + "$ref": "#/$defs/Range" + }, + { + "type": "null" + } + ], + "description": "The current text selection range, if any." + }, + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session ID for this request." + }, + "triggerKind": { + "allOf": [ + { + "$ref": "#/$defs/NesTriggerKind" + } + ], + "description": "What triggered this suggestion request." }, - "name": { - "description": "Human-readable name identifying this MCP server.", + "uri": { + "description": "The URI of the document to suggest for.", "type": "string" }, - "url": { - "description": "URL to the MCP server.", - "type": "string" + "version": { + "description": "The version number of the document.", + "format": "int64", + "type": "integer" } }, - "required": ["name", "url", "headers"], - "type": "object" + "required": ["sessionId", "uri", "version", "position", "triggerKind"], + "type": "object", + "x-method": "nes/suggest", + "x-side": "agent" }, - "McpServerStdio": { - "description": "Stdio transport configuration for MCP.", + "NesSuggestResponse": { + "description": "Response to `nes/suggest`.", "properties": { "_meta": { "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "description": "The _meta property is reserved by ACP.", "type": ["object", "null"] }, - "args": { - "description": "Command-line arguments to pass to the MCP server.", - "items": { - "type": "string" - }, - "type": "array" - }, - "command": { - "description": "Path to the MCP server executable.", - "type": "string" - }, - "env": { - "description": "Environment variables to set when launching the MCP server.", + "suggestions": { + "description": "The list of suggestions.", "items": { - "$ref": "#/$defs/EnvVariable" + "$ref": "#/$defs/NesSuggestion" }, "type": "array" - }, - "name": { - "description": "Human-readable name identifying this MCP server.", - "type": "string" } }, - "required": ["name", "command", "args", "env"], - "type": "object" - }, - "ModelId": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nA unique identifier for a model.", - "type": "string" + "required": ["suggestions"], + "type": "object", + "x-method": "nes/suggest", + "x-side": "agent" }, - "ModelInfo": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nInformation about a selectable model.", - "properties": { - "_meta": { - "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"] - }, - "description": { - "description": "Optional description of the model.", - "type": ["string", "null"] + "NesSuggestion": { + "description": "A suggestion returned by the agent.", + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/NesEditSuggestion" + } + ], + "description": "A text edit suggestion.", + "properties": { + "kind": { + "const": "edit", + "type": "string" + } + }, + "required": ["kind"], + "type": "object" }, - "modelId": { + { "allOf": [ { - "$ref": "#/$defs/ModelId" + "$ref": "#/$defs/NesJumpSuggestion" } ], - "description": "Unique identifier for the model." + "description": "A jump-to-location suggestion.", + "properties": { + "kind": { + "const": "jump", + "type": "string" + } + }, + "required": ["kind"], + "type": "object" }, - "name": { - "description": "Human-readable name of the model.", - "type": "string" - } - }, - "required": ["modelId", "name"], - "type": "object" - }, - "MultiSelectItems": { - "anyOf": [ { "allOf": [ { - "$ref": "#/$defs/UntitledMultiSelectItems" + "$ref": "#/$defs/NesRenameActionSuggestion" } ], - "description": "Untitled multi-select items with plain string values.", - "title": "Untitled" + "description": "A rename symbol suggestion.", + "properties": { + "kind": { + "const": "rename", + "type": "string" + } + }, + "required": ["kind"], + "type": "object" }, { "allOf": [ { - "$ref": "#/$defs/TitledMultiSelectItems" + "$ref": "#/$defs/NesSearchAndReplaceActionSuggestion" } ], - "description": "Titled multi-select items with human-readable labels.", - "title": "Titled" + "description": "A search-and-replace suggestion.", + "properties": { + "kind": { + "const": "searchAndReplace", + "type": "string" + } + }, + "required": ["kind"], + "type": "object" } - ], - "description": "Items for a multi-select (array) property schema." + ] }, - "MultiSelectPropertySchema": { - "description": "Schema for multi-select (array) properties in an elicitation form.", + "NesTextEdit": { + "description": "A text edit within a suggestion.", "properties": { - "default": { - "description": "Default selected values.", - "items": { - "type": "string" - }, - "type": ["array", "null"] + "newText": { + "description": "The replacement text.", + "type": "string" }, - "description": { - "description": "Human-readable description.", - "type": ["string", "null"] + "range": { + "allOf": [ + { + "$ref": "#/$defs/Range" + } + ], + "description": "The range to replace." + } + }, + "required": ["range", "newText"], + "type": "object" + }, + "NesTriggerKind": { + "description": "What triggered the suggestion request.", + "oneOf": [ + { + "const": "automatic", + "description": "Triggered by user typing or cursor movement.", + "type": "string" }, - "items": { + { + "const": "diagnostic", + "description": "Triggered by a diagnostic appearing at or near the cursor.", + "type": "string" + }, + { + "const": "manual", + "description": "Triggered by an explicit user action (keyboard shortcut).", + "type": "string" + } + ] + }, + "NesUserAction": { + "description": "A user action (typing, cursor movement, etc.).", + "properties": { + "action": { + "description": "The kind of action (e.g., \"insertChar\", \"cursorMovement\").", + "type": "string" + }, + "position": { "allOf": [ { - "$ref": "#/$defs/MultiSelectItems" + "$ref": "#/$defs/Position" } ], - "description": "The items definition describing allowed values." + "description": "The position where the action occurred." }, - "maxItems": { - "description": "Maximum number of items to select.", + "timestampMs": { + "description": "Timestamp in milliseconds since epoch.", "format": "uint64", "minimum": 0, - "type": ["integer", "null"] + "type": "integer" }, - "minItems": { - "description": "Minimum number of items to select.", - "format": "uint64", + "uri": { + "description": "The URI of the file where the action occurred.", + "type": "string" + } + }, + "required": ["action", "uri", "position", "timestampMs"], + "type": "object" + }, + "NesUserActionsCapabilities": { + "description": "Capabilities for user actions context.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP.", + "type": ["object", "null"] + }, + "maxCount": { + "description": "Maximum number of user actions the agent can use.", + "format": "uint32", "minimum": 0, "type": ["integer", "null"] - }, - "title": { - "description": "Optional title for the property.", - "type": ["string", "null"] } }, - "required": ["items"], "type": "object" }, "NewSessionRequest": { @@ -3126,6 +4631,45 @@ } ] }, + "Position": { + "description": "A zero-based position in a text document.\n\nThe meaning of `character` depends on the negotiated position encoding.", + "properties": { + "character": { + "description": "Zero-based character offset (encoding-dependent).", + "format": "uint32", + "minimum": 0, + "type": "integer" + }, + "line": { + "description": "Zero-based line number.", + "format": "uint32", + "minimum": 0, + "type": "integer" + } + }, + "required": ["line", "character"], + "type": "object" + }, + "PositionEncodingKind": { + "description": "The encoding used for character offsets in positions.\n\nFollows the same conventions as LSP 3.17. The default is UTF-16.", + "oneOf": [ + { + "const": "utf-16", + "description": "Character offsets count UTF-16 code units. This is the default.", + "type": "string" + }, + { + "const": "utf-32", + "description": "Character offsets count Unicode code points.", + "type": "string" + }, + { + "const": "utf-8", + "description": "Character offsets count UTF-8 code units (bytes).", + "type": "string" + } + ] + }, "PromptCapabilities": { "description": "Prompt capabilities supported by the agent in `session/prompt` requests.\n\nBaseline agent functionality requires support for [`ContentBlock::Text`]\nand [`ContentBlock::ResourceLink`] in prompt requests.\n\nOther variants must be explicitly opted in to.\nCapabilities for different types of content in prompt requests.\n\nIndicates which content types beyond the baseline (text and resource links)\nthe agent can process.\n\nSee protocol docs: [Prompt Capabilities](https://agentclientprotocol.com/protocol/initialization#prompt-capabilities)", "properties": { @@ -3229,6 +4773,29 @@ "minimum": 0, "type": "integer" }, + "Range": { + "description": "A range in a text document, expressed as start and end positions.", + "properties": { + "end": { + "allOf": [ + { + "$ref": "#/$defs/Position" + } + ], + "description": "The end position (exclusive)." + }, + "start": { + "allOf": [ + { + "$ref": "#/$defs/Position" + } + ], + "description": "The start position (inclusive)." + } + }, + "required": ["start", "end"], + "type": "object" + }, "ReadTextFileRequest": { "description": "Request to read content from a text file.\n\nOnly available if the client supports the `fs.readTextFile` capability.", "properties": { @@ -4654,6 +6221,43 @@ "required": ["text"], "type": "object" }, + "TextDocumentContentChangeEvent": { + "description": "A content change event for a document.\n\nWhen `range` is `None`, `text` is the full content of the document.\nWhen `range` is `Some`, `text` replaces the given range.", + "properties": { + "range": { + "anyOf": [ + { + "$ref": "#/$defs/Range" + }, + { + "type": "null" + } + ], + "description": "The range of the document that changed. If `None`, the entire content is replaced." + }, + "text": { + "description": "The new text for the range, or the full document content if `range` is `None`.", + "type": "string" + } + }, + "required": ["text"], + "type": "object" + }, + "TextDocumentSyncKind": { + "description": "How the agent wants document changes delivered.", + "oneOf": [ + { + "const": "full", + "description": "Client sends the entire file content on each change.", + "type": "string" + }, + { + "const": "incremental", + "description": "Client sends only the changed ranges.", + "type": "string" + } + ] + }, "TextResourceContents": { "description": "Text-based resource contents.", "properties": { @@ -5142,6 +6746,21 @@ "x-method": "terminal/wait_for_exit", "x-side": "client" }, + "WorkspaceFolder": { + "description": "A workspace folder.", + "properties": { + "name": { + "description": "The display name of the folder.", + "type": "string" + }, + "uri": { + "description": "The URI of the folder.", + "type": "string" + } + }, + "required": ["uri", "name"], + "type": "object" + }, "WriteTextFileRequest": { "description": "Request to write content to a text file.\n\nOnly available if the client supports the `fs.writeTextFile` capability.", "properties": { diff --git a/src/agent.rs b/src/agent.rs index b8ce81de..d51a68f7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -17,6 +17,14 @@ use crate::{ ProtocolVersion, SessionId, }; +#[cfg(feature = "unstable_nes")] +use crate::{ + DocumentDidChangeNotification, DocumentDidCloseNotification, DocumentDidFocusNotification, + DocumentDidOpenNotification, DocumentDidSaveNotification, NesAcceptNotification, + NesCapabilities, NesCloseRequest, NesCloseResponse, NesRejectNotification, NesStartRequest, + NesStartResponse, NesSuggestRequest, NesSuggestResponse, PositionEncodingKind, +}; + // Initialize /// Request parameters for the initialize method. @@ -3243,6 +3251,22 @@ pub struct AgentCapabilities { #[cfg(feature = "unstable_logout")] #[serde(default)] pub auth: AgentAuthCapabilities, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// NES (Next Edit Suggestions) capabilities supported by the agent. + #[cfg(feature = "unstable_nes")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nes: Option, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// The position encoding selected by the agent from the client's supported encodings. + #[cfg(feature = "unstable_nes")] + #[serde(skip_serializing_if = "Option::is_none")] + pub position_encoding: Option, /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -3298,6 +3322,29 @@ impl AgentCapabilities { self } + /// **UNSTABLE** + /// + /// NES (Next Edit Suggestions) capabilities supported by the agent. + #[cfg(feature = "unstable_nes")] + #[must_use] + pub fn nes(mut self, nes: impl IntoOption) -> Self { + self.nes = nes.into_option(); + self + } + + /// **UNSTABLE** + /// + /// The position encoding selected by the agent from the client's supported encodings. + #[cfg(feature = "unstable_nes")] + #[must_use] + pub fn position_encoding( + mut self, + position_encoding: impl IntoOption, + ) -> Self { + self.position_encoding = position_encoding.into_option(); + self + } + /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -3728,6 +3775,36 @@ pub struct AgentMethodNames { /// Method for logging out of an authenticated session. #[cfg(feature = "unstable_logout")] pub logout: &'static str, + /// Method for starting an NES session. + #[cfg(feature = "unstable_nes")] + pub nes_start: &'static str, + /// Method for requesting a suggestion. + #[cfg(feature = "unstable_nes")] + pub nes_suggest: &'static str, + /// Notification for accepting a suggestion. + #[cfg(feature = "unstable_nes")] + pub nes_accept: &'static str, + /// Notification for rejecting a suggestion. + #[cfg(feature = "unstable_nes")] + pub nes_reject: &'static str, + /// Method for closing an NES session. + #[cfg(feature = "unstable_nes")] + pub nes_close: &'static str, + /// Notification for document open events. + #[cfg(feature = "unstable_nes")] + pub document_did_open: &'static str, + /// Notification for document change events. + #[cfg(feature = "unstable_nes")] + pub document_did_change: &'static str, + /// Notification for document close events. + #[cfg(feature = "unstable_nes")] + pub document_did_close: &'static str, + /// Notification for document save events. + #[cfg(feature = "unstable_nes")] + pub document_did_save: &'static str, + /// Notification for document focus events. + #[cfg(feature = "unstable_nes")] + pub document_did_focus: &'static str, } /// Constant containing all agent method names. @@ -3751,6 +3828,34 @@ pub const AGENT_METHOD_NAMES: AgentMethodNames = AgentMethodNames { session_close: SESSION_CLOSE_METHOD_NAME, #[cfg(feature = "unstable_logout")] logout: LOGOUT_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + nes_start: NES_START_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + nes_suggest: NES_SUGGEST_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + nes_accept: NES_ACCEPT_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + nes_reject: NES_REJECT_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + nes_close: NES_CLOSE_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + document_did_open: DOCUMENT_DID_OPEN_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + document_did_change: DOCUMENT_DID_CHANGE_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + document_did_close: DOCUMENT_DID_CLOSE_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + document_did_save: DOCUMENT_DID_SAVE_METHOD_NAME, + #[cfg(feature = "unstable_nes")] + document_did_focus: DOCUMENT_DID_FOCUS_METHOD_NAME, +}; + +#[cfg(feature = "unstable_nes")] +use crate::nes::{ + DOCUMENT_DID_CHANGE_METHOD_NAME, DOCUMENT_DID_CLOSE_METHOD_NAME, + DOCUMENT_DID_FOCUS_METHOD_NAME, DOCUMENT_DID_OPEN_METHOD_NAME, DOCUMENT_DID_SAVE_METHOD_NAME, + NES_ACCEPT_METHOD_NAME, NES_CLOSE_METHOD_NAME, NES_REJECT_METHOD_NAME, NES_START_METHOD_NAME, + NES_SUGGEST_METHOD_NAME, }; /// Method name for the initialize request. @@ -3932,6 +4037,30 @@ pub enum ClientRequest { /// /// Select a model for a given session. SetSessionModelRequest(SetSessionModelRequest), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Starts an NES session. + NesStartRequest(NesStartRequest), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Requests a code suggestion. + NesSuggestRequest(NesSuggestRequest), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Closes an active NES session and frees up any resources associated with it. + /// + /// The agent must cancel any ongoing work and then free up any resources + /// associated with the NES session. + NesCloseRequest(NesCloseRequest), /// Handles extension method requests from the client. /// /// Extension methods provide a way to add custom functionality while maintaining @@ -3964,6 +4093,12 @@ impl ClientRequest { Self::PromptRequest(_) => AGENT_METHOD_NAMES.session_prompt, #[cfg(feature = "unstable_session_model")] Self::SetSessionModelRequest(_) => AGENT_METHOD_NAMES.session_set_model, + #[cfg(feature = "unstable_nes")] + Self::NesStartRequest(_) => AGENT_METHOD_NAMES.nes_start, + #[cfg(feature = "unstable_nes")] + Self::NesSuggestRequest(_) => AGENT_METHOD_NAMES.nes_suggest, + #[cfg(feature = "unstable_nes")] + Self::NesCloseRequest(_) => AGENT_METHOD_NAMES.nes_close, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -3999,6 +4134,12 @@ pub enum AgentResponse { PromptResponse(PromptResponse), #[cfg(feature = "unstable_session_model")] SetSessionModelResponse(#[serde(default)] SetSessionModelResponse), + #[cfg(feature = "unstable_nes")] + NesStartResponse(NesStartResponse), + #[cfg(feature = "unstable_nes")] + NesSuggestResponse(NesSuggestResponse), + #[cfg(feature = "unstable_nes")] + NesCloseResponse(#[serde(default)] NesCloseResponse), ExtMethodResponse(ExtResponse), } @@ -4025,6 +4166,41 @@ pub enum ClientNotification { /// /// See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) CancelNotification(CancelNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a file is opened in the editor. + DocumentDidOpenNotification(DocumentDidOpenNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a file is edited. + DocumentDidChangeNotification(DocumentDidChangeNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a file is closed. + DocumentDidCloseNotification(DocumentDidCloseNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a file is saved. + DocumentDidSaveNotification(DocumentDidSaveNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a file becomes the active editor tab. + DocumentDidFocusNotification(DocumentDidFocusNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a suggestion is accepted. + NesAcceptNotification(NesAcceptNotification), + #[cfg(feature = "unstable_nes")] + /// **UNSTABLE** + /// + /// Notification sent when a suggestion is rejected. + NesRejectNotification(NesRejectNotification), /// Handles extension notifications from the client. /// /// Extension notifications provide a way to send one-way messages for custom functionality @@ -4040,6 +4216,20 @@ impl ClientNotification { pub fn method(&self) -> &str { match self { Self::CancelNotification(_) => AGENT_METHOD_NAMES.session_cancel, + #[cfg(feature = "unstable_nes")] + Self::DocumentDidOpenNotification(_) => AGENT_METHOD_NAMES.document_did_open, + #[cfg(feature = "unstable_nes")] + Self::DocumentDidChangeNotification(_) => AGENT_METHOD_NAMES.document_did_change, + #[cfg(feature = "unstable_nes")] + Self::DocumentDidCloseNotification(_) => AGENT_METHOD_NAMES.document_did_close, + #[cfg(feature = "unstable_nes")] + Self::DocumentDidSaveNotification(_) => AGENT_METHOD_NAMES.document_did_save, + #[cfg(feature = "unstable_nes")] + Self::DocumentDidFocusNotification(_) => AGENT_METHOD_NAMES.document_did_focus, + #[cfg(feature = "unstable_nes")] + Self::NesAcceptNotification(_) => AGENT_METHOD_NAMES.nes_accept, + #[cfg(feature = "unstable_nes")] + Self::NesRejectNotification(_) => AGENT_METHOD_NAMES.nes_reject, Self::ExtNotification(ext_notification) => &ext_notification.method, } } diff --git a/src/bin/generate.rs b/src/bin/generate.rs index 3051d30f..859b0f5d 100644 --- a/src/bin/generate.rs +++ b/src/bin/generate.rs @@ -1032,6 +1032,16 @@ starting with '$/' it is free to ignore the notification." "session/set_model" => self.agent.get("SetSessionModelRequest").unwrap(), "session/close" => self.agent.get("CloseSessionRequest").unwrap(), "logout" => self.agent.get("LogoutRequest").unwrap(), + "nes/start" => self.agent.get("NesStartRequest").unwrap(), + "nes/suggest" => self.agent.get("NesSuggestRequest").unwrap(), + "nes/close" => self.agent.get("NesCloseRequest").unwrap(), + "nes/accept" => self.agent.get("NesAcceptNotification").unwrap(), + "nes/reject" => self.agent.get("NesRejectNotification").unwrap(), + "document/didOpen" => self.agent.get("DocumentDidOpenNotification").unwrap(), + "document/didChange" => self.agent.get("DocumentDidChangeNotification").unwrap(), + "document/didClose" => self.agent.get("DocumentDidCloseNotification").unwrap(), + "document/didSave" => self.agent.get("DocumentDidSaveNotification").unwrap(), + "document/didFocus" => self.agent.get("DocumentDidFocusNotification").unwrap(), _ => panic!("Introduced a method? Add it here :)"), } } diff --git a/src/client.rs b/src/client.rs index ef81f671..0a34cfe3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -20,6 +20,9 @@ use crate::{ }; use crate::{IntoMaybeUndefined, MaybeUndefined}; +#[cfg(feature = "unstable_nes")] +use crate::{ClientNesCapabilities, PositionEncodingKind}; + // Session updates /// Notification containing a session update from the agent. @@ -1500,6 +1503,23 @@ pub struct ClientCapabilities { #[cfg(feature = "unstable_elicitation")] #[serde(default, skip_serializing_if = "Option::is_none")] pub elicitation: Option, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// NES (Next Edit Suggestions) capabilities supported by the client. + #[cfg(feature = "unstable_nes")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nes: Option, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// The position encodings supported by the client, in order of preference. + #[cfg(feature = "unstable_nes")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub position_encodings: Vec, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -1557,6 +1577,26 @@ impl ClientCapabilities { self } + /// **UNSTABLE** + /// + /// NES (Next Edit Suggestions) capabilities supported by the client. + #[cfg(feature = "unstable_nes")] + #[must_use] + pub fn nes(mut self, nes: impl IntoOption) -> Self { + self.nes = nes.into_option(); + self + } + + /// **UNSTABLE** + /// + /// The position encodings supported by the client, in order of preference. + #[cfg(feature = "unstable_nes")] + #[must_use] + pub fn position_encodings(mut self, position_encodings: Vec) -> Self { + self.position_encodings = position_encodings; + self + } + /// The _meta property is reserved by ACP to allow clients and agents to attach additional /// metadata to their interactions. Implementations MUST NOT make assumptions about values at /// these keys. @@ -2020,4 +2060,18 @@ mod tests { json!({}) ); } + + #[cfg(feature = "unstable_nes")] + #[test] + fn test_client_capabilities_position_encodings_serialization() { + use serde_json::json; + + let capabilities = ClientCapabilities::new().position_encodings(vec![ + PositionEncodingKind::Utf32, + PositionEncodingKind::Utf16, + ]); + let json = serde_json::to_value(&capabilities).unwrap(); + + assert_eq!(json["positionEncodings"], json!(["utf-32", "utf-16"])); + } } diff --git a/src/lib.rs b/src/lib.rs index f6cdb638..1be0f6c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,8 @@ mod elicitation; mod error; mod ext; mod maybe_undefined; +#[cfg(feature = "unstable_nes")] +mod nes; mod plan; #[cfg(feature = "unstable_cancel_request")] mod protocol_level; @@ -74,6 +76,8 @@ pub use elicitation::*; pub use error::*; pub use ext::*; pub use maybe_undefined::*; +#[cfg(feature = "unstable_nes")] +pub use nes::*; pub use plan::*; #[cfg(feature = "unstable_cancel_request")] pub use protocol_level::*; diff --git a/src/nes.rs b/src/nes.rs new file mode 100644 index 00000000..a6ba01a0 --- /dev/null +++ b/src/nes.rs @@ -0,0 +1,2288 @@ +//! Next Edit Suggestions (NES) types and constants. +//! +//! NES allows agents to provide predictive code edits via capability negotiation, +//! document events, and a suggestion request/response flow. NES sessions are +//! independent of chat sessions and have their own lifecycle. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{IntoOption, Meta, SessionId}; + +// Method name constants + +/// Method name for starting an NES session. +pub(crate) const NES_START_METHOD_NAME: &str = "nes/start"; +/// Method name for requesting a suggestion. +pub(crate) const NES_SUGGEST_METHOD_NAME: &str = "nes/suggest"; +/// Method name for accepting a suggestion. +pub(crate) const NES_ACCEPT_METHOD_NAME: &str = "nes/accept"; +/// Method name for rejecting a suggestion. +pub(crate) const NES_REJECT_METHOD_NAME: &str = "nes/reject"; +/// Method name for closing an NES session. +pub(crate) const NES_CLOSE_METHOD_NAME: &str = "nes/close"; +/// Notification name for document open events. +pub(crate) const DOCUMENT_DID_OPEN_METHOD_NAME: &str = "document/didOpen"; +/// Notification name for document change events. +pub(crate) const DOCUMENT_DID_CHANGE_METHOD_NAME: &str = "document/didChange"; +/// Notification name for document close events. +pub(crate) const DOCUMENT_DID_CLOSE_METHOD_NAME: &str = "document/didClose"; +/// Notification name for document save events. +pub(crate) const DOCUMENT_DID_SAVE_METHOD_NAME: &str = "document/didSave"; +/// Notification name for document focus events. +pub(crate) const DOCUMENT_DID_FOCUS_METHOD_NAME: &str = "document/didFocus"; + +/// Names of all NES-related methods. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[non_exhaustive] +pub struct NesMethodNames { + /// Method for starting an NES session. + pub nes_start: &'static str, + /// Method for requesting a suggestion. + pub nes_suggest: &'static str, + /// Notification for accepting a suggestion. + pub nes_accept: &'static str, + /// Notification for rejecting a suggestion. + pub nes_reject: &'static str, + /// Method for closing an NES session. + pub nes_close: &'static str, + /// Notification for document open events. + pub document_did_open: &'static str, + /// Notification for document change events. + pub document_did_change: &'static str, + /// Notification for document close events. + pub document_did_close: &'static str, + /// Notification for document save events. + pub document_did_save: &'static str, + /// Notification for document focus events. + pub document_did_focus: &'static str, +} + +/// Constant containing all NES method names. +pub const NES_METHOD_NAMES: NesMethodNames = NesMethodNames { + nes_start: NES_START_METHOD_NAME, + nes_suggest: NES_SUGGEST_METHOD_NAME, + nes_accept: NES_ACCEPT_METHOD_NAME, + nes_reject: NES_REJECT_METHOD_NAME, + nes_close: NES_CLOSE_METHOD_NAME, + document_did_open: DOCUMENT_DID_OPEN_METHOD_NAME, + document_did_change: DOCUMENT_DID_CHANGE_METHOD_NAME, + document_did_close: DOCUMENT_DID_CLOSE_METHOD_NAME, + document_did_save: DOCUMENT_DID_SAVE_METHOD_NAME, + document_did_focus: DOCUMENT_DID_FOCUS_METHOD_NAME, +}; + +// Position primitives + +/// The encoding used for character offsets in positions. +/// +/// Follows the same conventions as LSP 3.17. The default is UTF-16. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[non_exhaustive] +pub enum PositionEncodingKind { + /// Character offsets count UTF-16 code units. This is the default. + #[serde(rename = "utf-16")] + Utf16, + /// Character offsets count Unicode code points. + #[serde(rename = "utf-32")] + Utf32, + /// Character offsets count UTF-8 code units (bytes). + #[serde(rename = "utf-8")] + Utf8, +} + +/// A zero-based position in a text document. +/// +/// The meaning of `character` depends on the negotiated position encoding. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Position { + /// Zero-based line number. + pub line: u32, + /// Zero-based character offset (encoding-dependent). + pub character: u32, +} + +impl Position { + #[must_use] + pub fn new(line: u32, character: u32) -> Self { + Self { line, character } + } +} + +/// A range in a text document, expressed as start and end positions. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Range { + /// The start position (inclusive). + pub start: Position, + /// The end position (exclusive). + pub end: Position, +} + +impl Range { + #[must_use] + pub fn new(start: Position, end: Position) -> Self { + Self { start, end } + } +} + +// Agent NES capabilities + +/// NES capabilities advertised by the agent during initialization. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesCapabilities { + /// Events the agent wants to receive. + #[serde(skip_serializing_if = "Option::is_none")] + pub events: Option, + /// Context the agent wants attached to each suggestion request. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn events(mut self, events: impl IntoOption) -> Self { + self.events = events.into_option(); + self + } + + #[must_use] + pub fn context(mut self, context: impl IntoOption) -> Self { + self.context = context.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Event capabilities the agent can consume. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesEventCapabilities { + /// Document event capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub document: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesEventCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn document(mut self, document: impl IntoOption) -> Self { + self.document = document.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Document event capabilities the agent wants to receive. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentEventCapabilities { + /// Whether the agent wants `document/didOpen` events. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_open: Option, + /// Whether the agent wants `document/didChange` events, and the sync kind. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_change: Option, + /// Whether the agent wants `document/didClose` events. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_close: Option, + /// Whether the agent wants `document/didSave` events. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_save: Option, + /// Whether the agent wants `document/didFocus` events. + #[serde(skip_serializing_if = "Option::is_none")] + pub did_focus: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesDocumentEventCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn did_open(mut self, did_open: impl IntoOption) -> Self { + self.did_open = did_open.into_option(); + self + } + + #[must_use] + pub fn did_change( + mut self, + did_change: impl IntoOption, + ) -> Self { + self.did_change = did_change.into_option(); + self + } + + #[must_use] + pub fn did_close( + mut self, + did_close: impl IntoOption, + ) -> Self { + self.did_close = did_close.into_option(); + self + } + + #[must_use] + pub fn did_save(mut self, did_save: impl IntoOption) -> Self { + self.did_save = did_save.into_option(); + self + } + + #[must_use] + pub fn did_focus( + mut self, + did_focus: impl IntoOption, + ) -> Self { + self.did_focus = did_focus.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Marker for `document/didOpen` capability support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentDidOpenCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for `document/didChange` events. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentDidChangeCapabilities { + /// The sync kind the agent wants: `"full"` or `"incremental"`. + pub sync_kind: TextDocumentSyncKind, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesDocumentDidChangeCapabilities { + #[must_use] + pub fn new(sync_kind: TextDocumentSyncKind) -> Self { + Self { + sync_kind, + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// How the agent wants document changes delivered. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[non_exhaustive] +pub enum TextDocumentSyncKind { + /// Client sends the entire file content on each change. + #[serde(rename = "full")] + Full, + /// Client sends only the changed ranges. + #[serde(rename = "incremental")] + Incremental, +} + +/// Marker for `document/didClose` capability support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentDidCloseCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Marker for `document/didSave` capability support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentDidSaveCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Marker for `document/didFocus` capability support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDocumentDidFocusCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Context capabilities the agent wants attached to each suggestion request. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesContextCapabilities { + /// Whether the agent wants recent files context. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_files: Option, + /// Whether the agent wants related snippets context. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_snippets: Option, + /// Whether the agent wants edit history context. + #[serde(skip_serializing_if = "Option::is_none")] + pub edit_history: Option, + /// Whether the agent wants user actions context. + #[serde(skip_serializing_if = "Option::is_none")] + pub user_actions: Option, + /// Whether the agent wants open files context. + #[serde(skip_serializing_if = "Option::is_none")] + pub open_files: Option, + /// Whether the agent wants diagnostics context. + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostics: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesContextCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn recent_files( + mut self, + recent_files: impl IntoOption, + ) -> Self { + self.recent_files = recent_files.into_option(); + self + } + + #[must_use] + pub fn related_snippets( + mut self, + related_snippets: impl IntoOption, + ) -> Self { + self.related_snippets = related_snippets.into_option(); + self + } + + #[must_use] + pub fn edit_history( + mut self, + edit_history: impl IntoOption, + ) -> Self { + self.edit_history = edit_history.into_option(); + self + } + + #[must_use] + pub fn user_actions( + mut self, + user_actions: impl IntoOption, + ) -> Self { + self.user_actions = user_actions.into_option(); + self + } + + #[must_use] + pub fn open_files(mut self, open_files: impl IntoOption) -> Self { + self.open_files = open_files.into_option(); + self + } + + #[must_use] + pub fn diagnostics(mut self, diagnostics: impl IntoOption) -> Self { + self.diagnostics = diagnostics.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Capabilities for recent files context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRecentFilesCapabilities { + /// Maximum number of recent files the agent can use. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_count: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for related snippets context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRelatedSnippetsCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for edit history context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesEditHistoryCapabilities { + /// Maximum number of edit history entries the agent can use. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_count: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for user actions context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesUserActionsCapabilities { + /// Maximum number of user actions the agent can use. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_count: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for open files context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesOpenFilesCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Capabilities for diagnostics context. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDiagnosticsCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +// Client NES capabilities + +/// NES capabilities advertised by the client during initialization. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ClientNesCapabilities { + /// Whether the client supports the `jump` suggestion kind. + #[serde(skip_serializing_if = "Option::is_none")] + pub jump: Option, + /// Whether the client supports the `rename` suggestion kind. + #[serde(skip_serializing_if = "Option::is_none")] + pub rename: Option, + /// Whether the client supports the `searchAndReplace` suggestion kind. + #[serde(skip_serializing_if = "Option::is_none")] + pub search_and_replace: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl ClientNesCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn jump(mut self, jump: impl IntoOption) -> Self { + self.jump = jump.into_option(); + self + } + + #[must_use] + pub fn rename(mut self, rename: impl IntoOption) -> Self { + self.rename = rename.into_option(); + self + } + + #[must_use] + pub fn search_and_replace( + mut self, + search_and_replace: impl IntoOption, + ) -> Self { + self.search_and_replace = search_and_replace.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Marker for jump suggestion support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesJumpCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Marker for rename action support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRenameActionCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +/// Marker for search and replace action support. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesSearchAndReplaceActionCapabilities { + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +// Document event notifications (client -> agent) + +/// Notification sent when a file is opened in the editor. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = DOCUMENT_DID_OPEN_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DocumentDidOpenNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The URI of the opened document. + pub uri: String, + /// The language identifier of the document (e.g., "rust", "python"). + pub language_id: String, + /// The version number of the document. + pub version: i64, + /// The full text content of the document. + pub text: String, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl DocumentDidOpenNotification { + #[must_use] + pub fn new( + session_id: impl Into, + uri: impl Into, + language_id: impl Into, + version: i64, + text: impl Into, + ) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + language_id: language_id.into(), + version, + text: text.into(), + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Notification sent when a file is edited. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = DOCUMENT_DID_CHANGE_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DocumentDidChangeNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The URI of the changed document. + pub uri: String, + /// The new version number of the document. + pub version: i64, + /// The content changes. + pub content_changes: Vec, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl DocumentDidChangeNotification { + #[must_use] + pub fn new( + session_id: impl Into, + uri: impl Into, + version: i64, + content_changes: Vec, + ) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + version, + content_changes, + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// A content change event for a document. +/// +/// When `range` is `None`, `text` is the full content of the document. +/// When `range` is `Some`, `text` replaces the given range. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct TextDocumentContentChangeEvent { + /// The range of the document that changed. If `None`, the entire content is replaced. + #[serde(skip_serializing_if = "Option::is_none")] + pub range: Option, + /// The new text for the range, or the full document content if `range` is `None`. + pub text: String, +} + +impl TextDocumentContentChangeEvent { + #[must_use] + pub fn full(text: impl Into) -> Self { + Self { + range: None, + text: text.into(), + } + } + + #[must_use] + pub fn incremental(range: Range, text: impl Into) -> Self { + Self { + range: Some(range), + text: text.into(), + } + } +} + +/// Notification sent when a file is closed. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = DOCUMENT_DID_CLOSE_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DocumentDidCloseNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The URI of the closed document. + pub uri: String, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl DocumentDidCloseNotification { + #[must_use] + pub fn new(session_id: impl Into, uri: impl Into) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Notification sent when a file is saved. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = DOCUMENT_DID_SAVE_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DocumentDidSaveNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The URI of the saved document. + pub uri: String, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl DocumentDidSaveNotification { + #[must_use] + pub fn new(session_id: impl Into, uri: impl Into) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Notification sent when a file becomes the active editor tab. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = DOCUMENT_DID_FOCUS_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct DocumentDidFocusNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The URI of the focused document. + pub uri: String, + /// The version number of the document. + pub version: i64, + /// The current cursor position. + pub position: Position, + /// The portion of the file currently visible in the editor viewport. + pub visible_range: Range, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl DocumentDidFocusNotification { + #[must_use] + pub fn new( + session_id: impl Into, + uri: impl Into, + version: i64, + position: Position, + visible_range: Range, + ) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + version, + position, + visible_range, + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +// NES session start + +/// Request to start an NES session. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_START_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesStartRequest { + /// The root URI of the workspace. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_uri: Option, + /// The workspace folders. + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_folders: Option>, + /// Repository metadata, if the workspace is a git repository. + #[serde(skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesStartRequest { + #[must_use] + pub fn new() -> Self { + Self { + workspace_uri: None, + workspace_folders: None, + repository: None, + meta: None, + } + } + + #[must_use] + pub fn workspace_uri(mut self, workspace_uri: impl IntoOption) -> Self { + self.workspace_uri = workspace_uri.into_option(); + self + } + + #[must_use] + pub fn workspace_folders( + mut self, + workspace_folders: impl IntoOption>, + ) -> Self { + self.workspace_folders = workspace_folders.into_option(); + self + } + + #[must_use] + pub fn repository(mut self, repository: impl IntoOption) -> Self { + self.repository = repository.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +impl Default for NesStartRequest { + fn default() -> Self { + Self::new() + } +} + +/// A workspace folder. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct WorkspaceFolder { + /// The URI of the folder. + pub uri: String, + /// The display name of the folder. + pub name: String, +} + +impl WorkspaceFolder { + #[must_use] + pub fn new(uri: impl Into, name: impl Into) -> Self { + Self { + uri: uri.into(), + name: name.into(), + } + } +} + +/// Repository metadata for an NES session. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRepository { + /// The repository name. + pub name: String, + /// The repository owner. + pub owner: String, + /// The remote URL of the repository. + pub remote_url: String, +} + +impl NesRepository { + #[must_use] + pub fn new( + name: impl Into, + owner: impl Into, + remote_url: impl Into, + ) -> Self { + Self { + name: name.into(), + owner: owner.into(), + remote_url: remote_url.into(), + } + } +} + +/// Response to `nes/start`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_START_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesStartResponse { + /// The session ID for the newly started NES session. + pub session_id: SessionId, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesStartResponse { + #[must_use] + pub fn new(session_id: impl Into) -> Self { + Self { + session_id: session_id.into(), + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +// NES session close + +/// Request to close an NES session. +/// +/// The agent **must** cancel any ongoing work related to the NES session +/// and then free up any resources associated with the session. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_CLOSE_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesCloseRequest { + /// The ID of the NES session to close. + pub session_id: SessionId, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesCloseRequest { + #[must_use] + pub fn new(session_id: impl Into) -> Self { + Self { + session_id: session_id.into(), + meta: None, + } + } + + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Response from closing an NES session. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_CLOSE_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesCloseResponse { + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesCloseResponse { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +// NES suggest request + +/// What triggered the suggestion request. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[non_exhaustive] +pub enum NesTriggerKind { + /// Triggered by user typing or cursor movement. + #[serde(rename = "automatic")] + Automatic, + /// Triggered by a diagnostic appearing at or near the cursor. + #[serde(rename = "diagnostic")] + Diagnostic, + /// Triggered by an explicit user action (keyboard shortcut). + #[serde(rename = "manual")] + Manual, +} + +/// Request for a code suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_SUGGEST_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesSuggestRequest { + /// The session ID for this request. + pub session_id: SessionId, + /// The URI of the document to suggest for. + pub uri: String, + /// The version number of the document. + pub version: i64, + /// The current cursor position. + pub position: Position, + /// The current text selection range, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection: Option, + /// What triggered this suggestion request. + pub trigger_kind: NesTriggerKind, + /// Context for the suggestion, included based on agent capabilities. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesSuggestRequest { + #[must_use] + pub fn new( + session_id: impl Into, + uri: impl Into, + version: i64, + position: Position, + trigger_kind: NesTriggerKind, + ) -> Self { + Self { + session_id: session_id.into(), + uri: uri.into(), + version, + position, + selection: None, + trigger_kind, + context: None, + meta: None, + } + } + + #[must_use] + pub fn selection(mut self, selection: impl IntoOption) -> Self { + self.selection = selection.into_option(); + self + } + + #[must_use] + pub fn context(mut self, context: impl IntoOption) -> Self { + self.context = context.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Context attached to a suggestion request. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesSuggestContext { + /// Recently accessed files. + #[serde(skip_serializing_if = "Option::is_none")] + pub recent_files: Option>, + /// Related code snippets. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_snippets: Option>, + /// Recent edit history. + #[serde(skip_serializing_if = "Option::is_none")] + pub edit_history: Option>, + /// Recent user actions (typing, navigation, etc.). + #[serde(skip_serializing_if = "Option::is_none")] + pub user_actions: Option>, + /// Currently open files in the editor. + #[serde(skip_serializing_if = "Option::is_none")] + pub open_files: Option>, + /// Current diagnostics (errors, warnings). + #[serde(skip_serializing_if = "Option::is_none")] + pub diagnostics: Option>, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesSuggestContext { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn recent_files(mut self, recent_files: impl IntoOption>) -> Self { + self.recent_files = recent_files.into_option(); + self + } + + #[must_use] + pub fn related_snippets( + mut self, + related_snippets: impl IntoOption>, + ) -> Self { + self.related_snippets = related_snippets.into_option(); + self + } + + #[must_use] + pub fn edit_history(mut self, edit_history: impl IntoOption>) -> Self { + self.edit_history = edit_history.into_option(); + self + } + + #[must_use] + pub fn user_actions(mut self, user_actions: impl IntoOption>) -> Self { + self.user_actions = user_actions.into_option(); + self + } + + #[must_use] + pub fn open_files(mut self, open_files: impl IntoOption>) -> Self { + self.open_files = open_files.into_option(); + self + } + + #[must_use] + pub fn diagnostics(mut self, diagnostics: impl IntoOption>) -> Self { + self.diagnostics = diagnostics.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// A recently accessed file. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRecentFile { + /// The URI of the file. + pub uri: String, + /// The language identifier. + pub language_id: String, + /// The full text content of the file. + pub text: String, +} + +impl NesRecentFile { + #[must_use] + pub fn new( + uri: impl Into, + language_id: impl Into, + text: impl Into, + ) -> Self { + Self { + uri: uri.into(), + language_id: language_id.into(), + text: text.into(), + } + } +} + +/// A related code snippet from a file. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRelatedSnippet { + /// The URI of the file containing the snippets. + pub uri: String, + /// The code excerpts. + pub excerpts: Vec, +} + +impl NesRelatedSnippet { + #[must_use] + pub fn new(uri: impl Into, excerpts: Vec) -> Self { + Self { + uri: uri.into(), + excerpts, + } + } +} + +/// A code excerpt from a file. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesExcerpt { + /// The start line of the excerpt (zero-based). + pub start_line: u32, + /// The end line of the excerpt (zero-based). + pub end_line: u32, + /// The text content of the excerpt. + pub text: String, +} + +impl NesExcerpt { + #[must_use] + pub fn new(start_line: u32, end_line: u32, text: impl Into) -> Self { + Self { + start_line, + end_line, + text: text.into(), + } + } +} + +/// An entry in the edit history. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesEditHistoryEntry { + /// The URI of the edited file. + pub uri: String, + /// A diff representing the edit. + pub diff: String, +} + +impl NesEditHistoryEntry { + #[must_use] + pub fn new(uri: impl Into, diff: impl Into) -> Self { + Self { + uri: uri.into(), + diff: diff.into(), + } + } +} + +/// A user action (typing, cursor movement, etc.). +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesUserAction { + /// The kind of action (e.g., "insertChar", "cursorMovement"). + pub action: String, + /// The URI of the file where the action occurred. + pub uri: String, + /// The position where the action occurred. + pub position: Position, + /// Timestamp in milliseconds since epoch. + pub timestamp_ms: u64, +} + +impl NesUserAction { + #[must_use] + pub fn new( + action: impl Into, + uri: impl Into, + position: Position, + timestamp_ms: u64, + ) -> Self { + Self { + action: action.into(), + uri: uri.into(), + position, + timestamp_ms, + } + } +} + +/// An open file in the editor. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesOpenFile { + /// The URI of the file. + pub uri: String, + /// The language identifier. + pub language_id: String, + /// The visible range in the editor, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub visible_range: Option, + /// Timestamp in milliseconds since epoch of when the file was last focused. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_focused_ms: Option, +} + +impl NesOpenFile { + #[must_use] + pub fn new(uri: impl Into, language_id: impl Into) -> Self { + Self { + uri: uri.into(), + language_id: language_id.into(), + visible_range: None, + last_focused_ms: None, + } + } + + #[must_use] + pub fn visible_range(mut self, visible_range: impl IntoOption) -> Self { + self.visible_range = visible_range.into_option(); + self + } + + #[must_use] + pub fn last_focused_ms(mut self, last_focused_ms: impl IntoOption) -> Self { + self.last_focused_ms = last_focused_ms.into_option(); + self + } +} + +/// A diagnostic (error, warning, etc.). +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesDiagnostic { + /// The URI of the file containing the diagnostic. + pub uri: String, + /// The range of the diagnostic. + pub range: Range, + /// The severity of the diagnostic. + pub severity: NesDiagnosticSeverity, + /// The diagnostic message. + pub message: String, +} + +impl NesDiagnostic { + #[must_use] + pub fn new( + uri: impl Into, + range: Range, + severity: NesDiagnosticSeverity, + message: impl Into, + ) -> Self { + Self { + uri: uri.into(), + range, + severity, + message: message.into(), + } + } +} + +/// Severity of a diagnostic. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[non_exhaustive] +pub enum NesDiagnosticSeverity { + /// An error. + #[serde(rename = "error")] + Error, + /// A warning. + #[serde(rename = "warning")] + Warning, + /// An informational message. + #[serde(rename = "information")] + Information, + /// A hint. + #[serde(rename = "hint")] + Hint, +} + +// NES suggest response + +/// Response to `nes/suggest`. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_SUGGEST_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesSuggestResponse { + /// The list of suggestions. + pub suggestions: Vec, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesSuggestResponse { + #[must_use] + pub fn new(suggestions: Vec) -> Self { + Self { + suggestions, + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// A suggestion returned by the agent. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "camelCase")] +#[non_exhaustive] +pub enum NesSuggestion { + /// A text edit suggestion. + Edit(NesEditSuggestion), + /// A jump-to-location suggestion. + Jump(NesJumpSuggestion), + /// A rename symbol suggestion. + Rename(NesRenameActionSuggestion), + /// A search-and-replace suggestion. + SearchAndReplace(NesSearchAndReplaceActionSuggestion), +} + +/// A text edit suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesEditSuggestion { + /// Unique identifier for accept/reject tracking. + pub id: String, + /// The URI of the file to edit. + pub uri: String, + /// The text edits to apply. + pub edits: Vec, + /// Optional suggested cursor position after applying edits. + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor_position: Option, +} + +impl NesEditSuggestion { + #[must_use] + pub fn new(id: impl Into, uri: impl Into, edits: Vec) -> Self { + Self { + id: id.into(), + uri: uri.into(), + edits, + cursor_position: None, + } + } + + #[must_use] + pub fn cursor_position(mut self, cursor_position: impl IntoOption) -> Self { + self.cursor_position = cursor_position.into_option(); + self + } +} + +/// A text edit within a suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesTextEdit { + /// The range to replace. + pub range: Range, + /// The replacement text. + pub new_text: String, +} + +impl NesTextEdit { + #[must_use] + pub fn new(range: Range, new_text: impl Into) -> Self { + Self { + range, + new_text: new_text.into(), + } + } +} + +/// A jump-to-location suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesJumpSuggestion { + /// Unique identifier for accept/reject tracking. + pub id: String, + /// The file to navigate to. + pub uri: String, + /// The target position within the file. + pub position: Position, +} + +impl NesJumpSuggestion { + #[must_use] + pub fn new(id: impl Into, uri: impl Into, position: Position) -> Self { + Self { + id: id.into(), + uri: uri.into(), + position, + } + } +} + +/// A rename symbol suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRenameActionSuggestion { + /// Unique identifier for accept/reject tracking. + pub id: String, + /// The file URI containing the symbol. + pub uri: String, + /// The position of the symbol to rename. + pub position: Position, + /// The new name for the symbol. + pub new_name: String, +} + +impl NesRenameActionSuggestion { + #[must_use] + pub fn new( + id: impl Into, + uri: impl Into, + position: Position, + new_name: impl Into, + ) -> Self { + Self { + id: id.into(), + uri: uri.into(), + position, + new_name: new_name.into(), + } + } +} + +/// A search-and-replace suggestion. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesSearchAndReplaceActionSuggestion { + /// Unique identifier for accept/reject tracking. + pub id: String, + /// The file URI to search within. + pub uri: String, + /// The text or pattern to find. + pub search: String, + /// The replacement text. + pub replace: String, + /// Whether `search` is a regular expression. Defaults to `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_regex: Option, +} + +impl NesSearchAndReplaceActionSuggestion { + #[must_use] + pub fn new( + id: impl Into, + uri: impl Into, + search: impl Into, + replace: impl Into, + ) -> Self { + Self { + id: id.into(), + uri: uri.into(), + search: search.into(), + replace: replace.into(), + is_regex: None, + } + } + + #[must_use] + pub fn is_regex(mut self, is_regex: impl IntoOption) -> Self { + self.is_regex = is_regex.into_option(); + self + } +} + +// NES accept/reject notifications + +/// Notification sent when a suggestion is accepted. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_ACCEPT_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesAcceptNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The ID of the accepted suggestion. + pub id: String, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesAcceptNotification { + #[must_use] + pub fn new(session_id: impl Into, id: impl Into) -> Self { + Self { + session_id: session_id.into(), + id: id.into(), + meta: None, + } + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// Notification sent when a suggestion is rejected. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "agent", "x-method" = NES_REJECT_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct NesRejectNotification { + /// The session ID for this notification. + pub session_id: SessionId, + /// The ID of the rejected suggestion. + pub id: String, + /// The reason for rejection. + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + /// The _meta property is reserved by ACP. + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +impl NesRejectNotification { + #[must_use] + pub fn new(session_id: impl Into, id: impl Into) -> Self { + Self { + session_id: session_id.into(), + id: id.into(), + reason: None, + meta: None, + } + } + + #[must_use] + pub fn reason(mut self, reason: impl IntoOption) -> Self { + self.reason = reason.into_option(); + self + } + + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// The reason a suggestion was rejected. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[non_exhaustive] +pub enum NesRejectReason { + /// The user explicitly dismissed the suggestion. + #[serde(rename = "rejected")] + Rejected, + /// The suggestion was shown but the user continued editing without interacting. + #[serde(rename = "ignored")] + Ignored, + /// The suggestion was superseded by a newer suggestion. + #[serde(rename = "replaced")] + Replaced, + /// The request was cancelled before the agent returned a response. + #[serde(rename = "cancelled")] + Cancelled, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_position_encoding_kind_serialization() { + assert_eq!( + serde_json::to_value(&PositionEncodingKind::Utf16).unwrap(), + json!("utf-16") + ); + assert_eq!( + serde_json::to_value(&PositionEncodingKind::Utf32).unwrap(), + json!("utf-32") + ); + assert_eq!( + serde_json::to_value(&PositionEncodingKind::Utf8).unwrap(), + json!("utf-8") + ); + + assert_eq!( + serde_json::from_value::(json!("utf-16")).unwrap(), + PositionEncodingKind::Utf16 + ); + assert_eq!( + serde_json::from_value::(json!("utf-32")).unwrap(), + PositionEncodingKind::Utf32 + ); + assert_eq!( + serde_json::from_value::(json!("utf-8")).unwrap(), + PositionEncodingKind::Utf8 + ); + } + + #[test] + fn test_agent_nes_capabilities_serialization() { + let caps = NesCapabilities::new() + .events( + NesEventCapabilities::new().document( + NesDocumentEventCapabilities::new() + .did_open(NesDocumentDidOpenCapabilities::default()) + .did_change(NesDocumentDidChangeCapabilities::new( + TextDocumentSyncKind::Incremental, + )) + .did_close(NesDocumentDidCloseCapabilities::default()) + .did_save(NesDocumentDidSaveCapabilities::default()) + .did_focus(NesDocumentDidFocusCapabilities::default()), + ), + ) + .context( + NesContextCapabilities::new() + .recent_files(NesRecentFilesCapabilities { + max_count: Some(10), + meta: None, + }) + .related_snippets(NesRelatedSnippetsCapabilities::default()) + .edit_history(NesEditHistoryCapabilities { + max_count: Some(6), + meta: None, + }) + .user_actions(NesUserActionsCapabilities { + max_count: Some(16), + meta: None, + }) + .open_files(NesOpenFilesCapabilities::default()) + .diagnostics(NesDiagnosticsCapabilities::default()), + ); + + let json = serde_json::to_value(&caps).unwrap(); + assert_eq!( + json, + json!({ + "events": { + "document": { + "didOpen": {}, + "didChange": { + "syncKind": "incremental" + }, + "didClose": {}, + "didSave": {}, + "didFocus": {} + } + }, + "context": { + "recentFiles": { + "maxCount": 10 + }, + "relatedSnippets": {}, + "editHistory": { + "maxCount": 6 + }, + "userActions": { + "maxCount": 16 + }, + "openFiles": {}, + "diagnostics": {} + } + }) + ); + + // Round-trip + let deserialized: NesCapabilities = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, caps); + } + + #[test] + fn test_client_nes_capabilities_serialization() { + let caps = ClientNesCapabilities::new() + .jump(NesJumpCapabilities::default()) + .rename(NesRenameActionCapabilities::default()) + .search_and_replace(NesSearchAndReplaceActionCapabilities::default()); + + let json = serde_json::to_value(&caps).unwrap(); + assert_eq!( + json, + json!({ + "jump": {}, + "rename": {}, + "searchAndReplace": {} + }) + ); + + let deserialized: ClientNesCapabilities = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, caps); + } + + #[test] + fn test_document_did_open_serialization() { + let notification = DocumentDidOpenNotification::new( + "session_123", + "file:///path/to/file.rs", + "rust", + 1, + "fn main() {\n println!(\"hello\");\n}\n", + ); + + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "languageId": "rust", + "version": 1, + "text": "fn main() {\n println!(\"hello\");\n}\n" + }) + ); + + let deserialized: DocumentDidOpenNotification = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, notification); + } + + #[test] + fn test_document_did_change_incremental_serialization() { + let notification = DocumentDidChangeNotification::new( + "session_123", + "file:///path/to/file.rs", + 2, + vec![TextDocumentContentChangeEvent::incremental( + Range::new(Position::new(1, 4), Position::new(1, 4)), + "let x = 42;\n ", + )], + ); + + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "contentChanges": [ + { + "range": { + "start": { "line": 1, "character": 4 }, + "end": { "line": 1, "character": 4 } + }, + "text": "let x = 42;\n " + } + ] + }) + ); + } + + #[test] + fn test_document_did_change_full_serialization() { + let notification = DocumentDidChangeNotification::new( + "session_123", + "file:///path/to/file.rs", + 2, + vec![TextDocumentContentChangeEvent::full( + "fn main() {\n let x = 42;\n println!(\"hello\");\n}\n", + )], + ); + + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "contentChanges": [ + { + "text": "fn main() {\n let x = 42;\n println!(\"hello\");\n}\n" + } + ] + }) + ); + } + + #[test] + fn test_document_did_close_serialization() { + let notification = + DocumentDidCloseNotification::new("session_123", "file:///path/to/file.rs"); + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ "sessionId": "session_123", "uri": "file:///path/to/file.rs" }) + ); + } + + #[test] + fn test_document_did_save_serialization() { + let notification = + DocumentDidSaveNotification::new("session_123", "file:///path/to/file.rs"); + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ "sessionId": "session_123", "uri": "file:///path/to/file.rs" }) + ); + } + + #[test] + fn test_document_did_focus_serialization() { + let notification = DocumentDidFocusNotification::new( + "session_123", + "file:///path/to/file.rs", + 2, + Position::new(5, 12), + Range::new(Position::new(0, 0), Position::new(45, 0)), + ); + + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "position": { "line": 5, "character": 12 }, + "visibleRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 45, "character": 0 } + } + }) + ); + } + + #[test] + fn test_nes_suggestion_edit_serialization() { + let suggestion = NesSuggestion::Edit( + NesEditSuggestion::new( + "sugg_001", + "file:///path/to/other_file.rs", + vec![NesTextEdit::new( + Range::new(Position::new(5, 0), Position::new(5, 10)), + "let result = helper();", + )], + ) + .cursor_position(Position::new(5, 22)), + ); + + let json = serde_json::to_value(&suggestion).unwrap(); + assert_eq!( + json, + json!({ + "kind": "edit", + "id": "sugg_001", + "uri": "file:///path/to/other_file.rs", + "edits": [ + { + "range": { + "start": { "line": 5, "character": 0 }, + "end": { "line": 5, "character": 10 } + }, + "newText": "let result = helper();" + } + ], + "cursorPosition": { "line": 5, "character": 22 } + }) + ); + + let deserialized: NesSuggestion = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, suggestion); + } + + #[test] + fn test_nes_suggestion_jump_serialization() { + let suggestion = NesSuggestion::Jump(NesJumpSuggestion::new( + "sugg_002", + "file:///path/to/other_file.rs", + Position::new(15, 4), + )); + + let json = serde_json::to_value(&suggestion).unwrap(); + assert_eq!( + json, + json!({ + "kind": "jump", + "id": "sugg_002", + "uri": "file:///path/to/other_file.rs", + "position": { "line": 15, "character": 4 } + }) + ); + + let deserialized: NesSuggestion = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, suggestion); + } + + #[test] + fn test_nes_suggestion_rename_action_serialization() { + let suggestion = NesSuggestion::Rename(NesRenameActionSuggestion::new( + "sugg_003", + "file:///path/to/file.rs", + Position::new(5, 10), + "calculateTotal", + )); + + let json = serde_json::to_value(&suggestion).unwrap(); + assert_eq!( + json, + json!({ + "kind": "rename", + "id": "sugg_003", + "uri": "file:///path/to/file.rs", + "position": { "line": 5, "character": 10 }, + "newName": "calculateTotal" + }) + ); + + let deserialized: NesSuggestion = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, suggestion); + } + + #[test] + fn test_nes_suggestion_search_and_replace_action_serialization() { + let suggestion = NesSuggestion::SearchAndReplace( + NesSearchAndReplaceActionSuggestion::new( + "sugg_004", + "file:///path/to/file.rs", + "oldFunction", + "newFunction", + ) + .is_regex(false), + ); + + let json = serde_json::to_value(&suggestion).unwrap(); + assert_eq!( + json, + json!({ + "kind": "searchAndReplace", + "id": "sugg_004", + "uri": "file:///path/to/file.rs", + "search": "oldFunction", + "replace": "newFunction", + "isRegex": false + }) + ); + + let deserialized: NesSuggestion = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, suggestion); + } + + #[test] + fn test_nes_start_request_serialization() { + let request = NesStartRequest::new() + .workspace_uri("file:///Users/alice/projects/my-app") + .workspace_folders(vec![WorkspaceFolder::new( + "file:///Users/alice/projects/my-app", + "my-app", + )]) + .repository(NesRepository::new( + "my-app", + "alice", + "https://github.com/alice/my-app.git", + )); + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!( + json, + json!({ + "workspaceUri": "file:///Users/alice/projects/my-app", + "workspaceFolders": [ + { + "uri": "file:///Users/alice/projects/my-app", + "name": "my-app" + } + ], + "repository": { + "name": "my-app", + "owner": "alice", + "remoteUrl": "https://github.com/alice/my-app.git" + } + }) + ); + } + + #[test] + fn test_nes_start_response_serialization() { + let response = NesStartResponse::new("nes_abc123"); + let json = serde_json::to_value(&response).unwrap(); + assert_eq!(json, json!({ "sessionId": "nes_abc123" })); + } + + #[test] + fn test_nes_trigger_kind_serialization() { + assert_eq!( + serde_json::to_value(&NesTriggerKind::Automatic).unwrap(), + json!("automatic") + ); + assert_eq!( + serde_json::to_value(&NesTriggerKind::Diagnostic).unwrap(), + json!("diagnostic") + ); + assert_eq!( + serde_json::to_value(&NesTriggerKind::Manual).unwrap(), + json!("manual") + ); + } + + #[test] + fn test_nes_reject_reason_serialization() { + assert_eq!( + serde_json::to_value(&NesRejectReason::Rejected).unwrap(), + json!("rejected") + ); + assert_eq!( + serde_json::to_value(&NesRejectReason::Ignored).unwrap(), + json!("ignored") + ); + assert_eq!( + serde_json::to_value(&NesRejectReason::Replaced).unwrap(), + json!("replaced") + ); + assert_eq!( + serde_json::to_value(&NesRejectReason::Cancelled).unwrap(), + json!("cancelled") + ); + } + + #[test] + fn test_nes_accept_notification_serialization() { + let notification = NesAcceptNotification::new("session_123", "sugg_001"); + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ "sessionId": "session_123", "id": "sugg_001" }) + ); + } + + #[test] + fn test_nes_reject_notification_serialization() { + let notification = + NesRejectNotification::new("session_123", "sugg_001").reason(NesRejectReason::Rejected); + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!( + json, + json!({ "sessionId": "session_123", "id": "sugg_001", "reason": "rejected" }) + ); + } + + #[test] + fn test_nes_suggest_request_with_context_serialization() { + let request = NesSuggestRequest::new( + "session_123", + "file:///path/to/file.rs", + 2, + Position::new(5, 12), + NesTriggerKind::Automatic, + ) + .selection(Range::new(Position::new(5, 4), Position::new(5, 12))) + .context( + NesSuggestContext::new() + .recent_files(vec![NesRecentFile::new( + "file:///path/to/utils.rs", + "rust", + "pub fn helper() -> i32 { 42 }\n", + )]) + .diagnostics(vec![NesDiagnostic::new( + "file:///path/to/file.rs", + Range::new(Position::new(5, 0), Position::new(5, 10)), + NesDiagnosticSeverity::Error, + "cannot find value `foo` in this scope", + )]), + ); + + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["sessionId"], "session_123"); + assert_eq!(json["uri"], "file:///path/to/file.rs"); + assert_eq!(json["version"], 2); + assert_eq!(json["triggerKind"], "automatic"); + assert_eq!( + json["context"]["recentFiles"][0]["uri"], + "file:///path/to/utils.rs" + ); + assert_eq!(json["context"]["diagnostics"][0]["severity"], "error"); + } + + #[test] + fn test_text_document_sync_kind_serialization() { + assert_eq!( + serde_json::to_value(&TextDocumentSyncKind::Full).unwrap(), + json!("full") + ); + assert_eq!( + serde_json::to_value(&TextDocumentSyncKind::Incremental).unwrap(), + json!("incremental") + ); + } + + #[test] + fn test_document_did_change_capabilities_requires_sync_kind() { + assert!(serde_json::from_value::(json!({})).is_err()); + } + + #[test] + fn test_nes_suggest_response_serialization() { + let response = NesSuggestResponse::new(vec![ + NesSuggestion::Edit(NesEditSuggestion::new( + "sugg_001", + "file:///path/to/file.rs", + vec![NesTextEdit::new( + Range::new(Position::new(5, 0), Position::new(5, 10)), + "let result = helper();", + )], + )), + NesSuggestion::Jump(NesJumpSuggestion::new( + "sugg_002", + "file:///path/to/other.rs", + Position::new(10, 0), + )), + ]); + + let json = serde_json::to_value(&response).unwrap(); + assert_eq!(json["suggestions"].as_array().unwrap().len(), 2); + assert_eq!(json["suggestions"][0]["kind"], "edit"); + assert_eq!(json["suggestions"][1]["kind"], "jump"); + } +} diff --git a/src/rpc.rs b/src/rpc.rs index 868087ee..47c4ec1e 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -10,6 +10,9 @@ use crate::{ ClientNotification, ClientRequest, ClientResponse, Error, ExtNotification, ExtRequest, Result, }; +#[cfg(feature = "unstable_nes")] +use crate::NES_METHOD_NAMES; + /// JSON RPC Request Id /// /// An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] @@ -324,6 +327,18 @@ impl Side for AgentSide { m if m == AGENT_METHOD_NAMES.session_prompt => serde_json::from_str(params.get()) .map(ClientRequest::PromptRequest) .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.nes_start => serde_json::from_str(params.get()) + .map(ClientRequest::NesStartRequest) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.nes_suggest => serde_json::from_str(params.get()) + .map(ClientRequest::NesSuggestRequest) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.nes_close => serde_json::from_str(params.get()) + .map(ClientRequest::NesCloseRequest) + .map_err(Into::into), _ => { if let Some(custom_method) = method.strip_prefix('_') { Ok(ClientRequest::ExtMethodRequest(ExtRequest { @@ -344,6 +359,34 @@ impl Side for AgentSide { m if m == AGENT_METHOD_NAMES.session_cancel => serde_json::from_str(params.get()) .map(ClientNotification::CancelNotification) .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.document_did_open => serde_json::from_str(params.get()) + .map(ClientNotification::DocumentDidOpenNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.document_did_change => serde_json::from_str(params.get()) + .map(ClientNotification::DocumentDidChangeNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.document_did_close => serde_json::from_str(params.get()) + .map(ClientNotification::DocumentDidCloseNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.document_did_save => serde_json::from_str(params.get()) + .map(ClientNotification::DocumentDidSaveNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.document_did_focus => serde_json::from_str(params.get()) + .map(ClientNotification::DocumentDidFocusNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.nes_accept => serde_json::from_str(params.get()) + .map(ClientNotification::NesAcceptNotification) + .map_err(Into::into), + #[cfg(feature = "unstable_nes")] + m if m == NES_METHOD_NAMES.nes_reject => serde_json::from_str(params.get()) + .map(ClientNotification::NesRejectNotification) + .map_err(Into::into), _ => { if let Some(custom_method) = method.strip_prefix('_') { Ok(ClientNotification::ExtNotification(ExtNotification { @@ -412,6 +455,160 @@ mod tests { } } +#[cfg(feature = "unstable_nes")] +#[cfg(test)] +mod nes_rpc_tests { + use super::*; + use serde_json::json; + + #[test] + fn test_decode_nes_start_request() { + let params = serde_json::to_string(&json!({ + "workspaceUri": "file:///Users/alice/projects/my-app", + "workspaceFolders": [ + { "uri": "file:///Users/alice/projects/my-app", "name": "my-app" } + ] + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let request = AgentSide::decode_request("nes/start", Some(&raw)).unwrap(); + assert!(matches!(request, ClientRequest::NesStartRequest(_))); + } + + #[test] + fn test_decode_nes_suggest_request() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "position": { "line": 5, "character": 12 }, + "triggerKind": "automatic" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let request = AgentSide::decode_request("nes/suggest", Some(&raw)).unwrap(); + assert!(matches!(request, ClientRequest::NesSuggestRequest(_))); + } + + #[test] + fn test_decode_document_did_open_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "languageId": "rust", + "version": 1, + "text": "fn main() {}" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("document/didOpen", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::DocumentDidOpenNotification(_) + )); + } + + #[test] + fn test_decode_document_did_change_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "contentChanges": [{ "text": "fn main() { let x = 1; }" }] + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = + AgentSide::decode_notification("document/didChange", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::DocumentDidChangeNotification(_) + )); + } + + #[test] + fn test_decode_document_did_close_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("document/didClose", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::DocumentDidCloseNotification(_) + )); + } + + #[test] + fn test_decode_document_did_save_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("document/didSave", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::DocumentDidSaveNotification(_) + )); + } + + #[test] + fn test_decode_document_did_focus_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "uri": "file:///path/to/file.rs", + "version": 2, + "position": { "line": 5, "character": 12 }, + "visibleRange": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 45, "character": 0 } + } + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("document/didFocus", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::DocumentDidFocusNotification(_) + )); + } + + #[test] + fn test_decode_nes_accept_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "id": "sugg_001" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("nes/accept", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::NesAcceptNotification(_) + )); + } + + #[test] + fn test_decode_nes_reject_notification() { + let params = serde_json::to_string(&json!({ + "sessionId": "session_123", + "id": "sugg_001", + "reason": "rejected" + })) + .unwrap(); + let raw = serde_json::value::RawValue::from_string(params).unwrap(); + let notification = AgentSide::decode_notification("nes/reject", Some(&raw)).unwrap(); + assert!(matches!( + notification, + ClientNotification::NesRejectNotification(_) + )); + } +} + #[test] fn test_notification_wire_format() { use super::*;