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