@team-agent/installer 0.5.50 → 0.5.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +46 -115
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +284 -21
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
18
|
+
pub(crate) struct LayoutPlacement {
|
|
19
|
+
pub agent_id: AgentId,
|
|
20
|
+
pub layout_window: WindowName,
|
|
21
|
+
pub layout_index: usize,
|
|
22
|
+
pub pane_index: usize,
|
|
23
|
+
pub starts_window: bool,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(crate) fn adaptive_layout_plan(
|
|
27
|
+
agent_ids: &[AgentId],
|
|
28
|
+
max_per_window: usize,
|
|
29
|
+
) -> Vec<LayoutPlacement> {
|
|
30
|
+
let max_per_window = max_per_window.max(1);
|
|
31
|
+
agent_ids
|
|
32
|
+
.iter()
|
|
33
|
+
.enumerate()
|
|
34
|
+
.map(|(idx, agent_id)| {
|
|
35
|
+
let layout_index = idx / max_per_window;
|
|
36
|
+
let pane_index = idx % max_per_window;
|
|
37
|
+
LayoutPlacement {
|
|
38
|
+
agent_id: agent_id.clone(),
|
|
39
|
+
layout_window: WindowName::new(format!("team-w{}", layout_index + 1)),
|
|
40
|
+
layout_index,
|
|
41
|
+
pane_index,
|
|
42
|
+
starts_window: pane_index == 0,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.collect()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub(crate) const ADAPTIVE_LAYOUT_MAX_PER_WINDOW: usize = 3;
|
|
49
|
+
|
|
50
|
+
pub(crate) fn state_uses_adaptive_layout(state: &serde_json::Value) -> bool {
|
|
51
|
+
state
|
|
52
|
+
.get("display_backend")
|
|
53
|
+
.and_then(serde_json::Value::as_str)
|
|
54
|
+
.is_some_and(|backend| backend == "adaptive")
|
|
55
|
+
|| state
|
|
56
|
+
.get("runtime")
|
|
57
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
58
|
+
.and_then(serde_json::Value::as_str)
|
|
59
|
+
.is_some_and(|backend| backend == "adaptive")
|
|
60
|
+
|| state
|
|
61
|
+
.get("agents")
|
|
62
|
+
.and_then(serde_json::Value::as_object)
|
|
63
|
+
.is_some_and(|agents| {
|
|
64
|
+
agents.values().any(|agent| {
|
|
65
|
+
agent
|
|
66
|
+
.get("layout_window")
|
|
67
|
+
.and_then(serde_json::Value::as_str)
|
|
68
|
+
.is_some_and(|window| !window.is_empty())
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub(crate) fn adaptive_placement_for_agent(
|
|
74
|
+
state: &serde_json::Value,
|
|
75
|
+
transport: &dyn Transport,
|
|
76
|
+
session_name: &SessionName,
|
|
77
|
+
agent_id: &AgentId,
|
|
78
|
+
) -> Option<LayoutPlacement> {
|
|
79
|
+
if !state_uses_adaptive_layout(state) {
|
|
80
|
+
return None;
|
|
81
|
+
}
|
|
82
|
+
// E43 Fix A (0.3.24 bug#3, demo-director blocker): cross-check live panes
|
|
83
|
+
// by both pane_id AND window_name. Real-machine state can carry a stale
|
|
84
|
+
// `layout_window` residue (e.g. "team-w2") while the live pane for that
|
|
85
|
+
// agent actually lives under a DIFFERENT window_name (e.g. "architect").
|
|
86
|
+
// The prior pane_id-only check let the stale claim slip through, and the
|
|
87
|
+
// resulting placement asked spawn_agent_window to split a phantom window.
|
|
88
|
+
let live_targets = transport.list_targets().unwrap_or_default();
|
|
89
|
+
let live_panes: BTreeSet<String> = live_targets
|
|
90
|
+
.iter()
|
|
91
|
+
.map(|pane| pane.pane_id.as_str().to_string())
|
|
92
|
+
.collect();
|
|
93
|
+
// Map pane_id → window_name for the window-name cross-check.
|
|
94
|
+
let live_pane_window: BTreeMap<String, String> = live_targets
|
|
95
|
+
.iter()
|
|
96
|
+
.filter_map(|pane| {
|
|
97
|
+
pane.window_name
|
|
98
|
+
.as_ref()
|
|
99
|
+
.map(|name| (pane.pane_id.as_str().to_string(), name.as_str().to_string()))
|
|
100
|
+
})
|
|
101
|
+
.collect();
|
|
102
|
+
let live_windows: BTreeSet<String> = transport
|
|
103
|
+
.list_windows(session_name)
|
|
104
|
+
.unwrap_or_default()
|
|
105
|
+
.into_iter()
|
|
106
|
+
.map(|window| window.as_str().to_string())
|
|
107
|
+
.collect();
|
|
108
|
+
let mut windows: BTreeMap<usize, (String, usize)> = BTreeMap::new();
|
|
109
|
+
if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
|
|
110
|
+
for (id, agent) in agents {
|
|
111
|
+
if id == agent_id.as_str() {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
let Some(window) = agent
|
|
115
|
+
.get("layout_window")
|
|
116
|
+
.or_else(|| agent.get("window"))
|
|
117
|
+
.and_then(serde_json::Value::as_str)
|
|
118
|
+
.filter(|window| !window.is_empty())
|
|
119
|
+
else {
|
|
120
|
+
continue;
|
|
121
|
+
};
|
|
122
|
+
// E45 (0.3.24 bug#4): only canonical `team-w<N>[-suffix]` windows
|
|
123
|
+
// count as adaptive layout windows. Per-agent windows (named
|
|
124
|
+
// after agent_id like `developer`) are NOT layout windows even if
|
|
125
|
+
// the state's `layout_window`/`layout_index` carries them — those
|
|
126
|
+
// are residue from a prior launch or explicit per-agent topology.
|
|
127
|
+
// Treating them as adaptive caused add-agent to split into the
|
|
128
|
+
// developer worker's pane on macmini (split-window -t :developer
|
|
129
|
+
// succeeded → 2 panes in developer window, no new window for
|
|
130
|
+
// demo-director).
|
|
131
|
+
if !is_adaptive_layout_window(window) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
let pane_id = agent.get("pane_id").and_then(serde_json::Value::as_str);
|
|
135
|
+
let pane_live = pane_id.is_some_and(|pane| live_panes.contains(pane));
|
|
136
|
+
// E43 Fix A: window_name match — when the agent's pane is live,
|
|
137
|
+
// its live window_name MUST equal the claimed `window`; otherwise
|
|
138
|
+
// the claim is stale residue from a respawn/rename and must not
|
|
139
|
+
// count toward the layout map.
|
|
140
|
+
let pane_window_matches = pane_id
|
|
141
|
+
.and_then(|pane| live_pane_window.get(pane))
|
|
142
|
+
.is_some_and(|name| name == window);
|
|
143
|
+
if pane_live && !pane_window_matches {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if !pane_live && (!live_panes.is_empty() || !live_windows.contains(window)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let layout_index = agent
|
|
150
|
+
.get("layout_index")
|
|
151
|
+
.and_then(serde_json::Value::as_u64)
|
|
152
|
+
.map(|idx| idx as usize)
|
|
153
|
+
.or_else(|| parse_team_layout_index(window))
|
|
154
|
+
.unwrap_or(windows.len());
|
|
155
|
+
let entry = windows
|
|
156
|
+
.entry(layout_index)
|
|
157
|
+
.or_insert_with(|| (window.to_string(), 0));
|
|
158
|
+
entry.1 = entry.1.saturating_add(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if let Some((&layout_index, (window, count))) = windows.iter().next_back() {
|
|
162
|
+
if *count < ADAPTIVE_LAYOUT_MAX_PER_WINDOW {
|
|
163
|
+
return Some(LayoutPlacement {
|
|
164
|
+
agent_id: agent_id.clone(),
|
|
165
|
+
layout_window: WindowName::new(window.clone()),
|
|
166
|
+
layout_index,
|
|
167
|
+
pane_index: *count,
|
|
168
|
+
starts_window: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// E45 (0.3.24 bug#4): when the live session has NO real adaptive layout
|
|
173
|
+
// window (the topology is effectively per-agent, even though state says
|
|
174
|
+
// display_backend=adaptive), DO NOT synthesise a fresh `team-w<N>`
|
|
175
|
+
// window — that would force the new agent into an adaptive-layout pane
|
|
176
|
+
// shape the rest of the session does not actually use. Return None so
|
|
177
|
+
// the caller (`start_agent_at_paths` → `spawn_agent_window`) falls back
|
|
178
|
+
// to its non-placement path, which opens a new window named after the
|
|
179
|
+
// agent_id (canonical per-agent pattern, matches the existing 7 workers).
|
|
180
|
+
if windows.is_empty() {
|
|
181
|
+
return None;
|
|
182
|
+
}
|
|
183
|
+
let next_index = windows.keys().next_back().map(|idx| idx + 1).unwrap_or(0);
|
|
184
|
+
let base = format!("team-w{}", next_index + 1);
|
|
185
|
+
Some(LayoutPlacement {
|
|
186
|
+
agent_id: agent_id.clone(),
|
|
187
|
+
layout_window: unique_layout_window_name(&base, &live_windows),
|
|
188
|
+
layout_index: next_index,
|
|
189
|
+
pane_index: 0,
|
|
190
|
+
starts_window: true,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub(crate) fn adaptive_existing_placement_for_agent(
|
|
195
|
+
state: &serde_json::Value,
|
|
196
|
+
transport: &dyn Transport,
|
|
197
|
+
session_name: &SessionName,
|
|
198
|
+
agent_id: &AgentId,
|
|
199
|
+
) -> Option<LayoutPlacement> {
|
|
200
|
+
if !state_uses_adaptive_layout(state) {
|
|
201
|
+
return None;
|
|
202
|
+
}
|
|
203
|
+
let agent = state.get("agents")?.get(agent_id.as_str())?;
|
|
204
|
+
let window = agent
|
|
205
|
+
.get("layout_window")
|
|
206
|
+
.or_else(|| agent.get("window"))
|
|
207
|
+
.and_then(serde_json::Value::as_str)
|
|
208
|
+
.filter(|window| !window.is_empty())?;
|
|
209
|
+
// E45 (0.3.24 bug#4): existing-placement is meaningless for a per-agent
|
|
210
|
+
// window name (e.g. `developer`). Return None and let the caller fall
|
|
211
|
+
// back to its non-placement spawn path, which opens / reuses a window
|
|
212
|
+
// named after the agent_id. Only canonical `team-w<N>` windows are
|
|
213
|
+
// honored as existing adaptive placements.
|
|
214
|
+
if !is_adaptive_layout_window(window) {
|
|
215
|
+
return None;
|
|
216
|
+
}
|
|
217
|
+
let layout_index = agent
|
|
218
|
+
.get("layout_index")
|
|
219
|
+
.and_then(serde_json::Value::as_u64)
|
|
220
|
+
.map(|idx| idx as usize)
|
|
221
|
+
.or_else(|| parse_team_layout_index(window))
|
|
222
|
+
.unwrap_or(0);
|
|
223
|
+
let desired_pane_index = agent
|
|
224
|
+
.get("pane_index")
|
|
225
|
+
.and_then(serde_json::Value::as_u64)
|
|
226
|
+
.map(|idx| idx as usize)
|
|
227
|
+
.unwrap_or(0);
|
|
228
|
+
let live_windows: BTreeSet<String> = transport
|
|
229
|
+
.list_windows(session_name)
|
|
230
|
+
.unwrap_or_default()
|
|
231
|
+
.into_iter()
|
|
232
|
+
.map(|window| window.as_str().to_string())
|
|
233
|
+
.collect();
|
|
234
|
+
if !live_windows.contains(window) {
|
|
235
|
+
// E43 Fix B (0.3.24 bug#3): the claimed `layout_window` is NOT in
|
|
236
|
+
// live_windows — it's stale residue. The session is effectively
|
|
237
|
+
// per-agent (live windows are named after agent_ids), so fall back
|
|
238
|
+
// to a new window named after the agent_id itself instead of
|
|
239
|
+
// synthesising a fresh phantom-named window the next spawn would
|
|
240
|
+
// try (and fail) to split.
|
|
241
|
+
return Some(LayoutPlacement {
|
|
242
|
+
agent_id: agent_id.clone(),
|
|
243
|
+
layout_window: WindowName::new(agent_id.as_str()),
|
|
244
|
+
layout_index,
|
|
245
|
+
pane_index: 0,
|
|
246
|
+
starts_window: true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
let existing_panes = transport
|
|
250
|
+
.list_targets()
|
|
251
|
+
.unwrap_or_default()
|
|
252
|
+
.into_iter()
|
|
253
|
+
.filter(|pane| {
|
|
254
|
+
pane.window_name
|
|
255
|
+
.as_ref()
|
|
256
|
+
.is_some_and(|name| name.as_str() == window)
|
|
257
|
+
&& agent
|
|
258
|
+
.get("pane_id")
|
|
259
|
+
.and_then(serde_json::Value::as_str)
|
|
260
|
+
.is_none_or(|agent_pane| agent_pane != pane.pane_id.as_str())
|
|
261
|
+
})
|
|
262
|
+
.count();
|
|
263
|
+
Some(LayoutPlacement {
|
|
264
|
+
agent_id: agent_id.clone(),
|
|
265
|
+
layout_window: WindowName::new(window),
|
|
266
|
+
layout_index,
|
|
267
|
+
pane_index: existing_panes,
|
|
268
|
+
starts_window: false,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pub(super) fn parse_team_layout_index(window: &str) -> Option<usize> {
|
|
273
|
+
window
|
|
274
|
+
.strip_prefix("team-w")
|
|
275
|
+
.and_then(|rest| rest.split('-').next())
|
|
276
|
+
.and_then(|raw| raw.parse::<usize>().ok())
|
|
277
|
+
.and_then(|idx| idx.checked_sub(1))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// E45 (0.3.24 bug#4, demo-director second-layer drift): a window name is a
|
|
281
|
+
/// REAL adaptive layout window only when it matches the canonical
|
|
282
|
+
/// `team-w<N>[-suffix]` shape (i.e. `parse_team_layout_index` returns Some).
|
|
283
|
+
/// Per-agent window names like `developer` / `architect` / `demo-director`
|
|
284
|
+
/// are NOT adaptive layout windows even if state happens to carry them in
|
|
285
|
+
/// the agent's `layout_window` or `layout_index` field — they are residue
|
|
286
|
+
/// from a prior launch / explicit per-agent topology. Treating them as
|
|
287
|
+
/// adaptive caused the macmini repro: add-agent demo-director split into
|
|
288
|
+
/// the developer worker's window @453 instead of opening its own.
|
|
289
|
+
pub(super) fn is_adaptive_layout_window(window: &str) -> bool {
|
|
290
|
+
parse_team_layout_index(window).is_some()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Crate-public wrapper for the defensive guard at
|
|
294
|
+
/// `restart/common.rs::spawn_agent_window`. Same semantics as the private
|
|
295
|
+
/// helper above; promoted to `pub(crate)` so the spawn-time defence-in-depth
|
|
296
|
+
/// layer can refuse to split into a per-agent window even if a stale
|
|
297
|
+
/// placement asks for it.
|
|
298
|
+
pub(crate) fn is_adaptive_layout_window_pub(window: &str) -> bool {
|
|
299
|
+
is_adaptive_layout_window(window)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
pub(super) fn unique_layout_window_name(base: &str, live_windows: &BTreeSet<String>) -> WindowName {
|
|
303
|
+
if !live_windows.contains(base) {
|
|
304
|
+
return WindowName::new(base);
|
|
305
|
+
}
|
|
306
|
+
for suffix in 2.. {
|
|
307
|
+
let candidate = format!("{base}-{suffix}");
|
|
308
|
+
if !live_windows.contains(&candidate) {
|
|
309
|
+
return WindowName::new(candidate);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
unreachable!("unbounded suffix search always returns")
|
|
313
|
+
}
|