Skip to main content

scv_protocol/
workbench.rs

1//! The agent workbench: what SCV knows about delegated agents, background
2//! jobs, and their sessions across the whole instance, as a dashboard reads
3//! it ([`WorkbenchSnapshot`]), the activity a delegated run records for
4//! its live view ([`RunEvent`], [`RunLogPage`]), and the screen of a
5//! conversation the owner took over in its agent's own terminal
6//! ([`AttachPage`]).
7//!
8//! Every type here is additive to protocol version 3: a daemon sends them
9//! only in answer to the `workbench*`, `run_log`, `job_result`,
10//! `worktree*`, and `attach*` `daemon.control` actions, which only newer
11//! clients send.
12
13use serde::{Deserialize, Serialize};
14
15use crate::review::JobOutcome;
16
17/// What a delegated run, a background job, or a session is doing, as far as
18/// SCV can tell. A state a client does not know parses as `unknown`, which
19/// also means SCV itself cannot tell.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum AgentState {
23    /// Launching: starting the process, its handshake, or its session.
24    Starting,
25    /// A turn runs and the agent reports activity.
26    Working,
27    /// The agent waits for a person: an approval request it relayed, or the
28    /// session's own approval request, is unanswered.
29    AwaitingInput,
30    /// A live agent (an ACP server or nested SCV) that waits between turns
31    /// for its next prompt; or a session with no turn running.
32    Idle,
33    /// A prompt that waits for its conversation's running turn.
34    Queued,
35    /// The turn finished and the agent completed its task.
36    Completed,
37    /// The run or job failed.
38    Failed,
39    /// The agent's model refused the request.
40    Declined,
41    /// The run reached its time limit.
42    Timeout,
43    /// Stopped on request.
44    Cancelled,
45    /// Its SCV process or session stopped before it finished: a crash, a
46    /// restart, or the session's client going away.
47    Interrupted,
48    /// SCV cannot tell: a turn has reported nothing for a long while, or the
49    /// process that owns it cannot be observed. Also any state this client
50    /// does not know.
51    #[serde(other)]
52    Unknown,
53}
54
55impl AgentState {
56    /// The state as SCV writes it, such as `awaiting_input`.
57    pub fn as_str(self) -> &'static str {
58        match self {
59            Self::Starting => "starting",
60            Self::Working => "working",
61            Self::AwaitingInput => "awaiting_input",
62            Self::Idle => "idle",
63            Self::Queued => "queued",
64            Self::Completed => "completed",
65            Self::Failed => "failed",
66            Self::Declined => "declined",
67            Self::Timeout => "timeout",
68            Self::Cancelled => "cancelled",
69            Self::Interrupted => "interrupted",
70            Self::Unknown => "unknown",
71        }
72    }
73
74    /// Whether nothing more happens in this state by itself: the run or job
75    /// has ended one way or another.
76    pub fn is_final(self) -> bool {
77        matches!(
78            self,
79            Self::Completed
80                | Self::Failed
81                | Self::Declined
82                | Self::Timeout
83                | Self::Cancelled
84                | Self::Interrupted
85        )
86    }
87
88    /// Whether a person is needed to go on.
89    pub fn needs_attention(self) -> bool {
90        matches!(self, Self::AwaitingInput)
91    }
92}
93
94impl std::fmt::Display for AgentState {
95    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        formatter.write_str(self.as_str())
97    }
98}
99
100/// Who started a background job.
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum JobOrigin {
104    /// The session's model, through an `agent` call.
105    #[default]
106    Model,
107    /// The owner, from the workbench (a dashboard or `scv jobs`).
108    Owner,
109    /// An origin this client does not know.
110    #[serde(other)]
111    Unknown,
112}
113
114/// A git worktree SCV created for one job, so its agent works apart from
115/// the repository's own checkout.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct WorktreeRef {
118    /// The worktree's directory, under `$SCV_HOME/worktrees`.
119    pub path: String,
120    /// The branch SCV created for it, such as `scv/codex-3f9a2c`.
121    pub branch: String,
122    /// The repository it was created from.
123    pub repo: String,
124}
125
126/// A worktree SCV created, as `scv agents worktrees` lists it.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct WorktreeInfo {
129    /// Where it is, its branch, and the repository it belongs to.
130    #[serde(flatten)]
131    pub worktree: WorktreeRef,
132    /// Whether it has uncommitted changes; absent when git could not tell.
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub dirty: Option<bool>,
135    /// The jobs recorded as working in it, as `<session>/<job>`.
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub jobs: Vec<String>,
138}
139
140/// One SCV session the workbench knows: a live daemon session, the
141/// workbench's own host for jobs the owner starts, or one that ended.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct WorkbenchSession {
144    /// The session ID.
145    pub id: String,
146    /// Who it serves, such as `TUI`, `WeChat`, `scv exec`, or `workbench`.
147    pub label: String,
148    /// Its workspace directory.
149    pub workspace: String,
150    /// Whether it still runs in a process of this instance.
151    pub live: bool,
152    /// What it is doing, when it is live.
153    pub state: AgentState,
154    /// SCV's words for that state, such as which approval it waits for.
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub detail: Option<String>,
157}
158
159/// One background job, from the instance's job ledger and, while it runs,
160/// its live state.
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct WorkbenchJob {
163    /// The session that owns it.
164    pub session: String,
165    /// Its handle in that session, such as `job-3`.
166    pub job: String,
167    /// The agent that runs it, such as `codex`.
168    pub agent: String,
169    /// The first line of its prompt, shortened.
170    pub task: String,
171    /// The session's workspace.
172    pub workspace: String,
173    /// Where its agent runs: a directory in the workspace, or its worktree.
174    pub cwd: String,
175    /// What it is doing.
176    pub state: AgentState,
177    /// SCV's words for that state.
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub detail: Option<String>,
180    /// Who started it.
181    pub started_by: JobOrigin,
182    /// The conversation it runs a turn of, such as `codex-2`, once known.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub conversation: Option<String>,
185    /// The delegated run serving it now, or that served it last.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub run: Option<String>,
188    /// The worktree SCV created for it.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub worktree: Option<WorktreeRef>,
191    /// A reviewed job (`review`), whose journal holds its rounds.
192    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
193    pub reviewed: bool,
194    /// The owner can resume it from the workbench.
195    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
196    pub resumable: bool,
197    /// The job it was resumed as, `<session>/<job>`, once it was.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub resumed_as: Option<String>,
200    /// Whether the owner can take `conversation` over in its agent's own
201    /// terminal now, and why not. Absent without a conversation the owner
202    /// can reach, and from daemons that predate it.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub attach: Option<AttachOffer>,
205    /// When it was started.
206    pub created_unix_seconds: u64,
207    /// When its record last changed.
208    pub updated_unix_seconds: u64,
209}
210
211/// One delegated run (an agent process) and what it is doing.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct WorkbenchRun {
214    /// Its delegation handle, such as `codex-3f9a2c`; its run log's name.
215    pub handle: String,
216    /// The agent, such as `codex`.
217    pub agent: String,
218    /// The session that started it.
219    pub session: String,
220    /// How SCV reaches it: `cli` (one process per turn), `acp`, or `scv`.
221    pub transport: String,
222    /// The conversation it serves, such as `codex-2`.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub conversation: Option<String>,
225    /// The turn of that conversation it serves, or served last.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub turn: Option<u32>,
228    /// The background job it serves, when it serves one.
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub job: Option<String>,
231    /// Every background job its turns served, oldest first: a live
232    /// conversation's run serves several.
233    #[serde(default, skip_serializing_if = "Vec::is_empty")]
234    pub jobs: Vec<String>,
235    /// Its working directory.
236    pub cwd: String,
237    /// What it is doing.
238    pub state: AgentState,
239    /// SCV's words for that state, such as the approval it waits for or
240    /// how long it has been quiet.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub detail: Option<String>,
243    /// When it started.
244    pub started_unix_seconds: u64,
245    /// When it last reported anything.
246    pub last_activity_unix_seconds: u64,
247    /// Its process still runs.
248    pub live: bool,
249    /// A running turn of it can take a further prompt (ACP steering).
250    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
251    pub steerable: bool,
252    /// Whether the owner can take `conversation` over in its agent's own
253    /// terminal now, and why not; absent as on [`WorkbenchJob::attach`].
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub attach: Option<AttachOffer>,
256}
257
258/// Where a conversation stands for the owner's takeover of it in its
259/// agent's own terminal (`attach`).
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
261#[serde(rename_all = "snake_case")]
262pub enum AttachState {
263    /// It is idle: `attach` takes it over.
264    Ready,
265    /// It is taken over: `attach` attaches to its terminal, taking the
266    /// hold from any other client.
267    TakenOver,
268    /// A turn runs, or turns wait: `attach` is refused. With `interrupt`,
269    /// when [`AttachOffer::interrupt`] says so, it stops the running turn
270    /// first.
271    Busy,
272    /// Its agent's terminal is never offered (only an agent whose terminal
273    /// asks no approval is).
274    Ineligible,
275    /// It has no session of the agent's own to open yet: its first turn has
276    /// not ended.
277    NoSession,
278    /// A reviewed job holds it.
279    Reviewed,
280    /// As many takeovers as may run at once already run.
281    Full,
282    /// A state this client does not know.
283    #[serde(other)]
284    Unknown,
285}
286
287impl AttachState {
288    /// The state as SCV writes it, such as `taken_over`.
289    pub fn as_str(self) -> &'static str {
290        match self {
291            Self::Ready => "ready",
292            Self::TakenOver => "taken_over",
293            Self::Busy => "busy",
294            Self::Ineligible => "ineligible",
295            Self::NoSession => "no_session",
296            Self::Reviewed => "reviewed",
297            Self::Full => "full",
298            Self::Unknown => "unknown",
299        }
300    }
301}
302
303/// Whether the owner can take a conversation over in its agent's own
304/// terminal now, as `attach` would answer it at this moment.
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306pub struct AttachOffer {
307    /// Where it stands.
308    pub state: AttachState,
309    /// SCV's words for why `attach` is refused, as it would refuse it.
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub detail: Option<String>,
312    /// `attach` with `interrupt` would stop the job running its turn and
313    /// then take it over.
314    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
315    pub interrupt: bool,
316    /// The background job running its turn, such as `job-3`, which
317    /// `interrupt` stops.
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub job: Option<String>,
320}
321
322impl AttachOffer {
323    /// Whether `attach` (without `interrupt`) goes ahead.
324    pub fn allowed(&self) -> bool {
325        matches!(self.state, AttachState::Ready | AttachState::TakenOver)
326    }
327}
328
329/// An approval request a background job relayed while the owner watches the
330/// workbench, waiting for the owner's answer.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct WorkbenchApproval {
333    /// Answer with `workbench_approve` naming this ID.
334    pub id: String,
335    /// The session whose job asks.
336    pub session: String,
337    /// The job that asks.
338    pub job: String,
339    /// The tool the request is for, as the nested agent named it.
340    pub name: String,
341    /// Its risk, such as `process`.
342    pub risk: String,
343    /// The working directory it concerns.
344    pub cwd: String,
345    /// What it will do.
346    pub summary: String,
347    /// When it was asked.
348    pub since_unix_seconds: u64,
349    /// When it gets the answer the session gives unattended (a denial).
350    pub expires_unix_seconds: u64,
351}
352
353/// A session whose queued prompts SCV kept when its process stopped (a
354/// restart or crash), which a new session can restore.
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356pub struct RestorableSession {
357    /// The session that stopped.
358    pub id: String,
359    /// Who it served.
360    pub label: String,
361    /// Its workspace.
362    pub workspace: String,
363    /// Prompts that never started.
364    pub queued: u32,
365    /// The start of the prompt whose turn ran when it stopped, which is
366    /// never sent again by itself.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub interrupted: Option<String>,
369    /// When it was last saved.
370    pub saved_unix_seconds: u64,
371}
372
373/// Everything the workbench shows, at one moment.
374#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
375pub struct WorkbenchSnapshot {
376    /// When it was taken.
377    pub generated_unix_seconds: u64,
378    /// Sessions with jobs, runs, or live state.
379    #[serde(default, skip_serializing_if = "Vec::is_empty")]
380    pub sessions: Vec<WorkbenchSession>,
381    /// Background jobs, newest first.
382    #[serde(default, skip_serializing_if = "Vec::is_empty")]
383    pub jobs: Vec<WorkbenchJob>,
384    /// Delegated runs, newest first.
385    #[serde(default, skip_serializing_if = "Vec::is_empty")]
386    pub runs: Vec<WorkbenchRun>,
387    /// Approval requests waiting for the owner.
388    #[serde(default, skip_serializing_if = "Vec::is_empty")]
389    pub approvals: Vec<WorkbenchApproval>,
390    /// Sessions whose queued prompts can be restored.
391    #[serde(default, skip_serializing_if = "Vec::is_empty")]
392    pub restorable: Vec<RestorableSession>,
393}
394
395/// What one line of a run's activity log records.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
397#[serde(rename_all = "snake_case")]
398pub enum RunEventKind {
399    /// The run began: its agent, transport, and working directory.
400    RunStarted,
401    /// A turn began with a prompt.
402    TurnStarted,
403    /// A line of the agent's own message.
404    Text,
405    /// A tool call or command the agent started, as a short redacted line.
406    Tool,
407    /// A tool call that failed.
408    ToolFailed,
409    /// The agent's current plan step.
410    Plan,
411    /// The agent asked for approval.
412    ApprovalRequested,
413    /// That request was answered.
414    ApprovalResolved,
415    /// A prompt was added to the running turn.
416    Steered,
417    /// A turn ended.
418    TurnEnded,
419    /// The run's process ended.
420    RunEnded,
421    /// SCV's own note, such as why the run stopped.
422    Note,
423    /// The log reached its size limit; only turns, approvals, and the end
424    /// are recorded after this.
425    Truncated,
426    /// A kind this client does not know.
427    #[serde(other)]
428    Unknown,
429}
430
431/// One entry of a run's activity log. Text is display-only: agent messages
432/// and short tool lines with credential-like values redacted, never tool
433/// output.
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct RunEvent {
436    /// Its position in the log, from 1.
437    pub seq: u64,
438    /// When it happened, in Unix milliseconds.
439    pub ts_ms: u64,
440    /// What it records.
441    pub kind: RunEventKind,
442    /// Its text: the message line, tool line, prompt, summary, or note.
443    #[serde(default, skip_serializing_if = "String::is_empty")]
444    pub text: String,
445    /// The conversation turn it belongs to.
446    #[serde(default, skip_serializing_if = "Option::is_none")]
447    pub turn: Option<u32>,
448    /// How a turn or run ended, or an approval was answered (`approved`,
449    /// `denied`).
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub status: Option<String>,
452    /// The background job a turn serves.
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub job: Option<String>,
455    /// An approval request's risk.
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub risk: Option<String>,
458    /// Reserved for who sent a turn's prompt or answered an approval:
459    /// `model`, `owner`, or `policy`. Logs omit it until a writer records
460    /// that actor.
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub by: Option<String>,
463}
464
465/// The most bytes of a job's reply, and of its error, a [`JobResult`]
466/// carries; a longer one is cut at a character boundary and flagged
467/// `truncated`. The ledger itself keeps up to 8 KiB of each.
468pub const MAX_JOB_RESULT_BYTES: usize = 4 * 1024;
469
470/// A background job's result, for a `job_result` request: where it stands
471/// and, once it has ended, its agent's reply or error, bounded by
472/// [`MAX_JOB_RESULT_BYTES`]. The reply and error are untrusted
473/// delegated-agent text.
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
475pub struct JobResult {
476    /// The session that owns it.
477    pub session: String,
478    /// Its handle in that session, such as `job-3`.
479    pub job: String,
480    /// The agent that runs it, such as `codex`.
481    pub agent: String,
482    /// What it is doing, as the workbench lists it.
483    pub state: AgentState,
484    /// SCV's words for that state.
485    #[serde(default, skip_serializing_if = "Option::is_none")]
486    pub detail: Option<String>,
487    /// The agent's reply, once the job has ended with one.
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub reply: Option<String>,
490    /// Why it failed, once it has failed.
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub error: Option<String>,
493    /// The reply or the error was cut to [`MAX_JOB_RESULT_BYTES`].
494    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
495    pub truncated: bool,
496    /// A reviewed job's outcome, while the session that ran it still holds
497    /// the job.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub outcome: Option<JobOutcome>,
500    /// When it ended.
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub finished_unix_seconds: Option<u64>,
503}
504
505/// Part of a run's activity log, after a position.
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507pub struct RunLogPage {
508    /// The run, by delegation handle.
509    pub handle: String,
510    /// Its events after the requested position, oldest first.
511    #[serde(default, skip_serializing_if = "Vec::is_empty")]
512    pub events: Vec<RunEvent>,
513    /// Pass as `after` to read on: the last position returned, or the
514    /// requested one when nothing was new.
515    pub next: u64,
516    /// The run has ended, so nothing more will be logged.
517    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
518    pub ended: bool,
519    /// More events wait beyond this page.
520    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
521    pub more: bool,
522}
523
524/// How `workbench_send` hands the owner's prompt to a conversation that is
525/// running a turn.
526#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(rename_all = "snake_case")]
528pub enum SendMode {
529    /// Steer the running turn when its agent can take the prompt, else
530    /// queue it.
531    #[default]
532    Auto,
533    /// Queue it behind the running turn.
534    Queue,
535    /// Steer the running turn, or fail when it cannot be steered.
536    Steer,
537}
538
539/// A color of a cell of a taken-over terminal: one of the terminal's 256
540/// indexed colors, or a direct RGB color.
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
542#[serde(untagged)]
543pub enum AttachColor {
544    /// An indexed color: 0-15 are the terminal's own, 16-255 the xterm cube.
545    Index(u8),
546    /// A direct color, as `[red, green, blue]`.
547    Rgb([u8; 3]),
548}
549
550/// A run of cells of one row of a taken-over terminal, all in one style.
551/// Its text is display-only: printable characters, never an escape
552/// sequence, with values that look like credentials masked.
553#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
554pub struct AttachSpan {
555    /// The characters, one per cell (two for a wide one).
556    pub text: String,
557    /// Foreground color; absent is the terminal's default.
558    #[serde(default, skip_serializing_if = "Option::is_none")]
559    pub fg: Option<AttachColor>,
560    /// Background color; absent is the terminal's default.
561    #[serde(default, skip_serializing_if = "Option::is_none")]
562    pub bg: Option<AttachColor>,
563    /// Bold.
564    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
565    pub bold: bool,
566    /// Italic.
567    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
568    pub italic: bool,
569    /// Underlined.
570    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
571    pub underline: bool,
572    /// Foreground and background swapped.
573    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
574    pub inverse: bool,
575}
576
577/// One row of a taken-over terminal's screen.
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579pub struct AttachRow {
580    /// The row, from 0 at the top.
581    pub row: u16,
582    /// Its cells, left to right; cells past the last span are blank.
583    #[serde(default, skip_serializing_if = "Vec::is_empty")]
584    pub spans: Vec<AttachSpan>,
585}
586
587/// The screen of a conversation the owner took over in its agent's own
588/// terminal, as `attach` and `attach_io` return it: the rows that changed
589/// since the client's `after`, never the terminal's raw output.
590#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
591pub struct AttachPage {
592    /// This client's hold on the terminal: `attach_io` and `attach_detach`
593    /// name it. Another `attach` of the same conversation replaces it.
594    pub id: String,
595    /// The session that holds the conversation.
596    pub session: String,
597    /// The conversation taken over, such as `codex-2`.
598    pub conversation: String,
599    /// The agent, such as `codex`.
600    pub agent: String,
601    /// The takeover's delegated run, whose activity log records it.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub run: Option<String>,
604    /// Pass as `after` to read only what changes next.
605    pub generation: u64,
606    /// The screen's width, in cells.
607    pub cols: u16,
608    /// The screen's height, in rows.
609    pub rows: u16,
610    /// Every row whose content changed since `after`; all of them when
611    /// `full`.
612    #[serde(default, skip_serializing_if = "Vec::is_empty")]
613    pub lines: Vec<AttachRow>,
614    /// `lines` holds the whole screen: the client starts over from blank.
615    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
616    pub full: bool,
617    /// The cursor, as `[row, col]`; absent while the agent hides it.
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub cursor: Option<[u16; 2]>,
620    /// The agent asked for application cursor keys (`ESC O A` for Up).
621    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
622    pub application_cursor: bool,
623    /// The agent asked for bracketed paste.
624    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
625    pub bracketed_paste: bool,
626    /// The agent's terminal closed: its process exited or was stopped, so
627    /// the conversation goes back to SCV's turns.
628    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
629    pub ended: bool,
630    /// How it ended, such as `exit 0` or `stopped by the owner`.
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub exit: Option<String>,
633    /// SCV's sentence about input it could not deliver, such as an agent
634    /// that stopped reading its terminal.
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub note: Option<String>,
637}
638
639/// What `session.start` restored from a session that stopped.
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct RestoredQueue {
642    /// The session whose queue was restored.
643    pub from: String,
644    /// Queued prompts restored; the new session's queue starts paused.
645    pub queued: u32,
646    /// The start of the prompt that was running when the old session
647    /// stopped. It is not sent again: its outcome is unknown.
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub interrupted: Option<String>,
650    /// Queued prompts that could not be restored, because an attachment
651    /// was gone or the prompt was too long. Older servers omit it, and it
652    /// is left off the wire when none were.
653    #[serde(default, skip_serializing_if = "zero")]
654    pub dropped: u32,
655}
656
657#[allow(
658    clippy::trivially_copy_pass_by_ref,
659    reason = "serde skip_serializing_if passes the field by reference"
660)]
661fn zero(value: &u32) -> bool {
662    *value == 0
663}
664
665#[cfg(test)]
666mod tests;