///|
/// Wire protocol version this build speaks. Bumped when peers need to
/// reason about a new protocol capability or when a breaking
/// Register/Connect/sys-channel handshake change lands. Optional
/// version fields default to 0 so peers built before versioning was
/// introduced decode as version 0 — fully backwards compatible.
pub const ProtoVersion : Int = 4
///|
/// Lowest `proto_version` the relay accepts on Register/Connect. Bump
/// to match `ProtoVersion` when dropping support for a prior version.
pub const ProtoMinVersion : Int = 0
///|
pub(all) enum ErrorCode {
TaskNotFound
TaskOffline
InternalError
/// Relay refused the Connect because the client's `proto_version` is
/// below `proto_min_version`. `min_version` tells the client which
/// version to upgrade to before retrying.
ProtoVersionTooOld(min_version~ : Int)
} derive(Debug, Eq)
///|
pub fn ErrorCode::to_wire(self : ErrorCode) -> String {
match self {
TaskNotFound => "server_not_found"
TaskOffline => "server_offline"
InternalError => "internal_error"
ProtoVersionTooOld(_) => "proto_version_too_old"
}
}
///|
pub(all) enum RegisterRejectReason {
UuidAlreadyClaimedThisSession
InvalidUuid
/// Relay refused the Register because the server's `proto_version`
/// is below `proto_min_version`. `min_version` tells the server
/// operator which version to upgrade to before retrying.
ProtoVersionTooOld(min_version~ : Int)
} derive(Eq, Debug)
///|
pub fn RegisterRejectReason::to_wire(self : RegisterRejectReason) -> String {
match self {
UuidAlreadyClaimedThisSession => "uuid_already_claimed_this_session"
InvalidUuid => "invalid_uuid"
ProtoVersionTooOld(_) => "proto_version_too_old"
}
}
///|
pub(all) enum SysMessage {
Register(
task_uuid~ : @uuid.UUID,
hostname~ : String,
cwd~ : String,
proto_version~ : Int,
server_capabilities~ : CapabilitySet
)
Registered(task_uuid~ : @uuid.UUID)
RegisterRejected(reason~ : RegisterRejectReason)
TaskExited(exit_code~ : Int)
/// Server -> relay: the task's current terminal title (OSC 0/2), sent on
/// registration and whenever it changes. `None` means the child has no
/// title set. The relay persists it for task listings; it is not
/// forwarded to attached clients, which receive the title inside render
/// frames instead.
Title(title~ : String?)
Connect(
task_uuid~ : @uuid.UUID,
last_seen_id~ : Int64?,
proto_version~ : Int,
client_capabilities~ : CapabilitySet
)
Connected(server_proto_version~ : Int, server_capabilities~ : CapabilitySet)
TaskOnline(server_proto_version~ : Int, server_capabilities~ : CapabilitySet)
NewClient(
client_id~ : String,
last_seen_id~ : Int64?,
client_proto_version~ : Int,
client_capabilities~ : CapabilitySet
)
ClientDisconnected(client_id~ : String)
Error(ErrorCode)
AuthError(message~ : String)
}
///|
/// Manual `Debug` impl because `@uuid.UUID` does not implement `Debug` and
/// the orphan rule prevents us from adding it from this package. We render
/// UUID fields as their canonical hex-string form via `Repr::string`, which
/// matches what `derive(Debug)` would produce if `UUID : Debug` were bridged
/// through its `Show` impl.
pub impl @debug.Debug for SysMessage with fn to_repr(self) {
match self {
Register(task_uuid~, hostname~, cwd~, proto_version~, server_capabilities~) => {
let fields = [
(Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
(Some("hostname"), @debug.Repr(hostname)),
(Some("cwd"), @debug.Repr(cwd)),
(Some("proto_version"), @debug.Repr(proto_version)),
]
if server_capabilities.length() != 0 {
fields.push(
(Some("server_capabilities"), @debug.Repr(server_capabilities)),
)
}
@debug.Repr::ctor("Register", fields)
}
Registered(task_uuid~) =>
@debug.Repr::ctor("Registered", [
(Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
])
RegisterRejected(reason~) =>
@debug.Repr::ctor("RegisterRejected", [
(Some("reason"), @debug.Repr(reason)),
])
TaskExited(exit_code~) =>
@debug.Repr::ctor("TaskExited", [
(Some("exit_code"), @debug.Repr(exit_code)),
])
Title(title~) =>
@debug.Repr::ctor("Title", [(Some("title"), @debug.Repr(title))])
Connect(task_uuid~, last_seen_id~, proto_version~, client_capabilities~) => {
let fields = [
(Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
(Some("last_seen_id"), @debug.Repr(last_seen_id)),
(Some("proto_version"), @debug.Repr(proto_version)),
]
if client_capabilities.length() != 0 {
fields.push(
(Some("client_capabilities"), @debug.Repr(client_capabilities)),
)
}
@debug.Repr::ctor("Connect", fields)
}
Connected(server_proto_version~, server_capabilities~) => {
let fields = [
(Some("server_proto_version"), @debug.Repr(server_proto_version)),
]
if server_capabilities.length() != 0 {
fields.push(
(Some("server_capabilities"), @debug.Repr(server_capabilities)),
)
}
@debug.Repr::ctor("Connected", fields)
}
TaskOnline(server_proto_version~, server_capabilities~) => {
let fields = [
(Some("server_proto_version"), @debug.Repr(server_proto_version)),
]
if server_capabilities.length() != 0 {
fields.push(
(Some("server_capabilities"), @debug.Repr(server_capabilities)),
)
}
@debug.Repr::ctor("TaskOnline", fields)
}
NewClient(
client_id~,
last_seen_id~,
client_proto_version~,
client_capabilities~
) => {
let fields = [
(Some("client_id"), @debug.Repr(client_id)),
(Some("last_seen_id"), @debug.Repr(last_seen_id)),
]
if client_proto_version != 0 {
fields.push(
(Some("client_proto_version"), @debug.Repr(client_proto_version)),
)
}
if client_capabilities.length() != 0 {
fields.push(
(Some("client_capabilities"), @debug.Repr(client_capabilities)),
)
}
@debug.Repr::ctor("NewClient", fields)
}
ClientDisconnected(client_id~) =>
@debug.Repr::ctor("ClientDisconnected", [
(Some("client_id"), @debug.Repr(client_id)),
])
Error(code) => @debug.Repr::ctor("Error", [(None, @debug.Repr(code))])
AuthError(message~) =>
@debug.Repr::ctor("AuthError", [(Some("message"), @debug.Repr(message))])
}
}
///|
pub(all) enum TermMessage {
// Optional client -> server replay cursor. Relay treats this as opaque
// user data and only injects `client_id`; server wrappers that
// understand it can update the per-client replay cursor without relay
// owning terminal semantics.
ClientReplayCursor(client_id~ : String, last_seen_id~ : Int64?)
// Client -> server visible viewport metadata. `active = false`
// keeps the client attached but removes it from resize negotiation.
Viewport(client_id~ : String, cols~ : Int, rows~ : Int, active~ : Bool)
// Server -> client full VT framebuffer. `data` is base64-encoded VT
// bytes that represent the complete visible viewport at `cols` x `rows`.
RenderFrame(
client_id~ : String,
seq~ : Int64,
cols~ : Int,
rows~ : Int,
scroll_offset~ : Int64,
scrollback_rows~ : Int64,
// Optional for wire compatibility with render-frame peers built before
// title propagation existed. New servers send `Some("")` to clear title.
title~ : String?,
// Whether the child application currently consumes mouse input. Optional
// for wire compatibility with older peers; render clients treat `None`
// as `true` so a stale server keeps its current mouse routing.
mouse~ : Bool?,
data~ : String
)
// Server -> client structured render state for `render_frame_state`
// clients: dirty-line styled runs instead of a VT byte stream. Frame-level
// metadata matches `RenderFrame`; `frame` carries the run payload.
RenderState(
client_id~ : String,
seq~ : Int64,
cols~ : Int,
rows~ : Int,
scroll_offset~ : Int64,
scrollback_rows~ : Int64,
title~ : String?,
mouse~ : Bool?,
frame~ : Frame
)
// Render-frame clients send semantic terminal input instead of
// pre-encoded VT bytes. The server owns keyboard, paste, mouse, and
// scroll encoding because it owns the authoritative terminal modes.
InputText(client_id~ : String, text~ : String)
InputKey(
client_id~ : String,
key~ : String,
mods~ : Array[String],
// Printable text produced by the browser keyboard layout. Render-frame
// servers use this to preserve shifted/locale-specific printable chords.
text~ : String?
)
InputPaste(client_id~ : String, text~ : String)
// `button` is absent when no button participates (plain motion, or a
// release whose press was never tracked).
InputMouse(
client_id~ : String,
x~ : Int,
y~ : Int,
button~ : String?,
action~ : String,
mods~ : Array[String]
)
InputScroll(
client_id~ : String,
x~ : Int,
y~ : Int,
delta_rows~ : Int,
mods~ : Array[String]
)
// Client -> server request to reset the server-owned render scrollback view
// to live output. This is intentionally separate from InputScroll so UI
// jump actions cannot be forwarded to the child as mouse-wheel input.
ScrollToLive(client_id~ : String)
// Client -> server report of the client's default colors (`#rrggbb`). The
// server installs them as the terminal's default foreground/background so
// child applications probing OSC 10/11 see the colors the user is looking
// at instead of the agent's host terminal. Theme follows the active
// viewport: a report installs immediately only from a client whose
// viewport is active, and a viewport activation installs that client's
// stored theme.
Theme(client_id~ : String, background~ : String, foreground~ : String)
// `id` is monotonic per task, dense from 1, assigned by the wrapper. A
// client records the highest id it has written to its log; on reconnect
// it sends that id as Connect.last_seen_id and the wrapper replays the
// tail as ordinary Data frames — there is no separate scrollback variant.
// `created` is ms since epoch, stamped by the wrapper on PTY output.
// `None` means the sender did not stamp this frame (older peer, or a
// client→server keystroke that is not the stamped direction).
// `replay` explicitly marks frames emitted from the retained replay log.
// `None` means legacy peer; clients should fall back to `created` age.
Data(
client_id~ : String,
id~ : Int64,
created~ : Int64?,
replay~ : Bool?,
data~ : String
)
// `id` is the next id after the last Data emitted, so a client can tell
// whether its log is caught up at stream end.
Exited(client_id~ : String, id~ : Int64)
} derive(Debug)
///|
pub(all) enum Message {
Sys(SysMessage)
Fs(FsMessage)
Ide(IdeMessage)
Term(TermMessage)
Codex(client_id~ : String, data~ : String)
} derive(Debug)
///|
/// Fixed error classifications for the Moon IDE hover channel.
pub(all) enum IdeErrorCode {
InvalidRequest
MoonUnavailable
MoonIdeFailed
IdeTimeout
} derive(Debug, Eq)
///|
pub fn IdeErrorCode::to_wire(self : IdeErrorCode) -> String {
match self {
InvalidRequest => "invalid_request"
MoonUnavailable => "moon_unavailable"
MoonIdeFailed => "moon_ide_failed"
IdeTimeout => "timeout"
}
}
///|
pub fn IdeErrorCode::from_wire(value : String) -> IdeErrorCode? {
match value {
"invalid_request" => Some(InvalidRequest)
"moon_unavailable" => Some(MoonUnavailable)
"moon_ide_failed" => Some(MoonIdeFailed)
"timeout" => Some(IdeTimeout)
_ => None
}
}
///|
/// Moon IDE hover messages. Positions and compact ranges use one-based
/// Unicode code-point coordinates, matching `moon ide hover`.
pub(all) enum IdeMessage {
Hover(
req_id~ : String,
client_id~ : String,
path~ : String,
line~ : Int,
column~ : Int
)
CancelHover(req_id~ : String, client_id~ : String)
HoverResult(
req_id~ : String,
client_id~ : String,
path~ : String,
range~ : String?,
contents~ : Array[String]
)
IdeError(
req_id~ : String,
client_id~ : String,
path~ : String,
code~ : IdeErrorCode,
message~ : String
)
} derive(Debug)
///|
pub(all) struct FileStat {
file_type : Int
size : Int64
mtime : Int64
ctime : Int64
} derive(Debug)
///|
pub(all) enum FsMessage {
/// Start watching one editor file. The relay injects `client_id`; `req_id`
/// identifies this watch until replacement, explicit unwatch, or disconnect.
WatchFile(req_id~ : String, client_id~ : String, path~ : String)
/// Cancel the matching watch owned by `client_id`.
UnwatchFile(req_id~ : String, client_id~ : String)
/// Watcher construction completed. `path` is the requested identity and
/// `resolved_path` is the file the server actually watches and reads.
WatchFileReady(
req_id~ : String,
client_id~ : String,
path~ : String,
resolved_path~ : String
)
/// At least one filesystem event affected `resolved_path`.
FileChanged(
req_id~ : String,
client_id~ : String,
path~ : String,
resolved_path~ : String
)
Stat(req_id~ : String, client_id~ : String, path~ : String)
StatResult(req_id~ : String, client_id~ : String, stat~ : FileStat)
Readdir(req_id~ : String, client_id~ : String, path~ : String)
ReaddirResult(
req_id~ : String,
client_id~ : String,
entries~ : Array[(String, Int)]
)
ReadFile(req_id~ : String, client_id~ : String, path~ : String)
ReadFileResult(req_id~ : String, client_id~ : String, data~ : String)
WriteFile(
req_id~ : String,
client_id~ : String,
path~ : String,
data~ : String,
create~ : Bool,
overwrite~ : Bool
)
Mkdir(req_id~ : String, client_id~ : String, path~ : String)
FsDelete(
req_id~ : String,
client_id~ : String,
path~ : String,
recursive~ : Bool
)
FsRename(
req_id~ : String,
client_id~ : String,
from~ : String,
to~ : String,
overwrite~ : Bool
)
FsOk(req_id~ : String, client_id~ : String)
FsError(
req_id~ : String,
client_id~ : String,
code~ : String,
message~ : String
)
} derive(Debug)