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;