1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! **The roster** — the window's altitude-0 chrome (yog's `docs/REMOTE.md`
//! §8, §9.7): the workspaces an engine enumerates, each with its rollups, and
//! how current the derivation behind the answer is.
//!
//! **A row names a workspace; it does not locate it** (REMOTE §8: *"paths
//! never cross the wire"*). The name is the workspace's directory leaf on its
//! host and it is the token every gesture addresses it by — so a row is both
//! the label the roster paints and the address the next act carries, with no
//! table in between.
//!
//! **What this build does not carry.** The row also spells the config-lineage
//! tip a model picker advances. There is no picker here, and rung 4 of
//! [`super`]'s policy means the field rides through unread rather than
//! refusing — a kind nothing renders is a kind nobody has to carry, and the
//! field arrives with the pane that paints it.
use serde_json::{Map, Value};
use super::fields;
/// This reply's kind token.
pub(crate) const KIND: &str = "workspaces";
/// The whole answer: the enumeration, and the two notes about its currency.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Workspaces {
/// Every enumerated workspace with its rollups.
pub rows: Vec<WsRow>,
/// **How stale the derivation this answer came from is**, or `None` while
/// it is current — which is the ordinary case and paints nothing.
///
/// It crosses as the **rendered line** rather than as an age and a
/// threshold, and that is the engine's ruling rather than this seat's: the
/// wording is one derivation's, with a bound the operator tunes in the
/// engine's own config, and a seat that rebuilt the sentence from parts
/// would be a second place deciding when a derivation is late.
pub stale: Option<String>,
/// **What grew since the previous derivation**, or `None` when nothing
/// did. Also a rendered line, for the same reason.
pub growth: Option<String>,
}
/// One workspace as the roster paints it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WsRow {
/// Its name — the leaf on its host, and the token a gesture addresses it
/// by. **Not rewritten here**: the client-side rename is spent at the
/// channel boundary and nowhere else ([`crate::seat::route`]), so a row
/// arrives in the host's spelling and the roster is what maps it back.
pub workspace: String,
/// How the engine classifies it.
pub kind: WorkspaceKind,
/// Attention-bearing conversations in it.
pub attention: u64,
/// Root-and-member conversation count.
pub agents: u64,
/// Whether anything in it is running right now.
pub running: bool,
/// **Where the operator pinned it** — its rank in the durable pin list,
/// `None` for a workspace that is not pinned.
///
/// A rank rather than a flag, because the roster hoists pinned rows *in
/// pin order*: a seat given only a boolean would have to read the pin list
/// back to sort them, which is the seat joining an answer against a
/// document only the engine holds.
pub pinned: Option<u64>,
/// **What the workspace has cost, whole** — the ledger column (REMOTE
/// §9.23), `None` where the engine has no price table.
pub spend: Option<super::spend::Cost>,
}
/// How the engine classifies a workspace.
///
/// **Rung 3 lives on this enum.** A classification this build does not know
/// keeps its word and paints it; it does not become one of the three below,
/// because a replay painted as a wall is a lie about what may be written to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceKind {
/// The server's own — a wall an operator named.
Named,
/// The engine's, enumerated rather than named here.
Foreign,
/// A read-only replay.
Replay,
/// A word this build does not know, verbatim.
Unknown(String),
}
impl WorkspaceKind {
/// The word the roster paints — for [`Unknown`](Self::Unknown), the
/// engine's own, which is the whole point of keeping it.
pub fn label(&self) -> String {
match self {
Self::Named => NAMED.to_owned(),
Self::Foreign => FOREIGN.to_owned(),
Self::Replay => REPLAY.to_owned(),
Self::Unknown(word) => word.clone(),
}
}
}
const NAMED: &str = "named";
const FOREIGN: &str = "foreign";
const REPLAY: &str = "replay";
/// Read the whole answer. The two notes are **absent** rather than null in the
/// ordinary case, which is what makes "current" and "the engine declined to
/// say" one reading rather than two.
pub(crate) fn workspaces(obj: &Map<String, Value>) -> Result<Workspaces, String> {
Ok(Workspaces {
rows: fields::rows(obj, row)?,
stale: fields::opt_text(obj, "stale")?,
growth: fields::opt_text(obj, "growth")?,
})
}
/// Read one row. `pinned` is absent — never null and never a rank of its own —
/// for an unpinned workspace, so a reader never has to tell "rank 0" from "not
/// pinned", and rank 0 is the first hoisted row.
fn row(v: &Value) -> Result<WsRow, String> {
let o = v.as_object().ok_or("workspace row: not an object")?;
Ok(WsRow {
workspace: fields::text(o, "workspace")?,
kind: kind(&fields::text(o, "kind")?),
attention: fields::count(o, "attention")?,
agents: fields::count(o, "agents")?,
running: fields::flag(o, "running")?,
pinned: match o.get("pinned") {
None | Some(Value::Null) => None,
Some(_) => Some(fields::count(o, "pinned")?),
},
spend: super::spend::cost(o, "spend")?,
})
}
/// One classification token, total by construction (rung 3).
fn kind(word: &str) -> WorkspaceKind {
match word {
NAMED => WorkspaceKind::Named,
FOREIGN => WorkspaceKind::Foreign,
REPLAY => WorkspaceKind::Replay,
other => WorkspaceKind::Unknown(other.to_owned()),
}
}
#[cfg(test)]
mod tests;