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
//! **The live dock**: what is running under the aimed workspace, folded to one
//! line above the rule and unfolded with Ctrl+T (DESIGN §4.42, item 9).
//!
//! The baseline keeps a dock of its subagents and background processes over
//! its status line, one line while folded. This seat's running things are
//! the aimed workspace's conversations whose drivers are at work — live or in
//! flight, by the server's own word (`AgentState`) — which the roster's rows
//! already carry; so the dock is a reading of those rows and asks nothing of
//! its own. **The fleet's runs join it when their read stands outside the
//! fleet pane** (§4.33); until then they are the pane's.
//!
//! Folded, it is one line: `+ N running · <the first's name>`. Unfolded, one
//! row per running conversation — its name, its state and its age — and a
//! click on a row selects it, which is the one act a dock affords. Whether it
//! is unfolded is the toolkit's memory, as a fold's is (`crate::ui::chat`):
//! a view state no wire gesture corresponds to and no other fact can be
//! asked for; Ctrl+T is the chord (`crate::ui::keys`) and a click on the
//! line — a door, lit under the pointer (DESIGN §4.45 item 6) — is the same
//! toggle.
use crate::reply::convs::{AgentState, ConvRow};
use crate::settings::Toggle;
use crate::ui::{Model, theme};
/// **The conversations running under the aimed workspace**, in the list's order.
pub fn running(model: &Model) -> Vec<ConvRow> {
model
.rows()
.into_iter()
.filter(|row| matches!(row.state, AgentState::Live | AgentState::InFlight))
.collect()
}
/// The folded line's words.
pub fn line(rows: &[ConvRow], unfolded: bool) -> String {
let count = rows.len();
let first = rows
.first()
.map(|row| row.display.as_str())
.unwrap_or_default();
format!(
"{} {count} running · {first} · Ctrl+T",
if unfolded {
theme::glyph::OPEN
} else {
theme::glyph::SHUT
}
)
}
/// One running conversation's row: its name, its state, its age.
pub fn row_words(row: &ConvRow) -> String {
format!(
"{} {} {}",
row.display,
row.state.label(),
crate::ui::convs::age(row.age_secs)
)
}
/// Where the fold is kept.
const UNFOLDED_ID: &str = "the dock's fold";
/// What the dock's line does, each way, and what a row does.
pub const UNFOLD_HELP: &str = "unfolds the dock: one row per conversation that is running";
pub const FOLD_HELP: &str = "folds the dock to its one line";
pub const ROW_HELP: &str = "shows this running conversation";
/// **Whether the dock is unfolded** — as it was last toggled, or as the
/// `dock` knob says until it has been (DESIGN §4.46 item 4).
pub fn unfolded(ctx: &egui::Context, at_first: bool) -> bool {
ctx.data(|d| d.get_temp::<bool>(egui::Id::new(UNFOLDED_ID)))
.unwrap_or(at_first)
}
/// **Unfold the dock, or fold it**: the one toggle Ctrl+T is.
pub fn toggle(ctx: &egui::Context, at_first: bool) {
let now = unfolded(ctx, at_first);
ctx.data_mut(|d| d.insert_temp(egui::Id::new(UNFOLDED_ID), !now));
}
/// Paint the dock for `rows`, which the shell has found non-empty.
pub fn render(ui: &mut egui::Ui, model: &mut Model, rows: &[ConvRow]) {
let open = unfolded(ui.ctx(), model.settings.on(Toggle::Dock));
// **The line is a door** (DESIGN §4.45 item 6): a label that sensed a
// click lit nothing under the pointer, so the one toggle a click on it is
// was a toggle the glass never offered. The same small run in the working
// accent, through the one painter that wears the hover wash.
let help = if open { FOLD_HELP } else { UNFOLD_HELP };
let help = crate::ui::keys::cited(help, &[crate::ui::keys::Act::Dock]);
let head = theme::paint::link(
ui,
&line(rows, open),
theme::accent(theme::State::Working),
&help,
);
if head.clicked() {
toggle(ui.ctx(), model.settings.on(Toggle::Dock));
}
if !open {
return;
}
let mut chosen = None;
for row in rows {
let seat = theme::paint::row(
ui,
&row_words(row),
theme::INK,
Some(theme::state_ink(&row.state)),
model.conversation.as_deref() == Some(row.root_id.as_str()),
theme::space::S,
ROW_HELP,
);
// **The two reads this gesture reaches**, as a row in the switcher
// (`crate::ui::convs::row`): selecting is what makes this seat read
// the transcript and open the follow lane.
crate::ui::act::tag(
&seat,
&[crate::verbs::TRANSCRIPT.word, crate::verbs::FOLLOW.word],
);
if seat.clicked() {
chosen = Some(row.root_id.clone());
}
}
if let Some(id) = chosen {
model.select(&id);
}
}
#[cfg(test)]
mod tests;