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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! The model's read surface — everything the shell may ask an [`AppModel`],
//! and nothing that changes one.
//!
//! Split from `app/mod.rs` at the cap. The parent boots the model and takes
//! the worker's published snapshots; this is what a frame *reads* off one.
//! Every query here is over [`AppModel::snap`] — a completed derivation the
//! frame cannot change and never waits for (§7.2).
use super::AppModel;
#[cfg(test)]
use super::Focus;
#[cfg(test)]
use crate::binding::Workspace;
use crate::binding::WorkspaceKind;
use crate::git_tree::GitTree;
use crate::nav;
use std::path::Path;
impl AppModel {
/// The published-snapshot cell this model reads (§7.2) — what the gesture
/// consumer answers queries from (§8.5), shared by `Arc`.
pub fn snapshot_cell(&self) -> crate::state::SnapshotCell {
std::sync::Arc::clone(&self.cell)
}
/// The durable `ui.json` path (§4.1) — the consumer's write-through copy
/// opens the same file this instance adopts external changes from.
pub fn ui_json_path(&self) -> std::path::PathBuf {
self.roots.ui_json()
}
/// Wall-clock unix seconds off this model's **injected** clock (§7.2) —
/// what a derivation dates its ages against. The shell mints its own at the
/// process boundary for the seats that take one as a parameter; this is the
/// same fact for the view-models that do not, and it is the injected seam,
/// so a test advances it like anything else.
pub(crate) fn now_unix(&self) -> i64 {
self.clock.stamp().parse().unwrap_or(0)
}
/// The §11 ops-surface staleness line, or `None` while the rendered
/// snapshot is current (§7.2). Honest by construction: it is the age of the
/// derivation on screen, not a claim about how fresh it ought to be.
pub fn staleness(&self) -> Option<String> {
super::drift::stale_label(
self.clock
.now()
.saturating_duration_since(self.snap.derived_at),
self.snap.cadence.stale_after(),
)
}
/// The clock's live periods off the rendered snapshot (bl-3381): what the
/// frame derives its own rhythms from — the I4 poll floor
/// (`cadence().cheap_sweep`) and the wound-banner grace
/// ([`Cadence::wound_grace`](super::Cadence::wound_grace)) — so an operator
/// who re-tunes the sweeps re-tunes everything spelled in terms of them,
/// and no frame reads disk.
pub fn cadence(&self) -> super::Cadence {
self.snap.cadence
}
/// The classified workspace set — a test-only reader of the roster input
/// the shell derives internally (§11).
#[cfg(test)]
pub(crate) fn workspaces(&self) -> &[Workspace] {
&self.snap.workspaces
}
/// The frame→worker dirty hand-off (§7.2) — a test-only reader, so a test
/// can mark a root with a specific [`Mark`](crate::watch::Mark) rather than
/// the `Watch` every production caller means.
#[cfg(test)]
pub(crate) fn dirty_handle(&self) -> crate::state::DirtySet {
self.dirty.clone()
}
/// What grew since the last derivation (§7.2), as the §11 ops accessory
/// says it — `None` when nothing did. A dispatch storm is a fact about a
/// *conversation*, and before bl-ee0a yog had no way to say it: 227 branches
/// under one root read as yog being slow, so the operator's one signal
/// pointed at the wrong layer.
pub fn growth_note(&self) -> Option<String> {
super::snapshot::growth_label(&self.snap.growth)
}
/// The current snapshot for `ws`, if derived.
pub fn tree(&self, ws: &Path) -> Option<&GitTree> {
self.snap.trees.get(ws)
}
/// The focused workspace path (center-panel target), if any.
pub fn focused_workspace(&self) -> Option<&Path> {
self.focus.ws.as_deref()
}
/// The focused workspace's snapshot (the center panel renders this tree).
pub fn focused_tree(&self) -> Option<&GitTree> {
self.focus
.ws
.as_deref()
.and_then(|w| self.snap.trees.get(w))
}
/// Whether the focused workspace is a read-only replay (§3.1
/// `<lernie-data>/replays/*`). "Replay is not a mode": the ordinary center
/// view renders it through the same tree renderer — this query only gates
/// the mutating composer off, so a replay offers no write surface.
pub fn focused_is_replay(&self) -> bool {
let Some(ws) = self.focus.ws.as_deref() else {
return false;
};
self.snap
.workspaces
.iter()
.any(|w| w.path == ws && w.kind == WorkspaceKind::Replay)
}
/// The per-instance focus (RAM, §13.1) — a test-only reader.
#[cfg(test)]
pub(crate) fn focus(&self) -> &Focus {
&self.focus
}
/// The focused agent's snapshot row — the inspector's per-agent target
/// (§11 Altitude-2): the [`Agent`](crate::git_tree::Agent) in the focused
/// workspace's tree whose id is the focused agent id. `None` when no agent
/// is selected or its row is absent (an unfetched or moved tree).
pub fn focused_agent(&self) -> Option<&crate::git_tree::Agent> {
let agent_id = self.focus.agent.as_deref()?;
let tree = self.focused_tree()?;
tree.agents.iter().find(|a| a.agent_id == agent_id)
}
}
/// The derived projections a frame renders — the roster rollups, the tab
/// bar, the conversation rows and the activity chip. Split out of
/// `app/focus.rs` at the cap: those are *reads*, and focus.rs is the focus
/// and seen-acknowledgement **state machine** (§6). Same surface, one home.
impl AppModel {
/// Per-workspace roster facts (§6 rollup): attention-bearing agent count,
/// total agent count, and whether any agent is running (Live/InFlight). An
/// unfetched workspace contributes zeros.
pub fn workspace_stats(&self, ws: &Path) -> (usize, usize, bool) {
crate::boundary::answer::workspace_stats(&self.snap, &self.ui, ws)
}
/// The §6 attention-strip total: attention-bearing agents across every
/// workspace.
pub fn strip_total(&self) -> usize {
self.snap
.workspaces
.iter()
.map(|w| self.workspace_stats(&w.path).0)
.sum()
}
/// The §6 decision queue over this instance's snapshot (VISION §5 V5.2) —
/// the frame's delegation to the same derivation the §8.5 `Query::Attention`
/// answers, so the window's desktop escalation (bl-e160) and a headless
/// reader see one list of what needs the operator.
pub fn decision_queue(&self, now_unix: i64) -> Vec<crate::boundary::answer::queue::QueueRow> {
crate::boundary::answer::queue::queue(&self.snap, &self.ui, now_unix)
}
/// The §11 workspace tab bar (pinned hoists + named tabs; foreign/replay in
/// the overflow), built from the classification + attention rollups + the
/// `ui.json` pin order.
pub fn tab_bar(&self) -> nav::tabs::TabBar {
let items: Vec<nav::tabs::Item> = self
.snap
.workspaces
.iter()
.map(|w| nav::tabs::Item {
ws: w.clone(),
attention: self.workspace_stats(&w.path).0,
})
.collect();
nav::tabs::build(&items, &self.ui.pinned(), self.focus.ws.as_deref())
}
/// The §11 conversation list for the focused workspace with nothing
/// unfolded: one row per root agent, subtree-aggregated, sorted by last
/// action of any kind, newest first (§11 as amended by bl-cad5).
/// `now_unix` is the caller's wall clock (the shell boundary mints it, so
/// the derivation stays clock-free here).
pub fn conversations(&self, now_unix: i64) -> Vec<nav::convs::ConvRow> {
self.visible_conversations(now_unix, &std::collections::HashSet::new())
}
/// The same list as the frame paints it (§11, bl-fa82): the **visible** rows
/// of the focused workspace's descent forest, given the viewport's
/// `expanded` set of agent ids (§5.3 — the shell owns that set, the model
/// only reads it). [`conversations`](Self::conversations) is this call with
/// nothing expanded.
pub fn visible_conversations(
&self,
now_unix: i64,
expanded: &std::collections::HashSet<String>,
) -> Vec<nav::convs::ConvRow> {
let Some(ws) = self.focus.ws.as_deref() else {
return Vec::new();
};
crate::boundary::answer::visible_conversations(&self.snap, &self.ui, ws, now_unix, expanded)
}
/// The frame-side query chokepoint (§8.5): the same [`answer`]
/// (crate::boundary::answer::answer) the deposit consumer runs, over
/// `deps` — one derivation, two serializations (VISION §4.8). `deps` is
/// the same [`boundary_deps`](Self::boundary_deps) every dispatch takes:
/// the §9 config family's three reads (bl-0164) ask the world through it
/// exactly as their writes do.
///
/// **Search is the one query a frame hands over instead of running** (§8.5):
/// it walks the world's bytes, and a frame that walked them would freeze.
/// The ask goes to this instance's [`Searcher`](crate::search::Searcher) and
/// the reply is whatever has landed — the same contract the frame already
/// has with the derivation worker, and the same engine at the far end, so
/// the seat is asynchronous without the derivation being duplicated.
pub fn answer(
&self,
deps: &crate::boundary::dispatch::Deps,
query: &crate::boundary::Query,
now_unix: i64,
) -> Result<crate::boundary::reply::Reply, String> {
if let crate::boundary::Query::Search { text } = query {
self.search(text);
return Ok(crate::boundary::reply::Reply::Search(self.found()));
}
crate::boundary::answer::answer(query, deps, &self.ui, now_unix)
}
/// The §11 **bottom in-flight strip** for the open conversation (bl-905f):
/// the third seat of the one §5.1 #28 derivation, carrying the live
/// characteristics of what is running. `None` — no strip painted, no
/// repaint scheduled (§7.2) — whenever nothing is selected or nothing in
/// the selection's conversation is in flight.
/// `now_unix` is the caller's wall clock (the shell boundary mints it, as it
/// does for the list's ages), so the elapsed segment ticks per frame off the
/// snapshot's structural start with nothing stored (§5.1 #28).
pub fn flight_strip(&self, now_unix: i64) -> Option<nav::convs::FlightStrip> {
let agent = self.focus.agent.as_deref()?;
let tree = self.focused_tree()?;
let root = nav::convs::root_of(&tree.agents, agent)?;
nav::convs::strip(&tree.agents, &root, now_unix)
}
/// The §11 **live mark's** seats for the open conversation (§5.1 #28b): the
/// eye — the agent the operator is talking to — then its subagents in §2.3
/// descent order, each with what it is doing right now.
///
/// Empty whenever nothing is selected, which is not a case the mark
/// branches on: no seats is every circle at rest, which is the logo.
pub fn mark_seats(&self) -> Vec<nav::convs::Seat> {
let Some(agent) = self.focus.agent.as_deref() else {
return Vec::new();
};
let Some(tree) = self.focused_tree() else {
return Vec::new();
};
let Some(root) = nav::convs::root_of(&tree.agents, agent) else {
return Vec::new();
};
nav::convs::seats(&tree.agents, &root)
}
/// The §11 activity-accessory summary over the cached ops tail — the
/// collapsed chip's counts, its ⚠ being the **live** failures only (§6's
/// retirement rule); the expansion renders [`ops_rows`](Self::ops_rows).
pub fn activity(&self) -> crate::opslog::Activity {
crate::opslog::activity(&self.snap.ops)
}
/// Whether a left-panel section carries a persisted collapse override
/// (§4.1 `collapsed` — the balls section's key).
pub fn is_collapsed(&self, key: &str) -> bool {
self.ui.is_collapsed(key)
}
}