Skip to main content

pi_ext/
protocol.rs

1//! Versioned extension-host / pi-tui protocol (UTF-8 JSONL frames).
2//!
3//! One frame is a single JSON object on one line, at most [`MAX_FRAME_BYTES`]
4//! UTF-8 bytes (excluding the trailing newline):
5//!
6//! ```json
7//! {"id":1,"kind":"req","method":"hello","payload":{"protocolVersion":1,"compatibilityVersion":"0.80.10"}}
8//! ```
9//!
10//! Rust is the authoritative validation boundary. TypeScript mirrors live in
11//! `packages/pi-tui-protocol` and share golden JSONL fixtures under that
12//! package's tests.
13
14use std::collections::BTreeMap;
15use std::fmt;
16use std::str;
17
18use serde::{Deserialize, Serialize};
19use serde_json::{Map, Value};
20use thiserror::Error;
21/// Host-control method that synchronizes validated extension flag values.
22pub const FLAGS_SET_METHOD: &str = Method::FlagsSet.as_str();
23
24/// Host-control method that dispatches one effective extension shortcut.
25pub const SHORTCUT_EXECUTE_METHOD: &str = Method::ShortcutExecute.as_str();
26
27// Local wire copies of overlay layout value types (camelCase). Kept here so
28// the protocol module does not depend on pi-tui compile health for validation.
29// Field names match `pi_tui::layout::{SizeValue, OverlayAnchor, OverlayMargin, OverlaySpec}`.
30
31/// Absolute cells or percent of a reference dimension.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum SizeValue {
34    /// Absolute size in terminal cells.
35    Cells(u16),
36    /// Percentage of the reference size (`0..=100`).
37    Percent(u8),
38}
39
40impl Serialize for SizeValue {
41    fn serialize<S: serde::Serializer>(
42        &self,
43        serializer: S,
44    ) -> std::result::Result<S::Ok, S::Error> {
45        match *self {
46            Self::Cells(n) => serializer.serialize_u16(n),
47            Self::Percent(n) => serializer.serialize_str(&format!("{n}%")),
48        }
49    }
50}
51
52impl<'de> Deserialize<'de> for SizeValue {
53    fn deserialize<D: serde::Deserializer<'de>>(
54        deserializer: D,
55    ) -> std::result::Result<Self, D::Error> {
56        struct Visitor;
57        impl serde::de::Visitor<'_> for Visitor {
58            type Value = SizeValue;
59            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60                f.write_str("a cell count number or a percent string like \"50%\"")
61            }
62            fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<Self::Value, E> {
63                u16::try_from(v)
64                    .map(SizeValue::Cells)
65                    .map_err(|_| E::custom("size value exceeds u16"))
66            }
67            fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<Self::Value, E> {
68                if v < 0 {
69                    return Err(E::custom("size value must be non-negative"));
70                }
71                self.visit_u64(v.cast_unsigned())
72            }
73            fn visit_str<E: serde::de::Error>(
74                self,
75                v: &str,
76            ) -> std::result::Result<Self::Value, E> {
77                let stripped = v
78                    .strip_suffix('%')
79                    .ok_or_else(|| E::custom(format!("invalid percent size: {v}")))?;
80                if stripped.is_empty() || !stripped.bytes().all(|b| b.is_ascii_digit()) {
81                    return Err(E::custom(format!("invalid percent size: {v}")));
82                }
83                let n: u32 = stripped.parse().map_err(E::custom)?;
84                Ok(SizeValue::Percent(
85                    u8::try_from(n.min(100)).map_err(E::custom)?,
86                ))
87            }
88        }
89        deserializer.deserialize_any(Visitor)
90    }
91}
92
93/// Anchor point for overlay placement.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
95#[serde(rename_all = "kebab-case")]
96pub enum OverlayAnchor {
97    /// Center of the available area.
98    #[default]
99    Center,
100    /// Top-left corner.
101    TopLeft,
102    /// Top-right corner.
103    TopRight,
104    /// Bottom-left corner.
105    BottomLeft,
106    /// Bottom-right corner.
107    BottomRight,
108    /// Top edge, horizontally centered.
109    TopCenter,
110    /// Bottom edge, horizontally centered.
111    BottomCenter,
112    /// Left edge, vertically centered.
113    LeftCenter,
114    /// Right edge, vertically centered.
115    RightCenter,
116}
117
118/// Per-side overlay margin from terminal edges.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase", default)]
121pub struct OverlayMargin {
122    /// Top margin in rows.
123    pub top: u16,
124    /// Right margin in columns.
125    pub right: u16,
126    /// Bottom margin in rows.
127    pub bottom: u16,
128    /// Left margin in columns.
129    pub left: u16,
130}
131/// Wire margin accepts either a uniform scalar or per-side object.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(untagged)]
134pub enum OverlayMarginWire {
135    /// Uniform margin applied to all four sides.
136    Uniform(u16),
137    /// Individually specified sides.
138    Sides(OverlayMargin),
139}
140
141/// Serializable overlay layout specification for `uiSlot.overlayOptions`.
142#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase", default)]
144pub struct OverlaySpec {
145    /// Width in columns, or percentage of terminal width.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub width: Option<SizeValue>,
148    /// Minimum width in columns.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub min_width: Option<u16>,
151    /// Maximum height in rows, or percentage of terminal height.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub max_height: Option<SizeValue>,
154    /// Anchor point when `row`/`col` are unset (default center).
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub anchor: Option<OverlayAnchor>,
157    /// Horizontal offset from the resolved position (positive = right).
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub offset_x: Option<i16>,
160    /// Vertical offset from the resolved position (positive = down).
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub offset_y: Option<i16>,
163    /// Absolute or percent row position (overrides vertical anchor).
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub row: Option<SizeValue>,
166    /// Absolute or percent column position (overrides horizontal anchor).
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub col: Option<SizeValue>,
169    /// Margin from terminal edges: a uniform scalar or per-side object.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub margin: Option<OverlayMarginWire>,
172    /// When true, showing the overlay does not capture keyboard focus.
173    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
174    pub non_capturing: bool,
175}
176
177/// Wire protocol version negotiated in [`Hello`] / [`HelloAck`].
178pub const PROTOCOL_VERSION: u32 = 1;
179
180/// Compatibility target: reference `@earendil-works/pi-coding-agent` version.
181pub const COMPATIBILITY_VERSION: &str = "0.80.10";
182
183/// Maximum UTF-8 byte length of one frame line (excluding the trailing newline).
184pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
185
186/// Correlation identifier for request/response/error frames.
187///
188/// Event frames use `0` when unsolicited, or the parent request id when
189/// streaming updates for that call.
190pub type FrameId = u64;
191
192/// Result type for protocol encode/decode/validation operations.
193pub type Result<T, E = ProtocolError> = std::result::Result<T, E>;
194/// Protocol encode, decode, validation, or handshake failure.
195#[derive(Debug, Error, Clone, PartialEq, Eq)]
196pub enum ProtocolError {
197    /// Frame line exceeded [`MAX_FRAME_BYTES`] before a newline arrived.
198    #[error("frame exceeds maximum size of {MAX_FRAME_BYTES} bytes")]
199    FrameTooLarge,
200    /// Bytes were not valid UTF-8.
201    #[error("invalid UTF-8 in protocol stream: {0}")]
202    InvalidUtf8(String),
203    /// A complete line was not valid JSON.
204    #[error("invalid JSON frame: {0}")]
205    InvalidJson(String),
206    /// JSON decoded but was not a protocol frame object.
207    #[error("malformed frame: {0}")]
208    MalformedFrame(String),
209    /// Frame kind/id/method rules were violated.
210    #[error("invalid frame: {0}")]
211    InvalidFrame(String),
212    /// Hello handshake versions are incompatible.
213    #[error("protocol version mismatch: remote={remote} local={local}")]
214    VersionMismatch {
215        /// Remote peer protocol version.
216        remote: u32,
217        /// Local protocol version.
218        local: u32,
219    },
220    /// Compatibility string does not match the supported coding-agent version.
221    #[error("compatibility version mismatch: remote={remote} local={local}")]
222    CompatibilityMismatch {
223        /// Remote compatibility version string.
224        remote: String,
225        /// Local compatibility version string.
226        local: String,
227    },
228    /// Method name is not in the bridge/host-control allowlist.
229    #[error("unknown protocol method: {0}")]
230    UnknownMethod(String),
231    /// Stream ended while a partial line was still buffered.
232    #[error("truncated protocol frame at end of stream")]
233    Truncated,
234}
235
236/// Frame kind discriminant on the wire (`req` | `res` | `event` | `error`).
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum FrameKind {
240    /// Correlated request; requires nonzero [`Frame::id`].
241    Req,
242    /// Correlated success response; requires nonzero [`Frame::id`].
243    Res,
244    /// Unsolicited or streaming event; id may be `0` or a parent request id.
245    #[default]
246    Event,
247    /// Correlated or unsolicited error; nonzero id when correlated.
248    Error,
249}
250
251impl FrameKind {
252    /// Wire string for this kind.
253    #[must_use]
254    pub const fn as_str(self) -> &'static str {
255        match self {
256            Self::Req => "req",
257            Self::Res => "res",
258            Self::Event => "event",
259            Self::Error => "error",
260        }
261    }
262
263    /// Parse a wire kind string.
264    #[must_use]
265    pub fn parse(raw: &str) -> Option<Self> {
266        match raw {
267            "req" => Some(Self::Req),
268            "res" => Some(Self::Res),
269            "event" => Some(Self::Event),
270            "error" => Some(Self::Error),
271            _ => None,
272        }
273    }
274
275    /// Whether this kind requires a nonzero frame id.
276    #[must_use]
277    pub const fn requires_nonzero_id(self) -> bool {
278        matches!(self, Self::Req | Self::Res)
279    }
280}
281
282impl fmt::Display for FrameKind {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.write_str(self.as_str())
285    }
286}
287
288/// Allowlisted bridge and host-control methods.
289///
290/// Lifecycle event methods reuse the exact `type` discriminants from the
291/// reference extension API and are carried as open method strings on
292/// [`Frame`]; this enum covers the fixed bridge surface plus host-control
293/// dialog / input / slot methods that have typed payloads in this module.
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub enum Method {
297    /// Protocol handshake request/response.
298    Hello,
299    /// Extension tool partial UI/result update.
300    ToolUpdate,
301    /// Extension custom-provider stream event.
302    ProviderEvent,
303    /// Host-rendered UI slot push (structured runs).
304    UiSlot,
305    /// Dispose a previously pushed UI slot.
306    DisposeSlot,
307    /// Non-retryable extension failure notification.
308    ExtensionError,
309    /// Native select dialog.
310    Select,
311    /// Native confirm dialog.
312    Confirm,
313    /// Native single-line input dialog.
314    Input,
315    /// Native multi-line editor dialog.
316    Editor,
317    /// Fire-and-forget notification.
318    Notify,
319    /// Raw terminal input to a registered host handler / focused slot.
320    TerminalInput,
321    /// Synchronize validated extension flag values.
322    #[serde(rename = "flags.set")]
323    FlagsSet,
324    /// Dispatch one effective extension shortcut.
325    #[serde(rename = "shortcut.execute")]
326    ShortcutExecute,
327    /// Structured UI event delivered to a focused host component.
328    UiEvent,
329    /// Request a host component measure for a given width/theme generation.
330    Measure,
331    /// Request a host component render for a given width/theme generation.
332    Render,
333}
334
335impl Method {
336    /// All allowlisted methods in stable order.
337    pub const ALL: &'static [Self] = &[
338        Self::Hello,
339        Self::ToolUpdate,
340        Self::ProviderEvent,
341        Self::UiSlot,
342        Self::DisposeSlot,
343        Self::ExtensionError,
344        Self::Select,
345        Self::Confirm,
346        Self::Input,
347        Self::Editor,
348        Self::Notify,
349        Self::TerminalInput,
350        Self::FlagsSet,
351        Self::ShortcutExecute,
352        Self::UiEvent,
353        Self::Measure,
354        Self::Render,
355    ];
356
357    /// Wire method string.
358    #[must_use]
359    pub const fn as_str(self) -> &'static str {
360        match self {
361            Self::Hello => "hello",
362            Self::ToolUpdate => "toolUpdate",
363            Self::ProviderEvent => "providerEvent",
364            Self::UiSlot => "uiSlot",
365            Self::DisposeSlot => "disposeSlot",
366            Self::ExtensionError => "extensionError",
367            Self::Select => "select",
368            Self::Confirm => "confirm",
369            Self::Input => "input",
370            Self::Editor => "editor",
371            Self::Notify => "notify",
372            Self::TerminalInput => "terminalInput",
373            Self::FlagsSet => "flags.set",
374            Self::ShortcutExecute => "shortcut.execute",
375            Self::UiEvent => "uiEvent",
376            Self::Measure => "measure",
377            Self::Render => "render",
378        }
379    }
380
381    /// Parse an allowlisted method string.
382    #[must_use]
383    pub fn parse(raw: &str) -> Option<Self> {
384        match raw {
385            "hello" => Some(Self::Hello),
386            "toolUpdate" => Some(Self::ToolUpdate),
387            "providerEvent" => Some(Self::ProviderEvent),
388            "uiSlot" => Some(Self::UiSlot),
389            "disposeSlot" => Some(Self::DisposeSlot),
390            "extensionError" => Some(Self::ExtensionError),
391            "select" => Some(Self::Select),
392            "confirm" => Some(Self::Confirm),
393            "input" => Some(Self::Input),
394            "editor" => Some(Self::Editor),
395            "notify" => Some(Self::Notify),
396            "terminalInput" => Some(Self::TerminalInput),
397            "flags.set" => Some(Self::FlagsSet),
398            "shortcut.execute" => Some(Self::ShortcutExecute),
399            "uiEvent" => Some(Self::UiEvent),
400            "measure" => Some(Self::Measure),
401            "render" => Some(Self::Render),
402            _ => None,
403        }
404    }
405
406    /// Whether `raw` is an allowlisted bridge/host-control method.
407    #[must_use]
408    pub fn is_allowlisted(raw: &str) -> bool {
409        Self::parse(raw).is_some()
410    }
411}
412
413impl fmt::Display for Method {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        f.write_str(self.as_str())
416    }
417}
418/// Wire name for [`Method::UiSlot`].
419#[must_use]
420pub const fn ui_slot_method() -> &'static str {
421    "uiSlot"
422}
423
424/// Wire name for [`Method::DisposeSlot`].
425#[must_use]
426pub const fn dispose_slot_method() -> &'static str {
427    "disposeSlot"
428}
429
430/// Wire name for [`Method::ToolUpdate`].
431#[must_use]
432pub const fn tool_update_method() -> &'static str {
433    "toolUpdate"
434}
435
436/// Wire name for [`Method::ProviderEvent`].
437#[must_use]
438pub const fn provider_event_method() -> &'static str {
439    "providerEvent"
440}
441
442/// Wire name for [`Method::ExtensionError`].
443#[must_use]
444pub const fn extension_error_method() -> &'static str {
445    "extensionError"
446}
447
448/// Compatibility validation message helper used by the host client.
449pub struct FrameValidationError;
450
451impl FrameValidationError {
452    /// Generic validation message for a rejected frame.
453    #[must_use]
454    pub const fn message_for(_frame: &Frame) -> &'static str {
455        "invalid protocol frame"
456    }
457}
458
459/// One protocol frame.
460#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct Frame {
463    /// Correlation id (`0` only for unsolicited events / uncorrelated errors).
464    pub id: FrameId,
465    /// Frame kind.
466    pub kind: FrameKind,
467    /// Method name (bridge, host-control, or lifecycle event type).
468    pub method: String,
469    /// Method-specific JSON payload (object for typed methods).
470    #[serde(default)]
471    pub payload: Value,
472}
473
474impl Frame {
475    /// Build a frame with a typed method.
476    #[must_use]
477    pub fn new(id: FrameId, kind: FrameKind, method: Method, payload: Value) -> Self {
478        Self {
479            id,
480            kind,
481            method: method.as_str().to_owned(),
482            payload,
483        }
484    }
485
486    /// Build a request frame.
487    #[must_use]
488    pub fn request(id: FrameId, method: Method, payload: Value) -> Self {
489        Self::new(id, FrameKind::Req, method, payload)
490    }
491
492    /// Build a response frame.
493    #[must_use]
494    pub fn response(id: FrameId, method: Method, payload: Value) -> Self {
495        Self::new(id, FrameKind::Res, method, payload)
496    }
497
498    /// Build an event frame.
499    #[must_use]
500    pub fn event(id: FrameId, method: Method, payload: Value) -> Self {
501        Self::new(id, FrameKind::Event, method, payload)
502    }
503
504    /// Build an error frame with a structured error body.
505    ///
506    /// # Errors
507    ///
508    /// Returns [`ProtocolError::InvalidJson`] if the error body cannot be
509    /// serialized (should not occur for well-formed [`ErrorPayload`] values).
510    pub fn error_frame(id: FrameId, method: Method, error: &ErrorPayload) -> Result<Self> {
511        let payload = serde_json::to_value(error)
512            .map_err(|e| ProtocolError::InvalidJson(format!("serialize error payload: {e}")))?;
513        Ok(Self::new(id, FrameKind::Error, method, payload))
514    }
515
516    /// Parse the method field as an allowlisted [`Method`].
517    #[must_use]
518    pub fn method_enum(&self) -> Option<Method> {
519        Method::parse(&self.method)
520    }
521
522    /// Validate id/kind rules and, when `require_allowlisted`, the method set.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`ProtocolError::InvalidFrame`] or
527    /// [`ProtocolError::UnknownMethod`] when validation fails.
528    pub fn validate(&self, require_allowlisted: bool) -> Result<()> {
529        if self.kind.requires_nonzero_id() && self.id == 0 {
530            return Err(ProtocolError::InvalidFrame(format!(
531                "kind {} requires nonzero id",
532                self.kind
533            )));
534        }
535        if self.method.is_empty() {
536            return Err(ProtocolError::InvalidFrame(
537                "method must be a non-empty string".to_owned(),
538            ));
539        }
540        if require_allowlisted && !Method::is_allowlisted(&self.method) {
541            return Err(ProtocolError::UnknownMethod(self.method.clone()));
542        }
543        // Reject scalar payloads; typed methods use objects (arrays allowed for open payloads).
544        match &self.payload {
545            Value::Null | Value::Object(_) | Value::Array(_) => {}
546            Value::Bool(_) | Value::Number(_) | Value::String(_) => {
547                return Err(ProtocolError::InvalidFrame(
548                    "payload must be a JSON object or array".to_owned(),
549                ));
550            }
551        }
552        if self.method == Method::UiSlot.as_str() {
553            let slot: UiSlot = serde_json::from_value(self.payload.clone()).map_err(|error| {
554                ProtocolError::InvalidFrame(format!("invalid uiSlot payload: {error}"))
555            })?;
556            slot.validate()?;
557        }
558        Ok(())
559    }
560}
561
562/// Client → host hello request payload.
563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
564#[serde(rename_all = "camelCase")]
565pub struct Hello {
566    /// Protocol version (`1`).
567    pub protocol_version: u32,
568    /// Reference coding-agent compatibility version.
569    pub compatibility_version: String,
570}
571
572impl Hello {
573    /// Local hello payload for the current build.
574    #[must_use]
575    pub fn local() -> Self {
576        Self {
577            protocol_version: PROTOCOL_VERSION,
578            compatibility_version: COMPATIBILITY_VERSION.to_owned(),
579        }
580    }
581
582    /// Validate remote hello against local constants.
583    ///
584    /// # Errors
585    ///
586    /// Returns version or compatibility mismatch errors.
587    pub fn validate_remote(&self) -> Result<()> {
588        if self.protocol_version != PROTOCOL_VERSION {
589            return Err(ProtocolError::VersionMismatch {
590                remote: self.protocol_version,
591                local: PROTOCOL_VERSION,
592            });
593        }
594        if self.compatibility_version != COMPATIBILITY_VERSION {
595            return Err(ProtocolError::CompatibilityMismatch {
596                remote: self.compatibility_version.clone(),
597                local: COMPATIBILITY_VERSION.to_owned(),
598            });
599        }
600        Ok(())
601    }
602}
603
604/// Host → client hello acknowledgment payload.
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct HelloAck {
608    /// Protocol version accepted by the peer.
609    pub protocol_version: u32,
610    /// Compatibility version accepted by the peer.
611    pub compatibility_version: String,
612}
613
614impl HelloAck {
615    /// Local acknowledgment payload.
616    #[must_use]
617    pub fn local() -> Self {
618        Self {
619            protocol_version: PROTOCOL_VERSION,
620            compatibility_version: COMPATIBILITY_VERSION.to_owned(),
621        }
622    }
623}
624
625/// Structured error payload for `kind: "error"` frames.
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
627#[serde(rename_all = "camelCase")]
628pub struct ErrorPayload {
629    /// Stable machine-readable error code.
630    pub code: String,
631    /// Human-readable message.
632    pub message: String,
633    /// Whether the caller may retry the same side effect (always false for
634    /// extension failures per host policy).
635    pub retryable: bool,
636    /// Optional structured detail.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub data: Option<Value>,
639}
640
641impl ErrorPayload {
642    /// Non-retryable error without detail data.
643    #[must_use]
644    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
645        Self {
646            code: code.into(),
647            message: message.into(),
648            retryable: false,
649            data: None,
650        }
651    }
652}
653
654/// Allowlisted text style for structured UI runs (no raw ANSI).
655#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
656#[serde(rename_all = "camelCase", default)]
657pub struct Style {
658    /// Bold emphasis.
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub bold: Option<bool>,
661    /// Dim/faint emphasis.
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub dim: Option<bool>,
664    /// Italic emphasis.
665    #[serde(default, skip_serializing_if = "Option::is_none")]
666    pub italic: Option<bool>,
667    /// Underline.
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub underline: Option<bool>,
670    /// Reverse video.
671    #[serde(default, skip_serializing_if = "Option::is_none")]
672    pub reverse: Option<bool>,
673    /// Strikethrough.
674    #[serde(default, skip_serializing_if = "Option::is_none")]
675    pub strikethrough: Option<bool>,
676    /// Foreground color.
677    #[serde(default, skip_serializing_if = "Option::is_none")]
678    pub fg: Option<WireColor>,
679    /// Background color.
680    #[serde(default, skip_serializing_if = "Option::is_none")]
681    pub bg: Option<WireColor>,
682    /// Optional validated hyperlink (OSC 8 fields).
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub link: Option<Hyperlink>,
685}
686
687/// Allowlisted color encoding for styled runs.
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689#[serde(tag = "type", rename_all = "camelCase")]
690pub enum WireColor {
691    /// Named 8/16-color palette entry.
692    Named {
693        /// Palette name (`black`, `red`, …, `brightWhite`).
694        name: NamedColor,
695    },
696    /// 256-color index.
697    Indexed {
698        /// Palette index `0..=255`.
699        index: u8,
700    },
701    /// Truecolor RGB triple.
702    Rgb {
703        /// Red channel.
704        r: u8,
705        /// Green channel.
706        g: u8,
707        /// Blue channel.
708        b: u8,
709    },
710}
711
712/// Named ANSI palette colors (standard + bright).
713#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
714#[serde(rename_all = "camelCase")]
715pub enum NamedColor {
716    /// Black.
717    Black,
718    /// Red.
719    Red,
720    /// Green.
721    Green,
722    /// Yellow.
723    Yellow,
724    /// Blue.
725    Blue,
726    /// Magenta.
727    Magenta,
728    /// Cyan.
729    Cyan,
730    /// White.
731    White,
732    /// Bright black / gray.
733    BrightBlack,
734    /// Bright red.
735    BrightRed,
736    /// Bright green.
737    BrightGreen,
738    /// Bright yellow.
739    BrightYellow,
740    /// Bright blue.
741    BrightBlue,
742    /// Bright magenta.
743    BrightMagenta,
744    /// Bright cyan.
745    BrightCyan,
746    /// Bright white.
747    BrightWhite,
748}
749
750/// Validated OSC 8 hyperlink fields (http/https only on the Rust boundary).
751#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
752#[serde(rename_all = "camelCase")]
753pub struct Hyperlink {
754    /// Optional link id (≤ 128 bytes when validated).
755    #[serde(default, skip_serializing_if = "Option::is_none")]
756    pub id: Option<String>,
757    /// Absolute URI (`http` / `https`, ≤ 2048 bytes when validated).
758    pub uri: String,
759}
760
761impl Hyperlink {
762    /// Maximum accepted id length in bytes.
763    pub const MAX_ID_BYTES: usize = 128;
764    /// Maximum accepted URI length in bytes.
765    pub const MAX_URI_BYTES: usize = 2048;
766
767    /// Validate scheme and size limits.
768    ///
769    /// # Errors
770    ///
771    /// Returns [`ProtocolError::InvalidFrame`] when the link is rejected.
772    pub fn validate(&self) -> Result<()> {
773        if let Some(id) = &self.id
774            && id.len() > Self::MAX_ID_BYTES
775        {
776            return Err(ProtocolError::InvalidFrame(format!(
777                "hyperlink id exceeds {} bytes",
778                Self::MAX_ID_BYTES
779            )));
780        }
781        if self.uri.len() > Self::MAX_URI_BYTES {
782            return Err(ProtocolError::InvalidFrame(format!(
783                "hyperlink uri exceeds {} bytes",
784                Self::MAX_URI_BYTES
785            )));
786        }
787        let ok = self.uri.starts_with("http://") || self.uri.starts_with("https://");
788        if !ok {
789            return Err(ProtocolError::InvalidFrame(
790                "hyperlink uri must use http or https".to_owned(),
791            ));
792        }
793        Ok(())
794    }
795}
796impl UiSlot {
797    /// Validate every hyperlink carried by every styled run.
798    ///
799    /// # Errors
800    ///
801    /// Returns [`ProtocolError::InvalidFrame`] for a forbidden scheme or an
802    /// oversized hyperlink id/URI.
803    pub fn validate(&self) -> Result<()> {
804        for line in &self.runs {
805            for run in line {
806                if let Some(link) = &run.style.link {
807                    link.validate()?;
808                }
809            }
810        }
811        Ok(())
812    }
813}
814
815/// One contiguous styled text run inside a UI slot line.
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(rename_all = "camelCase")]
818pub struct StyledRun {
819    /// Printable text (no embedded newlines; tabs expanded by the host).
820    pub text: String,
821    /// Optional style; omitted/default means unstyled.
822    #[serde(default, skip_serializing_if = "is_default_style")]
823    pub style: Style,
824}
825
826fn is_default_style(style: &Style) -> bool {
827    style == &Style::default()
828}
829
830/// Where a host UI slot is placed in the native composition tree.
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
832#[serde(rename_all = "camelCase")]
833pub enum SlotPlacement {
834    /// Startup / chat header region.
835    #[default]
836    Header,
837    /// Footer region.
838    Footer,
839    /// Widget row above the editor.
840    AboveEditor,
841    /// Widget row below the editor.
842    BelowEditor,
843    /// Full editor replacement.
844    Editor,
845    /// Custom message / entry renderer.
846    MessageRenderer,
847    /// Modal overlay.
848    Overlay,
849}
850
851/// Cursor cell within a focusable slot (column/row in the slot's content).
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
853#[serde(rename_all = "camelCase")]
854pub struct SlotCursor {
855    /// Zero-based column.
856    pub col: u16,
857    /// Zero-based row.
858    pub row: u16,
859}
860
861/// Host → Rust `uiSlot` event payload (structured runs only).
862#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
863#[serde(rename_all = "camelCase")]
864pub struct UiSlot {
865    /// Stable slot key.
866    pub key: String,
867    /// Monotonic generation; stale generations are discarded.
868    pub generation: u64,
869    /// Composition placement.
870    pub placement: SlotPlacement,
871    /// Measured height in rows.
872    pub height: u16,
873    /// Lines of styled runs (`lines[row][run]`).
874    pub runs: Vec<Vec<StyledRun>>,
875    /// Whether the slot can receive focus / input.
876    #[serde(default)]
877    pub focusable: bool,
878    /// Optional hardware-cursor hint inside the slot.
879    #[serde(default, skip_serializing_if = "Option::is_none")]
880    pub cursor: Option<SlotCursor>,
881    /// Overlay layout options when [`SlotPlacement::Overlay`].
882    #[serde(default, skip_serializing_if = "Option::is_none")]
883    pub overlay_options: Option<OverlaySpec>,
884}
885
886/// Dispose a keyed slot (and any focused state).
887#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(rename_all = "camelCase")]
889pub struct DisposeSlot {
890    /// Slot key to dispose.
891    pub key: String,
892    /// Optional generation that triggered dispose.
893    #[serde(default, skip_serializing_if = "Option::is_none")]
894    pub generation: Option<u64>,
895}
896
897/// Non-retryable extension failure event payload.
898#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
899#[serde(rename_all = "camelCase")]
900pub struct ExtensionErrorEvent {
901    /// Stable error code.
902    pub code: String,
903    /// Human-readable message.
904    pub message: String,
905    /// Always false for extension side effects.
906    #[serde(default)]
907    pub retryable: bool,
908    /// Optional extension path / detail.
909    #[serde(default, skip_serializing_if = "Option::is_none")]
910    pub data: Option<Value>,
911}
912
913/// Partial tool update from the host.
914#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
915#[serde(rename_all = "camelCase")]
916pub struct ToolUpdate {
917    /// Tool call id.
918    pub tool_call_id: String,
919    /// Tool name.
920    pub tool_name: String,
921    /// Partial result payload (open JSON).
922    pub partial_result: Value,
923}
924
925/// Custom provider stream event from the host.
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
927#[serde(rename_all = "camelCase")]
928pub struct ProviderEvent {
929    /// Provider registration id.
930    pub provider_id: String,
931    /// Stream / call correlation id.
932    pub call_id: String,
933    /// Event name within the provider stream.
934    pub event: String,
935    /// Event payload (open JSON).
936    #[serde(default)]
937    pub data: Value,
938}
939
940/// Key modifiers on the wire (shift|alt|ctrl|super).
941#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
942#[serde(rename_all = "camelCase", default)]
943pub struct KeyModifiersWire {
944    /// Shift.
945    #[serde(default, skip_serializing_if = "Option::is_none")]
946    pub shift: Option<bool>,
947    /// Alt / option.
948    #[serde(default, skip_serializing_if = "Option::is_none")]
949    pub alt: Option<bool>,
950    /// Control.
951    #[serde(default, skip_serializing_if = "Option::is_none")]
952    pub ctrl: Option<bool>,
953    /// Super / meta / command.
954    #[serde(default, skip_serializing_if = "Option::is_none")]
955    pub super_key: Option<bool>,
956}
957
958/// Key event kind on the wire.
959#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
960#[serde(rename_all = "camelCase")]
961pub enum KeyEventKindWire {
962    /// Key press (default).
963    #[default]
964    Press,
965    /// Key release (Kitty).
966    Release,
967    /// Key repeat.
968    Repeat,
969}
970
971/// Structured UI event delivered over the protocol (never Ratatui/crossterm types).
972#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973#[serde(tag = "type", rename_all = "camelCase")]
974pub enum UiEventWire {
975    /// Keyboard event.
976    Key {
977        /// Key id grammar base (`enter`, `a`, `f1`, …).
978        code: String,
979        /// Modifier set.
980        #[serde(default)]
981        modifiers: KeyModifiersWire,
982        /// Press / release / repeat.
983        #[serde(default)]
984        kind: KeyEventKindWire,
985    },
986    /// Bracketed paste text (newlines normalized to `\n`).
987    Paste {
988        /// Pasted text.
989        text: String,
990    },
991    /// Terminal focus gained.
992    FocusGained,
993    /// Terminal focus lost.
994    FocusLost,
995    /// Terminal resize.
996    Resize {
997        /// Columns.
998        width: u16,
999        /// Rows.
1000        height: u16,
1001    },
1002}
1003
1004/// Validated extension flag value sent to the TypeScript runtime.
1005#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(untagged)]
1007pub enum FlagValueWire {
1008    /// Boolean CLI flag.
1009    Boolean(bool),
1010    /// String CLI flag.
1011    String(String),
1012}
1013
1014/// Payload for [`FLAGS_SET_METHOD`].
1015#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1016#[serde(rename_all = "camelCase")]
1017pub struct FlagsSetRequest {
1018    /// Complete validated flag-value overlay.
1019    pub values: BTreeMap<String, FlagValueWire>,
1020}
1021
1022/// Acknowledgement for [`FLAGS_SET_METHOD`].
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1024#[serde(rename_all = "camelCase")]
1025pub struct FlagsSetResponse {
1026    /// True when the host applied every supplied value.
1027    pub ok: bool,
1028}
1029
1030/// Payload for [`SHORTCUT_EXECUTE_METHOD`].
1031#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1032#[serde(rename_all = "camelCase")]
1033pub struct ShortcutExecuteRequest {
1034    /// Canonical lower-case key identifier.
1035    pub key: String,
1036}
1037
1038/// Immediate dispatch acknowledgement for [`SHORTCUT_EXECUTE_METHOD`].
1039#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct ShortcutExecuteResponse {
1042    /// Whether a live extension shortcut owned this key.
1043    pub handled: bool,
1044}
1045
1046/// Keyed UI event request for [`Method::UiEvent`].
1047#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct UiEventRequest {
1050    /// UI slot key.
1051    pub key: String,
1052    /// Slot generation observed by the native product.
1053    pub generation: u64,
1054    /// Structured event for cross-language inspection.
1055    pub event: UiEventWire,
1056    /// Raw terminal input bytes for component `handleInput`, when applicable.
1057    #[serde(default, skip_serializing_if = "Option::is_none")]
1058    pub data: Option<String>,
1059}
1060
1061/// Host delivery result for [`Method::UiEvent`].
1062#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1063#[serde(rename_all = "camelCase")]
1064pub struct UiEventResponse {
1065    /// True only when the key and generation matched a live component.
1066    pub delivered: bool,
1067}
1068
1069/// Terminal-input rewrite / consume result for `terminalInput`.
1070#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1071#[serde(rename_all = "camelCase", default)]
1072pub struct TerminalInputResult {
1073    /// When true, native handling is skipped.
1074    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1075    pub consume: bool,
1076    /// Optional rewritten input data.
1077    #[serde(default, skip_serializing_if = "Option::is_none")]
1078    pub data: Option<String>,
1079}
1080
1081/// Dialog timeout option shared by select/confirm/input.
1082#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1083#[serde(rename_all = "camelCase", default)]
1084pub struct DialogOptions {
1085    /// Auto-dismiss timeout in milliseconds.
1086    #[serde(default, skip_serializing_if = "Option::is_none")]
1087    pub timeout_ms: Option<u64>,
1088}
1089
1090/// `select` request payload.
1091#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092#[serde(rename_all = "camelCase")]
1093pub struct SelectRequest {
1094    /// Dialog title.
1095    pub title: String,
1096    /// Options presented to the user.
1097    pub options: Vec<String>,
1098    /// Optional timeout.
1099    #[serde(default, flatten)]
1100    pub options_meta: DialogOptions,
1101}
1102
1103/// `select` response payload.
1104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1105#[serde(rename_all = "camelCase")]
1106pub struct SelectResponse {
1107    /// Chosen option, or `null`/missing when dismissed.
1108    #[serde(default, skip_serializing_if = "Option::is_none")]
1109    pub value: Option<String>,
1110}
1111
1112/// `confirm` request payload.
1113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1114#[serde(rename_all = "camelCase")]
1115pub struct ConfirmRequest {
1116    /// Dialog title.
1117    pub title: String,
1118    /// Dialog message body.
1119    pub message: String,
1120    /// Optional timeout.
1121    #[serde(default, flatten)]
1122    pub options_meta: DialogOptions,
1123}
1124
1125/// `confirm` response payload.
1126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1127#[serde(rename_all = "camelCase")]
1128pub struct ConfirmResponse {
1129    /// Whether the user confirmed.
1130    pub confirmed: bool,
1131}
1132
1133/// `input` request payload.
1134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1135#[serde(rename_all = "camelCase")]
1136pub struct InputRequest {
1137    /// Dialog title.
1138    pub title: String,
1139    /// Placeholder text.
1140    #[serde(default, skip_serializing_if = "Option::is_none")]
1141    pub placeholder: Option<String>,
1142    /// Optional timeout.
1143    #[serde(default, flatten)]
1144    pub options_meta: DialogOptions,
1145}
1146
1147/// `input` response payload.
1148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1149#[serde(rename_all = "camelCase")]
1150pub struct InputResponse {
1151    /// Entered value, or dismissed.
1152    #[serde(default, skip_serializing_if = "Option::is_none")]
1153    pub value: Option<String>,
1154}
1155
1156/// `editor` request payload.
1157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1158#[serde(rename_all = "camelCase")]
1159pub struct EditorRequest {
1160    /// Dialog title.
1161    pub title: String,
1162    /// Prefill text.
1163    #[serde(default, skip_serializing_if = "Option::is_none")]
1164    pub prefill: Option<String>,
1165}
1166
1167/// `editor` response payload.
1168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1169#[serde(rename_all = "camelCase")]
1170pub struct EditorResponse {
1171    /// Edited value, or dismissed.
1172    #[serde(default, skip_serializing_if = "Option::is_none")]
1173    pub value: Option<String>,
1174}
1175
1176/// Notification level for `notify`.
1177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub enum NotifyLevel {
1180    /// Informational.
1181    #[default]
1182    Info,
1183    /// Warning.
1184    Warning,
1185    /// Error.
1186    Error,
1187}
1188
1189/// `notify` request/event payload.
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1191#[serde(rename_all = "camelCase")]
1192pub struct NotifyRequest {
1193    /// Notification text.
1194    pub message: String,
1195    /// Severity.
1196    #[serde(default, rename = "type")]
1197    pub level: NotifyLevel,
1198}
1199
1200/// Measure/render request shared fields.
1201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1202#[serde(rename_all = "camelCase")]
1203pub struct SlotRenderRequest {
1204    /// Slot key.
1205    pub key: String,
1206    /// Available width in columns.
1207    pub width: u16,
1208    /// Theme generation counter.
1209    pub theme_generation: u64,
1210}
1211
1212/// Measure response height.
1213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1214#[serde(rename_all = "camelCase")]
1215pub struct MeasureResponse {
1216    /// Measured height in rows.
1217    pub height: u16,
1218}
1219
1220/// Encode a frame to a single UTF-8 JSON line including the trailing `\n`.
1221///
1222/// # Errors
1223///
1224/// Returns [`ProtocolError::InvalidFrame`] when validation fails, or
1225/// [`ProtocolError::InvalidJson`] / [`ProtocolError::FrameTooLarge`] when the
1226/// encoded line is invalid or too large.
1227pub fn encode_frame(frame: &Frame) -> Result<Vec<u8>> {
1228    frame.validate(false)?;
1229    let mut bytes = serde_json::to_vec(frame)
1230        .map_err(|e| ProtocolError::InvalidJson(format!("encode frame: {e}")))?;
1231    if bytes.len() > MAX_FRAME_BYTES {
1232        return Err(ProtocolError::FrameTooLarge);
1233    }
1234    bytes.push(b'\n');
1235    Ok(bytes)
1236}
1237
1238/// Encode a frame as a UTF-8 string including the trailing newline.
1239///
1240/// # Errors
1241///
1242/// Same as [`encode_frame`].
1243pub fn encode_frame_string(frame: &Frame) -> Result<String> {
1244    let bytes = encode_frame(frame)?;
1245    String::from_utf8(bytes).map_err(|e| ProtocolError::InvalidUtf8(e.to_string()))
1246}
1247
1248/// Decode one complete JSON line (no trailing newline required) into a frame.
1249///
1250/// # Errors
1251///
1252/// Returns UTF-8, JSON, malformation, size, or validation errors.
1253pub fn decode_frame_line(line: &[u8]) -> Result<Frame> {
1254    if line.len() > MAX_FRAME_BYTES {
1255        return Err(ProtocolError::FrameTooLarge);
1256    }
1257    let text = str::from_utf8(line).map_err(|e| ProtocolError::InvalidUtf8(e.to_string()))?;
1258    decode_frame_str(text)
1259}
1260
1261/// Decode one complete JSON line string into a frame.
1262///
1263/// # Errors
1264///
1265/// Returns JSON, malformation, size, or validation errors.
1266pub fn decode_frame_str(line: &str) -> Result<Frame> {
1267    let trimmed = line.trim_end_matches('\r');
1268    if trimmed.is_empty() {
1269        return Err(ProtocolError::MalformedFrame("empty line".to_owned()));
1270    }
1271    if trimmed.len() > MAX_FRAME_BYTES {
1272        return Err(ProtocolError::FrameTooLarge);
1273    }
1274    let frame: Frame =
1275        serde_json::from_str(trimmed).map_err(|e| ProtocolError::InvalidJson(e.to_string()))?;
1276    frame.validate(false)?;
1277    Ok(frame)
1278}
1279
1280/// Decode and require an allowlisted method.
1281///
1282/// # Errors
1283///
1284/// Propagates [`decode_frame_str`] errors and [`ProtocolError::UnknownMethod`].
1285pub fn decode_frame_str_strict(line: &str) -> Result<Frame> {
1286    let frame = decode_frame_str(line)?;
1287    frame.validate(true)?;
1288    Ok(frame)
1289}
1290
1291/// Incremental JSONL frame decoder with a hard size bound.
1292///
1293/// Accepts partial chunks, multiple frames per push, LF or CRLF separators,
1294/// and rejects oversize lines **before** the internal buffer can grow past
1295/// [`MAX_FRAME_BYTES`] + 1 pending newline scan window.
1296#[derive(Debug, Default)]
1297pub struct FrameDecoder {
1298    buf: Vec<u8>,
1299    max_frame_bytes: usize,
1300}
1301
1302impl FrameDecoder {
1303    /// Create a decoder with the protocol default size limit.
1304    #[must_use]
1305    pub fn new() -> Self {
1306        Self {
1307            buf: Vec::new(),
1308            max_frame_bytes: MAX_FRAME_BYTES,
1309        }
1310    }
1311
1312    /// Create a decoder with a custom max frame size (tests).
1313    #[must_use]
1314    pub fn with_max_frame_bytes(max_frame_bytes: usize) -> Self {
1315        Self {
1316            buf: Vec::new(),
1317            max_frame_bytes,
1318        }
1319    }
1320
1321    /// Bytes currently buffered (incomplete line).
1322    #[must_use]
1323    pub fn buffered_len(&self) -> usize {
1324        self.buf.len()
1325    }
1326
1327    /// Push bytes and return every complete frame decoded from this chunk.
1328    ///
1329    /// # Errors
1330    ///
1331    /// Returns the first size / UTF-8 / JSON / validation error encountered.
1332    /// On error, the decoder may drop the offending line and keep subsequent
1333    /// buffered data only when the error was per-line; oversize clears the
1334    /// current line buffer.
1335    pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<Frame>> {
1336        let mut out = Vec::new();
1337        let mut offset = 0usize;
1338        while offset < chunk.len() {
1339            // Find next newline in chunk without copying the whole remainder.
1340            if let Some(rel) = chunk[offset..].iter().position(|&b| b == b'\n') {
1341                let line_end_in_chunk = offset + rel;
1342                let pending = self.buf.len() + (line_end_in_chunk - offset);
1343                if pending > self.max_frame_bytes {
1344                    self.buf.clear();
1345                    return Err(ProtocolError::FrameTooLarge);
1346                }
1347                self.buf
1348                    .extend_from_slice(&chunk[offset..line_end_in_chunk]);
1349                // Strip one trailing CR for CRLF.
1350                if self.buf.last() == Some(&b'\r') {
1351                    self.buf.pop();
1352                }
1353                let line = std::mem::take(&mut self.buf);
1354                out.push(decode_frame_line(&line)?);
1355                offset = line_end_in_chunk + 1;
1356            } else {
1357                let pending = self.buf.len() + (chunk.len() - offset);
1358                if pending > self.max_frame_bytes {
1359                    self.buf.clear();
1360                    return Err(ProtocolError::FrameTooLarge);
1361                }
1362                // Grow only up to the remaining allowed bytes.
1363                self.buf.extend_from_slice(&chunk[offset..]);
1364                break;
1365            }
1366        }
1367        Ok(out)
1368    }
1369
1370    /// Finish the stream: error if a partial line remains; `Ok(None)` if empty.
1371    ///
1372    /// # Errors
1373    ///
1374    /// Returns [`ProtocolError::Truncated`] when buffered bytes remain, or
1375    /// decode errors if a final line without newline should be accepted — this
1376    /// API requires newline-terminated frames, so remainder is truncated.
1377    pub fn finish(&mut self) -> Result<Option<Frame>> {
1378        if self.buf.is_empty() {
1379            return Ok(None);
1380        }
1381        // Final non-empty buffer without newline is a truncated frame.
1382        let leftover = std::mem::take(&mut self.buf);
1383        if leftover.iter().all(u8::is_ascii_whitespace) {
1384            return Ok(None);
1385        }
1386        Err(ProtocolError::Truncated)
1387    }
1388
1389    /// Finish accepting a final line without a trailing newline (EOF flush).
1390    ///
1391    /// # Errors
1392    ///
1393    /// Returns decode errors for the final line, or [`ProtocolError::FrameTooLarge`].
1394    pub fn finish_with_final_line(&mut self) -> Result<Option<Frame>> {
1395        if self.buf.is_empty() {
1396            return Ok(None);
1397        }
1398        if self.buf.len() > self.max_frame_bytes {
1399            self.buf.clear();
1400            return Err(ProtocolError::FrameTooLarge);
1401        }
1402        if self.buf.last() == Some(&b'\r') {
1403            self.buf.pop();
1404        }
1405        let line = std::mem::take(&mut self.buf);
1406        if line.is_empty() {
1407            return Ok(None);
1408        }
1409        Ok(Some(decode_frame_line(&line)?))
1410    }
1411
1412    /// Reset buffered state.
1413    pub fn reset(&mut self) {
1414        self.buf.clear();
1415    }
1416}
1417
1418/// Serialize a typed payload into a JSON object value.
1419///
1420/// # Errors
1421///
1422/// Returns [`ProtocolError::InvalidJson`] on serialization failure.
1423pub fn to_payload<T: Serialize>(value: &T) -> Result<Value> {
1424    serde_json::to_value(value).map_err(|e| ProtocolError::InvalidJson(e.to_string()))
1425}
1426
1427/// Deserialize a typed payload from a frame payload value.
1428///
1429/// # Errors
1430///
1431/// Returns [`ProtocolError::InvalidJson`] on deserialization failure.
1432pub fn from_payload<T: for<'de> Deserialize<'de>>(payload: &Value) -> Result<T> {
1433    serde_json::from_value(payload.clone()).map_err(|e| ProtocolError::InvalidJson(e.to_string()))
1434}
1435
1436/// Empty object payload helper.
1437#[must_use]
1438pub fn empty_object() -> Value {
1439    Value::Object(Map::new())
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444    use super::*;
1445
1446    const FIXTURES: &str = include_str!("../tests/fixtures/protocol/frames.jsonl");
1447
1448    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
1449
1450    fn sample_hello_req() -> Result<Frame> {
1451        Ok(Frame::request(
1452            1,
1453            Method::Hello,
1454            to_payload(&Hello::local())?,
1455        ))
1456    }
1457
1458    #[test]
1459    fn versions_are_stable() {
1460        assert_eq!(PROTOCOL_VERSION, 1);
1461        assert_eq!(COMPATIBILITY_VERSION, "0.80.10");
1462        assert_eq!(MAX_FRAME_BYTES, 8 * 1024 * 1024);
1463    }
1464
1465    #[test]
1466    fn method_allowlist_roundtrip() {
1467        for method in Method::ALL {
1468            assert_eq!(Method::parse(method.as_str()), Some(*method));
1469        }
1470        assert_eq!(FLAGS_SET_METHOD, Method::FlagsSet.as_str());
1471        assert_eq!(SHORTCUT_EXECUTE_METHOD, Method::ShortcutExecute.as_str());
1472        assert!(Method::parse("notAMethod").is_none());
1473    }
1474
1475    #[test]
1476    fn frame_id_rules() -> TestResult {
1477        let mut frame = sample_hello_req()?;
1478        frame.id = 0;
1479        assert!(matches!(
1480            frame.validate(false),
1481            Err(ProtocolError::InvalidFrame(_))
1482        ));
1483        Frame::event(0, Method::Notify, empty_object()).validate(false)?;
1484        Ok(())
1485    }
1486
1487    #[test]
1488    fn hello_version_gate() -> TestResult {
1489        Hello::local().validate_remote()?;
1490        let bad = Hello {
1491            protocol_version: 99,
1492            compatibility_version: COMPATIBILITY_VERSION.to_owned(),
1493        };
1494        assert!(matches!(
1495            bad.validate_remote(),
1496            Err(ProtocolError::VersionMismatch {
1497                remote: 99,
1498                local: 1
1499            })
1500        ));
1501        let bad_compat = Hello {
1502            protocol_version: 1,
1503            compatibility_version: "0.0.0".to_owned(),
1504        };
1505        assert!(matches!(
1506            bad_compat.validate_remote(),
1507            Err(ProtocolError::CompatibilityMismatch { .. })
1508        ));
1509        Ok(())
1510    }
1511
1512    #[test]
1513    fn encode_decode_roundtrip_typed() -> TestResult {
1514        let hello = sample_hello_req()?;
1515        let line = encode_frame_string(&hello)?;
1516        assert!(line.ends_with('\n'));
1517        let decoded = decode_frame_str(line.trim_end())?;
1518        assert_eq!(decoded, hello);
1519        assert_eq!(from_payload::<Hello>(&decoded.payload)?, Hello::local());
1520
1521        let ack = Frame::response(1, Method::Hello, to_payload(&HelloAck::local())?);
1522        let ack_line = encode_frame_string(&ack)?;
1523        let decoded_ack = decode_frame_str(ack_line.trim_end())?;
1524        assert_eq!(
1525            from_payload::<HelloAck>(&decoded_ack.payload)?,
1526            HelloAck::local()
1527        );
1528        Ok(())
1529    }
1530
1531    fn sample_slot() -> UiSlot {
1532        UiSlot {
1533            key: "widget.demo".to_owned(),
1534            generation: 3,
1535            placement: SlotPlacement::AboveEditor,
1536            height: 2,
1537            runs: vec![
1538                vec![StyledRun {
1539                    text: "hi".to_owned(),
1540                    style: Style {
1541                        bold: Some(true),
1542                        fg: Some(WireColor::Named {
1543                            name: NamedColor::Green,
1544                        }),
1545                        ..Style::default()
1546                    },
1547                }],
1548                vec![StyledRun {
1549                    text: "link".to_owned(),
1550                    style: Style {
1551                        underline: Some(true),
1552                        link: Some(Hyperlink {
1553                            id: Some("a".to_owned()),
1554                            uri: "https://example.com".to_owned(),
1555                        }),
1556                        fg: Some(WireColor::Rgb { r: 1, g: 2, b: 3 }),
1557                        ..Style::default()
1558                    },
1559                }],
1560            ],
1561            focusable: true,
1562            cursor: Some(SlotCursor { col: 1, row: 0 }),
1563            overlay_options: Some(OverlaySpec {
1564                width: Some(SizeValue::Percent(50)),
1565                anchor: Some(OverlayAnchor::TopCenter),
1566                margin: Some(OverlayMarginWire::Uniform(2)),
1567                non_capturing: true,
1568                ..OverlaySpec::default()
1569            }),
1570        }
1571    }
1572
1573    #[test]
1574    fn ui_slot_and_style_roundtrip() -> TestResult {
1575        let slot = sample_slot();
1576        let frame = Frame::event(0, Method::UiSlot, to_payload(&slot)?);
1577        let line = encode_frame_string(&frame)?;
1578        let decoded = decode_frame_str(line.trim_end())?;
1579        let back: UiSlot = from_payload(&decoded.payload)?;
1580        assert_eq!(back, slot);
1581        back.validate()?;
1582        Ok(())
1583    }
1584
1585    #[test]
1586    fn overlay_margin_accepts_uniform_and_sides() -> TestResult {
1587        let uniform: OverlaySpec = serde_json::from_value(serde_json::json!({"margin": 3}))?;
1588        assert_eq!(uniform.margin, Some(OverlayMarginWire::Uniform(3)));
1589
1590        let sides: OverlaySpec = serde_json::from_value(serde_json::json!({
1591            "margin": {"top": 1, "right": 2, "bottom": 3, "left": 4}
1592        }))?;
1593        assert_eq!(
1594            sides.margin,
1595            Some(OverlayMarginWire::Sides(OverlayMargin {
1596                top: 1,
1597                right: 2,
1598                bottom: 3,
1599                left: 4,
1600            }))
1601        );
1602        Ok(())
1603    }
1604
1605    #[test]
1606    fn ui_slot_rejects_forbidden_and_oversized_links() {
1607        for link in [
1608            serde_json::json!({"uri": "javascript:alert(1)"}),
1609            serde_json::json!({"uri": "file:///tmp/x"}),
1610            serde_json::json!({"uri": format!("https://example.com/{}", "x".repeat(2048))}),
1611            serde_json::json!({"id": "x".repeat(129), "uri": "https://example.com"}),
1612        ] {
1613            let frame = Frame::event(
1614                0,
1615                Method::UiSlot,
1616                serde_json::json!({
1617                    "key": "bad",
1618                    "generation": 1,
1619                    "placement": "aboveEditor",
1620                    "height": 1,
1621                    "runs": [[{"text": "bad", "style": {"link": link}}]]
1622                }),
1623            );
1624            assert!(matches!(
1625                frame.validate(false),
1626                Err(ProtocolError::InvalidFrame(_))
1627            ));
1628        }
1629    }
1630
1631    #[test]
1632    fn dialog_payloads_roundtrip() -> TestResult {
1633        let select = SelectRequest {
1634            title: "Pick".to_owned(),
1635            options: vec!["a".to_owned(), "b".to_owned()],
1636            options_meta: DialogOptions {
1637                timeout_ms: Some(1000),
1638            },
1639        };
1640        let frame = Frame::request(7, Method::Select, to_payload(&select)?);
1641        let line = encode_frame_string(&frame)?;
1642        let decoded = decode_frame_str(line.trim_end())?;
1643        assert_eq!(from_payload::<SelectRequest>(&decoded.payload)?, select);
1644
1645        let confirm = ConfirmResponse { confirmed: true };
1646        let frame = Frame::response(7, Method::Confirm, to_payload(&confirm)?);
1647        let line = encode_frame_string(&frame)?;
1648        let decoded = decode_frame_str(line.trim_end())?;
1649        assert!(from_payload::<ConfirmResponse>(&decoded.payload)?.confirmed);
1650        Ok(())
1651    }
1652
1653    #[test]
1654    fn ui_event_wire_variants() -> TestResult {
1655        let events = [
1656            UiEventWire::Key {
1657                code: "enter".to_owned(),
1658                modifiers: KeyModifiersWire {
1659                    ctrl: Some(true),
1660                    ..KeyModifiersWire::default()
1661                },
1662                kind: KeyEventKindWire::Press,
1663            },
1664            UiEventWire::Paste {
1665                text: "a\nb".to_owned(),
1666            },
1667            UiEventWire::FocusGained,
1668            UiEventWire::FocusLost,
1669            UiEventWire::Resize {
1670                width: 80,
1671                height: 24,
1672            },
1673        ];
1674        for event in events {
1675            let frame = Frame::request(2, Method::UiEvent, to_payload(&event)?);
1676            let line = encode_frame_string(&frame)?;
1677            let decoded = decode_frame_str(line.trim_end())?;
1678            assert_eq!(from_payload::<UiEventWire>(&decoded.payload)?, event);
1679        }
1680        Ok(())
1681    }
1682
1683    #[test]
1684    fn decoder_fragmentation_and_multiple() -> TestResult {
1685        let first = sample_hello_req()?;
1686        let second = Frame::response(1, Method::Hello, to_payload(&HelloAck::local())?);
1687        let mut bytes = encode_frame(&first)?;
1688        bytes.extend(encode_frame(&second)?);
1689        let mut decoder = FrameDecoder::new();
1690        let mut got = Vec::new();
1691        for byte in bytes {
1692            got.extend(decoder.push(&[byte])?);
1693        }
1694        assert!(decoder.finish()?.is_none());
1695        assert_eq!(got, vec![first, second]);
1696        Ok(())
1697    }
1698
1699    #[test]
1700    fn decoder_crlf() -> TestResult {
1701        let frame = sample_hello_req()?;
1702        let mut line = serde_json::to_vec(&frame)?;
1703        line.extend_from_slice(b"\r\n");
1704        let mut decoder = FrameDecoder::new();
1705        let got = decoder.push(&line)?;
1706        assert_eq!(got.first(), Some(&frame));
1707        assert_eq!(got.len(), 1);
1708        Ok(())
1709    }
1710
1711    #[test]
1712    fn decoder_final_line_without_newline() -> TestResult {
1713        let frame = sample_hello_req()?;
1714        let line = serde_json::to_vec(&frame)?;
1715        let mut decoder = FrameDecoder::new();
1716        assert!(decoder.push(&line)?.is_empty());
1717        assert_eq!(decoder.finish_with_final_line()?, Some(frame));
1718
1719        let mut strict = FrameDecoder::new();
1720        assert!(strict.push(&line)?.is_empty());
1721        assert!(matches!(strict.finish(), Err(ProtocolError::Truncated)));
1722        Ok(())
1723    }
1724
1725    #[test]
1726    fn decoder_invalid_utf8_and_json() {
1727        let mut decoder = FrameDecoder::new();
1728        assert!(matches!(
1729            decoder.push(b"\xff\n"),
1730            Err(ProtocolError::InvalidUtf8(_))
1731        ));
1732        let mut decoder = FrameDecoder::new();
1733        assert!(matches!(
1734            decoder.push(b"{not-json}\n"),
1735            Err(ProtocolError::InvalidJson(_))
1736        ));
1737    }
1738
1739    #[test]
1740    fn decoder_oversized_before_growth() -> TestResult {
1741        let limit = 64;
1742        let mut decoder = FrameDecoder::with_max_frame_bytes(limit);
1743        assert_eq!(
1744            decoder.push(&vec![b'a'; limit + 1]),
1745            Err(ProtocolError::FrameTooLarge)
1746        );
1747        assert_eq!(decoder.buffered_len(), 0);
1748
1749        let mut decoder = FrameDecoder::with_max_frame_bytes(limit);
1750        assert!(decoder.push(&vec![b'b'; limit / 2])?.is_empty());
1751        assert_eq!(
1752            decoder.push(&vec![b'c'; limit]),
1753            Err(ProtocolError::FrameTooLarge)
1754        );
1755        Ok(())
1756    }
1757
1758    #[test]
1759    fn strict_unknown_method() -> TestResult {
1760        let frame = Frame {
1761            id: 1,
1762            kind: FrameKind::Req,
1763            method: "notAllowlisted".to_owned(),
1764            payload: empty_object(),
1765        };
1766        let line = encode_frame_string(&frame)?;
1767        assert!(decode_frame_str(line.trim_end()).is_ok());
1768        assert!(matches!(
1769            decode_frame_str_strict(line.trim_end()),
1770            Err(ProtocolError::UnknownMethod(_))
1771        ));
1772        Ok(())
1773    }
1774
1775    #[test]
1776    fn error_payload_shape() -> TestResult {
1777        let error = ErrorPayload {
1778            code: "extension_error".to_owned(),
1779            message: "boom".to_owned(),
1780            retryable: false,
1781            data: Some(serde_json::json!({"path": "x.ts"})),
1782        };
1783        let frame = Frame::error_frame(9, Method::ExtensionError, &error)?;
1784        let line = encode_frame_string(&frame)?;
1785        let decoded = decode_frame_str(line.trim_end())?;
1786        assert_eq!(from_payload::<ErrorPayload>(&decoded.payload)?, error);
1787        Ok(())
1788    }
1789
1790    #[test]
1791    fn shared_fixtures_field_and_discriminant_parity() -> TestResult {
1792        let mut count = 0usize;
1793        for line in FIXTURES.lines() {
1794            if line.trim().is_empty() || line.trim_start().starts_with('#') {
1795                continue;
1796            }
1797            let frame = decode_frame_str_strict(line)?;
1798            let encoded = encode_frame_string(&frame)?;
1799            let again = decode_frame_str_strict(encoded.trim_end())?;
1800            assert_eq!(again, frame);
1801            let method = frame.method_enum();
1802            assert!(method.is_some(), "strict decode accepted unknown method");
1803            assert!(method.is_some_and(|method| Method::ALL.contains(&method)));
1804            count += 1;
1805        }
1806        assert!(count >= 8);
1807        Ok(())
1808    }
1809
1810    #[test]
1811    fn eight_mib_limit_constant_and_encode_guard() {
1812        let frame = Frame {
1813            id: 1,
1814            kind: FrameKind::Req,
1815            method: Method::Notify.as_str().to_owned(),
1816            payload: serde_json::json!({"blob": "x".repeat(MAX_FRAME_BYTES)}),
1817        };
1818        assert_eq!(encode_frame(&frame), Err(ProtocolError::FrameTooLarge));
1819    }
1820
1821    #[test]
1822    fn hyperlink_validation() -> TestResult {
1823        Hyperlink {
1824            id: None,
1825            uri: "https://ok".to_owned(),
1826        }
1827        .validate()?;
1828        assert!(
1829            Hyperlink {
1830                id: None,
1831                uri: "javascript:alert(1)".to_owned(),
1832            }
1833            .validate()
1834            .is_err()
1835        );
1836        assert!(
1837            Hyperlink {
1838                id: Some("a".repeat(Hyperlink::MAX_ID_BYTES + 1)),
1839                uri: "https://ok".to_owned(),
1840            }
1841            .validate()
1842            .is_err()
1843        );
1844        Ok(())
1845    }
1846
1847    #[test]
1848    fn extension_control_payloads_roundtrip() -> TestResult {
1849        let flags = FlagsSetRequest {
1850            values: BTreeMap::from([
1851                ("plan".to_owned(), FlagValueWire::Boolean(true)),
1852                (
1853                    "profile".to_owned(),
1854                    FlagValueWire::String("fast".to_owned()),
1855                ),
1856            ]),
1857        };
1858        let payload = to_payload(&flags)?;
1859        assert_eq!(from_payload::<FlagsSetRequest>(&payload)?, flags);
1860
1861        let shortcut = ShortcutExecuteRequest {
1862            key: "ctrl+alt+p".to_owned(),
1863        };
1864        let payload = to_payload(&shortcut)?;
1865        assert_eq!(from_payload::<ShortcutExecuteRequest>(&payload)?, shortcut);
1866
1867        let ui = UiEventRequest {
1868            key: "overlay.1".to_owned(),
1869            generation: 2,
1870            event: UiEventWire::Paste {
1871                text: "hello".to_owned(),
1872            },
1873            data: Some("hello".to_owned()),
1874        };
1875        let frame = Frame::request(9, Method::UiEvent, to_payload(&ui)?);
1876        let decoded = decode_frame_str(encode_frame_string(&frame)?.trim_end())?;
1877        assert_eq!(from_payload::<UiEventRequest>(&decoded.payload)?, ui);
1878        Ok(())
1879    }
1880}