Skip to main content

mj_client/
web.rs

1//! Web-viewer data shared by Mjolnir's daemon and control surfaces.
2
3use std::net::SocketAddr;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7
8/// The conversation shape the phone reads. The chat layer projects its
9/// entries into this; the browser API owns the wire form.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11pub struct BrowserTranscript {
12    pub latest_seq: u64,
13    /// Opaque Rich-presentation topology key. A browser sends the key it last
14    /// rendered with its next delta request; a mismatch means an append-only
15    /// feed may contain rows the current presentation has removed.
16    pub presentation_key: String,
17    /// Cursor boundary below which a client must replace its feed. Usually
18    /// this is the oldest retained entry, but presentation coalescing may
19    /// advance it so an append-only client drops a marker hidden by a newer
20    /// entry.
21    pub window_start_seq: u64,
22    pub reset: bool,
23    pub entries: Vec<BrowserTranscriptEntry>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
27pub struct BrowserTranscriptEntry {
28    pub jev_notice: bool,
29    /// Submission identity for replacing a client-local pending row.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub command_id: Option<String>,
32    pub id: u64,
33    pub updated_seq: u64,
34    pub role: &'static str,
35    pub label: String,
36    pub recorded_at_ms: Option<i64>,
37    pub lines: Vec<String>,
38    /// The glyph the terminal draws for this role, so both surfaces read alike
39    /// without the browser keeping a second copy of the mapping. Taken from
40    /// the same `entry_visual` the terminal renders from.
41    pub glyph: &'static str,
42    /// The semantic colour name, not a colour. The stylesheet decides what
43    /// `agent` or `failed` looks like; this says which one applies.
44    pub tone: &'static str,
45    /// A tool call's state, for a tool entry. `None` for every other role.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub tool_status: Option<&'static str>,
48    /// The changed files a tool reported, as data rather than as extra lines
49    /// appended to `lines`. The terminal formats these for a terminal; a
50    /// browser re-parsing that formatting is how the phone came to render
51    /// every diffstat as one unsplit path.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub diffstats: Vec<BrowserDiffStat>,
54}
55
56/// One file a tool changed, and by how much.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58pub struct BrowserDiffStat {
59    pub path: String,
60    pub insertions: u32,
61    pub deletions: u32,
62}
63
64#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub enum WebViewerAccess {
66    Starting,
67    Ready {
68        viewer_url: String,
69        viewer_code: String,
70        qr_login_url: Option<String>,
71        fallback_reason: Option<String>,
72    },
73    Failed {
74        address: SocketAddr,
75        message: String,
76        port_conflict: bool,
77    },
78    Unavailable(String),
79}
80
81impl std::fmt::Debug for WebViewerAccess {
82    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Self::Starting => formatter.write_str("Starting"),
85            Self::Ready {
86                viewer_url,
87                fallback_reason,
88                ..
89            } => formatter
90                .debug_struct("Ready")
91                .field("viewer_url", viewer_url)
92                .field("credentials", &"[redacted]")
93                .field("fallback_reason", fallback_reason)
94                .finish(),
95            Self::Failed {
96                address,
97                message,
98                port_conflict,
99            } => formatter
100                .debug_struct("Failed")
101                .field("address", address)
102                .field("message", message)
103                .field("port_conflict", port_conflict)
104                .finish(),
105            Self::Unavailable(message) => {
106                formatter.debug_tuple("Unavailable").field(message).finish()
107            }
108        }
109    }
110}
111
112/// Identity shown before an explicit stop request and checked again before signalling.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct WebListenerProcess {
115    pub pid: u32,
116    pub name: String,
117    pub executable: PathBuf,
118    pub started_at: u64,
119    pub stop_disabled_reason: Option<String>,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub enum WebViewerRecovery {
124    Retry,
125    AnotherPort,
126    StopAndRetry(WebListenerProcess),
127}