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    /// Submission identity for replacing a client-local pending row.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub command_id: Option<String>,
31    pub id: u64,
32    pub updated_seq: u64,
33    pub role: &'static str,
34    pub label: String,
35    pub recorded_at_ms: Option<i64>,
36    pub lines: Vec<String>,
37    /// The glyph the terminal draws for this role, so both surfaces read alike
38    /// without the browser keeping a second copy of the mapping. Taken from
39    /// the same `entry_visual` the terminal renders from.
40    pub glyph: &'static str,
41    /// The semantic colour name, not a colour. The stylesheet decides what
42    /// `agent` or `failed` looks like; this says which one applies.
43    pub tone: &'static str,
44    /// A tool call's state, for a tool entry. `None` for every other role.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub tool_status: Option<&'static str>,
47    /// The changed files a tool reported, as data rather than as extra lines
48    /// appended to `lines`. The terminal formats these for a terminal; a
49    /// browser re-parsing that formatting is how the phone came to render
50    /// every diffstat as one unsplit path.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub diffstats: Vec<BrowserDiffStat>,
53}
54
55/// One file a tool changed, and by how much.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub struct BrowserDiffStat {
58    pub path: String,
59    pub insertions: u32,
60    pub deletions: u32,
61}
62
63#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub enum WebViewerAccess {
65    Starting,
66    Ready {
67        viewer_url: String,
68        viewer_code: String,
69        qr_login_url: Option<String>,
70        fallback_reason: Option<String>,
71    },
72    Failed {
73        address: SocketAddr,
74        message: String,
75        port_conflict: bool,
76    },
77    Unavailable(String),
78}
79
80impl std::fmt::Debug for WebViewerAccess {
81    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::Starting => formatter.write_str("Starting"),
84            Self::Ready {
85                viewer_url,
86                fallback_reason,
87                ..
88            } => formatter
89                .debug_struct("Ready")
90                .field("viewer_url", viewer_url)
91                .field("credentials", &"[redacted]")
92                .field("fallback_reason", fallback_reason)
93                .finish(),
94            Self::Failed {
95                address,
96                message,
97                port_conflict,
98            } => formatter
99                .debug_struct("Failed")
100                .field("address", address)
101                .field("message", message)
102                .field("port_conflict", port_conflict)
103                .finish(),
104            Self::Unavailable(message) => {
105                formatter.debug_tuple("Unavailable").field(message).finish()
106            }
107        }
108    }
109}
110
111/// Identity shown before an explicit stop request and checked again before signalling.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct WebListenerProcess {
114    pub pid: u32,
115    pub name: String,
116    pub executable: PathBuf,
117    pub started_at: u64,
118    pub stop_disabled_reason: Option<String>,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub enum WebViewerRecovery {
123    Retry,
124    AnotherPort,
125    StopAndRetry(WebListenerProcess),
126}