Skip to main content

vtcode_desktop_protocol/
turn.rs

1//! Turn submission, acceptance, cancellation, and queue DTOs.
2
3use serde::{Deserialize, Serialize};
4
5/// A turn was rejected before delivery because a startup interaction is pending.
6pub const STARTUP_INTERACTION_PENDING_ERROR: i32 = -32001;
7
8/// Fallback turn id for interactions raised with no active turn (startup
9/// decisions). Produced by the service, matched by the desktop client.
10pub const SESSION_START_TURN_ID: &str = "session-start";
11
12/// Maximum queued-message previews carried by one queue-status notification.
13/// Mirrors the CLI sidebar, which renders the first few queued inputs.
14pub const MAX_QUEUE_STATUS_PREVIEWS: usize = 5;
15
16/// Maximum characters per queued-message preview before truncation.
17/// Mirrors the CLI sidebar preview width.
18pub const MAX_QUEUE_PREVIEW_CHARS: usize = 50;
19
20/// Bounded identity token for one queue entry, minted by the service so the
21/// client can address one message for take-latest and remove without relying
22/// on positional order (which races against concurrent submits).
23pub const MAX_QUEUE_ENTRY_ID_LEN: usize = 64;
24
25/// Build the display preview for one queued message: the full text when it
26/// fits, otherwise the first [`MAX_QUEUE_PREVIEW_CHARS`] characters plus an
27/// ellipsis. Truncation is char-boundary safe; newlines are preserved for the
28/// client to render.
29#[must_use]
30pub fn queue_preview_label(text: &str) -> String {
31    let mut preview = String::new();
32    for (index, ch) in text.chars().enumerate() {
33        if index >= MAX_QUEUE_PREVIEW_CHARS {
34            preview.push('\u{2026}');
35            return preview;
36        }
37        preview.push(ch);
38    }
39    preview
40}
41
42/// Parameters for submitting a prompt to the selected session.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TurnSubmitParams {
46    /// Session that owns the new turn.
47    pub session_id: String,
48    /// User prompt.
49    pub prompt: String,
50    /// Inline text attachments read by the client, applied as content parts.
51    #[serde(default)]
52    pub attachments: Vec<TurnAttachment>,
53    /// Whether a busy-time submission may merge with adjacent queued text-only
54    /// submissions into one turn. Mirrors the CLI Ctrl+Enter batchable flag;
55    /// submissions carrying attachments always dispatch alone.
56    #[serde(default)]
57    pub batchable: bool,
58}
59
60/// One client-read text attachment for a submitted turn.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct TurnAttachment {
64    /// Display path used for labeling: workspace-relative for workspace
65    /// files, absolute for files attached from anywhere else.
66    pub path: String,
67    /// Bounded file content.
68    pub text: String,
69}
70
71/// Accepted turn response.
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct TurnAccepted {
75    /// Session identifier.
76    pub session_id: String,
77    /// Runtime turn identifier. Empty when `queued` is set: a queued prompt
78    /// owns no turn until its batch dispatches.
79    pub turn_id: String,
80    /// Whether the prompt was queued behind a running turn instead of
81    /// starting immediately. Absent (legacy services) means immediate.
82    #[serde(default)]
83    pub queued: bool,
84    /// Queue depth after this acceptance.
85    #[serde(default)]
86    pub queue_depth: usize,
87    /// Oldest messages discarded by the queue cap while accepting this one.
88    #[serde(default)]
89    pub dropped: usize,
90}
91
92/// Parameters for cancelling an active turn.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct TurnCancelParams {
96    /// Owning session.
97    pub session_id: String,
98    /// Exact turn identity to cancel.
99    pub turn_id: String,
100    /// Discard queued turns behind the cancelled one (running-switch flow:
101    /// the terminal pump auto-dispatches the queue, so a deferred session
102    /// switch can only land on an empty queue). Absent (legacy clients)
103    /// means keep the queue: a plain stop lets queued prompts run
104    /// afterwards (TUI parity).
105    #[serde(default)]
106    pub clear_queue: bool,
107    /// Skip the cooperative-cancellation grace period and start the
108    /// teardown escalation immediately. The desktop client sets this on a
109    /// repeat Stop press while a cancel is already outstanding; the first
110    /// press stays cooperative so a healthy turn stops without a runtime
111    /// restart. Absent (legacy clients) means cooperative with the
112    /// standard grace backstop.
113    #[serde(default)]
114    pub force: bool,
115}
116
117/// Parameters for reversing one turn's exact file changes.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct TurnUndoParams {
121    /// Session that owns the turn.
122    pub session_id: String,
123    /// Canonical turn identity (event `context.turn_id`).
124    pub turn_id: String,
125}
126
127/// One reversed file change.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct TurnUndoFile {
131    /// Workspace-relative path.
132    pub path: String,
133    /// Kind of the reversed change.
134    pub kind: vtcode_exec_events::PatchChangeKind,
135    /// Reversed added-line count.
136    #[serde(default)]
137    pub additions: u64,
138    /// Reversed deleted-line count.
139    #[serde(default)]
140    pub deletions: u64,
141}
142
143/// Outcome of a turn undo request.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct TurnUndoResult {
147    /// Session that owns the turn.
148    pub session_id: String,
149    /// Canonical turn identity.
150    pub turn_id: String,
151    /// Whether every recorded change was reversed. `false` carries the
152    /// refusal reason; nothing is partially reversed without disclosure.
153    pub undone: bool,
154    /// Reversed changes in recorded order: empty on clean refusals, holding
155    /// the applied prefix when a later file aborts the flush.
156    #[serde(default)]
157    pub files: Vec<TurnUndoFile>,
158    /// Refusal or failure explanation; empty on success.
159    #[serde(default)]
160    pub reason: String,
161}
162
163/// Parameters for popping the newest queued message back for editing.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct TurnQueueTakeLatestParams {
167    /// Session that owns the queue.
168    pub session_id: String,
169}
170
171/// Result of popping the newest queued message.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct TurnQueueTakeLatestResult {
175    /// Session that owns the queue.
176    pub session_id: String,
177    /// Popped message text, or `None` when the queue was empty.
178    #[serde(default)]
179    pub text: Option<String>,
180    /// Queue depth after the pop.
181    #[serde(default)]
182    pub queue_depth: usize,
183}
184
185/// Authoritative queued-message snapshot published by the service.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TurnQueueStatus {
189    /// Session that owns the queue.
190    pub session_id: String,
191    /// Number of messages waiting for dispatch.
192    pub queue_depth: usize,
193    /// Display previews for the oldest queued messages, oldest first, up to
194    /// [`MAX_QUEUE_STATUS_PREVIEWS`].
195    #[serde(default)]
196    pub previews: Vec<String>,
197    /// Entry identity tokens matching `previews` one-to-one (when the service
198    /// supplies them). Absent or shorter (legacy services) means the client
199    /// falls back to newest-first take-latest and hides per-row actions.
200    #[serde(default)]
201    pub entry_ids: Vec<String>,
202    /// Seconds since each entry was queued, oldest first, matching `previews`
203    /// one-to-one. Absent (legacy services) hides the elapsed-time column.
204    #[serde(default)]
205    pub queued_for_secs: Vec<u64>,
206    /// Total oldest messages discarded by the queue cap so far.
207    #[serde(default)]
208    pub dropped_total: usize,
209}
210
211/// Parameters for removing one queued message by entry id.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct TurnQueueRemoveParams {
215    /// Session that owns the queue.
216    pub session_id: String,
217    /// Entry identity token from the latest `turn/queue-status` snapshot.
218    pub entry_id: String,
219}
220
221/// Outcome of a remove-one-queued-message request.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct TurnQueueRemoveResult {
225    /// Session that owns the queue.
226    pub session_id: String,
227    /// Whether the entry id matched a queued message and it was removed.
228    pub removed: bool,
229    /// Queue depth after the removal.
230    pub queue_depth: usize,
231}